From 11c5269ccea6c42550440359ae7f216c76228e4b Mon Sep 17 00:00:00 2001 From: Richjerk Date: Thu, 23 May 2024 11:40:07 +0200 Subject: [PATCH] Commit message describing your changes --- venv/Lib/site-packages/bson/__init__.py | 1464 +++++++ .../bson/__pycache__/__init__.cpython-312.pyc | Bin 0 -> 57846 bytes .../bson/__pycache__/_helpers.cpython-312.pyc | Bin 0 -> 1522 bytes .../bson/__pycache__/binary.cpython-312.pyc | Bin 0 -> 9743 bytes .../bson/__pycache__/code.cpython-312.pyc | Bin 0 -> 4007 bytes .../__pycache__/codec_options.cpython-312.pyc | Bin 0 -> 21126 bytes .../__pycache__/datetime_ms.cpython-312.pyc | Bin 0 -> 8849 bytes .../bson/__pycache__/dbref.cpython-312.pyc | Bin 0 -> 6419 bytes .../__pycache__/decimal128.cpython-312.pyc | Bin 0 -> 13040 bytes .../bson/__pycache__/errors.cpython-312.pyc | Bin 0 -> 1438 bytes .../bson/__pycache__/int64.cpython-312.pyc | Bin 0 -> 1220 bytes .../__pycache__/json_util.cpython-312.pyc | Bin 0 -> 46219 bytes .../bson/__pycache__/max_key.cpython-312.pyc | Bin 0 -> 2188 bytes .../bson/__pycache__/min_key.cpython-312.pyc | Bin 0 -> 2188 bytes .../bson/__pycache__/objectid.cpython-312.pyc | Bin 0 -> 11715 bytes .../bson/__pycache__/raw_bson.cpython-312.pyc | Bin 0 -> 8556 bytes .../bson/__pycache__/regex.cpython-312.pyc | Bin 0 -> 5671 bytes .../bson/__pycache__/son.cpython-312.pyc | Bin 0 -> 9474 bytes .../__pycache__/timestamp.cpython-312.pyc | Bin 0 -> 5861 bytes .../bson/__pycache__/typings.cpython-312.pyc | Bin 0 -> 883 bytes .../bson/__pycache__/tz_util.cpython-312.pyc | Bin 0 -> 2220 bytes .../bson/_cbson.cp312-win_amd64.pyd | Bin 0 -> 46080 bytes venv/Lib/site-packages/bson/_cbsonmodule.c | 3164 +++++++++++++++ venv/Lib/site-packages/bson/_cbsonmodule.h | 181 + venv/Lib/site-packages/bson/_helpers.py | 43 + venv/Lib/site-packages/bson/binary.py | 367 ++ venv/Lib/site-packages/bson/bson-endian.h | 233 ++ venv/Lib/site-packages/bson/buffer.c | 157 + venv/Lib/site-packages/bson/buffer.h | 51 + venv/Lib/site-packages/bson/code.py | 100 + venv/Lib/site-packages/bson/codec_options.py | 505 +++ venv/Lib/site-packages/bson/datetime_ms.py | 171 + venv/Lib/site-packages/bson/dbref.py | 133 + venv/Lib/site-packages/bson/decimal128.py | 312 ++ venv/Lib/site-packages/bson/errors.py | 36 + venv/Lib/site-packages/bson/int64.py | 39 + venv/Lib/site-packages/bson/json_util.py | 1161 ++++++ venv/Lib/site-packages/bson/max_key.py | 56 + venv/Lib/site-packages/bson/min_key.py | 56 + venv/Lib/site-packages/bson/objectid.py | 278 ++ venv/Lib/site-packages/bson/py.typed | 2 + venv/Lib/site-packages/bson/raw_bson.py | 196 + venv/Lib/site-packages/bson/regex.py | 133 + venv/Lib/site-packages/bson/son.py | 211 + venv/Lib/site-packages/bson/time64.c | 781 ++++ venv/Lib/site-packages/bson/time64.h | 67 + venv/Lib/site-packages/bson/time64_config.h | 78 + venv/Lib/site-packages/bson/time64_limits.h | 95 + venv/Lib/site-packages/bson/timestamp.py | 123 + venv/Lib/site-packages/bson/typings.py | 31 + venv/Lib/site-packages/bson/tz_util.py | 53 + venv/Lib/site-packages/dns/__init__.py | 70 + .../dns/__pycache__/__init__.cpython-312.pyc | Bin 0 -> 760 bytes .../__pycache__/_asyncbackend.cpython-312.pyc | Bin 0 -> 4828 bytes .../_asyncio_backend.cpython-312.pyc | Bin 0 -> 14165 bytes .../dns/__pycache__/_ddr.cpython-312.pyc | Bin 0 -> 7877 bytes .../dns/__pycache__/_features.cpython-312.pyc | Bin 0 -> 3326 bytes .../_immutable_ctx.cpython-312.pyc | Bin 0 -> 3273 bytes .../__pycache__/_trio_backend.cpython-312.pyc | Bin 0 -> 13216 bytes .../__pycache__/asyncbackend.cpython-312.pyc | Bin 0 -> 3456 bytes .../__pycache__/asyncquery.cpython-312.pyc | Bin 0 -> 31714 bytes .../__pycache__/asyncresolver.cpython-312.pyc | Bin 0 -> 21370 bytes .../dns/__pycache__/dnssec.cpython-312.pyc | Bin 0 -> 50935 bytes .../__pycache__/dnssectypes.cpython-312.pyc | Bin 0 -> 2014 bytes .../dns/__pycache__/e164.cpython-312.pyc | Bin 0 -> 4947 bytes .../dns/__pycache__/edns.cpython-312.pyc | Bin 0 -> 22076 bytes .../dns/__pycache__/entropy.cpython-312.pyc | Bin 0 -> 6009 bytes .../dns/__pycache__/enum.cpython-312.pyc | Bin 0 -> 4922 bytes .../dns/__pycache__/exception.cpython-312.pyc | Bin 0 -> 7266 bytes .../dns/__pycache__/flags.cpython-312.pyc | Bin 0 -> 3116 bytes .../dns/__pycache__/grange.cpython-312.pyc | Bin 0 -> 1816 bytes .../dns/__pycache__/immutable.cpython-312.pyc | Bin 0 -> 3826 bytes .../dns/__pycache__/inet.cpython-312.pyc | Bin 0 -> 6739 bytes .../dns/__pycache__/ipv4.cpython-312.pyc | Bin 0 -> 2766 bytes .../dns/__pycache__/ipv6.cpython-312.pyc | Bin 0 -> 6902 bytes .../dns/__pycache__/message.cpython-312.pyc | Bin 0 -> 83707 bytes .../dns/__pycache__/name.cpython-312.pyc | Bin 0 -> 49268 bytes .../dns/__pycache__/namedict.cpython-312.pyc | Bin 0 -> 4400 bytes .../__pycache__/nameserver.cpython-312.pyc | Bin 0 -> 14177 bytes .../dns/__pycache__/node.cpython-312.pyc | Bin 0 -> 16683 bytes .../dns/__pycache__/opcode.cpython-312.pyc | Bin 0 -> 3174 bytes .../dns/__pycache__/query.cpython-312.pyc | Bin 0 -> 59111 bytes .../dns/__pycache__/rcode.cpython-312.pyc | Bin 0 -> 4471 bytes .../dns/__pycache__/rdata.cpython-312.pyc | Bin 0 -> 37505 bytes .../__pycache__/rdataclass.cpython-312.pyc | Bin 0 -> 3562 bytes .../dns/__pycache__/rdataset.cpython-312.pyc | Bin 0 -> 23008 bytes .../dns/__pycache__/rdatatype.cpython-312.pyc | Bin 0 -> 10158 bytes .../dns/__pycache__/renderer.cpython-312.pyc | Bin 0 -> 16270 bytes .../dns/__pycache__/resolver.cpython-312.pyc | Bin 0 -> 88419 bytes .../__pycache__/reversename.cpython-312.pyc | Bin 0 -> 4768 bytes .../dns/__pycache__/rrset.cpython-312.pyc | Bin 0 -> 12527 bytes .../dns/__pycache__/serial.cpython-312.pyc | Bin 0 -> 5226 bytes .../dns/__pycache__/set.cpython-312.pyc | Bin 0 -> 12201 bytes .../dns/__pycache__/tokenizer.cpython-312.pyc | Bin 0 -> 26588 bytes .../__pycache__/transaction.cpython-312.pyc | Bin 0 -> 29446 bytes .../dns/__pycache__/tsig.cpython-312.pyc | Bin 0 -> 16800 bytes .../__pycache__/tsigkeyring.cpython-312.pyc | Bin 0 -> 2954 bytes .../dns/__pycache__/ttl.cpython-312.pyc | Bin 0 -> 2487 bytes .../dns/__pycache__/update.cpython-312.pyc | Bin 0 -> 16372 bytes .../dns/__pycache__/version.cpython-312.pyc | Bin 0 -> 819 bytes .../dns/__pycache__/versioned.cpython-312.pyc | Bin 0 -> 14729 bytes .../dns/__pycache__/win32util.cpython-312.pyc | Bin 0 -> 9906 bytes .../dns/__pycache__/wire.cpython-312.pyc | Bin 0 -> 5493 bytes .../dns/__pycache__/xfr.cpython-312.pyc | Bin 0 -> 14532 bytes .../dns/__pycache__/zone.cpython-312.pyc | Bin 0 -> 67659 bytes .../dns/__pycache__/zonefile.cpython-312.pyc | Bin 0 -> 33874 bytes .../dns/__pycache__/zonetypes.cpython-312.pyc | Bin 0 -> 1388 bytes venv/Lib/site-packages/dns/_asyncbackend.py | 99 + .../Lib/site-packages/dns/_asyncio_backend.py | 275 ++ venv/Lib/site-packages/dns/_ddr.py | 154 + venv/Lib/site-packages/dns/_features.py | 92 + venv/Lib/site-packages/dns/_immutable_ctx.py | 76 + venv/Lib/site-packages/dns/_trio_backend.py | 250 ++ venv/Lib/site-packages/dns/asyncbackend.py | 101 + venv/Lib/site-packages/dns/asyncquery.py | 780 ++++ venv/Lib/site-packages/dns/asyncresolver.py | 475 +++ venv/Lib/site-packages/dns/dnssec.py | 1223 ++++++ .../site-packages/dns/dnssecalgs/__init__.py | 120 + .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 5560 bytes .../__pycache__/base.cpython-312.pyc | Bin 0 -> 4604 bytes .../__pycache__/cryptography.cpython-312.pyc | Bin 0 -> 3869 bytes .../__pycache__/dsa.cpython-312.pyc | Bin 0 -> 6208 bytes .../__pycache__/ecdsa.cpython-312.pyc | Bin 0 -> 6021 bytes .../__pycache__/eddsa.cpython-312.pyc | Bin 0 -> 4184 bytes .../__pycache__/rsa.cpython-312.pyc | Bin 0 -> 7240 bytes venv/Lib/site-packages/dns/dnssecalgs/base.py | 84 + .../dns/dnssecalgs/cryptography.py | 68 + venv/Lib/site-packages/dns/dnssecalgs/dsa.py | 101 + .../Lib/site-packages/dns/dnssecalgs/ecdsa.py | 89 + .../Lib/site-packages/dns/dnssecalgs/eddsa.py | 65 + venv/Lib/site-packages/dns/dnssecalgs/rsa.py | 119 + venv/Lib/site-packages/dns/dnssectypes.py | 71 + venv/Lib/site-packages/dns/e164.py | 116 + venv/Lib/site-packages/dns/edns.py | 516 +++ venv/Lib/site-packages/dns/entropy.py | 130 + venv/Lib/site-packages/dns/enum.py | 116 + venv/Lib/site-packages/dns/exception.py | 169 + venv/Lib/site-packages/dns/flags.py | 123 + venv/Lib/site-packages/dns/grange.py | 72 + venv/Lib/site-packages/dns/immutable.py | 68 + venv/Lib/site-packages/dns/inet.py | 197 + venv/Lib/site-packages/dns/ipv4.py | 77 + venv/Lib/site-packages/dns/ipv6.py | 219 ++ venv/Lib/site-packages/dns/message.py | 1888 +++++++++ venv/Lib/site-packages/dns/name.py | 1283 ++++++ venv/Lib/site-packages/dns/namedict.py | 109 + venv/Lib/site-packages/dns/nameserver.py | 359 ++ venv/Lib/site-packages/dns/node.py | 359 ++ venv/Lib/site-packages/dns/opcode.py | 117 + venv/Lib/site-packages/dns/py.typed | 0 venv/Lib/site-packages/dns/query.py | 1578 ++++++++ venv/Lib/site-packages/dns/quic/__init__.py | 75 + .../quic/__pycache__/__init__.cpython-312.pyc | Bin 0 -> 3130 bytes .../quic/__pycache__/_asyncio.cpython-312.pyc | Bin 0 -> 16888 bytes .../quic/__pycache__/_common.cpython-312.pyc | Bin 0 -> 10335 bytes .../quic/__pycache__/_sync.cpython-312.pyc | Bin 0 -> 15831 bytes .../quic/__pycache__/_trio.cpython-312.pyc | Bin 0 -> 13723 bytes venv/Lib/site-packages/dns/quic/_asyncio.py | 228 ++ venv/Lib/site-packages/dns/quic/_common.py | 224 ++ venv/Lib/site-packages/dns/quic/_sync.py | 238 ++ venv/Lib/site-packages/dns/quic/_trio.py | 210 + venv/Lib/site-packages/dns/rcode.py | 168 + venv/Lib/site-packages/dns/rdata.py | 884 +++++ venv/Lib/site-packages/dns/rdataclass.py | 118 + venv/Lib/site-packages/dns/rdataset.py | 516 +++ venv/Lib/site-packages/dns/rdatatype.py | 332 ++ .../site-packages/dns/rdtypes/ANY/AFSDB.py | 45 + .../site-packages/dns/rdtypes/ANY/AMTRELAY.py | 91 + venv/Lib/site-packages/dns/rdtypes/ANY/AVC.py | 26 + venv/Lib/site-packages/dns/rdtypes/ANY/CAA.py | 71 + .../site-packages/dns/rdtypes/ANY/CDNSKEY.py | 33 + venv/Lib/site-packages/dns/rdtypes/ANY/CDS.py | 29 + .../Lib/site-packages/dns/rdtypes/ANY/CERT.py | 116 + .../site-packages/dns/rdtypes/ANY/CNAME.py | 28 + .../site-packages/dns/rdtypes/ANY/CSYNC.py | 68 + venv/Lib/site-packages/dns/rdtypes/ANY/DLV.py | 24 + .../site-packages/dns/rdtypes/ANY/DNAME.py | 27 + .../site-packages/dns/rdtypes/ANY/DNSKEY.py | 33 + venv/Lib/site-packages/dns/rdtypes/ANY/DS.py | 24 + .../site-packages/dns/rdtypes/ANY/EUI48.py | 30 + .../site-packages/dns/rdtypes/ANY/EUI64.py | 30 + .../Lib/site-packages/dns/rdtypes/ANY/GPOS.py | 125 + .../site-packages/dns/rdtypes/ANY/HINFO.py | 66 + venv/Lib/site-packages/dns/rdtypes/ANY/HIP.py | 85 + .../Lib/site-packages/dns/rdtypes/ANY/ISDN.py | 77 + venv/Lib/site-packages/dns/rdtypes/ANY/L32.py | 41 + venv/Lib/site-packages/dns/rdtypes/ANY/L64.py | 47 + venv/Lib/site-packages/dns/rdtypes/ANY/LOC.py | 354 ++ venv/Lib/site-packages/dns/rdtypes/ANY/LP.py | 42 + venv/Lib/site-packages/dns/rdtypes/ANY/MX.py | 24 + venv/Lib/site-packages/dns/rdtypes/ANY/NID.py | 47 + .../site-packages/dns/rdtypes/ANY/NINFO.py | 26 + venv/Lib/site-packages/dns/rdtypes/ANY/NS.py | 24 + .../Lib/site-packages/dns/rdtypes/ANY/NSEC.py | 67 + .../site-packages/dns/rdtypes/ANY/NSEC3.py | 126 + .../dns/rdtypes/ANY/NSEC3PARAM.py | 69 + .../dns/rdtypes/ANY/OPENPGPKEY.py | 53 + venv/Lib/site-packages/dns/rdtypes/ANY/OPT.py | 77 + venv/Lib/site-packages/dns/rdtypes/ANY/PTR.py | 24 + venv/Lib/site-packages/dns/rdtypes/ANY/RP.py | 58 + .../site-packages/dns/rdtypes/ANY/RRSIG.py | 157 + venv/Lib/site-packages/dns/rdtypes/ANY/RT.py | 24 + .../site-packages/dns/rdtypes/ANY/SMIMEA.py | 9 + venv/Lib/site-packages/dns/rdtypes/ANY/SOA.py | 86 + venv/Lib/site-packages/dns/rdtypes/ANY/SPF.py | 26 + .../site-packages/dns/rdtypes/ANY/SSHFP.py | 68 + .../Lib/site-packages/dns/rdtypes/ANY/TKEY.py | 142 + .../Lib/site-packages/dns/rdtypes/ANY/TLSA.py | 9 + .../Lib/site-packages/dns/rdtypes/ANY/TSIG.py | 160 + venv/Lib/site-packages/dns/rdtypes/ANY/TXT.py | 24 + venv/Lib/site-packages/dns/rdtypes/ANY/URI.py | 79 + venv/Lib/site-packages/dns/rdtypes/ANY/X25.py | 57 + .../site-packages/dns/rdtypes/ANY/ZONEMD.py | 66 + .../site-packages/dns/rdtypes/ANY/__init__.py | 68 + .../ANY/__pycache__/AFSDB.cpython-312.pyc | Bin 0 -> 1105 bytes .../ANY/__pycache__/AMTRELAY.cpython-312.pyc | Bin 0 -> 4225 bytes .../ANY/__pycache__/AVC.cpython-312.pyc | Bin 0 -> 678 bytes .../ANY/__pycache__/CAA.cpython-312.pyc | Bin 0 -> 3405 bytes .../ANY/__pycache__/CDNSKEY.cpython-312.pyc | Bin 0 -> 765 bytes .../ANY/__pycache__/CDS.cpython-312.pyc | Bin 0 -> 873 bytes .../ANY/__pycache__/CERT.cpython-312.pyc | Bin 0 -> 4493 bytes .../ANY/__pycache__/CNAME.cpython-312.pyc | Bin 0 -> 888 bytes .../ANY/__pycache__/CSYNC.cpython-312.pyc | Bin 0 -> 3374 bytes .../ANY/__pycache__/DLV.cpython-312.pyc | Bin 0 -> 675 bytes .../ANY/__pycache__/DNAME.cpython-312.pyc | Bin 0 -> 951 bytes .../ANY/__pycache__/DNSKEY.cpython-312.pyc | Bin 0 -> 762 bytes .../ANY/__pycache__/DS.cpython-312.pyc | Bin 0 -> 672 bytes .../ANY/__pycache__/EUI48.cpython-312.pyc | Bin 0 -> 760 bytes .../ANY/__pycache__/EUI64.cpython-312.pyc | Bin 0 -> 760 bytes .../ANY/__pycache__/GPOS.cpython-312.pyc | Bin 0 -> 6170 bytes .../ANY/__pycache__/HINFO.cpython-312.pyc | Bin 0 -> 3035 bytes .../ANY/__pycache__/HIP.cpython-312.pyc | Bin 0 -> 4871 bytes .../ANY/__pycache__/ISDN.cpython-312.pyc | Bin 0 -> 3495 bytes .../ANY/__pycache__/L32.cpython-312.pyc | Bin 0 -> 2540 bytes .../ANY/__pycache__/L64.cpython-312.pyc | Bin 0 -> 2997 bytes .../ANY/__pycache__/LOC.cpython-312.pyc | Bin 0 -> 14111 bytes .../ANY/__pycache__/LP.cpython-312.pyc | Bin 0 -> 2512 bytes .../ANY/__pycache__/MX.cpython-312.pyc | Bin 0 -> 672 bytes .../ANY/__pycache__/NID.cpython-312.pyc | Bin 0 -> 2990 bytes .../ANY/__pycache__/NINFO.cpython-312.pyc | Bin 0 -> 684 bytes .../ANY/__pycache__/NS.cpython-312.pyc | Bin 0 -> 672 bytes .../ANY/__pycache__/NSEC.cpython-312.pyc | Bin 0 -> 3155 bytes .../ANY/__pycache__/NSEC3.cpython-312.pyc | Bin 0 -> 6394 bytes .../__pycache__/NSEC3PARAM.cpython-312.pyc | Bin 0 -> 3426 bytes .../__pycache__/OPENPGPKEY.cpython-312.pyc | Bin 0 -> 2350 bytes .../ANY/__pycache__/OPT.cpython-312.pyc | Bin 0 -> 3564 bytes .../ANY/__pycache__/PTR.cpython-312.pyc | Bin 0 -> 675 bytes .../ANY/__pycache__/RP.cpython-312.pyc | Bin 0 -> 2604 bytes .../ANY/__pycache__/RRSIG.cpython-312.pyc | Bin 0 -> 6603 bytes .../ANY/__pycache__/RT.cpython-312.pyc | Bin 0 -> 690 bytes .../ANY/__pycache__/SMIMEA.cpython-312.pyc | Bin 0 -> 690 bytes .../ANY/__pycache__/SOA.cpython-312.pyc | Bin 0 -> 3832 bytes .../ANY/__pycache__/SPF.cpython-312.pyc | Bin 0 -> 678 bytes .../ANY/__pycache__/SSHFP.cpython-312.pyc | Bin 0 -> 3159 bytes .../ANY/__pycache__/TKEY.cpython-312.pyc | Bin 0 -> 5129 bytes .../ANY/__pycache__/TLSA.cpython-312.pyc | Bin 0 -> 684 bytes .../ANY/__pycache__/TSIG.cpython-312.pyc | Bin 0 -> 5944 bytes .../ANY/__pycache__/TXT.cpython-312.pyc | Bin 0 -> 678 bytes .../ANY/__pycache__/URI.cpython-312.pyc | Bin 0 -> 4191 bytes .../ANY/__pycache__/X25.cpython-312.pyc | Bin 0 -> 2388 bytes .../ANY/__pycache__/ZONEMD.cpython-312.pyc | Bin 0 -> 4208 bytes .../ANY/__pycache__/__init__.cpython-312.pyc | Bin 0 -> 602 bytes venv/Lib/site-packages/dns/rdtypes/CH/A.py | 59 + .../site-packages/dns/rdtypes/CH/__init__.py | 22 + .../rdtypes/CH/__pycache__/A.cpython-312.pyc | Bin 0 -> 2558 bytes .../CH/__pycache__/__init__.cpython-312.pyc | Bin 0 -> 304 bytes venv/Lib/site-packages/dns/rdtypes/IN/A.py | 51 + venv/Lib/site-packages/dns/rdtypes/IN/AAAA.py | 51 + venv/Lib/site-packages/dns/rdtypes/IN/APL.py | 150 + .../Lib/site-packages/dns/rdtypes/IN/DHCID.py | 54 + .../Lib/site-packages/dns/rdtypes/IN/HTTPS.py | 9 + .../site-packages/dns/rdtypes/IN/IPSECKEY.py | 91 + venv/Lib/site-packages/dns/rdtypes/IN/KX.py | 24 + .../Lib/site-packages/dns/rdtypes/IN/NAPTR.py | 110 + venv/Lib/site-packages/dns/rdtypes/IN/NSAP.py | 60 + .../site-packages/dns/rdtypes/IN/NSAP_PTR.py | 24 + venv/Lib/site-packages/dns/rdtypes/IN/PX.py | 73 + venv/Lib/site-packages/dns/rdtypes/IN/SRV.py | 75 + venv/Lib/site-packages/dns/rdtypes/IN/SVCB.py | 9 + venv/Lib/site-packages/dns/rdtypes/IN/WKS.py | 100 + .../site-packages/dns/rdtypes/IN/__init__.py | 35 + .../rdtypes/IN/__pycache__/A.cpython-312.pyc | Bin 0 -> 2152 bytes .../IN/__pycache__/AAAA.cpython-312.pyc | Bin 0 -> 2176 bytes .../IN/__pycache__/APL.cpython-312.pyc | Bin 0 -> 6946 bytes .../IN/__pycache__/DHCID.cpython-312.pyc | Bin 0 -> 2296 bytes .../IN/__pycache__/HTTPS.cpython-312.pyc | Bin 0 -> 686 bytes .../IN/__pycache__/IPSECKEY.cpython-312.pyc | Bin 0 -> 4239 bytes .../rdtypes/IN/__pycache__/KX.cpython-312.pyc | Bin 0 -> 689 bytes .../IN/__pycache__/NAPTR.cpython-312.pyc | Bin 0 -> 5065 bytes .../IN/__pycache__/NSAP.cpython-312.pyc | Bin 0 -> 2741 bytes .../IN/__pycache__/NSAP_PTR.cpython-312.pyc | Bin 0 -> 697 bytes .../rdtypes/IN/__pycache__/PX.cpython-312.pyc | Bin 0 -> 3489 bytes .../IN/__pycache__/SRV.cpython-312.pyc | Bin 0 -> 3738 bytes .../IN/__pycache__/SVCB.cpython-312.pyc | Bin 0 -> 683 bytes .../IN/__pycache__/WKS.cpython-312.pyc | Bin 0 -> 4648 bytes .../IN/__pycache__/__init__.cpython-312.pyc | Bin 0 -> 385 bytes .../Lib/site-packages/dns/rdtypes/__init__.py | 33 + .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 384 bytes .../__pycache__/dnskeybase.cpython-312.pyc | Bin 0 -> 3908 bytes .../__pycache__/dsbase.cpython-312.pyc | Bin 0 -> 4207 bytes .../__pycache__/euibase.cpython-312.pyc | Bin 0 -> 3567 bytes .../__pycache__/mxbase.cpython-312.pyc | Bin 0 -> 4438 bytes .../__pycache__/nsbase.cpython-312.pyc | Bin 0 -> 2869 bytes .../__pycache__/svcbbase.cpython-312.pyc | Bin 0 -> 28526 bytes .../__pycache__/tlsabase.cpython-312.pyc | Bin 0 -> 3405 bytes .../__pycache__/txtbase.cpython-312.pyc | Bin 0 -> 5070 bytes .../rdtypes/__pycache__/util.cpython-312.pyc | Bin 0 -> 12875 bytes .../site-packages/dns/rdtypes/dnskeybase.py | 87 + venv/Lib/site-packages/dns/rdtypes/dsbase.py | 85 + venv/Lib/site-packages/dns/rdtypes/euibase.py | 70 + venv/Lib/site-packages/dns/rdtypes/mxbase.py | 87 + venv/Lib/site-packages/dns/rdtypes/nsbase.py | 63 + .../Lib/site-packages/dns/rdtypes/svcbbase.py | 553 +++ .../Lib/site-packages/dns/rdtypes/tlsabase.py | 71 + venv/Lib/site-packages/dns/rdtypes/txtbase.py | 104 + venv/Lib/site-packages/dns/rdtypes/util.py | 257 ++ venv/Lib/site-packages/dns/renderer.py | 346 ++ venv/Lib/site-packages/dns/resolver.py | 2054 ++++++++++ venv/Lib/site-packages/dns/reversename.py | 105 + venv/Lib/site-packages/dns/rrset.py | 285 ++ venv/Lib/site-packages/dns/serial.py | 118 + venv/Lib/site-packages/dns/set.py | 307 ++ venv/Lib/site-packages/dns/tokenizer.py | 708 ++++ venv/Lib/site-packages/dns/transaction.py | 651 +++ venv/Lib/site-packages/dns/tsig.py | 352 ++ venv/Lib/site-packages/dns/tsigkeyring.py | 68 + venv/Lib/site-packages/dns/ttl.py | 92 + venv/Lib/site-packages/dns/update.py | 386 ++ venv/Lib/site-packages/dns/version.py | 58 + venv/Lib/site-packages/dns/versioned.py | 318 ++ venv/Lib/site-packages/dns/win32util.py | 252 ++ venv/Lib/site-packages/dns/wire.py | 89 + venv/Lib/site-packages/dns/xfr.py | 343 ++ venv/Lib/site-packages/dns/zone.py | 1434 +++++++ venv/Lib/site-packages/dns/zonefile.py | 746 ++++ venv/Lib/site-packages/dns/zonetypes.py | 37 + .../dnspython-2.6.1.dist-info/INSTALLER | 1 + .../dnspython-2.6.1.dist-info/METADATA | 147 + .../dnspython-2.6.1.dist-info/RECORD | 290 ++ .../dnspython-2.6.1.dist-info/WHEEL | 4 + .../licenses/LICENSE | 35 + venv/Lib/site-packages/gridfs/__init__.py | 1000 +++++ .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 41030 bytes .../gridfs/__pycache__/errors.cpython-312.pyc | Bin 0 -> 1242 bytes .../__pycache__/grid_file.cpython-312.pyc | Bin 0 -> 44280 bytes venv/Lib/site-packages/gridfs/errors.py | 34 + venv/Lib/site-packages/gridfs/grid_file.py | 964 +++++ venv/Lib/site-packages/gridfs/py.typed | 2 + .../pymongo-4.7.2.dist-info/INSTALLER | 1 + .../pymongo-4.7.2.dist-info/LICENSE | 201 + .../pymongo-4.7.2.dist-info/METADATA | 485 +++ .../pymongo-4.7.2.dist-info/RECORD | 194 + .../pymongo-4.7.2.dist-info/REQUESTED | 0 .../pymongo-4.7.2.dist-info/WHEEL | 5 + .../pymongo-4.7.2.dist-info/top_level.txt | 3 + venv/Lib/site-packages/pymongo/__init__.py | 176 + .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 4275 bytes .../_azure_helpers.cpython-312.pyc | Bin 0 -> 2150 bytes .../pymongo/__pycache__/_csot.cpython-312.pyc | Bin 0 -> 6920 bytes .../__pycache__/_gcp_helpers.cpython-312.pyc | Bin 0 -> 1516 bytes .../__pycache__/_lazy_import.cpython-312.pyc | Bin 0 -> 1463 bytes .../__pycache__/_version.cpython-312.pyc | Bin 0 -> 1012 bytes .../__pycache__/aggregation.cpython-312.pyc | Bin 0 -> 10669 bytes .../pymongo/__pycache__/auth.cpython-312.pyc | Bin 0 -> 25543 bytes .../__pycache__/auth_aws.cpython-312.pyc | Bin 0 -> 3950 bytes .../__pycache__/auth_oidc.cpython-312.pyc | Bin 0 -> 16236 bytes .../pymongo/__pycache__/bulk.cpython-312.pyc | Bin 0 -> 22215 bytes .../__pycache__/change_stream.cpython-312.pyc | Bin 0 -> 20608 bytes .../client_options.cpython-312.pyc | Bin 0 -> 15716 bytes .../client_session.cpython-312.pyc | Bin 0 -> 51589 bytes .../__pycache__/collation.cpython-312.pyc | Bin 0 -> 8508 bytes .../__pycache__/collection.cpython-312.pyc | Bin 0 -> 143437 bytes .../command_cursor.cpython-312.pyc | Bin 0 -> 17123 bytes .../__pycache__/common.cpython-312.pyc | Bin 0 -> 40537 bytes .../compression_support.cpython-312.pyc | Bin 0 -> 6256 bytes .../__pycache__/cursor.cpython-312.pyc | Bin 0 -> 52786 bytes .../__pycache__/daemon.cpython-312.pyc | Bin 0 -> 4456 bytes .../__pycache__/database.cpython-312.pyc | Bin 0 -> 57452 bytes .../__pycache__/driver_info.cpython-312.pyc | Bin 0 -> 1820 bytes .../__pycache__/encryption.cpython-312.pyc | Bin 0 -> 47741 bytes .../encryption_options.cpython-312.pyc | Bin 0 -> 13403 bytes .../__pycache__/errors.cpython-312.pyc | Bin 0 -> 17519 bytes .../__pycache__/event_loggers.cpython-312.pyc | Bin 0 -> 13541 bytes .../pymongo/__pycache__/hello.cpython-312.pyc | Bin 0 -> 11328 bytes .../__pycache__/helpers.cpython-312.pyc | Bin 0 -> 11498 bytes .../pymongo/__pycache__/lock.cpython-312.pyc | Bin 0 -> 1224 bytes .../__pycache__/logger.cpython-312.pyc | Bin 0 -> 8400 bytes .../max_staleness_selectors.cpython-312.pyc | Bin 0 -> 4030 bytes .../__pycache__/message.cpython-312.pyc | Bin 0 -> 65118 bytes .../__pycache__/mongo_client.cpython-312.pyc | Bin 0 -> 114606 bytes .../__pycache__/monitor.cpython-312.pyc | Bin 0 -> 21432 bytes .../__pycache__/monitoring.cpython-312.pyc | Bin 0 -> 80130 bytes .../__pycache__/network.cpython-312.pyc | Bin 0 -> 15515 bytes .../__pycache__/ocsp_cache.cpython-312.pyc | Bin 0 -> 4069 bytes .../__pycache__/ocsp_support.cpython-312.pyc | Bin 0 -> 17308 bytes .../__pycache__/operations.cpython-312.pyc | Bin 0 -> 24887 bytes .../periodic_executor.cpython-312.pyc | Bin 0 -> 7390 bytes .../pymongo/__pycache__/pool.cpython-312.pyc | Bin 0 -> 92438 bytes .../pyopenssl_context.cpython-312.pyc | Bin 0 -> 18420 bytes .../__pycache__/read_concern.cpython-312.pyc | Bin 0 -> 3013 bytes .../read_preferences.cpython-312.pyc | Bin 0 -> 25568 bytes .../__pycache__/response.cpython-312.pyc | Bin 0 -> 5208 bytes .../__pycache__/results.cpython-312.pyc | Bin 0 -> 12568 bytes .../__pycache__/saslprep.cpython-312.pyc | Bin 0 -> 3487 bytes .../__pycache__/server.cpython-312.pyc | Bin 0 -> 13241 bytes .../__pycache__/server_api.cpython-312.pyc | Bin 0 -> 6501 bytes .../server_description.cpython-312.pyc | Bin 0 -> 13744 bytes .../server_selectors.cpython-312.pyc | Bin 0 -> 8060 bytes .../__pycache__/server_type.cpython-312.pyc | Bin 0 -> 864 bytes .../__pycache__/settings.cpython-312.pyc | Bin 0 -> 8201 bytes .../socket_checker.cpython-312.pyc | Bin 0 -> 3734 bytes .../__pycache__/srv_resolver.cpython-312.pyc | Bin 0 -> 6338 bytes .../__pycache__/ssl_context.cpython-312.pyc | Bin 0 -> 1061 bytes .../__pycache__/ssl_support.cpython-312.pyc | Bin 0 -> 3571 bytes .../__pycache__/topology.cpython-312.pyc | Bin 0 -> 44411 bytes .../topology_description.cpython-312.pyc | Bin 0 -> 28239 bytes .../__pycache__/typings.cpython-312.pyc | Bin 0 -> 1290 bytes .../__pycache__/uri_parser.cpython-312.pyc | Bin 0 -> 23754 bytes .../__pycache__/write_concern.cpython-312.pyc | Bin 0 -> 6315 bytes .../site-packages/pymongo/_azure_helpers.py | 57 + .../pymongo/_cmessage.cp312-win_amd64.pyd | Bin 0 -> 56832 bytes .../site-packages/pymongo/_cmessagemodule.c | 1046 +++++ venv/Lib/site-packages/pymongo/_csot.py | 153 + .../Lib/site-packages/pymongo/_gcp_helpers.py | 39 + .../Lib/site-packages/pymongo/_lazy_import.py | 43 + venv/Lib/site-packages/pymongo/_version.py | 30 + venv/Lib/site-packages/pymongo/aggregation.py | 255 ++ venv/Lib/site-packages/pymongo/auth.py | 656 ++++ venv/Lib/site-packages/pymongo/auth_aws.py | 106 + venv/Lib/site-packages/pymongo/auth_oidc.py | 365 ++ venv/Lib/site-packages/pymongo/bulk.py | 595 +++ .../site-packages/pymongo/change_stream.py | 490 +++ .../site-packages/pymongo/client_options.py | 330 ++ .../site-packages/pymongo/client_session.py | 1155 ++++++ venv/Lib/site-packages/pymongo/collation.py | 224 ++ venv/Lib/site-packages/pymongo/collection.py | 3483 +++++++++++++++++ .../site-packages/pymongo/command_cursor.py | 401 ++ venv/Lib/site-packages/pymongo/common.py | 1055 +++++ .../pymongo/compression_support.py | 157 + venv/Lib/site-packages/pymongo/cursor.py | 1357 +++++++ venv/Lib/site-packages/pymongo/daemon.py | 148 + venv/Lib/site-packages/pymongo/database.py | 1388 +++++++ venv/Lib/site-packages/pymongo/driver_info.py | 42 + venv/Lib/site-packages/pymongo/encryption.py | 1112 ++++++ .../pymongo/encryption_options.py | 268 ++ venv/Lib/site-packages/pymongo/errors.py | 376 ++ .../site-packages/pymongo/event_loggers.py | 223 ++ venv/Lib/site-packages/pymongo/hello.py | 224 ++ venv/Lib/site-packages/pymongo/helpers.py | 350 ++ venv/Lib/site-packages/pymongo/lock.py | 40 + venv/Lib/site-packages/pymongo/logger.py | 169 + .../pymongo/max_staleness_selectors.py | 122 + venv/Lib/site-packages/pymongo/message.py | 1753 +++++++++ .../Lib/site-packages/pymongo/mongo_client.py | 2529 ++++++++++++ venv/Lib/site-packages/pymongo/monitor.py | 485 +++ venv/Lib/site-packages/pymongo/monitoring.py | 1916 +++++++++ venv/Lib/site-packages/pymongo/network.py | 412 ++ venv/Lib/site-packages/pymongo/ocsp_cache.py | 108 + .../Lib/site-packages/pymongo/ocsp_support.py | 432 ++ venv/Lib/site-packages/pymongo/operations.py | 623 +++ .../pymongo/periodic_executor.py | 200 + venv/Lib/site-packages/pymongo/pool.py | 2105 ++++++++++ venv/Lib/site-packages/pymongo/py.typed | 2 + .../pymongo/pyopenssl_context.py | 417 ++ .../Lib/site-packages/pymongo/read_concern.py | 76 + .../site-packages/pymongo/read_preferences.py | 622 +++ venv/Lib/site-packages/pymongo/response.py | 131 + venv/Lib/site-packages/pymongo/results.py | 242 ++ venv/Lib/site-packages/pymongo/saslprep.py | 116 + venv/Lib/site-packages/pymongo/server.py | 346 ++ venv/Lib/site-packages/pymongo/server_api.py | 173 + .../pymongo/server_description.py | 299 ++ .../site-packages/pymongo/server_selectors.py | 174 + venv/Lib/site-packages/pymongo/server_type.py | 33 + venv/Lib/site-packages/pymongo/settings.py | 168 + .../site-packages/pymongo/socket_checker.py | 105 + .../Lib/site-packages/pymongo/srv_resolver.py | 138 + venv/Lib/site-packages/pymongo/ssl_context.py | 40 + venv/Lib/site-packages/pymongo/ssl_support.py | 104 + venv/Lib/site-packages/pymongo/topology.py | 1027 +++++ .../pymongo/topology_description.py | 676 ++++ venv/Lib/site-packages/pymongo/typings.py | 60 + venv/Lib/site-packages/pymongo/uri_parser.py | 628 +++ .../site-packages/pymongo/write_concern.py | 141 + 484 files changed, 75598 insertions(+) create mode 100644 venv/Lib/site-packages/bson/__init__.py create mode 100644 venv/Lib/site-packages/bson/__pycache__/__init__.cpython-312.pyc create mode 100644 venv/Lib/site-packages/bson/__pycache__/_helpers.cpython-312.pyc create mode 100644 venv/Lib/site-packages/bson/__pycache__/binary.cpython-312.pyc create mode 100644 venv/Lib/site-packages/bson/__pycache__/code.cpython-312.pyc create mode 100644 venv/Lib/site-packages/bson/__pycache__/codec_options.cpython-312.pyc create mode 100644 venv/Lib/site-packages/bson/__pycache__/datetime_ms.cpython-312.pyc create mode 100644 venv/Lib/site-packages/bson/__pycache__/dbref.cpython-312.pyc create mode 100644 venv/Lib/site-packages/bson/__pycache__/decimal128.cpython-312.pyc create mode 100644 venv/Lib/site-packages/bson/__pycache__/errors.cpython-312.pyc create mode 100644 venv/Lib/site-packages/bson/__pycache__/int64.cpython-312.pyc create mode 100644 venv/Lib/site-packages/bson/__pycache__/json_util.cpython-312.pyc create mode 100644 venv/Lib/site-packages/bson/__pycache__/max_key.cpython-312.pyc create mode 100644 venv/Lib/site-packages/bson/__pycache__/min_key.cpython-312.pyc create mode 100644 venv/Lib/site-packages/bson/__pycache__/objectid.cpython-312.pyc create mode 100644 venv/Lib/site-packages/bson/__pycache__/raw_bson.cpython-312.pyc create mode 100644 venv/Lib/site-packages/bson/__pycache__/regex.cpython-312.pyc create mode 100644 venv/Lib/site-packages/bson/__pycache__/son.cpython-312.pyc create mode 100644 venv/Lib/site-packages/bson/__pycache__/timestamp.cpython-312.pyc create mode 100644 venv/Lib/site-packages/bson/__pycache__/typings.cpython-312.pyc create mode 100644 venv/Lib/site-packages/bson/__pycache__/tz_util.cpython-312.pyc create mode 100644 venv/Lib/site-packages/bson/_cbson.cp312-win_amd64.pyd create mode 100644 venv/Lib/site-packages/bson/_cbsonmodule.c create mode 100644 venv/Lib/site-packages/bson/_cbsonmodule.h create mode 100644 venv/Lib/site-packages/bson/_helpers.py create mode 100644 venv/Lib/site-packages/bson/binary.py create mode 100644 venv/Lib/site-packages/bson/bson-endian.h create mode 100644 venv/Lib/site-packages/bson/buffer.c create mode 100644 venv/Lib/site-packages/bson/buffer.h create mode 100644 venv/Lib/site-packages/bson/code.py create mode 100644 venv/Lib/site-packages/bson/codec_options.py create mode 100644 venv/Lib/site-packages/bson/datetime_ms.py create mode 100644 venv/Lib/site-packages/bson/dbref.py create mode 100644 venv/Lib/site-packages/bson/decimal128.py create mode 100644 venv/Lib/site-packages/bson/errors.py create mode 100644 venv/Lib/site-packages/bson/int64.py create mode 100644 venv/Lib/site-packages/bson/json_util.py create mode 100644 venv/Lib/site-packages/bson/max_key.py create mode 100644 venv/Lib/site-packages/bson/min_key.py create mode 100644 venv/Lib/site-packages/bson/objectid.py create mode 100644 venv/Lib/site-packages/bson/py.typed create mode 100644 venv/Lib/site-packages/bson/raw_bson.py create mode 100644 venv/Lib/site-packages/bson/regex.py create mode 100644 venv/Lib/site-packages/bson/son.py create mode 100644 venv/Lib/site-packages/bson/time64.c create mode 100644 venv/Lib/site-packages/bson/time64.h create mode 100644 venv/Lib/site-packages/bson/time64_config.h create mode 100644 venv/Lib/site-packages/bson/time64_limits.h create mode 100644 venv/Lib/site-packages/bson/timestamp.py create mode 100644 venv/Lib/site-packages/bson/typings.py create mode 100644 venv/Lib/site-packages/bson/tz_util.py create mode 100644 venv/Lib/site-packages/dns/__init__.py create mode 100644 venv/Lib/site-packages/dns/__pycache__/__init__.cpython-312.pyc create mode 100644 venv/Lib/site-packages/dns/__pycache__/_asyncbackend.cpython-312.pyc create mode 100644 venv/Lib/site-packages/dns/__pycache__/_asyncio_backend.cpython-312.pyc create mode 100644 venv/Lib/site-packages/dns/__pycache__/_ddr.cpython-312.pyc create mode 100644 venv/Lib/site-packages/dns/__pycache__/_features.cpython-312.pyc create mode 100644 venv/Lib/site-packages/dns/__pycache__/_immutable_ctx.cpython-312.pyc create mode 100644 venv/Lib/site-packages/dns/__pycache__/_trio_backend.cpython-312.pyc create mode 100644 venv/Lib/site-packages/dns/__pycache__/asyncbackend.cpython-312.pyc create mode 100644 venv/Lib/site-packages/dns/__pycache__/asyncquery.cpython-312.pyc create mode 100644 venv/Lib/site-packages/dns/__pycache__/asyncresolver.cpython-312.pyc create mode 100644 venv/Lib/site-packages/dns/__pycache__/dnssec.cpython-312.pyc create mode 100644 venv/Lib/site-packages/dns/__pycache__/dnssectypes.cpython-312.pyc create mode 100644 venv/Lib/site-packages/dns/__pycache__/e164.cpython-312.pyc create mode 100644 venv/Lib/site-packages/dns/__pycache__/edns.cpython-312.pyc create mode 100644 venv/Lib/site-packages/dns/__pycache__/entropy.cpython-312.pyc create mode 100644 venv/Lib/site-packages/dns/__pycache__/enum.cpython-312.pyc create mode 100644 venv/Lib/site-packages/dns/__pycache__/exception.cpython-312.pyc create mode 100644 venv/Lib/site-packages/dns/__pycache__/flags.cpython-312.pyc create mode 100644 venv/Lib/site-packages/dns/__pycache__/grange.cpython-312.pyc create mode 100644 venv/Lib/site-packages/dns/__pycache__/immutable.cpython-312.pyc create mode 100644 venv/Lib/site-packages/dns/__pycache__/inet.cpython-312.pyc create mode 100644 venv/Lib/site-packages/dns/__pycache__/ipv4.cpython-312.pyc create mode 100644 venv/Lib/site-packages/dns/__pycache__/ipv6.cpython-312.pyc create mode 100644 venv/Lib/site-packages/dns/__pycache__/message.cpython-312.pyc create mode 100644 venv/Lib/site-packages/dns/__pycache__/name.cpython-312.pyc create mode 100644 venv/Lib/site-packages/dns/__pycache__/namedict.cpython-312.pyc create mode 100644 venv/Lib/site-packages/dns/__pycache__/nameserver.cpython-312.pyc create mode 100644 venv/Lib/site-packages/dns/__pycache__/node.cpython-312.pyc create mode 100644 venv/Lib/site-packages/dns/__pycache__/opcode.cpython-312.pyc create mode 100644 venv/Lib/site-packages/dns/__pycache__/query.cpython-312.pyc create mode 100644 venv/Lib/site-packages/dns/__pycache__/rcode.cpython-312.pyc create mode 100644 venv/Lib/site-packages/dns/__pycache__/rdata.cpython-312.pyc create mode 100644 venv/Lib/site-packages/dns/__pycache__/rdataclass.cpython-312.pyc create mode 100644 venv/Lib/site-packages/dns/__pycache__/rdataset.cpython-312.pyc create mode 100644 venv/Lib/site-packages/dns/__pycache__/rdatatype.cpython-312.pyc create mode 100644 venv/Lib/site-packages/dns/__pycache__/renderer.cpython-312.pyc create mode 100644 venv/Lib/site-packages/dns/__pycache__/resolver.cpython-312.pyc create mode 100644 venv/Lib/site-packages/dns/__pycache__/reversename.cpython-312.pyc create mode 100644 venv/Lib/site-packages/dns/__pycache__/rrset.cpython-312.pyc create mode 100644 venv/Lib/site-packages/dns/__pycache__/serial.cpython-312.pyc create mode 100644 venv/Lib/site-packages/dns/__pycache__/set.cpython-312.pyc create mode 100644 venv/Lib/site-packages/dns/__pycache__/tokenizer.cpython-312.pyc create mode 100644 venv/Lib/site-packages/dns/__pycache__/transaction.cpython-312.pyc create mode 100644 venv/Lib/site-packages/dns/__pycache__/tsig.cpython-312.pyc create mode 100644 venv/Lib/site-packages/dns/__pycache__/tsigkeyring.cpython-312.pyc create mode 100644 venv/Lib/site-packages/dns/__pycache__/ttl.cpython-312.pyc create mode 100644 venv/Lib/site-packages/dns/__pycache__/update.cpython-312.pyc create mode 100644 venv/Lib/site-packages/dns/__pycache__/version.cpython-312.pyc create mode 100644 venv/Lib/site-packages/dns/__pycache__/versioned.cpython-312.pyc create mode 100644 venv/Lib/site-packages/dns/__pycache__/win32util.cpython-312.pyc create mode 100644 venv/Lib/site-packages/dns/__pycache__/wire.cpython-312.pyc create mode 100644 venv/Lib/site-packages/dns/__pycache__/xfr.cpython-312.pyc create mode 100644 venv/Lib/site-packages/dns/__pycache__/zone.cpython-312.pyc create mode 100644 venv/Lib/site-packages/dns/__pycache__/zonefile.cpython-312.pyc create mode 100644 venv/Lib/site-packages/dns/__pycache__/zonetypes.cpython-312.pyc create mode 100644 venv/Lib/site-packages/dns/_asyncbackend.py create mode 100644 venv/Lib/site-packages/dns/_asyncio_backend.py create mode 100644 venv/Lib/site-packages/dns/_ddr.py create mode 100644 venv/Lib/site-packages/dns/_features.py create mode 100644 venv/Lib/site-packages/dns/_immutable_ctx.py create mode 100644 venv/Lib/site-packages/dns/_trio_backend.py create mode 100644 venv/Lib/site-packages/dns/asyncbackend.py create mode 100644 venv/Lib/site-packages/dns/asyncquery.py create mode 100644 venv/Lib/site-packages/dns/asyncresolver.py create mode 100644 venv/Lib/site-packages/dns/dnssec.py create mode 100644 venv/Lib/site-packages/dns/dnssecalgs/__init__.py create mode 100644 venv/Lib/site-packages/dns/dnssecalgs/__pycache__/__init__.cpython-312.pyc create mode 100644 venv/Lib/site-packages/dns/dnssecalgs/__pycache__/base.cpython-312.pyc create mode 100644 venv/Lib/site-packages/dns/dnssecalgs/__pycache__/cryptography.cpython-312.pyc create mode 100644 venv/Lib/site-packages/dns/dnssecalgs/__pycache__/dsa.cpython-312.pyc create mode 100644 venv/Lib/site-packages/dns/dnssecalgs/__pycache__/ecdsa.cpython-312.pyc create mode 100644 venv/Lib/site-packages/dns/dnssecalgs/__pycache__/eddsa.cpython-312.pyc create mode 100644 venv/Lib/site-packages/dns/dnssecalgs/__pycache__/rsa.cpython-312.pyc create mode 100644 venv/Lib/site-packages/dns/dnssecalgs/base.py create mode 100644 venv/Lib/site-packages/dns/dnssecalgs/cryptography.py create mode 100644 venv/Lib/site-packages/dns/dnssecalgs/dsa.py create mode 100644 venv/Lib/site-packages/dns/dnssecalgs/ecdsa.py create mode 100644 venv/Lib/site-packages/dns/dnssecalgs/eddsa.py create mode 100644 venv/Lib/site-packages/dns/dnssecalgs/rsa.py create mode 100644 venv/Lib/site-packages/dns/dnssectypes.py create mode 100644 venv/Lib/site-packages/dns/e164.py create mode 100644 venv/Lib/site-packages/dns/edns.py create mode 100644 venv/Lib/site-packages/dns/entropy.py create mode 100644 venv/Lib/site-packages/dns/enum.py create mode 100644 venv/Lib/site-packages/dns/exception.py create mode 100644 venv/Lib/site-packages/dns/flags.py create mode 100644 venv/Lib/site-packages/dns/grange.py create mode 100644 venv/Lib/site-packages/dns/immutable.py create mode 100644 venv/Lib/site-packages/dns/inet.py create mode 100644 venv/Lib/site-packages/dns/ipv4.py create mode 100644 venv/Lib/site-packages/dns/ipv6.py create mode 100644 venv/Lib/site-packages/dns/message.py create mode 100644 venv/Lib/site-packages/dns/name.py create mode 100644 venv/Lib/site-packages/dns/namedict.py create mode 100644 venv/Lib/site-packages/dns/nameserver.py create mode 100644 venv/Lib/site-packages/dns/node.py create mode 100644 venv/Lib/site-packages/dns/opcode.py create mode 100644 venv/Lib/site-packages/dns/py.typed create mode 100644 venv/Lib/site-packages/dns/query.py create mode 100644 venv/Lib/site-packages/dns/quic/__init__.py create mode 100644 venv/Lib/site-packages/dns/quic/__pycache__/__init__.cpython-312.pyc create mode 100644 venv/Lib/site-packages/dns/quic/__pycache__/_asyncio.cpython-312.pyc create mode 100644 venv/Lib/site-packages/dns/quic/__pycache__/_common.cpython-312.pyc create mode 100644 venv/Lib/site-packages/dns/quic/__pycache__/_sync.cpython-312.pyc create mode 100644 venv/Lib/site-packages/dns/quic/__pycache__/_trio.cpython-312.pyc create mode 100644 venv/Lib/site-packages/dns/quic/_asyncio.py create mode 100644 venv/Lib/site-packages/dns/quic/_common.py create mode 100644 venv/Lib/site-packages/dns/quic/_sync.py create mode 100644 venv/Lib/site-packages/dns/quic/_trio.py create mode 100644 venv/Lib/site-packages/dns/rcode.py create mode 100644 venv/Lib/site-packages/dns/rdata.py create mode 100644 venv/Lib/site-packages/dns/rdataclass.py create mode 100644 venv/Lib/site-packages/dns/rdataset.py create mode 100644 venv/Lib/site-packages/dns/rdatatype.py create mode 100644 venv/Lib/site-packages/dns/rdtypes/ANY/AFSDB.py create mode 100644 venv/Lib/site-packages/dns/rdtypes/ANY/AMTRELAY.py create mode 100644 venv/Lib/site-packages/dns/rdtypes/ANY/AVC.py create mode 100644 venv/Lib/site-packages/dns/rdtypes/ANY/CAA.py create mode 100644 venv/Lib/site-packages/dns/rdtypes/ANY/CDNSKEY.py create mode 100644 venv/Lib/site-packages/dns/rdtypes/ANY/CDS.py create mode 100644 venv/Lib/site-packages/dns/rdtypes/ANY/CERT.py create mode 100644 venv/Lib/site-packages/dns/rdtypes/ANY/CNAME.py create mode 100644 venv/Lib/site-packages/dns/rdtypes/ANY/CSYNC.py create mode 100644 venv/Lib/site-packages/dns/rdtypes/ANY/DLV.py create mode 100644 venv/Lib/site-packages/dns/rdtypes/ANY/DNAME.py create mode 100644 venv/Lib/site-packages/dns/rdtypes/ANY/DNSKEY.py create mode 100644 venv/Lib/site-packages/dns/rdtypes/ANY/DS.py create mode 100644 venv/Lib/site-packages/dns/rdtypes/ANY/EUI48.py create mode 100644 venv/Lib/site-packages/dns/rdtypes/ANY/EUI64.py create mode 100644 venv/Lib/site-packages/dns/rdtypes/ANY/GPOS.py create mode 100644 venv/Lib/site-packages/dns/rdtypes/ANY/HINFO.py create mode 100644 venv/Lib/site-packages/dns/rdtypes/ANY/HIP.py create mode 100644 venv/Lib/site-packages/dns/rdtypes/ANY/ISDN.py create mode 100644 venv/Lib/site-packages/dns/rdtypes/ANY/L32.py create mode 100644 venv/Lib/site-packages/dns/rdtypes/ANY/L64.py create mode 100644 venv/Lib/site-packages/dns/rdtypes/ANY/LOC.py create mode 100644 venv/Lib/site-packages/dns/rdtypes/ANY/LP.py create mode 100644 venv/Lib/site-packages/dns/rdtypes/ANY/MX.py create mode 100644 venv/Lib/site-packages/dns/rdtypes/ANY/NID.py create mode 100644 venv/Lib/site-packages/dns/rdtypes/ANY/NINFO.py create mode 100644 venv/Lib/site-packages/dns/rdtypes/ANY/NS.py create mode 100644 venv/Lib/site-packages/dns/rdtypes/ANY/NSEC.py create mode 100644 venv/Lib/site-packages/dns/rdtypes/ANY/NSEC3.py create mode 100644 venv/Lib/site-packages/dns/rdtypes/ANY/NSEC3PARAM.py create mode 100644 venv/Lib/site-packages/dns/rdtypes/ANY/OPENPGPKEY.py create mode 100644 venv/Lib/site-packages/dns/rdtypes/ANY/OPT.py create mode 100644 venv/Lib/site-packages/dns/rdtypes/ANY/PTR.py create mode 100644 venv/Lib/site-packages/dns/rdtypes/ANY/RP.py create mode 100644 venv/Lib/site-packages/dns/rdtypes/ANY/RRSIG.py create mode 100644 venv/Lib/site-packages/dns/rdtypes/ANY/RT.py create mode 100644 venv/Lib/site-packages/dns/rdtypes/ANY/SMIMEA.py create mode 100644 venv/Lib/site-packages/dns/rdtypes/ANY/SOA.py create mode 100644 venv/Lib/site-packages/dns/rdtypes/ANY/SPF.py create mode 100644 venv/Lib/site-packages/dns/rdtypes/ANY/SSHFP.py create mode 100644 venv/Lib/site-packages/dns/rdtypes/ANY/TKEY.py create mode 100644 venv/Lib/site-packages/dns/rdtypes/ANY/TLSA.py create mode 100644 venv/Lib/site-packages/dns/rdtypes/ANY/TSIG.py create mode 100644 venv/Lib/site-packages/dns/rdtypes/ANY/TXT.py create mode 100644 venv/Lib/site-packages/dns/rdtypes/ANY/URI.py create mode 100644 venv/Lib/site-packages/dns/rdtypes/ANY/X25.py create mode 100644 venv/Lib/site-packages/dns/rdtypes/ANY/ZONEMD.py create mode 100644 venv/Lib/site-packages/dns/rdtypes/ANY/__init__.py create mode 100644 venv/Lib/site-packages/dns/rdtypes/ANY/__pycache__/AFSDB.cpython-312.pyc create mode 100644 venv/Lib/site-packages/dns/rdtypes/ANY/__pycache__/AMTRELAY.cpython-312.pyc create mode 100644 venv/Lib/site-packages/dns/rdtypes/ANY/__pycache__/AVC.cpython-312.pyc create mode 100644 venv/Lib/site-packages/dns/rdtypes/ANY/__pycache__/CAA.cpython-312.pyc create mode 100644 venv/Lib/site-packages/dns/rdtypes/ANY/__pycache__/CDNSKEY.cpython-312.pyc create mode 100644 venv/Lib/site-packages/dns/rdtypes/ANY/__pycache__/CDS.cpython-312.pyc create mode 100644 venv/Lib/site-packages/dns/rdtypes/ANY/__pycache__/CERT.cpython-312.pyc create mode 100644 venv/Lib/site-packages/dns/rdtypes/ANY/__pycache__/CNAME.cpython-312.pyc create mode 100644 venv/Lib/site-packages/dns/rdtypes/ANY/__pycache__/CSYNC.cpython-312.pyc create mode 100644 venv/Lib/site-packages/dns/rdtypes/ANY/__pycache__/DLV.cpython-312.pyc create mode 100644 venv/Lib/site-packages/dns/rdtypes/ANY/__pycache__/DNAME.cpython-312.pyc create mode 100644 venv/Lib/site-packages/dns/rdtypes/ANY/__pycache__/DNSKEY.cpython-312.pyc create mode 100644 venv/Lib/site-packages/dns/rdtypes/ANY/__pycache__/DS.cpython-312.pyc create mode 100644 venv/Lib/site-packages/dns/rdtypes/ANY/__pycache__/EUI48.cpython-312.pyc create mode 100644 venv/Lib/site-packages/dns/rdtypes/ANY/__pycache__/EUI64.cpython-312.pyc create mode 100644 venv/Lib/site-packages/dns/rdtypes/ANY/__pycache__/GPOS.cpython-312.pyc create mode 100644 venv/Lib/site-packages/dns/rdtypes/ANY/__pycache__/HINFO.cpython-312.pyc create mode 100644 venv/Lib/site-packages/dns/rdtypes/ANY/__pycache__/HIP.cpython-312.pyc create mode 100644 venv/Lib/site-packages/dns/rdtypes/ANY/__pycache__/ISDN.cpython-312.pyc create mode 100644 venv/Lib/site-packages/dns/rdtypes/ANY/__pycache__/L32.cpython-312.pyc create mode 100644 venv/Lib/site-packages/dns/rdtypes/ANY/__pycache__/L64.cpython-312.pyc create mode 100644 venv/Lib/site-packages/dns/rdtypes/ANY/__pycache__/LOC.cpython-312.pyc create mode 100644 venv/Lib/site-packages/dns/rdtypes/ANY/__pycache__/LP.cpython-312.pyc create mode 100644 venv/Lib/site-packages/dns/rdtypes/ANY/__pycache__/MX.cpython-312.pyc create mode 100644 venv/Lib/site-packages/dns/rdtypes/ANY/__pycache__/NID.cpython-312.pyc create mode 100644 venv/Lib/site-packages/dns/rdtypes/ANY/__pycache__/NINFO.cpython-312.pyc create mode 100644 venv/Lib/site-packages/dns/rdtypes/ANY/__pycache__/NS.cpython-312.pyc create mode 100644 venv/Lib/site-packages/dns/rdtypes/ANY/__pycache__/NSEC.cpython-312.pyc create mode 100644 venv/Lib/site-packages/dns/rdtypes/ANY/__pycache__/NSEC3.cpython-312.pyc create mode 100644 venv/Lib/site-packages/dns/rdtypes/ANY/__pycache__/NSEC3PARAM.cpython-312.pyc create mode 100644 venv/Lib/site-packages/dns/rdtypes/ANY/__pycache__/OPENPGPKEY.cpython-312.pyc create mode 100644 venv/Lib/site-packages/dns/rdtypes/ANY/__pycache__/OPT.cpython-312.pyc create mode 100644 venv/Lib/site-packages/dns/rdtypes/ANY/__pycache__/PTR.cpython-312.pyc create mode 100644 venv/Lib/site-packages/dns/rdtypes/ANY/__pycache__/RP.cpython-312.pyc create mode 100644 venv/Lib/site-packages/dns/rdtypes/ANY/__pycache__/RRSIG.cpython-312.pyc create mode 100644 venv/Lib/site-packages/dns/rdtypes/ANY/__pycache__/RT.cpython-312.pyc create mode 100644 venv/Lib/site-packages/dns/rdtypes/ANY/__pycache__/SMIMEA.cpython-312.pyc create mode 100644 venv/Lib/site-packages/dns/rdtypes/ANY/__pycache__/SOA.cpython-312.pyc create mode 100644 venv/Lib/site-packages/dns/rdtypes/ANY/__pycache__/SPF.cpython-312.pyc create mode 100644 venv/Lib/site-packages/dns/rdtypes/ANY/__pycache__/SSHFP.cpython-312.pyc create mode 100644 venv/Lib/site-packages/dns/rdtypes/ANY/__pycache__/TKEY.cpython-312.pyc create mode 100644 venv/Lib/site-packages/dns/rdtypes/ANY/__pycache__/TLSA.cpython-312.pyc create mode 100644 venv/Lib/site-packages/dns/rdtypes/ANY/__pycache__/TSIG.cpython-312.pyc create mode 100644 venv/Lib/site-packages/dns/rdtypes/ANY/__pycache__/TXT.cpython-312.pyc create mode 100644 venv/Lib/site-packages/dns/rdtypes/ANY/__pycache__/URI.cpython-312.pyc create mode 100644 venv/Lib/site-packages/dns/rdtypes/ANY/__pycache__/X25.cpython-312.pyc create mode 100644 venv/Lib/site-packages/dns/rdtypes/ANY/__pycache__/ZONEMD.cpython-312.pyc create mode 100644 venv/Lib/site-packages/dns/rdtypes/ANY/__pycache__/__init__.cpython-312.pyc create mode 100644 venv/Lib/site-packages/dns/rdtypes/CH/A.py create mode 100644 venv/Lib/site-packages/dns/rdtypes/CH/__init__.py create mode 100644 venv/Lib/site-packages/dns/rdtypes/CH/__pycache__/A.cpython-312.pyc create mode 100644 venv/Lib/site-packages/dns/rdtypes/CH/__pycache__/__init__.cpython-312.pyc create mode 100644 venv/Lib/site-packages/dns/rdtypes/IN/A.py create mode 100644 venv/Lib/site-packages/dns/rdtypes/IN/AAAA.py create mode 100644 venv/Lib/site-packages/dns/rdtypes/IN/APL.py create mode 100644 venv/Lib/site-packages/dns/rdtypes/IN/DHCID.py create mode 100644 venv/Lib/site-packages/dns/rdtypes/IN/HTTPS.py create mode 100644 venv/Lib/site-packages/dns/rdtypes/IN/IPSECKEY.py create mode 100644 venv/Lib/site-packages/dns/rdtypes/IN/KX.py create mode 100644 venv/Lib/site-packages/dns/rdtypes/IN/NAPTR.py create mode 100644 venv/Lib/site-packages/dns/rdtypes/IN/NSAP.py create mode 100644 venv/Lib/site-packages/dns/rdtypes/IN/NSAP_PTR.py create mode 100644 venv/Lib/site-packages/dns/rdtypes/IN/PX.py create mode 100644 venv/Lib/site-packages/dns/rdtypes/IN/SRV.py create mode 100644 venv/Lib/site-packages/dns/rdtypes/IN/SVCB.py create mode 100644 venv/Lib/site-packages/dns/rdtypes/IN/WKS.py create mode 100644 venv/Lib/site-packages/dns/rdtypes/IN/__init__.py create mode 100644 venv/Lib/site-packages/dns/rdtypes/IN/__pycache__/A.cpython-312.pyc create mode 100644 venv/Lib/site-packages/dns/rdtypes/IN/__pycache__/AAAA.cpython-312.pyc create mode 100644 venv/Lib/site-packages/dns/rdtypes/IN/__pycache__/APL.cpython-312.pyc create mode 100644 venv/Lib/site-packages/dns/rdtypes/IN/__pycache__/DHCID.cpython-312.pyc create mode 100644 venv/Lib/site-packages/dns/rdtypes/IN/__pycache__/HTTPS.cpython-312.pyc create mode 100644 venv/Lib/site-packages/dns/rdtypes/IN/__pycache__/IPSECKEY.cpython-312.pyc create mode 100644 venv/Lib/site-packages/dns/rdtypes/IN/__pycache__/KX.cpython-312.pyc create mode 100644 venv/Lib/site-packages/dns/rdtypes/IN/__pycache__/NAPTR.cpython-312.pyc create mode 100644 venv/Lib/site-packages/dns/rdtypes/IN/__pycache__/NSAP.cpython-312.pyc create mode 100644 venv/Lib/site-packages/dns/rdtypes/IN/__pycache__/NSAP_PTR.cpython-312.pyc create mode 100644 venv/Lib/site-packages/dns/rdtypes/IN/__pycache__/PX.cpython-312.pyc create mode 100644 venv/Lib/site-packages/dns/rdtypes/IN/__pycache__/SRV.cpython-312.pyc create mode 100644 venv/Lib/site-packages/dns/rdtypes/IN/__pycache__/SVCB.cpython-312.pyc create mode 100644 venv/Lib/site-packages/dns/rdtypes/IN/__pycache__/WKS.cpython-312.pyc create mode 100644 venv/Lib/site-packages/dns/rdtypes/IN/__pycache__/__init__.cpython-312.pyc create mode 100644 venv/Lib/site-packages/dns/rdtypes/__init__.py create mode 100644 venv/Lib/site-packages/dns/rdtypes/__pycache__/__init__.cpython-312.pyc create mode 100644 venv/Lib/site-packages/dns/rdtypes/__pycache__/dnskeybase.cpython-312.pyc create mode 100644 venv/Lib/site-packages/dns/rdtypes/__pycache__/dsbase.cpython-312.pyc create mode 100644 venv/Lib/site-packages/dns/rdtypes/__pycache__/euibase.cpython-312.pyc create mode 100644 venv/Lib/site-packages/dns/rdtypes/__pycache__/mxbase.cpython-312.pyc create mode 100644 venv/Lib/site-packages/dns/rdtypes/__pycache__/nsbase.cpython-312.pyc create mode 100644 venv/Lib/site-packages/dns/rdtypes/__pycache__/svcbbase.cpython-312.pyc create mode 100644 venv/Lib/site-packages/dns/rdtypes/__pycache__/tlsabase.cpython-312.pyc create mode 100644 venv/Lib/site-packages/dns/rdtypes/__pycache__/txtbase.cpython-312.pyc create mode 100644 venv/Lib/site-packages/dns/rdtypes/__pycache__/util.cpython-312.pyc create mode 100644 venv/Lib/site-packages/dns/rdtypes/dnskeybase.py create mode 100644 venv/Lib/site-packages/dns/rdtypes/dsbase.py create mode 100644 venv/Lib/site-packages/dns/rdtypes/euibase.py create mode 100644 venv/Lib/site-packages/dns/rdtypes/mxbase.py create mode 100644 venv/Lib/site-packages/dns/rdtypes/nsbase.py create mode 100644 venv/Lib/site-packages/dns/rdtypes/svcbbase.py create mode 100644 venv/Lib/site-packages/dns/rdtypes/tlsabase.py create mode 100644 venv/Lib/site-packages/dns/rdtypes/txtbase.py create mode 100644 venv/Lib/site-packages/dns/rdtypes/util.py create mode 100644 venv/Lib/site-packages/dns/renderer.py create mode 100644 venv/Lib/site-packages/dns/resolver.py create mode 100644 venv/Lib/site-packages/dns/reversename.py create mode 100644 venv/Lib/site-packages/dns/rrset.py create mode 100644 venv/Lib/site-packages/dns/serial.py create mode 100644 venv/Lib/site-packages/dns/set.py create mode 100644 venv/Lib/site-packages/dns/tokenizer.py create mode 100644 venv/Lib/site-packages/dns/transaction.py create mode 100644 venv/Lib/site-packages/dns/tsig.py create mode 100644 venv/Lib/site-packages/dns/tsigkeyring.py create mode 100644 venv/Lib/site-packages/dns/ttl.py create mode 100644 venv/Lib/site-packages/dns/update.py create mode 100644 venv/Lib/site-packages/dns/version.py create mode 100644 venv/Lib/site-packages/dns/versioned.py create mode 100644 venv/Lib/site-packages/dns/win32util.py create mode 100644 venv/Lib/site-packages/dns/wire.py create mode 100644 venv/Lib/site-packages/dns/xfr.py create mode 100644 venv/Lib/site-packages/dns/zone.py create mode 100644 venv/Lib/site-packages/dns/zonefile.py create mode 100644 venv/Lib/site-packages/dns/zonetypes.py create mode 100644 venv/Lib/site-packages/dnspython-2.6.1.dist-info/INSTALLER create mode 100644 venv/Lib/site-packages/dnspython-2.6.1.dist-info/METADATA create mode 100644 venv/Lib/site-packages/dnspython-2.6.1.dist-info/RECORD create mode 100644 venv/Lib/site-packages/dnspython-2.6.1.dist-info/WHEEL create mode 100644 venv/Lib/site-packages/dnspython-2.6.1.dist-info/licenses/LICENSE create mode 100644 venv/Lib/site-packages/gridfs/__init__.py create mode 100644 venv/Lib/site-packages/gridfs/__pycache__/__init__.cpython-312.pyc create mode 100644 venv/Lib/site-packages/gridfs/__pycache__/errors.cpython-312.pyc create mode 100644 venv/Lib/site-packages/gridfs/__pycache__/grid_file.cpython-312.pyc create mode 100644 venv/Lib/site-packages/gridfs/errors.py create mode 100644 venv/Lib/site-packages/gridfs/grid_file.py create mode 100644 venv/Lib/site-packages/gridfs/py.typed create mode 100644 venv/Lib/site-packages/pymongo-4.7.2.dist-info/INSTALLER create mode 100644 venv/Lib/site-packages/pymongo-4.7.2.dist-info/LICENSE create mode 100644 venv/Lib/site-packages/pymongo-4.7.2.dist-info/METADATA create mode 100644 venv/Lib/site-packages/pymongo-4.7.2.dist-info/RECORD create mode 100644 venv/Lib/site-packages/pymongo-4.7.2.dist-info/REQUESTED create mode 100644 venv/Lib/site-packages/pymongo-4.7.2.dist-info/WHEEL create mode 100644 venv/Lib/site-packages/pymongo-4.7.2.dist-info/top_level.txt create mode 100644 venv/Lib/site-packages/pymongo/__init__.py create mode 100644 venv/Lib/site-packages/pymongo/__pycache__/__init__.cpython-312.pyc create mode 100644 venv/Lib/site-packages/pymongo/__pycache__/_azure_helpers.cpython-312.pyc create mode 100644 venv/Lib/site-packages/pymongo/__pycache__/_csot.cpython-312.pyc create mode 100644 venv/Lib/site-packages/pymongo/__pycache__/_gcp_helpers.cpython-312.pyc create mode 100644 venv/Lib/site-packages/pymongo/__pycache__/_lazy_import.cpython-312.pyc create mode 100644 venv/Lib/site-packages/pymongo/__pycache__/_version.cpython-312.pyc create mode 100644 venv/Lib/site-packages/pymongo/__pycache__/aggregation.cpython-312.pyc create mode 100644 venv/Lib/site-packages/pymongo/__pycache__/auth.cpython-312.pyc create mode 100644 venv/Lib/site-packages/pymongo/__pycache__/auth_aws.cpython-312.pyc create mode 100644 venv/Lib/site-packages/pymongo/__pycache__/auth_oidc.cpython-312.pyc create mode 100644 venv/Lib/site-packages/pymongo/__pycache__/bulk.cpython-312.pyc create mode 100644 venv/Lib/site-packages/pymongo/__pycache__/change_stream.cpython-312.pyc create mode 100644 venv/Lib/site-packages/pymongo/__pycache__/client_options.cpython-312.pyc create mode 100644 venv/Lib/site-packages/pymongo/__pycache__/client_session.cpython-312.pyc create mode 100644 venv/Lib/site-packages/pymongo/__pycache__/collation.cpython-312.pyc create mode 100644 venv/Lib/site-packages/pymongo/__pycache__/collection.cpython-312.pyc create mode 100644 venv/Lib/site-packages/pymongo/__pycache__/command_cursor.cpython-312.pyc create mode 100644 venv/Lib/site-packages/pymongo/__pycache__/common.cpython-312.pyc create mode 100644 venv/Lib/site-packages/pymongo/__pycache__/compression_support.cpython-312.pyc create mode 100644 venv/Lib/site-packages/pymongo/__pycache__/cursor.cpython-312.pyc create mode 100644 venv/Lib/site-packages/pymongo/__pycache__/daemon.cpython-312.pyc create mode 100644 venv/Lib/site-packages/pymongo/__pycache__/database.cpython-312.pyc create mode 100644 venv/Lib/site-packages/pymongo/__pycache__/driver_info.cpython-312.pyc create mode 100644 venv/Lib/site-packages/pymongo/__pycache__/encryption.cpython-312.pyc create mode 100644 venv/Lib/site-packages/pymongo/__pycache__/encryption_options.cpython-312.pyc create mode 100644 venv/Lib/site-packages/pymongo/__pycache__/errors.cpython-312.pyc create mode 100644 venv/Lib/site-packages/pymongo/__pycache__/event_loggers.cpython-312.pyc create mode 100644 venv/Lib/site-packages/pymongo/__pycache__/hello.cpython-312.pyc create mode 100644 venv/Lib/site-packages/pymongo/__pycache__/helpers.cpython-312.pyc create mode 100644 venv/Lib/site-packages/pymongo/__pycache__/lock.cpython-312.pyc create mode 100644 venv/Lib/site-packages/pymongo/__pycache__/logger.cpython-312.pyc create mode 100644 venv/Lib/site-packages/pymongo/__pycache__/max_staleness_selectors.cpython-312.pyc create mode 100644 venv/Lib/site-packages/pymongo/__pycache__/message.cpython-312.pyc create mode 100644 venv/Lib/site-packages/pymongo/__pycache__/mongo_client.cpython-312.pyc create mode 100644 venv/Lib/site-packages/pymongo/__pycache__/monitor.cpython-312.pyc create mode 100644 venv/Lib/site-packages/pymongo/__pycache__/monitoring.cpython-312.pyc create mode 100644 venv/Lib/site-packages/pymongo/__pycache__/network.cpython-312.pyc create mode 100644 venv/Lib/site-packages/pymongo/__pycache__/ocsp_cache.cpython-312.pyc create mode 100644 venv/Lib/site-packages/pymongo/__pycache__/ocsp_support.cpython-312.pyc create mode 100644 venv/Lib/site-packages/pymongo/__pycache__/operations.cpython-312.pyc create mode 100644 venv/Lib/site-packages/pymongo/__pycache__/periodic_executor.cpython-312.pyc create mode 100644 venv/Lib/site-packages/pymongo/__pycache__/pool.cpython-312.pyc create mode 100644 venv/Lib/site-packages/pymongo/__pycache__/pyopenssl_context.cpython-312.pyc create mode 100644 venv/Lib/site-packages/pymongo/__pycache__/read_concern.cpython-312.pyc create mode 100644 venv/Lib/site-packages/pymongo/__pycache__/read_preferences.cpython-312.pyc create mode 100644 venv/Lib/site-packages/pymongo/__pycache__/response.cpython-312.pyc create mode 100644 venv/Lib/site-packages/pymongo/__pycache__/results.cpython-312.pyc create mode 100644 venv/Lib/site-packages/pymongo/__pycache__/saslprep.cpython-312.pyc create mode 100644 venv/Lib/site-packages/pymongo/__pycache__/server.cpython-312.pyc create mode 100644 venv/Lib/site-packages/pymongo/__pycache__/server_api.cpython-312.pyc create mode 100644 venv/Lib/site-packages/pymongo/__pycache__/server_description.cpython-312.pyc create mode 100644 venv/Lib/site-packages/pymongo/__pycache__/server_selectors.cpython-312.pyc create mode 100644 venv/Lib/site-packages/pymongo/__pycache__/server_type.cpython-312.pyc create mode 100644 venv/Lib/site-packages/pymongo/__pycache__/settings.cpython-312.pyc create mode 100644 venv/Lib/site-packages/pymongo/__pycache__/socket_checker.cpython-312.pyc create mode 100644 venv/Lib/site-packages/pymongo/__pycache__/srv_resolver.cpython-312.pyc create mode 100644 venv/Lib/site-packages/pymongo/__pycache__/ssl_context.cpython-312.pyc create mode 100644 venv/Lib/site-packages/pymongo/__pycache__/ssl_support.cpython-312.pyc create mode 100644 venv/Lib/site-packages/pymongo/__pycache__/topology.cpython-312.pyc create mode 100644 venv/Lib/site-packages/pymongo/__pycache__/topology_description.cpython-312.pyc create mode 100644 venv/Lib/site-packages/pymongo/__pycache__/typings.cpython-312.pyc create mode 100644 venv/Lib/site-packages/pymongo/__pycache__/uri_parser.cpython-312.pyc create mode 100644 venv/Lib/site-packages/pymongo/__pycache__/write_concern.cpython-312.pyc create mode 100644 venv/Lib/site-packages/pymongo/_azure_helpers.py create mode 100644 venv/Lib/site-packages/pymongo/_cmessage.cp312-win_amd64.pyd create mode 100644 venv/Lib/site-packages/pymongo/_cmessagemodule.c create mode 100644 venv/Lib/site-packages/pymongo/_csot.py create mode 100644 venv/Lib/site-packages/pymongo/_gcp_helpers.py create mode 100644 venv/Lib/site-packages/pymongo/_lazy_import.py create mode 100644 venv/Lib/site-packages/pymongo/_version.py create mode 100644 venv/Lib/site-packages/pymongo/aggregation.py create mode 100644 venv/Lib/site-packages/pymongo/auth.py create mode 100644 venv/Lib/site-packages/pymongo/auth_aws.py create mode 100644 venv/Lib/site-packages/pymongo/auth_oidc.py create mode 100644 venv/Lib/site-packages/pymongo/bulk.py create mode 100644 venv/Lib/site-packages/pymongo/change_stream.py create mode 100644 venv/Lib/site-packages/pymongo/client_options.py create mode 100644 venv/Lib/site-packages/pymongo/client_session.py create mode 100644 venv/Lib/site-packages/pymongo/collation.py create mode 100644 venv/Lib/site-packages/pymongo/collection.py create mode 100644 venv/Lib/site-packages/pymongo/command_cursor.py create mode 100644 venv/Lib/site-packages/pymongo/common.py create mode 100644 venv/Lib/site-packages/pymongo/compression_support.py create mode 100644 venv/Lib/site-packages/pymongo/cursor.py create mode 100644 venv/Lib/site-packages/pymongo/daemon.py create mode 100644 venv/Lib/site-packages/pymongo/database.py create mode 100644 venv/Lib/site-packages/pymongo/driver_info.py create mode 100644 venv/Lib/site-packages/pymongo/encryption.py create mode 100644 venv/Lib/site-packages/pymongo/encryption_options.py create mode 100644 venv/Lib/site-packages/pymongo/errors.py create mode 100644 venv/Lib/site-packages/pymongo/event_loggers.py create mode 100644 venv/Lib/site-packages/pymongo/hello.py create mode 100644 venv/Lib/site-packages/pymongo/helpers.py create mode 100644 venv/Lib/site-packages/pymongo/lock.py create mode 100644 venv/Lib/site-packages/pymongo/logger.py create mode 100644 venv/Lib/site-packages/pymongo/max_staleness_selectors.py create mode 100644 venv/Lib/site-packages/pymongo/message.py create mode 100644 venv/Lib/site-packages/pymongo/mongo_client.py create mode 100644 venv/Lib/site-packages/pymongo/monitor.py create mode 100644 venv/Lib/site-packages/pymongo/monitoring.py create mode 100644 venv/Lib/site-packages/pymongo/network.py create mode 100644 venv/Lib/site-packages/pymongo/ocsp_cache.py create mode 100644 venv/Lib/site-packages/pymongo/ocsp_support.py create mode 100644 venv/Lib/site-packages/pymongo/operations.py create mode 100644 venv/Lib/site-packages/pymongo/periodic_executor.py create mode 100644 venv/Lib/site-packages/pymongo/pool.py create mode 100644 venv/Lib/site-packages/pymongo/py.typed create mode 100644 venv/Lib/site-packages/pymongo/pyopenssl_context.py create mode 100644 venv/Lib/site-packages/pymongo/read_concern.py create mode 100644 venv/Lib/site-packages/pymongo/read_preferences.py create mode 100644 venv/Lib/site-packages/pymongo/response.py create mode 100644 venv/Lib/site-packages/pymongo/results.py create mode 100644 venv/Lib/site-packages/pymongo/saslprep.py create mode 100644 venv/Lib/site-packages/pymongo/server.py create mode 100644 venv/Lib/site-packages/pymongo/server_api.py create mode 100644 venv/Lib/site-packages/pymongo/server_description.py create mode 100644 venv/Lib/site-packages/pymongo/server_selectors.py create mode 100644 venv/Lib/site-packages/pymongo/server_type.py create mode 100644 venv/Lib/site-packages/pymongo/settings.py create mode 100644 venv/Lib/site-packages/pymongo/socket_checker.py create mode 100644 venv/Lib/site-packages/pymongo/srv_resolver.py create mode 100644 venv/Lib/site-packages/pymongo/ssl_context.py create mode 100644 venv/Lib/site-packages/pymongo/ssl_support.py create mode 100644 venv/Lib/site-packages/pymongo/topology.py create mode 100644 venv/Lib/site-packages/pymongo/topology_description.py create mode 100644 venv/Lib/site-packages/pymongo/typings.py create mode 100644 venv/Lib/site-packages/pymongo/uri_parser.py create mode 100644 venv/Lib/site-packages/pymongo/write_concern.py diff --git a/venv/Lib/site-packages/bson/__init__.py b/venv/Lib/site-packages/bson/__init__.py new file mode 100644 index 00000000..a7c9ddc5 --- /dev/null +++ b/venv/Lib/site-packages/bson/__init__.py @@ -0,0 +1,1464 @@ +# Copyright 2009-present MongoDB, 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. + +"""BSON (Binary JSON) encoding and decoding. + +The mapping from Python types to BSON types is as follows: + +======================================= ============= =================== +Python Type BSON Type Supported Direction +======================================= ============= =================== +None null both +bool boolean both +int [#int]_ int32 / int64 py -> bson +`bson.int64.Int64` int64 both +float number (real) both +str string both +list array both +dict / `SON` object both +datetime.datetime [#dt]_ [#dt2]_ date both +`bson.regex.Regex` regex both +compiled re [#re]_ regex py -> bson +`bson.binary.Binary` binary both +`bson.objectid.ObjectId` oid both +`bson.dbref.DBRef` dbref both +None undefined bson -> py +`bson.code.Code` code both +str symbol bson -> py +bytes [#bytes]_ binary both +======================================= ============= =================== + +.. [#int] A Python int will be saved as a BSON int32 or BSON int64 depending + on its size. A BSON int32 will always decode to a Python int. A BSON + int64 will always decode to a :class:`~bson.int64.Int64`. +.. [#dt] datetime.datetime instances will be rounded to the nearest + millisecond when saved +.. [#dt2] all datetime.datetime instances are treated as *naive*. clients + should always use UTC. +.. [#re] :class:`~bson.regex.Regex` instances and regular expression + objects from ``re.compile()`` are both saved as BSON regular expressions. + BSON regular expressions are decoded as :class:`~bson.regex.Regex` + instances. +.. [#bytes] The bytes type is encoded as BSON binary with + subtype 0. It will be decoded back to bytes. +""" +from __future__ import annotations + +import datetime +import itertools +import os +import re +import struct +import sys +import uuid +from codecs import utf_8_decode as _utf_8_decode +from codecs import utf_8_encode as _utf_8_encode +from collections import abc as _abc +from typing import ( + IO, + TYPE_CHECKING, + Any, + BinaryIO, + Callable, + Generator, + Iterator, + Mapping, + MutableMapping, + NoReturn, + Optional, + Sequence, + Tuple, + Type, + TypeVar, + Union, + cast, + overload, +) + +from bson.binary import ( + ALL_UUID_SUBTYPES, + CSHARP_LEGACY, + JAVA_LEGACY, + OLD_UUID_SUBTYPE, + STANDARD, + UUID_SUBTYPE, + Binary, + UuidRepresentation, +) +from bson.code import Code +from bson.codec_options import ( + DEFAULT_CODEC_OPTIONS, + CodecOptions, + DatetimeConversion, + _raw_document_class, +) +from bson.datetime_ms import ( + EPOCH_AWARE, + EPOCH_NAIVE, + DatetimeMS, + _datetime_to_millis, + _millis_to_datetime, +) +from bson.dbref import DBRef +from bson.decimal128 import Decimal128 +from bson.errors import InvalidBSON, InvalidDocument, InvalidStringData +from bson.int64 import Int64 +from bson.max_key import MaxKey +from bson.min_key import MinKey +from bson.objectid import ObjectId +from bson.regex import Regex +from bson.son import RE_TYPE, SON +from bson.timestamp import Timestamp +from bson.tz_util import utc + +# Import some modules for type-checking only. +if TYPE_CHECKING: + from bson.raw_bson import RawBSONDocument + from bson.typings import _DocumentType, _ReadableBuffer + +try: + from bson import _cbson # type: ignore[attr-defined] + + _USE_C = True +except ImportError: + _USE_C = False + +__all__ = [ + "ALL_UUID_SUBTYPES", + "CSHARP_LEGACY", + "JAVA_LEGACY", + "OLD_UUID_SUBTYPE", + "STANDARD", + "UUID_SUBTYPE", + "Binary", + "UuidRepresentation", + "Code", + "DEFAULT_CODEC_OPTIONS", + "CodecOptions", + "DBRef", + "Decimal128", + "InvalidBSON", + "InvalidDocument", + "InvalidStringData", + "Int64", + "MaxKey", + "MinKey", + "ObjectId", + "Regex", + "RE_TYPE", + "SON", + "Timestamp", + "utc", + "EPOCH_AWARE", + "EPOCH_NAIVE", + "BSONNUM", + "BSONSTR", + "BSONOBJ", + "BSONARR", + "BSONBIN", + "BSONUND", + "BSONOID", + "BSONBOO", + "BSONDAT", + "BSONNUL", + "BSONRGX", + "BSONREF", + "BSONCOD", + "BSONSYM", + "BSONCWS", + "BSONINT", + "BSONTIM", + "BSONLON", + "BSONDEC", + "BSONMIN", + "BSONMAX", + "get_data_and_view", + "gen_list_name", + "encode", + "decode", + "decode_all", + "decode_iter", + "decode_file_iter", + "is_valid", + "BSON", + "has_c", + "DatetimeConversion", + "DatetimeMS", +] + +BSONNUM = b"\x01" # Floating point +BSONSTR = b"\x02" # UTF-8 string +BSONOBJ = b"\x03" # Embedded document +BSONARR = b"\x04" # Array +BSONBIN = b"\x05" # Binary +BSONUND = b"\x06" # Undefined +BSONOID = b"\x07" # ObjectId +BSONBOO = b"\x08" # Boolean +BSONDAT = b"\x09" # UTC Datetime +BSONNUL = b"\x0A" # Null +BSONRGX = b"\x0B" # Regex +BSONREF = b"\x0C" # DBRef +BSONCOD = b"\x0D" # Javascript code +BSONSYM = b"\x0E" # Symbol +BSONCWS = b"\x0F" # Javascript code with scope +BSONINT = b"\x10" # 32bit int +BSONTIM = b"\x11" # Timestamp +BSONLON = b"\x12" # 64bit int +BSONDEC = b"\x13" # Decimal128 +BSONMIN = b"\xFF" # Min key +BSONMAX = b"\x7F" # Max key + + +_UNPACK_FLOAT_FROM = struct.Struct(" Tuple[Any, memoryview]: + if isinstance(data, (bytes, bytearray)): + return data, memoryview(data) + view = memoryview(data) + return view.tobytes(), view + + +def _raise_unknown_type(element_type: int, element_name: str) -> NoReturn: + """Unknown type helper.""" + raise InvalidBSON( + "Detected unknown BSON type {!r} for fieldname '{}'. Are " + "you using the latest driver version?".format(chr(element_type).encode(), element_name) + ) + + +def _get_int( + data: Any, _view: Any, position: int, dummy0: Any, dummy1: Any, dummy2: Any +) -> Tuple[int, int]: + """Decode a BSON int32 to python int.""" + return _UNPACK_INT_FROM(data, position)[0], position + 4 + + +def _get_c_string(data: Any, view: Any, position: int, opts: CodecOptions[Any]) -> Tuple[str, int]: + """Decode a BSON 'C' string to python str.""" + end = data.index(b"\x00", position) + return _utf_8_decode(view[position:end], opts.unicode_decode_error_handler, True)[0], end + 1 + + +def _get_float( + data: Any, _view: Any, position: int, dummy0: Any, dummy1: Any, dummy2: Any +) -> Tuple[float, int]: + """Decode a BSON double to python float.""" + return _UNPACK_FLOAT_FROM(data, position)[0], position + 8 + + +def _get_string( + data: Any, view: Any, position: int, obj_end: int, opts: CodecOptions[Any], dummy: Any +) -> Tuple[str, int]: + """Decode a BSON string to python str.""" + length = _UNPACK_INT_FROM(data, position)[0] + position += 4 + if length < 1 or obj_end - position < length: + raise InvalidBSON("invalid string length") + end = position + length - 1 + if data[end] != 0: + raise InvalidBSON("invalid end of string") + return _utf_8_decode(view[position:end], opts.unicode_decode_error_handler, True)[0], end + 1 + + +def _get_object_size(data: Any, position: int, obj_end: int) -> Tuple[int, int]: + """Validate and return a BSON document's size.""" + try: + obj_size = _UNPACK_INT_FROM(data, position)[0] + except struct.error as exc: + raise InvalidBSON(str(exc)) from None + end = position + obj_size - 1 + if data[end] != 0: + raise InvalidBSON("bad eoo") + if end >= obj_end: + raise InvalidBSON("invalid object length") + # If this is the top-level document, validate the total size too. + if position == 0 and obj_size != obj_end: + raise InvalidBSON("invalid object length") + return obj_size, end + + +def _get_object( + data: Any, view: Any, position: int, obj_end: int, opts: CodecOptions[Any], dummy: Any +) -> Tuple[Any, int]: + """Decode a BSON subdocument to opts.document_class or bson.dbref.DBRef.""" + obj_size, end = _get_object_size(data, position, obj_end) + if _raw_document_class(opts.document_class): + return (opts.document_class(data[position : end + 1], opts), position + obj_size) + + obj = _elements_to_dict(data, view, position + 4, end, opts) + + position += obj_size + # If DBRef validation fails, return a normal doc. + if ( + isinstance(obj.get("$ref"), str) + and "$id" in obj + and isinstance(obj.get("$db"), (str, type(None))) + ): + return (DBRef(obj.pop("$ref"), obj.pop("$id", None), obj.pop("$db", None), obj), position) + return obj, position + + +def _get_array( + data: Any, view: Any, position: int, obj_end: int, opts: CodecOptions[Any], element_name: str +) -> Tuple[Any, int]: + """Decode a BSON array to python list.""" + size = _UNPACK_INT_FROM(data, position)[0] + end = position + size - 1 + if data[end] != 0: + raise InvalidBSON("bad eoo") + + position += 4 + end -= 1 + result: list[Any] = [] + + # Avoid doing global and attribute lookups in the loop. + append = result.append + index = data.index + getter = _ELEMENT_GETTER + decoder_map = opts.type_registry._decoder_map + + while position < end: + element_type = data[position] + # Just skip the keys. + position = index(b"\x00", position) + 1 + try: + value, position = getter[element_type]( + data, view, position, obj_end, opts, element_name + ) + except KeyError: + _raise_unknown_type(element_type, element_name) + + if decoder_map: + custom_decoder = decoder_map.get(type(value)) + if custom_decoder is not None: + value = custom_decoder(value) + + append(value) + + if position != end + 1: + raise InvalidBSON("bad array length") + return result, position + 1 + + +def _get_binary( + data: Any, _view: Any, position: int, obj_end: int, opts: CodecOptions[Any], dummy1: Any +) -> Tuple[Union[Binary, uuid.UUID], int]: + """Decode a BSON binary to bson.binary.Binary or python UUID.""" + length, subtype = _UNPACK_LENGTH_SUBTYPE_FROM(data, position) + position += 5 + if subtype == 2: + length2 = _UNPACK_INT_FROM(data, position)[0] + position += 4 + if length2 != length - 4: + raise InvalidBSON("invalid binary (st 2) - lengths don't match!") + length = length2 + end = position + length + if length < 0 or end > obj_end: + raise InvalidBSON("bad binary object length") + + # Convert UUID subtypes to native UUIDs. + if subtype in ALL_UUID_SUBTYPES: + uuid_rep = opts.uuid_representation + binary_value = Binary(data[position:end], subtype) + if ( + (uuid_rep == UuidRepresentation.UNSPECIFIED) + or (subtype == UUID_SUBTYPE and uuid_rep != STANDARD) + or (subtype == OLD_UUID_SUBTYPE and uuid_rep == STANDARD) + ): + return binary_value, end + return binary_value.as_uuid(uuid_rep), end + + # Decode subtype 0 to 'bytes'. + if subtype == 0: + value = data[position:end] + else: + value = Binary(data[position:end], subtype) + + return value, end + + +def _get_oid( + data: Any, _view: Any, position: int, dummy0: Any, dummy1: Any, dummy2: Any +) -> Tuple[ObjectId, int]: + """Decode a BSON ObjectId to bson.objectid.ObjectId.""" + end = position + 12 + return ObjectId(data[position:end]), end + + +def _get_boolean( + data: Any, _view: Any, position: int, dummy0: Any, dummy1: Any, dummy2: Any +) -> Tuple[bool, int]: + """Decode a BSON true/false to python True/False.""" + end = position + 1 + boolean_byte = data[position:end] + if boolean_byte == b"\x00": + return False, end + elif boolean_byte == b"\x01": + return True, end + raise InvalidBSON("invalid boolean value: %r" % boolean_byte) + + +def _get_date( + data: Any, _view: Any, position: int, dummy0: int, opts: CodecOptions[Any], dummy1: Any +) -> Tuple[Union[datetime.datetime, DatetimeMS], int]: + """Decode a BSON datetime to python datetime.datetime.""" + return _millis_to_datetime(_UNPACK_LONG_FROM(data, position)[0], opts), position + 8 + + +def _get_code( + data: Any, view: Any, position: int, obj_end: int, opts: CodecOptions[Any], element_name: str +) -> Tuple[Code, int]: + """Decode a BSON code to bson.code.Code.""" + code, position = _get_string(data, view, position, obj_end, opts, element_name) + return Code(code), position + + +def _get_code_w_scope( + data: Any, view: Any, position: int, _obj_end: int, opts: CodecOptions[Any], element_name: str +) -> Tuple[Code, int]: + """Decode a BSON code_w_scope to bson.code.Code.""" + code_end = position + _UNPACK_INT_FROM(data, position)[0] + code, position = _get_string(data, view, position + 4, code_end, opts, element_name) + scope, position = _get_object(data, view, position, code_end, opts, element_name) + if position != code_end: + raise InvalidBSON("scope outside of javascript code boundaries") + return Code(code, scope), position + + +def _get_regex( + data: Any, view: Any, position: int, dummy0: Any, opts: CodecOptions[Any], dummy1: Any +) -> Tuple[Regex[Any], int]: + """Decode a BSON regex to bson.regex.Regex or a python pattern object.""" + pattern, position = _get_c_string(data, view, position, opts) + bson_flags, position = _get_c_string(data, view, position, opts) + bson_re = Regex(pattern, bson_flags) + return bson_re, position + + +def _get_ref( + data: Any, view: Any, position: int, obj_end: int, opts: CodecOptions[Any], element_name: str +) -> Tuple[DBRef, int]: + """Decode (deprecated) BSON DBPointer to bson.dbref.DBRef.""" + collection, position = _get_string(data, view, position, obj_end, opts, element_name) + oid, position = _get_oid(data, view, position, obj_end, opts, element_name) + return DBRef(collection, oid), position + + +def _get_timestamp( + data: Any, _view: Any, position: int, dummy0: Any, dummy1: Any, dummy2: Any +) -> Tuple[Timestamp, int]: + """Decode a BSON timestamp to bson.timestamp.Timestamp.""" + inc, timestamp = _UNPACK_TIMESTAMP_FROM(data, position) + return Timestamp(timestamp, inc), position + 8 + + +def _get_int64( + data: Any, _view: Any, position: int, dummy0: Any, dummy1: Any, dummy2: Any +) -> Tuple[Int64, int]: + """Decode a BSON int64 to bson.int64.Int64.""" + return Int64(_UNPACK_LONG_FROM(data, position)[0]), position + 8 + + +def _get_decimal128( + data: Any, _view: Any, position: int, dummy0: Any, dummy1: Any, dummy2: Any +) -> Tuple[Decimal128, int]: + """Decode a BSON decimal128 to bson.decimal128.Decimal128.""" + end = position + 16 + return Decimal128.from_bid(data[position:end]), end + + +# Each decoder function's signature is: +# - data: bytes +# - view: memoryview that references `data` +# - position: int, beginning of object in 'data' to decode +# - obj_end: int, end of object to decode in 'data' if variable-length type +# - opts: a CodecOptions +_ELEMENT_GETTER: dict[int, Callable[..., Tuple[Any, int]]] = { + ord(BSONNUM): _get_float, + ord(BSONSTR): _get_string, + ord(BSONOBJ): _get_object, + ord(BSONARR): _get_array, + ord(BSONBIN): _get_binary, + ord(BSONUND): lambda u, v, w, x, y, z: (None, w), # noqa: ARG005 # Deprecated undefined + ord(BSONOID): _get_oid, + ord(BSONBOO): _get_boolean, + ord(BSONDAT): _get_date, + ord(BSONNUL): lambda u, v, w, x, y, z: (None, w), # noqa: ARG005 + ord(BSONRGX): _get_regex, + ord(BSONREF): _get_ref, # Deprecated DBPointer + ord(BSONCOD): _get_code, + ord(BSONSYM): _get_string, # Deprecated symbol + ord(BSONCWS): _get_code_w_scope, + ord(BSONINT): _get_int, + ord(BSONTIM): _get_timestamp, + ord(BSONLON): _get_int64, + ord(BSONDEC): _get_decimal128, + ord(BSONMIN): lambda u, v, w, x, y, z: (MinKey(), w), # noqa: ARG005 + ord(BSONMAX): lambda u, v, w, x, y, z: (MaxKey(), w), # noqa: ARG005 +} + + +if _USE_C: + + def _element_to_dict( + data: Any, + view: Any, # noqa: ARG001 + position: int, + obj_end: int, + opts: CodecOptions[Any], + raw_array: bool = False, + ) -> Tuple[str, Any, int]: + return cast( + "Tuple[str, Any, int]", + _cbson._element_to_dict(data, position, obj_end, opts, raw_array), + ) + +else: + + def _element_to_dict( + data: Any, + view: Any, + position: int, + obj_end: int, + opts: CodecOptions[Any], + raw_array: bool = False, + ) -> Tuple[str, Any, int]: + """Decode a single key, value pair.""" + element_type = data[position] + position += 1 + element_name, position = _get_c_string(data, view, position, opts) + if raw_array and element_type == ord(BSONARR): + _, end = _get_object_size(data, position, len(data)) + return element_name, view[position : end + 1], end + 1 + try: + value, position = _ELEMENT_GETTER[element_type]( + data, view, position, obj_end, opts, element_name + ) + except KeyError: + _raise_unknown_type(element_type, element_name) + + if opts.type_registry._decoder_map: + custom_decoder = opts.type_registry._decoder_map.get(type(value)) + if custom_decoder is not None: + value = custom_decoder(value) + + return element_name, value, position + + +_T = TypeVar("_T", bound=MutableMapping[str, Any]) + + +def _raw_to_dict( + data: Any, + position: int, + obj_end: int, + opts: CodecOptions[RawBSONDocument], + result: _T, + raw_array: bool = False, +) -> _T: + data, view = get_data_and_view(data) + return cast( + _T, _elements_to_dict(data, view, position, obj_end, opts, result, raw_array=raw_array) + ) + + +def _elements_to_dict( + data: Any, + view: Any, + position: int, + obj_end: int, + opts: CodecOptions[Any], + result: Any = None, + raw_array: bool = False, +) -> Any: + """Decode a BSON document into result.""" + if result is None: + result = opts.document_class() + end = obj_end - 1 + while position < end: + key, value, position = _element_to_dict( + data, view, position, obj_end, opts, raw_array=raw_array + ) + result[key] = value + if position != obj_end: + raise InvalidBSON("bad object or element length") + return result + + +def _bson_to_dict(data: Any, opts: CodecOptions[_DocumentType]) -> _DocumentType: + """Decode a BSON string to document_class.""" + data, view = get_data_and_view(data) + try: + if _raw_document_class(opts.document_class): + return opts.document_class(data, opts) # type:ignore[call-arg] + _, end = _get_object_size(data, 0, len(data)) + return cast("_DocumentType", _elements_to_dict(data, view, 4, end, opts)) + except InvalidBSON: + raise + except Exception: + # Change exception type to InvalidBSON but preserve traceback. + _, exc_value, exc_tb = sys.exc_info() + raise InvalidBSON(str(exc_value)).with_traceback(exc_tb) from None + + +if _USE_C: + _bson_to_dict = _cbson._bson_to_dict + + +_PACK_FLOAT = struct.Struct(" Generator[bytes, None, None]: + """Generate "keys" for encoded lists in the sequence + b"0\x00", b"1\x00", b"2\x00", ... + + The first 1000 keys are returned from a pre-built cache. All + subsequent keys are generated on the fly. + """ + yield from _LIST_NAMES + + counter = itertools.count(1000) + while True: + yield (str(next(counter)) + "\x00").encode("utf8") + + +def _make_c_string_check(string: Union[str, bytes]) -> bytes: + """Make a 'C' string, checking for embedded NUL characters.""" + if isinstance(string, bytes): + if b"\x00" in string: + raise InvalidDocument("BSON keys / regex patterns must not contain a NUL character") + try: + _utf_8_decode(string, None, True) + return string + b"\x00" + except UnicodeError: + raise InvalidStringData( + "strings in documents must be valid UTF-8: %r" % string + ) from None + else: + if "\x00" in string: + raise InvalidDocument("BSON keys / regex patterns must not contain a NUL character") + return _utf_8_encode(string)[0] + b"\x00" + + +def _make_c_string(string: Union[str, bytes]) -> bytes: + """Make a 'C' string.""" + if isinstance(string, bytes): + try: + _utf_8_decode(string, None, True) + return string + b"\x00" + except UnicodeError: + raise InvalidStringData( + "strings in documents must be valid UTF-8: %r" % string + ) from None + else: + return _utf_8_encode(string)[0] + b"\x00" + + +def _make_name(string: str) -> bytes: + """Make a 'C' string suitable for a BSON key.""" + if "\x00" in string: + raise InvalidDocument("BSON keys must not contain a NUL character") + return _utf_8_encode(string)[0] + b"\x00" + + +def _encode_float(name: bytes, value: float, dummy0: Any, dummy1: Any) -> bytes: + """Encode a float.""" + return b"\x01" + name + _PACK_FLOAT(value) + + +def _encode_bytes(name: bytes, value: bytes, dummy0: Any, dummy1: Any) -> bytes: + """Encode a python bytes.""" + # Python3 special case. Store 'bytes' as BSON binary subtype 0. + return b"\x05" + name + _PACK_INT(len(value)) + b"\x00" + value + + +def _encode_mapping(name: bytes, value: Any, check_keys: bool, opts: CodecOptions[Any]) -> bytes: + """Encode a mapping type.""" + if _raw_document_class(value): + return b"\x03" + name + cast(bytes, value.raw) + data = b"".join([_element_to_bson(key, val, check_keys, opts) for key, val in value.items()]) + return b"\x03" + name + _PACK_INT(len(data) + 5) + data + b"\x00" + + +def _encode_dbref(name: bytes, value: DBRef, check_keys: bool, opts: CodecOptions[Any]) -> bytes: + """Encode bson.dbref.DBRef.""" + buf = bytearray(b"\x03" + name + b"\x00\x00\x00\x00") + begin = len(buf) - 4 + + buf += _name_value_to_bson(b"$ref\x00", value.collection, check_keys, opts) + buf += _name_value_to_bson(b"$id\x00", value.id, check_keys, opts) + if value.database is not None: + buf += _name_value_to_bson(b"$db\x00", value.database, check_keys, opts) + for key, val in value._DBRef__kwargs.items(): + buf += _element_to_bson(key, val, check_keys, opts) + + buf += b"\x00" + buf[begin : begin + 4] = _PACK_INT(len(buf) - begin) + return bytes(buf) + + +def _encode_list( + name: bytes, value: Sequence[Any], check_keys: bool, opts: CodecOptions[Any] +) -> bytes: + """Encode a list/tuple.""" + lname = gen_list_name() + data = b"".join([_name_value_to_bson(next(lname), item, check_keys, opts) for item in value]) + return b"\x04" + name + _PACK_INT(len(data) + 5) + data + b"\x00" + + +def _encode_text(name: bytes, value: str, dummy0: Any, dummy1: Any) -> bytes: + """Encode a python str.""" + bvalue = _utf_8_encode(value)[0] + return b"\x02" + name + _PACK_INT(len(bvalue) + 1) + bvalue + b"\x00" + + +def _encode_binary(name: bytes, value: Binary, dummy0: Any, dummy1: Any) -> bytes: + """Encode bson.binary.Binary.""" + subtype = value.subtype + if subtype == 2: + value = _PACK_INT(len(value)) + value # type: ignore + return b"\x05" + name + _PACK_LENGTH_SUBTYPE(len(value), subtype) + value + + +def _encode_uuid(name: bytes, value: uuid.UUID, dummy: Any, opts: CodecOptions[Any]) -> bytes: + """Encode uuid.UUID.""" + uuid_representation = opts.uuid_representation + binval = Binary.from_uuid(value, uuid_representation=uuid_representation) + return _encode_binary(name, binval, dummy, opts) + + +def _encode_objectid(name: bytes, value: ObjectId, dummy: Any, dummy1: Any) -> bytes: + """Encode bson.objectid.ObjectId.""" + return b"\x07" + name + value.binary + + +def _encode_bool(name: bytes, value: bool, dummy0: Any, dummy1: Any) -> bytes: + """Encode a python boolean (True/False).""" + return b"\x08" + name + (value and b"\x01" or b"\x00") + + +def _encode_datetime(name: bytes, value: datetime.datetime, dummy0: Any, dummy1: Any) -> bytes: + """Encode datetime.datetime.""" + millis = _datetime_to_millis(value) + return b"\x09" + name + _PACK_LONG(millis) + + +def _encode_datetime_ms(name: bytes, value: DatetimeMS, dummy0: Any, dummy1: Any) -> bytes: + """Encode datetime.datetime.""" + millis = int(value) + return b"\x09" + name + _PACK_LONG(millis) + + +def _encode_none(name: bytes, dummy0: Any, dummy1: Any, dummy2: Any) -> bytes: + """Encode python None.""" + return b"\x0A" + name + + +def _encode_regex(name: bytes, value: Regex[Any], dummy0: Any, dummy1: Any) -> bytes: + """Encode a python regex or bson.regex.Regex.""" + flags = value.flags + # Python 3 common case + if flags == re.UNICODE: + return b"\x0B" + name + _make_c_string_check(value.pattern) + b"u\x00" + elif flags == 0: + return b"\x0B" + name + _make_c_string_check(value.pattern) + b"\x00" + else: + sflags = b"" + if flags & re.IGNORECASE: + sflags += b"i" + if flags & re.LOCALE: + sflags += b"l" + if flags & re.MULTILINE: + sflags += b"m" + if flags & re.DOTALL: + sflags += b"s" + if flags & re.UNICODE: + sflags += b"u" + if flags & re.VERBOSE: + sflags += b"x" + sflags += b"\x00" + return b"\x0B" + name + _make_c_string_check(value.pattern) + sflags + + +def _encode_code(name: bytes, value: Code, dummy: Any, opts: CodecOptions[Any]) -> bytes: + """Encode bson.code.Code.""" + cstring = _make_c_string(value) + cstrlen = len(cstring) + if value.scope is None: + return b"\x0D" + name + _PACK_INT(cstrlen) + cstring + scope = _dict_to_bson(value.scope, False, opts, False) + full_length = _PACK_INT(8 + cstrlen + len(scope)) + return b"\x0F" + name + full_length + _PACK_INT(cstrlen) + cstring + scope + + +def _encode_int(name: bytes, value: int, dummy0: Any, dummy1: Any) -> bytes: + """Encode a python int.""" + if -2147483648 <= value <= 2147483647: + return b"\x10" + name + _PACK_INT(value) + else: + try: + return b"\x12" + name + _PACK_LONG(value) + except struct.error: + raise OverflowError("BSON can only handle up to 8-byte ints") from None + + +def _encode_timestamp(name: bytes, value: Any, dummy0: Any, dummy1: Any) -> bytes: + """Encode bson.timestamp.Timestamp.""" + return b"\x11" + name + _PACK_TIMESTAMP(value.inc, value.time) + + +def _encode_long(name: bytes, value: Any, dummy0: Any, dummy1: Any) -> bytes: + """Encode a bson.int64.Int64.""" + try: + return b"\x12" + name + _PACK_LONG(value) + except struct.error: + raise OverflowError("BSON can only handle up to 8-byte ints") from None + + +def _encode_decimal128(name: bytes, value: Decimal128, dummy0: Any, dummy1: Any) -> bytes: + """Encode bson.decimal128.Decimal128.""" + return b"\x13" + name + value.bid + + +def _encode_minkey(name: bytes, dummy0: Any, dummy1: Any, dummy2: Any) -> bytes: + """Encode bson.min_key.MinKey.""" + return b"\xFF" + name + + +def _encode_maxkey(name: bytes, dummy0: Any, dummy1: Any, dummy2: Any) -> bytes: + """Encode bson.max_key.MaxKey.""" + return b"\x7F" + name + + +# Each encoder function's signature is: +# - name: utf-8 bytes +# - value: a Python data type, e.g. a Python int for _encode_int +# - check_keys: bool, whether to check for invalid names +# - opts: a CodecOptions +_ENCODERS = { + bool: _encode_bool, + bytes: _encode_bytes, + datetime.datetime: _encode_datetime, + DatetimeMS: _encode_datetime_ms, + dict: _encode_mapping, + float: _encode_float, + int: _encode_int, + list: _encode_list, + str: _encode_text, + tuple: _encode_list, + type(None): _encode_none, + uuid.UUID: _encode_uuid, + Binary: _encode_binary, + Int64: _encode_long, + Code: _encode_code, + DBRef: _encode_dbref, + MaxKey: _encode_maxkey, + MinKey: _encode_minkey, + ObjectId: _encode_objectid, + Regex: _encode_regex, + RE_TYPE: _encode_regex, + SON: _encode_mapping, + Timestamp: _encode_timestamp, + Decimal128: _encode_decimal128, + # Special case. This will never be looked up directly. + _abc.Mapping: _encode_mapping, +} + +# Map each _type_marker to its encoder for faster lookup. +_MARKERS = {} +for _typ in _ENCODERS: + if hasattr(_typ, "_type_marker"): + _MARKERS[_typ._type_marker] = _ENCODERS[_typ] + + +_BUILT_IN_TYPES = tuple(t for t in _ENCODERS) + + +def _name_value_to_bson( + name: bytes, + value: Any, + check_keys: bool, + opts: CodecOptions[Any], + in_custom_call: bool = False, + in_fallback_call: bool = False, +) -> bytes: + """Encode a single name, value pair.""" + + was_integer_overflow = False + + # First see if the type is already cached. KeyError will only ever + # happen once per subtype. + try: + return _ENCODERS[type(value)](name, value, check_keys, opts) # type: ignore + except KeyError: + pass + except OverflowError: + if not isinstance(value, int): + raise + + # Give the fallback_encoder a chance + was_integer_overflow = True + + # Second, fall back to trying _type_marker. This has to be done + # before the loop below since users could subclass one of our + # custom types that subclasses a python built-in (e.g. Binary) + marker = getattr(value, "_type_marker", None) + if isinstance(marker, int) and marker in _MARKERS: + func = _MARKERS[marker] + # Cache this type for faster subsequent lookup. + _ENCODERS[type(value)] = func + return func(name, value, check_keys, opts) # type: ignore + + # Third, check if a type encoder is registered for this type. + # Note that subtypes of registered custom types are not auto-encoded. + if not in_custom_call and opts.type_registry._encoder_map: + custom_encoder = opts.type_registry._encoder_map.get(type(value)) + if custom_encoder is not None: + return _name_value_to_bson( + name, custom_encoder(value), check_keys, opts, in_custom_call=True + ) + + # Fourth, test each base type. This will only happen once for + # a subtype of a supported base type. Unlike in the C-extensions, this + # is done after trying the custom type encoder because checking for each + # subtype is expensive. + for base in _BUILT_IN_TYPES: + if not was_integer_overflow and isinstance(value, base): + func = _ENCODERS[base] + # Cache this type for faster subsequent lookup. + _ENCODERS[type(value)] = func + return func(name, value, check_keys, opts) # type: ignore + + # As a last resort, try using the fallback encoder, if the user has + # provided one. + fallback_encoder = opts.type_registry._fallback_encoder + if not in_fallback_call and fallback_encoder is not None: + return _name_value_to_bson( + name, fallback_encoder(value), check_keys, opts, in_fallback_call=True + ) + + if was_integer_overflow: + raise OverflowError("BSON can only handle up to 8-byte ints") + raise InvalidDocument(f"cannot encode object: {value!r}, of type: {type(value)!r}") + + +def _element_to_bson(key: Any, value: Any, check_keys: bool, opts: CodecOptions[Any]) -> bytes: + """Encode a single key, value pair.""" + if not isinstance(key, str): + raise InvalidDocument(f"documents must have only string keys, key was {key!r}") + if check_keys: + if key.startswith("$"): + raise InvalidDocument(f"key {key!r} must not start with '$'") + if "." in key: + raise InvalidDocument(f"key {key!r} must not contain '.'") + + name = _make_name(key) + return _name_value_to_bson(name, value, check_keys, opts) + + +def _dict_to_bson( + doc: Any, check_keys: bool, opts: CodecOptions[Any], top_level: bool = True +) -> bytes: + """Encode a document to BSON.""" + if _raw_document_class(doc): + return cast(bytes, doc.raw) + try: + elements = [] + if top_level and "_id" in doc: + elements.append(_name_value_to_bson(b"_id\x00", doc["_id"], check_keys, opts)) + for key, value in doc.items(): + if not top_level or key != "_id": + elements.append(_element_to_bson(key, value, check_keys, opts)) + except AttributeError: + raise TypeError(f"encoder expected a mapping type but got: {doc!r}") from None + + encoded = b"".join(elements) + return _PACK_INT(len(encoded) + 5) + encoded + b"\x00" + + +if _USE_C: + _dict_to_bson = _cbson._dict_to_bson + + +_CODEC_OPTIONS_TYPE_ERROR = TypeError("codec_options must be an instance of CodecOptions") + + +def encode( + document: Mapping[str, Any], + check_keys: bool = False, + codec_options: CodecOptions[Any] = DEFAULT_CODEC_OPTIONS, +) -> bytes: + """Encode a document to BSON. + + A document can be any mapping type (like :class:`dict`). + + Raises :class:`TypeError` if `document` is not a mapping type, + or contains keys that are not instances of :class:`str`. Raises + :class:`~bson.errors.InvalidDocument` if `document` cannot be + converted to :class:`BSON`. + + :param document: mapping type representing a document + :param check_keys: check if keys start with '$' or + contain '.', raising :class:`~bson.errors.InvalidDocument` in + either case + :param codec_options: An instance of + :class:`~bson.codec_options.CodecOptions`. + + .. versionadded:: 3.9 + """ + if not isinstance(codec_options, CodecOptions): + raise _CODEC_OPTIONS_TYPE_ERROR + + return _dict_to_bson(document, check_keys, codec_options) + + +@overload +def decode(data: _ReadableBuffer, codec_options: None = None) -> dict[str, Any]: + ... + + +@overload +def decode(data: _ReadableBuffer, codec_options: CodecOptions[_DocumentType]) -> _DocumentType: + ... + + +def decode( + data: _ReadableBuffer, codec_options: Optional[CodecOptions[_DocumentType]] = None +) -> Union[dict[str, Any], _DocumentType]: + """Decode BSON to a document. + + By default, returns a BSON document represented as a Python + :class:`dict`. To use a different :class:`MutableMapping` class, + configure a :class:`~bson.codec_options.CodecOptions`:: + + >>> import collections # From Python standard library. + >>> import bson + >>> from bson.codec_options import CodecOptions + >>> data = bson.encode({'a': 1}) + >>> decoded_doc = bson.decode(data) + + >>> options = CodecOptions(document_class=collections.OrderedDict) + >>> decoded_doc = bson.decode(data, codec_options=options) + >>> type(decoded_doc) + + + :param data: the BSON to decode. Any bytes-like object that implements + the buffer protocol. + :param codec_options: An instance of + :class:`~bson.codec_options.CodecOptions`. + + .. versionadded:: 3.9 + """ + opts: CodecOptions[Any] = codec_options or DEFAULT_CODEC_OPTIONS + if not isinstance(opts, CodecOptions): + raise _CODEC_OPTIONS_TYPE_ERROR + + return cast("Union[dict[str, Any], _DocumentType]", _bson_to_dict(data, opts)) + + +def _decode_all(data: _ReadableBuffer, opts: CodecOptions[_DocumentType]) -> list[_DocumentType]: + """Decode a BSON data to multiple documents.""" + data, view = get_data_and_view(data) + data_len = len(data) + docs: list[_DocumentType] = [] + position = 0 + end = data_len - 1 + use_raw = _raw_document_class(opts.document_class) + try: + while position < end: + obj_size = _UNPACK_INT_FROM(data, position)[0] + if data_len - position < obj_size: + raise InvalidBSON("invalid object size") + obj_end = position + obj_size - 1 + if data[obj_end] != 0: + raise InvalidBSON("bad eoo") + if use_raw: + docs.append(opts.document_class(data[position : obj_end + 1], opts)) # type: ignore + else: + docs.append(_elements_to_dict(data, view, position + 4, obj_end, opts)) + position += obj_size + return docs + except InvalidBSON: + raise + except Exception: + # Change exception type to InvalidBSON but preserve traceback. + _, exc_value, exc_tb = sys.exc_info() + raise InvalidBSON(str(exc_value)).with_traceback(exc_tb) from None + + +if _USE_C: + _decode_all = _cbson._decode_all + + +@overload +def decode_all(data: _ReadableBuffer, codec_options: None = None) -> list[dict[str, Any]]: + ... + + +@overload +def decode_all( + data: _ReadableBuffer, codec_options: CodecOptions[_DocumentType] +) -> list[_DocumentType]: + ... + + +def decode_all( + data: _ReadableBuffer, codec_options: Optional[CodecOptions[_DocumentType]] = None +) -> Union[list[dict[str, Any]], list[_DocumentType]]: + """Decode BSON data to multiple documents. + + `data` must be a bytes-like object implementing the buffer protocol that + provides concatenated, valid, BSON-encoded documents. + + :param data: BSON data + :param codec_options: An instance of + :class:`~bson.codec_options.CodecOptions`. + + .. versionchanged:: 3.9 + Supports bytes-like objects that implement the buffer protocol. + + .. versionchanged:: 3.0 + Removed `compile_re` option: PyMongo now always represents BSON regular + expressions as :class:`~bson.regex.Regex` objects. Use + :meth:`~bson.regex.Regex.try_compile` to attempt to convert from a + BSON regular expression to a Python regular expression object. + + Replaced `as_class`, `tz_aware`, and `uuid_subtype` options with + `codec_options`. + """ + if codec_options is None: + return _decode_all(data, DEFAULT_CODEC_OPTIONS) + + if not isinstance(codec_options, CodecOptions): + raise _CODEC_OPTIONS_TYPE_ERROR + + return _decode_all(data, codec_options) + + +def _decode_selective( + rawdoc: Any, fields: Any, codec_options: CodecOptions[_DocumentType] +) -> _DocumentType: + if _raw_document_class(codec_options.document_class): + # If document_class is RawBSONDocument, use vanilla dictionary for + # decoding command response. + doc: _DocumentType = {} # type:ignore[assignment] + else: + # Else, use the specified document_class. + doc = codec_options.document_class() + for key, value in rawdoc.items(): + if key in fields: + if fields[key] == 1: + doc[key] = _bson_to_dict(rawdoc.raw, codec_options)[key] # type:ignore[index] + else: + doc[key] = _decode_selective( # type:ignore[index] + value, fields[key], codec_options + ) + else: + doc[key] = value # type:ignore[index] + return doc + + +def _array_of_documents_to_buffer(view: memoryview) -> bytes: + # Extract the raw bytes of each document. + position = 0 + _, end = _get_object_size(view, position, len(view)) + position += 4 + buffers: list[memoryview] = [] + append = buffers.append + while position < end - 1: + # Just skip the keys. + while view[position] != 0: + position += 1 + position += 1 + obj_size, _ = _get_object_size(view, position, end) + append(view[position : position + obj_size]) + position += obj_size + if position != end: + raise InvalidBSON("bad object or element length") + return b"".join(buffers) + + +if _USE_C: + _array_of_documents_to_buffer = _cbson._array_of_documents_to_buffer + + +def _convert_raw_document_lists_to_streams(document: Any) -> None: + """Convert raw array of documents to a stream of BSON documents.""" + cursor = document.get("cursor") + if not cursor: + return + for key in ("firstBatch", "nextBatch"): + batch = cursor.get(key) + if not batch: + continue + data = _array_of_documents_to_buffer(batch) + if data: + cursor[key] = [data] + else: + cursor[key] = [] + + +def _decode_all_selective( + data: Any, codec_options: CodecOptions[_DocumentType], fields: Any +) -> list[_DocumentType]: + """Decode BSON data to a single document while using user-provided + custom decoding logic. + + `data` must be a string representing a valid, BSON-encoded document. + + :param data: BSON data + :param codec_options: An instance of + :class:`~bson.codec_options.CodecOptions` with user-specified type + decoders. If no decoders are found, this method is the same as + ``decode_all``. + :param fields: Map of document namespaces where data that needs + to be custom decoded lives or None. For example, to custom decode a + list of objects in 'field1.subfield1', the specified value should be + ``{'field1': {'subfield1': 1}}``. If ``fields`` is an empty map or + None, this method is the same as ``decode_all``. + + :return: Single-member list containing the decoded document. + + .. versionadded:: 3.8 + """ + if not codec_options.type_registry._decoder_map: + return decode_all(data, codec_options) + + if not fields: + return decode_all(data, codec_options.with_options(type_registry=None)) + + # Decode documents for internal use. + from bson.raw_bson import RawBSONDocument + + internal_codec_options: CodecOptions[RawBSONDocument] = codec_options.with_options( + document_class=RawBSONDocument, type_registry=None + ) + _doc = _bson_to_dict(data, internal_codec_options) + return [ + _decode_selective( + _doc, + fields, + codec_options, + ) + ] + + +@overload +def decode_iter(data: bytes, codec_options: None = None) -> Iterator[dict[str, Any]]: + ... + + +@overload +def decode_iter(data: bytes, codec_options: CodecOptions[_DocumentType]) -> Iterator[_DocumentType]: + ... + + +def decode_iter( + data: bytes, codec_options: Optional[CodecOptions[_DocumentType]] = None +) -> Union[Iterator[dict[str, Any]], Iterator[_DocumentType]]: + """Decode BSON data to multiple documents as a generator. + + Works similarly to the decode_all function, but yields one document at a + time. + + `data` must be a string of concatenated, valid, BSON-encoded + documents. + + :param data: BSON data + :param codec_options: An instance of + :class:`~bson.codec_options.CodecOptions`. + + .. versionchanged:: 3.0 + Replaced `as_class`, `tz_aware`, and `uuid_subtype` options with + `codec_options`. + + .. versionadded:: 2.8 + """ + opts = codec_options or DEFAULT_CODEC_OPTIONS + if not isinstance(opts, CodecOptions): + raise _CODEC_OPTIONS_TYPE_ERROR + + position = 0 + end = len(data) - 1 + while position < end: + obj_size = _UNPACK_INT_FROM(data, position)[0] + elements = data[position : position + obj_size] + position += obj_size + + yield _bson_to_dict(elements, opts) # type:ignore[misc, type-var] + + +@overload +def decode_file_iter( + file_obj: Union[BinaryIO, IO[bytes]], codec_options: None = None +) -> Iterator[dict[str, Any]]: + ... + + +@overload +def decode_file_iter( + file_obj: Union[BinaryIO, IO[bytes]], codec_options: CodecOptions[_DocumentType] +) -> Iterator[_DocumentType]: + ... + + +def decode_file_iter( + file_obj: Union[BinaryIO, IO[bytes]], + codec_options: Optional[CodecOptions[_DocumentType]] = None, +) -> Union[Iterator[dict[str, Any]], Iterator[_DocumentType]]: + """Decode bson data from a file to multiple documents as a generator. + + Works similarly to the decode_all function, but reads from the file object + in chunks and parses bson in chunks, yielding one document at a time. + + :param file_obj: A file object containing BSON data. + :param codec_options: An instance of + :class:`~bson.codec_options.CodecOptions`. + + .. versionchanged:: 3.0 + Replaced `as_class`, `tz_aware`, and `uuid_subtype` options with + `codec_options`. + + .. versionadded:: 2.8 + """ + opts = codec_options or DEFAULT_CODEC_OPTIONS + while True: + # Read size of next object. + size_data: Any = file_obj.read(4) + if not size_data: + break # Finished with file normally. + elif len(size_data) != 4: + raise InvalidBSON("cut off in middle of objsize") + obj_size = _UNPACK_INT_FROM(size_data, 0)[0] - 4 + elements = size_data + file_obj.read(max(0, obj_size)) + yield _bson_to_dict(elements, opts) # type:ignore[type-var, arg-type, misc] + + +def is_valid(bson: bytes) -> bool: + """Check that the given string represents valid :class:`BSON` data. + + Raises :class:`TypeError` if `bson` is not an instance of + :class:`bytes`. Returns ``True`` + if `bson` is valid :class:`BSON`, ``False`` otherwise. + + :param bson: the data to be validated + """ + if not isinstance(bson, bytes): + raise TypeError("BSON data must be an instance of a subclass of bytes") + + try: + _bson_to_dict(bson, DEFAULT_CODEC_OPTIONS) + return True + except Exception: + return False + + +class BSON(bytes): + """BSON (Binary JSON) data. + + .. warning:: Using this class to encode and decode BSON adds a performance + cost. For better performance use the module level functions + :func:`encode` and :func:`decode` instead. + """ + + @classmethod + def encode( + cls: Type[BSON], + document: Mapping[str, Any], + check_keys: bool = False, + codec_options: CodecOptions[Any] = DEFAULT_CODEC_OPTIONS, + ) -> BSON: + """Encode a document to a new :class:`BSON` instance. + + A document can be any mapping type (like :class:`dict`). + + Raises :class:`TypeError` if `document` is not a mapping type, + or contains keys that are not instances of + :class:`str'. Raises :class:`~bson.errors.InvalidDocument` + if `document` cannot be converted to :class:`BSON`. + + :param document: mapping type representing a document + :param check_keys: check if keys start with '$' or + contain '.', raising :class:`~bson.errors.InvalidDocument` in + either case + :param codec_options: An instance of + :class:`~bson.codec_options.CodecOptions`. + + .. versionchanged:: 3.0 + Replaced `uuid_subtype` option with `codec_options`. + """ + return cls(encode(document, check_keys, codec_options)) + + def decode( # type:ignore[override] + self, codec_options: CodecOptions[Any] = DEFAULT_CODEC_OPTIONS + ) -> dict[str, Any]: + """Decode this BSON data. + + By default, returns a BSON document represented as a Python + :class:`dict`. To use a different :class:`MutableMapping` class, + configure a :class:`~bson.codec_options.CodecOptions`:: + + >>> import collections # From Python standard library. + >>> import bson + >>> from bson.codec_options import CodecOptions + >>> data = bson.BSON.encode({'a': 1}) + >>> decoded_doc = bson.BSON(data).decode() + + >>> options = CodecOptions(document_class=collections.OrderedDict) + >>> decoded_doc = bson.BSON(data).decode(codec_options=options) + >>> type(decoded_doc) + + + :param codec_options: An instance of + :class:`~bson.codec_options.CodecOptions`. + + .. versionchanged:: 3.0 + Removed `compile_re` option: PyMongo now always represents BSON + regular expressions as :class:`~bson.regex.Regex` objects. Use + :meth:`~bson.regex.Regex.try_compile` to attempt to convert from a + BSON regular expression to a Python regular expression object. + + Replaced `as_class`, `tz_aware`, and `uuid_subtype` options with + `codec_options`. + """ + return decode(self, codec_options) + + +def has_c() -> bool: + """Is the C extension installed?""" + return _USE_C + + +def _after_fork() -> None: + """Releases the ObjectID lock child.""" + if ObjectId._inc_lock.locked(): + ObjectId._inc_lock.release() + + +if hasattr(os, "register_at_fork"): + # This will run in the same thread as the fork was called. + # If we fork in a critical region on the same thread, it should break. + # This is fine since we would never call fork directly from a critical region. + os.register_at_fork(after_in_child=_after_fork) diff --git a/venv/Lib/site-packages/bson/__pycache__/__init__.cpython-312.pyc b/venv/Lib/site-packages/bson/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..fc6036b68bad732f462088e5a70cb5874b2302f3 GIT binary patch literal 57846 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 literal 0 HcmV?d00001 diff --git a/venv/Lib/site-packages/bson/__pycache__/_helpers.cpython-312.pyc b/venv/Lib/site-packages/bson/__pycache__/_helpers.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..fdd80c66b00b9ea55fe634fa9eb96fa8d45726bf GIT binary patch literal 1522 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 w0@{Jg4&bs4xP-ze>1+;;~DS0 z=1`4V5aMdd{60 z$7ZwYM7eW+&OP@#-}$-YKMxEf2t2oA7t4PcAmpDo2%o6mk=EaW&I-{f1yb!PU7y7G-LP8=D@&?hPw}>7yk`HBHXCbxNmyI`$ zO*@XoY1v_v88v2b!*)&k8huln*QQF$thu!0=ms@y`t7NUley1ucUJO}nr%C-=9-Sp zVc7RQtyY75*{COGVdh8MkL!?5I^@3#RMX?-5XE5slL35<+f z0@Dhz9w{igY(#bCG_3X$eV`xO-ambd-ghfnh#7r)T94mS9tPSAaXq8=-%<+w`kdYIAnGi;r zy@*>xh+)jR70vC^XA3o5;JRQ3_U3CE)2bpU^YknQBGJB*W4oGZZx*?(gM1kCnpM|a zLq`iW@J_{G;xP1SmzOAp0@!H_iY(1_4F;TYV@ym4cY!lQPZy1nQw7%Dipe|bPz3~0 z9yYUl;NhHD4ZsU6`7Yafa}VwJ4}z>)sFNXI6b6o!1F2NSqjV`D+c&=+$d{x%LPLm8U|sGPJ>nGWYB7vK%G`bHo%7+JU&ba zGN)t;b{9;y0zj^r^M)Ogq!q#cJp^1|fSBU>z{)V5{NU9rEzi)U^Ce=)oO_jCm+ z76d5#_h1b3O0)FYJF{U@eg=h{H1>#kcO@u|SAh313dQm27{x5V>ZMeS6jc=@PBJX^B{`^H z_`)KZBt%;Ot%SwVh1P$Xk{b#84}%_0y1{J$#Z7t`$iCYpIoB8N*?2ceQwlY_qx8dQ zF21oE_L_>?(cO&Pe6tHl#N84~g?f6BL^hB#l{s=LUP_aMbK*lZf5d zjP&4=+40Ra-Q2zT{H7M_>8o?-(b(b(tb&{s%c+0OQR2@@)^f0cML?DG0M7mwGcs?NH zpUde2=Ti?HN`^Mc9_mtK2E6Yk-k^AY|&b>d+|0KUW{@z48PEv!d z4R&T;g!^#)L%2BpJYbwo&Pj* z`03sQA1EKD{*fH~Dyi2WXO4jW%?+b%yEk)SPg5*=2t4@E&*e;GO}q)F!B{o*qPpJa_=Q{q#g>!SB> zSghfl&k*?J2);3LSxJP-1Btx^`Zyj$8J;4dp^*^8i;$YebL{*h%QX_p0Qgh9<$Uhwq}8Bl|R z8Cd6-V@u>oa@z;SPp2Oz_pc4_Sedvpaewrl{)=pDXzW+pez*JAyZ-dFF zhAy;H7uM3lf+qAvMdKA!?I8>g7IA?XF^HQIf=>bb2h!vN!uI3IFTk_mYV5$nK?#M> zEb!aV_Pbxd^I9uSTX8B*W(Q#*n_wgGY9z2aUO~&Qvh6sL@vmZW1^d%j@t*Gtf1!N& zlD^7zfP}b|y%;mxIFL<_*+A*YV5kfytJxz{mz0A$9mz#YYe{mnY$*^dT}ZG&GncZ-3Ef& zURwIja%olB@l27T`+Q&sXEHm$j>8+7Dc6BxV1{9#@X{s6vJCN)h3B+lN&KQ@!?+X! zhTubAxQ4}GhG8Z#{B98y!OwwS2A;=H`^=wV-vBwRbsQ;!GAv1-?;+Buzmfg_Cdd9x WPJK#FJ&R3B(!S*r{~`DmaQ_FWLGuX! literal 0 HcmV?d00001 diff --git a/venv/Lib/site-packages/bson/__pycache__/codec_options.cpython-312.pyc b/venv/Lib/site-packages/bson/__pycache__/codec_options.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f0a4340ad075c504b6fdf954bce74be636516a48 GIT binary patch literal 21126 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 literal 0 HcmV?d00001 diff --git a/venv/Lib/site-packages/bson/__pycache__/errors.cpython-312.pyc b/venv/Lib/site-packages/bson/__pycache__/errors.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e0807673ff3afaa9e7a35a5916c2b66025c58f59 GIT binary patch literal 1438 zcmb7^&2AGh5XbFonq-@XQXvEq5?EA1$|2#3_>@Y&QlUgpdWj;foL#3`*x14LwoQ(x z7jC(gSExAi6udzsPMo-bS}r}|@g`}I+KTL9cINL{@6Y40zZDAe$UL7iwbs`(LO&oF zKXZwLPd*yw2nDE)0vx0w+)33_7^Tn#3Y z@i0$07n<69KLJcE9=%Sk8|MfyRL5p@sQ}lVUaIH}U6^i7N+J8pD^xM!M^q`&5|Yp; zN`A&BDCO#LdYJP(PCLx=hV!1+5kVY*zuq2>_fOynNi)QB0~v zH&!faXtB>#JM4O@LnE<B^CLCUVJdQ{5E$T!6f&G)^qR*dF71jaOl_p+e0{ql&0T zQwjc%!HQJ5+mo=h*4!|Tg6I$p(!sKHTP)*BKnE`}67z5hb3{rOFM7ZA6$fxi+lgnnr^(rVE^nI|;2u%0 zjrrwZjDO!nxbz*}Kh0oV9=NB-yiRi%Z{k4_f=vw37XEC9wro_xgAm4QHd@C6_m7Fz HvBm!jAz)a6 literal 0 HcmV?d00001 diff --git a/venv/Lib/site-packages/bson/__pycache__/int64.cpython-312.pyc b/venv/Lib/site-packages/bson/__pycache__/int64.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..bdd7082165b144b85222074299964de3c032e659 GIT binary patch literal 1220 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ⅆD565eMyu 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 S@^57MZ{rGG{gVKRqy7W!RnKk! literal 0 HcmV?d00001 diff --git a/venv/Lib/site-packages/bson/__pycache__/min_key.cpython-312.pyc b/venv/Lib/site-packages/bson/__pycache__/min_key.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9fddc91c08744b4c9373a1fb9b562b98da863d14 GIT binary patch literal 2188 zcmaJ?&2Jk;6rWkIH%?+_lh{d{h`_cbO;#j9NJSvNq(TKkv__)9!LqbkdnWdh^{zX! zE^?GZ4n4pPPRSq8i`74ci%|}g6-bp3hu#WCLh1$H+wn&nr`f~%+Bf@t^LuY*f6C=% z2t3~#pYL8a2>An@!J{jK_HKZ2L>MU%Mw#Z*hE~!j+9^NP&`UZc8u^4U{Q+U=N19qy zO1BMrYX8O;yeYWkLF7hW7+AZZu%bG*?u0=ty!D~w1rZm4>sxoc;4|K~qIQ$-JjGAg zG@f;XAXEz_xT%{#yQ;pfWWN;c)xkd^oRlbl)EF(Lm{!tRs+49Xz|uUKLdJ(<=HgoS@35-XF<<`&Otm2dX5!X4z$^xvzKB+f!$xba2Fl&^gqB= zrKcdHog#$xfNMbPk-MZri((4A4vlrm{auMU*7Np9@07oiT*z{>1>tfOejCWT*K}mV z^_@yfdI6Vm7~Zbd-KY{qqX6dl+k?bye0QG*_sh4vN?Cdl-)_3qJ-5bXxgx{h^@bNX zd%V5VY{yy0sqshx@!WCt^J=p@r-Ls6M2Vme$zyZjyXhX7q}p!>+R2k_6o=@Dw28nu zOEzk2A`5n$PAW)TWmzJJA*d|Rt2_$60%+INPaU{j@KV9pO~B7 z%%(y(L;(qaP9fX^V+^-E#>=fe7ldA*ytK1nA6O6Z&eXZx`!&;3- z`)pBvyePV_VbT7;nEA<4!-07~r_+M`Qa&cLyOXf&j|%YsdWI*C+X{ zuDR9CY+Zl{Cu(dwUOd3!g=lKygrNHdB40rf% zK~*?C{O48J&!`dIv{k8dW?8^bWNemI;H}fuR+4H(I3R}LQA-2@TWjlL0aRiOJaMsJ z3-gzzbC1nG!e`;xdXg6nMXhYbfxbiVYMt3Y=rb54J5i<;hKz zh$RTgn;;&LU-gYA^NWX{KFKd1-tOr%z1TBoI)}nWv4P?WiUNvl6v>OFf52KzNKC$2 zv<-pn#GUQ7fKb?(z{@K(;O6mynqmfT%QLv1XWmm(d?4P2x7dHV3gTIcQu^1+gueSb Tx%wwr{@1uhSN|p;lB)j$PFv5; literal 0 HcmV?d00001 diff --git a/venv/Lib/site-packages/bson/__pycache__/objectid.cpython-312.pyc b/venv/Lib/site-packages/bson/__pycache__/objectid.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..817704d8f7276a86dc332dbeec3d22bf5e9fd7e6 GIT binary patch literal 11715 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 KlB1Hb?*9N9kE*=@ literal 0 HcmV?d00001 diff --git a/venv/Lib/site-packages/bson/__pycache__/raw_bson.cpython-312.pyc b/venv/Lib/site-packages/bson/__pycache__/raw_bson.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..fd4f7e5cff95c0567e715ca14412eabb62c2f0a2 GIT binary patch literal 8556 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 literal 0 HcmV?d00001 diff --git a/venv/Lib/site-packages/bson/__pycache__/son.cpython-312.pyc b/venv/Lib/site-packages/bson/__pycache__/son.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..33dfe8de489da28f0155b8d3437c4da0d039e79b GIT binary patch literal 9474 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 literal 0 HcmV?d00001 diff --git a/venv/Lib/site-packages/bson/__pycache__/timestamp.cpython-312.pyc b/venv/Lib/site-packages/bson/__pycache__/timestamp.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..59276cf05d1203b67917a03937b4ccda3940765b GIT binary patch literal 5861 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 literal 0 HcmV?d00001 diff --git a/venv/Lib/site-packages/bson/__pycache__/tz_util.cpython-312.pyc b/venv/Lib/site-packages/bson/__pycache__/tz_util.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6fb9eed61eadc9ede4f283fe69ff2c031a3e060c GIT binary patch literal 2220 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 0) { + digit = absNum % 10ULL; + str[index++] = (char)digit + '0'; // Convert digit to character + absNum /= 10; + } + // Add minus sign if negative + if (sign == -1) { + str[index++] = '-'; + } + str[index] = '\0'; // Null terminator + // Reverse the string + int start = 0; + int end = index - 1; + while (start < end) { + char temp = str[start]; + str[start++] = str[end]; + str[end--] = temp; + } + return 0; +} + +static PyObject* _test_long_long_to_str(PyObject* self, PyObject* args) { + // Test extreme values + Py_ssize_t maxNum = PY_SSIZE_T_MAX; + Py_ssize_t minNum = PY_SSIZE_T_MIN; + Py_ssize_t num; + char str_1[BUF_SIZE]; + char str_2[BUF_SIZE]; + int res = LL2STR(str_1, (long long)minNum); + if (res == -1) { + return NULL; + } + INT2STRING(str_2, (long long)minNum); + if (strcmp(str_1, str_2) != 0) { + PyErr_Format( + PyExc_RuntimeError, + "LL2STR != INT2STRING: %s != %s", str_1, str_2); + return NULL; + } + LL2STR(str_1, (long long)maxNum); + INT2STRING(str_2, (long long)maxNum); + if (strcmp(str_1, str_2) != 0) { + PyErr_Format( + PyExc_RuntimeError, + "LL2STR != INT2STRING: %s != %s", str_1, str_2); + return NULL; + } + + // Test common values + for (num = 0; num < 10000; num++) { + char str_1[BUF_SIZE]; + char str_2[BUF_SIZE]; + LL2STR(str_1, (long long)num); + INT2STRING(str_2, (long long)num); + if (strcmp(str_1, str_2) != 0) { + PyErr_Format( + PyExc_RuntimeError, + "LL2STR != INT2STRING: %s != %s", str_1, str_2); + return NULL; + } + } + + return args; +} + +/* Get an error class from the bson.errors module. + * + * Returns a new ref */ +static PyObject* _error(char* name) { + PyObject* error; + PyObject* errors = PyImport_ImportModule("bson.errors"); + if (!errors) { + return NULL; + } + error = PyObject_GetAttrString(errors, name); + Py_DECREF(errors); + return error; +} + +/* Safely downcast from Py_ssize_t to int, setting an + * exception and returning -1 on error. */ +static int +_downcast_and_check(Py_ssize_t size, uint8_t extra) { + if (size > BSON_MAX_SIZE || ((BSON_MAX_SIZE - extra) < size)) { + PyObject* InvalidStringData = _error("InvalidStringData"); + if (InvalidStringData) { + PyErr_SetString(InvalidStringData, + "String length must be <= 2147483647"); + Py_DECREF(InvalidStringData); + } + return -1; + } + return (int)size + extra; +} + +static PyObject* elements_to_dict(PyObject* self, const char* string, + unsigned max, + const codec_options_t* options); + +static int _write_element_to_buffer(PyObject* self, buffer_t buffer, + int type_byte, PyObject* value, + unsigned char check_keys, + const codec_options_t* options, + unsigned char in_custom_call, + unsigned char in_fallback_call); + +/* Write a RawBSONDocument to the buffer. + * Returns the number of bytes written or 0 on failure. + */ +static int write_raw_doc(buffer_t buffer, PyObject* raw, PyObject* _raw); + +/* Date stuff */ +static PyObject* datetime_from_millis(long long millis) { + /* To encode a datetime instance like datetime(9999, 12, 31, 23, 59, 59, 999999) + * we follow these steps: + * 1. Calculate a timestamp in seconds: 253402300799 + * 2. Multiply that by 1000: 253402300799000 + * 3. Add in microseconds divided by 1000 253402300799999 + * + * (Note: BSON doesn't support microsecond accuracy, hence the rounding.) + * + * To decode we could do: + * 1. Get seconds: timestamp / 1000: 253402300799 + * 2. Get micros: (timestamp % 1000) * 1000: 999000 + * Resulting in datetime(9999, 12, 31, 23, 59, 59, 999000) -- the expected result + * + * Now what if the we encode (1, 1, 1, 1, 1, 1, 111111)? + * 1. and 2. gives: -62135593139000 + * 3. Gives us: -62135593138889 + * + * Now decode: + * 1. Gives us: -62135593138 + * 2. Gives us: -889000 + * Resulting in datetime(1, 1, 1, 1, 1, 2, 15888216) -- an invalid result + * + * If instead to decode we do: + * diff = ((millis % 1000) + 1000) % 1000: 111 + * seconds = (millis - diff) / 1000: -62135593139 + * micros = diff * 1000 111000 + * Resulting in datetime(1, 1, 1, 1, 1, 1, 111000) -- the expected result + */ + PyObject* datetime; + int diff = (int)(((millis % 1000) + 1000) % 1000); + int microseconds = diff * 1000; + Time64_T seconds = (millis - diff) / 1000; + struct TM timeinfo; + cbson_gmtime64_r(&seconds, &timeinfo); + + datetime = PyDateTime_FromDateAndTime(timeinfo.tm_year + 1900, + timeinfo.tm_mon + 1, + timeinfo.tm_mday, + timeinfo.tm_hour, + timeinfo.tm_min, + timeinfo.tm_sec, + microseconds); + if(!datetime) { + PyObject *etype, *evalue, *etrace; + + /* + * Calling _error clears the error state, so fetch it first. + */ + PyErr_Fetch(&etype, &evalue, &etrace); + + /* Only add addition error message on ValueError exceptions. */ + if (PyErr_GivenExceptionMatches(etype, PyExc_ValueError)) { + if (evalue) { + PyObject* err_msg = PyObject_Str(evalue); + if (err_msg) { + PyObject* appendage = PyUnicode_FromString(" (Consider Using CodecOptions(datetime_conversion=DATETIME_AUTO) or MongoClient(datetime_conversion='DATETIME_AUTO')). See: https://pymongo.readthedocs.io/en/stable/examples/datetimes.html#handling-out-of-range-datetimes"); + if (appendage) { + PyObject* msg = PyUnicode_Concat(err_msg, appendage); + if (msg) { + Py_DECREF(evalue); + evalue = msg; + } + } + Py_XDECREF(appendage); + } + Py_XDECREF(err_msg); + } + PyErr_NormalizeException(&etype, &evalue, &etrace); + } + /* Steals references to args. */ + PyErr_Restore(etype, evalue, etrace); + } + return datetime; +} + +static long long millis_from_datetime(PyObject* datetime) { + struct TM timeinfo; + long long millis; + + timeinfo.tm_year = PyDateTime_GET_YEAR(datetime) - 1900; + timeinfo.tm_mon = PyDateTime_GET_MONTH(datetime) - 1; + timeinfo.tm_mday = PyDateTime_GET_DAY(datetime); + timeinfo.tm_hour = PyDateTime_DATE_GET_HOUR(datetime); + timeinfo.tm_min = PyDateTime_DATE_GET_MINUTE(datetime); + timeinfo.tm_sec = PyDateTime_DATE_GET_SECOND(datetime); + + millis = cbson_timegm64(&timeinfo) * 1000; + millis += PyDateTime_DATE_GET_MICROSECOND(datetime) / 1000; + return millis; +} + +/* Extended-range datetime, returns a DatetimeMS object with millis */ +static PyObject* datetime_ms_from_millis(PyObject* self, long long millis){ + // Allocate a new DatetimeMS object. + struct module_state *state = GETSTATE(self); + if (!state) { + return NULL; + } + + PyObject* dt; + PyObject* ll_millis; + + if (!(ll_millis = PyLong_FromLongLong(millis))){ + return NULL; + } + dt = PyObject_CallFunctionObjArgs(state->DatetimeMS, ll_millis, NULL); + Py_DECREF(ll_millis); + return dt; +} + +/* Extended-range datetime, takes a DatetimeMS object and extracts the long long value. */ +static int millis_from_datetime_ms(PyObject* dt, long long* out){ + PyObject* ll_millis; + long long millis; + + if (!(ll_millis = PyNumber_Long(dt))){ + return 0; + } + millis = PyLong_AsLongLong(ll_millis); + Py_DECREF(ll_millis); + if (millis == -1 && PyErr_Occurred()) { /* Overflow */ + PyErr_SetString(PyExc_OverflowError, + "MongoDB datetimes can only handle up to 8-byte ints"); + return 0; + } + *out = millis; + return 1; +} + +/* Just make this compatible w/ the old API. */ +int buffer_write_bytes(buffer_t buffer, const char* data, int size) { + if (pymongo_buffer_write(buffer, data, size)) { + return 0; + } + return 1; +} + +int buffer_write_double(buffer_t buffer, double data) { + double data_le = BSON_DOUBLE_TO_LE(data); + return buffer_write_bytes(buffer, (const char*)&data_le, 8); +} + +int buffer_write_int32(buffer_t buffer, int32_t data) { + uint32_t data_le = BSON_UINT32_TO_LE(data); + return buffer_write_bytes(buffer, (const char*)&data_le, 4); +} + +int buffer_write_int64(buffer_t buffer, int64_t data) { + uint64_t data_le = BSON_UINT64_TO_LE(data); + return buffer_write_bytes(buffer, (const char*)&data_le, 8); +} + +void buffer_write_int32_at_position(buffer_t buffer, + int position, + int32_t data) { + uint32_t data_le = BSON_UINT32_TO_LE(data); + memcpy(pymongo_buffer_get_buffer(buffer) + position, &data_le, 4); +} + +static int write_unicode(buffer_t buffer, PyObject* py_string) { + int size; + const char* data; + PyObject* encoded = PyUnicode_AsUTF8String(py_string); + if (!encoded) { + return 0; + } + data = PyBytes_AS_STRING(encoded); + if (!data) + goto unicodefail; + + if ((size = _downcast_and_check(PyBytes_GET_SIZE(encoded), 1)) == -1) + goto unicodefail; + + if (!buffer_write_int32(buffer, (int32_t)size)) + goto unicodefail; + + if (!buffer_write_bytes(buffer, data, size)) + goto unicodefail; + + Py_DECREF(encoded); + return 1; + +unicodefail: + Py_DECREF(encoded); + return 0; +} + +/* returns 0 on failure */ +static int write_string(buffer_t buffer, PyObject* py_string) { + int size; + const char* data; + if (PyUnicode_Check(py_string)){ + return write_unicode(buffer, py_string); + } + data = PyBytes_AsString(py_string); + if (!data) { + return 0; + } + + if ((size = _downcast_and_check(PyBytes_Size(py_string), 1)) == -1) + return 0; + + if (!buffer_write_int32(buffer, (int32_t)size)) { + return 0; + } + if (!buffer_write_bytes(buffer, data, size)) { + return 0; + } + return 1; +} + +/* Load a Python object to cache. + * + * Returns non-zero on failure. */ +static int _load_object(PyObject** object, char* module_name, char* object_name) { + PyObject* module; + + module = PyImport_ImportModule(module_name); + if (!module) { + return 1; + } + + *object = PyObject_GetAttrString(module, object_name); + Py_DECREF(module); + + return (*object) ? 0 : 2; +} + +/* Load all Python objects to cache. + * + * Returns non-zero on failure. */ +static int _load_python_objects(PyObject* module) { + PyObject* empty_string = NULL; + PyObject* re_compile = NULL; + PyObject* compiled = NULL; + struct module_state *state = GETSTATE(module); + if (!state) { + return 1; + } + + /* Cache commonly used attribute names to improve performance. */ + if (!((state->_type_marker_str = PyUnicode_FromString("_type_marker")) && + (state->_flags_str = PyUnicode_FromString("flags")) && + (state->_pattern_str = PyUnicode_FromString("pattern")) && + (state->_encoder_map_str = PyUnicode_FromString("_encoder_map")) && + (state->_decoder_map_str = PyUnicode_FromString("_decoder_map")) && + (state->_fallback_encoder_str = PyUnicode_FromString("_fallback_encoder")) && + (state->_raw_str = PyUnicode_FromString("raw")) && + (state->_subtype_str = PyUnicode_FromString("subtype")) && + (state->_binary_str = PyUnicode_FromString("binary")) && + (state->_scope_str = PyUnicode_FromString("scope")) && + (state->_inc_str = PyUnicode_FromString("inc")) && + (state->_time_str = PyUnicode_FromString("time")) && + (state->_bid_str = PyUnicode_FromString("bid")) && + (state->_replace_str = PyUnicode_FromString("replace")) && + (state->_astimezone_str = PyUnicode_FromString("astimezone")) && + (state->_id_str = PyUnicode_FromString("_id")) && + (state->_dollar_ref_str = PyUnicode_FromString("$ref")) && + (state->_dollar_id_str = PyUnicode_FromString("$id")) && + (state->_dollar_db_str = PyUnicode_FromString("$db")) && + (state->_tzinfo_str = PyUnicode_FromString("tzinfo")) && + (state->_as_doc_str = PyUnicode_FromString("as_doc")) && + (state->_utcoffset_str = PyUnicode_FromString("utcoffset")) && + (state->_from_uuid_str = PyUnicode_FromString("from_uuid")) && + (state->_as_uuid_str = PyUnicode_FromString("as_uuid")) && + (state->_from_bid_str = PyUnicode_FromString("from_bid")))) { + return 1; + } + + if (_load_object(&state->Binary, "bson.binary", "Binary") || + _load_object(&state->Code, "bson.code", "Code") || + _load_object(&state->ObjectId, "bson.objectid", "ObjectId") || + _load_object(&state->DBRef, "bson.dbref", "DBRef") || + _load_object(&state->Timestamp, "bson.timestamp", "Timestamp") || + _load_object(&state->MinKey, "bson.min_key", "MinKey") || + _load_object(&state->MaxKey, "bson.max_key", "MaxKey") || + _load_object(&state->UTC, "bson.tz_util", "utc") || + _load_object(&state->Regex, "bson.regex", "Regex") || + _load_object(&state->BSONInt64, "bson.int64", "Int64") || + _load_object(&state->Decimal128, "bson.decimal128", "Decimal128") || + _load_object(&state->UUID, "uuid", "UUID") || + _load_object(&state->Mapping, "collections.abc", "Mapping") || + _load_object(&state->DatetimeMS, "bson.datetime_ms", "DatetimeMS") || + _load_object(&state->_min_datetime_ms, "bson.datetime_ms", "_min_datetime_ms") || + _load_object(&state->_max_datetime_ms, "bson.datetime_ms", "_max_datetime_ms")) { + return 1; + } + /* Reload our REType hack too. */ + empty_string = PyBytes_FromString(""); + if (empty_string == NULL) { + state->REType = NULL; + return 1; + } + + if (_load_object(&re_compile, "re", "compile")) { + state->REType = NULL; + Py_DECREF(empty_string); + return 1; + } + + compiled = PyObject_CallFunction(re_compile, "O", empty_string); + Py_DECREF(re_compile); + if (compiled == NULL) { + state->REType = NULL; + Py_DECREF(empty_string); + return 1; + } + Py_INCREF(Py_TYPE(compiled)); + state->REType = Py_TYPE(compiled); + Py_DECREF(empty_string); + Py_DECREF(compiled); + return 0; +} + +/* + * Get the _type_marker from an Object. + * + * Return the type marker, 0 if there is no marker, or -1 on failure. + */ +static long _type_marker(PyObject* object, PyObject* _type_marker_str) { + PyObject* type_marker = NULL; + long type = 0; + + if (PyObject_HasAttr(object, _type_marker_str)) { + type_marker = PyObject_GetAttr(object, _type_marker_str); + if (type_marker == NULL) { + return -1; + } + } + + /* + * Python objects with broken __getattr__ implementations could return + * arbitrary types for a call to PyObject_GetAttrString. For example + * pymongo.database.Database returns a new Collection instance for + * __getattr__ calls with names that don't match an existing attribute + * or method. In some cases "value" could be a subtype of something + * we know how to serialize. Make a best effort to encode these types. + */ + if (type_marker && PyLong_CheckExact(type_marker)) { + type = PyLong_AsLong(type_marker); + Py_DECREF(type_marker); + } else { + Py_XDECREF(type_marker); + } + + return type; +} + +/* Fill out a type_registry_t* from a TypeRegistry object. + * + * Return 1 on success. options->document_class is a new reference. + * Return 0 on failure. + */ +int cbson_convert_type_registry(PyObject* registry_obj, type_registry_t* registry, PyObject* _encoder_map_str, PyObject* _decoder_map_str, PyObject* _fallback_encoder_str) { + registry->encoder_map = NULL; + registry->decoder_map = NULL; + registry->fallback_encoder = NULL; + registry->registry_obj = NULL; + + registry->encoder_map = PyObject_GetAttr(registry_obj, _encoder_map_str); + if (registry->encoder_map == NULL) { + goto fail; + } + registry->is_encoder_empty = (PyDict_Size(registry->encoder_map) == 0); + + registry->decoder_map = PyObject_GetAttr(registry_obj, _decoder_map_str); + if (registry->decoder_map == NULL) { + goto fail; + } + registry->is_decoder_empty = (PyDict_Size(registry->decoder_map) == 0); + + registry->fallback_encoder = PyObject_GetAttr(registry_obj, _fallback_encoder_str); + if (registry->fallback_encoder == NULL) { + goto fail; + } + registry->has_fallback_encoder = (registry->fallback_encoder != Py_None); + + registry->registry_obj = registry_obj; + Py_INCREF(registry->registry_obj); + return 1; + +fail: + Py_XDECREF(registry->encoder_map); + Py_XDECREF(registry->decoder_map); + Py_XDECREF(registry->fallback_encoder); + return 0; +} + +/* Fill out a codec_options_t* from a CodecOptions object. + * + * Return 1 on success. options->document_class is a new reference. + * Return 0 on failure. + */ +int convert_codec_options(PyObject* self, PyObject* options_obj, codec_options_t* options) { + PyObject* type_registry_obj = NULL; + struct module_state *state = GETSTATE(self); + long type_marker; + if (!state) { + return 0; + } + + options->unicode_decode_error_handler = NULL; + + if (!PyArg_ParseTuple(options_obj, "ObbzOOb", + &options->document_class, + &options->tz_aware, + &options->uuid_rep, + &options->unicode_decode_error_handler, + &options->tzinfo, + &type_registry_obj, + &options->datetime_conversion)) { + return 0; + } + + type_marker = _type_marker(options->document_class, + state->_type_marker_str); + if (type_marker < 0) { + return 0; + } + + if (!cbson_convert_type_registry(type_registry_obj, + &options->type_registry, state->_encoder_map_str, state->_decoder_map_str, state->_fallback_encoder_str)) { + return 0; + } + + options->is_raw_bson = (101 == type_marker); + options->options_obj = options_obj; + + Py_INCREF(options->options_obj); + Py_INCREF(options->document_class); + Py_INCREF(options->tzinfo); + + return 1; +} + +void destroy_codec_options(codec_options_t* options) { + Py_CLEAR(options->document_class); + Py_CLEAR(options->tzinfo); + Py_CLEAR(options->options_obj); + Py_CLEAR(options->type_registry.registry_obj); + Py_CLEAR(options->type_registry.encoder_map); + Py_CLEAR(options->type_registry.decoder_map); + Py_CLEAR(options->type_registry.fallback_encoder); +} + +static int write_element_to_buffer(PyObject* self, buffer_t buffer, + int type_byte, PyObject* value, + unsigned char check_keys, + const codec_options_t* options, + unsigned char in_custom_call, + unsigned char in_fallback_call) { + int result = 0; + if(Py_EnterRecursiveCall(" while encoding an object to BSON ")) { + return 0; + } + result = _write_element_to_buffer(self, buffer, type_byte, + value, check_keys, options, + in_custom_call, in_fallback_call); + Py_LeaveRecursiveCall(); + return result; +} + +static void +_set_cannot_encode(PyObject* value) { + if (PyLong_Check(value)) { + if ((PyLong_AsLongLong(value) == -1) && PyErr_Occurred()) { + return PyErr_SetString(PyExc_OverflowError, + "MongoDB can only handle up to 8-byte ints"); + } + } + + PyObject* type = NULL; + PyObject* InvalidDocument = _error("InvalidDocument"); + if (InvalidDocument == NULL) { + goto error; + } + + type = PyObject_Type(value); + if (type == NULL) { + goto error; + } + PyErr_Format(InvalidDocument, "cannot encode object: %R, of type: %R", + value, type); +error: + Py_XDECREF(type); + Py_XDECREF(InvalidDocument); +} + +/* + * Encode a builtin Python regular expression or our custom Regex class. + * + * Sets exception and returns 0 on failure. + */ +static int _write_regex_to_buffer( + buffer_t buffer, int type_byte, PyObject* value, PyObject* _flags_str, PyObject* _pattern_str) { + + PyObject* py_flags; + PyObject* py_pattern; + PyObject* encoded_pattern; + PyObject* decoded_pattern; + long int_flags; + char flags[FLAGS_SIZE]; + char check_utf8 = 0; + const char* pattern_data; + int pattern_length, flags_length; + + /* + * Both the builtin re type and our Regex class have attributes + * "flags" and "pattern". + */ + py_flags = PyObject_GetAttr(value, _flags_str); + if (!py_flags) { + return 0; + } + int_flags = PyLong_AsLong(py_flags); + Py_DECREF(py_flags); + if (int_flags == -1 && PyErr_Occurred()) { + return 0; + } + py_pattern = PyObject_GetAttr(value, _pattern_str); + if (!py_pattern) { + return 0; + } + + if (PyUnicode_Check(py_pattern)) { + encoded_pattern = PyUnicode_AsUTF8String(py_pattern); + Py_DECREF(py_pattern); + if (!encoded_pattern) { + return 0; + } + } else { + encoded_pattern = py_pattern; + check_utf8 = 1; + } + + if (!(pattern_data = PyBytes_AsString(encoded_pattern))) { + Py_DECREF(encoded_pattern); + return 0; + } + if ((pattern_length = _downcast_and_check(PyBytes_Size(encoded_pattern), 0)) == -1) { + Py_DECREF(encoded_pattern); + return 0; + } + + if (strlen(pattern_data) != (size_t) pattern_length){ + PyObject* InvalidDocument = _error("InvalidDocument"); + if (InvalidDocument) { + PyErr_SetString(InvalidDocument, + "regex patterns must not contain the NULL byte"); + Py_DECREF(InvalidDocument); + } + Py_DECREF(encoded_pattern); + return 0; + } + + if (check_utf8) { + decoded_pattern = PyUnicode_DecodeUTF8(pattern_data, (Py_ssize_t) pattern_length, NULL); + if (decoded_pattern == NULL) { + PyErr_Clear(); + PyObject* InvalidStringData = _error("InvalidStringData"); + if (InvalidStringData) { + PyErr_SetString(InvalidStringData, + "regex patterns must be valid UTF-8"); + Py_DECREF(InvalidStringData); + } + Py_DECREF(encoded_pattern); + return 0; + } + Py_DECREF(decoded_pattern); + } + + if (!buffer_write_bytes(buffer, pattern_data, pattern_length + 1)) { + Py_DECREF(encoded_pattern); + return 0; + } + Py_DECREF(encoded_pattern); + + flags[0] = 0; + + if (int_flags & 2) { + STRCAT(flags, FLAGS_SIZE, "i"); + } + if (int_flags & 4) { + STRCAT(flags, FLAGS_SIZE, "l"); + } + if (int_flags & 8) { + STRCAT(flags, FLAGS_SIZE, "m"); + } + if (int_flags & 16) { + STRCAT(flags, FLAGS_SIZE, "s"); + } + if (int_flags & 32) { + STRCAT(flags, FLAGS_SIZE, "u"); + } + if (int_flags & 64) { + STRCAT(flags, FLAGS_SIZE, "x"); + } + flags_length = (int)strlen(flags) + 1; + if (!buffer_write_bytes(buffer, flags, flags_length)) { + return 0; + } + *(pymongo_buffer_get_buffer(buffer) + type_byte) = 0x0B; + return 1; +} + +/* Write a single value to the buffer (also write its type_byte, for which + * space has already been reserved. + * + * returns 0 on failure */ +static int _write_element_to_buffer(PyObject* self, buffer_t buffer, + int type_byte, PyObject* value, + unsigned char check_keys, + const codec_options_t* options, + unsigned char in_custom_call, + unsigned char in_fallback_call) { + PyObject* new_value = NULL; + int retval; + int is_list; + long type; + struct module_state *state = GETSTATE(self); + if (!state) { + return 0; + } + /* + * Use _type_marker attribute instead of PyObject_IsInstance for better perf. + */ + type = _type_marker(value, state->_type_marker_str); + if (type < 0) { + return 0; + } + + switch (type) { + case 5: + { + /* Binary */ + PyObject* subtype_object; + char subtype; + const char* data; + int size; + + *(pymongo_buffer_get_buffer(buffer) + type_byte) = 0x05; + subtype_object = PyObject_GetAttr(value, state->_subtype_str); + if (!subtype_object) { + return 0; + } + subtype = (char)PyLong_AsLong(subtype_object); + if (subtype == -1) { + Py_DECREF(subtype_object); + return 0; + } + size = _downcast_and_check(PyBytes_Size(value), 0); + if (size == -1) { + Py_DECREF(subtype_object); + return 0; + } + + Py_DECREF(subtype_object); + if (subtype == 2) { + int other_size = _downcast_and_check(PyBytes_Size(value), 4); + if (other_size == -1) + return 0; + if (!buffer_write_int32(buffer, other_size)) { + return 0; + } + if (!buffer_write_bytes(buffer, &subtype, 1)) { + return 0; + } + } + if (!buffer_write_int32(buffer, size)) { + return 0; + } + if (subtype != 2) { + if (!buffer_write_bytes(buffer, &subtype, 1)) { + return 0; + } + } + data = PyBytes_AsString(value); + if (!data) { + return 0; + } + if (!buffer_write_bytes(buffer, data, size)) { + return 0; + } + return 1; + } + case 7: + { + /* ObjectId */ + const char* data; + PyObject* pystring = PyObject_GetAttr(value, state->_binary_str); + if (!pystring) { + return 0; + } + data = PyBytes_AsString(pystring); + if (!data) { + Py_DECREF(pystring); + return 0; + } + if (!buffer_write_bytes(buffer, data, 12)) { + Py_DECREF(pystring); + return 0; + } + Py_DECREF(pystring); + *(pymongo_buffer_get_buffer(buffer) + type_byte) = 0x07; + return 1; + } + case 9: + { + /* DatetimeMS */ + long long millis; + if (!millis_from_datetime_ms(value, &millis)) { + return 0; + } + *(pymongo_buffer_get_buffer(buffer) + type_byte) = 0x09; + return buffer_write_int64(buffer, (int64_t)millis); + } + case 11: + { + /* Regex */ + return _write_regex_to_buffer(buffer, type_byte, value, state->_flags_str, state->_pattern_str); + } + case 13: + { + /* Code */ + int start_position, + length_location, + length; + + PyObject* scope = PyObject_GetAttr(value, state->_scope_str); + if (!scope) { + return 0; + } + + if (scope == Py_None) { + Py_DECREF(scope); + *(pymongo_buffer_get_buffer(buffer) + type_byte) = 0x0D; + return write_string(buffer, value); + } + + *(pymongo_buffer_get_buffer(buffer) + type_byte) = 0x0F; + + start_position = pymongo_buffer_get_position(buffer); + /* save space for length */ + length_location = pymongo_buffer_save_space(buffer, 4); + if (length_location == -1) { + Py_DECREF(scope); + return 0; + } + + if (!write_string(buffer, value)) { + Py_DECREF(scope); + return 0; + } + + if (!write_dict(self, buffer, scope, 0, options, 0)) { + Py_DECREF(scope); + return 0; + } + Py_DECREF(scope); + + length = pymongo_buffer_get_position(buffer) - start_position; + buffer_write_int32_at_position( + buffer, length_location, (int32_t)length); + return 1; + } + case 17: + { + /* Timestamp */ + PyObject* obj; + unsigned long i; + + obj = PyObject_GetAttr(value, state->_inc_str); + if (!obj) { + return 0; + } + i = PyLong_AsUnsignedLong(obj); + Py_DECREF(obj); + if (i == (unsigned long)-1 && PyErr_Occurred()) { + return 0; + } + if (!buffer_write_int32(buffer, (int32_t)i)) { + return 0; + } + + obj = PyObject_GetAttr(value, state->_time_str); + if (!obj) { + return 0; + } + i = PyLong_AsUnsignedLong(obj); + Py_DECREF(obj); + if (i == (unsigned long)-1 && PyErr_Occurred()) { + return 0; + } + if (!buffer_write_int32(buffer, (int32_t)i)) { + return 0; + } + + *(pymongo_buffer_get_buffer(buffer) + type_byte) = 0x11; + return 1; + } + case 18: + { + /* Int64 */ + const long long ll = PyLong_AsLongLong(value); + if (PyErr_Occurred()) { /* Overflow */ + PyErr_SetString(PyExc_OverflowError, + "MongoDB can only handle up to 8-byte ints"); + return 0; + } + if (!buffer_write_int64(buffer, (int64_t)ll)) { + return 0; + } + *(pymongo_buffer_get_buffer(buffer) + type_byte) = 0x12; + return 1; + } + case 19: + { + /* Decimal128 */ + const char* data; + PyObject* pystring = PyObject_GetAttr(value, state->_bid_str); + if (!pystring) { + return 0; + } + data = PyBytes_AsString(pystring); + if (!data) { + Py_DECREF(pystring); + return 0; + } + if (!buffer_write_bytes(buffer, data, 16)) { + Py_DECREF(pystring); + return 0; + } + Py_DECREF(pystring); + *(pymongo_buffer_get_buffer(buffer) + type_byte) = 0x13; + return 1; + } + case 100: + { + /* DBRef */ + PyObject* as_doc = PyObject_CallMethodObjArgs(value, state->_as_doc_str, NULL); + if (!as_doc) { + return 0; + } + if (!write_dict(self, buffer, as_doc, 0, options, 0)) { + Py_DECREF(as_doc); + return 0; + } + Py_DECREF(as_doc); + *(pymongo_buffer_get_buffer(buffer) + type_byte) = 0x03; + return 1; + } + case 101: + { + /* RawBSONDocument */ + if (!write_raw_doc(buffer, value, state->_raw_str)) { + return 0; + } + *(pymongo_buffer_get_buffer(buffer) + type_byte) = 0x03; + return 1; + } + case 255: + { + /* MinKey */ + *(pymongo_buffer_get_buffer(buffer) + type_byte) = 0xFF; + return 1; + } + case 127: + { + /* MaxKey */ + *(pymongo_buffer_get_buffer(buffer) + type_byte) = 0x7F; + return 1; + } + } + + /* No _type_marker attribute or not one of our types. */ + + if (PyBool_Check(value)) { + const char c = (value == Py_True) ? 0x01 : 0x00; + *(pymongo_buffer_get_buffer(buffer) + type_byte) = 0x08; + return buffer_write_bytes(buffer, &c, 1); + } + else if (PyLong_Check(value)) { + const long long long_long_value = PyLong_AsLongLong(value); + if (long_long_value == -1 && PyErr_Occurred()) { + /* Ignore error and give the fallback_encoder a chance. */ + PyErr_Clear(); + } else if (-2147483648LL <= long_long_value && long_long_value <= 2147483647LL) { + *(pymongo_buffer_get_buffer(buffer) + type_byte) = 0x10; + return buffer_write_int32(buffer, (int32_t)long_long_value); + } else { + *(pymongo_buffer_get_buffer(buffer) + type_byte) = 0x12; + return buffer_write_int64(buffer, (int64_t)long_long_value); + } + } else if (PyFloat_Check(value)) { + const double d = PyFloat_AsDouble(value); + *(pymongo_buffer_get_buffer(buffer) + type_byte) = 0x01; + return buffer_write_double(buffer, d); + } else if (value == Py_None) { + *(pymongo_buffer_get_buffer(buffer) + type_byte) = 0x0A; + return 1; + } else if (PyDict_Check(value)) { + *(pymongo_buffer_get_buffer(buffer) + type_byte) = 0x03; + return write_dict(self, buffer, value, check_keys, options, 0); + } else if ((is_list = PyList_Check(value)) || PyTuple_Check(value)) { + Py_ssize_t items, i; + int start_position, + length_location, + length; + char zero = 0; + + *(pymongo_buffer_get_buffer(buffer) + type_byte) = 0x04; + start_position = pymongo_buffer_get_position(buffer); + + /* save space for length */ + length_location = pymongo_buffer_save_space(buffer, 4); + if (length_location == -1) { + return 0; + } + if (is_list) { + items = PyList_Size(value); + } else { + items = PyTuple_Size(value); + } + if (items > BSON_MAX_SIZE) { + PyObject* BSONError = _error("BSONError"); + if (BSONError) { + PyErr_SetString(BSONError, + "Too many items to serialize."); + Py_DECREF(BSONError); + } + return 0; + } + for(i = 0; i < items; i++) { + int list_type_byte = pymongo_buffer_save_space(buffer, 1); + char name[BUF_SIZE]; + PyObject* item_value; + + if (list_type_byte == -1) { + return 0; + } + int res = LL2STR(name, (long long)i); + if (res == -1) { + return 0; + } + if (!buffer_write_bytes(buffer, name, (int)strlen(name) + 1)) { + return 0; + } + if (is_list) { + item_value = PyList_GET_ITEM(value, i); + } else { + item_value = PyTuple_GET_ITEM(value, i); + } + if (!item_value) { + return 0; + } + if (!write_element_to_buffer(self, buffer, list_type_byte, + item_value, check_keys, options, + 0, 0)) { + return 0; + } + } + + /* write null byte and fill in length */ + if (!buffer_write_bytes(buffer, &zero, 1)) { + return 0; + } + length = pymongo_buffer_get_position(buffer) - start_position; + buffer_write_int32_at_position( + buffer, length_location, (int32_t)length); + return 1; + /* Python3 special case. Store bytes as BSON binary subtype 0. */ + } else if (PyBytes_Check(value)) { + char subtype = 0; + int size; + const char* data = PyBytes_AS_STRING(value); + if (!data) + return 0; + if ((size = _downcast_and_check(PyBytes_GET_SIZE(value), 0)) == -1) + return 0; + *(pymongo_buffer_get_buffer(buffer) + type_byte) = 0x05; + if (!buffer_write_int32(buffer, (int32_t)size)) { + return 0; + } + if (!buffer_write_bytes(buffer, &subtype, 1)) { + return 0; + } + if (!buffer_write_bytes(buffer, data, size)) { + return 0; + } + return 1; + } else if (PyUnicode_Check(value)) { + *(pymongo_buffer_get_buffer(buffer) + type_byte) = 0x02; + return write_unicode(buffer, value); + } else if (PyDateTime_Check(value)) { + long long millis; + PyObject* utcoffset = PyObject_CallMethodObjArgs(value, state->_utcoffset_str , NULL); + if (utcoffset == NULL) + return 0; + if (utcoffset != Py_None) { + PyObject* result = PyNumber_Subtract(value, utcoffset); + Py_DECREF(utcoffset); + if (!result) { + return 0; + } + millis = millis_from_datetime(result); + Py_DECREF(result); + } else { + millis = millis_from_datetime(value); + } + *(pymongo_buffer_get_buffer(buffer) + type_byte) = 0x09; + return buffer_write_int64(buffer, (int64_t)millis); + } else if (PyObject_TypeCheck(value, state->REType)) { + return _write_regex_to_buffer(buffer, type_byte, value, state->_flags_str, state->_pattern_str); + } else if (PyObject_IsInstance(value, state->Mapping)) { + /* PyObject_IsInstance returns -1 on error */ + if (PyErr_Occurred()) { + return 0; + } + *(pymongo_buffer_get_buffer(buffer) + type_byte) = 0x03; + return write_dict(self, buffer, value, check_keys, options, 0); + } else if (PyObject_IsInstance(value, state->UUID)) { + PyObject* binary_value = NULL; + PyObject *uuid_rep_obj = NULL; + int result; + + /* PyObject_IsInstance returns -1 on error */ + if (PyErr_Occurred()) { + return 0; + } + + if (!(uuid_rep_obj = PyLong_FromLong(options->uuid_rep))) { + return 0; + } + binary_value = PyObject_CallMethodObjArgs(state->Binary, state->_from_uuid_str, value, uuid_rep_obj, NULL); + Py_DECREF(uuid_rep_obj); + + if (binary_value == NULL) { + return 0; + } + + result = _write_element_to_buffer(self, buffer, + type_byte, binary_value, + check_keys, options, + in_custom_call, + in_fallback_call); + Py_DECREF(binary_value); + return result; + } + + /* Try a custom encoder if one is provided and we have not already + * attempted to use a type encoder. */ + if (!in_custom_call && !options->type_registry.is_encoder_empty) { + PyObject* value_type = NULL; + PyObject* converter = NULL; + value_type = PyObject_Type(value); + if (value_type == NULL) { + return 0; + } + converter = PyDict_GetItem(options->type_registry.encoder_map, value_type); + Py_XDECREF(value_type); + if (converter != NULL) { + /* Transform types that have a registered converter. + * A new reference is created upon transformation. */ + new_value = PyObject_CallFunctionObjArgs(converter, value, NULL); + if (new_value == NULL) { + return 0; + } + retval = write_element_to_buffer(self, buffer, type_byte, new_value, + check_keys, options, 1, 0); + Py_XDECREF(new_value); + return retval; + } + } + + /* Try the fallback encoder if one is provided and we have not already + * attempted to use the fallback encoder. */ + if (!in_fallback_call && options->type_registry.has_fallback_encoder) { + new_value = PyObject_CallFunctionObjArgs( + options->type_registry.fallback_encoder, value, NULL); + if (new_value == NULL) { + // propagate any exception raised by the callback + return 0; + } + retval = write_element_to_buffer(self, buffer, type_byte, new_value, + check_keys, options, 0, 1); + Py_XDECREF(new_value); + return retval; + } + + /* We can't determine value's type. Fail. */ + _set_cannot_encode(value); + return 0; +} + +static int check_key_name(const char* name, int name_length) { + + if (name_length > 0 && name[0] == '$') { + PyObject* InvalidDocument = _error("InvalidDocument"); + if (InvalidDocument) { + PyObject* errmsg = PyUnicode_FromFormat( + "key '%s' must not start with '$'", name); + if (errmsg) { + PyErr_SetObject(InvalidDocument, errmsg); + Py_DECREF(errmsg); + } + Py_DECREF(InvalidDocument); + } + return 0; + } + if (strchr(name, '.')) { + PyObject* InvalidDocument = _error("InvalidDocument"); + if (InvalidDocument) { + PyObject* errmsg = PyUnicode_FromFormat( + "key '%s' must not contain '.'", name); + if (errmsg) { + PyErr_SetObject(InvalidDocument, errmsg); + Py_DECREF(errmsg); + } + Py_DECREF(InvalidDocument); + } + return 0; + } + return 1; +} + +/* Write a (key, value) pair to the buffer. + * + * Returns 0 on failure */ +int write_pair(PyObject* self, buffer_t buffer, const char* name, int name_length, + PyObject* value, unsigned char check_keys, + const codec_options_t* options, unsigned char allow_id) { + int type_byte; + + /* Don't write any _id elements unless we're explicitly told to - + * _id has to be written first so we do so, but don't bother + * deleting it from the dictionary being written. */ + if (!allow_id && strcmp(name, "_id") == 0) { + return 1; + } + + type_byte = pymongo_buffer_save_space(buffer, 1); + if (type_byte == -1) { + return 0; + } + if (check_keys && !check_key_name(name, name_length)) { + return 0; + } + if (!buffer_write_bytes(buffer, name, name_length + 1)) { + return 0; + } + if (!write_element_to_buffer(self, buffer, type_byte, + value, check_keys, options, 0, 0)) { + return 0; + } + return 1; +} + +int decode_and_write_pair(PyObject* self, buffer_t buffer, + PyObject* key, PyObject* value, + unsigned char check_keys, + const codec_options_t* options, + unsigned char top_level) { + PyObject* encoded; + const char* data; + int size; + if (PyUnicode_Check(key)) { + encoded = PyUnicode_AsUTF8String(key); + if (!encoded) { + return 0; + } + if (!(data = PyBytes_AS_STRING(encoded))) { + Py_DECREF(encoded); + return 0; + } + if ((size = _downcast_and_check(PyBytes_GET_SIZE(encoded), 1)) == -1) { + Py_DECREF(encoded); + return 0; + } + if (strlen(data) != (size_t)(size - 1)) { + PyObject* InvalidDocument = _error("InvalidDocument"); + if (InvalidDocument) { + PyErr_SetString(InvalidDocument, + "Key names must not contain the NULL byte"); + Py_DECREF(InvalidDocument); + } + Py_DECREF(encoded); + return 0; + } + } else { + PyObject* InvalidDocument = _error("InvalidDocument"); + if (InvalidDocument) { + PyObject* repr = PyObject_Repr(key); + if (repr) { + PyObject* errmsg = PyUnicode_FromString( + "documents must have only string keys, key was "); + if (errmsg) { + PyObject* error = PyUnicode_Concat(errmsg, repr); + if (error) { + PyErr_SetObject(InvalidDocument, error); + Py_DECREF(error); + } + Py_DECREF(errmsg); + Py_DECREF(repr); + } else { + Py_DECREF(repr); + } + } + Py_DECREF(InvalidDocument); + } + return 0; + } + + /* If top_level is True, don't allow writing _id here - it was already written. */ + if (!write_pair(self, buffer, data, + size - 1, value, check_keys, options, !top_level)) { + Py_DECREF(encoded); + return 0; + } + + Py_DECREF(encoded); + return 1; +} + + +/* Write a RawBSONDocument to the buffer. + * Returns the number of bytes written or 0 on failure. + */ +static int write_raw_doc(buffer_t buffer, PyObject* raw, PyObject* _raw_str) { + char* bytes; + Py_ssize_t len; + int len_int; + int bytes_written = 0; + PyObject* bytes_obj = NULL; + + bytes_obj = PyObject_GetAttr(raw, _raw_str); + if (!bytes_obj) { + goto fail; + } + + if (-1 == PyBytes_AsStringAndSize(bytes_obj, &bytes, &len)) { + goto fail; + } + len_int = _downcast_and_check(len, 0); + if (-1 == len_int) { + goto fail; + } + if (!buffer_write_bytes(buffer, bytes, len_int)) { + goto fail; + } + bytes_written = len_int; +fail: + Py_XDECREF(bytes_obj); + return bytes_written; +} + +/* returns the number of bytes written or 0 on failure */ +int write_dict(PyObject* self, buffer_t buffer, + PyObject* dict, unsigned char check_keys, + const codec_options_t* options, unsigned char top_level) { + PyObject* key; + PyObject* iter; + char zero = 0; + int length; + int length_location; + struct module_state *state = GETSTATE(self); + long type_marker; + int is_dict = PyDict_Check(dict); + if (!state) { + return 0; + } + + if (!is_dict) { + /* check for RawBSONDocument */ + type_marker = _type_marker(dict, state->_type_marker_str); + if (type_marker < 0) { + return 0; + } + + if (101 == type_marker) { + return write_raw_doc(buffer, dict, state->_raw_str); + } + + if (!PyObject_IsInstance(dict, state->Mapping)) { + PyObject* repr; + if ((repr = PyObject_Repr(dict))) { + PyObject* errmsg = PyUnicode_FromString( + "encoder expected a mapping type but got: "); + if (errmsg) { + PyObject* error = PyUnicode_Concat(errmsg, repr); + if (error) { + PyErr_SetObject(PyExc_TypeError, error); + Py_DECREF(error); + } + Py_DECREF(errmsg); + Py_DECREF(repr); + } + else { + Py_DECREF(repr); + } + } else { + PyErr_SetString(PyExc_TypeError, + "encoder expected a mapping type"); + } + + return 0; + } + /* PyObject_IsInstance returns -1 on error */ + if (PyErr_Occurred()) { + return 0; + } + } + + length_location = pymongo_buffer_save_space(buffer, 4); + if (length_location == -1) { + return 0; + } + + /* Write _id first if this is a top level doc. */ + if (top_level) { + /* + * If "dict" is a defaultdict we don't want to call + * PyObject_GetItem on it. That would **create** + * an _id where one didn't previously exist (PYTHON-871). + */ + if (is_dict) { + /* PyDict_GetItem returns a borrowed reference. */ + PyObject* _id = PyDict_GetItem(dict, state->_id_str); + if (_id) { + if (!write_pair(self, buffer, "_id", 3, + _id, check_keys, options, 1)) { + return 0; + } + } + } else if (PyMapping_HasKey(dict, state->_id_str)) { + PyObject* _id = PyObject_GetItem(dict, state->_id_str); + if (!_id) { + return 0; + } + if (!write_pair(self, buffer, "_id", 3, + _id, check_keys, options, 1)) { + Py_DECREF(_id); + return 0; + } + /* PyObject_GetItem returns a new reference. */ + Py_DECREF(_id); + } + } + + if (is_dict) { + PyObject* value; + Py_ssize_t pos = 0; + while (PyDict_Next(dict, &pos, &key, &value)) { + if (!decode_and_write_pair(self, buffer, key, value, + check_keys, options, top_level)) { + return 0; + } + } + } else { + iter = PyObject_GetIter(dict); + if (iter == NULL) { + return 0; + } + while ((key = PyIter_Next(iter)) != NULL) { + PyObject* value = PyObject_GetItem(dict, key); + if (!value) { + PyErr_SetObject(PyExc_KeyError, key); + Py_DECREF(key); + Py_DECREF(iter); + return 0; + } + if (!decode_and_write_pair(self, buffer, key, value, + check_keys, options, top_level)) { + Py_DECREF(key); + Py_DECREF(value); + Py_DECREF(iter); + return 0; + } + Py_DECREF(key); + Py_DECREF(value); + } + Py_DECREF(iter); + if (PyErr_Occurred()) { + return 0; + } + } + + /* write null byte and fill in length */ + if (!buffer_write_bytes(buffer, &zero, 1)) { + return 0; + } + length = pymongo_buffer_get_position(buffer) - length_location; + buffer_write_int32_at_position( + buffer, length_location, (int32_t)length); + return length; +} + +static PyObject* _cbson_dict_to_bson(PyObject* self, PyObject* args) { + PyObject* dict; + PyObject* result; + unsigned char check_keys; + unsigned char top_level = 1; + PyObject* options_obj; + codec_options_t options; + buffer_t buffer; + PyObject* raw_bson_document_bytes_obj; + long type_marker; + struct module_state *state = GETSTATE(self); + if (!state) { + return NULL; + } + + if (!(PyArg_ParseTuple(args, "ObO|b", &dict, &check_keys, + &options_obj, &top_level) && + convert_codec_options(self, options_obj, &options))) { + return NULL; + } + + /* check for RawBSONDocument */ + type_marker = _type_marker(dict, state->_type_marker_str); + if (type_marker < 0) { + destroy_codec_options(&options); + return NULL; + } else if (101 == type_marker) { + destroy_codec_options(&options); + raw_bson_document_bytes_obj = PyObject_GetAttr(dict, state->_raw_str); + if (NULL == raw_bson_document_bytes_obj) { + return NULL; + } + return raw_bson_document_bytes_obj; + } + + buffer = pymongo_buffer_new(); + if (!buffer) { + destroy_codec_options(&options); + return NULL; + } + + if (!write_dict(self, buffer, dict, check_keys, &options, top_level)) { + destroy_codec_options(&options); + pymongo_buffer_free(buffer); + return NULL; + } + + /* objectify buffer */ + result = Py_BuildValue("y#", pymongo_buffer_get_buffer(buffer), + (Py_ssize_t)pymongo_buffer_get_position(buffer)); + destroy_codec_options(&options); + pymongo_buffer_free(buffer); + return result; +} + +/* + * Hook for optional decoding BSON documents to DBRef. + */ +static PyObject *_dbref_hook(PyObject* self, PyObject* value) { + struct module_state *state = GETSTATE(self); + PyObject* ref = NULL; + PyObject* id = NULL; + PyObject* database = NULL; + PyObject* ret = NULL; + int db_present = 0; + if (!state) { + return NULL; + } + + /* Decoding for DBRefs */ + if (PyMapping_HasKey(value, state->_dollar_ref_str) && PyMapping_HasKey(value, state->_dollar_id_str)) { /* DBRef */ + ref = PyObject_GetItem(value, state->_dollar_ref_str); + /* PyObject_GetItem returns NULL to indicate error. */ + if (!ref) { + goto invalid; + } + id = PyObject_GetItem(value, state->_dollar_id_str); + /* PyObject_GetItem returns NULL to indicate error. */ + if (!id) { + goto invalid; + } + + if (PyMapping_HasKey(value, state->_dollar_db_str)) { + database = PyObject_GetItem(value, state->_dollar_db_str); + if (!database) { + goto invalid; + } + db_present = 1; + } else { + database = Py_None; + Py_INCREF(database); + } + + // check types + if (!(PyUnicode_Check(ref) && (database == Py_None || PyUnicode_Check(database)))) { + ret = value; + goto invalid; + } + + PyMapping_DelItem(value, state->_dollar_ref_str); + PyMapping_DelItem(value, state->_dollar_id_str); + if (db_present) { + PyMapping_DelItem(value, state->_dollar_db_str); + } + + ret = PyObject_CallFunctionObjArgs(state->DBRef, ref, id, database, value, NULL); + Py_DECREF(value); + } else { + ret = value; + } +invalid: + Py_XDECREF(ref); + Py_XDECREF(id); + Py_XDECREF(database); + return ret; +} + +static PyObject* get_value(PyObject* self, PyObject* name, const char* buffer, + unsigned* position, unsigned char type, + unsigned max, const codec_options_t* options, int raw_array) { + struct module_state *state = GETSTATE(self); + PyObject* value = NULL; + if (!state) { + return NULL; + } + switch (type) { + case 1: + { + double d; + if (max < 8) { + goto invalid; + } + memcpy(&d, buffer + *position, 8); + value = PyFloat_FromDouble(BSON_DOUBLE_FROM_LE(d)); + *position += 8; + break; + } + case 2: + case 14: + { + uint32_t value_length; + if (max < 4) { + goto invalid; + } + memcpy(&value_length, buffer + *position, 4); + value_length = BSON_UINT32_FROM_LE(value_length); + /* Encoded string length + string */ + if (!value_length || max < value_length || max < 4 + value_length) { + goto invalid; + } + *position += 4; + /* Strings must end in \0 */ + if (buffer[*position + value_length - 1]) { + goto invalid; + } + value = PyUnicode_DecodeUTF8( + buffer + *position, value_length - 1, + options->unicode_decode_error_handler); + if (!value) { + goto invalid; + } + *position += value_length; + break; + } + case 3: + { + uint32_t size; + + if (max < 4) { + goto invalid; + } + memcpy(&size, buffer + *position, 4); + size = BSON_UINT32_FROM_LE(size); + if (size < BSON_MIN_SIZE || max < size) { + goto invalid; + } + /* Check for bad eoo */ + if (buffer[*position + size - 1]) { + goto invalid; + } + + value = elements_to_dict(self, buffer + *position, + size, options); + if (!value) { + goto invalid; + } + + if (options->is_raw_bson) { + *position += size; + break; + } + + /* Hook for DBRefs */ + value = _dbref_hook(self, value); + if (!value) { + goto invalid; + } + + *position += size; + break; + } + case 4: + { + uint32_t size, end; + + if (max < 4) { + goto invalid; + } + memcpy(&size, buffer + *position, 4); + size = BSON_UINT32_FROM_LE(size); + if (size < BSON_MIN_SIZE || max < size) { + goto invalid; + } + + end = *position + size - 1; + /* Check for bad eoo */ + if (buffer[end]) { + goto invalid; + } + + if (raw_array != 0) { + // Treat it as a binary buffer. + value = PyBytes_FromStringAndSize(buffer + *position, size); + *position += size; + break; + } + + *position += 4; + + value = PyList_New(0); + if (!value) { + goto invalid; + } + while (*position < end) { + PyObject* to_append; + + unsigned char bson_type = (unsigned char)buffer[(*position)++]; + + size_t key_size = strlen(buffer + *position); + if (max < key_size) { + Py_DECREF(value); + goto invalid; + } + /* just skip the key, they're in order. */ + *position += (unsigned)key_size + 1; + if (Py_EnterRecursiveCall(" while decoding a list value")) { + Py_DECREF(value); + goto invalid; + } + to_append = get_value(self, name, buffer, position, bson_type, + max - (unsigned)key_size, options, raw_array); + Py_LeaveRecursiveCall(); + if (!to_append) { + Py_DECREF(value); + goto invalid; + } + if (PyList_Append(value, to_append) < 0) { + Py_DECREF(value); + Py_DECREF(to_append); + goto invalid; + } + Py_DECREF(to_append); + } + if (*position != end) { + goto invalid; + } + (*position)++; + break; + } + case 5: + { + PyObject* data; + PyObject* st; + uint32_t length, length2; + unsigned char subtype; + + if (max < 5) { + goto invalid; + } + memcpy(&length, buffer + *position, 4); + length = BSON_UINT32_FROM_LE(length); + if (max < length) { + goto invalid; + } + + subtype = (unsigned char)buffer[*position + 4]; + *position += 5; + if (subtype == 2) { + if (length < 4) { + goto invalid; + } + memcpy(&length2, buffer + *position, 4); + length2 = BSON_UINT32_FROM_LE(length2); + if (length2 != length - 4) { + goto invalid; + } + } + /* Python3 special case. Decode BSON binary subtype 0 to bytes. */ + if (subtype == 0) { + value = PyBytes_FromStringAndSize(buffer + *position, length); + *position += length; + break; + } + if (subtype == 2) { + data = PyBytes_FromStringAndSize(buffer + *position + 4, length - 4); + } else { + data = PyBytes_FromStringAndSize(buffer + *position, length); + } + if (!data) { + goto invalid; + } + /* Encode as UUID or Binary based on options->uuid_rep */ + if (subtype == 3 || subtype == 4) { + PyObject* binary_value = NULL; + char uuid_rep = options->uuid_rep; + + /* UUID should always be 16 bytes */ + if (length != 16) { + goto uuiderror; + } + + binary_value = PyObject_CallFunction(state->Binary, "(Oi)", data, subtype); + if (binary_value == NULL) { + goto uuiderror; + } + + if ((uuid_rep == UNSPECIFIED) || + (subtype == 4 && uuid_rep != STANDARD) || + (subtype == 3 && uuid_rep == STANDARD)) { + value = binary_value; + Py_INCREF(value); + } else { + PyObject *uuid_rep_obj = PyLong_FromLong(uuid_rep); + if (!uuid_rep_obj) { + goto uuiderror; + } + value = PyObject_CallMethodObjArgs(binary_value, state->_as_uuid_str, uuid_rep_obj, NULL); + Py_DECREF(uuid_rep_obj); + } + + uuiderror: + Py_XDECREF(binary_value); + Py_DECREF(data); + if (!value) { + goto invalid; + } + *position += length; + break; + } + + st = PyLong_FromLong(subtype); + if (!st) { + Py_DECREF(data); + goto invalid; + } + value = PyObject_CallFunctionObjArgs(state->Binary, data, st, NULL); + Py_DECREF(st); + Py_DECREF(data); + if (!value) { + goto invalid; + } + *position += length; + break; + } + case 6: + case 10: + { + value = Py_None; + Py_INCREF(value); + break; + } + case 7: + { + if (max < 12) { + goto invalid; + } + value = PyObject_CallFunction(state->ObjectId, "y#", buffer + *position, (Py_ssize_t)12); + *position += 12; + break; + } + case 8: + { + char boolean_raw = buffer[(*position)++]; + if (0 == boolean_raw) { + value = Py_False; + } else if (1 == boolean_raw) { + value = Py_True; + } else { + PyObject* InvalidBSON = _error("InvalidBSON"); + if (InvalidBSON) { + PyErr_Format(InvalidBSON, "invalid boolean value: %x", boolean_raw); + Py_DECREF(InvalidBSON); + } + return NULL; + } + Py_INCREF(value); + break; + } + case 9: + { + PyObject* naive; + PyObject* replace; + PyObject* args; + PyObject* kwargs; + PyObject* astimezone; + int64_t millis; + if (max < 8) { + goto invalid; + } + memcpy(&millis, buffer + *position, 8); + millis = (int64_t)BSON_UINT64_FROM_LE(millis); + *position += 8; + + if (options->datetime_conversion == DATETIME_MS){ + value = datetime_ms_from_millis(self, millis); + break; + } + + int dt_clamp = options->datetime_conversion == DATETIME_CLAMP; + int dt_auto = options->datetime_conversion == DATETIME_AUTO; + + + if (dt_clamp || dt_auto){ + PyObject *min_millis_fn_res; + PyObject *max_millis_fn_res; + int64_t min_millis; + int64_t max_millis; + + if (options->tz_aware){ + PyObject* tzinfo = options->tzinfo; + if (tzinfo == Py_None) { + // Default to UTC. + tzinfo = state->UTC; + } + min_millis_fn_res = PyObject_CallFunctionObjArgs(state->_min_datetime_ms, tzinfo, NULL); + max_millis_fn_res = PyObject_CallFunctionObjArgs(state->_max_datetime_ms, tzinfo, NULL); + } else { + min_millis_fn_res = PyObject_CallObject(state->_min_datetime_ms, NULL); + max_millis_fn_res = PyObject_CallObject(state->_max_datetime_ms, NULL); + } + + if (!min_millis_fn_res || !max_millis_fn_res){ + Py_XDECREF(min_millis_fn_res); + Py_XDECREF(max_millis_fn_res); + goto invalid; + } + + min_millis = PyLong_AsLongLong(min_millis_fn_res); + max_millis = PyLong_AsLongLong(max_millis_fn_res); + + if ((min_millis == -1 || max_millis == -1) && PyErr_Occurred()) + { + // min/max_millis check + goto invalid; + } + + if (dt_clamp) { + if (millis < min_millis) { + millis = min_millis; + } else if (millis > max_millis) { + millis = max_millis; + } + // Continues from here to return a datetime. + } else { // dt_auto + if (millis < min_millis || millis > max_millis){ + value = datetime_ms_from_millis(self, millis); + break; // Out-of-range so done. + } + } + } + + naive = datetime_from_millis(millis); + if (!options->tz_aware) { /* In the naive case, we're done here. */ + value = naive; + break; + } + + if (!naive) { + goto invalid; + } + replace = PyObject_GetAttr(naive, state->_replace_str); + Py_DECREF(naive); + if (!replace) { + goto invalid; + } + args = PyTuple_New(0); + if (!args) { + Py_DECREF(replace); + goto invalid; + } + kwargs = PyDict_New(); + if (!kwargs) { + Py_DECREF(replace); + Py_DECREF(args); + goto invalid; + } + if (PyDict_SetItem(kwargs, state->_tzinfo_str, state->UTC) == -1) { + Py_DECREF(replace); + Py_DECREF(args); + Py_DECREF(kwargs); + goto invalid; + } + value = PyObject_Call(replace, args, kwargs); + if (!value) { + Py_DECREF(replace); + Py_DECREF(args); + Py_DECREF(kwargs); + goto invalid; + } + + /* convert to local time */ + if (options->tzinfo != Py_None) { + astimezone = PyObject_GetAttr(value, state->_astimezone_str); + Py_DECREF(value); + if (!astimezone) { + Py_DECREF(replace); + Py_DECREF(args); + Py_DECREF(kwargs); + goto invalid; + } + value = PyObject_CallFunctionObjArgs(astimezone, options->tzinfo, NULL); + Py_DECREF(astimezone); + } + + Py_DECREF(replace); + Py_DECREF(args); + Py_DECREF(kwargs); + break; + } + case 11: + { + PyObject* pattern; + int flags; + size_t flags_length, i; + size_t pattern_length = strlen(buffer + *position); + if (pattern_length > BSON_MAX_SIZE || max < pattern_length) { + goto invalid; + } + pattern = PyUnicode_DecodeUTF8( + buffer + *position, pattern_length, + options->unicode_decode_error_handler); + if (!pattern) { + goto invalid; + } + *position += (unsigned)pattern_length + 1; + flags_length = strlen(buffer + *position); + if (flags_length > BSON_MAX_SIZE || + (BSON_MAX_SIZE - pattern_length) < flags_length) { + Py_DECREF(pattern); + goto invalid; + } + if (max < pattern_length + flags_length) { + Py_DECREF(pattern); + goto invalid; + } + flags = 0; + for (i = 0; i < flags_length; i++) { + if (buffer[*position + i] == 'i') { + flags |= 2; + } else if (buffer[*position + i] == 'l') { + flags |= 4; + } else if (buffer[*position + i] == 'm') { + flags |= 8; + } else if (buffer[*position + i] == 's') { + flags |= 16; + } else if (buffer[*position + i] == 'u') { + flags |= 32; + } else if (buffer[*position + i] == 'x') { + flags |= 64; + } + } + *position += (unsigned)flags_length + 1; + + value = PyObject_CallFunction(state->Regex, "Oi", pattern, flags); + Py_DECREF(pattern); + break; + } + case 12: + { + uint32_t coll_length; + PyObject* collection; + PyObject* id = NULL; + + if (max < 4) { + goto invalid; + } + memcpy(&coll_length, buffer + *position, 4); + coll_length = BSON_UINT32_FROM_LE(coll_length); + /* Encoded string length + string + 12 byte ObjectId */ + if (!coll_length || max < coll_length || max < 4 + coll_length + 12) { + goto invalid; + } + *position += 4; + /* Strings must end in \0 */ + if (buffer[*position + coll_length - 1]) { + goto invalid; + } + + collection = PyUnicode_DecodeUTF8( + buffer + *position, coll_length - 1, + options->unicode_decode_error_handler); + if (!collection) { + goto invalid; + } + *position += coll_length; + + id = PyObject_CallFunction(state->ObjectId, "y#", buffer + *position, (Py_ssize_t)12); + if (!id) { + Py_DECREF(collection); + goto invalid; + } + *position += 12; + value = PyObject_CallFunctionObjArgs(state->DBRef, collection, id, NULL); + Py_DECREF(collection); + Py_DECREF(id); + break; + } + case 13: + { + PyObject* code; + uint32_t value_length; + if (max < 4) { + goto invalid; + } + memcpy(&value_length, buffer + *position, 4); + value_length = BSON_UINT32_FROM_LE(value_length); + /* Encoded string length + string */ + if (!value_length || max < value_length || max < 4 + value_length) { + goto invalid; + } + *position += 4; + /* Strings must end in \0 */ + if (buffer[*position + value_length - 1]) { + goto invalid; + } + code = PyUnicode_DecodeUTF8( + buffer + *position, value_length - 1, + options->unicode_decode_error_handler); + if (!code) { + goto invalid; + } + *position += value_length; + value = PyObject_CallFunctionObjArgs(state->Code, code, NULL, NULL); + Py_DECREF(code); + break; + } + case 15: + { + uint32_t c_w_s_size; + uint32_t code_size; + uint32_t scope_size; + uint32_t len; + PyObject* code; + PyObject* scope; + + if (max < 8) { + goto invalid; + } + + memcpy(&c_w_s_size, buffer + *position, 4); + c_w_s_size = BSON_UINT32_FROM_LE(c_w_s_size); + *position += 4; + + if (max < c_w_s_size) { + goto invalid; + } + + memcpy(&code_size, buffer + *position, 4); + code_size = BSON_UINT32_FROM_LE(code_size); + /* code_w_scope length + code length + code + scope length */ + len = 4 + 4 + code_size + 4; + if (!code_size || max < code_size || max < len || len < code_size) { + goto invalid; + } + *position += 4; + /* Strings must end in \0 */ + if (buffer[*position + code_size - 1]) { + goto invalid; + } + code = PyUnicode_DecodeUTF8( + buffer + *position, code_size - 1, + options->unicode_decode_error_handler); + if (!code) { + goto invalid; + } + *position += code_size; + + memcpy(&scope_size, buffer + *position, 4); + scope_size = BSON_UINT32_FROM_LE(scope_size); + /* code length + code + scope length + scope */ + len = 4 + 4 + code_size + scope_size; + if (scope_size < BSON_MIN_SIZE || len != c_w_s_size || len < scope_size) { + Py_DECREF(code); + goto invalid; + } + + /* Check for bad eoo */ + if (buffer[*position + scope_size - 1]) { + goto invalid; + } + scope = elements_to_dict(self, buffer + *position, + scope_size, options); + if (!scope) { + Py_DECREF(code); + goto invalid; + } + *position += scope_size; + + value = PyObject_CallFunctionObjArgs(state->Code, code, scope, NULL); + Py_DECREF(code); + Py_DECREF(scope); + break; + } + case 16: + { + int32_t i; + if (max < 4) { + goto invalid; + } + memcpy(&i, buffer + *position, 4); + i = (int32_t)BSON_UINT32_FROM_LE(i); + value = PyLong_FromLong(i); + if (!value) { + goto invalid; + } + *position += 4; + break; + } + case 17: + { + uint32_t time, inc; + if (max < 8) { + goto invalid; + } + memcpy(&inc, buffer + *position, 4); + memcpy(&time, buffer + *position + 4, 4); + inc = BSON_UINT32_FROM_LE(inc); + time = BSON_UINT32_FROM_LE(time); + value = PyObject_CallFunction(state->Timestamp, "II", time, inc); + *position += 8; + break; + } + case 18: + { + int64_t ll; + if (max < 8) { + goto invalid; + } + memcpy(&ll, buffer + *position, 8); + ll = (int64_t)BSON_UINT64_FROM_LE(ll); + value = PyObject_CallFunction(state->BSONInt64, "L", ll); + *position += 8; + break; + } + case 19: + { + if (max < 16) { + goto invalid; + } + PyObject *_bytes_obj = PyBytes_FromStringAndSize(buffer + *position, (Py_ssize_t)16); + if (!_bytes_obj) { + goto invalid; + } + value = PyObject_CallMethodObjArgs(state->Decimal128, state->_from_bid_str, _bytes_obj, NULL); + Py_DECREF(_bytes_obj); + *position += 16; + break; + } + case 255: + { + value = PyObject_CallFunctionObjArgs(state->MinKey, NULL); + break; + } + case 127: + { + value = PyObject_CallFunctionObjArgs(state->MaxKey, NULL); + break; + } + default: + { + PyObject* InvalidBSON = _error("InvalidBSON"); + if (InvalidBSON) { + PyObject* bobj = PyBytes_FromFormat("%c", type); + if (bobj) { + PyObject* repr = PyObject_Repr(bobj); + Py_DECREF(bobj); + /* + * See http://bugs.python.org/issue22023 for why we can't + * just use PyUnicode_FromFormat with %S or %R to do this + * work. + */ + if (repr) { + PyObject* left = PyUnicode_FromString( + "Detected unknown BSON type "); + if (left) { + PyObject* lmsg = PyUnicode_Concat(left, repr); + Py_DECREF(left); + if (lmsg) { + PyObject* errmsg = PyUnicode_FromFormat( + "%U for fieldname '%U'. Are you using the " + "latest driver version?", lmsg, name); + if (errmsg) { + PyErr_SetObject(InvalidBSON, errmsg); + Py_DECREF(errmsg); + } + Py_DECREF(lmsg); + } + } + Py_DECREF(repr); + } + } + Py_DECREF(InvalidBSON); + } + goto invalid; + } + } + + if (value) { + if (!options->type_registry.is_decoder_empty) { + PyObject* value_type = NULL; + PyObject* converter = NULL; + value_type = PyObject_Type(value); + if (value_type == NULL) { + goto invalid; + } + converter = PyDict_GetItem(options->type_registry.decoder_map, value_type); + if (converter != NULL) { + PyObject* new_value = PyObject_CallFunctionObjArgs(converter, value, NULL); + Py_DECREF(value_type); + Py_DECREF(value); + return new_value; + } else { + Py_DECREF(value_type); + return value; + } + } + return value; + } + + invalid: + + /* + * Wrap any non-InvalidBSON errors in InvalidBSON. + */ + if (PyErr_Occurred()) { + PyObject *etype, *evalue, *etrace; + PyObject *InvalidBSON; + + /* + * Calling _error clears the error state, so fetch it first. + */ + PyErr_Fetch(&etype, &evalue, &etrace); + + /* Dont reraise anything but PyExc_Exceptions as InvalidBSON. */ + if (PyErr_GivenExceptionMatches(etype, PyExc_Exception)) { + InvalidBSON = _error("InvalidBSON"); + if (InvalidBSON) { + if (!PyErr_GivenExceptionMatches(etype, InvalidBSON)) { + /* + * Raise InvalidBSON(str(e)). + */ + Py_DECREF(etype); + etype = InvalidBSON; + + if (evalue) { + PyObject *msg = PyObject_Str(evalue); + Py_DECREF(evalue); + evalue = msg; + } + PyErr_NormalizeException(&etype, &evalue, &etrace); + } else { + /* + * The current exception matches InvalidBSON, so we don't + * need this reference after all. + */ + Py_DECREF(InvalidBSON); + } + } + } + /* Steals references to args. */ + PyErr_Restore(etype, evalue, etrace); + } else { + PyObject *InvalidBSON = _error("InvalidBSON"); + if (InvalidBSON) { + PyErr_SetString(InvalidBSON, "invalid length or type code"); + Py_DECREF(InvalidBSON); + } + } + return NULL; +} + +/* + * Get the next 'name' and 'value' from a document in a string, whose position + * is provided. + * + * Returns the position of the next element in the document, or -1 on error. + */ +static int _element_to_dict(PyObject* self, const char* string, + unsigned position, unsigned max, + const codec_options_t* options, + int raw_array, + PyObject** name, PyObject** value) { + unsigned char type = (unsigned char)string[position++]; + size_t name_length = strlen(string + position); + if (name_length > BSON_MAX_SIZE || position + name_length >= max) { + PyObject* InvalidBSON = _error("InvalidBSON"); + if (InvalidBSON) { + PyErr_SetString(InvalidBSON, "field name too large"); + Py_DECREF(InvalidBSON); + } + return -1; + } + *name = PyUnicode_DecodeUTF8( + string + position, name_length, + options->unicode_decode_error_handler); + if (!*name) { + /* If NULL is returned then wrap the UnicodeDecodeError + in an InvalidBSON error */ + PyObject *etype, *evalue, *etrace; + PyObject *InvalidBSON; + + PyErr_Fetch(&etype, &evalue, &etrace); + if (PyErr_GivenExceptionMatches(etype, PyExc_Exception)) { + InvalidBSON = _error("InvalidBSON"); + if (InvalidBSON) { + Py_DECREF(etype); + etype = InvalidBSON; + + if (evalue) { + PyObject *msg = PyObject_Str(evalue); + Py_DECREF(evalue); + evalue = msg; + } + PyErr_NormalizeException(&etype, &evalue, &etrace); + } + } + PyErr_Restore(etype, evalue, etrace); + return -1; + } + position += (unsigned)name_length + 1; + *value = get_value(self, *name, string, &position, type, + max - position, options, raw_array); + if (!*value) { + Py_DECREF(*name); + return -1; + } + return position; +} + +static PyObject* _cbson_element_to_dict(PyObject* self, PyObject* args) { + /* TODO: Support buffer protocol */ + char* string; + PyObject* bson; + PyObject* options_obj; + codec_options_t options; + unsigned position; + unsigned max; + int new_position; + int raw_array = 0; + PyObject* name; + PyObject* value; + PyObject* result_tuple; + + if (!(PyArg_ParseTuple(args, "OIIOp", &bson, &position, &max, + &options_obj, &raw_array) && + convert_codec_options(self, options_obj, &options))) { + return NULL; + } + + if (!PyBytes_Check(bson)) { + PyErr_SetString(PyExc_TypeError, "argument to _element_to_dict must be a bytes object"); + return NULL; + } + string = PyBytes_AS_STRING(bson); + + new_position = _element_to_dict(self, string, position, max, &options, raw_array, &name, &value); + if (new_position < 0) { + return NULL; + } + + result_tuple = Py_BuildValue("NNi", name, value, new_position); + if (!result_tuple) { + Py_DECREF(name); + Py_DECREF(value); + return NULL; + } + + destroy_codec_options(&options); + return result_tuple; +} + +static PyObject* _elements_to_dict(PyObject* self, const char* string, + unsigned max, + const codec_options_t* options) { + unsigned position = 0; + PyObject* dict = PyObject_CallObject(options->document_class, NULL); + if (!dict) { + return NULL; + } + int raw_array = 0; + while (position < max) { + PyObject* name = NULL; + PyObject* value = NULL; + int new_position; + + new_position = _element_to_dict( + self, string, position, max, options, raw_array, &name, &value); + if (new_position < 0) { + Py_DECREF(dict); + return NULL; + } else { + position = (unsigned)new_position; + } + + PyObject_SetItem(dict, name, value); + Py_DECREF(name); + Py_DECREF(value); + } + return dict; +} + +static PyObject* elements_to_dict(PyObject* self, const char* string, + unsigned max, + const codec_options_t* options) { + PyObject* result; + if (options->is_raw_bson) { + return PyObject_CallFunction( + options->document_class, "y#O", + string, max, options->options_obj); + } + if (Py_EnterRecursiveCall(" while decoding a BSON document")) + return NULL; + result = _elements_to_dict(self, string + 4, max - 5, options); + Py_LeaveRecursiveCall(); + return result; +} + +static int _get_buffer(PyObject *exporter, Py_buffer *view) { + if (PyObject_GetBuffer(exporter, view, PyBUF_SIMPLE) == -1) { + return 0; + } + if (!PyBuffer_IsContiguous(view, 'C')) { + PyErr_SetString(PyExc_ValueError, + "must be a contiguous buffer"); + goto fail; + } + if (!view->buf || view->len < 0) { + PyErr_SetString(PyExc_ValueError, "invalid buffer"); + goto fail; + } + if (view->itemsize != 1) { + PyErr_SetString(PyExc_ValueError, + "buffer data must be ascii or utf8"); + goto fail; + } + return 1; +fail: + PyBuffer_Release(view); + return 0; +} + +static PyObject* _cbson_bson_to_dict(PyObject* self, PyObject* args) { + int32_t size; + Py_ssize_t total_size; + const char* string; + PyObject* bson; + codec_options_t options; + PyObject* result = NULL; + PyObject* options_obj; + Py_buffer view = {0}; + + if (! (PyArg_ParseTuple(args, "OO", &bson, &options_obj) && + convert_codec_options(self, options_obj, &options))) { + return result; + } + + if (!_get_buffer(bson, &view)) { + destroy_codec_options(&options); + return result; + } + + total_size = view.len; + + if (total_size < BSON_MIN_SIZE) { + PyObject* InvalidBSON = _error("InvalidBSON"); + if (InvalidBSON) { + PyErr_SetString(InvalidBSON, + "not enough data for a BSON document"); + Py_DECREF(InvalidBSON); + } + goto done;; + } + + string = (char*)view.buf; + memcpy(&size, string, 4); + size = (int32_t)BSON_UINT32_FROM_LE(size); + if (size < BSON_MIN_SIZE) { + PyObject* InvalidBSON = _error("InvalidBSON"); + if (InvalidBSON) { + PyErr_SetString(InvalidBSON, "invalid message size"); + Py_DECREF(InvalidBSON); + } + goto done; + } + + if (total_size < size || total_size > BSON_MAX_SIZE) { + PyObject* InvalidBSON = _error("InvalidBSON"); + if (InvalidBSON) { + PyErr_SetString(InvalidBSON, "objsize too large"); + Py_DECREF(InvalidBSON); + } + goto done; + } + + if (size != total_size || string[size - 1]) { + PyObject* InvalidBSON = _error("InvalidBSON"); + if (InvalidBSON) { + PyErr_SetString(InvalidBSON, "bad eoo"); + Py_DECREF(InvalidBSON); + } + goto done; + } + + result = elements_to_dict(self, string, (unsigned)size, &options); +done: + PyBuffer_Release(&view); + destroy_codec_options(&options); + return result; +} + +static PyObject* _cbson_decode_all(PyObject* self, PyObject* args) { + int32_t size; + Py_ssize_t total_size; + const char* string; + PyObject* bson; + PyObject* dict; + PyObject* result = NULL; + codec_options_t options; + PyObject* options_obj = NULL; + Py_buffer view = {0}; + + if (!(PyArg_ParseTuple(args, "OO", &bson, &options_obj) && + convert_codec_options(self, options_obj, &options))) { + return NULL; + } + + if (!_get_buffer(bson, &view)) { + destroy_codec_options(&options); + return NULL; + } + total_size = view.len; + string = (char*)view.buf; + + if (!(result = PyList_New(0))) { + goto fail; + } + + while (total_size > 0) { + if (total_size < BSON_MIN_SIZE) { + PyObject* InvalidBSON = _error("InvalidBSON"); + if (InvalidBSON) { + PyErr_SetString(InvalidBSON, + "not enough data for a BSON document"); + Py_DECREF(InvalidBSON); + } + Py_DECREF(result); + goto fail; + } + + memcpy(&size, string, 4); + size = (int32_t)BSON_UINT32_FROM_LE(size); + if (size < BSON_MIN_SIZE) { + PyObject* InvalidBSON = _error("InvalidBSON"); + if (InvalidBSON) { + PyErr_SetString(InvalidBSON, "invalid message size"); + Py_DECREF(InvalidBSON); + } + Py_DECREF(result); + goto fail; + } + + if (total_size < size) { + PyObject* InvalidBSON = _error("InvalidBSON"); + if (InvalidBSON) { + PyErr_SetString(InvalidBSON, "objsize too large"); + Py_DECREF(InvalidBSON); + } + Py_DECREF(result); + goto fail; + } + + if (string[size - 1]) { + PyObject* InvalidBSON = _error("InvalidBSON"); + if (InvalidBSON) { + PyErr_SetString(InvalidBSON, "bad eoo"); + Py_DECREF(InvalidBSON); + } + Py_DECREF(result); + goto fail; + } + + dict = elements_to_dict(self, string, (unsigned)size, &options); + if (!dict) { + Py_DECREF(result); + goto fail; + } + if (PyList_Append(result, dict) < 0) { + Py_DECREF(dict); + Py_DECREF(result); + goto fail; + } + Py_DECREF(dict); + string += size; + total_size -= size; + } + goto done; +fail: + result = NULL; +done: + PyBuffer_Release(&view); + destroy_codec_options(&options); + return result; +} + + +static PyObject* _cbson_array_of_documents_to_buffer(PyObject* self, PyObject* args) { + uint32_t size; + uint32_t value_length; + uint32_t position = 0; + buffer_t buffer; + const char* string; + PyObject* arr; + PyObject* result = NULL; + Py_buffer view = {0}; + + if (!PyArg_ParseTuple(args, "O", &arr)) { + return NULL; + } + + if (!_get_buffer(arr, &view)) { + return NULL; + } + + buffer = pymongo_buffer_new(); + if (!buffer) { + PyBuffer_Release(&view); + return NULL; + } + + string = (char*)view.buf; + + if (view.len < BSON_MIN_SIZE) { + PyObject* InvalidBSON = _error("InvalidBSON"); + if (InvalidBSON) { + PyErr_SetString(InvalidBSON, + "not enough data for a BSON document"); + Py_DECREF(InvalidBSON); + } + goto done; + } + + memcpy(&size, string, 4); + size = BSON_UINT32_FROM_LE(size); + /* save space for length */ + if (pymongo_buffer_save_space(buffer, size) == -1) { + goto fail; + } + pymongo_buffer_update_position(buffer, 0); + + position += 4; + while (position < size - 1) { + // Verify the value is an object. + unsigned char type = (unsigned char)string[position]; + if (type != 3) { + PyObject* InvalidBSON = _error("InvalidBSON"); + if (InvalidBSON) { + PyErr_SetString(InvalidBSON, "array element was not an object"); + Py_DECREF(InvalidBSON); + } + goto fail; + } + + // Just skip the keys. + position = position + strlen(string + position) + 1; + + if (position >= size || (size - position) < BSON_MIN_SIZE) { + PyObject* InvalidBSON = _error("InvalidBSON"); + if (InvalidBSON) { + PyErr_SetString(InvalidBSON, "invalid array content"); + Py_DECREF(InvalidBSON); + } + goto fail; + } + + memcpy(&value_length, string + position, 4); + value_length = BSON_UINT32_FROM_LE(value_length); + if (value_length < BSON_MIN_SIZE) { + PyObject* InvalidBSON = _error("InvalidBSON"); + if (InvalidBSON) { + PyErr_SetString(InvalidBSON, "invalid message size"); + Py_DECREF(InvalidBSON); + } + goto fail; + } + + if (view.len < size) { + PyObject* InvalidBSON = _error("InvalidBSON"); + if (InvalidBSON) { + PyErr_SetString(InvalidBSON, "objsize too large"); + Py_DECREF(InvalidBSON); + } + goto fail; + } + + if (string[size - 1]) { + PyObject* InvalidBSON = _error("InvalidBSON"); + if (InvalidBSON) { + PyErr_SetString(InvalidBSON, "bad eoo"); + Py_DECREF(InvalidBSON); + } + goto fail; + } + + if (pymongo_buffer_write(buffer, string + position, value_length) == 1) { + goto fail; + } + position += value_length; + } + + /* objectify buffer */ + result = Py_BuildValue("y#", pymongo_buffer_get_buffer(buffer), + (Py_ssize_t)pymongo_buffer_get_position(buffer)); + goto done; +fail: + result = NULL; +done: + PyBuffer_Release(&view); + pymongo_buffer_free(buffer); + return result; +} + + +static PyMethodDef _CBSONMethods[] = { + {"_dict_to_bson", _cbson_dict_to_bson, METH_VARARGS, + "convert a dictionary to a string containing its BSON representation."}, + {"_bson_to_dict", _cbson_bson_to_dict, METH_VARARGS, + "convert a BSON string to a SON object."}, + {"_decode_all", _cbson_decode_all, METH_VARARGS, + "convert binary data to a sequence of documents."}, + {"_element_to_dict", _cbson_element_to_dict, METH_VARARGS, + "Decode a single key, value pair."}, + {"_array_of_documents_to_buffer", _cbson_array_of_documents_to_buffer, METH_VARARGS, "Convert raw array of documents to a stream of BSON documents"}, + {"_test_long_long_to_str", _test_long_long_to_str, METH_VARARGS, "Test conversion of extreme and common Py_ssize_t values to str."}, + {NULL, NULL, 0, NULL} +}; + +#define INITERROR return -1; +static int _cbson_traverse(PyObject *m, visitproc visit, void *arg) { + struct module_state *state = GETSTATE(m); + if (!state) { + return 0; + } + Py_VISIT(state->Binary); + Py_VISIT(state->Code); + Py_VISIT(state->ObjectId); + Py_VISIT(state->DBRef); + Py_VISIT(state->Regex); + Py_VISIT(state->UUID); + Py_VISIT(state->Timestamp); + Py_VISIT(state->MinKey); + Py_VISIT(state->MaxKey); + Py_VISIT(state->UTC); + Py_VISIT(state->REType); + Py_VISIT(state->_type_marker_str); + Py_VISIT(state->_flags_str); + Py_VISIT(state->_pattern_str); + Py_VISIT(state->_encoder_map_str); + Py_VISIT(state->_decoder_map_str); + Py_VISIT(state->_fallback_encoder_str); + Py_VISIT(state->_raw_str); + Py_VISIT(state->_subtype_str); + Py_VISIT(state->_binary_str); + Py_VISIT(state->_scope_str); + Py_VISIT(state->_inc_str); + Py_VISIT(state->_time_str); + Py_VISIT(state->_bid_str); + Py_VISIT(state->_replace_str); + Py_VISIT(state->_astimezone_str); + Py_VISIT(state->_id_str); + Py_VISIT(state->_dollar_ref_str); + Py_VISIT(state->_dollar_id_str); + Py_VISIT(state->_dollar_db_str); + Py_VISIT(state->_tzinfo_str); + Py_VISIT(state->_as_doc_str); + Py_VISIT(state->_utcoffset_str); + Py_VISIT(state->_from_uuid_str); + Py_VISIT(state->_as_uuid_str); + Py_VISIT(state->_from_bid_str); + return 0; +} + +static int _cbson_clear(PyObject *m) { + struct module_state *state = GETSTATE(m); + if (!state) { + return 0; + } + Py_CLEAR(state->Binary); + Py_CLEAR(state->Code); + Py_CLEAR(state->ObjectId); + Py_CLEAR(state->DBRef); + Py_CLEAR(state->Regex); + Py_CLEAR(state->UUID); + Py_CLEAR(state->Timestamp); + Py_CLEAR(state->MinKey); + Py_CLEAR(state->MaxKey); + Py_CLEAR(state->UTC); + Py_CLEAR(state->REType); + Py_CLEAR(state->_type_marker_str); + Py_CLEAR(state->_flags_str); + Py_CLEAR(state->_pattern_str); + Py_CLEAR(state->_encoder_map_str); + Py_CLEAR(state->_decoder_map_str); + Py_CLEAR(state->_fallback_encoder_str); + Py_CLEAR(state->_raw_str); + Py_CLEAR(state->_subtype_str); + Py_CLEAR(state->_binary_str); + Py_CLEAR(state->_scope_str); + Py_CLEAR(state->_inc_str); + Py_CLEAR(state->_time_str); + Py_CLEAR(state->_bid_str); + Py_CLEAR(state->_replace_str); + Py_CLEAR(state->_astimezone_str); + Py_CLEAR(state->_id_str); + Py_CLEAR(state->_dollar_ref_str); + Py_CLEAR(state->_dollar_id_str); + Py_CLEAR(state->_dollar_db_str); + Py_CLEAR(state->_tzinfo_str); + Py_CLEAR(state->_as_doc_str); + Py_CLEAR(state->_utcoffset_str); + Py_CLEAR(state->_from_uuid_str); + Py_CLEAR(state->_as_uuid_str); + Py_CLEAR(state->_from_bid_str); + return 0; +} + +/* Multi-phase extension module initialization code. + * See https://peps.python.org/pep-0489/. +*/ +static int +_cbson_exec(PyObject *m) +{ + PyObject *c_api_object; + static void *_cbson_API[_cbson_API_POINTER_COUNT]; + + PyDateTime_IMPORT; + if (PyDateTimeAPI == NULL) { + INITERROR; + } + + /* Export C API */ + _cbson_API[_cbson_buffer_write_bytes_INDEX] = (void *) buffer_write_bytes; + _cbson_API[_cbson_write_dict_INDEX] = (void *) write_dict; + _cbson_API[_cbson_write_pair_INDEX] = (void *) write_pair; + _cbson_API[_cbson_decode_and_write_pair_INDEX] = (void *) decode_and_write_pair; + _cbson_API[_cbson_convert_codec_options_INDEX] = (void *) convert_codec_options; + _cbson_API[_cbson_destroy_codec_options_INDEX] = (void *) destroy_codec_options; + _cbson_API[_cbson_buffer_write_double_INDEX] = (void *) buffer_write_double; + _cbson_API[_cbson_buffer_write_int32_INDEX] = (void *) buffer_write_int32; + _cbson_API[_cbson_buffer_write_int64_INDEX] = (void *) buffer_write_int64; + _cbson_API[_cbson_buffer_write_int32_at_position_INDEX] = + (void *) buffer_write_int32_at_position; + _cbson_API[_cbson_downcast_and_check_INDEX] = (void *) _downcast_and_check; + + c_api_object = PyCapsule_New((void *) _cbson_API, "_cbson._C_API", NULL); + if (c_api_object == NULL) + INITERROR; + + /* Import several python objects */ + if (_load_python_objects(m)) { + Py_DECREF(c_api_object); + Py_DECREF(m); + INITERROR; + } + + if (PyModule_AddObject(m, "_C_API", c_api_object) < 0) { + Py_DECREF(c_api_object); + Py_DECREF(m); + INITERROR; + } + + return 0; +} + +static PyModuleDef_Slot _cbson_slots[] = { + {Py_mod_exec, _cbson_exec}, +#if defined(Py_MOD_MULTIPLE_INTERPRETERS_SUPPORTED) + {Py_mod_multiple_interpreters, Py_MOD_MULTIPLE_INTERPRETERS_SUPPORTED}, +#endif + {0, NULL}, +}; + + +static struct PyModuleDef moduledef = { + PyModuleDef_HEAD_INIT, + "_cbson", + NULL, + sizeof(struct module_state), + _CBSONMethods, + _cbson_slots, + _cbson_traverse, + _cbson_clear, + NULL +}; + +PyMODINIT_FUNC +PyInit__cbson(void) +{ + return PyModuleDef_Init(&moduledef); +} diff --git a/venv/Lib/site-packages/bson/_cbsonmodule.h b/venv/Lib/site-packages/bson/_cbsonmodule.h new file mode 100644 index 00000000..3be2b744 --- /dev/null +++ b/venv/Lib/site-packages/bson/_cbsonmodule.h @@ -0,0 +1,181 @@ +/* + * Copyright 2009-present MongoDB, 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. + */ + +#include "bson-endian.h" + +#ifndef _CBSONMODULE_H +#define _CBSONMODULE_H + +#if defined(WIN32) || defined(_MSC_VER) +/* + * This macro is basically an implementation of asprintf for win32 + * We print to the provided buffer to get the string value as an int. + * USE LL2STR. This is kept only to test LL2STR. + */ +#if defined(_MSC_VER) && (_MSC_VER >= 1400) +#define INT2STRING(buffer, i) \ + _snprintf_s((buffer), \ + _scprintf("%lld", (i)) + 1, \ + _scprintf("%lld", (i)) + 1, \ + "%lld", \ + (i)) +#define STRCAT(dest, n, src) strcat_s((dest), (n), (src)) +#else +#define INT2STRING(buffer, i) \ + _snprintf((buffer), \ + _scprintf("%lld", (i)) + 1, \ + "%lld", \ + (i)) +#define STRCAT(dest, n, src) strcat((dest), (src)) +#endif +#else +#define INT2STRING(buffer, i) snprintf((buffer), sizeof((buffer)), "%lld", (i)) +#define STRCAT(dest, n, src) strcat((dest), (src)) +#endif + +/* Just enough space in char array to hold LLONG_MIN and null terminator */ +#define BUF_SIZE 21 +/* Converts integer to its string representation in decimal notation. */ +extern int cbson_long_long_to_str(long long int num, char* str, size_t size); +#define LL2STR(buffer, i) cbson_long_long_to_str((i), (buffer), sizeof(buffer)) + +typedef struct type_registry_t { + PyObject* encoder_map; + PyObject* decoder_map; + PyObject* fallback_encoder; + PyObject* registry_obj; + unsigned char is_encoder_empty; + unsigned char is_decoder_empty; + unsigned char has_fallback_encoder; +} type_registry_t; + +typedef struct codec_options_t { + PyObject* document_class; + unsigned char tz_aware; + unsigned char uuid_rep; + char* unicode_decode_error_handler; + PyObject* tzinfo; + type_registry_t type_registry; + unsigned char datetime_conversion; + PyObject* options_obj; + unsigned char is_raw_bson; +} codec_options_t; + +/* C API functions */ +#define _cbson_buffer_write_bytes_INDEX 0 +#define _cbson_buffer_write_bytes_RETURN int +#define _cbson_buffer_write_bytes_PROTO (buffer_t buffer, const char* data, int size) + +#define _cbson_write_dict_INDEX 1 +#define _cbson_write_dict_RETURN int +#define _cbson_write_dict_PROTO (PyObject* self, buffer_t buffer, PyObject* dict, unsigned char check_keys, const codec_options_t* options, unsigned char top_level) + +#define _cbson_write_pair_INDEX 2 +#define _cbson_write_pair_RETURN int +#define _cbson_write_pair_PROTO (PyObject* self, buffer_t buffer, const char* name, int name_length, PyObject* value, unsigned char check_keys, const codec_options_t* options, unsigned char allow_id) + +#define _cbson_decode_and_write_pair_INDEX 3 +#define _cbson_decode_and_write_pair_RETURN int +#define _cbson_decode_and_write_pair_PROTO (PyObject* self, buffer_t buffer, PyObject* key, PyObject* value, unsigned char check_keys, const codec_options_t* options, unsigned char top_level) + +#define _cbson_convert_codec_options_INDEX 4 +#define _cbson_convert_codec_options_RETURN int +#define _cbson_convert_codec_options_PROTO (PyObject* self, PyObject* options_obj, codec_options_t* options) + +#define _cbson_destroy_codec_options_INDEX 5 +#define _cbson_destroy_codec_options_RETURN void +#define _cbson_destroy_codec_options_PROTO (codec_options_t* options) + +#define _cbson_buffer_write_double_INDEX 6 +#define _cbson_buffer_write_double_RETURN int +#define _cbson_buffer_write_double_PROTO (buffer_t buffer, double data) + +#define _cbson_buffer_write_int32_INDEX 7 +#define _cbson_buffer_write_int32_RETURN int +#define _cbson_buffer_write_int32_PROTO (buffer_t buffer, int32_t data) + +#define _cbson_buffer_write_int64_INDEX 8 +#define _cbson_buffer_write_int64_RETURN int +#define _cbson_buffer_write_int64_PROTO (buffer_t buffer, int64_t data) + +#define _cbson_buffer_write_int32_at_position_INDEX 9 +#define _cbson_buffer_write_int32_at_position_RETURN void +#define _cbson_buffer_write_int32_at_position_PROTO (buffer_t buffer, int position, int32_t data) + +#define _cbson_downcast_and_check_INDEX 10 +#define _cbson_downcast_and_check_RETURN int +#define _cbson_downcast_and_check_PROTO (Py_ssize_t size, uint8_t extra) + +/* Total number of C API pointers */ +#define _cbson_API_POINTER_COUNT 11 + +#ifdef _CBSON_MODULE +/* This section is used when compiling _cbsonmodule */ + +static _cbson_buffer_write_bytes_RETURN buffer_write_bytes _cbson_buffer_write_bytes_PROTO; + +static _cbson_write_dict_RETURN write_dict _cbson_write_dict_PROTO; + +static _cbson_write_pair_RETURN write_pair _cbson_write_pair_PROTO; + +static _cbson_decode_and_write_pair_RETURN decode_and_write_pair _cbson_decode_and_write_pair_PROTO; + +static _cbson_convert_codec_options_RETURN convert_codec_options _cbson_convert_codec_options_PROTO; + +static _cbson_destroy_codec_options_RETURN destroy_codec_options _cbson_destroy_codec_options_PROTO; + +static _cbson_buffer_write_double_RETURN buffer_write_double _cbson_buffer_write_double_PROTO; + +static _cbson_buffer_write_int32_RETURN buffer_write_int32 _cbson_buffer_write_int32_PROTO; + +static _cbson_buffer_write_int64_RETURN buffer_write_int64 _cbson_buffer_write_int64_PROTO; + +static _cbson_buffer_write_int32_at_position_RETURN buffer_write_int32_at_position _cbson_buffer_write_int32_at_position_PROTO; + +static _cbson_downcast_and_check_RETURN _downcast_and_check _cbson_downcast_and_check_PROTO; + +#else +/* This section is used in modules that use _cbsonmodule's API */ + +static void **_cbson_API; + +#define buffer_write_bytes (*(_cbson_buffer_write_bytes_RETURN (*)_cbson_buffer_write_bytes_PROTO) _cbson_API[_cbson_buffer_write_bytes_INDEX]) + +#define write_dict (*(_cbson_write_dict_RETURN (*)_cbson_write_dict_PROTO) _cbson_API[_cbson_write_dict_INDEX]) + +#define write_pair (*(_cbson_write_pair_RETURN (*)_cbson_write_pair_PROTO) _cbson_API[_cbson_write_pair_INDEX]) + +#define decode_and_write_pair (*(_cbson_decode_and_write_pair_RETURN (*)_cbson_decode_and_write_pair_PROTO) _cbson_API[_cbson_decode_and_write_pair_INDEX]) + +#define convert_codec_options (*(_cbson_convert_codec_options_RETURN (*)_cbson_convert_codec_options_PROTO) _cbson_API[_cbson_convert_codec_options_INDEX]) + +#define destroy_codec_options (*(_cbson_destroy_codec_options_RETURN (*)_cbson_destroy_codec_options_PROTO) _cbson_API[_cbson_destroy_codec_options_INDEX]) + +#define buffer_write_double (*(_cbson_buffer_write_double_RETURN (*)_cbson_buffer_write_double_PROTO) _cbson_API[_cbson_buffer_write_double_INDEX]) + +#define buffer_write_int32 (*(_cbson_buffer_write_int32_RETURN (*)_cbson_buffer_write_int32_PROTO) _cbson_API[_cbson_buffer_write_int32_INDEX]) + +#define buffer_write_int64 (*(_cbson_buffer_write_int64_RETURN (*)_cbson_buffer_write_int64_PROTO) _cbson_API[_cbson_buffer_write_int64_INDEX]) + +#define buffer_write_int32_at_position (*(_cbson_buffer_write_int32_at_position_RETURN (*)_cbson_buffer_write_int32_at_position_PROTO) _cbson_API[_cbson_buffer_write_int32_at_position_INDEX]) + +#define _downcast_and_check (*(_cbson_downcast_and_check_RETURN (*)_cbson_downcast_and_check_PROTO) _cbson_API[_cbson_downcast_and_check_INDEX]) + +#define _cbson_IMPORT _cbson_API = (void **)PyCapsule_Import("_cbson._C_API", 0) + +#endif + +#endif // _CBSONMODULE_H diff --git a/venv/Lib/site-packages/bson/_helpers.py b/venv/Lib/site-packages/bson/_helpers.py new file mode 100644 index 00000000..5a479867 --- /dev/null +++ b/venv/Lib/site-packages/bson/_helpers.py @@ -0,0 +1,43 @@ +# Copyright 2021-present MongoDB, 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. + +"""Setstate and getstate functions for objects with __slots__, allowing +compatibility with default pickling protocol +""" +from __future__ import annotations + +from typing import Any, Mapping + + +def _setstate_slots(self: Any, state: Any) -> None: + for slot, value in state.items(): + setattr(self, slot, value) + + +def _mangle_name(name: str, prefix: str) -> str: + if name.startswith("__"): + prefix = "_" + prefix + else: + prefix = "" + return prefix + name + + +def _getstate_slots(self: Any) -> Mapping[Any, Any]: + prefix = self.__class__.__name__ + ret = {} + for name in self.__slots__: + mangled_name = _mangle_name(name, prefix) + if hasattr(self, mangled_name): + ret[mangled_name] = getattr(self, mangled_name) + return ret diff --git a/venv/Lib/site-packages/bson/binary.py b/venv/Lib/site-packages/bson/binary.py new file mode 100644 index 00000000..be334644 --- /dev/null +++ b/venv/Lib/site-packages/bson/binary.py @@ -0,0 +1,367 @@ +# Copyright 2009-present MongoDB, 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. +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, Tuple, Type, Union +from uuid import UUID + +"""Tools for representing BSON binary data. +""" + +BINARY_SUBTYPE = 0 +"""BSON binary subtype for binary data. + +This is the default subtype for binary data. +""" + +FUNCTION_SUBTYPE = 1 +"""BSON binary subtype for functions. +""" + +OLD_BINARY_SUBTYPE = 2 +"""Old BSON binary subtype for binary data. + +This is the old default subtype, the current +default is :data:`BINARY_SUBTYPE`. +""" + +OLD_UUID_SUBTYPE = 3 +"""Old BSON binary subtype for a UUID. + +:class:`uuid.UUID` instances will automatically be encoded +by :mod:`bson` using this subtype when using +:data:`UuidRepresentation.PYTHON_LEGACY`, +:data:`UuidRepresentation.JAVA_LEGACY`, or +:data:`UuidRepresentation.CSHARP_LEGACY`. + +.. versionadded:: 2.1 +""" + +UUID_SUBTYPE = 4 +"""BSON binary subtype for a UUID. + +This is the standard BSON binary subtype for UUIDs. +:class:`uuid.UUID` instances will automatically be encoded +by :mod:`bson` using this subtype when using +:data:`UuidRepresentation.STANDARD`. +""" + + +if TYPE_CHECKING: + from array import array as _array + from mmap import mmap as _mmap + + +class UuidRepresentation: + UNSPECIFIED = 0 + """An unspecified UUID representation. + + When configured, :class:`uuid.UUID` instances will **not** be + automatically encoded to or decoded from :class:`~bson.binary.Binary`. + When encoding a :class:`uuid.UUID` instance, an error will be raised. + To encode a :class:`uuid.UUID` instance with this configuration, it must + be wrapped in the :class:`~bson.binary.Binary` class by the application + code. When decoding a BSON binary field with a UUID subtype, a + :class:`~bson.binary.Binary` instance will be returned instead of a + :class:`uuid.UUID` instance. + + See :ref:`unspecified-representation-details` for details. + + .. versionadded:: 3.11 + """ + + STANDARD = UUID_SUBTYPE + """The standard UUID representation. + + :class:`uuid.UUID` instances will automatically be encoded to + and decoded from BSON binary, using RFC-4122 byte order with + binary subtype :data:`UUID_SUBTYPE`. + + See :ref:`standard-representation-details` for details. + + .. versionadded:: 3.11 + """ + + PYTHON_LEGACY = OLD_UUID_SUBTYPE + """The Python legacy UUID representation. + + :class:`uuid.UUID` instances will automatically be encoded to + and decoded from BSON binary, using RFC-4122 byte order with + binary subtype :data:`OLD_UUID_SUBTYPE`. + + See :ref:`python-legacy-representation-details` for details. + + .. versionadded:: 3.11 + """ + + JAVA_LEGACY = 5 + """The Java legacy UUID representation. + + :class:`uuid.UUID` instances will automatically be encoded to + and decoded from BSON binary subtype :data:`OLD_UUID_SUBTYPE`, + using the Java driver's legacy byte order. + + See :ref:`java-legacy-representation-details` for details. + + .. versionadded:: 3.11 + """ + + CSHARP_LEGACY = 6 + """The C#/.net legacy UUID representation. + + :class:`uuid.UUID` instances will automatically be encoded to + and decoded from BSON binary subtype :data:`OLD_UUID_SUBTYPE`, + using the C# driver's legacy byte order. + + See :ref:`csharp-legacy-representation-details` for details. + + .. versionadded:: 3.11 + """ + + +STANDARD = UuidRepresentation.STANDARD +"""An alias for :data:`UuidRepresentation.STANDARD`. + +.. versionadded:: 3.0 +""" + +PYTHON_LEGACY = UuidRepresentation.PYTHON_LEGACY +"""An alias for :data:`UuidRepresentation.PYTHON_LEGACY`. + +.. versionadded:: 3.0 +""" + +JAVA_LEGACY = UuidRepresentation.JAVA_LEGACY +"""An alias for :data:`UuidRepresentation.JAVA_LEGACY`. + +.. versionchanged:: 3.6 + BSON binary subtype 4 is decoded using RFC-4122 byte order. +.. versionadded:: 2.3 +""" + +CSHARP_LEGACY = UuidRepresentation.CSHARP_LEGACY +"""An alias for :data:`UuidRepresentation.CSHARP_LEGACY`. + +.. versionchanged:: 3.6 + BSON binary subtype 4 is decoded using RFC-4122 byte order. +.. versionadded:: 2.3 +""" + +ALL_UUID_SUBTYPES = (OLD_UUID_SUBTYPE, UUID_SUBTYPE) +ALL_UUID_REPRESENTATIONS = ( + UuidRepresentation.UNSPECIFIED, + UuidRepresentation.STANDARD, + UuidRepresentation.PYTHON_LEGACY, + UuidRepresentation.JAVA_LEGACY, + UuidRepresentation.CSHARP_LEGACY, +) +UUID_REPRESENTATION_NAMES = { + UuidRepresentation.UNSPECIFIED: "UuidRepresentation.UNSPECIFIED", + UuidRepresentation.STANDARD: "UuidRepresentation.STANDARD", + UuidRepresentation.PYTHON_LEGACY: "UuidRepresentation.PYTHON_LEGACY", + UuidRepresentation.JAVA_LEGACY: "UuidRepresentation.JAVA_LEGACY", + UuidRepresentation.CSHARP_LEGACY: "UuidRepresentation.CSHARP_LEGACY", +} + +MD5_SUBTYPE = 5 +"""BSON binary subtype for an MD5 hash. +""" + +COLUMN_SUBTYPE = 7 +"""BSON binary subtype for columns. + +.. versionadded:: 4.0 +""" + +SENSITIVE_SUBTYPE = 8 +"""BSON binary subtype for sensitive data. + +.. versionadded:: 4.5 +""" + + +USER_DEFINED_SUBTYPE = 128 +"""BSON binary subtype for any user defined structure. +""" + + +class Binary(bytes): + """Representation of BSON binary data. + + This is necessary because we want to represent Python strings as + the BSON string type. We need to wrap binary data so we can tell + the difference between what should be considered binary data and + what should be considered a string when we encode to BSON. + + Raises TypeError if `data` is not an instance of :class:`bytes` + or `subtype` is not an instance of :class:`int`. + Raises ValueError if `subtype` is not in [0, 256). + + .. note:: + Instances of Binary with subtype 0 will be decoded directly to :class:`bytes`. + + :param data: the binary data to represent. Can be any bytes-like type + that implements the buffer protocol. + :param subtype: the `binary subtype + `_ + to use + + .. versionchanged:: 3.9 + Support any bytes-like type that implements the buffer protocol. + """ + + _type_marker = 5 + __subtype: int + + def __new__( + cls: Type[Binary], + data: Union[memoryview, bytes, _mmap, _array[Any]], + subtype: int = BINARY_SUBTYPE, + ) -> Binary: + if not isinstance(subtype, int): + raise TypeError("subtype must be an instance of int") + if subtype >= 256 or subtype < 0: + raise ValueError("subtype must be contained in [0, 256)") + # Support any type that implements the buffer protocol. + self = bytes.__new__(cls, memoryview(data).tobytes()) + self.__subtype = subtype + return self + + @classmethod + def from_uuid( + cls: Type[Binary], uuid: UUID, uuid_representation: int = UuidRepresentation.STANDARD + ) -> Binary: + """Create a BSON Binary object from a Python UUID. + + Creates a :class:`~bson.binary.Binary` object from a + :class:`uuid.UUID` instance. Assumes that the native + :class:`uuid.UUID` instance uses the byte-order implied by the + provided ``uuid_representation``. + + Raises :exc:`TypeError` if `uuid` is not an instance of + :class:`~uuid.UUID`. + + :param uuid: A :class:`uuid.UUID` instance. + :param uuid_representation: A member of + :class:`~bson.binary.UuidRepresentation`. Default: + :const:`~bson.binary.UuidRepresentation.STANDARD`. + See :ref:`handling-uuid-data-example` for details. + + .. versionadded:: 3.11 + """ + if not isinstance(uuid, UUID): + raise TypeError("uuid must be an instance of uuid.UUID") + + if uuid_representation not in ALL_UUID_REPRESENTATIONS: + raise ValueError( + "uuid_representation must be a value from bson.binary.UuidRepresentation" + ) + + if uuid_representation == UuidRepresentation.UNSPECIFIED: + raise ValueError( + "cannot encode native uuid.UUID with " + "UuidRepresentation.UNSPECIFIED. UUIDs can be manually " + "converted to bson.Binary instances using " + "bson.Binary.from_uuid() or a different UuidRepresentation " + "can be configured. See the documentation for " + "UuidRepresentation for more information." + ) + + subtype = OLD_UUID_SUBTYPE + if uuid_representation == UuidRepresentation.PYTHON_LEGACY: + payload = uuid.bytes + elif uuid_representation == UuidRepresentation.JAVA_LEGACY: + from_uuid = uuid.bytes + payload = from_uuid[0:8][::-1] + from_uuid[8:16][::-1] + elif uuid_representation == UuidRepresentation.CSHARP_LEGACY: + payload = uuid.bytes_le + else: + # uuid_representation == UuidRepresentation.STANDARD + subtype = UUID_SUBTYPE + payload = uuid.bytes + + return cls(payload, subtype) + + def as_uuid(self, uuid_representation: int = UuidRepresentation.STANDARD) -> UUID: + """Create a Python UUID from this BSON Binary object. + + Decodes this binary object as a native :class:`uuid.UUID` instance + with the provided ``uuid_representation``. + + Raises :exc:`ValueError` if this :class:`~bson.binary.Binary` instance + does not contain a UUID. + + :param uuid_representation: A member of + :class:`~bson.binary.UuidRepresentation`. Default: + :const:`~bson.binary.UuidRepresentation.STANDARD`. + See :ref:`handling-uuid-data-example` for details. + + .. versionadded:: 3.11 + """ + if self.subtype not in ALL_UUID_SUBTYPES: + raise ValueError(f"cannot decode subtype {self.subtype} as a uuid") + + if uuid_representation not in ALL_UUID_REPRESENTATIONS: + raise ValueError( + "uuid_representation must be a value from bson.binary.UuidRepresentation" + ) + + if uuid_representation == UuidRepresentation.UNSPECIFIED: + raise ValueError("uuid_representation cannot be UNSPECIFIED") + elif uuid_representation == UuidRepresentation.PYTHON_LEGACY: + if self.subtype == OLD_UUID_SUBTYPE: + return UUID(bytes=self) + elif uuid_representation == UuidRepresentation.JAVA_LEGACY: + if self.subtype == OLD_UUID_SUBTYPE: + return UUID(bytes=self[0:8][::-1] + self[8:16][::-1]) + elif uuid_representation == UuidRepresentation.CSHARP_LEGACY: + if self.subtype == OLD_UUID_SUBTYPE: + return UUID(bytes_le=self) + else: + # uuid_representation == UuidRepresentation.STANDARD + if self.subtype == UUID_SUBTYPE: + return UUID(bytes=self) + + raise ValueError( + f"cannot decode subtype {self.subtype} to {UUID_REPRESENTATION_NAMES[uuid_representation]}" + ) + + @property + def subtype(self) -> int: + """Subtype of this binary data.""" + return self.__subtype + + def __getnewargs__(self) -> Tuple[bytes, int]: # type: ignore[override] + # Work around http://bugs.python.org/issue7382 + data = super().__getnewargs__()[0] + if not isinstance(data, bytes): + data = data.encode("latin-1") + return data, self.__subtype + + def __eq__(self, other: Any) -> bool: + if isinstance(other, Binary): + return (self.__subtype, bytes(self)) == (other.subtype, bytes(other)) + # We don't return NotImplemented here because if we did then + # Binary("foo") == "foo" would return True, since Binary is a + # subclass of str... + return False + + def __hash__(self) -> int: + return super().__hash__() ^ hash(self.__subtype) + + def __ne__(self, other: Any) -> bool: + return not self == other + + def __repr__(self) -> str: + return f"Binary({bytes.__repr__(self)}, {self.__subtype})" diff --git a/venv/Lib/site-packages/bson/bson-endian.h b/venv/Lib/site-packages/bson/bson-endian.h new file mode 100644 index 00000000..e906b077 --- /dev/null +++ b/venv/Lib/site-packages/bson/bson-endian.h @@ -0,0 +1,233 @@ +/* + * Copyright 2013-2016 MongoDB, 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. + */ + + +#ifndef BSON_ENDIAN_H +#define BSON_ENDIAN_H + + +#if defined(__sun) +# include +#endif + + +#ifdef _MSC_VER +# define BSON_INLINE __inline +#else +# include +# define BSON_INLINE __inline__ +#endif + + +#define BSON_BIG_ENDIAN 4321 +#define BSON_LITTLE_ENDIAN 1234 + + +/* WORDS_BIGENDIAN from pyconfig.h / Python.h */ +#ifdef WORDS_BIGENDIAN +# define BSON_BYTE_ORDER BSON_BIG_ENDIAN +#else +# define BSON_BYTE_ORDER BSON_LITTLE_ENDIAN +#endif + + +#if defined(__sun) +# define BSON_UINT16_SWAP_LE_BE(v) BSWAP_16((uint16_t)v) +# define BSON_UINT32_SWAP_LE_BE(v) BSWAP_32((uint32_t)v) +# define BSON_UINT64_SWAP_LE_BE(v) BSWAP_64((uint64_t)v) +#elif defined(__clang__) && defined(__clang_major__) && defined(__clang_minor__) && \ + (__clang_major__ >= 3) && (__clang_minor__ >= 1) +# if __has_builtin(__builtin_bswap16) +# define BSON_UINT16_SWAP_LE_BE(v) __builtin_bswap16(v) +# endif +# if __has_builtin(__builtin_bswap32) +# define BSON_UINT32_SWAP_LE_BE(v) __builtin_bswap32(v) +# endif +# if __has_builtin(__builtin_bswap64) +# define BSON_UINT64_SWAP_LE_BE(v) __builtin_bswap64(v) +# endif +#elif defined(__GNUC__) && (__GNUC__ >= 4) +# if __GNUC__ >= 4 && defined (__GNUC_MINOR__) && __GNUC_MINOR__ >= 3 +# define BSON_UINT32_SWAP_LE_BE(v) __builtin_bswap32 ((uint32_t)v) +# define BSON_UINT64_SWAP_LE_BE(v) __builtin_bswap64 ((uint64_t)v) +# endif +# if __GNUC__ >= 4 && defined (__GNUC_MINOR__) && __GNUC_MINOR__ >= 8 +# define BSON_UINT16_SWAP_LE_BE(v) __builtin_bswap16 ((uint32_t)v) +# endif +#endif + + +#ifndef BSON_UINT16_SWAP_LE_BE +# define BSON_UINT16_SWAP_LE_BE(v) __bson_uint16_swap_slow ((uint16_t)v) +#endif + + +#ifndef BSON_UINT32_SWAP_LE_BE +# define BSON_UINT32_SWAP_LE_BE(v) __bson_uint32_swap_slow ((uint32_t)v) +#endif + + +#ifndef BSON_UINT64_SWAP_LE_BE +# define BSON_UINT64_SWAP_LE_BE(v) __bson_uint64_swap_slow ((uint64_t)v) +#endif + + +#if BSON_BYTE_ORDER == BSON_LITTLE_ENDIAN +# define BSON_UINT16_FROM_LE(v) ((uint16_t)v) +# define BSON_UINT16_TO_LE(v) ((uint16_t)v) +# define BSON_UINT16_FROM_BE(v) BSON_UINT16_SWAP_LE_BE (v) +# define BSON_UINT16_TO_BE(v) BSON_UINT16_SWAP_LE_BE (v) +# define BSON_UINT32_FROM_LE(v) ((uint32_t)v) +# define BSON_UINT32_TO_LE(v) ((uint32_t)v) +# define BSON_UINT32_FROM_BE(v) BSON_UINT32_SWAP_LE_BE (v) +# define BSON_UINT32_TO_BE(v) BSON_UINT32_SWAP_LE_BE (v) +# define BSON_UINT64_FROM_LE(v) ((uint64_t)v) +# define BSON_UINT64_TO_LE(v) ((uint64_t)v) +# define BSON_UINT64_FROM_BE(v) BSON_UINT64_SWAP_LE_BE (v) +# define BSON_UINT64_TO_BE(v) BSON_UINT64_SWAP_LE_BE (v) +# define BSON_DOUBLE_FROM_LE(v) ((double)v) +# define BSON_DOUBLE_TO_LE(v) ((double)v) +#elif BSON_BYTE_ORDER == BSON_BIG_ENDIAN +# define BSON_UINT16_FROM_LE(v) BSON_UINT16_SWAP_LE_BE (v) +# define BSON_UINT16_TO_LE(v) BSON_UINT16_SWAP_LE_BE (v) +# define BSON_UINT16_FROM_BE(v) ((uint16_t)v) +# define BSON_UINT16_TO_BE(v) ((uint16_t)v) +# define BSON_UINT32_FROM_LE(v) BSON_UINT32_SWAP_LE_BE (v) +# define BSON_UINT32_TO_LE(v) BSON_UINT32_SWAP_LE_BE (v) +# define BSON_UINT32_FROM_BE(v) ((uint32_t)v) +# define BSON_UINT32_TO_BE(v) ((uint32_t)v) +# define BSON_UINT64_FROM_LE(v) BSON_UINT64_SWAP_LE_BE (v) +# define BSON_UINT64_TO_LE(v) BSON_UINT64_SWAP_LE_BE (v) +# define BSON_UINT64_FROM_BE(v) ((uint64_t)v) +# define BSON_UINT64_TO_BE(v) ((uint64_t)v) +# define BSON_DOUBLE_FROM_LE(v) (__bson_double_swap_slow (v)) +# define BSON_DOUBLE_TO_LE(v) (__bson_double_swap_slow (v)) +#else +# error "The endianness of target architecture is unknown." +#endif + + +/* + *-------------------------------------------------------------------------- + * + * __bson_uint16_swap_slow -- + * + * Fallback endianness conversion for 16-bit integers. + * + * Returns: + * The endian swapped version. + * + * Side effects: + * None. + * + *-------------------------------------------------------------------------- + */ + +static BSON_INLINE uint16_t +__bson_uint16_swap_slow (uint16_t v) /* IN */ +{ + return ((v & 0x00FF) << 8) | + ((v & 0xFF00) >> 8); +} + + +/* + *-------------------------------------------------------------------------- + * + * __bson_uint32_swap_slow -- + * + * Fallback endianness conversion for 32-bit integers. + * + * Returns: + * The endian swapped version. + * + * Side effects: + * None. + * + *-------------------------------------------------------------------------- + */ + +static BSON_INLINE uint32_t +__bson_uint32_swap_slow (uint32_t v) /* IN */ +{ + return ((v & 0x000000FFU) << 24) | + ((v & 0x0000FF00U) << 8) | + ((v & 0x00FF0000U) >> 8) | + ((v & 0xFF000000U) >> 24); +} + + +/* + *-------------------------------------------------------------------------- + * + * __bson_uint64_swap_slow -- + * + * Fallback endianness conversion for 64-bit integers. + * + * Returns: + * The endian swapped version. + * + * Side effects: + * None. + * + *-------------------------------------------------------------------------- + */ + +static BSON_INLINE uint64_t +__bson_uint64_swap_slow (uint64_t v) /* IN */ +{ + return ((v & 0x00000000000000FFULL) << 56) | + ((v & 0x000000000000FF00ULL) << 40) | + ((v & 0x0000000000FF0000ULL) << 24) | + ((v & 0x00000000FF000000ULL) << 8) | + ((v & 0x000000FF00000000ULL) >> 8) | + ((v & 0x0000FF0000000000ULL) >> 24) | + ((v & 0x00FF000000000000ULL) >> 40) | + ((v & 0xFF00000000000000ULL) >> 56); +} + + +/* + *-------------------------------------------------------------------------- + * + * __bson_double_swap_slow -- + * + * Fallback endianness conversion for double floating point. + * + * Returns: + * The endian swapped version. + * + * Side effects: + * None. + * + *-------------------------------------------------------------------------- + */ + + +static BSON_INLINE double +__bson_double_swap_slow (double v) /* IN */ +{ + uint64_t uv; + + memcpy(&uv, &v, sizeof(v)); + uv = BSON_UINT64_SWAP_LE_BE(uv); + memcpy(&v, &uv, sizeof(v)); + + return v; +} + + +#endif /* BSON_ENDIAN_H */ diff --git a/venv/Lib/site-packages/bson/buffer.c b/venv/Lib/site-packages/bson/buffer.c new file mode 100644 index 00000000..cc752027 --- /dev/null +++ b/venv/Lib/site-packages/bson/buffer.c @@ -0,0 +1,157 @@ +/* + * Copyright 2009-2015 MongoDB, 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. + */ + +/* Include Python.h so we can set Python's error indicator. */ +#define PY_SSIZE_T_CLEAN +#include "Python.h" + +#include +#include + +#include "buffer.h" + +#define INITIAL_BUFFER_SIZE 256 + +struct buffer { + char* buffer; + int size; + int position; +}; + +/* Set Python's error indicator to MemoryError. + * Called after allocation failures. */ +static void set_memory_error(void) { + PyErr_NoMemory(); +} + +/* Allocate and return a new buffer. + * Return NULL and sets MemoryError on allocation failure. */ +buffer_t pymongo_buffer_new(void) { + buffer_t buffer; + buffer = (buffer_t)malloc(sizeof(struct buffer)); + if (buffer == NULL) { + set_memory_error(); + return NULL; + } + + buffer->size = INITIAL_BUFFER_SIZE; + buffer->position = 0; + buffer->buffer = (char*)malloc(sizeof(char) * INITIAL_BUFFER_SIZE); + if (buffer->buffer == NULL) { + free(buffer); + set_memory_error(); + return NULL; + } + + return buffer; +} + +/* Free the memory allocated for `buffer`. + * Return non-zero on failure. */ +int pymongo_buffer_free(buffer_t buffer) { + if (buffer == NULL) { + return 1; + } + /* Buffer will be NULL when buffer_grow fails. */ + if (buffer->buffer != NULL) { + free(buffer->buffer); + } + free(buffer); + return 0; +} + +/* Grow `buffer` to at least `min_length`. + * Return non-zero and sets MemoryError on allocation failure. */ +static int buffer_grow(buffer_t buffer, int min_length) { + int old_size = 0; + int size = buffer->size; + char* old_buffer = buffer->buffer; + if (size >= min_length) { + return 0; + } + while (size < min_length) { + old_size = size; + size *= 2; + if (size <= old_size) { + /* Size did not increase. Could be an overflow + * or size < 1. Just go with min_length. */ + size = min_length; + } + } + buffer->buffer = (char*)realloc(buffer->buffer, sizeof(char) * size); + if (buffer->buffer == NULL) { + free(old_buffer); + set_memory_error(); + return 1; + } + buffer->size = size; + return 0; +} + +/* Assure that `buffer` has at least `size` free bytes (and grow if needed). + * Return non-zero and sets MemoryError on allocation failure. + * Return non-zero and sets ValueError if `size` would exceed 2GiB. */ +static int buffer_assure_space(buffer_t buffer, int size) { + int new_size = buffer->position + size; + /* Check for overflow. */ + if (new_size < buffer->position) { + PyErr_SetString(PyExc_ValueError, + "Document would overflow BSON size limit"); + return 1; + } + + if (new_size <= buffer->size) { + return 0; + } + return buffer_grow(buffer, new_size); +} + +/* Save `size` bytes from the current position in `buffer` (and grow if needed). + * Return offset for writing, or -1 on failure. + * Sets MemoryError or ValueError on failure. */ +buffer_position pymongo_buffer_save_space(buffer_t buffer, int size) { + int position = buffer->position; + if (buffer_assure_space(buffer, size) != 0) { + return -1; + } + buffer->position += size; + return position; +} + +/* Write `size` bytes from `data` to `buffer` (and grow if needed). + * Return non-zero on failure. + * Sets MemoryError or ValueError on failure. */ +int pymongo_buffer_write(buffer_t buffer, const char* data, int size) { + if (buffer_assure_space(buffer, size) != 0) { + return 1; + } + + memcpy(buffer->buffer + buffer->position, data, size); + buffer->position += size; + return 0; +} + +int pymongo_buffer_get_position(buffer_t buffer) { + return buffer->position; +} + +char* pymongo_buffer_get_buffer(buffer_t buffer) { + return buffer->buffer; +} + +void pymongo_buffer_update_position(buffer_t buffer, buffer_position new_position) { + buffer->position = new_position; +} diff --git a/venv/Lib/site-packages/bson/buffer.h b/venv/Lib/site-packages/bson/buffer.h new file mode 100644 index 00000000..a78e34e4 --- /dev/null +++ b/venv/Lib/site-packages/bson/buffer.h @@ -0,0 +1,51 @@ +/* + * Copyright 2009-2015 MongoDB, 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. + */ + +#ifndef BUFFER_H +#define BUFFER_H + +/* Note: if any of these functions return a failure condition then the buffer + * has already been freed. */ + +/* A buffer */ +typedef struct buffer* buffer_t; +/* A position in the buffer */ +typedef int buffer_position; + +/* Allocate and return a new buffer. + * Return NULL on allocation failure. */ +buffer_t pymongo_buffer_new(void); + +/* Free the memory allocated for `buffer`. + * Return non-zero on failure. */ +int pymongo_buffer_free(buffer_t buffer); + +/* Save `size` bytes from the current position in `buffer` (and grow if needed). + * Return offset for writing, or -1 on allocation failure. */ +buffer_position pymongo_buffer_save_space(buffer_t buffer, int size); + +/* Write `size` bytes from `data` to `buffer` (and grow if needed). + * Return non-zero on allocation failure. */ +int pymongo_buffer_write(buffer_t buffer, const char* data, int size); + +/* Getters for the internals of a buffer_t. + * Should try to avoid using these as much as possible + * since they break the abstraction. */ +buffer_position pymongo_buffer_get_position(buffer_t buffer); +char* pymongo_buffer_get_buffer(buffer_t buffer); +void pymongo_buffer_update_position(buffer_t buffer, buffer_position new_position); + +#endif diff --git a/venv/Lib/site-packages/bson/code.py b/venv/Lib/site-packages/bson/code.py new file mode 100644 index 00000000..6b4541d0 --- /dev/null +++ b/venv/Lib/site-packages/bson/code.py @@ -0,0 +1,100 @@ +# Copyright 2009-present MongoDB, 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. + +"""Tools for representing JavaScript code in BSON.""" +from __future__ import annotations + +from collections.abc import Mapping as _Mapping +from typing import Any, Mapping, Optional, Type, Union + + +class Code(str): + """BSON's JavaScript code type. + + Raises :class:`TypeError` if `code` is not an instance of + :class:`str` or `scope` is not ``None`` or an instance + of :class:`dict`. + + Scope variables can be set by passing a dictionary as the `scope` + argument or by using keyword arguments. If a variable is set as a + keyword argument it will override any setting for that variable in + the `scope` dictionary. + + :param code: A string containing JavaScript code to be evaluated or another + instance of Code. In the latter case, the scope of `code` becomes this + Code's :attr:`scope`. + :param scope: dictionary representing the scope in which + `code` should be evaluated - a mapping from identifiers (as + strings) to values. Defaults to ``None``. This is applied after any + scope associated with a given `code` above. + :param kwargs: scope variables can also be passed as + keyword arguments. These are applied after `scope` and `code`. + + .. versionchanged:: 3.4 + The default value for :attr:`scope` is ``None`` instead of ``{}``. + + """ + + _type_marker = 13 + __scope: Union[Mapping[str, Any], None] + + def __new__( + cls: Type[Code], + code: Union[str, Code], + scope: Optional[Mapping[str, Any]] = None, + **kwargs: Any, + ) -> Code: + if not isinstance(code, str): + raise TypeError("code must be an instance of str") + + self = str.__new__(cls, code) + + try: + self.__scope = code.scope # type: ignore + except AttributeError: + self.__scope = None + + if scope is not None: + if not isinstance(scope, _Mapping): + raise TypeError("scope must be an instance of dict") + if self.__scope is not None: + self.__scope.update(scope) # type: ignore + else: + self.__scope = scope + + if kwargs: + if self.__scope is not None: + self.__scope.update(kwargs) # type: ignore + else: + self.__scope = kwargs + + return self + + @property + def scope(self) -> Optional[Mapping[str, Any]]: + """Scope dictionary for this instance or ``None``.""" + return self.__scope + + def __repr__(self) -> str: + return f"Code({str.__repr__(self)}, {self.__scope!r})" + + def __eq__(self, other: Any) -> bool: + if isinstance(other, Code): + return (self.__scope, str(self)) == (other.__scope, str(other)) + return False + + __hash__: Any = None + + def __ne__(self, other: Any) -> bool: + return not self == other diff --git a/venv/Lib/site-packages/bson/codec_options.py b/venv/Lib/site-packages/bson/codec_options.py new file mode 100644 index 00000000..3a0b83b7 --- /dev/null +++ b/venv/Lib/site-packages/bson/codec_options.py @@ -0,0 +1,505 @@ +# Copyright 2014-present MongoDB, 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. + +"""Tools for specifying BSON codec options.""" +from __future__ import annotations + +import abc +import datetime +import enum +from collections.abc import MutableMapping as _MutableMapping +from typing import ( + TYPE_CHECKING, + Any, + Callable, + Generic, + Iterable, + Mapping, + NamedTuple, + Optional, + Tuple, + Type, + Union, + cast, +) + +from bson.binary import ( + ALL_UUID_REPRESENTATIONS, + UUID_REPRESENTATION_NAMES, + UuidRepresentation, +) +from bson.typings import _DocumentType + +_RAW_BSON_DOCUMENT_MARKER = 101 + + +def _raw_document_class(document_class: Any) -> bool: + """Determine if a document_class is a RawBSONDocument class.""" + marker = getattr(document_class, "_type_marker", None) + return marker == _RAW_BSON_DOCUMENT_MARKER + + +class TypeEncoder(abc.ABC): + """Base class for defining type codec classes which describe how a + custom type can be transformed to one of the types BSON understands. + + Codec classes must implement the ``python_type`` attribute, and the + ``transform_python`` method to support encoding. + + See :ref:`custom-type-type-codec` documentation for an example. + """ + + @abc.abstractproperty + def python_type(self) -> Any: + """The Python type to be converted into something serializable.""" + + @abc.abstractmethod + def transform_python(self, value: Any) -> Any: + """Convert the given Python object into something serializable.""" + + +class TypeDecoder(abc.ABC): + """Base class for defining type codec classes which describe how a + BSON type can be transformed to a custom type. + + Codec classes must implement the ``bson_type`` attribute, and the + ``transform_bson`` method to support decoding. + + See :ref:`custom-type-type-codec` documentation for an example. + """ + + @abc.abstractproperty + def bson_type(self) -> Any: + """The BSON type to be converted into our own type.""" + + @abc.abstractmethod + def transform_bson(self, value: Any) -> Any: + """Convert the given BSON value into our own type.""" + + +class TypeCodec(TypeEncoder, TypeDecoder): + """Base class for defining type codec classes which describe how a + custom type can be transformed to/from one of the types :mod:`bson` + can already encode/decode. + + Codec classes must implement the ``python_type`` attribute, and the + ``transform_python`` method to support encoding, as well as the + ``bson_type`` attribute, and the ``transform_bson`` method to support + decoding. + + See :ref:`custom-type-type-codec` documentation for an example. + """ + + +_Codec = Union[TypeEncoder, TypeDecoder, TypeCodec] +_Fallback = Callable[[Any], Any] + + +class TypeRegistry: + """Encapsulates type codecs used in encoding and / or decoding BSON, as + well as the fallback encoder. Type registries cannot be modified after + instantiation. + + ``TypeRegistry`` can be initialized with an iterable of type codecs, and + a callable for the fallback encoder:: + + >>> from bson.codec_options import TypeRegistry + >>> type_registry = TypeRegistry([Codec1, Codec2, Codec3, ...], + ... fallback_encoder) + + See :ref:`custom-type-type-registry` documentation for an example. + + :param type_codecs: iterable of type codec instances. If + ``type_codecs`` contains multiple codecs that transform a single + python or BSON type, the transformation specified by the type codec + occurring last prevails. A TypeError will be raised if one or more + type codecs modify the encoding behavior of a built-in :mod:`bson` + type. + :param fallback_encoder: callable that accepts a single, + unencodable python value and transforms it into a type that + :mod:`bson` can encode. See :ref:`fallback-encoder-callable` + documentation for an example. + """ + + def __init__( + self, + type_codecs: Optional[Iterable[_Codec]] = None, + fallback_encoder: Optional[_Fallback] = None, + ) -> None: + self.__type_codecs = list(type_codecs or []) + self._fallback_encoder = fallback_encoder + self._encoder_map: dict[Any, Any] = {} + self._decoder_map: dict[Any, Any] = {} + + if self._fallback_encoder is not None: + if not callable(fallback_encoder): + raise TypeError("fallback_encoder %r is not a callable" % (fallback_encoder)) + + for codec in self.__type_codecs: + is_valid_codec = False + if isinstance(codec, TypeEncoder): + self._validate_type_encoder(codec) + is_valid_codec = True + self._encoder_map[codec.python_type] = codec.transform_python + if isinstance(codec, TypeDecoder): + is_valid_codec = True + self._decoder_map[codec.bson_type] = codec.transform_bson + if not is_valid_codec: + raise TypeError( + f"Expected an instance of {TypeEncoder.__name__}, {TypeDecoder.__name__}, or {TypeCodec.__name__}, got {codec!r} instead" + ) + + def _validate_type_encoder(self, codec: _Codec) -> None: + from bson import _BUILT_IN_TYPES + + for pytype in _BUILT_IN_TYPES: + if issubclass(cast(TypeCodec, codec).python_type, pytype): + err_msg = ( + "TypeEncoders cannot change how built-in types are " + f"encoded (encoder {codec} transforms type {pytype})" + ) + raise TypeError(err_msg) + + def __repr__(self) -> str: + return "{}(type_codecs={!r}, fallback_encoder={!r})".format( + self.__class__.__name__, + self.__type_codecs, + self._fallback_encoder, + ) + + def __eq__(self, other: Any) -> Any: + if not isinstance(other, type(self)): + return NotImplemented + return ( + (self._decoder_map == other._decoder_map) + and (self._encoder_map == other._encoder_map) + and (self._fallback_encoder == other._fallback_encoder) + ) + + +class DatetimeConversion(int, enum.Enum): + """Options for decoding BSON datetimes.""" + + DATETIME = 1 + """Decode a BSON UTC datetime as a :class:`datetime.datetime`. + + BSON UTC datetimes that cannot be represented as a + :class:`~datetime.datetime` will raise an :class:`OverflowError` + or a :class:`ValueError`. + + .. versionadded 4.3 + """ + + DATETIME_CLAMP = 2 + """Decode a BSON UTC datetime as a :class:`datetime.datetime`, clamping + to :attr:`~datetime.datetime.min` and :attr:`~datetime.datetime.max`. + + .. versionadded 4.3 + """ + + DATETIME_MS = 3 + """Decode a BSON UTC datetime as a :class:`~bson.datetime_ms.DatetimeMS` + object. + + .. versionadded 4.3 + """ + + DATETIME_AUTO = 4 + """Decode a BSON UTC datetime as a :class:`datetime.datetime` if possible, + and a :class:`~bson.datetime_ms.DatetimeMS` if not. + + .. versionadded 4.3 + """ + + +class _BaseCodecOptions(NamedTuple): + document_class: Type[Mapping[str, Any]] + tz_aware: bool + uuid_representation: int + unicode_decode_error_handler: str + tzinfo: Optional[datetime.tzinfo] + type_registry: TypeRegistry + datetime_conversion: Optional[DatetimeConversion] + + +if TYPE_CHECKING: + + class CodecOptions(Tuple[_DocumentType], Generic[_DocumentType]): + document_class: Type[_DocumentType] + tz_aware: bool + uuid_representation: int + unicode_decode_error_handler: Optional[str] + tzinfo: Optional[datetime.tzinfo] + type_registry: TypeRegistry + datetime_conversion: Optional[int] + + def __new__( + cls: Type[CodecOptions[_DocumentType]], + document_class: Optional[Type[_DocumentType]] = ..., + tz_aware: bool = ..., + uuid_representation: Optional[int] = ..., + unicode_decode_error_handler: Optional[str] = ..., + tzinfo: Optional[datetime.tzinfo] = ..., + type_registry: Optional[TypeRegistry] = ..., + datetime_conversion: Optional[int] = ..., + ) -> CodecOptions[_DocumentType]: + ... + + # CodecOptions API + def with_options(self, **kwargs: Any) -> CodecOptions[Any]: + ... + + def _arguments_repr(self) -> str: + ... + + def _options_dict(self) -> dict[Any, Any]: + ... + + # NamedTuple API + @classmethod + def _make(cls, obj: Iterable[Any]) -> CodecOptions[_DocumentType]: + ... + + def _asdict(self) -> dict[str, Any]: + ... + + def _replace(self, **kwargs: Any) -> CodecOptions[_DocumentType]: + ... + + _source: str + _fields: Tuple[str] + +else: + + class CodecOptions(_BaseCodecOptions): + """Encapsulates options used encoding and / or decoding BSON.""" + + def __init__(self, *args, **kwargs): + """Encapsulates options used encoding and / or decoding BSON. + + The `document_class` option is used to define a custom type for use + decoding BSON documents. Access to the underlying raw BSON bytes for + a document is available using the :class:`~bson.raw_bson.RawBSONDocument` + type:: + + >>> from bson.raw_bson import RawBSONDocument + >>> from bson.codec_options import CodecOptions + >>> codec_options = CodecOptions(document_class=RawBSONDocument) + >>> coll = db.get_collection('test', codec_options=codec_options) + >>> doc = coll.find_one() + >>> doc.raw + '\\x16\\x00\\x00\\x00\\x07_id\\x00[0\\x165\\x91\\x10\\xea\\x14\\xe8\\xc5\\x8b\\x93\\x00' + + The document class can be any type that inherits from + :class:`~collections.abc.MutableMapping`:: + + >>> class AttributeDict(dict): + ... # A dict that supports attribute access. + ... def __getattr__(self, key): + ... return self[key] + ... def __setattr__(self, key, value): + ... self[key] = value + ... + >>> codec_options = CodecOptions(document_class=AttributeDict) + >>> coll = db.get_collection('test', codec_options=codec_options) + >>> doc = coll.find_one() + >>> doc._id + ObjectId('5b3016359110ea14e8c58b93') + + See :doc:`/examples/datetimes` for examples using the `tz_aware` and + `tzinfo` options. + + See :doc:`/examples/uuid` for examples using the `uuid_representation` + option. + + :param document_class: BSON documents returned in queries will be decoded + to an instance of this class. Must be a subclass of + :class:`~collections.abc.MutableMapping`. Defaults to :class:`dict`. + :param tz_aware: If ``True``, BSON datetimes will be decoded to timezone + aware instances of :class:`~datetime.datetime`. Otherwise they will be + naive. Defaults to ``False``. + :param uuid_representation: The BSON representation to use when encoding + and decoding instances of :class:`~uuid.UUID`. Defaults to + :data:`~bson.binary.UuidRepresentation.UNSPECIFIED`. New + applications should consider setting this to + :data:`~bson.binary.UuidRepresentation.STANDARD` for cross language + compatibility. See :ref:`handling-uuid-data-example` for details. + :param unicode_decode_error_handler: The error handler to apply when + a Unicode-related error occurs during BSON decoding that would + otherwise raise :exc:`UnicodeDecodeError`. Valid options include + 'strict', 'replace', 'backslashreplace', 'surrogateescape', and + 'ignore'. Defaults to 'strict'. + :param tzinfo: A :class:`~datetime.tzinfo` subclass that specifies the + timezone to/from which :class:`~datetime.datetime` objects should be + encoded/decoded. + :param type_registry: Instance of :class:`TypeRegistry` used to customize + encoding and decoding behavior. + :param datetime_conversion: Specifies how UTC datetimes should be decoded + within BSON. Valid options include 'datetime_ms' to return as a + DatetimeMS, 'datetime' to return as a datetime.datetime and + raising a ValueError for out-of-range values, 'datetime_auto' to + return DatetimeMS objects when the underlying datetime is + out-of-range and 'datetime_clamp' to clamp to the minimum and + maximum possible datetimes. Defaults to 'datetime'. + + .. versionchanged:: 4.0 + The default for `uuid_representation` was changed from + :const:`~bson.binary.UuidRepresentation.PYTHON_LEGACY` to + :const:`~bson.binary.UuidRepresentation.UNSPECIFIED`. + + .. versionadded:: 3.8 + `type_registry` attribute. + + .. warning:: Care must be taken when changing + `unicode_decode_error_handler` from its default value ('strict'). + The 'replace' and 'ignore' modes should not be used when documents + retrieved from the server will be modified in the client application + and stored back to the server. + """ + super().__init__() + + def __new__( + cls: Type[CodecOptions], + document_class: Optional[Type[Mapping[str, Any]]] = None, + tz_aware: bool = False, + uuid_representation: Optional[int] = UuidRepresentation.UNSPECIFIED, + unicode_decode_error_handler: str = "strict", + tzinfo: Optional[datetime.tzinfo] = None, + type_registry: Optional[TypeRegistry] = None, + datetime_conversion: Optional[DatetimeConversion] = DatetimeConversion.DATETIME, + ) -> CodecOptions: + doc_class = document_class or dict + # issubclass can raise TypeError for generic aliases like SON[str, Any]. + # In that case we can use the base class for the comparison. + is_mapping = False + try: + is_mapping = issubclass(doc_class, _MutableMapping) + except TypeError: + if hasattr(doc_class, "__origin__"): + is_mapping = issubclass(doc_class.__origin__, _MutableMapping) + if not (is_mapping or _raw_document_class(doc_class)): + raise TypeError( + "document_class must be dict, bson.son.SON, " + "bson.raw_bson.RawBSONDocument, or a " + "subclass of collections.abc.MutableMapping" + ) + if not isinstance(tz_aware, bool): + raise TypeError(f"tz_aware must be True or False, was: tz_aware={tz_aware}") + if uuid_representation not in ALL_UUID_REPRESENTATIONS: + raise ValueError( + "uuid_representation must be a value from bson.binary.UuidRepresentation" + ) + if not isinstance(unicode_decode_error_handler, str): + raise ValueError("unicode_decode_error_handler must be a string") + if tzinfo is not None: + if not isinstance(tzinfo, datetime.tzinfo): + raise TypeError("tzinfo must be an instance of datetime.tzinfo") + if not tz_aware: + raise ValueError("cannot specify tzinfo without also setting tz_aware=True") + + type_registry = type_registry or TypeRegistry() + + if not isinstance(type_registry, TypeRegistry): + raise TypeError("type_registry must be an instance of TypeRegistry") + + return tuple.__new__( + cls, + ( + doc_class, + tz_aware, + uuid_representation, + unicode_decode_error_handler, + tzinfo, + type_registry, + datetime_conversion, + ), + ) + + def _arguments_repr(self) -> str: + """Representation of the arguments used to create this object.""" + document_class_repr = ( + "dict" if self.document_class is dict else repr(self.document_class) + ) + + uuid_rep_repr = UUID_REPRESENTATION_NAMES.get( + self.uuid_representation, self.uuid_representation + ) + + return ( + "document_class={}, tz_aware={!r}, uuid_representation={}, " + "unicode_decode_error_handler={!r}, tzinfo={!r}, " + "type_registry={!r}, datetime_conversion={!s}".format( + document_class_repr, + self.tz_aware, + uuid_rep_repr, + self.unicode_decode_error_handler, + self.tzinfo, + self.type_registry, + self.datetime_conversion, + ) + ) + + def _options_dict(self) -> dict[str, Any]: + """Dictionary of the arguments used to create this object.""" + # TODO: PYTHON-2442 use _asdict() instead + return { + "document_class": self.document_class, + "tz_aware": self.tz_aware, + "uuid_representation": self.uuid_representation, + "unicode_decode_error_handler": self.unicode_decode_error_handler, + "tzinfo": self.tzinfo, + "type_registry": self.type_registry, + "datetime_conversion": self.datetime_conversion, + } + + def __repr__(self) -> str: + return f"{self.__class__.__name__}({self._arguments_repr()})" + + def with_options(self, **kwargs: Any) -> CodecOptions: + """Make a copy of this CodecOptions, overriding some options:: + + >>> from bson.codec_options import DEFAULT_CODEC_OPTIONS + >>> DEFAULT_CODEC_OPTIONS.tz_aware + False + >>> options = DEFAULT_CODEC_OPTIONS.with_options(tz_aware=True) + >>> options.tz_aware + True + + .. versionadded:: 3.5 + """ + opts = self._options_dict() + opts.update(kwargs) + return CodecOptions(**opts) + + +DEFAULT_CODEC_OPTIONS: CodecOptions[dict[str, Any]] = CodecOptions() + + +def _parse_codec_options(options: Any) -> CodecOptions[Any]: + """Parse BSON codec options.""" + kwargs = {} + for k in set(options) & { + "document_class", + "tz_aware", + "uuidrepresentation", + "unicode_decode_error_handler", + "tzinfo", + "type_registry", + "datetime_conversion", + }: + if k == "uuidrepresentation": + kwargs["uuid_representation"] = options[k] + else: + kwargs[k] = options[k] + return CodecOptions(**kwargs) diff --git a/venv/Lib/site-packages/bson/datetime_ms.py b/venv/Lib/site-packages/bson/datetime_ms.py new file mode 100644 index 00000000..112871a1 --- /dev/null +++ b/venv/Lib/site-packages/bson/datetime_ms.py @@ -0,0 +1,171 @@ +# Copyright 2022-present MongoDB, 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. + +"""Tools for representing the BSON datetime type. + +.. versionadded:: 4.3 +""" +from __future__ import annotations + +import calendar +import datetime +import functools +from typing import Any, Union, cast + +from bson.codec_options import DEFAULT_CODEC_OPTIONS, CodecOptions, DatetimeConversion +from bson.errors import InvalidBSON +from bson.tz_util import utc + +EPOCH_AWARE = datetime.datetime.fromtimestamp(0, utc) +EPOCH_NAIVE = EPOCH_AWARE.replace(tzinfo=None) +_DATETIME_ERROR_SUGGESTION = ( + "(Consider Using CodecOptions(datetime_conversion=DATETIME_AUTO)" + " or MongoClient(datetime_conversion='DATETIME_AUTO'))." + " See: https://pymongo.readthedocs.io/en/stable/examples/datetimes.html#handling-out-of-range-datetimes" +) + + +class DatetimeMS: + """Represents a BSON UTC datetime.""" + + __slots__ = ("_value",) + + def __init__(self, value: Union[int, datetime.datetime]): + """Represents a BSON UTC datetime. + + BSON UTC datetimes are defined as an int64 of milliseconds since the + Unix epoch. The principal use of DatetimeMS is to represent + datetimes outside the range of the Python builtin + :class:`~datetime.datetime` class when + encoding/decoding BSON. + + To decode UTC datetimes as a ``DatetimeMS``, `datetime_conversion` in + :class:`~bson.codec_options.CodecOptions` must be set to 'datetime_ms' or + 'datetime_auto'. See :ref:`handling-out-of-range-datetimes` for + details. + + :param value: An instance of :class:`datetime.datetime` to be + represented as milliseconds since the Unix epoch, or int of + milliseconds since the Unix epoch. + """ + if isinstance(value, int): + if not (-(2**63) <= value <= 2**63 - 1): + raise OverflowError("Must be a 64-bit integer of milliseconds") + self._value = value + elif isinstance(value, datetime.datetime): + self._value = _datetime_to_millis(value) + else: + raise TypeError(f"{type(value)} is not a valid type for DatetimeMS") + + def __hash__(self) -> int: + return hash(self._value) + + def __repr__(self) -> str: + return type(self).__name__ + "(" + str(self._value) + ")" + + def __lt__(self, other: Union[DatetimeMS, int]) -> bool: + return self._value < other + + def __le__(self, other: Union[DatetimeMS, int]) -> bool: + return self._value <= other + + def __eq__(self, other: Any) -> bool: + if isinstance(other, DatetimeMS): + return self._value == other._value + return False + + def __ne__(self, other: Any) -> bool: + if isinstance(other, DatetimeMS): + return self._value != other._value + return True + + def __gt__(self, other: Union[DatetimeMS, int]) -> bool: + return self._value > other + + def __ge__(self, other: Union[DatetimeMS, int]) -> bool: + return self._value >= other + + _type_marker = 9 + + def as_datetime( + self, codec_options: CodecOptions[Any] = DEFAULT_CODEC_OPTIONS + ) -> datetime.datetime: + """Create a Python :class:`~datetime.datetime` from this DatetimeMS object. + + :param codec_options: A CodecOptions instance for specifying how the + resulting DatetimeMS object will be formatted using ``tz_aware`` + and ``tz_info``. Defaults to + :const:`~bson.codec_options.DEFAULT_CODEC_OPTIONS`. + """ + return cast(datetime.datetime, _millis_to_datetime(self._value, codec_options)) + + def __int__(self) -> int: + return self._value + + +# Inclusive and exclusive min and max for timezones. +# Timezones are hashed by their offset, which is a timedelta +# and therefore there are more than 24 possible timezones. +@functools.lru_cache(maxsize=None) +def _min_datetime_ms(tz: datetime.timezone = datetime.timezone.utc) -> int: + return _datetime_to_millis(datetime.datetime.min.replace(tzinfo=tz)) + + +@functools.lru_cache(maxsize=None) +def _max_datetime_ms(tz: datetime.timezone = datetime.timezone.utc) -> int: + return _datetime_to_millis(datetime.datetime.max.replace(tzinfo=tz)) + + +def _millis_to_datetime( + millis: int, opts: CodecOptions[Any] +) -> Union[datetime.datetime, DatetimeMS]: + """Convert milliseconds since epoch UTC to datetime.""" + if ( + opts.datetime_conversion == DatetimeConversion.DATETIME + or opts.datetime_conversion == DatetimeConversion.DATETIME_CLAMP + or opts.datetime_conversion == DatetimeConversion.DATETIME_AUTO + ): + tz = opts.tzinfo or datetime.timezone.utc + if opts.datetime_conversion == DatetimeConversion.DATETIME_CLAMP: + millis = max(_min_datetime_ms(tz), min(millis, _max_datetime_ms(tz))) + elif opts.datetime_conversion == DatetimeConversion.DATETIME_AUTO: + if not (_min_datetime_ms(tz) <= millis <= _max_datetime_ms(tz)): + return DatetimeMS(millis) + + diff = ((millis % 1000) + 1000) % 1000 + seconds = (millis - diff) // 1000 + micros = diff * 1000 + + try: + if opts.tz_aware: + dt = EPOCH_AWARE + datetime.timedelta(seconds=seconds, microseconds=micros) + if opts.tzinfo: + dt = dt.astimezone(tz) + return dt + else: + return EPOCH_NAIVE + datetime.timedelta(seconds=seconds, microseconds=micros) + except ArithmeticError as err: + raise InvalidBSON(f"{err} {_DATETIME_ERROR_SUGGESTION}") from err + + elif opts.datetime_conversion == DatetimeConversion.DATETIME_MS: + return DatetimeMS(millis) + else: + raise ValueError("datetime_conversion must be an element of DatetimeConversion") + + +def _datetime_to_millis(dtm: datetime.datetime) -> int: + """Convert datetime to milliseconds since epoch UTC.""" + if dtm.utcoffset() is not None: + dtm = dtm - dtm.utcoffset() # type: ignore + return int(calendar.timegm(dtm.timetuple()) * 1000 + dtm.microsecond // 1000) diff --git a/venv/Lib/site-packages/bson/dbref.py b/venv/Lib/site-packages/bson/dbref.py new file mode 100644 index 00000000..6c21b816 --- /dev/null +++ b/venv/Lib/site-packages/bson/dbref.py @@ -0,0 +1,133 @@ +# Copyright 2009-2015 MongoDB, 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. + +"""Tools for manipulating DBRefs (references to MongoDB documents).""" +from __future__ import annotations + +from copy import deepcopy +from typing import Any, Mapping, Optional + +from bson._helpers import _getstate_slots, _setstate_slots +from bson.son import SON + + +class DBRef: + """A reference to a document stored in MongoDB.""" + + __slots__ = "__collection", "__id", "__database", "__kwargs" + __getstate__ = _getstate_slots + __setstate__ = _setstate_slots + # DBRef isn't actually a BSON "type" so this number was arbitrarily chosen. + _type_marker = 100 + + def __init__( + self, + collection: str, + id: Any, + database: Optional[str] = None, + _extra: Optional[Mapping[str, Any]] = None, + **kwargs: Any, + ) -> None: + """Initialize a new :class:`DBRef`. + + Raises :class:`TypeError` if `collection` or `database` is not + an instance of :class:`str`. `database` is optional and allows + references to documents to work across databases. Any additional + keyword arguments will create additional fields in the resultant + embedded document. + + :param collection: name of the collection the document is stored in + :param id: the value of the document's ``"_id"`` field + :param database: name of the database to reference + :param kwargs: additional keyword arguments will + create additional, custom fields + + .. seealso:: The MongoDB documentation on `dbrefs `_. + """ + if not isinstance(collection, str): + raise TypeError("collection must be an instance of str") + if database is not None and not isinstance(database, str): + raise TypeError("database must be an instance of str") + + self.__collection = collection + self.__id = id + self.__database = database + kwargs.update(_extra or {}) + self.__kwargs = kwargs + + @property + def collection(self) -> str: + """Get the name of this DBRef's collection.""" + return self.__collection + + @property + def id(self) -> Any: + """Get this DBRef's _id.""" + return self.__id + + @property + def database(self) -> Optional[str]: + """Get the name of this DBRef's database. + + Returns None if this DBRef doesn't specify a database. + """ + return self.__database + + def __getattr__(self, key: Any) -> Any: + try: + return self.__kwargs[key] + except KeyError: + raise AttributeError(key) from None + + def as_doc(self) -> SON[str, Any]: + """Get the SON document representation of this DBRef. + + Generally not needed by application developers + """ + doc = SON([("$ref", self.collection), ("$id", self.id)]) + if self.database is not None: + doc["$db"] = self.database + doc.update(self.__kwargs) + return doc + + def __repr__(self) -> str: + extra = "".join([f", {k}={v!r}" for k, v in self.__kwargs.items()]) + if self.database is None: + return f"DBRef({self.collection!r}, {self.id!r}{extra})" + return f"DBRef({self.collection!r}, {self.id!r}, {self.database!r}{extra})" + + def __eq__(self, other: Any) -> bool: + if isinstance(other, DBRef): + us = (self.__database, self.__collection, self.__id, self.__kwargs) + them = (other.__database, other.__collection, other.__id, other.__kwargs) + return us == them + return NotImplemented + + def __ne__(self, other: Any) -> bool: + return not self == other + + def __hash__(self) -> int: + """Get a hash value for this :class:`DBRef`.""" + return hash( + (self.__collection, self.__id, self.__database, tuple(sorted(self.__kwargs.items()))) + ) + + def __deepcopy__(self, memo: Any) -> DBRef: + """Support function for `copy.deepcopy()`.""" + return DBRef( + deepcopy(self.__collection, memo), + deepcopy(self.__id, memo), + deepcopy(self.__database, memo), + deepcopy(self.__kwargs, memo), + ) diff --git a/venv/Lib/site-packages/bson/decimal128.py b/venv/Lib/site-packages/bson/decimal128.py new file mode 100644 index 00000000..8581d5a3 --- /dev/null +++ b/venv/Lib/site-packages/bson/decimal128.py @@ -0,0 +1,312 @@ +# Copyright 2016-present MongoDB, 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. + +"""Tools for working with the BSON decimal128 type. + +.. versionadded:: 3.4 +""" +from __future__ import annotations + +import decimal +import struct +from typing import Any, Sequence, Tuple, Type, Union + +_PACK_64 = struct.Struct(" decimal.Context: + """Returns an instance of :class:`decimal.Context` appropriate + for working with IEEE-754 128-bit decimal floating point values. + """ + opts = _CTX_OPTIONS.copy() + opts["traps"] = [] + return decimal.Context(**opts) # type: ignore + + +def _decimal_to_128(value: _VALUE_OPTIONS) -> Tuple[int, int]: + """Converts a decimal.Decimal to BID (high bits, low bits). + + :param value: An instance of decimal.Decimal + """ + with decimal.localcontext(_DEC128_CTX) as ctx: + value = ctx.create_decimal(value) + + if value.is_infinite(): + return _NINF if value.is_signed() else _PINF + + sign, digits, exponent = value.as_tuple() + + if value.is_nan(): + if digits: + raise ValueError("NaN with debug payload is not supported") + if value.is_snan(): + return _NSNAN if value.is_signed() else _PSNAN + return _NNAN if value.is_signed() else _PNAN + + significand = int("".join([str(digit) for digit in digits])) + bit_length = significand.bit_length() + + high = 0 + low = 0 + for i in range(min(64, bit_length)): + if significand & (1 << i): + low |= 1 << i + + for i in range(64, bit_length): + if significand & (1 << i): + high |= 1 << (i - 64) + + biased_exponent = exponent + _EXPONENT_BIAS # type: ignore[operator] + + if high >> 49 == 1: + high = high & 0x7FFFFFFFFFFF + high |= _EXPONENT_MASK + high |= (biased_exponent & 0x3FFF) << 47 + else: + high |= biased_exponent << 49 + + if sign: + high |= _SIGN + + return high, low + + +class Decimal128: + """BSON Decimal128 type:: + + >>> Decimal128(Decimal("0.0005")) + Decimal128('0.0005') + >>> Decimal128("0.0005") + Decimal128('0.0005') + >>> Decimal128((3474527112516337664, 5)) + Decimal128('0.0005') + + :param value: An instance of :class:`decimal.Decimal`, string, or tuple of + (high bits, low bits) from Binary Integer Decimal (BID) format. + + .. note:: :class:`~Decimal128` uses an instance of :class:`decimal.Context` + configured for IEEE-754 Decimal128 when validating parameters. + Signals like :class:`decimal.InvalidOperation`, :class:`decimal.Inexact`, + and :class:`decimal.Overflow` are trapped and raised as exceptions:: + + >>> Decimal128(".13.1") + Traceback (most recent call last): + File "", line 1, in + ... + decimal.InvalidOperation: [] + >>> + >>> Decimal128("1E-6177") + Traceback (most recent call last): + File "", line 1, in + ... + decimal.Inexact: [] + >>> + >>> Decimal128("1E6145") + Traceback (most recent call last): + File "", line 1, in + ... + decimal.Overflow: [, ] + + To ensure the result of a calculation can always be stored as BSON + Decimal128 use the context returned by + :func:`create_decimal128_context`:: + + >>> import decimal + >>> decimal128_ctx = create_decimal128_context() + >>> with decimal.localcontext(decimal128_ctx) as ctx: + ... Decimal128(ctx.create_decimal(".13.3")) + ... + Decimal128('NaN') + >>> + >>> with decimal.localcontext(decimal128_ctx) as ctx: + ... Decimal128(ctx.create_decimal("1E-6177")) + ... + Decimal128('0E-6176') + >>> + >>> with decimal.localcontext(DECIMAL128_CTX) as ctx: + ... Decimal128(ctx.create_decimal("1E6145")) + ... + Decimal128('Infinity') + + To match the behavior of MongoDB's Decimal128 implementation + str(Decimal(value)) may not match str(Decimal128(value)) for NaN values:: + + >>> Decimal128(Decimal('NaN')) + Decimal128('NaN') + >>> Decimal128(Decimal('-NaN')) + Decimal128('NaN') + >>> Decimal128(Decimal('sNaN')) + Decimal128('NaN') + >>> Decimal128(Decimal('-sNaN')) + Decimal128('NaN') + + However, :meth:`~Decimal128.to_decimal` will return the exact value:: + + >>> Decimal128(Decimal('NaN')).to_decimal() + Decimal('NaN') + >>> Decimal128(Decimal('-NaN')).to_decimal() + Decimal('-NaN') + >>> Decimal128(Decimal('sNaN')).to_decimal() + Decimal('sNaN') + >>> Decimal128(Decimal('-sNaN')).to_decimal() + Decimal('-sNaN') + + Two instances of :class:`Decimal128` compare equal if their Binary + Integer Decimal encodings are equal:: + + >>> Decimal128('NaN') == Decimal128('NaN') + True + >>> Decimal128('NaN').bid == Decimal128('NaN').bid + True + + This differs from :class:`decimal.Decimal` comparisons for NaN:: + + >>> Decimal('NaN') == Decimal('NaN') + False + """ + + __slots__ = ("__high", "__low") + + _type_marker = 19 + + def __init__(self, value: _VALUE_OPTIONS) -> None: + if isinstance(value, (str, decimal.Decimal)): + self.__high, self.__low = _decimal_to_128(value) + elif isinstance(value, (list, tuple)): + if len(value) != 2: + raise ValueError( + "Invalid size for creation of Decimal128 " + "from list or tuple. Must have exactly 2 " + "elements." + ) + self.__high, self.__low = value # type: ignore + else: + raise TypeError(f"Cannot convert {value!r} to Decimal128") + + def to_decimal(self) -> decimal.Decimal: + """Returns an instance of :class:`decimal.Decimal` for this + :class:`Decimal128`. + """ + high = self.__high + low = self.__low + sign = 1 if (high & _SIGN) else 0 + + if (high & _SNAN) == _SNAN: + return decimal.Decimal((sign, (), "N")) # type: ignore + elif (high & _NAN) == _NAN: + return decimal.Decimal((sign, (), "n")) # type: ignore + elif (high & _INF) == _INF: + return decimal.Decimal((sign, (), "F")) # type: ignore + + if (high & _EXPONENT_MASK) == _EXPONENT_MASK: + exponent = ((high & 0x1FFFE00000000000) >> 47) - _EXPONENT_BIAS + return decimal.Decimal((sign, (0,), exponent)) + else: + exponent = ((high & 0x7FFF800000000000) >> 49) - _EXPONENT_BIAS + + arr = bytearray(15) + mask = 0x00000000000000FF + for i in range(14, 6, -1): + arr[i] = (low & mask) >> ((14 - i) << 3) + mask = mask << 8 + + mask = 0x00000000000000FF + for i in range(6, 0, -1): + arr[i] = (high & mask) >> ((6 - i) << 3) + mask = mask << 8 + + mask = 0x0001000000000000 + arr[0] = (high & mask) >> 48 + + # cdecimal only accepts a tuple for digits. + digits = tuple(int(digit) for digit in str(int.from_bytes(arr, "big"))) + + with decimal.localcontext(_DEC128_CTX) as ctx: + return ctx.create_decimal((sign, digits, exponent)) + + @classmethod + def from_bid(cls: Type[Decimal128], value: bytes) -> Decimal128: + """Create an instance of :class:`Decimal128` from Binary Integer + Decimal string. + + :param value: 16 byte string (128-bit IEEE 754-2008 decimal floating + point in Binary Integer Decimal (BID) format). + """ + if not isinstance(value, bytes): + raise TypeError("value must be an instance of bytes") + if len(value) != 16: + raise ValueError("value must be exactly 16 bytes") + return cls((_UNPACK_64(value[8:])[0], _UNPACK_64(value[:8])[0])) # type: ignore + + @property + def bid(self) -> bytes: + """The Binary Integer Decimal (BID) encoding of this instance.""" + return _PACK_64(self.__low) + _PACK_64(self.__high) + + def __str__(self) -> str: + dec = self.to_decimal() + if dec.is_nan(): + # Required by the drivers spec to match MongoDB behavior. + return "NaN" + return str(dec) + + def __repr__(self) -> str: + return f"Decimal128('{self!s}')" + + def __setstate__(self, value: Tuple[int, int]) -> None: + self.__high, self.__low = value + + def __getstate__(self) -> Tuple[int, int]: + return self.__high, self.__low + + def __eq__(self, other: Any) -> bool: + if isinstance(other, Decimal128): + return self.bid == other.bid + return NotImplemented + + def __ne__(self, other: Any) -> bool: + return not self == other diff --git a/venv/Lib/site-packages/bson/errors.py b/venv/Lib/site-packages/bson/errors.py new file mode 100644 index 00000000..a3699e70 --- /dev/null +++ b/venv/Lib/site-packages/bson/errors.py @@ -0,0 +1,36 @@ +# Copyright 2009-present MongoDB, 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. + +"""Exceptions raised by the BSON package.""" +from __future__ import annotations + + +class BSONError(Exception): + """Base class for all BSON exceptions.""" + + +class InvalidBSON(BSONError): + """Raised when trying to create a BSON object from invalid data.""" + + +class InvalidStringData(BSONError): + """Raised when trying to encode a string containing non-UTF8 data.""" + + +class InvalidDocument(BSONError): + """Raised when trying to create a BSON object from an invalid document.""" + + +class InvalidId(BSONError): + """Raised when trying to create an ObjectId from invalid data.""" diff --git a/venv/Lib/site-packages/bson/int64.py b/venv/Lib/site-packages/bson/int64.py new file mode 100644 index 00000000..5846504a --- /dev/null +++ b/venv/Lib/site-packages/bson/int64.py @@ -0,0 +1,39 @@ +# Copyright 2014-2015 MongoDB, 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. + +"""A BSON wrapper for long (int in python3)""" +from __future__ import annotations + +from typing import Any + + +class Int64(int): + """Representation of the BSON int64 type. + + This is necessary because every integral number is an :class:`int` in + Python 3. Small integral numbers are encoded to BSON int32 by default, + but Int64 numbers will always be encoded to BSON int64. + + :param value: the numeric value to represent + """ + + __slots__ = () + + _type_marker = 18 + + def __getstate__(self) -> Any: + return {} + + def __setstate__(self, state: Any) -> None: + pass diff --git a/venv/Lib/site-packages/bson/json_util.py b/venv/Lib/site-packages/bson/json_util.py new file mode 100644 index 00000000..6c5197c7 --- /dev/null +++ b/venv/Lib/site-packages/bson/json_util.py @@ -0,0 +1,1161 @@ +# Copyright 2009-present MongoDB, 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. + +"""Tools for using Python's :mod:`json` module with BSON documents. + +This module provides two helper methods `dumps` and `loads` that wrap the +native :mod:`json` methods and provide explicit BSON conversion to and from +JSON. :class:`~bson.json_util.JSONOptions` provides a way to control how JSON +is emitted and parsed, with the default being the Relaxed Extended JSON format. +:mod:`~bson.json_util` can also generate Canonical or legacy `Extended JSON`_ +when :const:`CANONICAL_JSON_OPTIONS` or :const:`LEGACY_JSON_OPTIONS` is +provided, respectively. + +.. _Extended JSON: https://github.com/mongodb/specifications/blob/master/source/extended-json.rst + +Example usage (deserialization): + +.. doctest:: + + >>> from bson.json_util import loads + >>> loads( + ... '[{"foo": [1, 2]}, {"bar": {"hello": "world"}}, {"code": {"$scope": {}, "$code": "function x() { return 1; }"}}, {"bin": {"$type": "80", "$binary": "AQIDBA=="}}]' + ... ) + [{'foo': [1, 2]}, {'bar': {'hello': 'world'}}, {'code': Code('function x() { return 1; }', {})}, {'bin': Binary(b'...', 128)}] + +Example usage with :const:`RELAXED_JSON_OPTIONS` (the default): + +.. doctest:: + + >>> from bson import Binary, Code + >>> from bson.json_util import dumps + >>> dumps( + ... [ + ... {"foo": [1, 2]}, + ... {"bar": {"hello": "world"}}, + ... {"code": Code("function x() { return 1; }")}, + ... {"bin": Binary(b"\x01\x02\x03\x04")}, + ... ] + ... ) + '[{"foo": [1, 2]}, {"bar": {"hello": "world"}}, {"code": {"$code": "function x() { return 1; }"}}, {"bin": {"$binary": {"base64": "AQIDBA==", "subType": "00"}}}]' + +Example usage (with :const:`CANONICAL_JSON_OPTIONS`): + +.. doctest:: + + >>> from bson import Binary, Code + >>> from bson.json_util import dumps, CANONICAL_JSON_OPTIONS + >>> dumps( + ... [ + ... {"foo": [1, 2]}, + ... {"bar": {"hello": "world"}}, + ... {"code": Code("function x() { return 1; }")}, + ... {"bin": Binary(b"\x01\x02\x03\x04")}, + ... ], + ... json_options=CANONICAL_JSON_OPTIONS, + ... ) + '[{"foo": [{"$numberInt": "1"}, {"$numberInt": "2"}]}, {"bar": {"hello": "world"}}, {"code": {"$code": "function x() { return 1; }"}}, {"bin": {"$binary": {"base64": "AQIDBA==", "subType": "00"}}}]' + +Example usage (with :const:`LEGACY_JSON_OPTIONS`): + +.. doctest:: + + >>> from bson import Binary, Code + >>> from bson.json_util import dumps, LEGACY_JSON_OPTIONS + >>> dumps( + ... [ + ... {"foo": [1, 2]}, + ... {"bar": {"hello": "world"}}, + ... {"code": Code("function x() { return 1; }", {})}, + ... {"bin": Binary(b"\x01\x02\x03\x04")}, + ... ], + ... json_options=LEGACY_JSON_OPTIONS, + ... ) + '[{"foo": [1, 2]}, {"bar": {"hello": "world"}}, {"code": {"$code": "function x() { return 1; }", "$scope": {}}}, {"bin": {"$binary": "AQIDBA==", "$type": "00"}}]' + +Alternatively, you can manually pass the `default` to :func:`json.dumps`. +It won't handle :class:`~bson.binary.Binary` and :class:`~bson.code.Code` +instances (as they are extended strings you can't provide custom defaults), +but it will be faster as there is less recursion. + +.. note:: + If your application does not need the flexibility offered by + :class:`JSONOptions` and spends a large amount of time in the `json_util` + module, look to + `python-bsonjs `_ for a nice + performance improvement. `python-bsonjs` is a fast BSON to MongoDB + Extended JSON converter for Python built on top of + `libbson `_. `python-bsonjs` works best + with PyMongo when using :class:`~bson.raw_bson.RawBSONDocument`. +""" +from __future__ import annotations + +import base64 +import datetime +import json +import math +import re +import uuid +from typing import ( + TYPE_CHECKING, + Any, + Callable, + Mapping, + MutableMapping, + Optional, + Sequence, + Tuple, + Type, + Union, + cast, +) + +from bson.binary import ALL_UUID_SUBTYPES, UUID_SUBTYPE, Binary, UuidRepresentation +from bson.code import Code +from bson.codec_options import CodecOptions, DatetimeConversion +from bson.datetime_ms import ( + EPOCH_AWARE, + DatetimeMS, + _datetime_to_millis, + _max_datetime_ms, + _millis_to_datetime, +) +from bson.dbref import DBRef +from bson.decimal128 import Decimal128 +from bson.int64 import Int64 +from bson.max_key import MaxKey +from bson.min_key import MinKey +from bson.objectid import ObjectId +from bson.regex import Regex +from bson.son import RE_TYPE +from bson.timestamp import Timestamp +from bson.tz_util import utc + +_RE_OPT_TABLE = { + "i": re.I, + "l": re.L, + "m": re.M, + "s": re.S, + "u": re.U, + "x": re.X, +} + + +class DatetimeRepresentation: + LEGACY = 0 + """Legacy MongoDB Extended JSON datetime representation. + + :class:`datetime.datetime` instances will be encoded to JSON in the + format `{"$date": }`, where `dateAsMilliseconds` is + a 64-bit signed integer giving the number of milliseconds since the Unix + epoch UTC. This was the default encoding before PyMongo version 3.4. + + .. versionadded:: 3.4 + """ + + NUMBERLONG = 1 + """NumberLong datetime representation. + + :class:`datetime.datetime` instances will be encoded to JSON in the + format `{"$date": {"$numberLong": ""}}`, + where `dateAsMilliseconds` is the string representation of a 64-bit signed + integer giving the number of milliseconds since the Unix epoch UTC. + + .. versionadded:: 3.4 + """ + + ISO8601 = 2 + """ISO-8601 datetime representation. + + :class:`datetime.datetime` instances greater than or equal to the Unix + epoch UTC will be encoded to JSON in the format `{"$date": ""}`. + :class:`datetime.datetime` instances before the Unix epoch UTC will be + encoded as if the datetime representation is + :const:`~DatetimeRepresentation.NUMBERLONG`. + + .. versionadded:: 3.4 + """ + + +class JSONMode: + LEGACY = 0 + """Legacy Extended JSON representation. + + In this mode, :func:`~bson.json_util.dumps` produces PyMongo's legacy + non-standard JSON output. Consider using + :const:`~bson.json_util.JSONMode.RELAXED` or + :const:`~bson.json_util.JSONMode.CANONICAL` instead. + + .. versionadded:: 3.5 + """ + + RELAXED = 1 + """Relaxed Extended JSON representation. + + In this mode, :func:`~bson.json_util.dumps` produces Relaxed Extended JSON, + a mostly JSON-like format. Consider using this for things like a web API, + where one is sending a document (or a projection of a document) that only + uses ordinary JSON type primitives. In particular, the ``int``, + :class:`~bson.int64.Int64`, and ``float`` numeric types are represented in + the native JSON number format. This output is also the most human readable + and is useful for debugging and documentation. + + .. seealso:: The specification for Relaxed `Extended JSON`_. + + .. versionadded:: 3.5 + """ + + CANONICAL = 2 + """Canonical Extended JSON representation. + + In this mode, :func:`~bson.json_util.dumps` produces Canonical Extended + JSON, a type preserving format. Consider using this for things like + testing, where one has to precisely specify expected types in JSON. In + particular, the ``int``, :class:`~bson.int64.Int64`, and ``float`` numeric + types are encoded with type wrappers. + + .. seealso:: The specification for Canonical `Extended JSON`_. + + .. versionadded:: 3.5 + """ + + +if TYPE_CHECKING: + _BASE_CLASS = CodecOptions[MutableMapping[str, Any]] +else: + _BASE_CLASS = CodecOptions + +_INT32_MAX = 2**31 + + +class JSONOptions(_BASE_CLASS): + json_mode: int + strict_number_long: bool + datetime_representation: int + strict_uuid: bool + document_class: Type[MutableMapping[str, Any]] + + def __init__(self, *args: Any, **kwargs: Any): + """Encapsulates JSON options for :func:`dumps` and :func:`loads`. + + :param strict_number_long: If ``True``, :class:`~bson.int64.Int64` objects + are encoded to MongoDB Extended JSON's *Strict mode* type + `NumberLong`, ie ``'{"$numberLong": "" }'``. Otherwise they + will be encoded as an `int`. Defaults to ``False``. + :param datetime_representation: The representation to use when encoding + instances of :class:`datetime.datetime`. Defaults to + :const:`~DatetimeRepresentation.LEGACY`. + :param strict_uuid: If ``True``, :class:`uuid.UUID` object are encoded to + MongoDB Extended JSON's *Strict mode* type `Binary`. Otherwise it + will be encoded as ``'{"$uuid": "" }'``. Defaults to ``False``. + :param json_mode: The :class:`JSONMode` to use when encoding BSON types to + Extended JSON. Defaults to :const:`~JSONMode.LEGACY`. + :param document_class: BSON documents returned by :func:`loads` will be + decoded to an instance of this class. Must be a subclass of + :class:`collections.MutableMapping`. Defaults to :class:`dict`. + :param uuid_representation: The :class:`~bson.binary.UuidRepresentation` + to use when encoding and decoding instances of :class:`uuid.UUID`. + Defaults to :const:`~bson.binary.UuidRepresentation.UNSPECIFIED`. + :param tz_aware: If ``True``, MongoDB Extended JSON's *Strict mode* type + `Date` will be decoded to timezone aware instances of + :class:`datetime.datetime`. Otherwise they will be naive. Defaults + to ``False``. + :param tzinfo: A :class:`datetime.tzinfo` subclass that specifies the + timezone from which :class:`~datetime.datetime` objects should be + decoded. Defaults to :const:`~bson.tz_util.utc`. + :param datetime_conversion: Specifies how UTC datetimes should be decoded + within BSON. Valid options include 'datetime_ms' to return as a + DatetimeMS, 'datetime' to return as a datetime.datetime and + raising a ValueError for out-of-range values, 'datetime_auto' to + return DatetimeMS objects when the underlying datetime is + out-of-range and 'datetime_clamp' to clamp to the minimum and + maximum possible datetimes. Defaults to 'datetime'. See + :ref:`handling-out-of-range-datetimes` for details. + :param args: arguments to :class:`~bson.codec_options.CodecOptions` + :param kwargs: arguments to :class:`~bson.codec_options.CodecOptions` + + .. seealso:: The specification for Relaxed and Canonical `Extended JSON`_. + + .. versionchanged:: 4.0 + The default for `json_mode` was changed from :const:`JSONMode.LEGACY` + to :const:`JSONMode.RELAXED`. + The default for `uuid_representation` was changed from + :const:`~bson.binary.UuidRepresentation.PYTHON_LEGACY` to + :const:`~bson.binary.UuidRepresentation.UNSPECIFIED`. + + .. versionchanged:: 3.5 + Accepts the optional parameter `json_mode`. + + .. versionchanged:: 4.0 + Changed default value of `tz_aware` to False. + """ + super().__init__() + + def __new__( + cls: Type[JSONOptions], + strict_number_long: Optional[bool] = None, + datetime_representation: Optional[int] = None, + strict_uuid: Optional[bool] = None, + json_mode: int = JSONMode.RELAXED, + *args: Any, + **kwargs: Any, + ) -> JSONOptions: + kwargs["tz_aware"] = kwargs.get("tz_aware", False) + if kwargs["tz_aware"]: + kwargs["tzinfo"] = kwargs.get("tzinfo", utc) + if datetime_representation not in ( + DatetimeRepresentation.LEGACY, + DatetimeRepresentation.NUMBERLONG, + DatetimeRepresentation.ISO8601, + None, + ): + raise ValueError( + "JSONOptions.datetime_representation must be one of LEGACY, " + "NUMBERLONG, or ISO8601 from DatetimeRepresentation." + ) + self = cast(JSONOptions, super().__new__(cls, *args, **kwargs)) # type:ignore[arg-type] + if json_mode not in (JSONMode.LEGACY, JSONMode.RELAXED, JSONMode.CANONICAL): + raise ValueError( + "JSONOptions.json_mode must be one of LEGACY, RELAXED, " + "or CANONICAL from JSONMode." + ) + self.json_mode = json_mode + if self.json_mode == JSONMode.RELAXED: + if strict_number_long: + raise ValueError("Cannot specify strict_number_long=True with JSONMode.RELAXED") + if datetime_representation not in (None, DatetimeRepresentation.ISO8601): + raise ValueError( + "datetime_representation must be DatetimeRepresentation." + "ISO8601 or omitted with JSONMode.RELAXED" + ) + if strict_uuid not in (None, True): + raise ValueError("Cannot specify strict_uuid=False with JSONMode.RELAXED") + self.strict_number_long = False + self.datetime_representation = DatetimeRepresentation.ISO8601 + self.strict_uuid = True + elif self.json_mode == JSONMode.CANONICAL: + if strict_number_long not in (None, True): + raise ValueError("Cannot specify strict_number_long=False with JSONMode.RELAXED") + if datetime_representation not in (None, DatetimeRepresentation.NUMBERLONG): + raise ValueError( + "datetime_representation must be DatetimeRepresentation." + "NUMBERLONG or omitted with JSONMode.RELAXED" + ) + if strict_uuid not in (None, True): + raise ValueError("Cannot specify strict_uuid=False with JSONMode.RELAXED") + self.strict_number_long = True + self.datetime_representation = DatetimeRepresentation.NUMBERLONG + self.strict_uuid = True + else: # JSONMode.LEGACY + self.strict_number_long = False + self.datetime_representation = DatetimeRepresentation.LEGACY + self.strict_uuid = False + if strict_number_long is not None: + self.strict_number_long = strict_number_long + if datetime_representation is not None: + self.datetime_representation = datetime_representation + if strict_uuid is not None: + self.strict_uuid = strict_uuid + return self + + def _arguments_repr(self) -> str: + return ( + "strict_number_long={!r}, " + "datetime_representation={!r}, " + "strict_uuid={!r}, json_mode={!r}, {}".format( + self.strict_number_long, + self.datetime_representation, + self.strict_uuid, + self.json_mode, + super()._arguments_repr(), + ) + ) + + def _options_dict(self) -> dict[Any, Any]: + # TODO: PYTHON-2442 use _asdict() instead + options_dict = super()._options_dict() + options_dict.update( + { + "strict_number_long": self.strict_number_long, + "datetime_representation": self.datetime_representation, + "strict_uuid": self.strict_uuid, + "json_mode": self.json_mode, + } + ) + return options_dict + + def with_options(self, **kwargs: Any) -> JSONOptions: + """ + Make a copy of this JSONOptions, overriding some options:: + + >>> from bson.json_util import CANONICAL_JSON_OPTIONS + >>> CANONICAL_JSON_OPTIONS.tz_aware + True + >>> json_options = CANONICAL_JSON_OPTIONS.with_options(tz_aware=False, tzinfo=None) + >>> json_options.tz_aware + False + + .. versionadded:: 3.12 + """ + opts = self._options_dict() + for opt in ("strict_number_long", "datetime_representation", "strict_uuid", "json_mode"): + opts[opt] = kwargs.get(opt, getattr(self, opt)) + opts.update(kwargs) + return JSONOptions(**opts) + + +LEGACY_JSON_OPTIONS: JSONOptions = JSONOptions(json_mode=JSONMode.LEGACY) +""":class:`JSONOptions` for encoding to PyMongo's legacy JSON format. + +.. seealso:: The documentation for :const:`bson.json_util.JSONMode.LEGACY`. + +.. versionadded:: 3.5 +""" + +CANONICAL_JSON_OPTIONS: JSONOptions = JSONOptions(json_mode=JSONMode.CANONICAL) +""":class:`JSONOptions` for Canonical Extended JSON. + +.. seealso:: The documentation for :const:`bson.json_util.JSONMode.CANONICAL`. + +.. versionadded:: 3.5 +""" + +RELAXED_JSON_OPTIONS: JSONOptions = JSONOptions(json_mode=JSONMode.RELAXED) +""":class:`JSONOptions` for Relaxed Extended JSON. + +.. seealso:: The documentation for :const:`bson.json_util.JSONMode.RELAXED`. + +.. versionadded:: 3.5 +""" + +DEFAULT_JSON_OPTIONS: JSONOptions = RELAXED_JSON_OPTIONS +"""The default :class:`JSONOptions` for JSON encoding/decoding. + +The same as :const:`RELAXED_JSON_OPTIONS`. + +.. versionchanged:: 4.0 + Changed from :const:`LEGACY_JSON_OPTIONS` to + :const:`RELAXED_JSON_OPTIONS`. + +.. versionadded:: 3.4 +""" + + +def dumps(obj: Any, *args: Any, **kwargs: Any) -> str: + """Helper function that wraps :func:`json.dumps`. + + Recursive function that handles all BSON types including + :class:`~bson.binary.Binary` and :class:`~bson.code.Code`. + + :param json_options: A :class:`JSONOptions` instance used to modify the + encoding of MongoDB Extended JSON types. Defaults to + :const:`DEFAULT_JSON_OPTIONS`. + + .. versionchanged:: 4.0 + Now outputs MongoDB Relaxed Extended JSON by default (using + :const:`DEFAULT_JSON_OPTIONS`). + + .. versionchanged:: 3.4 + Accepts optional parameter `json_options`. See :class:`JSONOptions`. + """ + json_options = kwargs.pop("json_options", DEFAULT_JSON_OPTIONS) + return json.dumps(_json_convert(obj, json_options), *args, **kwargs) + + +def loads(s: Union[str, bytes, bytearray], *args: Any, **kwargs: Any) -> Any: + """Helper function that wraps :func:`json.loads`. + + Automatically passes the object_hook for BSON type conversion. + + Raises ``TypeError``, ``ValueError``, ``KeyError``, or + :exc:`~bson.errors.InvalidId` on invalid MongoDB Extended JSON. + + :param json_options: A :class:`JSONOptions` instance used to modify the + decoding of MongoDB Extended JSON types. Defaults to + :const:`DEFAULT_JSON_OPTIONS`. + + .. versionchanged:: 4.0 + Now loads :class:`datetime.datetime` instances as naive by default. To + load timezone aware instances utilize the `json_options` parameter. + See :ref:`tz_aware_default_change` for an example. + + .. versionchanged:: 3.5 + Parses Relaxed and Canonical Extended JSON as well as PyMongo's legacy + format. Now raises ``TypeError`` or ``ValueError`` when parsing JSON + type wrappers with values of the wrong type or any extra keys. + + .. versionchanged:: 3.4 + Accepts optional parameter `json_options`. See :class:`JSONOptions`. + """ + json_options = kwargs.pop("json_options", DEFAULT_JSON_OPTIONS) + # Execution time optimization if json_options.document_class is dict + if json_options.document_class is dict: + kwargs["object_hook"] = lambda obj: object_hook(obj, json_options) + else: + kwargs["object_pairs_hook"] = lambda pairs: object_pairs_hook(pairs, json_options) + return json.loads(s, *args, **kwargs) + + +def _json_convert(obj: Any, json_options: JSONOptions = DEFAULT_JSON_OPTIONS) -> Any: + """Recursive helper method that converts BSON types so they can be + converted into json. + """ + if hasattr(obj, "items"): + return {k: _json_convert(v, json_options) for k, v in obj.items()} + elif hasattr(obj, "__iter__") and not isinstance(obj, (str, bytes)): + return [_json_convert(v, json_options) for v in obj] + try: + return default(obj, json_options) + except TypeError: + return obj + + +def object_pairs_hook( + pairs: Sequence[Tuple[str, Any]], json_options: JSONOptions = DEFAULT_JSON_OPTIONS +) -> Any: + return object_hook(json_options.document_class(pairs), json_options) # type:ignore[call-arg] + + +def object_hook(dct: Mapping[str, Any], json_options: JSONOptions = DEFAULT_JSON_OPTIONS) -> Any: + match = None + for k in dct: + if k in _PARSERS_SET: + match = k + break + if match: + return _PARSERS[match](dct, json_options) + return dct + + +def _parse_legacy_regex(doc: Any, dummy0: Any) -> Any: + pattern = doc["$regex"] + # Check if this is the $regex query operator. + if not isinstance(pattern, (str, bytes)): + return doc + flags = 0 + # PyMongo always adds $options but some other tools may not. + for opt in doc.get("$options", ""): + flags |= _RE_OPT_TABLE.get(opt, 0) + return Regex(pattern, flags) + + +def _parse_legacy_uuid(doc: Any, json_options: JSONOptions) -> Union[Binary, uuid.UUID]: + """Decode a JSON legacy $uuid to Python UUID.""" + if len(doc) != 1: + raise TypeError(f"Bad $uuid, extra field(s): {doc}") + if not isinstance(doc["$uuid"], str): + raise TypeError(f"$uuid must be a string: {doc}") + if json_options.uuid_representation == UuidRepresentation.UNSPECIFIED: + return Binary.from_uuid(uuid.UUID(doc["$uuid"])) + else: + return uuid.UUID(doc["$uuid"]) + + +def _binary_or_uuid(data: Any, subtype: int, json_options: JSONOptions) -> Union[Binary, uuid.UUID]: + # special handling for UUID + if subtype in ALL_UUID_SUBTYPES: + uuid_representation = json_options.uuid_representation + binary_value = Binary(data, subtype) + if uuid_representation == UuidRepresentation.UNSPECIFIED: + return binary_value + if subtype == UUID_SUBTYPE: + # Legacy behavior: use STANDARD with binary subtype 4. + uuid_representation = UuidRepresentation.STANDARD + elif uuid_representation == UuidRepresentation.STANDARD: + # subtype == OLD_UUID_SUBTYPE + # Legacy behavior: STANDARD is the same as PYTHON_LEGACY. + uuid_representation = UuidRepresentation.PYTHON_LEGACY + return binary_value.as_uuid(uuid_representation) + + if subtype == 0: + return cast(uuid.UUID, data) + return Binary(data, subtype) + + +def _parse_legacy_binary(doc: Any, json_options: JSONOptions) -> Union[Binary, uuid.UUID]: + if isinstance(doc["$type"], int): + doc["$type"] = "%02x" % doc["$type"] + subtype = int(doc["$type"], 16) + if subtype >= 0xFFFFFF80: # Handle mongoexport values + subtype = int(doc["$type"][6:], 16) + data = base64.b64decode(doc["$binary"].encode()) + return _binary_or_uuid(data, subtype, json_options) + + +def _parse_canonical_binary(doc: Any, json_options: JSONOptions) -> Union[Binary, uuid.UUID]: + binary = doc["$binary"] + b64 = binary["base64"] + subtype = binary["subType"] + if not isinstance(b64, str): + raise TypeError(f"$binary base64 must be a string: {doc}") + if not isinstance(subtype, str) or len(subtype) > 2: + raise TypeError(f"$binary subType must be a string at most 2 characters: {doc}") + if len(binary) != 2: + raise TypeError(f'$binary must include only "base64" and "subType" components: {doc}') + + data = base64.b64decode(b64.encode()) + return _binary_or_uuid(data, int(subtype, 16), json_options) + + +def _parse_canonical_datetime( + doc: Any, json_options: JSONOptions +) -> Union[datetime.datetime, DatetimeMS]: + """Decode a JSON datetime to python datetime.datetime.""" + dtm = doc["$date"] + if len(doc) != 1: + raise TypeError(f"Bad $date, extra field(s): {doc}") + # mongoexport 2.6 and newer + if isinstance(dtm, str): + # Parse offset + if dtm[-1] == "Z": + dt = dtm[:-1] + offset = "Z" + elif dtm[-6] in ("+", "-") and dtm[-3] == ":": + # (+|-)HH:MM + dt = dtm[:-6] + offset = dtm[-6:] + elif dtm[-5] in ("+", "-"): + # (+|-)HHMM + dt = dtm[:-5] + offset = dtm[-5:] + elif dtm[-3] in ("+", "-"): + # (+|-)HH + dt = dtm[:-3] + offset = dtm[-3:] + else: + dt = dtm + offset = "" + + # Parse the optional factional seconds portion. + dot_index = dt.rfind(".") + microsecond = 0 + if dot_index != -1: + microsecond = int(float(dt[dot_index:]) * 1000000) + dt = dt[:dot_index] + + aware = datetime.datetime.strptime(dt, "%Y-%m-%dT%H:%M:%S").replace( + microsecond=microsecond, tzinfo=utc + ) + + if offset and offset != "Z": + if len(offset) == 6: + hours, minutes = offset[1:].split(":") + secs = int(hours) * 3600 + int(minutes) * 60 + elif len(offset) == 5: + secs = int(offset[1:3]) * 3600 + int(offset[3:]) * 60 + elif len(offset) == 3: + secs = int(offset[1:3]) * 3600 + if offset[0] == "-": + secs *= -1 + aware = aware - datetime.timedelta(seconds=secs) + + if json_options.tz_aware: + if json_options.tzinfo: + aware = aware.astimezone(json_options.tzinfo) + if json_options.datetime_conversion == DatetimeConversion.DATETIME_MS: + return DatetimeMS(aware) + return aware + else: + aware_tzinfo_none = aware.replace(tzinfo=None) + if json_options.datetime_conversion == DatetimeConversion.DATETIME_MS: + return DatetimeMS(aware_tzinfo_none) + return aware_tzinfo_none + return _millis_to_datetime(int(dtm), cast("CodecOptions[Any]", json_options)) + + +def _parse_canonical_oid(doc: Any, dummy0: Any) -> ObjectId: + """Decode a JSON ObjectId to bson.objectid.ObjectId.""" + if len(doc) != 1: + raise TypeError(f"Bad $oid, extra field(s): {doc}") + return ObjectId(doc["$oid"]) + + +def _parse_canonical_symbol(doc: Any, dummy0: Any) -> str: + """Decode a JSON symbol to Python string.""" + symbol = doc["$symbol"] + if len(doc) != 1: + raise TypeError(f"Bad $symbol, extra field(s): {doc}") + return str(symbol) + + +def _parse_canonical_code(doc: Any, dummy0: Any) -> Code: + """Decode a JSON code to bson.code.Code.""" + for key in doc: + if key not in ("$code", "$scope"): + raise TypeError(f"Bad $code, extra field(s): {doc}") + return Code(doc["$code"], scope=doc.get("$scope")) + + +def _parse_canonical_regex(doc: Any, dummy0: Any) -> Regex[str]: + """Decode a JSON regex to bson.regex.Regex.""" + regex = doc["$regularExpression"] + if len(doc) != 1: + raise TypeError(f"Bad $regularExpression, extra field(s): {doc}") + if len(regex) != 2: + raise TypeError( + f'Bad $regularExpression must include only "pattern and "options" components: {doc}' + ) + opts = regex["options"] + if not isinstance(opts, str): + raise TypeError( + "Bad $regularExpression options, options must be string, was type %s" % (type(opts)) + ) + return Regex(regex["pattern"], opts) + + +def _parse_canonical_dbref(doc: Any, dummy0: Any) -> Any: + """Decode a JSON DBRef to bson.dbref.DBRef.""" + if ( + isinstance(doc.get("$ref"), str) + and "$id" in doc + and isinstance(doc.get("$db"), (str, type(None))) + ): + return DBRef(doc.pop("$ref"), doc.pop("$id"), database=doc.pop("$db", None), **doc) + return doc + + +def _parse_canonical_dbpointer(doc: Any, dummy0: Any) -> Any: + """Decode a JSON (deprecated) DBPointer to bson.dbref.DBRef.""" + dbref = doc["$dbPointer"] + if len(doc) != 1: + raise TypeError(f"Bad $dbPointer, extra field(s): {doc}") + if isinstance(dbref, DBRef): + dbref_doc = dbref.as_doc() + # DBPointer must not contain $db in its value. + if dbref.database is not None: + raise TypeError(f"Bad $dbPointer, extra field $db: {dbref_doc}") + if not isinstance(dbref.id, ObjectId): + raise TypeError(f"Bad $dbPointer, $id must be an ObjectId: {dbref_doc}") + if len(dbref_doc) != 2: + raise TypeError(f"Bad $dbPointer, extra field(s) in DBRef: {dbref_doc}") + return dbref + else: + raise TypeError(f"Bad $dbPointer, expected a DBRef: {doc}") + + +def _parse_canonical_int32(doc: Any, dummy0: Any) -> int: + """Decode a JSON int32 to python int.""" + i_str = doc["$numberInt"] + if len(doc) != 1: + raise TypeError(f"Bad $numberInt, extra field(s): {doc}") + if not isinstance(i_str, str): + raise TypeError(f"$numberInt must be string: {doc}") + return int(i_str) + + +def _parse_canonical_int64(doc: Any, dummy0: Any) -> Int64: + """Decode a JSON int64 to bson.int64.Int64.""" + l_str = doc["$numberLong"] + if len(doc) != 1: + raise TypeError(f"Bad $numberLong, extra field(s): {doc}") + return Int64(l_str) + + +def _parse_canonical_double(doc: Any, dummy0: Any) -> float: + """Decode a JSON double to python float.""" + d_str = doc["$numberDouble"] + if len(doc) != 1: + raise TypeError(f"Bad $numberDouble, extra field(s): {doc}") + if not isinstance(d_str, str): + raise TypeError(f"$numberDouble must be string: {doc}") + return float(d_str) + + +def _parse_canonical_decimal128(doc: Any, dummy0: Any) -> Decimal128: + """Decode a JSON decimal128 to bson.decimal128.Decimal128.""" + d_str = doc["$numberDecimal"] + if len(doc) != 1: + raise TypeError(f"Bad $numberDecimal, extra field(s): {doc}") + if not isinstance(d_str, str): + raise TypeError(f"$numberDecimal must be string: {doc}") + return Decimal128(d_str) + + +def _parse_canonical_minkey(doc: Any, dummy0: Any) -> MinKey: + """Decode a JSON MinKey to bson.min_key.MinKey.""" + if type(doc["$minKey"]) is not int or doc["$minKey"] != 1: # noqa: E721 + raise TypeError(f"$minKey value must be 1: {doc}") + if len(doc) != 1: + raise TypeError(f"Bad $minKey, extra field(s): {doc}") + return MinKey() + + +def _parse_canonical_maxkey(doc: Any, dummy0: Any) -> MaxKey: + """Decode a JSON MaxKey to bson.max_key.MaxKey.""" + if type(doc["$maxKey"]) is not int or doc["$maxKey"] != 1: # noqa: E721 + raise TypeError("$maxKey value must be 1: %s", (doc,)) + if len(doc) != 1: + raise TypeError(f"Bad $minKey, extra field(s): {doc}") + return MaxKey() + + +def _parse_binary(doc: Any, json_options: JSONOptions) -> Union[Binary, uuid.UUID]: + if "$type" in doc: + return _parse_legacy_binary(doc, json_options) + else: + return _parse_canonical_binary(doc, json_options) + + +def _parse_timestamp(doc: Any, dummy0: Any) -> Timestamp: + tsp = doc["$timestamp"] + return Timestamp(tsp["t"], tsp["i"]) + + +_PARSERS: dict[str, Callable[[Any, JSONOptions], Any]] = { + "$oid": _parse_canonical_oid, + "$ref": _parse_canonical_dbref, + "$date": _parse_canonical_datetime, + "$regex": _parse_legacy_regex, + "$minKey": _parse_canonical_minkey, + "$maxKey": _parse_canonical_maxkey, + "$binary": _parse_binary, + "$code": _parse_canonical_code, + "$uuid": _parse_legacy_uuid, + "$undefined": lambda _, _1: None, + "$numberLong": _parse_canonical_int64, + "$timestamp": _parse_timestamp, + "$numberDecimal": _parse_canonical_decimal128, + "$dbPointer": _parse_canonical_dbpointer, + "$regularExpression": _parse_canonical_regex, + "$symbol": _parse_canonical_symbol, + "$numberInt": _parse_canonical_int32, + "$numberDouble": _parse_canonical_double, +} +_PARSERS_SET = set(_PARSERS) + + +def _encode_binary(data: bytes, subtype: int, json_options: JSONOptions) -> Any: + if json_options.json_mode == JSONMode.LEGACY: + return {"$binary": base64.b64encode(data).decode(), "$type": "%02x" % subtype} + return {"$binary": {"base64": base64.b64encode(data).decode(), "subType": "%02x" % subtype}} + + +def _encode_datetimems(obj: Any, json_options: JSONOptions) -> dict: + if ( + json_options.datetime_representation == DatetimeRepresentation.ISO8601 + and 0 <= int(obj) <= _max_datetime_ms() + ): + return _encode_datetime(obj.as_datetime(), json_options) + elif json_options.datetime_representation == DatetimeRepresentation.LEGACY: + return {"$date": str(int(obj))} + return {"$date": {"$numberLong": str(int(obj))}} + + +def _encode_code(obj: Code, json_options: JSONOptions) -> dict: + if obj.scope is None: + return {"$code": str(obj)} + else: + return {"$code": str(obj), "$scope": _json_convert(obj.scope, json_options)} + + +def _encode_int64(obj: Int64, json_options: JSONOptions) -> Any: + if json_options.strict_number_long: + return {"$numberLong": str(obj)} + else: + return int(obj) + + +def _encode_noop(obj: Any, dummy0: Any) -> Any: + return obj + + +def _encode_regex(obj: Any, json_options: JSONOptions) -> dict: + flags = "" + if obj.flags & re.IGNORECASE: + flags += "i" + if obj.flags & re.LOCALE: + flags += "l" + if obj.flags & re.MULTILINE: + flags += "m" + if obj.flags & re.DOTALL: + flags += "s" + if obj.flags & re.UNICODE: + flags += "u" + if obj.flags & re.VERBOSE: + flags += "x" + if isinstance(obj.pattern, str): + pattern = obj.pattern + else: + pattern = obj.pattern.decode("utf-8") + if json_options.json_mode == JSONMode.LEGACY: + return {"$regex": pattern, "$options": flags} + return {"$regularExpression": {"pattern": pattern, "options": flags}} + + +def _encode_int(obj: int, json_options: JSONOptions) -> Any: + if json_options.json_mode == JSONMode.CANONICAL: + if -_INT32_MAX <= obj < _INT32_MAX: + return {"$numberInt": str(obj)} + return {"$numberLong": str(obj)} + return obj + + +def _encode_float(obj: float, json_options: JSONOptions) -> Any: + if json_options.json_mode != JSONMode.LEGACY: + if math.isnan(obj): + return {"$numberDouble": "NaN"} + elif math.isinf(obj): + representation = "Infinity" if obj > 0 else "-Infinity" + return {"$numberDouble": representation} + elif json_options.json_mode == JSONMode.CANONICAL: + # repr() will return the shortest string guaranteed to produce the + # original value, when float() is called on it. + return {"$numberDouble": str(repr(obj))} + return obj + + +def _encode_datetime(obj: datetime.datetime, json_options: JSONOptions) -> dict: + if json_options.datetime_representation == DatetimeRepresentation.ISO8601: + if not obj.tzinfo: + obj = obj.replace(tzinfo=utc) + assert obj.tzinfo is not None + if obj >= EPOCH_AWARE: + off = obj.tzinfo.utcoffset(obj) + if (off.days, off.seconds, off.microseconds) == (0, 0, 0): # type: ignore + tz_string = "Z" + else: + tz_string = obj.strftime("%z") + millis = int(obj.microsecond / 1000) + fracsecs = ".%03d" % (millis,) if millis else "" + return { + "$date": "{}{}{}".format(obj.strftime("%Y-%m-%dT%H:%M:%S"), fracsecs, tz_string) + } + + millis = _datetime_to_millis(obj) + if json_options.datetime_representation == DatetimeRepresentation.LEGACY: + return {"$date": millis} + return {"$date": {"$numberLong": str(millis)}} + + +def _encode_bytes(obj: bytes, json_options: JSONOptions) -> dict: + return _encode_binary(obj, 0, json_options) + + +def _encode_binary_obj(obj: Binary, json_options: JSONOptions) -> dict: + return _encode_binary(obj, obj.subtype, json_options) + + +def _encode_uuid(obj: uuid.UUID, json_options: JSONOptions) -> dict: + if json_options.strict_uuid: + binval = Binary.from_uuid(obj, uuid_representation=json_options.uuid_representation) + return _encode_binary(binval, binval.subtype, json_options) + else: + return {"$uuid": obj.hex} + + +def _encode_objectid(obj: ObjectId, dummy0: Any) -> dict: + return {"$oid": str(obj)} + + +def _encode_timestamp(obj: Timestamp, dummy0: Any) -> dict: + return {"$timestamp": {"t": obj.time, "i": obj.inc}} + + +def _encode_decimal128(obj: Timestamp, dummy0: Any) -> dict: + return {"$numberDecimal": str(obj)} + + +def _encode_dbref(obj: DBRef, json_options: JSONOptions) -> dict: + return _json_convert(obj.as_doc(), json_options=json_options) + + +def _encode_minkey(dummy0: Any, dummy1: Any) -> dict: + return {"$minKey": 1} + + +def _encode_maxkey(dummy0: Any, dummy1: Any) -> dict: + return {"$maxKey": 1} + + +# Encoders for BSON types +# Each encoder function's signature is: +# - obj: a Python data type, e.g. a Python int for _encode_int +# - json_options: a JSONOptions +_ENCODERS: dict[Type, Callable[[Any, JSONOptions], Any]] = { + bool: _encode_noop, + bytes: _encode_bytes, + datetime.datetime: _encode_datetime, + DatetimeMS: _encode_datetimems, + float: _encode_float, + int: _encode_int, + str: _encode_noop, + type(None): _encode_noop, + uuid.UUID: _encode_uuid, + Binary: _encode_binary_obj, + Int64: _encode_int64, + Code: _encode_code, + DBRef: _encode_dbref, + MaxKey: _encode_maxkey, + MinKey: _encode_minkey, + ObjectId: _encode_objectid, + Regex: _encode_regex, + RE_TYPE: _encode_regex, + Timestamp: _encode_timestamp, + Decimal128: _encode_decimal128, +} + +# Map each _type_marker to its encoder for faster lookup. +_MARKERS: dict[int, Callable[[Any, JSONOptions], Any]] = {} +for _typ in _ENCODERS: + if hasattr(_typ, "_type_marker"): + _MARKERS[_typ._type_marker] = _ENCODERS[_typ] + +_BUILT_IN_TYPES = tuple(t for t in _ENCODERS) + + +def default(obj: Any, json_options: JSONOptions = DEFAULT_JSON_OPTIONS) -> Any: + # First see if the type is already cached. KeyError will only ever + # happen once per subtype. + try: + return _ENCODERS[type(obj)](obj, json_options) + except KeyError: + pass + + # Second, fall back to trying _type_marker. This has to be done + # before the loop below since users could subclass one of our + # custom types that subclasses a python built-in (e.g. Binary) + if hasattr(obj, "_type_marker"): + marker = obj._type_marker + if marker in _MARKERS: + func = _MARKERS[marker] + # Cache this type for faster subsequent lookup. + _ENCODERS[type(obj)] = func + return func(obj, json_options) + + # Third, test each base type. This will only happen once for + # a subtype of a supported base type. + for base in _BUILT_IN_TYPES: + if isinstance(obj, base): + func = _ENCODERS[base] + # Cache this type for faster subsequent lookup. + _ENCODERS[type(obj)] = func + return func(obj, json_options) + + raise TypeError("%r is not JSON serializable" % obj) + + +def _get_str_size(obj: Any) -> int: + return len(obj) + + +def _get_datetime_size(obj: datetime.datetime) -> int: + return 5 + len(str(obj.time())) + + +def _get_regex_size(obj: Regex) -> int: + return 18 + len(obj.pattern) + + +def _get_dbref_size(obj: DBRef) -> int: + return 34 + len(obj.collection) + + +_CONSTANT_SIZE_TABLE: dict[Any, int] = { + ObjectId: 28, + int: 11, + Int64: 11, + Decimal128: 11, + Timestamp: 14, + MinKey: 8, + MaxKey: 8, +} + +_VARIABLE_SIZE_TABLE: dict[Any, Callable[[Any], int]] = { + str: _get_str_size, + bytes: _get_str_size, + datetime.datetime: _get_datetime_size, + Regex: _get_regex_size, + DBRef: _get_dbref_size, +} + + +def get_size(obj: Any, max_size: int, current_size: int = 0) -> int: + """Recursively finds size of objects""" + if current_size >= max_size: + return current_size + + obj_type = type(obj) + + # Check to see if the obj has a constant size estimate + try: + return _CONSTANT_SIZE_TABLE[obj_type] + except KeyError: + pass + + # Check to see if the obj has a variable but simple size estimate + try: + return _VARIABLE_SIZE_TABLE[obj_type](obj) + except KeyError: + pass + + # Special cases that require recursion + if obj_type == Code: + if obj.scope: + current_size += ( + 5 + get_size(obj.scope, max_size, current_size) + len(obj) - len(obj.scope) + ) + else: + current_size += 5 + len(obj) + elif obj_type == dict: + for k, v in obj.items(): + current_size += get_size(k, max_size, current_size) + current_size += get_size(v, max_size, current_size) + if current_size >= max_size: + return current_size + elif hasattr(obj, "__iter__"): + for i in obj: + current_size += get_size(i, max_size, current_size) + if current_size >= max_size: + return current_size + return current_size + + +def _truncate_documents(obj: Any, max_length: int) -> Tuple[Any, int]: + """Recursively truncate documents as needed to fit inside max_length characters.""" + if max_length <= 0: + return None, 0 + remaining = max_length + if hasattr(obj, "items"): + truncated: Any = {} + for k, v in obj.items(): + truncated_v, remaining = _truncate_documents(v, remaining) + if truncated_v: + truncated[k] = truncated_v + if remaining <= 0: + break + return truncated, remaining + elif hasattr(obj, "__iter__") and not isinstance(obj, (str, bytes)): + truncated: Any = [] # type:ignore[no-redef] + for v in obj: + truncated_v, remaining = _truncate_documents(v, remaining) + if truncated_v: + truncated.append(truncated_v) + if remaining <= 0: + break + return truncated, remaining + else: + return _truncate(obj, remaining) + + +def _truncate(obj: Any, remaining: int) -> Tuple[Any, int]: + size = get_size(obj, remaining) + + if size <= remaining: + return obj, remaining - size + else: + try: + truncated = obj[:remaining] + except TypeError: + truncated = obj + return truncated, remaining - size diff --git a/venv/Lib/site-packages/bson/max_key.py b/venv/Lib/site-packages/bson/max_key.py new file mode 100644 index 00000000..445e12f5 --- /dev/null +++ b/venv/Lib/site-packages/bson/max_key.py @@ -0,0 +1,56 @@ +# Copyright 2010-present MongoDB, 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. + +"""Representation for the MongoDB internal MaxKey type.""" +from __future__ import annotations + +from typing import Any + + +class MaxKey: + """MongoDB internal MaxKey type.""" + + __slots__ = () + + _type_marker = 127 + + def __getstate__(self) -> Any: + return {} + + def __setstate__(self, state: Any) -> None: + pass + + def __eq__(self, other: Any) -> bool: + return isinstance(other, MaxKey) + + def __hash__(self) -> int: + return hash(self._type_marker) + + def __ne__(self, other: Any) -> bool: + return not self == other + + def __le__(self, other: Any) -> bool: + return isinstance(other, MaxKey) + + def __lt__(self, dummy: Any) -> bool: + return False + + def __ge__(self, dummy: Any) -> bool: + return True + + def __gt__(self, other: Any) -> bool: + return not isinstance(other, MaxKey) + + def __repr__(self) -> str: + return "MaxKey()" diff --git a/venv/Lib/site-packages/bson/min_key.py b/venv/Lib/site-packages/bson/min_key.py new file mode 100644 index 00000000..37828dcf --- /dev/null +++ b/venv/Lib/site-packages/bson/min_key.py @@ -0,0 +1,56 @@ +# Copyright 2010-present MongoDB, 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. + +"""Representation for the MongoDB internal MinKey type.""" +from __future__ import annotations + +from typing import Any + + +class MinKey: + """MongoDB internal MinKey type.""" + + __slots__ = () + + _type_marker = 255 + + def __getstate__(self) -> Any: + return {} + + def __setstate__(self, state: Any) -> None: + pass + + def __eq__(self, other: Any) -> bool: + return isinstance(other, MinKey) + + def __hash__(self) -> int: + return hash(self._type_marker) + + def __ne__(self, other: Any) -> bool: + return not self == other + + def __le__(self, dummy: Any) -> bool: + return True + + def __lt__(self, other: Any) -> bool: + return not isinstance(other, MinKey) + + def __ge__(self, other: Any) -> bool: + return isinstance(other, MinKey) + + def __gt__(self, dummy: Any) -> bool: + return False + + def __repr__(self) -> str: + return "MinKey()" diff --git a/venv/Lib/site-packages/bson/objectid.py b/venv/Lib/site-packages/bson/objectid.py new file mode 100644 index 00000000..57efdc79 --- /dev/null +++ b/venv/Lib/site-packages/bson/objectid.py @@ -0,0 +1,278 @@ +# Copyright 2009-2015 MongoDB, 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. + +"""Tools for working with MongoDB ObjectIds.""" +from __future__ import annotations + +import binascii +import calendar +import datetime +import os +import struct +import threading +import time +from random import SystemRandom +from typing import Any, NoReturn, Optional, Type, Union + +from bson.errors import InvalidId +from bson.tz_util import utc + +_MAX_COUNTER_VALUE = 0xFFFFFF + + +def _raise_invalid_id(oid: str) -> NoReturn: + raise InvalidId( + "%r is not a valid ObjectId, it must be a 12-byte input" + " or a 24-character hex string" % oid + ) + + +def _random_bytes() -> bytes: + """Get the 5-byte random field of an ObjectId.""" + return os.urandom(5) + + +class ObjectId: + """A MongoDB ObjectId.""" + + _pid = os.getpid() + + _inc = SystemRandom().randint(0, _MAX_COUNTER_VALUE) + _inc_lock = threading.Lock() + + __random = _random_bytes() + + __slots__ = ("__id",) + + _type_marker = 7 + + def __init__(self, oid: Optional[Union[str, ObjectId, bytes]] = None) -> None: + """Initialize a new ObjectId. + + An ObjectId is a 12-byte unique identifier consisting of: + + - a 4-byte value representing the seconds since the Unix epoch, + - a 5-byte random value, + - a 3-byte counter, starting with a random value. + + By default, ``ObjectId()`` creates a new unique identifier. The + optional parameter `oid` can be an :class:`ObjectId`, or any 12 + :class:`bytes`. + + For example, the 12 bytes b'foo-bar-quux' do not follow the ObjectId + specification but they are acceptable input:: + + >>> ObjectId(b'foo-bar-quux') + ObjectId('666f6f2d6261722d71757578') + + `oid` can also be a :class:`str` of 24 hex digits:: + + >>> ObjectId('0123456789ab0123456789ab') + ObjectId('0123456789ab0123456789ab') + + Raises :class:`~bson.errors.InvalidId` if `oid` is not 12 bytes nor + 24 hex digits, or :class:`TypeError` if `oid` is not an accepted type. + + :param oid: a valid ObjectId. + + .. seealso:: The MongoDB documentation on `ObjectIds `_. + + .. versionchanged:: 3.8 + :class:`~bson.objectid.ObjectId` now implements the `ObjectID + specification version 0.2 + `_. + """ + if oid is None: + self.__generate() + elif isinstance(oid, bytes) and len(oid) == 12: + self.__id = oid + else: + self.__validate(oid) + + @classmethod + def from_datetime(cls: Type[ObjectId], generation_time: datetime.datetime) -> ObjectId: + """Create a dummy ObjectId instance with a specific generation time. + + This method is useful for doing range queries on a field + containing :class:`ObjectId` instances. + + .. warning:: + It is not safe to insert a document containing an ObjectId + generated using this method. This method deliberately + eliminates the uniqueness guarantee that ObjectIds + generally provide. ObjectIds generated with this method + should be used exclusively in queries. + + `generation_time` will be converted to UTC. Naive datetime + instances will be treated as though they already contain UTC. + + An example using this helper to get documents where ``"_id"`` + was generated before January 1, 2010 would be: + + >>> gen_time = datetime.datetime(2010, 1, 1) + >>> dummy_id = ObjectId.from_datetime(gen_time) + >>> result = collection.find({"_id": {"$lt": dummy_id}}) + + :param generation_time: :class:`~datetime.datetime` to be used + as the generation time for the resulting ObjectId. + """ + offset = generation_time.utcoffset() + if offset is not None: + generation_time = generation_time - offset + timestamp = calendar.timegm(generation_time.timetuple()) + oid = struct.pack(">I", int(timestamp)) + b"\x00\x00\x00\x00\x00\x00\x00\x00" + return cls(oid) + + @classmethod + def is_valid(cls: Type[ObjectId], oid: Any) -> bool: + """Checks if a `oid` string is valid or not. + + :param oid: the object id to validate + + .. versionadded:: 2.3 + """ + if not oid: + return False + + try: + ObjectId(oid) + return True + except (InvalidId, TypeError): + return False + + @classmethod + def _random(cls) -> bytes: + """Generate a 5-byte random number once per process.""" + pid = os.getpid() + if pid != cls._pid: + cls._pid = pid + cls.__random = _random_bytes() + return cls.__random + + def __generate(self) -> None: + """Generate a new value for this ObjectId.""" + # 4 bytes current time + oid = struct.pack(">I", int(time.time())) + + # 5 bytes random + oid += ObjectId._random() + + # 3 bytes inc + with ObjectId._inc_lock: + oid += struct.pack(">I", ObjectId._inc)[1:4] + ObjectId._inc = (ObjectId._inc + 1) % (_MAX_COUNTER_VALUE + 1) + + self.__id = oid + + def __validate(self, oid: Any) -> None: + """Validate and use the given id for this ObjectId. + + Raises TypeError if id is not an instance of :class:`str`, + :class:`bytes`, or ObjectId. Raises InvalidId if it is not a + valid ObjectId. + + :param oid: a valid ObjectId + """ + if isinstance(oid, ObjectId): + self.__id = oid.binary + elif isinstance(oid, str): + if len(oid) == 24: + try: + self.__id = bytes.fromhex(oid) + except (TypeError, ValueError): + _raise_invalid_id(oid) + else: + _raise_invalid_id(oid) + else: + raise TypeError(f"id must be an instance of (bytes, str, ObjectId), not {type(oid)}") + + @property + def binary(self) -> bytes: + """12-byte binary representation of this ObjectId.""" + return self.__id + + @property + def generation_time(self) -> datetime.datetime: + """A :class:`datetime.datetime` instance representing the time of + generation for this :class:`ObjectId`. + + The :class:`datetime.datetime` is timezone aware, and + represents the generation time in UTC. It is precise to the + second. + """ + timestamp = struct.unpack(">I", self.__id[0:4])[0] + return datetime.datetime.fromtimestamp(timestamp, utc) + + def __getstate__(self) -> bytes: + """Return value of object for pickling. + needed explicitly because __slots__() defined. + """ + return self.__id + + def __setstate__(self, value: Any) -> None: + """Explicit state set from pickling""" + # Provide backwards compatibility with OIDs + # pickled with pymongo-1.9 or older. + if isinstance(value, dict): + oid = value["_ObjectId__id"] + else: + oid = value + # ObjectIds pickled in python 2.x used `str` for __id. + # In python 3.x this has to be converted to `bytes` + # by encoding latin-1. + if isinstance(oid, str): + self.__id = oid.encode("latin-1") + else: + self.__id = oid + + def __str__(self) -> str: + return binascii.hexlify(self.__id).decode() + + def __repr__(self) -> str: + return f"ObjectId('{self!s}')" + + def __eq__(self, other: Any) -> bool: + if isinstance(other, ObjectId): + return self.__id == other.binary + return NotImplemented + + def __ne__(self, other: Any) -> bool: + if isinstance(other, ObjectId): + return self.__id != other.binary + return NotImplemented + + def __lt__(self, other: Any) -> bool: + if isinstance(other, ObjectId): + return self.__id < other.binary + return NotImplemented + + def __le__(self, other: Any) -> bool: + if isinstance(other, ObjectId): + return self.__id <= other.binary + return NotImplemented + + def __gt__(self, other: Any) -> bool: + if isinstance(other, ObjectId): + return self.__id > other.binary + return NotImplemented + + def __ge__(self, other: Any) -> bool: + if isinstance(other, ObjectId): + return self.__id >= other.binary + return NotImplemented + + def __hash__(self) -> int: + """Get a hash value for this :class:`ObjectId`.""" + return hash(self.__id) diff --git a/venv/Lib/site-packages/bson/py.typed b/venv/Lib/site-packages/bson/py.typed new file mode 100644 index 00000000..0f405706 --- /dev/null +++ b/venv/Lib/site-packages/bson/py.typed @@ -0,0 +1,2 @@ +# PEP-561 Support File. +# "Package maintainers who wish to support type checking of their code MUST add a marker file named py.typed to their package supporting typing". diff --git a/venv/Lib/site-packages/bson/raw_bson.py b/venv/Lib/site-packages/bson/raw_bson.py new file mode 100644 index 00000000..2ce53143 --- /dev/null +++ b/venv/Lib/site-packages/bson/raw_bson.py @@ -0,0 +1,196 @@ +# Copyright 2015-present MongoDB, 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. + +"""Tools for representing raw BSON documents. + +Inserting and Retrieving RawBSONDocuments +========================================= + +Example: Moving a document between different databases/collections + +.. doctest:: + + >>> import bson + >>> from pymongo import MongoClient + >>> from bson.raw_bson import RawBSONDocument + >>> client = MongoClient(document_class=RawBSONDocument) + >>> client.drop_database("db") + >>> client.drop_database("replica_db") + >>> db = client.db + >>> result = db.test.insert_many( + ... [{"_id": 1, "a": 1}, {"_id": 2, "b": 1}, {"_id": 3, "c": 1}, {"_id": 4, "d": 1}] + ... ) + >>> replica_db = client.replica_db + >>> for doc in db.test.find(): + ... print(f"raw document: {doc.raw}") + ... print(f"decoded document: {bson.decode(doc.raw)}") + ... result = replica_db.test.insert_one(doc) + ... + raw document: b'...' + decoded document: {'_id': 1, 'a': 1} + raw document: b'...' + decoded document: {'_id': 2, 'b': 1} + raw document: b'...' + decoded document: {'_id': 3, 'c': 1} + raw document: b'...' + decoded document: {'_id': 4, 'd': 1} + +For use cases like moving documents across different databases or writing binary +blobs to disk, using raw BSON documents provides better speed and avoids the +overhead of decoding or encoding BSON. +""" +from __future__ import annotations + +from typing import Any, ItemsView, Iterator, Mapping, Optional + +from bson import _get_object_size, _raw_to_dict +from bson.codec_options import _RAW_BSON_DOCUMENT_MARKER, CodecOptions +from bson.codec_options import DEFAULT_CODEC_OPTIONS as DEFAULT + + +def _inflate_bson( + bson_bytes: bytes, codec_options: CodecOptions[RawBSONDocument], raw_array: bool = False +) -> dict[str, Any]: + """Inflates the top level fields of a BSON document. + + :param bson_bytes: the BSON bytes that compose this document + :param codec_options: An instance of + :class:`~bson.codec_options.CodecOptions` whose ``document_class`` + must be :class:`RawBSONDocument`. + """ + return _raw_to_dict(bson_bytes, 4, len(bson_bytes) - 1, codec_options, {}, raw_array=raw_array) + + +class RawBSONDocument(Mapping[str, Any]): + """Representation for a MongoDB document that provides access to the raw + BSON bytes that compose it. + + Only when a field is accessed or modified within the document does + RawBSONDocument decode its bytes. + """ + + __slots__ = ("__raw", "__inflated_doc", "__codec_options") + _type_marker = _RAW_BSON_DOCUMENT_MARKER + __codec_options: CodecOptions[RawBSONDocument] + + def __init__( + self, bson_bytes: bytes, codec_options: Optional[CodecOptions[RawBSONDocument]] = None + ) -> None: + """Create a new :class:`RawBSONDocument` + + :class:`RawBSONDocument` is a representation of a BSON document that + provides access to the underlying raw BSON bytes. Only when a field is + accessed or modified within the document does RawBSONDocument decode + its bytes. + + :class:`RawBSONDocument` implements the ``Mapping`` abstract base + class from the standard library so it can be used like a read-only + ``dict``:: + + >>> from bson import encode + >>> raw_doc = RawBSONDocument(encode({'_id': 'my_doc'})) + >>> raw_doc.raw + b'...' + >>> raw_doc['_id'] + 'my_doc' + + :param bson_bytes: the BSON bytes that compose this document + :param codec_options: An instance of + :class:`~bson.codec_options.CodecOptions` whose ``document_class`` + must be :class:`RawBSONDocument`. The default is + :attr:`DEFAULT_RAW_BSON_OPTIONS`. + + .. versionchanged:: 3.8 + :class:`RawBSONDocument` now validates that the ``bson_bytes`` + passed in represent a single bson document. + + .. versionchanged:: 3.5 + If a :class:`~bson.codec_options.CodecOptions` is passed in, its + `document_class` must be :class:`RawBSONDocument`. + """ + self.__raw = bson_bytes + self.__inflated_doc: Optional[Mapping[str, Any]] = None + # Can't default codec_options to DEFAULT_RAW_BSON_OPTIONS in signature, + # it refers to this class RawBSONDocument. + if codec_options is None: + codec_options = DEFAULT_RAW_BSON_OPTIONS + elif not issubclass(codec_options.document_class, RawBSONDocument): + raise TypeError( + "RawBSONDocument cannot use CodecOptions with document " + f"class {codec_options.document_class}" + ) + self.__codec_options = codec_options + # Validate the bson object size. + _get_object_size(bson_bytes, 0, len(bson_bytes)) + + @property + def raw(self) -> bytes: + """The raw BSON bytes composing this document.""" + return self.__raw + + def items(self) -> ItemsView[str, Any]: + """Lazily decode and iterate elements in this document.""" + return self.__inflated.items() + + @property + def __inflated(self) -> Mapping[str, Any]: + if self.__inflated_doc is None: + # We already validated the object's size when this document was + # created, so no need to do that again. + self.__inflated_doc = self._inflate_bson(self.__raw, self.__codec_options) + return self.__inflated_doc + + @staticmethod + def _inflate_bson( + bson_bytes: bytes, codec_options: CodecOptions[RawBSONDocument] + ) -> Mapping[str, Any]: + return _inflate_bson(bson_bytes, codec_options) + + def __getitem__(self, item: str) -> Any: + return self.__inflated[item] + + def __iter__(self) -> Iterator[str]: + return iter(self.__inflated) + + def __len__(self) -> int: + return len(self.__inflated) + + def __eq__(self, other: Any) -> bool: + if isinstance(other, RawBSONDocument): + return self.__raw == other.raw + return NotImplemented + + def __repr__(self) -> str: + return f"{self.__class__.__name__}({self.raw!r}, codec_options={self.__codec_options!r})" + + +class _RawArrayBSONDocument(RawBSONDocument): + """A RawBSONDocument that only expands sub-documents and arrays when accessed.""" + + @staticmethod + def _inflate_bson( + bson_bytes: bytes, codec_options: CodecOptions[RawBSONDocument] + ) -> Mapping[str, Any]: + return _inflate_bson(bson_bytes, codec_options, raw_array=True) + + +DEFAULT_RAW_BSON_OPTIONS: CodecOptions[RawBSONDocument] = DEFAULT.with_options( + document_class=RawBSONDocument +) +_RAW_ARRAY_BSON_OPTIONS: CodecOptions[_RawArrayBSONDocument] = DEFAULT.with_options( + document_class=_RawArrayBSONDocument +) +"""The default :class:`~bson.codec_options.CodecOptions` for +:class:`RawBSONDocument`. +""" diff --git a/venv/Lib/site-packages/bson/regex.py b/venv/Lib/site-packages/bson/regex.py new file mode 100644 index 00000000..60cff4fd --- /dev/null +++ b/venv/Lib/site-packages/bson/regex.py @@ -0,0 +1,133 @@ +# Copyright 2013-present MongoDB, 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. + +"""Tools for representing MongoDB regular expressions.""" +from __future__ import annotations + +import re +from typing import Any, Generic, Pattern, Type, TypeVar, Union + +from bson._helpers import _getstate_slots, _setstate_slots +from bson.son import RE_TYPE + + +def str_flags_to_int(str_flags: str) -> int: + flags = 0 + if "i" in str_flags: + flags |= re.IGNORECASE + if "l" in str_flags: + flags |= re.LOCALE + if "m" in str_flags: + flags |= re.MULTILINE + if "s" in str_flags: + flags |= re.DOTALL + if "u" in str_flags: + flags |= re.UNICODE + if "x" in str_flags: + flags |= re.VERBOSE + + return flags + + +_T = TypeVar("_T", str, bytes) + + +class Regex(Generic[_T]): + """BSON regular expression data.""" + + __slots__ = ("pattern", "flags") + + __getstate__ = _getstate_slots + __setstate__ = _setstate_slots + + _type_marker = 11 + + @classmethod + def from_native(cls: Type[Regex[Any]], regex: Pattern[_T]) -> Regex[_T]: + """Convert a Python regular expression into a ``Regex`` instance. + + Note that in Python 3, a regular expression compiled from a + :class:`str` has the ``re.UNICODE`` flag set. If it is undesirable + to store this flag in a BSON regular expression, unset it first:: + + >>> pattern = re.compile('.*') + >>> regex = Regex.from_native(pattern) + >>> regex.flags ^= re.UNICODE + >>> db.collection.insert_one({'pattern': regex}) + + :param regex: A regular expression object from ``re.compile()``. + + .. warning:: + Python regular expressions use a different syntax and different + set of flags than MongoDB, which uses `PCRE`_. A regular + expression retrieved from the server may not compile in + Python, or may match a different set of strings in Python than + when used in a MongoDB query. + + .. _PCRE: http://www.pcre.org/ + """ + if not isinstance(regex, RE_TYPE): + raise TypeError("regex must be a compiled regular expression, not %s" % type(regex)) + + return Regex(regex.pattern, regex.flags) + + def __init__(self, pattern: _T, flags: Union[str, int] = 0) -> None: + """BSON regular expression data. + + This class is useful to store and retrieve regular expressions that are + incompatible with Python's regular expression dialect. + + :param pattern: string + :param flags: an integer bitmask, or a string of flag + characters like "im" for IGNORECASE and MULTILINE + """ + if not isinstance(pattern, (str, bytes)): + raise TypeError("pattern must be a string, not %s" % type(pattern)) + self.pattern: _T = pattern + + if isinstance(flags, str): + self.flags = str_flags_to_int(flags) + elif isinstance(flags, int): + self.flags = flags + else: + raise TypeError("flags must be a string or int, not %s" % type(flags)) + + def __eq__(self, other: Any) -> bool: + if isinstance(other, Regex): + return self.pattern == other.pattern and self.flags == other.flags + else: + return NotImplemented + + __hash__ = None # type: ignore + + def __ne__(self, other: Any) -> bool: + return not self == other + + def __repr__(self) -> str: + return f"Regex({self.pattern!r}, {self.flags!r})" + + def try_compile(self) -> Pattern[_T]: + """Compile this :class:`Regex` as a Python regular expression. + + .. warning:: + Python regular expressions use a different syntax and different + set of flags than MongoDB, which uses `PCRE`_. A regular + expression retrieved from the server may not compile in + Python, or may match a different set of strings in Python than + when used in a MongoDB query. :meth:`try_compile()` may raise + :exc:`re.error`. + + .. _PCRE: http://www.pcre.org/ + """ + return re.compile(self.pattern, self.flags) diff --git a/venv/Lib/site-packages/bson/son.py b/venv/Lib/site-packages/bson/son.py new file mode 100644 index 00000000..cf627172 --- /dev/null +++ b/venv/Lib/site-packages/bson/son.py @@ -0,0 +1,211 @@ +# Copyright 2009-present MongoDB, 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. + +"""Tools for creating and manipulating SON, the Serialized Ocument Notation. + +Regular dictionaries can be used instead of SON objects, but not when the order +of keys is important. A SON object can be used just like a normal Python +dictionary. +""" +from __future__ import annotations + +import copy +import re +from collections.abc import Mapping as _Mapping +from typing import ( + Any, + Dict, + Iterable, + Iterator, + Mapping, + Optional, + Pattern, + Tuple, + Type, + TypeVar, + Union, + cast, +) + +# This sort of sucks, but seems to be as good as it gets... +# This is essentially the same as re._pattern_type +RE_TYPE: Type[Pattern[Any]] = type(re.compile("")) + +_Key = TypeVar("_Key") +_Value = TypeVar("_Value") +_T = TypeVar("_T") + + +class SON(Dict[_Key, _Value]): + """SON data. + + A subclass of dict that maintains ordering of keys and provides a + few extra niceties for dealing with SON. SON provides an API + similar to collections.OrderedDict. + """ + + __keys: list[Any] + + def __init__( + self, + data: Optional[Union[Mapping[_Key, _Value], Iterable[Tuple[_Key, _Value]]]] = None, + **kwargs: Any, + ) -> None: + self.__keys = [] + dict.__init__(self) + self.update(data) + self.update(kwargs) + + def __new__(cls: Type[SON[_Key, _Value]], *args: Any, **kwargs: Any) -> SON[_Key, _Value]: + instance = super().__new__(cls, *args, **kwargs) # type: ignore[type-var] + instance.__keys = [] + return instance + + def __repr__(self) -> str: + result = [] + for key in self.__keys: + result.append(f"({key!r}, {self[key]!r})") + return "SON([%s])" % ", ".join(result) + + def __setitem__(self, key: _Key, value: _Value) -> None: + if key not in self.__keys: + self.__keys.append(key) + dict.__setitem__(self, key, value) + + def __delitem__(self, key: _Key) -> None: + self.__keys.remove(key) + dict.__delitem__(self, key) + + def copy(self) -> SON[_Key, _Value]: + other: SON[_Key, _Value] = SON() + other.update(self) + return other + + # TODO this is all from UserDict.DictMixin. it could probably be made more + # efficient. + # second level definitions support higher levels + def __iter__(self) -> Iterator[_Key]: + yield from self.__keys + + def has_key(self, key: _Key) -> bool: + return key in self.__keys + + def iterkeys(self) -> Iterator[_Key]: + return self.__iter__() + + # fourth level uses definitions from lower levels + def itervalues(self) -> Iterator[_Value]: + for _, v in self.items(): + yield v + + def values(self) -> list[_Value]: # type: ignore[override] + return [v for _, v in self.items()] + + def clear(self) -> None: + self.__keys = [] + super().clear() + + def setdefault(self, key: _Key, default: _Value) -> _Value: + try: + return self[key] + except KeyError: + self[key] = default + return default + + def pop(self, key: _Key, *args: Union[_Value, _T]) -> Union[_Value, _T]: + if len(args) > 1: + raise TypeError("pop expected at most 2 arguments, got " + repr(1 + len(args))) + try: + value = self[key] + except KeyError: + if args: + return args[0] + raise + del self[key] + return value + + def popitem(self) -> Tuple[_Key, _Value]: + try: + k, v = next(iter(self.items())) + except StopIteration: + raise KeyError("container is empty") from None + del self[k] + return (k, v) + + def update(self, other: Optional[Any] = None, **kwargs: _Value) -> None: # type: ignore[override] + # Make progressively weaker assumptions about "other" + if other is None: + pass + elif hasattr(other, "items"): + for k, v in other.items(): + self[k] = v + elif hasattr(other, "keys"): + for k in other: + self[k] = other[k] + else: + for k, v in other: + self[k] = v + if kwargs: + self.update(kwargs) + + def get( # type: ignore[override] + self, key: _Key, default: Optional[Union[_Value, _T]] = None + ) -> Union[_Value, _T, None]: + try: + return self[key] + except KeyError: + return default + + def __eq__(self, other: Any) -> bool: + """Comparison to another SON is order-sensitive while comparison to a + regular dictionary is order-insensitive. + """ + if isinstance(other, SON): + return len(self) == len(other) and list(self.items()) == list(other.items()) + return cast(bool, self.to_dict() == other) + + def __ne__(self, other: Any) -> bool: + return not self == other + + def __len__(self) -> int: + return len(self.__keys) + + def to_dict(self) -> dict[_Key, _Value]: + """Convert a SON document to a normal Python dictionary instance. + + This is trickier than just *dict(...)* because it needs to be + recursive. + """ + + def transform_value(value: Any) -> Any: + if isinstance(value, list): + return [transform_value(v) for v in value] + elif isinstance(value, _Mapping): + return {k: transform_value(v) for k, v in value.items()} + else: + return value + + return cast("dict[_Key, _Value]", transform_value(dict(self))) + + def __deepcopy__(self, memo: dict[int, SON[_Key, _Value]]) -> SON[_Key, _Value]: + out: SON[_Key, _Value] = SON() + val_id = id(self) + if val_id in memo: + return memo[val_id] + memo[val_id] = out + for k, v in self.items(): + if not isinstance(v, RE_TYPE): + v = copy.deepcopy(v, memo) # noqa: PLW2901 + out[k] = v + return out diff --git a/venv/Lib/site-packages/bson/time64.c b/venv/Lib/site-packages/bson/time64.c new file mode 100644 index 00000000..a21fbb90 --- /dev/null +++ b/venv/Lib/site-packages/bson/time64.c @@ -0,0 +1,781 @@ +/* + +Copyright (c) 2007-2010 Michael G Schwern + +This software originally derived from Paul Sheer's pivotal_gmtime_r.c. + +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. + +*/ + +/* + +Programmers who have available to them 64-bit time values as a 'long +long' type can use cbson_localtime64_r() and cbson_gmtime64_r() which correctly +converts the time even on 32-bit systems. Whether you have 64-bit time +values will depend on the operating system. + +cbson_localtime64_r() is a 64-bit equivalent of localtime_r(). + +cbson_gmtime64_r() is a 64-bit equivalent of gmtime_r(). + +*/ + +#ifdef _MSC_VER + #define _CRT_SECURE_NO_WARNINGS +#endif + +/* Including Python.h fixes issues with interpreters built with -std=c99. */ +#define PY_SSIZE_T_CLEAN +#include "Python.h" + +#include +#include "time64.h" +#include "time64_limits.h" + + +/* Spec says except for stftime() and the _r() functions, these + all return static memory. Stabbings! */ +static struct TM Static_Return_Date; + +static const int days_in_month[2][12] = { + {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}, + {31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}, +}; + +static const int julian_days_by_month[2][12] = { + {0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334}, + {0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335}, +}; + +static const int length_of_year[2] = { 365, 366 }; + +/* Some numbers relating to the gregorian cycle */ +static const Year years_in_gregorian_cycle = 400; +#define days_in_gregorian_cycle ((365 * 400) + 100 - 4 + 1) +static const Time64_T seconds_in_gregorian_cycle = days_in_gregorian_cycle * 60LL * 60LL * 24LL; + +/* Year range we can trust the time functions with */ +#define MAX_SAFE_YEAR 2037 +#define MIN_SAFE_YEAR 1971 + +/* 28 year Julian calendar cycle */ +#define SOLAR_CYCLE_LENGTH 28 + +/* Year cycle from MAX_SAFE_YEAR down. */ +static const int safe_years_high[SOLAR_CYCLE_LENGTH] = { + 2016, 2017, 2018, 2019, + 2020, 2021, 2022, 2023, + 2024, 2025, 2026, 2027, + 2028, 2029, 2030, 2031, + 2032, 2033, 2034, 2035, + 2036, 2037, 2010, 2011, + 2012, 2013, 2014, 2015 +}; + +/* Year cycle from MIN_SAFE_YEAR up */ +static const int safe_years_low[SOLAR_CYCLE_LENGTH] = { + 1996, 1997, 1998, 1971, + 1972, 1973, 1974, 1975, + 1976, 1977, 1978, 1979, + 1980, 1981, 1982, 1983, + 1984, 1985, 1986, 1987, + 1988, 1989, 1990, 1991, + 1992, 1993, 1994, 1995, +}; + +/* Let's assume people are going to be looking for dates in the future. + Let's provide some cheats so you can skip ahead. + This has a 4x speed boost when near 2008. +*/ +/* Number of days since epoch on Jan 1st, 2008 GMT */ +#define CHEAT_DAYS (1199145600 / 24 / 60 / 60) +#define CHEAT_YEARS 108 + +#define IS_LEAP(n) ((!(((n) + 1900) % 400) || (!(((n) + 1900) % 4) && (((n) + 1900) % 100))) != 0) +#define _TIME64_WRAP(a,b,m) ((a) = ((a) < 0 ) ? ((b)--, (a) + (m)) : (a)) + +#ifdef USE_SYSTEM_LOCALTIME +# define SHOULD_USE_SYSTEM_LOCALTIME(a) ( \ + (a) <= SYSTEM_LOCALTIME_MAX && \ + (a) >= SYSTEM_LOCALTIME_MIN \ +) +#else +# define SHOULD_USE_SYSTEM_LOCALTIME(a) (0) +#endif + +#ifdef USE_SYSTEM_GMTIME +# define SHOULD_USE_SYSTEM_GMTIME(a) ( \ + (a) <= SYSTEM_GMTIME_MAX && \ + (a) >= SYSTEM_GMTIME_MIN \ +) +#else +# define SHOULD_USE_SYSTEM_GMTIME(a) (0) +#endif + +/* Multi varadic macros are a C99 thing, alas */ +#ifdef TIME_64_DEBUG +# define TIME64_TRACE(format) (fprintf(stderr, format)) +# define TIME64_TRACE1(format, var1) (fprintf(stderr, format, var1)) +# define TIME64_TRACE2(format, var1, var2) (fprintf(stderr, format, var1, var2)) +# define TIME64_TRACE3(format, var1, var2, var3) (fprintf(stderr, format, var1, var2, var3)) +#else +# define TIME64_TRACE(format) ((void)0) +# define TIME64_TRACE1(format, var1) ((void)0) +# define TIME64_TRACE2(format, var1, var2) ((void)0) +# define TIME64_TRACE3(format, var1, var2, var3) ((void)0) +#endif + + +static int is_exception_century(Year year) +{ + int is_exception = ((year % 100 == 0) && !(year % 400 == 0)); + TIME64_TRACE1("# is_exception_century: %s\n", is_exception ? "yes" : "no"); + + return(is_exception); +} + + +/* Compare two dates. + The result is like cmp. + Ignores things like gmtoffset and dst +*/ +int cbson_cmp_date( const struct TM* left, const struct tm* right ) { + if( left->tm_year > right->tm_year ) + return 1; + else if( left->tm_year < right->tm_year ) + return -1; + + if( left->tm_mon > right->tm_mon ) + return 1; + else if( left->tm_mon < right->tm_mon ) + return -1; + + if( left->tm_mday > right->tm_mday ) + return 1; + else if( left->tm_mday < right->tm_mday ) + return -1; + + if( left->tm_hour > right->tm_hour ) + return 1; + else if( left->tm_hour < right->tm_hour ) + return -1; + + if( left->tm_min > right->tm_min ) + return 1; + else if( left->tm_min < right->tm_min ) + return -1; + + if( left->tm_sec > right->tm_sec ) + return 1; + else if( left->tm_sec < right->tm_sec ) + return -1; + + return 0; +} + + +/* Check if a date is safely inside a range. + The intention is to check if its a few days inside. +*/ +int cbson_date_in_safe_range( const struct TM* date, const struct tm* min, const struct tm* max ) { + if( cbson_cmp_date(date, min) == -1 ) + return 0; + + if( cbson_cmp_date(date, max) == 1 ) + return 0; + + return 1; +} + + +/* timegm() is not in the C or POSIX spec, but it is such a useful + extension I would be remiss in leaving it out. Also I need it + for cbson_localtime64() +*/ +Time64_T cbson_timegm64(const struct TM *date) { + Time64_T days = 0; + Time64_T seconds = 0; + Year year; + Year orig_year = (Year)date->tm_year; + int cycles = 0; + + if( orig_year > 100 ) { + cycles = (int)((orig_year - 100) / 400); + orig_year -= cycles * 400; + days += (Time64_T)cycles * days_in_gregorian_cycle; + } + else if( orig_year < -300 ) { + cycles = (int)((orig_year - 100) / 400); + orig_year -= cycles * 400; + days += (Time64_T)cycles * days_in_gregorian_cycle; + } + TIME64_TRACE3("# timegm/ cycles: %d, days: %lld, orig_year: %lld\n", cycles, days, orig_year); + + if( orig_year > 70 ) { + year = 70; + while( year < orig_year ) { + days += length_of_year[IS_LEAP(year)]; + year++; + } + } + else if ( orig_year < 70 ) { + year = 69; + do { + days -= length_of_year[IS_LEAP(year)]; + year--; + } while( year >= orig_year ); + } + + days += julian_days_by_month[IS_LEAP(orig_year)][date->tm_mon]; + days += date->tm_mday - 1; + + seconds = days * 60 * 60 * 24; + + seconds += date->tm_hour * 60 * 60; + seconds += date->tm_min * 60; + seconds += date->tm_sec; + + return(seconds); +} + + +#ifndef NDEBUG +static int check_tm(struct TM *tm) +{ + /* Don't forget leap seconds */ + assert(tm->tm_sec >= 0); + assert(tm->tm_sec <= 61); + + assert(tm->tm_min >= 0); + assert(tm->tm_min <= 59); + + assert(tm->tm_hour >= 0); + assert(tm->tm_hour <= 23); + + assert(tm->tm_mday >= 1); + assert(tm->tm_mday <= days_in_month[IS_LEAP(tm->tm_year)][tm->tm_mon]); + + assert(tm->tm_mon >= 0); + assert(tm->tm_mon <= 11); + + assert(tm->tm_wday >= 0); + assert(tm->tm_wday <= 6); + + assert(tm->tm_yday >= 0); + assert(tm->tm_yday <= length_of_year[IS_LEAP(tm->tm_year)]); + +#ifdef HAS_TM_TM_GMTOFF + assert(tm->tm_gmtoff >= -24 * 60 * 60); + assert(tm->tm_gmtoff <= 24 * 60 * 60); +#endif + + return 1; +} +#endif + + +/* The exceptional centuries without leap years cause the cycle to + shift by 16 +*/ +static Year cycle_offset(Year year) +{ + const Year start_year = 2000; + Year year_diff = year - start_year; + Year exceptions; + + if( year > start_year ) + year_diff--; + + exceptions = year_diff / 100; + exceptions -= year_diff / 400; + + TIME64_TRACE3("# year: %lld, exceptions: %lld, year_diff: %lld\n", + year, exceptions, year_diff); + + return exceptions * 16; +} + +/* For a given year after 2038, pick the latest possible matching + year in the 28 year calendar cycle. + + A matching year... + 1) Starts on the same day of the week. + 2) Has the same leap year status. + + This is so the calendars match up. + + Also the previous year must match. When doing Jan 1st you might + wind up on Dec 31st the previous year when doing a -UTC time zone. + + Finally, the next year must have the same start day of week. This + is for Dec 31st with a +UTC time zone. + It doesn't need the same leap year status since we only care about + January 1st. +*/ +static int safe_year(const Year year) +{ + int safe_year = 0; + Year year_cycle; + + if( year >= MIN_SAFE_YEAR && year <= MAX_SAFE_YEAR ) { + return (int)year; + } + + year_cycle = year + cycle_offset(year); + + /* safe_years_low is off from safe_years_high by 8 years */ + if( year < MIN_SAFE_YEAR ) + year_cycle -= 8; + + /* Change non-leap xx00 years to an equivalent */ + if( is_exception_century(year) ) + year_cycle += 11; + + /* Also xx01 years, since the previous year will be wrong */ + if( is_exception_century(year - 1) ) + year_cycle += 17; + + year_cycle %= SOLAR_CYCLE_LENGTH; + if( year_cycle < 0 ) + year_cycle = SOLAR_CYCLE_LENGTH + year_cycle; + + assert( year_cycle >= 0 ); + assert( year_cycle < SOLAR_CYCLE_LENGTH ); + if( year < MIN_SAFE_YEAR ) + safe_year = safe_years_low[year_cycle]; + else if( year > MAX_SAFE_YEAR ) + safe_year = safe_years_high[year_cycle]; + else + assert(0); + + TIME64_TRACE3("# year: %lld, year_cycle: %lld, safe_year: %d\n", + year, year_cycle, safe_year); + + assert(safe_year <= MAX_SAFE_YEAR && safe_year >= MIN_SAFE_YEAR); + + return safe_year; +} + + +void pymongo_copy_tm_to_TM64(const struct tm *src, struct TM *dest) { + if( src == NULL ) { + memset(dest, 0, sizeof(*dest)); + } + else { +# ifdef USE_TM64 + dest->tm_sec = src->tm_sec; + dest->tm_min = src->tm_min; + dest->tm_hour = src->tm_hour; + dest->tm_mday = src->tm_mday; + dest->tm_mon = src->tm_mon; + dest->tm_year = (Year)src->tm_year; + dest->tm_wday = src->tm_wday; + dest->tm_yday = src->tm_yday; + dest->tm_isdst = src->tm_isdst; + +# ifdef HAS_TM_TM_GMTOFF + dest->tm_gmtoff = src->tm_gmtoff; +# endif + +# ifdef HAS_TM_TM_ZONE + dest->tm_zone = src->tm_zone; +# endif + +# else + /* They're the same type */ + memcpy(dest, src, sizeof(*dest)); +# endif + } +} + + +void cbson_copy_TM64_to_tm(const struct TM *src, struct tm *dest) { + if( src == NULL ) { + memset(dest, 0, sizeof(*dest)); + } + else { +# ifdef USE_TM64 + dest->tm_sec = src->tm_sec; + dest->tm_min = src->tm_min; + dest->tm_hour = src->tm_hour; + dest->tm_mday = src->tm_mday; + dest->tm_mon = src->tm_mon; + dest->tm_year = (int)src->tm_year; + dest->tm_wday = src->tm_wday; + dest->tm_yday = src->tm_yday; + dest->tm_isdst = src->tm_isdst; + +# ifdef HAS_TM_TM_GMTOFF + dest->tm_gmtoff = src->tm_gmtoff; +# endif + +# ifdef HAS_TM_TM_ZONE + dest->tm_zone = src->tm_zone; +# endif + +# else + /* They're the same type */ + memcpy(dest, src, sizeof(*dest)); +# endif + } +} + + +/* Simulate localtime_r() to the best of our ability */ +struct tm * cbson_fake_localtime_r(const time_t *time, struct tm *result) { + const struct tm *static_result = localtime(time); + + assert(result != NULL); + + if( static_result == NULL ) { + memset(result, 0, sizeof(*result)); + return NULL; + } + else { + memcpy(result, static_result, sizeof(*result)); + return result; + } +} + + +/* Simulate gmtime_r() to the best of our ability */ +struct tm * cbson_fake_gmtime_r(const time_t *time, struct tm *result) { + const struct tm *static_result = gmtime(time); + + assert(result != NULL); + + if( static_result == NULL ) { + memset(result, 0, sizeof(*result)); + return NULL; + } + else { + memcpy(result, static_result, sizeof(*result)); + return result; + } +} + + +static Time64_T seconds_between_years(Year left_year, Year right_year) { + int increment = (left_year > right_year) ? 1 : -1; + Time64_T seconds = 0; + int cycles; + + if( left_year > 2400 ) { + cycles = (int)((left_year - 2400) / 400); + left_year -= cycles * 400; + seconds += cycles * seconds_in_gregorian_cycle; + } + else if( left_year < 1600 ) { + cycles = (int)((left_year - 1600) / 400); + left_year += cycles * 400; + seconds += cycles * seconds_in_gregorian_cycle; + } + + while( left_year != right_year ) { + seconds += length_of_year[IS_LEAP(right_year - 1900)] * 60 * 60 * 24; + right_year += increment; + } + + return seconds * increment; +} + + +Time64_T cbson_mktime64(const struct TM *input_date) { + struct tm safe_date; + struct TM date; + Time64_T time; + Year year = input_date->tm_year + 1900; + + if( cbson_date_in_safe_range(input_date, &SYSTEM_MKTIME_MIN, &SYSTEM_MKTIME_MAX) ) + { + cbson_copy_TM64_to_tm(input_date, &safe_date); + return (Time64_T)mktime(&safe_date); + } + + /* Have to make the year safe in date else it won't fit in safe_date */ + date = *input_date; + date.tm_year = safe_year(year) - 1900; + cbson_copy_TM64_to_tm(&date, &safe_date); + + time = (Time64_T)mktime(&safe_date); + + time += seconds_between_years(year, (Year)(safe_date.tm_year + 1900)); + + return time; +} + + +/* Because I think mktime() is a crappy name */ +Time64_T timelocal64(const struct TM *date) { + return cbson_mktime64(date); +} + + +struct TM *cbson_gmtime64_r (const Time64_T *in_time, struct TM *p) +{ + int v_tm_sec, v_tm_min, v_tm_hour, v_tm_mon, v_tm_wday; + Time64_T v_tm_tday; + int leap; + Time64_T m; + Time64_T time = *in_time; + Year year = 70; + int cycles = 0; + + assert(p != NULL); + +#ifdef USE_SYSTEM_GMTIME + /* Use the system gmtime() if time_t is small enough */ + if( SHOULD_USE_SYSTEM_GMTIME(*in_time) ) { + time_t safe_time = (time_t)*in_time; + struct tm safe_date; + GMTIME_R(&safe_time, &safe_date); + + pymongo_copy_tm_to_TM64(&safe_date, p); + assert(check_tm(p)); + + return p; + } +#endif + +#ifdef HAS_TM_TM_GMTOFF + p->tm_gmtoff = 0; +#endif + p->tm_isdst = 0; + +#ifdef HAS_TM_TM_ZONE + p->tm_zone = "UTC"; +#endif + + v_tm_sec = (int)(time % 60); + time /= 60; + v_tm_min = (int)(time % 60); + time /= 60; + v_tm_hour = (int)(time % 24); + time /= 24; + v_tm_tday = time; + + _TIME64_WRAP (v_tm_sec, v_tm_min, 60); + _TIME64_WRAP (v_tm_min, v_tm_hour, 60); + _TIME64_WRAP (v_tm_hour, v_tm_tday, 24); + + v_tm_wday = (int)((v_tm_tday + 4) % 7); + if (v_tm_wday < 0) + v_tm_wday += 7; + m = v_tm_tday; + + if (m >= CHEAT_DAYS) { + year = CHEAT_YEARS; + m -= CHEAT_DAYS; + } + + if (m >= 0) { + /* Gregorian cycles, this is huge optimization for distant times */ + cycles = (int)(m / (Time64_T) days_in_gregorian_cycle); + if( cycles ) { + m -= (cycles * (Time64_T) days_in_gregorian_cycle); + year += (cycles * years_in_gregorian_cycle); + } + + /* Years */ + leap = IS_LEAP (year); + while (m >= (Time64_T) length_of_year[leap]) { + m -= (Time64_T) length_of_year[leap]; + year++; + leap = IS_LEAP (year); + } + + /* Months */ + v_tm_mon = 0; + while (m >= (Time64_T) days_in_month[leap][v_tm_mon]) { + m -= (Time64_T) days_in_month[leap][v_tm_mon]; + v_tm_mon++; + } + } else { + year--; + + /* Gregorian cycles */ + cycles = (int)((m / (Time64_T) days_in_gregorian_cycle) + 1); + if( cycles ) { + m -= (cycles * (Time64_T) days_in_gregorian_cycle); + year += (cycles * years_in_gregorian_cycle); + } + + /* Years */ + leap = IS_LEAP (year); + while (m < (Time64_T) -length_of_year[leap]) { + m += (Time64_T) length_of_year[leap]; + year--; + leap = IS_LEAP (year); + } + + /* Months */ + v_tm_mon = 11; + while (m < (Time64_T) -days_in_month[leap][v_tm_mon]) { + m += (Time64_T) days_in_month[leap][v_tm_mon]; + v_tm_mon--; + } + m += (Time64_T) days_in_month[leap][v_tm_mon]; + } + + p->tm_year = (int)year; + if( p->tm_year != year ) { +#ifdef EOVERFLOW + errno = EOVERFLOW; +#endif + return NULL; + } + + /* At this point m is less than a year so casting to an int is safe */ + p->tm_mday = (int) m + 1; + p->tm_yday = julian_days_by_month[leap][v_tm_mon] + (int)m; + p->tm_sec = v_tm_sec; + p->tm_min = v_tm_min; + p->tm_hour = v_tm_hour; + p->tm_mon = v_tm_mon; + p->tm_wday = v_tm_wday; + + assert(check_tm(p)); + + return p; +} + + +struct TM *cbson_localtime64_r (const Time64_T *time, struct TM *local_tm) +{ + time_t safe_time; + struct tm safe_date; + struct TM gm_tm; + Year orig_year; + int month_diff; + + assert(local_tm != NULL); + +#ifdef USE_SYSTEM_LOCALTIME + /* Use the system localtime() if time_t is small enough */ + if( SHOULD_USE_SYSTEM_LOCALTIME(*time) ) { + safe_time = (time_t)*time; + + TIME64_TRACE1("Using system localtime for %lld\n", *time); + + LOCALTIME_R(&safe_time, &safe_date); + + pymongo_copy_tm_to_TM64(&safe_date, local_tm); + assert(check_tm(local_tm)); + + return local_tm; + } +#endif + + if( cbson_gmtime64_r(time, &gm_tm) == NULL ) { + TIME64_TRACE1("cbson_gmtime64_r returned null for %lld\n", *time); + return NULL; + } + + orig_year = gm_tm.tm_year; + + if (gm_tm.tm_year > (2037 - 1900) || + gm_tm.tm_year < (1970 - 1900) + ) + { + TIME64_TRACE1("Mapping tm_year %lld to safe_year\n", (Year)gm_tm.tm_year); + gm_tm.tm_year = safe_year((Year)(gm_tm.tm_year + 1900)) - 1900; + } + + safe_time = (time_t)cbson_timegm64(&gm_tm); + if( LOCALTIME_R(&safe_time, &safe_date) == NULL ) { + TIME64_TRACE1("localtime_r(%d) returned NULL\n", (int)safe_time); + return NULL; + } + + pymongo_copy_tm_to_TM64(&safe_date, local_tm); + + local_tm->tm_year = (int)orig_year; + if( local_tm->tm_year != orig_year ) { + TIME64_TRACE2("tm_year overflow: tm_year %lld, orig_year %lld\n", + (Year)local_tm->tm_year, (Year)orig_year); + +#ifdef EOVERFLOW + errno = EOVERFLOW; +#endif + return NULL; + } + + + month_diff = local_tm->tm_mon - gm_tm.tm_mon; + + /* When localtime is Dec 31st previous year and + gmtime is Jan 1st next year. + */ + if( month_diff == 11 ) { + local_tm->tm_year--; + } + + /* When localtime is Jan 1st, next year and + gmtime is Dec 31st, previous year. + */ + if( month_diff == -11 ) { + local_tm->tm_year++; + } + + /* GMT is Jan 1st, xx01 year, but localtime is still Dec 31st + in a non-leap xx00. There is one point in the cycle + we can't account for which the safe xx00 year is a leap + year. So we need to correct for Dec 31st coming out as + the 366th day of the year. + */ + if( !IS_LEAP(local_tm->tm_year) && local_tm->tm_yday == 365 ) + local_tm->tm_yday--; + + assert(check_tm(local_tm)); + + return local_tm; +} + + +int cbson_valid_tm_wday( const struct TM* date ) { + if( 0 <= date->tm_wday && date->tm_wday <= 6 ) + return 1; + else + return 0; +} + +int cbson_valid_tm_mon( const struct TM* date ) { + if( 0 <= date->tm_mon && date->tm_mon <= 11 ) + return 1; + else + return 0; +} + + +/* Non-thread safe versions of the above */ +struct TM *cbson_localtime64(const Time64_T *time) { +#ifdef _MSC_VER + _tzset(); +#else + tzset(); +#endif + return cbson_localtime64_r(time, &Static_Return_Date); +} + +struct TM *cbson_gmtime64(const Time64_T *time) { + return cbson_gmtime64_r(time, &Static_Return_Date); +} diff --git a/venv/Lib/site-packages/bson/time64.h b/venv/Lib/site-packages/bson/time64.h new file mode 100644 index 00000000..6321eb30 --- /dev/null +++ b/venv/Lib/site-packages/bson/time64.h @@ -0,0 +1,67 @@ +#ifndef TIME64_H +# define TIME64_H + +#include +#include "time64_config.h" + +/* Set our custom types */ +typedef INT_64_T Int64; +typedef Int64 Time64_T; +typedef Int64 Year; + + +/* A copy of the tm struct but with a 64 bit year */ +struct TM64 { + int tm_sec; + int tm_min; + int tm_hour; + int tm_mday; + int tm_mon; + Year tm_year; + int tm_wday; + int tm_yday; + int tm_isdst; + +#ifdef HAS_TM_TM_GMTOFF + long tm_gmtoff; +#endif + +#ifdef HAS_TM_TM_ZONE + char *tm_zone; +#endif +}; + + +/* Decide which tm struct to use */ +#ifdef USE_TM64 +#define TM TM64 +#else +#define TM tm +#endif + + +/* Declare public functions */ +struct TM *cbson_gmtime64_r (const Time64_T *, struct TM *); +struct TM *cbson_localtime64_r (const Time64_T *, struct TM *); +struct TM *cbson_gmtime64 (const Time64_T *); +struct TM *cbson_localtime64 (const Time64_T *); + +Time64_T cbson_timegm64 (const struct TM *); +Time64_T cbson_mktime64 (const struct TM *); +Time64_T timelocal64 (const struct TM *); + + +/* Not everyone has gm/localtime_r(), provide a replacement */ +#ifdef HAS_LOCALTIME_R +# define LOCALTIME_R(clock, result) localtime_r(clock, result) +#else +# define LOCALTIME_R(clock, result) cbson_fake_localtime_r(clock, result) +#endif +#ifdef HAS_GMTIME_R +# define GMTIME_R(clock, result) gmtime_r(clock, result) +#else +# define GMTIME_R(clock, result) cbson_fake_gmtime_r(clock, result) +#endif + + +#endif diff --git a/venv/Lib/site-packages/bson/time64_config.h b/venv/Lib/site-packages/bson/time64_config.h new file mode 100644 index 00000000..9d4c111c --- /dev/null +++ b/venv/Lib/site-packages/bson/time64_config.h @@ -0,0 +1,78 @@ +/* Configuration + ------------- + Define as appropriate for your system. + Sensible defaults provided. +*/ + + +#ifndef TIME64_CONFIG_H +# define TIME64_CONFIG_H + +/* Debugging + TIME_64_DEBUG + Define if you want debugging messages +*/ +/* #define TIME_64_DEBUG */ + + +/* INT_64_T + A 64 bit integer type to use to store time and others. + Must be defined. +*/ +#define INT_64_T long long + + +/* USE_TM64 + Should we use a 64 bit safe replacement for tm? This will + let you go past year 2 billion but the struct will be incompatible + with tm. Conversion functions will be provided. +*/ +/* #define USE_TM64 */ + + +/* Availability of system functions. + + HAS_GMTIME_R + Define if your system has gmtime_r() + + HAS_LOCALTIME_R + Define if your system has localtime_r() + + HAS_TIMEGM + Define if your system has timegm(), a GNU extension. +*/ +#if !defined(WIN32) && !defined(_MSC_VER) +#define HAS_GMTIME_R +#define HAS_LOCALTIME_R +#endif +/* #define HAS_TIMEGM */ + + +/* Details of non-standard tm struct elements. + + HAS_TM_TM_GMTOFF + True if your tm struct has a "tm_gmtoff" element. + A BSD extension. + + HAS_TM_TM_ZONE + True if your tm struct has a "tm_zone" element. + A BSD extension. +*/ +/* #define HAS_TM_TM_GMTOFF */ +/* #define HAS_TM_TM_ZONE */ + + +/* USE_SYSTEM_LOCALTIME + USE_SYSTEM_GMTIME + USE_SYSTEM_MKTIME + USE_SYSTEM_TIMEGM + Should we use the system functions if the time is inside their range? + Your system localtime() is probably more accurate, but our gmtime() is + fast and safe. +*/ +#define USE_SYSTEM_LOCALTIME +/* #define USE_SYSTEM_GMTIME */ +#define USE_SYSTEM_MKTIME +/* #define USE_SYSTEM_TIMEGM */ + +#endif /* TIME64_CONFIG_H */ diff --git a/venv/Lib/site-packages/bson/time64_limits.h b/venv/Lib/site-packages/bson/time64_limits.h new file mode 100644 index 00000000..1d30607b --- /dev/null +++ b/venv/Lib/site-packages/bson/time64_limits.h @@ -0,0 +1,95 @@ +/* + Maximum and minimum inputs your system's respective time functions + can correctly handle. time64.h will use your system functions if + the input falls inside these ranges and corresponding USE_SYSTEM_* + constant is defined. +*/ + +#ifndef TIME64_LIMITS_H +#define TIME64_LIMITS_H + +/* Max/min for localtime() */ +#define SYSTEM_LOCALTIME_MAX 2147483647 +#define SYSTEM_LOCALTIME_MIN -2147483647-1 + +/* Max/min for gmtime() */ +#define SYSTEM_GMTIME_MAX 2147483647 +#define SYSTEM_GMTIME_MIN -2147483647-1 + +/* Max/min for mktime() */ +static const struct tm SYSTEM_MKTIME_MAX = { + 7, + 14, + 19, + 18, + 0, + 138, + 1, + 17, + 0 +#ifdef HAS_TM_TM_GMTOFF + ,-28800 +#endif +#ifdef HAS_TM_TM_ZONE + ,"PST" +#endif +}; + +static const struct tm SYSTEM_MKTIME_MIN = { + 52, + 45, + 12, + 13, + 11, + 1, + 5, + 346, + 0 +#ifdef HAS_TM_TM_GMTOFF + ,-28800 +#endif +#ifdef HAS_TM_TM_ZONE + ,"PST" +#endif +}; + +/* Max/min for timegm() */ +#ifdef HAS_TIMEGM +static const struct tm SYSTEM_TIMEGM_MAX = { + 7, + 14, + 3, + 19, + 0, + 138, + 2, + 18, + 0 + #ifdef HAS_TM_TM_GMTOFF + ,0 + #endif + #ifdef HAS_TM_TM_ZONE + ,"UTC" + #endif +}; + +static const struct tm SYSTEM_TIMEGM_MIN = { + 52, + 45, + 20, + 13, + 11, + 1, + 5, + 346, + 0 + #ifdef HAS_TM_TM_GMTOFF + ,0 + #endif + #ifdef HAS_TM_TM_ZONE + ,"UTC" + #endif +}; +#endif /* HAS_TIMEGM */ + +#endif /* TIME64_LIMITS_H */ diff --git a/venv/Lib/site-packages/bson/timestamp.py b/venv/Lib/site-packages/bson/timestamp.py new file mode 100644 index 00000000..3e76e7ba --- /dev/null +++ b/venv/Lib/site-packages/bson/timestamp.py @@ -0,0 +1,123 @@ +# Copyright 2010-2015 MongoDB, 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. + +"""Tools for representing MongoDB internal Timestamps.""" +from __future__ import annotations + +import calendar +import datetime +from typing import Any, Union + +from bson._helpers import _getstate_slots, _setstate_slots +from bson.tz_util import utc + +UPPERBOUND = 4294967296 + + +class Timestamp: + """MongoDB internal timestamps used in the opLog.""" + + __slots__ = ("__time", "__inc") + + __getstate__ = _getstate_slots + __setstate__ = _setstate_slots + + _type_marker = 17 + + def __init__(self, time: Union[datetime.datetime, int], inc: int) -> None: + """Create a new :class:`Timestamp`. + + This class is only for use with the MongoDB opLog. If you need + to store a regular timestamp, please use a + :class:`~datetime.datetime`. + + Raises :class:`TypeError` if `time` is not an instance of + :class: `int` or :class:`~datetime.datetime`, or `inc` is not + an instance of :class:`int`. Raises :class:`ValueError` if + `time` or `inc` is not in [0, 2**32). + + :param time: time in seconds since epoch UTC, or a naive UTC + :class:`~datetime.datetime`, or an aware + :class:`~datetime.datetime` + :param inc: the incrementing counter + """ + if isinstance(time, datetime.datetime): + offset = time.utcoffset() + if offset is not None: + time = time - offset + time = int(calendar.timegm(time.timetuple())) + if not isinstance(time, int): + raise TypeError("time must be an instance of int") + if not isinstance(inc, int): + raise TypeError("inc must be an instance of int") + if not 0 <= time < UPPERBOUND: + raise ValueError("time must be contained in [0, 2**32)") + if not 0 <= inc < UPPERBOUND: + raise ValueError("inc must be contained in [0, 2**32)") + + self.__time = time + self.__inc = inc + + @property + def time(self) -> int: + """Get the time portion of this :class:`Timestamp`.""" + return self.__time + + @property + def inc(self) -> int: + """Get the inc portion of this :class:`Timestamp`.""" + return self.__inc + + def __eq__(self, other: Any) -> bool: + if isinstance(other, Timestamp): + return self.__time == other.time and self.__inc == other.inc + else: + return NotImplemented + + def __hash__(self) -> int: + return hash(self.time) ^ hash(self.inc) + + def __ne__(self, other: Any) -> bool: + return not self == other + + def __lt__(self, other: Any) -> bool: + if isinstance(other, Timestamp): + return (self.time, self.inc) < (other.time, other.inc) + return NotImplemented + + def __le__(self, other: Any) -> bool: + if isinstance(other, Timestamp): + return (self.time, self.inc) <= (other.time, other.inc) + return NotImplemented + + def __gt__(self, other: Any) -> bool: + if isinstance(other, Timestamp): + return (self.time, self.inc) > (other.time, other.inc) + return NotImplemented + + def __ge__(self, other: Any) -> bool: + if isinstance(other, Timestamp): + return (self.time, self.inc) >= (other.time, other.inc) + return NotImplemented + + def __repr__(self) -> str: + return f"Timestamp({self.__time}, {self.__inc})" + + def as_datetime(self) -> datetime.datetime: + """Return a :class:`~datetime.datetime` instance corresponding + to the time portion of this :class:`Timestamp`. + + The returned datetime's timezone is UTC. + """ + return datetime.datetime.fromtimestamp(self.__time, utc) diff --git a/venv/Lib/site-packages/bson/typings.py b/venv/Lib/site-packages/bson/typings.py new file mode 100644 index 00000000..b80c6614 --- /dev/null +++ b/venv/Lib/site-packages/bson/typings.py @@ -0,0 +1,31 @@ +# Copyright 2023-Present MongoDB, 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. + +"""Type aliases used by bson""" +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, Mapping, MutableMapping, TypeVar, Union + +if TYPE_CHECKING: + from array import array + from mmap import mmap + + from bson.raw_bson import RawBSONDocument + + +# Common Shared Types. +_DocumentOut = Union[MutableMapping[str, Any], "RawBSONDocument"] +_DocumentType = TypeVar("_DocumentType", bound=Mapping[str, Any]) +_DocumentTypeArg = TypeVar("_DocumentTypeArg", bound=Mapping[str, Any]) +_ReadableBuffer = Union[bytes, memoryview, "mmap", "array"] diff --git a/venv/Lib/site-packages/bson/tz_util.py b/venv/Lib/site-packages/bson/tz_util.py new file mode 100644 index 00000000..a21d3c17 --- /dev/null +++ b/venv/Lib/site-packages/bson/tz_util.py @@ -0,0 +1,53 @@ +# Copyright 2010-2015 MongoDB, 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. + +"""Timezone related utilities for BSON.""" +from __future__ import annotations + +from datetime import datetime, timedelta, tzinfo +from typing import Optional, Tuple, Union + +ZERO: timedelta = timedelta(0) + + +class FixedOffset(tzinfo): + """Fixed offset timezone, in minutes east from UTC. + + Implementation based from the Python `standard library documentation + `_. + Defining __getinitargs__ enables pickling / copying. + """ + + def __init__(self, offset: Union[float, timedelta], name: str) -> None: + if isinstance(offset, timedelta): + self.__offset = offset + else: + self.__offset = timedelta(minutes=offset) + self.__name = name + + def __getinitargs__(self) -> Tuple[timedelta, str]: + return self.__offset, self.__name + + def utcoffset(self, dt: Optional[datetime]) -> timedelta: + return self.__offset + + def tzname(self, dt: Optional[datetime]) -> str: + return self.__name + + def dst(self, dt: Optional[datetime]) -> timedelta: + return ZERO + + +utc: FixedOffset = FixedOffset(0, "UTC") +"""Fixed offset timezone representing UTC.""" diff --git a/venv/Lib/site-packages/dns/__init__.py b/venv/Lib/site-packages/dns/__init__.py new file mode 100644 index 00000000..a4249b9e --- /dev/null +++ b/venv/Lib/site-packages/dns/__init__.py @@ -0,0 +1,70 @@ +# 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", + "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/venv/Lib/site-packages/dns/__pycache__/__init__.cpython-312.pyc b/venv/Lib/site-packages/dns/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b3a495d49189ce64de22b192cf6caf32a38d320f GIT binary patch literal 760 zcmYLHy^a$x5Z+BT|D;4TS6o38q+CEkhd_u1N^}&6CW>S^>q%n8Ywvlyx!W}G7W8yH z3vVzj6&-LA#Z`J+Vd<;>TJ^4-&YpPI&7RheRJ{tG&pCrI!tDCPhxHJrOdZr6%8BhvI1Yz zxZxhqnH_lQas`B(h6gFl8fkRkp)LU_gD2-t8QDfXu)({srU6{%PzLQRbyh}l&OjGM z739(YW!geuEO(s8cuu_n5apU(Ru(}0h)U-iWSs))x-yTlbLcR;taMHtf3SHagQQ_2 zaw8F`-*DtR1`UgSvXKV`&l52_&NlMEBO)}Hc098{T?DVCp>~rW9KVm=bJi$$X;J3Q zOj$s^W)tHUYOn2@+94R~d3AV{ZdGzyK)qN$zQW*0Kv8z3Jy834VC z(guFSB~McSh{eUELp>ps%Y}$U8p9+m;p|wo38Rq^M_q`o$xW_`oCKqO1_i zBDgDy8(j)4q!G&sc^8QP%+q`$!_tNSh8Sn2cY)LB+kxFJ-stT|f{LUVaTAzA0ttBAjZmx6>Ejd-|w$gUeWI|JG2aVaP44ey#8?S#>kfqB73I&lMId%U-|{cdMxTXrJ8(RaNY?o}?g z{hs`aX3Di8D%5_P-Cw(O*ZhZlc1&N)GeDDu zzJlT$bqES}JsrvXUSR@8B1%LK4WTy1!locwVsXWBG*-^&^sFEWyGpa<0uhp=vGhr! z`L{;%W4kH9_e>($rlbM$fDQGzhwRW)hsHL)8I2Oh{L50!mI-3456qj-gg9 zh+X@1j2-S2OnpOgEW)+tcDdukbg^#e4+8XSj`RM&4fKeB-3cHpPJ_Gvau!4h2E{8K z320jp-{!)iS$8P)}my6TP5Q#w+!J(@UasnW(@B~~U#7!(SM6etiP5GX7a zC`u|&lvSXxhh@4?=}JgG70vj?)4-cov*iV$2}AM=#pa5Qag;^?)C%Tg&Fy82@#58R#v^rqdw;X8W^2QgvjukwDTC&qBQ%@g$p0s z7ZT-`E5>t)CN9!;a|MwEX zpQ7P$5#`YU?~AiSq&7rc^*kscV85y3U&%XHPW#HlM<)~5rw-quJtXWsb?m#<#G{MW ztVb7Vk~;Qv>ZHeB&k9C-w0ru8Cj0DHwCcmcLGibiMG%Y7H``{D%+~4hUPb7@z&hmNVLu1W6D4jRjt%_K~0m{nH zqo>YO*uI3%4Uo@t{w792u!%%xpFJYyS1!K}zn_w2M1WNt0Wi~}|4D=4=EQTCGyN@TMENSB2ZvAjG9zx7!JZ$IKCG`HVzCg?s|}>og@XM%B+6qKbs0 z7iJHeywP8yl@igGV*bk7UGtCCT?-&dT!JSdqOD}v3&YM zmL#PTQ8+rC6Hl|hL31;H3Lw(@|I-1^4!LMc6$CQ#$YpO`l5A zZk2Op7Z*FdNWVF1>US|e_tF4r0fJXTo*}Z&9+0uU_>mkl`cdWvBU{%-hN&|~mU`kC zYqLf+eP)KmmKoQsM1rSgEmcQd)#KieHDwQA@;e25Z|Va z1dX(t9zE4GhRH-Cf zE6F~qUfAn#FLENUKRP=j%VuWiO%i9SsdmfE!}c~NV2dsQ2_} zg*DcZbm_b&jVI~V;AJY;rosp4eSbb`Yku->K~xwR#dfMv-yd`Y_eU2~DL!x3&mdM( z`81aGi<6oNqYWSXNOLmBGM}T2U4`rj;B~{%zY2!ryxHE`31-?L2{Ae9q>SYNdFQq zx{j@9A1tuOB30w}m(Sc^Tn0U}HeRYy#&yoHPET{MObo_0?N&+GY)$%5*s0wG78 zlM~>H{i?1IUlX_^)GOa2@}OuK#^Z$rqyCT)dHm8zWAz~;a&*Zy*8lw85u=}@6^r^W aA2IrQyjU>SA2K43Pplg2j~Ef9wEqFnRlkD( literal 0 HcmV?d00001 diff --git a/venv/Lib/site-packages/dns/__pycache__/_asyncio_backend.cpython-312.pyc b/venv/Lib/site-packages/dns/__pycache__/_asyncio_backend.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5079bc0e4529a146782f9bef2e62e8ea2a9c40bb GIT binary patch literal 14165 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 Vu(#*z?FD;Z&ffQX0$G$${|$EUjyV7T literal 0 HcmV?d00001 diff --git a/venv/Lib/site-packages/dns/__pycache__/_ddr.cpython-312.pyc b/venv/Lib/site-packages/dns/__pycache__/_ddr.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..27f2430bb0b405eeb885ad6020ea78caea096a06 GIT binary patch literal 7877 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 literal 0 HcmV?d00001 diff --git a/venv/Lib/site-packages/dns/__pycache__/_features.cpython-312.pyc b/venv/Lib/site-packages/dns/__pycache__/_features.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..646cb4328988db6012fadafb8a4946109f0c4161 GIT binary patch literal 3326 zcmai0O-vlg6|Vjr=8qW$Fkpje8x(sG9{>c& z!@y`pnvE=FELjSBiCJk6R!&|yQm(5g7jlY}3k0p0?M2E;IpsDn99Eq2s+k^G;wY`| zdR6bedR6`2_g>ZCqR|k7$B-Xqe-T9JpKRbifmUb#uh4mkK0-RuITOut9_43wj|#H_ zP~H^t(yWx1XXSigHozfWz#Y1X16fg*o{6(TUG~Ny7zbb+HqdMYOSl7vvitzp$5Fjw z0;nEr>xDqC(;J6zNEdZ3E9(*9#q{VRw}|x4-*b9wk<;Vwb-|Z_ulsX(HtyMl^_bnZ z^&Vz@jZ60a3n-dO3a|k1SERAEzt%vzTfN!4% z?n_ibODCl*C7pZI;#OggRldS~fPQrsp{k(sOKkS3he|$UN%nikR2QD1itvU%;KN9dE9=IhK}nKxxmChE zsT`K?{&?;q0)d$;6bUxxs9my1&M0I^UNtidMPgW(kT&~@mQ(41O^+E&&%EL*USYBo zY^}^aG8X2DLGhJ>sx7OFn9S)Gxthsh6=IJ`s<3w0F=49t1zk;-FJ>r%NG9vp`3%LY zG&Nz`nrf0XbdGd7VY`I+w}xI{_`}K{t6x;N#wKcLA6O{a;Yy@n8i2SZ+pydqEbLI* zHi;Xg>M}0n3;>|477EzXUBR%Z8@#uw;R0oNl)S~0lEXmahS>Uvc4{Qb}@*8MssL|E`a_G~c4YJcUQhV5lCu$EGy~A%& zKuXkujlO}GrB6$Hk>1_N@HV{4#FxfSDmj&eL+(K)*(s2UN+I$JD2#)OTjy7 zX>*c;)(t!O#da0%To>ZxpglU|F}uALmyYKOhRtVdcahE7_u% zHB1xhKM93cNM}yT^F>0H1+3UvZ)(i{s^5xn(@mhbXy|AfXlYvgRs+A z%4j*PEjyP$-UlBsp{b!}0`-sV_FdlYyZpLmr!TcF_SAk>f70Xysdq1NZZ|QyofzFs zjBh8#Uq9JN+^Po-WYl%xRo_lLS$nV-S6aPBAin)bP;wz{P3$DU_ z0!P9$qHUnP>rgl`gRk(lkj@>K(F!^`$aKCcRCqXCVEX{KHb{>Pzr($V>0A|HWpN@L zBd2Xt1Sq~j`7MBKto4&)in^j2rn+F_HoOeLASRS3Mpl73s@PsdQcWLBM6oc|v918r z800)mwd^UXgO5!rs-**#HAq|qTXu0VmpX#xFTKN-DD(3(P7%+~v!xjBeJikq_P)kF z`^?h({Cz0DEp>w`pk>)qfnC6KAG*NxCgcqQ=fq;l8vxox=Q4!O6&5jdqnQ&4Mm!`! zrA;!qw4$0t>_$#1xV*jWCBW&rD&|xG{xz6)6+S|ssiA`&6z%@)!{-mzOFNO_+Py{~ zvK8nDIMyS3-O6tF=yvz$t0z0%S89(Mf$puqxo^)7zmV&t=P?G^V6^srqi^Ib5}@>k z_hLgE%U{LD8eM1V??cUO27uk{Kz#$x?$>|x$>T;iT6^FyJi7sGu_)?VyS4$Bfzzvy zXP^TW@*S{B^5r-8ZAZgi00@Pnkg!^Ej1X$G@-o)4Kh_H2Ob*-+PS4po3q--OR}2OU z6+)+!Y-d?@>>>n>fkd^|6ip@gC^DrS29UI?PU=MD6gb(lS*%fG1&_19dJqFJwj()} z&KqE%5lwED^PpUQ46`X z^yCk(Lt?~olomAG!Rg8CscWfgB*a^+vN~kY;14R&2p%eH<-0ssq0A@?m8ABSaSv4&aviWS7g&%kp99>^$X;A#=9}VH7IZEMH8o;pMp_#EhB$|1_~(0LEYufeuLuQpy)r) z=r$T{%3*GBLu(=+ukb!2&A|j0Z*)P$1^TTN+?Rkh$I!Wyyz24DYmmUGc?QKVZgpOC!xN3j&{p_-Bidj8c_TVd mpK3(<>*;^Tf|vMOv`*LWZuD$upAD=}ZO29qP>|!jfBO%ob2qdA literal 0 HcmV?d00001 diff --git a/venv/Lib/site-packages/dns/__pycache__/_immutable_ctx.cpython-312.pyc b/venv/Lib/site-packages/dns/__pycache__/_immutable_ctx.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0385611eb2eff789539392cbcad647b45f2cb063 GIT binary patch literal 3273 zcmb_eTWl0n7(Qoa_QuX`w>Or{LKi7**Ip1s4GmHhgcL}9z)M^j$LY?LE^K$#b7o6v zme^<$w+SjuBw&1?Bt}c{fmh#r_oZt~T1Or*^5R=@8%3V{|CycXw$Z53lgxi^|2gwt zzyEw23T-0L(%x5wKMN4@6Hc5aRR@(p7+fP7(P)Mw?Vcv7Ky-4>;c?DDuL+v?m2_LM zmy)7DjuXu@MKrIwCTYGYlJsbPpx%o#5%?LI2h3(AJvv%2)S-;7q_YZqX+u$tC8+7s z^kKD-F;e!eHm&}GRGxzQ2Oy$Ol2ju}L8D1g6OvMi3{y><@+UpkNP>~KM8;{-cbX(T z<|d`z`Q1dbJB*K}$J5z(Dx-3Ki=@PwKccocuZ#e>O0N;a-8@&zS6#hb?Te&9SBxv+YKl7xMWWGvcaY zu=G&D(BmL%`fPSo&l;duu*Q0(cvd&ez^fPY`pb;vnCXGTIs+bs>jqw+C<)1wxSkm{ zrK~!to1QT>Q_#(TqF6=%MpQf=k54{+_ek&HYg}i1FkgTZgGTN`mXDgnMiK&op3?M8 z)l(v?!;J`%1|S>-1<$ZXTtN$Lu#S5)hTNS%{;75D%6nK7@F)DHUr{hJ6a{id8O>=0 zNKnO;73H0RnyK#iK+IeUmLYCQJl(&^lBaEj0I>wK*i+S1f&(?4I|dU(1S{|9l>ZVnT)uO) zzP64LGEE$B6sh9w7b)O`Fp#P;Ssi9^3w)I?fc$-M6ah820;hBRB_Tirg1MfCO#hsI zL}@LGxEKLPstwSfDmn(r?tj63fsBi9k_!}6V5n0Y3 zgdJTg0KkJcY-o;~r-_1hTg^Z-SZx{t$teqL+q%1~67KNXea&5NNKKPV6eAi7vXvLY zlwT8TCBKb05A6~K2@!xcG{_BCZs0&H-q3)<=PUsS!qkQa9DZlX$}edEfdpcaLp1_e zS(j~9fk@>oICGV5Eb_Wt;z1Fo)+RmhF7duW?m*#p%@wZl!2`hM^H~o8|7kDG%|IZUA4HmGeC0@dwsk(TZ?0`2vd>0K z#Qy)L1ltQ&RMlX39oq+FJxo~Z6Rm$O@S3NO;$cVVrmZu_%S|0~t@BL>?zAm59k6Rp zfJN~?TaX}*vh7GPff-uH^o^(-OQS6gmJN^(3!^E5<183BY+Di@H|R=yAN0bw96hVb`iP@{vfFZPjPt znnb`VMJANMQI>D?3&m3Rc+gIM(x6pF9bYxME zE!Q=aq?IR$9GN`%>mE7K`$_M0zDi)QvXg9$-H<<(OVZ`ga;#-3)>V#mm83=g*5z$G zZuETG0~>YA!Me$lwnuzsFqO+1`nWNsGS1pSHgnSvd*yYN*-sdR3EK*&gq1(Gx$v3t z*niM|LmXo03CtY+9FP@}Qd$WU+V}$r-w(Hz&M$>q%i-4fJqN$h7sIC}gTHwjY1=~r R-gKLDetzGMIZ literal 0 HcmV?d00001 diff --git a/venv/Lib/site-packages/dns/__pycache__/_trio_backend.cpython-312.pyc b/venv/Lib/site-packages/dns/__pycache__/_trio_backend.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..cae5114030bc19e96e38149fde881d87e70086cd GIT binary patch literal 13216 zcmd^FYj7Lab-s(m`$-T4NDv?e@C{0&M2ULXl4Vn(D9es%*OU{7k&S_Xr9=uO=-s6r z4A{!FrXf4FMR$@&O4N#O-H9~LL(NQP+-cH@lE{;p`~d{J3$OXZGxkg$f653d~)oG5FsDpiy92YMB!UHLN1X6 z86^oOp-VEO41MZGb@a&=pZZY)eK!`LrsC5)YTyhLwQ9Ge=Y+SER#OS~O-4xzW*y;d z3BCG0&m;^N$fzCqYE)~No)f`RT1}0*bmTBe7%z~730j5J{L*uxrIc1vZ?Z~i)Jue0oQ{oM&Q7=__h#v>x)esc)s{guawk zQxn_OROva|2;Z?K1Czng;AGQi(`55#GehXS?J#c#SFg=y6U}u(u8Z?dJW}dIO=&g5 zQ@#z>Yf!CQdX9!^zctXWdxKU(rP^vr`&R2YU8HulP2|T@p_p)PDjqtr^~q3j{5T)u z&xKx?;UE@frl(W9_+Qv~Sk`0RSwC$tl@dc^shO!nD8+}wR4SB8CgMf%qn|=jz+WK= z(It{5FM36-d;*R|Wk?fsD@TZ^ZNh}HvK}E5mizPGEM|?68_aOTz~iT7$7Jd>7fnq? zV`C!6%WPswkR4Gv+2gVJDQ+qu+lFV7$%CmWkvl8iU}TdxKFOtKL;;5y`t&akK6vaI zf#Zc^(=!5>JSL{jObI8)r=!ATEEzpMBaBaRf}myh#81Y=<0-L}f%TF-YQ-KbJIzg< zKK6z22tC(IvG26oQoDm#XDeB9{35p5S=G^ zlQUy#Tyc3MS2*hmFL&l#-5GQDN3L4Q70S9ol50!WwI%23&6s-&(1OGZRis$#D;_(Vk2cFCS(kw=7b4i%K|yej9N!X#4I}p zV`A(CADbLW!Rm@}^$Yk#jUJFDzrkE0BIeSts7sSqfMuqcYKKpy3I@v4B#QmeaE|cE zW+Qsp0Avc=YmP?8r^dx-R5p&qCdZTKWTQaG5n*LL?A{oUOjEW*qw!=+5Ta25`wE55 z^LBXhxURGH>u1obb{U33App@Q=gErMv1rPg8!r2n;8(~)^f+N)jZ0@Bt+DH!P?9Fq&cuW&NT9tMU=?(47FYHq5G`MYS$vbd#QwFx zK;pUtxWp81f&$sXosDzT;&^IGHXT*YS47YIutD8 zT?FDtPvPLf=-g`llZ`-4`bpCnaZV*4dh`3#_ul z)m9)nmvxosEY^Dherw)7(9OIW+RuKU*=PI#yPy4`6*7Lz>}UVYFkmv>VmskAt`RA; z(1_sQ;pILe(5Z|0d5J)6_y0fwx=Iq@{V=t4`)}sC_~|h|HAyLe$CJY&%(%f8_gxsD z<&>>f4qONvi_dyiJX6jtr3CMtaU4frPTfaGoJu879gJqgbd)E{2>Vc%Brlrc=(5A)ZVL+zp1Vnj(~xBhdk^ zXX`4C1g+c$B?2BUx=MMo{i1ErCYk+Nv;SRl;2u3%^TFl~X0RU`{;4wzgFnTgAD@vDnpm0TTJS4Ym(l`(hSDbMXO$H@O)yed1Fd4PMZYbxwG1?5#I?rPA9oZ z4j@b75YMOh8#+oI%4x#BifmI>7eCJN;zsL?We-9d0cD_auAJeFIb2*?lZA=cP_p8j zL9$w-(aBU|CW&c#H2T6!EUA=G@yR2j=0g}Eee$@$JT45602-i|B6;InG8v5mQP)E* zC5T59f~di&7=^F^yZGb+S<~qZO?S-(gY~YHHSD}=WemZ)M#d1K(QhHuLM1P47>71m zz_q|o*aJ}s-XtslZkRs~+?+Ej2NP1j{}Bw9pa4%XHPWGq^JG0r`dRn7mBp+hP)VpN`1fdn zzc1_WyV;iW@0L8fQSk3U!M`W(*-Zui9u)k0HWvKKerbmlzJ~*Q34Vp65dEnVQ3Gc> z1)rmg2Rv>lm-BR?j4>*^?-P+!%-6145L$5<1|~XyUCc7mp?}@jwSkm;`wvWz@7+K-Dd&XH2ZqnDu5Zvc@}!FJ*=m1jNez@? zQr0Qghpe&ZcDn&3=g(D5?*18Og`~{4ra0nu0#ci&Od;SK8dx=| zM$T0cRM@T^@uCXOPzAhP1qTYR)~BVSLwHsD1Opgq)HKX9z%aH{D)29FRuuRW)f*M6 zcR(YB^zDgM^ki%*A)Jhz;wS*)(cH;X{h$mc(xTb>)*Defg2U&ajqp4~<#fN|-n4AW zxx0YJI{Nay#!IJOJ0GVM26iytY98oh-(m*ZjsK#D)K9t~?^Zhl$y=R&Ton= z48CIz=9}6V245b|ckIU3)_fqmF!;(B6eNuwQUji8%7I3KzbG8&1er5dGx{BaWt}t4 znMG}Rt8$4doTtRG_}a9wnp7;+08`&Lf&XaSL>(w0F9tVgTeY@oZG!%$MC36vq|Ivn z9B4NaX#cDTbC$H8-}DFO=&oo4uW8M91wD{I)&mCmmxUTAVf8U+ytxR!=B#P!1X4#u zPzip{m^KcQ=Wf8x*ev8v7`07X4|`gF-UTCXsQ47TCUmEE!=Z5Dk*QWbrkit2fgyL! zk+!9E@a7cJenvxrg*7iy%$%!iCtTW>SrJti8q|4BY*KSd&$L|(sc+SMc?n(Gk#>TS zx=7#JS{CKpT#bkflZJH71hU$SppGaGX6F&2Cn8!uXsG6oqjK&J$hzKEB6g`Y+X&C1 z(fZ|IogpQO7ts$#bc*I|PL7Xp=pNy5RZbM!Jrb$spMyd*})l_yU+)$mCQ~7cP<#KOSB?24=Q;@QQslB*@_YFX}o$JLdu@k=$KY)xpn?%kS> zyxS|eyRz=CjJvE}6&4%{o>l&7QH#@=jAKnMX|8VWcL>H)CGQk*|+N5Al zHrSI3Ze6nE18sMS%h3cSI;Xp7#UH%ZaHT==Z_E0(-K@{~A6Pn&uW!rLZ@W1t^$%wI z2Y<=j=|7Th*?O~1>Kn-R4P?Rxt{6WEw_i5qTf46hE^l3Wd?j+|r}1Atn2UTdQ`fQ5 z+?9#!%{A{^I=m9@`bWPM-jxmS$~QD#9g>2**ujlhIHSq(yiduwLW5WY(ochh@y z!BrNXzZZ_c-pn6lhc+`mH!wpD`kx#9m<}DV4Fz?#H#0+x#@kyOApN%93k7dCGMElp zhPn)Ix9c#clfiTsb`jSQ0`!VUl#0hcVCdwxLPgC*G#wL9QgY{~AbY(bO!lZmH8VAS zws`VpTQ{P-vPxI6FO*o60%6@e9e_*k@jD=EomPRegbw;KNEEHJnxUYY)2<|aHJ-VV z`VXdC>>fOrNsgNao<`9Q*qC5e2|>X1Qhi`^ke>BcPK>sD9f|}LG;}H}wZ2Q;uXSJk zQm(dLvbW!{w|`ndv^(!_WxmaX^;eCpMIVE?DttZzb@^G0@RK}7+MHMbZF|yBn?z^wJ9r8bWrge>Fc{gw7=> zPIpc=01{6n<(eo6M^qNndj}DV;!dctV6r~OpAcl@sWbRg?0NMt5CL(oDhG~khECse?16!oP&TL>OaIht_ipDP|GWMS3 zu{-vjmF@@fH4T@$Gab9%@&WVP>|7YU_}JoO@7g;lB9E9V>Dmilk}U&6(PxH7o;q|; zQ4^~Y_KQ%H$LJ0C31NtUuvdXe`t!{#d9Oe3@k^f8tf%#3huh-3OCVCTL2R>*s&Rub zR5~9RxHG|8Na-(Og8LPq(9!!927153Nbgse;DU~sPe30Uq0$~9LZ(qbdbvc-F-Q)W z!9RgfCq!jVHd&AUnOS=gq4eV0Pp}G}I`9yRAg#Rwzrq&ix5)C;vg-E~ z@cR{kQZ=hyH=45mptV{6*z>JvOSO|zIxhf>85QKJAW~zcnwcf7D+B0NZZDhy5XnE6 zVCHOPXU(YgM~yv}2~CwL4Zds~hR+y>UbW~ZbaS@KTNi1Y5E3R8Nhz1r~{xZpa+cxP!?*zW?}g(6^|kFL{%j!+6JJlbpp?PMNoMLsMMDG(2=vxedd}F1R=b2979#c6tasGgHM2bzCB(K=sh9K|D9jg(4p12vbg_ zKRymO)6CIiQk(%-FFyw3p?7O!QxWY{x>8PJ4OGEoE7dm!4?jKd1a*jl^O9;KiozjV zMn=9+)IAPin+Gx422sSJaD4$gG2`HHT(V)BPr)@F5v$=|D{6;L5Rzc=qL=3Qi`aq@ zin>g3fLucHTH^Uq1p|F5!hwVAS&zz-%9N&`LV@rq(3gVBe8l0t`1s=ElD#==Z@$bg z@45a=&c0)T0r0=iTyaqI0+B8w1xOS0Zmbdi^VJwdC3!$~Eqn>h@>q_Jc;S zbmU_LRQa_L0P;#>)3u%}JyPS=Y~$8^2$h4tHc$=%KDZg=^{v{8z3%I$rTVUHeb*gZ z*NP{+9L{;#GWNE-!+X`I+(5eH*u3KPOWuyGw`2K*oHw#Cl&`H{IQX8c5iF=n?5fV_ zZd_>!Uwi(_^ZAD6W!qcPHCET`TN+xkle*BdZh7eX!JKDX#=dRE8P40o%iQ%xuRoY+ z+m*HN0x?*;rnLG1aqsv9M3(nqu&=@lV2JMa->o@GjsGs(W2(*D9TyKT9)9^q-tJyC zK*sOZf>a66xf}D1A)M&9w@P)9Y+WQ@=Z6~Z#@`DF1?Ss_>X`3@4!OuJ6Epa*{+787 z(_1zjV%eYBm_v;5XLci|Yr7%;XZr^1Q2!T}hC@B%7vW}1Kg>YEZH9qLw^_p>yXm&s zggLIjp>FneHw$%c_XLOB?AsP*$ZmYwYQnS|^Z(BvIiH59sz}^C4m8f6jwSDh;trL1 zDFpu?q}vh%2Y=ke;>6{Kk}r<`E2yzfjw2IAu~|SlQGOkAq%ri{5sv0IjlYy`hA!LRTJM3p)YTsJH-d@3}VN>Ti| zFCD7TsJdT71G+GjVn}ar<4mvIIK&nuzjD!m50;uUv2ihW92h8K?c!z6kYcDXV;S0d zW^2@yQ*(ZUm7ju0(Wc676t?F5Eteif=W;7Lms|6m7V2DXMdxzsdgpSr`-3P3;r?I^ z^Qy6p{W{ZP{07^`UbaHUKQL|V8-^a!jZSz0qc^V5Fa`jC6)uifpomBQsivV&oBn?bufnJ zf)TFUD8LrGh_r!4KAK3yqfy0ghqNQx@V0WVBF7X(WZl(j#rEbv1tmJh#l#uVP-OaA zx(;f_Qfi&ElpqOT8TyLMYfh%@=oBVEI1u{b6&AN zA{Ft}yogG#Tub{~ub#e$1rEG~UQ$`<)~(;pPtE(3dsqAUA3+N|kAfFsFxfB+$S7v> zhs5zAvAs`fKO_zBlREnE`oLuU+T4qClF6Sn`7d|8V+y}#^=5p1IqUX}eml6en603& z+rgK;xJR;W&e}GgH?LU(Ov}=UBB&(q&a8Lm8iA~}eeF!g65Ok7%X-=rXZ9L_tktI& zqBni*p%)*LSZ|i~E^+U$!T0r^_w_CB>uWyP)G2M+o87cGqYvhN&62Mx>+6zyJy~Cm zYrZIZ#Q&g8n>?zc&?pfY7NfIUdfNwI(|doWwo35iqcc^_Gi8Q e=ZDvf`x)y(Tyk#8IyXtq&aAWZHw3aM;r=hhyg~8+ literal 0 HcmV?d00001 diff --git a/venv/Lib/site-packages/dns/__pycache__/asyncbackend.cpython-312.pyc b/venv/Lib/site-packages/dns/__pycache__/asyncbackend.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e3f7387c0768138eb47fd426a558cc3548029b62 GIT binary patch literal 3456 zcmai0-)kGm9iP!}S(2qlw&Psl%fvR0mDoO$h7`haoO`)*48?)C>2boaUTa78I?}E> zyNV-Y7dPa=c{-OqBycUH^x-J^AIjye&=v`i|rKq<}xNpn%4~L9Tm;?+?}Y_1qO@k z@H#CFkjG>%6ccHm?u5`@CnC6A6GqWL6Ido8`0xS`zJ=~7v7vX{ig&I4KaT61VJ;G!0QpbKmS)&nb%qr2SsO8hkKg-6<;#x;-=IwB`5<=~~AX#Yn} zw~b!QriItZXXG~l-{wbPbH~4RfrdurMa~B9)mXIrld(_CZYuZrJ6zqz~FcHwUE@)zH|W)B+S= zgT++AnfgH0%j#m8Pq+&2s=CE31|d3Asg`*K)p2J+ zs&qoAUIf7LStsi0Ub|2;^p059@wTLVOvHfQ2`&Nj&Or1{C@8~6o0B^BD=?g#9?9m3 zZ*yjpla$=|IFt{HGbkhKouCs0VHUxrOoLtquqPyzOC-PXkzQZqo2Quv9GedyV;?g?9guZ>Q44*Gdunyx))DvpZjzC ztyi%jxjeFy9ofxJ>|`gJ*~ur-*WH&I!|#95^F^dFINj{N-H6@Z>l=7Hv!(sw&R!zB zIV+J?1OwU)L5M~l976Y$#E$?=L~OhO*&b1iV#eM8j&-sU_353_hsYcmLJqu(6K$A? zBqeqmURtmd@T=ru@Ds91he$^!@J~V|K)vA&3VP{E>V!!6_4QKn38{x*f49$bN?-W2 zK3^|D>mO6L%_~)am&r74b5Vh0D6ya$E^-$a}zR#ziu;#^izmWz(!W$-6bf)+<~}$_KqOr-BnrZ$j8J@r?j>`($>E*k@L!T6U*;}+x1Tf{IG`#Lf3Bi6-47DwN{1* zai1e&OgD5}SIhbuT!9SM5;J%a-UIOHn1FXiM)9`zInBvgaA_xq@+t&^&@JmS6Xx)C zPcq`6+=b8Thi;SnEqZo8eX$X{_&V|SuWO&JHWQZ{q04T@I{FYmM`gwuh6x-X`iW^NxZX`JJo7cm{p;xHV19G!78dz2SrdoCm5S49m^u17>&~n%LYq0 z=r@IndV|^Avhm8`{Z&(K-83vL^RS~-Gm7}nV2ioxrWnrx^a)Y}+@F-fA6CnHQHSF1 zrU-x=)++(eB{)7==qx;Ex3Gyku3=Yz&qDk{LF*y?L(i`_&yF@iJ^P9CjnH{{clll? zV%Mu+puk&mQdo2O!y>PuX#z{4SFUB+@$8fJSzsg!e4+^~IMN!g$ir*KNh^xXajwsb zA{a0^@wRLyd24p=!?q^*_>^8@vJe=3t|P}|^)U9Xo3h^%KL8f=+WHhaKs2S_MhNZy zn)LiT5~hIz0$nRhdionA^A#K1jBj1v4sL(Eo!m(bG|6BqLef2(i8m2SssI20 literal 0 HcmV?d00001 diff --git a/venv/Lib/site-packages/dns/__pycache__/asyncquery.cpython-312.pyc b/venv/Lib/site-packages/dns/__pycache__/asyncquery.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4479d5807dd3e1b28c3a0cad62ba9b039d359ecd GIT binary patch literal 31714 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 S5LaDnTmt?Nnjnuj3H%=w^o-R2 literal 0 HcmV?d00001 diff --git a/venv/Lib/site-packages/dns/__pycache__/dnssec.cpython-312.pyc b/venv/Lib/site-packages/dns/__pycache__/dnssec.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b7481c8f13c47c227bbbe4e7bacbcc8e25deae50 GIT binary patch literal 50935 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 OHIsJ5d)dy&mH!vvX-5VC literal 0 HcmV?d00001 diff --git a/venv/Lib/site-packages/dns/__pycache__/dnssectypes.cpython-312.pyc b/venv/Lib/site-packages/dns/__pycache__/dnssectypes.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3214b9a74be896e2088e443d1277080c7eb74a6d GIT binary patch literal 2014 zcmbtUOLN;)6u$CH^848bOlSmj>V(1o!7_=N;FLIJq4A7Rd`0Xa+dc2IY&Rn;}HT^Gqm}c zEEDnzE+$uSOh!KdIVP0ogi6$BOS;cT)(G_<5gM?3d!b3f`EyS>=M(n8?=d|nvV)Kv zdV%ba$d(~nd4X)X@5_e|XNsLxtJ7AsO0`-l797hqyB1Zu{e6qg4e^+j`sCR7^oQ5w->G^)pGOi$3bo}>vq zMU#4(rt}O=>sc#DGe_jX_yzivem0-u;Z=LL<21XC)=&b^5Z^yU`Gx@mEX)v9A418< z5Vam+8HX{1I6?v;iI7704dHi$KLF~u>9ZGZJ;JSzNnbkgRfc~8KOrX)_t$O4FkF55 zRq;;k9Uu5~+KwppXOZ?KkW8#_JLY+H;?&4qfy?Cx~B=LU?k3zNVC`tDop z`?ZbcPK`CYR$o7AFXKrFE;Q3J%mz*Xu zBOs)bzX~}<+iY2e!6Syz>d>BzJZ2bQ^h|r~32jwZKhl=DPh01Ht-8wnrDBoGV6CpN z&hr?UD$pZ~VwD=3&n;okvao!ckLmLK0+(Trwaw}_PnL?X!Ci1eJ_d~qiz`cD0EAF& zVR?D}O&%<1OG_(UDOa@eR;jqnBX_sTpR8_|ph>5Ev#jxG-8LC(S=~m5I74tafYV^=iBAeyk}#nWia)dm7Ym8a~o^57O1m**yU{#hO? zi2wR&IYRwFKFGsj2-Z#3ILryEHh`<<)PrmvXlT!I5WtF!z!ot+g7xPx%1 zjPIfs;XSw+jtt6pL&$iUWEh@T#O_>yU(=k`?zOld7!L?4m)qSE*u3czl@jlHem0*f zZ+ritcO3i@>?`&$z+=B8Nl$JP>Ge^BNU5{X6)F3j|5*86d6ZHgrqnY6hO=8yDf>11 c_TZa?N6FcT$=NdkhO-&@`Pg3x7(^TX1t2`X!2kdN literal 0 HcmV?d00001 diff --git a/venv/Lib/site-packages/dns/__pycache__/e164.cpython-312.pyc b/venv/Lib/site-packages/dns/__pycache__/e164.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..701bc830cb616091407fa92f1928b58b4cb6e677 GIT binary patch literal 4947 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 I8g|CvFhlhW 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

%(;a!br@!U8G7iDVb#L1*sugb8b$uxMG3?YRQ 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 Os<9qwV!ql!LH%Dyl9%lO literal 0 HcmV?d00001 diff --git a/venv/Lib/site-packages/dns/__pycache__/enum.cpython-312.pyc b/venv/Lib/site-packages/dns/__pycache__/enum.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..108bda1b429d56ca16035e54d32e2b2e026dd94e GIT binary patch literal 4922 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 S#{{9H5XZl={}OC-eg6Z<&M8d* literal 0 HcmV?d00001 diff --git a/venv/Lib/site-packages/dns/__pycache__/exception.cpython-312.pyc b/venv/Lib/site-packages/dns/__pycache__/exception.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9d689b085d48f7c6397f99267ab8a8f1ca3393bd GIT binary patch literal 7266 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 L?T;85$)o=PJ{7** literal 0 HcmV?d00001 diff --git a/venv/Lib/site-packages/dns/__pycache__/flags.cpython-312.pyc b/venv/Lib/site-packages/dns/__pycache__/flags.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e6b7ace115c86c5027195bd3549290c184973dbc GIT binary patch literal 3116 zcmb_eTW=Fb6rS0O?f8;77XqXg)+ljtMbaWro3@B5BtStBRMHA*akbfaH^Gj*HoNPz zi5$t1kVphl{1#V5Y92xP5B&puNfcFBD^)_`DQ_;x6Y4|H8GCKVqEJ=yTR_%#ngw*mAabT1%Ao3nzI}R|H-E9YH~OD2VJW<%RS(a zHf>jvd%=IowVJcs2mX(nwyViUbWiHEYc*%Nf02vzEcHxGPDwX(%Tnib>2g|~vqttX zX);C~e#}@*G@l>@O3B7Pet(1L1d2&yPNOofak8NCvZx8NM-yeQ=8=7xSMFH!#X1}T zU%W?(&?`hh!4a~DZ`%VG*^^L&K}^Q{j!#hxHKQwv<5!eSR?DYR4=T#Nyqc~@xNmPc z+}N1IeK+oKwW%%!UF%P6!am}zknPtr77EKX8 z&cpyH))0vMfbR*S2!U|8 ziJRsTwAlNPqITRBBcNDb5Nw6+8Zhh#w*Hgt_;n+nQ4(p@vK)`8+j-MSK;;?(vEu&R z*FeA?r^{S{q}(QHq!F@J*O&PMKS6#tM#!>I;0m-Lr0V;-CApD}D_QD;T<+0;KnmOx ziE)!AE+ED^qLoW0ZAZ-Ka=IB4&G*1y&VqNC5QJteMRy z2+_oicNTuuS0IX{5)2hDRYKkC@|yfGUc6ih^}Zxj{AiU{e4)}ndFP~lb z#}{1_t31S>2g1ecHGU)Tl0ys$;9v}Gg0ey8084?UKyM#!;sF>tK}Z~cFY~|~DF{oz zu}0@Vhjr6OT?HO%Fl1CANXj}?w-8KrGn+5FhHFjT7DJD1p*nN6t26hl^ zKM@)}15Ka;+lJ-{scg~!(h+ZsbU(7G)R?F94qa#`-KW^u8s*q_$hI{F0x0k9EncgH z2cCtaJK^ZF@adiK>D}~nqC(sQb*+PWsCla?)IXC=fcA!=zsP3Lt>N*Z<+@)-wy>^9~f@iYAv+dY<~ z8q)MMWKK^rcZ_2l$PSFBlr^!1tE+cvN3&7SfOi~zRyT+uIp`$8{*v~1VmA=k79#H$ zGs93@zYFWFG&CVw?U(_wWt-E}BQ9oa0T(m&P%CDy+b8EBa07!)#C@epkI(G}q-{ZZ z7l`k@y~=pL{%ff*$RM@w)sFr3^=4=Ar)|hPb+}1d&Zf^Ge)}*1Sn_Xn14G-w(7PbO zY!F~b4W!n8D|{oLG0?IR+8KC#^%(=~$LiFuEHU+!h8I{@78+ zt0-DF0cVtew-p@wi@BsRXX2^rc&{4vWxO5nd}J4&!$T)JJnX>Q#$%m`d&|UjGm&x= zziW1)=t6;4uZcZq_MqrR0k;w{@rG}^TwnRl*Z#6xFu#TF z9t`>b6x_J&XumChpb#wtWuH;s`)mL|$M(!)|Q59b__ zz%}B?2$B*qG8}u`7KuIu`ainIyM(w3r*Rct<12zDR76dzNESeY7SKdSLX%d806Fb@@O{W!1R00dopi!ox8a{=JZV;G*XvF7g zHyPB;VK)*Jlui$yBq*u*Q{=aCS?V^x19$K-1t9D|FhXPrK}ea8EV30Aqv`aB%i{1KDu?W2I{>k%KjvcPtMklX$*6{55@iw66b8(xL|=u9Q7SA# zbj*z&GVgLYZkF>OOyPo>Pc4?PZa|Dr%(2XpN)}CWYG8i( zSjREO-mOAoH94yrPh|nS~Vl(umzO?2jjCw>Lm5 z41(PoiKaieG1c@3=;8}(#G8TOM&h*>cmuX*fBn?6efy`-eyI0Jp;mYq8r%&HHio|3 z3(YpXvj@T6zd%5m;i?=Bw}txHwzSjxvu{6gVOw}MQ%y9(k!rjd>8mB0!H|8e{_XyI z@7wsMq*9Jd){DB4LjtuUdiw!Kpoa?eFqL)vSAZ_D{YAPQd+!!Zt(1rN&=sn(;m0F% uX$c(1y}k~(8-Id}uYj*5k8)S+cni?Sc4Ft+4*n&+6WhJ~;or>S;{6NJTg`g_ literal 0 HcmV?d00001 diff --git a/venv/Lib/site-packages/dns/__pycache__/immutable.cpython-312.pyc b/venv/Lib/site-packages/dns/__pycache__/immutable.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d95df6a5414ac645801897381afeaa6848e2640c GIT binary patch literal 3826 zcmcInO>7j&6|U;;>G?I|Uu-~_HV`Xg@Zb$5N&vACOaK{-BC|?xjC$MCHF(B7)3en* z*kf$TihQsWB8)(5uM%mUQ`RD@lC74dY?O_Z+zjKxE~mWLGuhy&sQ~nt*eU=Xj%DA`tLzP{(&D}ldDX52AD;n6P+4lh(a$ILQWhKbJCDR zaa=S4IeAE?gvkQAPIPIO=mAEv2Rv1KeYSp&_qZ2gg?mvQ7z*n$6ZIey(~=&Vm4+fE znh5_JX9p63BVNguoX}OxFtibaC8!h5j)#+iN2TGAd)RFo! zDf@>`M29Y8<~3e&f_YO-nT3)Q&1nTSt)*;}mr_#AHprtp%6|c}NTvjS)~4S46txA9 z&(^N04raZOc|y>?sIK7gDLO?|f0d43zI=%aWJ>e`P%Ae@vY{Gv-cHf4k4@1RRZAf$ z_GwQD8imQ*+89%`yy6E;*>jGP=4MW@N10+3SSpjwFui6r8Hp&>b~CMXRsGndWGv>gcwRGnU7V#sB%flIVw>(U_AE*IQUi&g6UkU;hz7Dw zG-Vo)wwNE5BWfcl=fLesD&Gc^Gx-OOteU9Ek&6XgvzZf6M>T6SAvu!8j5NpCaLja$ zg=}GX(M_)>ubv-%&tlvfE)+p<*fz)W)@Y`nS~<;7M~Ywqvn+qOJ2k4=Bc{DKfO1B+ zC)ka14_W@<@U6_qu$8e{cR@>yX%Cn+tmmyWen69u+#yxXpx!js4zSF*lR9){?h2T5oS1J~`L-xPK=^4j!J9ei?ZgAV)fX)s16Y zvHBXm z0@!ebD<`U|510*y&Z#PlHQ|^z#!41$swlh}I*zz7SzkS{FWwHLRx6MhvK4Q7@@W3i zX1sGP-nne8$4{+>PVs{`$vEy`jrjKsx1qUzQ1`j z{-U~PAGr@_oE89EJir9h@n$FCRMYfvqeN`?qCjogy8Su{xL5}b9+1;-aQPrfhyxs* z#t~W*LAP^c%j7oGE54S{ua1OC?#Sjy7K#J?50$CeHV6NU75~#wei(FE=mJQ1HwKt_ zxUeSkDxV!F`BNYeg=qZA`T6s!(W6`S4RcqvTaPa3ACG-Bw%MLsYfrAX_pG{N!rr(AOi0rn!+k4a_2` z!EutUO;?!3=J|Y(@HY6wo6cZ)*DEX3J+y@}s~$C|sxS*Dja5_5{ptb%s{zOi*^W0Z z^e$d_dSN|&Y&CT3|6ra36~SHm__uK18tV`OV6r94`T=a(r{}IFc45Se5)Z zl1?NEAS=Y(t5UUCAsh)NzcmPCmh42xq4ur58!y8Rfy19Sx9$X>-;qhZF0wujC+bjWnQ?Wg6(H?1#HJ)O2Ld>>|&< zz34v#C%%~~dsn`Hl}Bv^3Aatvq)lyt+5Xk;uR0JWH6`w^!(ttLBRTDur`=!MX!}s2 z3$w7IkL(AMeLDQNpRePhjbU+{w{{wy30X|nea)K(a00L$hGLl(rbQcp*KP1r!DNF5 zj~d}3^p9HrqA=oao1(ht?K|k6??erNorK(=q~=8Q?I%hJ_rNpbWJ0`0#%V&DJR1R| zQLdW#hm6}w_06f+r9$P>_b`=4v>CAU3|X}ZUHQ>9te-*9qvQA zNGW;TAdwRJR2-)+F04=|l!E7iok^Eg1V>2rI3hFb+Pmm=_~HYWXA=c}Y4V6Co4jb4 zDb27hCH+ZsoQ0RyzsQ#}QkPtN|jZTZBx4&2@Y3U4D>(sVEhk_X z3kKtOA3Nb|6Dd}}N0%cR8O!DfOHKeUc#EF{-bI9j#CzQF6rzno#~k|5e_&P6!<9wy z4*aY?0+}JZArfj_=y-l_BY12(-n{UmUrfx!w}TBZ^xj7B$c`X|&uz!*7aAU4-)d@} zyRj=*?Yh%;S=+Hb64g* z*lsZ>f7x`1yk*gu`&p`?l{>Q{?-l$1xm*1Q Dh|e-q literal 0 HcmV?d00001 diff --git a/venv/Lib/site-packages/dns/__pycache__/inet.cpython-312.pyc b/venv/Lib/site-packages/dns/__pycache__/inet.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b22120a5c677d65d5d1f61e058cb8498a28c62e7 GIT binary patch literal 6739 zcmd5=Urbxq89&#)wy&|lPVC?S!MRYUU+@g?P+Y&UiM<365butBDH(y)7(%^6))R&&b`+* z#7(LLk>d23^@d9{GiFITn;&*-(PN>^e?5HSl@(iKuuBl-mb?C>{!HNpYN$ki7(2k6B62|5g=p^O@ zyqr!B)QT#b)aEvfSqAy*oTvy2P!6Qj{v=#bQh*!gFUWEVX0l?u%1W$~7Gh#t6nMNV zDW#H~isjiRZ*sS=Z(u(Ud9)$;M-u?-ORaD!*3{ptPH0!ijWvqQ&W;MCZ?lGl1oHK zGYTxBD5dVM*chjdrqpr=rb~30gk5-SQjjKxUlm7(6;Tzs(p+qUyCx{Zuyyxp4@OY z?HgRBH$7c>rt6csmORt41DufNFm7&-uwX$U!txg5?}IRNcmJboUsFNbT}rl%v%bX13{F{p!8_5q`qp>Cmy zkPZF>qYt2R2RSPg`glo|!;q22G5x-jMJ9$Qq++2b=`wn%LYaq_)esW%bM=}Yzp_=j}FMt8xF7hVqLz)R^ zqR2am_Wb&4{7o*A5gxC-W1Iv!n;jKc-rOfo)N))-CCQFODVA zM#8qWFb(zUaQ-*U{HAc`DQtz!QCYAnCx#K4rev@rp=F(DL^}9Na0h^&-3RcJB?C;1 zR9$>{+O$_HRU+IU^^nziW$x zENd+kRM$e@NRMr;hlYA}3HTH2|C>qxg=Q#Faqr~WU9&# zj7{P3E06(_yo{~kQB_&+_)N$&U`5$bO;P^U7cfE6p!p7C9oay1WW%oVG;qIuv=#lr z898eGfQ{5y*6N&*Cfizof{wLD3fG%xsB1N2;i z%~|HGS!&*;;!h+gYcU^ttQX7P8S&QWtR-uWTfmQ_RN9)ai_UPxx8ysWlz?Ixv1C_BPnY)caHfw zkui>6N0P=%hJ)FI(B3&Jw#P`~?OEz)=;x)~U&m5{tvZt6EjUqWs*G2OG|V~F^2${r z4)_eRjt^{i@tr`rN-a}Y(Ht}9oMW<1{ar96CG6mS1Q&Es1ZN1;dMm!=u7oftBw9_0 zV9BL-vSPQOn+PB|?FLXg*>p-)!>sP^>t<0pEvM45$f*L`nn+DmYunO^*2U3zS0kzm4e7(RW^(zql}1 za5(2@w`-en7dLA|3l?bJ`r-Atf;W)!FT9;+f(57R*7VI_!R=dmb@A1lZ`0kpKowl> zg|}|L@W9`c8(evAh0FWe^X~QszQ){%Tx9vh1qRQja*?0fa*n*0h3D4gT{^RP=GNJQ z|45EsiTp}gec_k0dEbe=`$W-Va|X=*$*s22>usm+zp>eNF7G>^cc1^?_}qoO!H3A|3~UFQw*sB(fzE}#?OW*Fkx50QoVNK@XHP84-F^J`K^ESQIPL>jIk0VN>zf&-UvW7 z?~15OYKE!|t5pl58FUu46i(xXiHp>^@A(&q7f~wGKXt;BLLtH)sGd_gB@;qeo=mEgu&84f0 zS2r9@1S1ai9MBxmD4&W&qeN~ryE>BwlU2s* zK*oAO#=#Dafk8;10wvKDgT5F>`99HEQF}%K1Q|wKyB-&?PKKm8}bxu zkn6xw_hR?FqX3n&i)ZJVqWvJ%x*RJaC{}!=SUt3ZTg8rwkqi1r%oy=e%{d^3V#T+E zi()5a%L?hrsYaKVYFNHpL{Q`gcW_Z`N4=*$v$jw#Qw8s#WDol&JM~iWV4cn#ik$K}f`S~we*wRmJxc%p literal 0 HcmV?d00001 diff --git a/venv/Lib/site-packages/dns/__pycache__/ipv4.cpython-312.pyc b/venv/Lib/site-packages/dns/__pycache__/ipv4.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..41808eecbd735093c3cbc681fe5736d449485790 GIT binary patch literal 2766 zcmb_eO>7fK6rSG#r_QW*odgg_s88N` zGxOe?eQ)-?@A=nY&<`jj-0#I7`T)4g77p?DWOIa0=79qGfdUm*681yNxuk21>*ru; zkM9>0UUe&i>WTA;`*XhEt9VpS^{DQ+YcI8@5;v82sl@k$gxUD0#6RT<`!l=WeSiFj zG@>R`Dw5)9Jw`M`$B_-@&gn2Tg)=(cFg>!OAQfZNjZ~6GdJHlJo7|Lh^x4ePVIGjO zEDM(GxdKN!06_J`W!r#+6S1>Fd4iqv7+s>bkOd0I;-Z|lxnOfQ<8eMiE&D*&lbP!_ z^l=ptN!BGRG~01W)1@Izm(i3IH_(_w42h_d#9BoBeu+}6t=f86lBH;LXo{#f8a*tL z5%pO|+14Lg_kt}`Oi(l$u@2o}VP?dpbj0}~b_E;kUN)>5ySXrLa!FM;IYq~&Po0dZ zDfS&qv2RKz@?;M}1`0#O&ce)6dX(;dW8e%{5gtgTv6>tp#)OVXwA3ITlaqr(X{_n= zbCtK-Vk0scGRW2j(@VBFifv3buIl3hr?jB~tP!;>CC5hPVHFQhpI+5c<3}Q?Dbq)f zC4)LKWK>1{sAmoxr^7|?pbB`!nU2fZ-1$46y7h|cxv|-?)r#ijispj1c}=X#pIA6{ z{p6zO7QE@db>hckKb-u@^9%ghU)a~XEcO=o-c5QAFnRV_o}OOs(EHk%UR&w3WG5D` z&Vq!%PTEOJ_Hr8PG@Yhl7A8D9HP}jlPQp&R2rbbPWnIp$;21Z<^kEK#v- zVNWl?tm`;97YAUP%W{e8Cp8=oaXoo~)39@h~YFCyk6%N_uyjKw%t!oeQ$~NUighY}uDY@9D0GQaNsS89?dW5ncHv75dg2YA8) zs$nL=5cAs9Af|J9lCBLjGu>^5DX;JCz3u-h=<% z=RmOa;XN^QA3&kyasP@O-F$=_X~egEe0LYP9jxi@;BUXQhs`?#Hg+92E^#|hjoqQu zG&6KUQH_ozO4vx|tQmhAqCLmrDd@J^g4%-UbiQw) TDc`@mr-fN|F7`cO%yQvBvW0=* literal 0 HcmV?d00001 diff --git a/venv/Lib/site-packages/dns/__pycache__/ipv6.cpython-312.pyc b/venv/Lib/site-packages/dns/__pycache__/ipv6.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..abac4e2fbc7e78b91531aa7812a0e832108d9af4 GIT binary patch literal 6902 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 literal 0 HcmV?d00001 diff --git a/venv/Lib/site-packages/dns/__pycache__/message.cpython-312.pyc b/venv/Lib/site-packages/dns/__pycache__/message.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f400c838d1505b4891ffcf5e55d744bc586567f3 GIT binary patch literal 83707 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 WhF08PulnmT)&rGda=0i!9{)dI=`0-p literal 0 HcmV?d00001 diff --git a/venv/Lib/site-packages/dns/__pycache__/name.cpython-312.pyc b/venv/Lib/site-packages/dns/__pycache__/name.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ea658eee786d7e4944344d288b73edf72fee7e27 GIT binary patch literal 49268 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 literal 0 HcmV?d00001 diff --git a/venv/Lib/site-packages/dns/__pycache__/nameserver.cpython-312.pyc b/venv/Lib/site-packages/dns/__pycache__/nameserver.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2f69332c71bcd255fa204467de3c7845c2581cab GIT binary patch literal 14177 zcmeHOYit`=cD}>s@Fl(}>S;-KY%`7@yLO&VvXSGu?#59zwbSkjt{Ivf+fpb>J43}5 zBX11bZ6zRCTPRw|SQPP(wus?vixya*z!n7@^jG`GLbEV2PSFMSPxFs9WjOV(e&^mH zIUG{9Wvltqy};Z#bLP%H_i^q$-#M53_dvkQkj7o_ocP6N#(qN&wd6F2>c4?lWI2|T z@@zsHl_qSXHi@2XdHaN8)WKFStHa*$&S2gBviF4RUS=xIq&)G-FSNz9u0ln=ZdI zRUDfvWb(T6y#k1oq|4K4ek!AC%1BnUMC4K9p*kzkP$+Cv%CS*fPP)iO?KvBAN6xM| zPuS2o2fAxl+@PJHWrObm?E&ot-woR5VIG!?=48b`-fb2o=<(iG9)=>A^C}_5*@c%F z0&mW*xY22Mt^=2Sm70cHYdR6i1ur^BBkR_N5M)DrtZ96!m4kLaNqZx?FxnfRiKJq> zcO)~RXo~uwqGmB0O^MW}>Oi*0X4o}pAa+RK%l45k;&rkO-$vL38hd}a=)Xhld9^&JJEYJ08)3S(^^qQBO*mVjF zrlm9a)0r79J&`$+*2YRosm~;|^>uu`n-Ih9v$gfcl@)Lq)-w3=0PpSY*#BVDeIsUDgvC}~wqPobkK zcFm$596MQ)v#FVlp?KC)%Gz?U}rIqf#|0*)-jbWv_M;TjJsT=PqC%{@zd|)uBFr1O)HC06%hS8;>8(bIx783C^|gou#}G+WLnJ*3kwgiR zM0K!8{8o5yIXq}II!N3QH6%<_gc?F}DzT=9ti-_+gGxi0#Z^zFBb2Jgko{>BfKdQn z6vqk296KT9>=*r`&YVNB8{owSnt%*}7a6dH0m6v4GB{q)4!rdcoS9>1q*4E2mh$S! z;apCIehe3KKfrd;SczZNQ8m1h&Q=7)VB$WMCe5<~O?3zQY);dG?q|@1aM$=#xX`HY$&k41p zRZ<#UtuuAI{W6Ma1lO=Ezmfy$TLk zYQxB)(kQNCQSnBMH1!rZNlMv94u~abfeL&#C~aF+P^%RkMwL|q!vtfk)|_7PStdnX zWV39XOoAoV=J~8NEBy>k2TIlFx=|EwFz@3v)o#3CKVyICgd*BU9!Re|klsky`GBeQ z8Bf``3h3^FmZq`NsP+M|74u!OG_Y!y4eYcZUue%E16GS97E=qUKif9vE=Rg9cP$Mr z?f>l1l|xIfe;)pP&)>g%?d8w6-3-2PQ+}a!IH>b{ILHIw!-2as9X1qN5~GG6L;DC4$^tQt39!gd9W#hq31`E>f4q>_emH2j8GK+|>{%1M?DIY1jSHeMI^)xtTa;vNr)&&~NbL1;_Lx!@tf zsaEXVUz8OGIn(eU{qPjt{i*o~XSM?%#&F zQHo$Mi0B>rC!gQlFhz~spvel+Zo(HNIkWAp5NwaF7QB$)WeRI!Q@@8Vghsvrs+}q6 zY;Ho`fc${U)BKqX$WrIkO}~=2w604OYuUYZ2i4qu3#a8#idrPou6GzOYVKw*E;N;< zB*<$Lk5^kdze#wt>K!s+qlQc2v`ys^l_JkiAOT+<7R0_ky-n!W7ak6 z{<(DBczaf!l@FVo;RkqpBF**ROz_H+AnqfQ||cnx~M@T&l5CRI%j8;{ck<;;|lg|iR2yGaHI z-}Yj5s-fb#eL6PL~h zab{s=@$ACca;UQ$iY@vV{N?0^a=dHtM+-kHclMQgx0DCJQy$n^3A5xgRpv={%pI)k zVxi<*aHWSOdmiKaOgE**)!vGBHTigCJTBm>?t$J49}l{F z;M>#PSsB`Ec%2szP5lU~=r3%G_CIkt*;&u5=iAlhS=rEMQWB*0EqalC@(wPUJYdpw zcRN*9X{;NndnDy;TZ%RyOHuU*@!msbXtVC+UL;p9mGrBj)aoBnG4CUky16Op5Ah8T z1JoUG1NLphi&D2mt4&_{NA%%8AmiTmodl_O-@s?bt{l77w{y8~=R*`63NLg#%KCRz znYX{=KJ#+@+#?jdkM$2eR!va#T4jc|ckwC%RAqoy*~_b}JP!fiRFdF5(4Il7&kx)7 zC8Zk_tpvnyBOrzw#RL-YeGMOf zqcdso@xd`1ZlW3vH$4f5n<(LM6D53nqJ)o6l-0*4-l$Y_@;A501jmp`R6`~`37JF* znMAcZ`NS1YK2gHSC#u=WzaIxrY-lu8SzPrLHK^*}Sx_aRHk>siYS_}igE+5HT;%F= zbm&lACuAN3a9#|Iy5aIOa{0lv<;dChz|CiH`Q1D?;3bzIv>$JMpaX`CPN2$;$3}Jm zC5)2`1Qd7+a7>d6DXv@?R|J=Pm72(iS`zSS6!;3(mArcg|uREk+M znxeHwS9KJb1-?4h5<~&MVrc@yJOOL5bX`sj^14irm)v&{BjS$O`_9962O9Ra!TH`9eK&d)d zkGA%UgmZz95lDa&%;$uFE8l<(6G;0)tP=^CM)L~Shr z10KuR(AD`qbIXjFVX$+_@tJ%@{*U0`t>EV6;O03; z*%zF9wG7?fj~LCIPpDZxNq*>oNYh~ptv;?3e*?lCPi^ZO)t5I48|fjg5xHOO9Wyva zctZhxO9Gc#WJ!Jo5Nwn_%&yr+QnIy+d|TOw@3{NKHyDZ^Yek?>4nVla<2+R?PHAFd znl~bqeEHU+BH_ruGd69zJ2=q%S1SK5GJaMRie5rE>e9#aAD8{nvOjd`;QYaItfw69 zDknDrhTZDlv)sR@+_$~lyR97UT->{`x18u+oLHE+8QEBgv(Sbr^M$+>=JR;PJd$B) zaz$udF5s$ufy{c`^nW^A?pgUsXUpm!9Aea{ zx_TOCVYlhC46@o;SfXq3*ut?}@zioW^<>kOcox%?@ZPV_6(%g?eZb=~=Z6!+8>Aaa zX}HgEqss{z!Q~?duGtS9I3Un+vMpMs)u)mP5O}zQC?2k)O`k`aMWm7kGik#R`fLHz zYlG2$MAcCCSGY9V-zlCcv*&h|H*P6+_22RNoP)m(gep$lfmwSuU48S)YZW&=0khy~sr16STt_qcb?%DoNH2w`Vgy zhKoDB7Kjdx0hfttz-4+8aG59pmx&S(ohSj(iLyd;;=Nu&=!q7Q;20u_YKWvKA(AK| zlBiaMPF#V|i4q8%C@Vq-PposAg)FXmsw4D^m=<%~*Feva8bYVv3>l~xxLo0wn{>v6 z&}EL$J;s+&pk?nkomN}Yc+$eaBYtxfKqR;(!Hc-C55FI3{FbPFO}(u(8NVY6a@-wT zhc<-Ji-A@-<3p{yCu*D=Np!E9R6w6?4J zkhO=|%~4_wr|Fc5Cl06Sd!A}=+F^pz8s2cRH4t4tyHe|~9uA^SeX4`#d#F{Tk!%9d z6lwla-ohdCCjkziKNWzZ>76Hz2JJIUxyA~_i2uce?N zh|Us<@vMqsXkIShs{S*wbw@F)&CMDSOk-{yf?v*{1GSmp_@x9!+nIP2elbslDVj*p zEsesNR{XMMIlg5Lvy#|~Ss~C;n;OgyKueVllg0tgT# zum@2Bdk|&C9>ja4hCN~}BEd0464ek%PeLS7LL^bG*n_wNdk`hC2T@k+0iM7fG`Lw@ z^|UtZVU7y|eXKk7FzUqFE@2NBY`F`+3w9wI`0Zg2Ya2pq!X9gHZ(Ho~ZaeJ3O}{#W zx%+lu$NP}T#kRtZ`m2Y-4pVpPuwxf(b{kQ3Ti9U)>^AI}bNou~<;we5xZ~aS;j|j= zFgJv7$6hqzx447mw?30K3&*jBeGrn&VB;tAKY55* zGomKF1Gjp2E%)vsxY7Ur2yR@&VAUpr!;L=%qwQQg3T~Ju!vr@D)0F%Wm0ID(*5&xt zHO)&R)fRB5v(yETX<1}7c{u3u?#ORQKcDDnE97 z2mcQhkCp2o{Cs=|*GA*V;5^EuchuQ@+t6it#=mchx{s*Z&&@sbz)OgqiSMPkrL#{S z@Y2@YW8PvZbZ;}&R8v;oQ$pmh_b)z=S4^*wwy3J@~-PdWk!)jsF)@ixZcyHZ@odb2E>k2opN#aMkEVxl~ za7Q`OeT5S}c>lNoweIgawfeg{Xkat@q5@xd-*srffkx1?-}=U=m4mUbefZihHLk(k zJn4uArlP&Eej$?;C8bLc4zZe$QiOz{NI4-pD9B7-6MqlPG@=hagv=D2Bon!CVVD@-d-SqB) zl(}%``P8{HN-8Jq9!n(8Cx#^D3~1a#)7v#RskX#N6S?HDq{NB5cvg<5a!OoD4P_F! zaaochAg>eu$}SX_xurnWwHFrx&G!PQZ*BVVzV{B^Iat`$oyXs{?t9x#eX#$ZUR>xo zwct9visoFB3gfEKE33amWsZ|L>`jjL<$zOk47fz+fLnA8c%%x^4Q{G7KC|v4wN~Az zfonJ3wnF3vJ)-xD%jD$%dh-E8gMrVwk2G3!qb3ptm*}@)2u}v0LDd_>bRJJ-#Hs2& zgE@s%Mq^@`c zx_e@WP8?Rl@r*Q{lfli}PN;|-K7Fe1(DQ0V-{GUDpFh+``#70atO+tUha4s}-efwV zD5=58sVZZ-yA06R&_L-%ahZGM<$@hQ?tI_#f#-hU=!2T3x$f)T3&Cv zNA8D?16rtQ&DXRp1Y7^pR+lSTM}~k&no|xiO#)k(7%Gq;+=#g}0O;2=UrvGmYEGfi zlH&~NX-uF~-h!9b=%Ws~nE<3}6rw~Bw27!PMjbZ@ooZY&Bj&pv`n(CnW$xoh_0{Rw z>G@}Gp1g7L_Dk=@@5C1)2N%2t*|=lTi0X~U+3Lkre>^^#701(3JdQPuzcQXk>u)OJ zaWR{W$7Mn)(>9T5|EK{*)~J*l&WbXzn(QO6kL`$4kS^NzXgI3Z*e0@o$2%M&fQ^o^ z3@b<(DX*fq!u^M<`Qvcp^3f$$1Vfh(f9$SYc6i-wOLYx%FJFJTP}iBS>s)pcWd9RC z9sS8@VgHHz{u2!Hv9D&ujW?@K&Qry{TCQa^-raq>=Dp@S&4p-pKH9y^0r0_-D|BOR zA39LeNFzor5@*IvSWSZ|O2yd_PXY}}7a~paj1c>lTqDG3B^LyrPx4EFLC0pifoStX zTvc3g4Fn;yxPg!ewK))m_~8a3VgO~O7(`hmhEP_cpE8ERNEwqr3=cZQ2- zcgx1dJIH?_*3~1{4SK~*puI(`-}snC`pzXbfaAAf0ylqD`uc zq+Q}ykco!`s;*-l`u#Ah1j~SwQn_JC77_x}N19!k#dAj3o@H-Hb#G^IW_Yq@3`d4K zNoF&-L@J{Q*-Uy;!_g@mKm5&3A=ZDmrxSQYR@-M8s6ry0%?t?>=vY7xDVbqomR|~R zW*l0rsnasFSRE%alMI0&_63dX+%SeK|qb7DBe{opKXawK`7=l7VmW0s+ zhL+3Xdm3aXk*UuQFcr;OvOCPv57^v$3!nH?8~6Bi_%B4J`Ul^nLsfks`?JZN(d z$}tV1NDF%6C3E03xKxe}EMtwX=zGwfP7CKGL6p)`P7*=Y^QrUFL`uPU*HWZ20#;C) zisJ5w4Hy;rG#ZPgO|6|Uy;p|B=$Sp}9t62KNxP9jMt~FltE7$QxFHxJan!VHQnSPE z@G_a|cX-tWH)2qs?{&KY$~~1y<>K+F5bc^S<8c5@6w=kO5H>f>xV{tC>>w8@eARJ& zLaxKhB4dcudjS8dzd>QQQp=e?hPi#o38SfuA=Jyg+{f;vq^LVH5 zZ&pt`M~KFf%Yn8dWvtd|evnVWIeVM`E8f5bXRRy_(P6cC^N5RDk$GNn8KV%LqN@>h zb<~~M4kCN98TfHIgImFDIq70f7|hCWthzK2L}KW00$gnHf{@5DR%CJouI-qd%4mEC zKUO#|O-^KGaFIMTK8j7F?Knb0*oV&Ic#O#dfAK1w0 zptbG5#u3h*g+JIuk6keoXU}Sk3ilJTlECD0V{b9eBqEGKC^mF`%#k4<{p?vKC!alQ zd&m5rPkDdrlmEdLtxz4Qtm^I0Va!9vPBLy)ooOkfx+i4#scMBzw(1>{au*WmaVhGS z_h2<-gnc-fj0KffRHrN|3VBOJlg8ollX#p8GUnEi{3#$x3I!a4MlKY&dVKcy8z*M? zrJ8#33syL%uVtp^L8SWHzBew;Z+l~Usj_OW;(En=VBUE%a3gT5H{Y=5_SaF|uYBBi z=lXtVxdI<9d%4Q0t5dU6Z%ohk%=axsgaxm__D<2ZCAW-iTMCgNI>p}LCI#efS8j~r?WQUb(bXgjcZ5v&Cl&M`K3OMqSrXM8V z-hS)U4|o3cXg;#*b}}E?yWricQBQ)#xQIeV{R}G1?=jxk?%2YRNtYamhVUacxKX{p z(-!8rNgms)YzrF(?(;ApO5}_kIVJA~`lyrfhU#XzP<4PSh+2asR8KrEy@Htt=pv6E z#?e4IiQ+PcB?KpQUGG}q9KJ0xUtfw;&DY)BdSmOo$j*POZ(eo-y6oX<8s_#~-(zc2 zd$Z+6%e_dutxaX^XG|WM=Ic9+#*zoI)S5TV%SZ49TtLS75P`)mAV!seXJ(Fy9$uqH z6eGdvVoXn6e`jE&}2wg+`a2+Ksl2Xq(EgCa}S{G&CWFQ8FsLN0FDk|GIT!-YMY-!-p)SL$#CW6X*$vBTUhCC?4)#ZC@6GDc4(7G`(E5SO$B z71_-!;>03WEP$nYv>A%JG*QdKQ?B!vBKaU%D=ri?CN1<<8P{5y(Y2{MO|O)I|B^oa zR}?TiYPn!!CU^DK*;f|>Ew`%f1-7&B$+F}tR2hu!pmXK7rGjr4Hvvy^4=};!ykIv zf4|a%K7PLvM5$2N>+d6bdY*Fq!_Gauoa+dkY_Pze6}ZINhAhDaOp(};Y-ps1gA)86anU{tsik0rOh&F82;c8!#g2qWaXlw8Hw zK?7G-2DsnVWgWZ;Hw-gA(E%1Y0)8RC)7U$3t8uDA3{G2;ZTG+%Vf2T({UwYKrku^t}`v!8B|X zs5Oug^s{hjM_3!f#oA&9SWo7tPb-KkBbvgz8fNQLEJt%m*f?49*vZ&oqUqdtj{I@^ zv}vbl8>9y0YAH4*oz_S`S|@Ehf<}iC6QCE26h=B*PoUnzu$be}zC_&M6k4ZK z=j4QpLD4RN7X}c%0DFjf#D@k2@dzGZgVBOIBxEKe2(pAQm>5lAQpoS3mYsssHPod` zyhG7Wy%y^?tNkYrMY~L5PB9lTaUOl>l6Z6+&Zm@6QY6l^0=&!fM!W`03o5jBCI&Vf z;$AT|I0%}+m(~<0I+;U5vNVKkE`pVTG$G)Kawu(aG{aez2Q~4a^Ub+zHVuD~DTq_@ zID$ahYc_AJS333ujVY80_6IrFz+I)O=Vd2_+@# zT*wwg+b?2PvYc${LAr>j7HiO<6KnK+_AC)uZvs?FI~__yC8W~ zH*-Mf#whYvrsH&^AtH=^K~a-(*sFg1hMaCO5%H9f|*f;y4P1u`m7~OBlzp z85V2Sj%|tY%QBfI?rHlH7n!SXu^VILKZwojsvrYq$F4I3^&cShOTJ_j>YKylTWe=^ zXo8KlYRQM}g(1%cp{yV`hi&c(>aL5#xm}n;Mldq4yNer=)XZuzbK>&imrT6a!nzuz z>j`g@6xNe2k~JZ&i~S7>?;=EYA-+a<7q{G?5GM^n0lre{Z6f=8DQ?>1rPABPL38jq zQe4vr|3A{(7CSVT7*f+FJ~cH?+s`gaV+fDD1o#9_D7SHD!fsZiaPuWq~b;_bn^gYOS6Rv(_}fh}EGGvoi=BR5ysfa77-ZkFWb z4CEsoKV^3Dw`&`?`9Qn#M|`_8mehq8aC4lQ)G>&M^}|4R^Y)T!+VxuVI?kzGsIm?? zTgaTEY?CAkdts^evJ;JBUHZoohsPlOEJN3j9s(&-OS-`?HT5=R2!b+M*`~U`z(UR3 zE=cT>2QbZ=;zrWWC>&lRCS9VNnOdghVQN`(8Y7x(YVlz$)5|5$NBBsgQ*W?h8yBf5 zgtz9yTW?j|-gI}gM!%$rUm zB|0=HI<7JKF9wz<3Rm%#QZ08hQ!b^*$XihYIrBh9G(%Q!y>tf&JZZ;XCKNP6HciQ8 zV3#WqCytc7#G`Qxa|s!IBNR<6x$qTv`6U;i6d5hK0Nqpq!lU6}!olN4=p4S}Dh>l7 z!I8wA%co4fh6k!ec<8hVUWWZ>$=t+BF3Wt9WOj^sBt^ZMsBGt1 zY{Rp!6rrdw1Oc5$+J;i}+B%CeB#BJ{*gi6i{TJwH$Y$;rVKdD2gh`^!4J=kJmTf-O zVw-algXPu7q9N6vf}4Y*@Jv!FG1*nWMUm>3W%NS%pL7f0G|Cv2{4)4SE)g5d+Sh~q z?8u5D@Mh3Sr^-a32c1g6e(<4k2_GWp)WTIZ&hL6#d1v~q=|cN~eEWg>k%JGan-{hm zT&zYqT=#=O-L>;??|bKox1M9?M~AJ_*i*R-S1xHsRpcd>Eb%yDMYH|M?0OTqd= zaBDue^;T11+rIp^eRscA_{!1zSB^e%I(&Y7<_qbz|M?HS9czR6ru6v*X#0%NPY+Ce zyTmA`<@jDWL4pGZLW( zOX4vteps4!tK6mu6K`C|1jd(c{D{a!j!g2p|ND z5v$4~_Dx{}HU<5#yAQ!G7#+(^GVCTkA|kD1Va_7QM1-gm1-(m~G$=pMN*2e~#)i`~ zNSK_Zb9xr}Hi9TxmW&kz$I4(WwLDcV;KSykXf~{vv_2bs($IK1fzxuV8;F*z7>GWA z(NVTIvSf*4M1SnrHI@*HdbDFJ(qbhUp)A=_Kk#H}q4GD-sOY?teWB-fFs~5#vTY<6 zNZ`P-XyG<*xf#9@ri1V1nP*ufx-DPRcI%~g;%~+84*xX!lk8&65j`edsBh2Lw=YK8 zm+G6o8=y1qW~A3vHWVUT^O3C!-mPmlJG#-1i^=|-gDDzy#E!@~^WoMU69!XCJT*Ex zo=codOIlKImmTy3(M>{tj;po9)o81{jZkf;qLqp^6sj*CSJGJ|uE%A{8r1~zUV7uA zoCcX_k)NT$NyU>?e4UCTR6I+?b5xw5f@HCly6PfNRrNDQVw0)*`^<=|yaVl27lp61 zwP0Zz=8q7~n#IUuJsJI4TI&|7Q8t1$A(R8ECB1e~jS8YgS-`-ra1R|Jw-*amz2#cy z^05_{!`-~>t#)sl@45Nxjc0G3xN%~c!{goByUF+W-5vf(3w!*aXO*BUJ9+n)Ry=-p z?Megh-m&80-K|u#t#}&UJ1O+DtLwepcXk&*xyEY&wZ479szKCY^rYq{Q1sOrd9 zbu7C9fpiBfbUjq4Y|mG=FIN!ETMY9N%+FQTEfbFAAmi;s{{ zT*r0_TjC^^*t`r2c$GHI>)0^H?;>V6By;jtyaP_DY=Toc+gWrVO_JJgG~w8_4(>7* zG?J6c(^0OiNAimmXkW&alY@WdBkeJeyi5_|(l@I?yx#>b7{&-jtx8$u#)75KxC^$Vk3J%is;4b;G@im0Ju zhC(3YqsWOR6OY9A>ffOFJ`Xq6WQJMfAbbZ>*KiJbE@v8#Fb+LtL$iY12pD@THv$Uf zk|Rgh*;UR!EPE1YW(^_}qLpPrm=I({5T)#zH1Y!+6UHxPRJSsYV?d^>RCjJ1sjr&O zCnA_KQLD^(U>XNbgnmG+y2#34;eS$lOf5nCk%mLa;j*SqNC{4DGEvY}n%SODlUD6U zgWn+!#~Yfh$a@>^dmA5YZdp3;%+F&#i~TBi{BHl7{a4S-o+*TOllMreGsDg&Sjwom2jY&2jjVv7K69kM zr<#gquIK>eFil?+tuTEL!(6cXL3Pvo@x|($3&EWaBDJ#<`G^rblolhs3*KJMM*2UT z3V9Obz%2YT+_a&yXw@_x*Nl(eOf?pd&mIu{#^(=DKlXY5^kSrE!P~=zR~+u7WoAp= zfQmibWMAYvj+i`!_efYK!j1e3vQw?IE`zVN^DX8&Pi-ltz9fwHEgAsDjo<_axk$r& zU@_db;BC|NFZtF*LS;jw%70EoU8X`pVgE6fOvc?*OEF2Mj(L)3qLgCnxNvQj3iNH6xK#i>=cC4l!U$xlVi6cG_F6Qvla?3ZT&`s%1=5>KSi}dw%Hi zxF0JSDj;h{O0d#C38tB*RIB9Oj}@Dy0FY^l+cNC`+IjJ}sB7L7TZ-rwxs4XjC1u7( z<^5Fkz?H{YzUC|uFUrJ8YKTr}QigtJPL}E=7-fm8Ve{-pbCrVTGeyAIx6F~EFoCMQ zM3NlbdRg_crW7z^ewUVjotEh(U{m$`6Y^FC(rW_+c^I5Uvq(vIN!f>JM!F)Uvsq|agmR> zx{tV;kEo8a5)0z?zJB7h69rd&-c`TsbW}Pn2eHfsbW`57X~|V#H8CIR@~*n&3Rkt` za(E@o^DhwdI6SX6z1H-|QOkEDK@3Ols~oDUPR(n8Ioz*r gd2LI9Z^-ivj~r3H_FC^MGLUNNi_H%Sz&85-0(lZ-*Z=?k literal 0 HcmV?d00001 diff --git a/venv/Lib/site-packages/dns/__pycache__/opcode.cpython-312.pyc b/venv/Lib/site-packages/dns/__pycache__/opcode.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ddc79ed2e96f379d67b40f849be4f8e354242608 GIT binary patch literal 3174 zcmb_e&2Jk;6rc63H~x%MH%&i7H*E@aNDLh4p`bvNCMbd?kPoR?k=ASPrm<`9nwfQ) zSUngi2Xd-8#oRywLFs=2mqa-fEtg8=lA9~%)D!Q`uD|keLF{4Xy*KZT=l$OA&G^q~ zG(_Ob%L{A2gY*iO*5~(x=Bhx*F40Mf=z{Jm2q_<*#gxxSJ|nugO?1id{uMd9)v*Wr*(Y^+GK_aMIR}4(feINDmnDnxqG}rBqlC84*tpgC5l*h75WH z)}x^JdwK};e$Zo{{{ZMQ(9aoie)Qz;rQ%?J`xN_mu-`t#J_z=)Q|uSOK7NXQXv>!v zs6_6~FDVP9tfd<)d5CWF*=}c_r!+qWVV4*rB|tr(4k=Ohr6gTU`E@BJ>;6=q5zysr z^4tqZ1wrrY(nDK;M9`7l-=ethA(WRKa!Eo7>z+kh_lHGm;_@!p5)!`o!x+fV$TPta zvjxV`U3v3(`tIrqGbmdvm6=gkwXMe{ThEnLR@4e=rp$7t!C2cokzLp9jAeHW=vSC% zIZoi(hGA~3ev!+pvYc&9l(g(4?V-U|b(6hkalDeHEhnHBwar|yT&%?0#F8xomXu*e zRq`@+t_ebKZg_B>8}e_L*yzB&C6W;46M_?6F&~+bhX+^$7Ts(qMmU$$vb(Mtn^~h| z=PXmr7Bt2x*IOq!!vV>|uxF~|pV+m%z%`0=pHGA#8`ac`hN?OtRV`Y2xqxy+Rlg}~ z1#iXw)ymA`*N!~r%}dKu%PUKcJio9!Hw*TaFYiq)&p2UzmZD*=TRO#((epTjaloJB zNl>gSA1;BwreN46FC)?F%*$whb@uq+A9(E({PCtpQ+`&kLQFt@$Du+ z;0)L%j&##K7cM%IZEV_(ObxqC%^t!dAvh_pVY^9@N9(cg33={ig_NoMHf@<32DKI3 zUCEiYrD&#?N-`8u;2XoGv2jIH(rISXbb4H|*9{&31@?Sh*F`PI3Im z21z9TB7Ai}e6@b>AUv`sjc`;Q|5`zN$a;`O0&pZpK>#FM^(Rl@EvbN}`zk?r6FLZr zmD0?nO|`7u!A-#!EIn?dAcm@)OWD~AE>y^(z@zqI{m%E3KW6shw-3UJJt^TqvY{2qMh_%-(Q-(T zgu5;fpaT#90#Hb-{LrFMn>Y%xNa1@atems8OaU;ksIst_GatGv!)!fxczwGwALF5C zK;#jJyLgbEg%rh?klw-p;R2izFm>Y;ur1X^343P_SMkQFl17|F(89slx3@nhd3;55I!igTj4YOQyM39^)S6k1tBk`5KIn$m27wK+}r$}8qbm>EAMy@*CUCcTWq6&!eay$|o3z{l?M zK1n|U7vur^1BQku2*T@8BAkCkVt5N)~0vw ze}DhQU~+#jc|^d_2!w literal 0 HcmV?d00001 diff --git a/venv/Lib/site-packages/dns/__pycache__/query.cpython-312.pyc b/venv/Lib/site-packages/dns/__pycache__/query.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..82430f6555772c22bc65ac81c15e43ce72a67321 GIT binary patch literal 59111 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 z%0D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�p*DncLuWNA83o`|ILu5iGGsx`B0 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 Pv7b(_Ovd&n6utfeAV6a0 literal 0 HcmV?d00001 diff --git a/venv/Lib/site-packages/dns/__pycache__/rcode.cpython-312.pyc b/venv/Lib/site-packages/dns/__pycache__/rcode.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..57f047e1d6c0b0ef17520f6146b9c9facd0a69dc GIT binary patch literal 4471 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 Qxe*?GXg)9>1VzYx1J9I2VgLXD literal 0 HcmV?d00001 diff --git a/venv/Lib/site-packages/dns/__pycache__/rdataset.cpython-312.pyc b/venv/Lib/site-packages/dns/__pycache__/rdataset.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8a81dd5e91a6e7049fc14dcf636468226724db89 GIT binary patch literal 23008 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 literal 0 HcmV?d00001 diff --git a/venv/Lib/site-packages/dns/__pycache__/resolver.cpython-312.pyc b/venv/Lib/site-packages/dns/__pycache__/resolver.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..299aef9d00eb1ff2038dbd4028b6a73d94ad0c74 GIT binary patch literal 88419 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 bX1`Q literal 0 HcmV?d00001 diff --git a/venv/Lib/site-packages/dns/__pycache__/rrset.cpython-312.pyc b/venv/Lib/site-packages/dns/__pycache__/rrset.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0a9996dece7f9192b566ad788546abe1de5b8bd2 GIT binary patch literal 12527 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 erL8C5+*_is<r^Gitc;QJpAKOs*dN&g964wPm9 literal 0 HcmV?d00001 diff --git a/venv/Lib/site-packages/dns/__pycache__/serial.cpython-312.pyc b/venv/Lib/site-packages/dns/__pycache__/serial.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..633e95400917e8e641f66aa652237a8a9decd641 GIT binary patch literal 5226 zcmeHLUrbY17(eH>x0L>&VvA74tqwF4C#fv<1(-#Zk** zHXdlUMR=lQE?}}m8Xv$X-P3&3>|uaBGz||X+ly~XAC~yE@4L4x6pGVqSr+vq{oV8D z>-qDY-}mRV>EZ<*P$%dBi-aOTy`Tpy7K#B4f&tJd7y(U!3D7K< z0ZW7uK+A~5S(?(Rdra}e-auGPX?y&#}ruW!J6 zQIxuckaQ4Mf~#p58cFFr9)HL$dpr?~s*8qfM-gmIssOl3l4ZN&XJ@t3=O;bm+V2cW zTgABc`{Fc=5nt}i(7;0~+Bymqpa+eTeyD9OKLA-^Btw^tqcEcRk?$Z*tvj+4HrkNc zsJgY92v_fTMx6}wPn?t$!$ZRL9SkXpXJO(H%R7tOVMNKd0pKj9RkJ5=w%=%~v?-ulzKE2Xt_ z;(ULi|L)0O+J0_ZZfIS#?prkNOIobgBC$xkbLPVIg(XYPqQ2(!y*s%14#G-7qiiR_ zE`ZJV5EAq50h?tgYe(3g_4k4AuQ2O@tjtx|A_`ki1Z+ofYXvM@;BXX!!vM|;QLykF zh1VN6sqEz?Z1x5Bg7d;&Mzjgu&CsboMj!V1sqMKEz;Li`v)IIVpPmQ!CpK%eTx*Us z$N3rKv~k&3HOJ2z6UGJMPXF!xrJ4`#HQu-VW`AH`>^%2i@6w6$%Qfed*7Bse^jcS} zE53iGZn|#Synn8GzBW<25WaKe_LZgDkM5mKns!`ki?zjR?31S&?i4kw5w6bgL`Q5D zkE|72sg3?gap=3D0udb?H9Eg9sl-#Y1rix^N))!GK)x+uvljJ(1_c4eVx0)bMwc!1 zv)bA44b!5&e(Nr@*@%*hjQ%FC1ihv$~jXR@ijm9~=l+6A%AWt5ZAev|@95dKFgxwOR$4ldid z;W?@a+4KIHbD_C^YN8aRHY71$QyEh?r9?HiGRc?XfKg$a%DMwqYl=(Fn+>vbO~g#d zC>Q0nnz_(ReXzusVbWfK8W}3P?TDtFJyhH z$~Rb_(&2|G*bxNOx!Xw}JK*Wuf!`lO-XMh7l)>W(4hmt+3{xhL=Tg`kP)8UB0){b- zSrG913fqH#Unf`;Ch=j@=SkJJ#h;=2BR3MB}_=3Xegymb%eV9)D2N;TO)|dyng|F=~!<7 literal 0 HcmV?d00001 diff --git a/venv/Lib/site-packages/dns/__pycache__/set.cpython-312.pyc b/venv/Lib/site-packages/dns/__pycache__/set.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e8d7ba96a95a8f89073a30e1a437ed819d7974c8 GIT binary patch literal 12201 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 literal 0 HcmV?d00001 diff --git a/venv/Lib/site-packages/dns/__pycache__/transaction.cpython-312.pyc b/venv/Lib/site-packages/dns/__pycache__/transaction.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ecc53a42bba4cd03f08d3275f29ca7456066d75f GIT binary patch literal 29446 zcmeHwYj9h~mFC5h7X(R=1mDlgheSdmsRw15dQh@Nik4!DiX}Sh(DpzOFDQa00p$Xe zLwNBP_ZMG~|vSnwJn%%A02Vh7A7-7{o75|w(v$W{l zr2J!Rzti`Diw7y$agy1pbdi1U?RTGZ`t<43ryGA?SeVbjjayHSPCU(Vzork>S(KNx zSB)HZjT1T1Fu@Je(>P(6G!7dlO~a;1^RRi+GHjW&4qGSlhVv%*VcsC8wGHR9Ja*+- zpgaqeXOZ%BD9>W$S)x2khx3ix2}`{P^mU-J7K zCiyvB!E)yzH~%?uSF+qT0 zM{3%jD=%p})1@%S#}5(crau^Su0cslFKb zeY`9?j8Nh=f7TqMCdxNz6s>559d+{KRoP{a*JOtwPag6VA6o^;3X-Km zlE)wPj1T}%c>JDoUMXpnyrC({pS0`ZMgTU=(L0T}wIX=exR4g{qQPaDX*ldX=kp65 zLGpUU&VYYnS_q{o>AG!5ItK%xW0Sztq}Lzvibo_VAR*Ws^iGThsc_-baNpi%p9*@V z;Irqa5d3T?aM2$e^PP7GCp{DHvr|Ez-x~~S!JQ*xp3vDqC>22ICOXxGofLP$>%Z{q zao^czgT9cr^So!|d2~1UtmqGJ(>L(^G(o^4dZn3a9Rw8UbjeXf6>B`a%iKprO%c9n zjewA}OWyMno)Olu5rw5u1h*l`JVl6Bp>|R7g^0}4fR>*xC~k|chAka{@|iPd1A&P% zXSxL8*r;%3NSgAV=@3F=UST8v==wuKV3aLBE)L*=crQO1!3yE=m~j)X-)B7!ua+1&uKMj#Y+wV&1IAb z&Qc}3AVE_QBqtt8D*Dco%koYM!5&#n+M4Tms z2Es+ZwycD?L6TPC~#bDOT2%#V6#sptT81eY2J4DvNiztjrfyp#DE2yGIY1ws6 z2zo<|9?yD5Jm5Q2q~}7wClUor1lUj-35cn&B!Vh@BfxpU-=#w^1|s!X=A&qa3~rC) z70wI>{L1(Q;sPp)eSRQ$l9hvQYAy3z@c7uA*Oelkp)tTAJ-vXY0*%w?x9Alv2BhbS zye|4eWBL&Y>;+OKPfP?ZQujgubq0!4G0(rcb$|UC2b$e!0 z4&I!dOrVk!KCtt1@c~o>RE|t|fH#t-Pnuo18*T$M+$`@*U5@r@vHQ?Wshqql zCIv1ePqw@8a*Ydf;{=!t_lENQEK<)JW{tC^uu(FG4W~H>QpTBSB za`ZPd(Lt0EY=L)~`*7>F_}0VGt%qY<`=fmOmBxiFi=Gec<319?QLJ^jIycwlsfeE4p%N!E}Sa&fh4!UKn+3UNSF@EeGGe^wy=fXWyEQ zb{&a0jznz(YZjzl-HoCax^En~ejr}ED_Xm2jYGgHZ?#k{wl8(R*>k(+&EDI+(dIp$ za0p{?V$B&rOEjkv%~~;nbONwp*fec)nUY2QUJ?W#nH{+Vad76q5p`~+?hB$1Tnr-_ zJZfZ2U_rt2-VxuZPv$(}yI#tNxr>VF8oX_m2(eNtd7I!R^W1JRFyeMg9TY@#E`zU& z-pD(O{~)bOUjnj@$|)%PGxdW%1<~y!gwB~QLlQL(G|xl=dhvk4<&sGUrERDuvtc_a zjJ)IcUsD^lOKup2f2M{N9GalYr;qqCf9V=W0}{~l$g!PXiq^C3uH21#gc?WQm+7UO zJO)wW6! zQ0hO;-8Kxm%*lKo#Q9*z;~()#ZAhKu+4#d`oGdvuIXT7TVKxjA1U9t?Y4p=Wjoa%F zPDx(3PfhGr3y_Gj4pDjn%w;Z-=ZNH0et+-c*pE$b7ra#v-#!@KK6tnGWWrH-#j#>9 zdS(CB{c*c1YIiNQ$L!q^b2o!)8b}G845VXtVIVEum@R)1ppqU%WdK%zFPQ453@)=D z^M0~W1Jxit0j2}Upa)Y$Hkc@*^cd0&Q(79B1S0$8@LVWf*&eNIzgxK_;c#BD6J&a> z_QdT?QG3&3Ys}sjF}KMSN31w)B-ju@Zmk#;m0~qE&c=y*uwFHZMy$0>Uh}9`G}Bt! zYaOSRWQH3jPL<(C4Wh+s^X4N5QG15VwRZLv%IXs0`=nA@r>?q;qVTEa6!Sh;E*>>C zWS2TFWQUFNq785;LEj#_Ur=z_kh(RywDHZ^VNA-0E>l5Er@mV4{P3ee7H zf)jekihax*?@h{g1HL!oyG{9S z#CJQ&a(TC`(}x_I9He|$nqW#khBu=qpsuZ~u7{=%C1(?Ic8JaBcc<6_ze{X|zfIf( zf4jID{tmGX{!XzSez)j?zf0T#{}FL3{M}*){71!3_&s75{5|3}_2!~bc}JdiV|A%bG06%gP!@es;ic)}BxEYWbw=?SvNR-G zpWhd9yJyODGIW<3`aBvG+yn12w_3neHC{1)yC6}!`HJ~2UzMnC#%EEYq81N(;@}|) z6%t1tr)Nu|_uv)tJ-$)q+wH7lk{Fsk?@d}q0vDidW5RVB3J6dP1!n>32Q5DgTu1|Y z*bve{u89mnNCWja3E>%ToTTUs7dD6nHUF$Zm!H-F87}8ELQ;8#3ma7tWY+ivm1xpR z1R;)-mMX)6c#UC`x>GT04wb73!e%w^X{fwr{7;)LydN1U?6v6fE#_F7?NpJFfJvl$2eopRbRXv`0(Y z<0YNZlFp^kSji(-tn0@loRwNB1Iq(3=e{faii6}wM|Xr*CVLu05zv!=-Or_c#{KrW&XnMBT{@QY>k zW2il8W2m8Yw-sG1VI)l#%M|dkC zAPr=Q#CFFk2VOp~Qd)CmaMi>W)UZY;%^vAoFsFdt+1c2E?uE=E;&IcG@+!TBy3c|8yLK8k_ORI=^1!Qv)dzzD*Zf*kTm)+ zAu@gZBH(W*6>aopj^6}d5a;WoeEnU%5$~_quG;1{EtK8k8yHIdIUvLH6VUG|c^`-| zRc)6;1t*PE;H|eV*U_RlZ-!gutdsEg{V_tE1D)%C(B?t8X&c{ ztdWAAoxnC3KtW8y$rM1M2!&@LwGpD!+72DGO1(pa(w~9yq%%Z=#I{u(XP%OH#;Koe zl)xWRYK3B(^)pT`ifvth7*_w!hhha#tgQdb!9WXi=0F>Or&}pFAo+>e&5SD2sWfP%y20|mo0=E`f2}=Cd$;D$Jjh$EC*p3Bnug| z^pH-HN6KTi^pL)cH>^-7IpB-wFep8ZpfrAN*Tv3M>cOvz_+ONZ_NlN%MY6|3t|Oo{ zjXQ*>Ls%?}IW|Z5%?u^~NRsVP(sIEwF$Fs&Wkb!Id%^u4@b_R$(en6tq?GI_oQc}< z_C-TT6ClH;)KXVnP)kCHN0XQvr)|;|gHDH4^1eOZ40%7h&EW@Sky1H`MR*lm>Uxo|AI*fE` zxoolCAhg1T4Kwg(E&j&OQNFgwo3(^3xwM27uJp$!n(?K!sEJjc=7lXkw5Y9`wdz{a zoR*AzjBm;g3!8Ikk;7IE?|Hy`u6j-3JT(oLCMK8F^H%`QQ_S{99?D+H=a~@vQ`f!o~#CV+dz_{t#@KAay|E@cBn1W=}=j7}ihB!p6IWVpU@&NF?AADYiTk!q%_A z3M8Qq>^MhWW}Sj1ua-@j63#r~on{tkXDAcOg;2xfAK~=LLn)bCTWsej;VU$}Q)MV! zW2Jb{k%17COy$z`{Yx2s1OvhZ7H*1W238dpnWc_wltLWQ@4bkAF;Jq1FnO7PIUHIG zfayUY+i@{Sqy$`vCWe^?zuFerGRrL&2l1FCACxAM%4JAe<#1OSW46isROgt^Cl}+D zXv0l<5?;~-pkZhIY1WTH*#&q$r zI*26;P%?~aIjvldMNV4)uxN^;AaNj7sF!s`g=t+)GwW_e0TPp+4#XHqX(aiIZs{T z6Lx3BUi@OLk zoVe(j21m@|YE(onlhUjbWM)b{53G%??n1uF6jK$aB-Lhd!Z)g!D(X6tZgfhj2-)ll z+GceWLaoRU%R|>j7GYu*I`fo>^fk5;*g(jnomz~_;xtLv;e2KG>g->>5Xq$nBJyM; zN*kl=ktjA9P&zqTrRI4HpM8PJNwSdx!_{?E$MFZrYHuRl!Wpv`rVM71M=&5G+)HFQ z!;GtGN)yRG5#?%NAuL04aTXTIf@dlOtIiON3Q=r_aCQplrQD>tgFzjX8s1A}bLb19 zRv^po^!@{=4P&Hn90eRGsrpOWv22_I!#Paff6l)L@I|=F(D~sWQtjZS*GYa zE_g;UsHi<-=-FAX*RS-1wk4EKkn#on(7{NVS4nxG)vwHbG8}|aSUHl-pdUtrKA0Dc zV9O&D=o+v{Dwx3hND$L1QweNnknG55vo4FWu){?_PhXbHVJ@O8l42EYBhxCje#eo` z?LHqVT|oJ#R0RM3PE2|;ravA{snCSMFKL_tfU0y6w9IHJH5vv6rGE*$W)TCgj<@tp z3pI2%5d=m%(%uJKpgE;vDR_r3-Sk(~ghT9OB)##L;o#>#MffxzKiEIs= z&d6krO^aQ5`;bU;of27FQsojq3VKHA+yHIiLF?!5Bs(qELLI9VCSi9TNSRtQMXb!# z&U(i@7wD7$+K4Sl?Cpd&BX%LV!BQ#TkdCE@{u$Bc7AH_6yi5cn?cfZv6=lm z){X%v%UJ6I5>|KEFD52UV!3w`r>5|Z*vXWR39>d$8Ok{*maPeVerSJuXoD<(tL3pl zKulGhP|uv2!Vc}EqT|x~hPp_UC!L#|5z9nal`&Ys0s-du!7k|=L=`2(h5$`YIX+UN zq)#pM)9E2A`W(?iBMQLGSemgEj643IEBl$>9Lt~y!fq~l&~cC76_u!K`#~Qw@Jc=?{PoSQjisq%QPsfN6WBimpsCGj|5}_za%OO=voYh#L(Ul~s8U9+xV zxnN1$GQ)+f*eauW5>CNQdOytiezb5QfhC34io;v5Ps1U@uX58|M(-{9To<`Zrmu1r z4KAxPvYdN`p`qj2_6&9}eCKA0_mW-*7tjqfNW-mOP4tNFlHkAJ^lN zsHvUr`>3*MvE}vP-O8*09g?r7ug<=~yZSmR@JM^~+KQ%ziL>*9sQu}IgUNbBKPO<$y}@3$X0s_%0a z^!x)y7dssAM>MVkEYYSpeXso#GYj$0l#TAVGF}j?IlVo3*uc1v$!`?dG2C1h zoEkqghlnPa1K*_$E9TTOiG59NQ+_(rTJ13%XxF177aBkIfx=jG}*cvarG#;J7eRbJ7Gq7WTsW-e?3 zM^58pVe5DfPKqtxdl|i9A@7GYAVdW*qlyLxS00> zXk3q5ZPw)nXK=WRT}spLnz3M2*zOwmbQ>0hUz>uBEi|gK88X&xY?8!D6Sj;U^+`b- zJ5h|sKNZ-PkE%7rjWQRd0M?9bN@Qo_XPhhrDT3u&7|2}W(`r)Ad;RQsjto&+NZLne z3x_GHgGq~s26?3)po{C<6}TvNiM+od?*@5`Mhub8IHmHrKqx(Z}z zV1)A0Vn8CfK)OXFnK|8BXp)XZS4Sr z{w*je_p`erqW26sz#B&urXEQ!jxYcumRb7suvbxrZwj%aO1thQ^O|EQudQCUAX z7HK#UDH~if87oUyIb%sF(iK<59gR^(x zeIn9yJXUccQhWl(wqJSS>I<*8-0Hm98E@{6Hg~`8=uR|kiZ|_vHtmTu?VB%}E15eq zcL8P?3%g%Euu{{yWL>-z*>*UxsV`R3A1Uik6con`TB8N6i(gq9x?8aG17{8EVrR6r zb4gr2`u54UPQLf_e|G=O9jkq6j!!siBhI!J2uAI&=Qw`*c)a6F(T*?Ot$acgWp+gy zcO~jt<8>X;x{jr>Hv_i=cP_pA!aFaZqe9IBL8H3RNUtbgj-qzK6=Nxd?`}*SgiC=q~Or1i6TB_ zH1pyy?wE=DucoSFwd+VU-@*yJKS?xm2C0i=nvix*JaPgJG|@uyG@KGBu~H-!G}>589GEf6W8x%OQzD?&HRD ztyj}!6Ki$dvATADmKO((5L-IC$b%c5Br>Dn?9;WKO! zEjk(4=0BvP3A3*MGzDb1ocCEO@|WZQjoQMt+%hm^Tupvsl4quL_h1Oxq<=!`T^UWn zC;FK~_8ITaPN_c^JiFWm{v>Kn=7pS^DiogGq3Nj<4NmG7`r z2IBkv(ZOj7bne@h+{8&A>OY4SqkrDR1KILTt=Bv4AVv_z>=5-JI& z(wpQRCND!UC3zEYl_qOu?7vF?o(g-Oa+hVMArQ#o zs9jc1TNr$E3ZzUrWiH=3Ve%%(B7Ov zXj!7#MM7xH?;?cWQ-n|)-<}J-wrAmMul25!H7tY{4_&|bL(|=|jvwdW+4Y{`okxGs zcX#_ysD>JvZajDWxp@7qX#K8u{hnz3o;$7YcD&Q^ldf3(vAF?QzAo%~ZF+IXtFtRj zt+(oL*2kNAqD?*Vrru~%?;Y{oiFYP`;*T|*xNiMOjqi>&buU;GWmWOAj%ZoO(pQ#; z?w0LMG`7SWcSIX^EVtgVzGsa!J`OpqvN2NG_Cc+%(!70nV7VuPRcJO`YK}|wF8QJvZSk04>vL_!b z4u4QtmuTA-Z`&Vj+kfZjSlhq??xbj2>UqDmTkHB0ksZfl%_kytCm`&`8+S$yx0{jb}hBuE8ezh=PGJ{`$+*;T=PIVn^ZP04p@)3bAM-Zl3(B7jmO`Y zl#<_i$cV>(Yp*&1ogxN>=IEWIk~I!(7us;rCypDn;9ut8(&r1sP6&7$Pw!$$Ihpt{7i>FP&v%K zXu#eRH?C(q;kRTHeg zKsvn%mh>$IWUQZ&wUl7gi>M46aUA+j!OLi}HL_J%Q?#IIar4~**M~I?SNgy6#BUc` z>G5fhuDyD_q?h}TwnNtRwU-q!461*Im-R5;&pn4l|E$RfX)+Axq~u8h_w6DxL6OSH za4emI1DcD~TM!;cI$W%wzo$mMdxpqQX7kKMjZj5wUgqyKsiYhKXmg9ctXcvY34;Jx zd3C(JGg{tB%2}E5f5RER5-Ye)NAayYy(Y*!>;^M&4z`YHx4H-tM z8-NKD_pA|M!u?44iz2>VxITzfVNeeqGT*@wN~K#QQYf3=BN0W5jKJi z6|#xoLi%I{E*;VG4uVUA2A6vU^#qd!?Y!ket9ofsZiY(Zi{POgACd2IEJ2{&#Kh(X zn}CpolOpA5ZB&E1s9{8kXc%32mrbWg@L)O(wuB8>apoMYDb6{K3#v9+$paAvi6n2p zoN+qhWztm&Q0`}i#&E<Y)a|V(n05P=ufP0t z=+q*PM$C`b?DO{T7D7%bF2{UPQhu#)zVN$_grh7QG}`1=gp#tZgrCq|RU;`InqT;p zMl?vtv72~r%TSU@^$CI-q4;9e19Fv9@eV zrS@CCDL8E?k<1-T8%jS%NJh)_?6)5+(_I%#bg8wF{ zks6<@sMXsaRC~zO70*n6oieC@ty3j zJX`xRROn8TX?H&fbOZ7Oar8VY&cG2Bs~n^MN_qpSQL5Xxftw%h@LSG%w51nONBU6> zh$ZX<*>lvMazuYtfF+M8rsX=pQHOq9rAEhv=s>azHu>ij;W)Uj#sK5fC$&z#%O-$G)`L+ROF2<9Yop1^|F~98Lz76 zQpRwKjLtYRCSoBMp*W+V<&<0bXFAmb3$Nb zWC{j*?cT0)T|(>0Q^y`ZHmJc1X1X3>h7Qt=!v0lv?+>FqSooEqr_zt%>4BHD<9u{T zz1uwPvIt$ogbsd1diXv3FnLs&D$zgLHWC}N?0UXwpLYVkL(ooRF?BPSW+j|^+`NEp zq#rWTY&BJm6P!vPK@Z>5%@1g*ytAD9W_;;iQOj!51hF(*(CPG6Jv;$^Stn~e{Iz)O zt|>B>K9hA9-a1p+zn~7JOOooHceB*?6cg7&l=?w4{n~DL>&fuB7J91asK~gW)59o% zU00u_5?Dq|h8dYWR`grmSJ9LpF-}};g5$H{tKqn#E9&TqIkrdm?Tnkci~|Fx2+LCZ z3{$_M_VQAig{@>y?u$iHu9LdlqpF`kd|mkEQ??+dOJzpDe{<>@g}`JMcx5; zxFrCW2ufe2?`Oy(Fi9TrM#wuyo{ziFcJ?QSz9M z;#>5_^t7~YlxTq@{TK57J9$Io{hT}!i;y zwhf z&QHcGI-?bxOTkFR&S-J>Y97V%>%~&htNC1h;VX4l>*BVCsI6hmPGOrZmi?*fRylkw zUAkj>mw$(UxA2|9=(d413R^2OT54DMLQCUiccP2QYbJ{{TI6BYJQ z6WZ>GRBIbf8B|mzs_S6}d1LJQ*bV=6|Hrmwi|a#2>8b@!K*L$Nnn!O~E0n%6c6IEP z$*YsAl!kg+v}VWK{i6mWrLdv#Xf+;d6?RM2T1~#CdbKLwvMtiSf0e`YJ;dU%T4lHF zjBGu;%HjE5XpJ7LNZb`^+e_)X?jQ^g%Io^jR)V__wV#z(49c2aQprZD_D4&5qqYNU z79?D)%Cl@*xU^(?lfTWsS$Mk;mX~WB!tU1?Exq^oI?I6J{{AhN!-o4$8xGLpG5nlG z<9$94Ua85l^?ts|vWq<7eyPDycHe5Xl;5v0Tbl0|;HN3>^A>nTDUXHO5LS@~uateN z>DH=@7Wt@{YZKd~Nm{X}g)ktGf0Cc&F=wSW##MNFBi52Eq?7KN0K`}jOmGoKlce$n zrjjh8FgD@Izv)q+eUn)*D}}|f<+P1O(xsT#h(SL2{*z=Lz2hzt#`lsot=42g|KRB( z>dAKYLl*M&qmNKC_mTG)dF|veHke|5|3G1>Ev66Y^H=1t@!Uagv z$yjBKaAg=RwJn*cdvEr}TlPj<_TK4x_ryCV;`>iW_n(fKhZ1HUowVeCY%%2Pu;V^QJ_FzX1GvWPlmGw# literal 0 HcmV?d00001 diff --git a/venv/Lib/site-packages/dns/__pycache__/tsig.cpython-312.pyc b/venv/Lib/site-packages/dns/__pycache__/tsig.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..da0de6fa77b24b8a8dac8348c56d5445353c3ae4 GIT binary patch literal 16800 zcmcgTTTC2RmR0?J(GA^T9tKlZjz8D`3VWliiU*){)|ocD2rGcH>`3~1#H_>V7m#n9k3lwf$bsKPQZ3O1$HsPE&^;fSFEi@ z34wY5TAT;<5@-oPy?Ia{f%*Ving=ZfTACkU@8QU>bR;7luaUEDy8dmi>ur_K~HS55t*05^VfmNenZCVFbt%k*LRsEZ}3U(7$ z$wHgm%xz@rxN5eZt6{frwQK{o31H;_W7w@>M}HmRutwmprrQREQ|bxK53ptpwuQj9 z0jx!XHL!cw*6VctR<`Xr)!)drv)kdR$xg8y><(!8*_{AyCYZYbbGL@MZNlK+8!I{5 z+sB;k>*;2M@v*TeFSb2E3VK5_9dfO(FSS|>&$ARqfe@omfb|<;5~E~29AwXiN4eO( zvqKyc8s~W~A~GUCn6SV^q9SuCEDnVuj0oNMvPOZq7#s<+VR3>CYwLaAB-sLiNN|)3 z1SCfwFdAjYN3iV*1l|}Ajwn6WK!A;g0)h9bRV3}v_>lwWUJ^K7I5##fa3kl$=%t7- z6dnr*qrs8Dz_<{OaDt$LTSG%ZaUd$@061=>RUO!heHXdN#dD{_1LuUW$hD3ILl=VQ zIpG`|5w?p$_|7UB7NF`BHxb*V(Zh40 zD9@VBb&=pLCkgs+P@`}a7GKgRP>KGu0SD8QK*2?_!;808sYH+gqC z>=u~w9IR$gmt>Gq$*pz1y>PY z%&=iL(kL<)BGF6gt}>T62F@6(PgFS)h1KqSqS|Dl5WX&=iWQ=Ar-E#7K4~N23mhR0 zj--5YkU#+hk3NBwNFY{wvEjptoK^&_i#Df18xeR7g)63!MWo-iQnV3iz^W7ew0+zz zlZEhgkyWCS#g9iqK`|VS#182a7aWa_gLIA#GNLw=$&=0BzKk_UG*-{%6WuKnjqq5- zi8EbCTF)G9Zv#Q!+I70S?Zso=Z9TnbWA>rZV5n6X3hs;et`D!gKDf8tWexM*nw3A zR?lHI36=hmH$l%@wZ|GjtD4Xn?>Dn1s4Xnm*8SEAi{C0)yZidi0_z65L2)`5C;(_B zBAry>(XkP36gZV^Qyasa9Rf88XA49;96%i0^}QY@6pe`7Ws%_oF*q<17KS*MOs4MD zsbjA&qoCbDJ26AS2n!2~MJE#<&Q}MM4uTs-)oYg+tL1R)EnwH%MlJ z36zgRtmwcn&cSnUjKgef3j-o>h`c>0TM3{>n6L;ceXxz7hZMsIc7md?ZG^woHmGK) zI31?~(2@&_PpZX2u?Be3a3?6gu~+8RS{MzciyJs3I2H~li!GVhpcs@)sNMVq$%F*( zXy*$!2gC5FXwgy$N5Wzt5Gz);x=n=)<5=XWNh(|0k}xgVHfBr96Q;NA+0xpC>1THG z_BCIFUy}bd)MXlHDJ*e1Rs;-c$bIPq^*-I}r+HLa?;Ch@JxF^Et(NgI_#`goRw!vm}~rAOT`jw&cH5)Gw-a$A|ZkK*wF z`i*3LrK0nk7|>)n`40Fr{3Zbrl$AH(h?qNAfoN1`kDbJ2O zrTJN+4s%_mz)l;GcrX^(y#an4gw5iY6=u6aP z<15r9T3!INya2_rZC9`clKt4_5I2VMoY;4Md7mX4cX

5XY|FY8gEb zhUOyyslV3HL@gHYGxqvzJboQ-#R_|PJ63oa`CVAGV|4&42UbMU5)&0ADUa0;;ZHy% zcb)peU^UxTyhih;6$fpuTCo_+2Ul$t^ClS&M~C1~E9z-p#h!ez3m)>!^NbzU%d*TMPXs#MXZPvI^L|!LF zf2s6L*0&*X5)PES zmD)nX{J6YiE2}|(5dmWu&X=pujU{dvRykS*$k4n^)7z+FTi#%$ncL=JZOd|_hVcLv zl-mluS50vfumpr4k$HyES1c%3&2f4d^+iFsYKdEh(V8wOadS@2PoOx|0+t{SjWqQN zFo`M#{f1s1m%#6o9D`xlbsl&?lq|uBaEaq3BY57DgGa?o+$hP|)%%KMCjfC`jFU`f z!4%@VVFt+}a3LOWO~^Qs4VaC1VPKRQLn8ti!ykiz{Bay@mS1ERqZhb{tmaXqAvZxG z48

K&-CdMC)+r&tRNz6)KRiF1x)XTe>k*x;0h0HCtRh>z;8hSU>Q6So?l$#^0Ip zcmA&L5BjfOIJ;@B4!s<|egLQNn zXsCPr)U!Xb22c*Qpf)4f5JzGgmw;r7R_1 z7~JNPRioOHB{$-Q;7=<^9G2B$aw78MNSui?>v1S&1-K61adv=fJS9(yHRsfavxY$f z=Yqbf+*q~6x%J_!5pX=v$EYK7zg&qvoHYTC7y7p5#;UEby}y*W31zGWbkznH0_y$c ztPSdl3A^7R+0b=D&mk5iMlU|f!Gb5a3}oQg1neLf{v>jS20J8d16@!%061r(1ar8LFUETT0)ir#*M zGBL77ltLevR2;D2^X6pD*b6Xi?nwQU=hUB#T{90TR2|7Tta7bPG^ye3sK=GI9)I%n zakS%ga9InyM~9<-Mg7{KSoqr96Cfo)c53{l>iH+`7zcdC&H) zZP9nvcCX|k=Mr-?adgR5ldWJDO6Hx3qd)8UJU3#A=~8;D6Gz|f$(B}RN*hw84VlvB zRB1DD;GCWt``_B1brs*}d8=n`=iJ#_ugt$PcWQ1^%GLBv&qE{S0-0Q{$mEylWpBfx z=dS(EmE_^R`Lhyrg|QtYN1~RpIy&aAz2PhE0oy0Ge zEHXRug9uZ6;|tJUvjFI(#EP^9KmqxmI0i$6;7yj4WlHK&C3Tq+f2zcvF4>l_WECDL z+nB6wn-gzM%ug(K&HqiZYD=!lFdNvz`{h@;+%sAvYYu!2QVz!LKt#(yWWC; zKcIP;f+go;UZYi;+ax<*A(&KhhoYlks&D~iF;Kd}0+B$$ejH$gLn`3TY32?(ZFxK) ztBXGYP-ub*7RBwo5uOVF^g_b8ta{rEyBBRso}J6Gzi6b2cac@-^;Z@e9Vpl&J1%&j z??l&5$%828bk#pr`iKiK``Zpg7}*Qws*)t_$6G6b3l|Y08zr+CugISwz`unr5&%B=3D4%P*)i&R@Sf8wHTQ9hHx^Z^f%(jOaFC_LaR0Xd-!~1pT&S8Ahh$p=roGcxe=sUk3R4i+( zqVnjp@<06sT4_DAX4nJkT~O3BpEs-S*FWjoM9rAcGdJ{%*S(bHG*kpi2)z5));WqRU16pNX!0&H`(L1xE*H2z-mF!Tv$gQhZ*$De080#I^#~( zdf_OXR;P%8LsD3W+JB7({=e{zyNiM2g7R&<8+XKDv^w`w@~;!Vohf)JJjR`B+y(k| z9Gk*7_kGj((!L$(17+%pt-Q#=ZFGbQYS$1LArC^>8)Ewy$S0O1@xjTpCW69|v2l?B zg#)n}2(Sqb^rD~R(MLbPfD_+FuFq9<3l|GHpyFRE_W}xHFpO6sF9%1)Ibv%bV0x}Z z@tPojv4E>+%8Jn_GZKxQUxT4zrXs3l1`|xoO3;j8g8s;``NRX%+`Tcw<{n}f^PAx@ z{PouqHG;zO>mPmnBO?_vZ9dU+A||iE^W@s%DA0rCJi~Ofx9@I;t6Y&2V#duUPWVeC zCq}lF`vqc&f#!j$w-7`rFu6c7j&Kpl2^ZPt!x2bY2(!dcmWsrvqPOAphvQGf`}s3i z^+F{%@qUrSiaAy?5010vImt{GM{?v=j6aNH%^Y4e`(2Xh5?r>(ff6&>I?067NKU+m z0pmYF%=eIZSu&4~kBDKIILr&8QwrsuPnLhvnK0LPQH2a?Ta_AOkJ7Vvrv}V zyeqYNSK70C$+So5Oo-E6(}xq6mrND#nyk%v!!hNU-aWf-W?#nVPx<_5-?k-Ni_-Cn zj@izc&N=Zneeb>g?(3O`y{U%1Y2Usj+x~(93ooR7jZ3zstkpRg&w5H_t2tqUnBrXB zO!I;zRkC&RWVWPi&H?7+^r;8wmoV;saO|^CO;TStC|gnP@T}zpVBI-+m`uEey!7 z76#;(pCJJWvVnOz3sp~^;v)NvIXQaDOTNt}7>EUT2chv1HBTovHa0S$XhJdz(T3D{b{?bRGIPI!ts7WPV}%?p^Ez@p=pk4F$^GHT<30`x(Uga4C$Zywd|0z`P!3tp11wTN1{6TU+Cy-%1~C!nv)toaj9IkS%E zpv+vQe&JKjT&?2gewCRuPvIIZZdQ9C{Hwb%G2oBez&%)Ce7)R)-82qwQQNuSxG4ty ztU4aXY|ZtlE#l!=#3uC_zK_<+S&thCFIdEuH6z>9r`#{RM+d54?@-49^}E$}+(82R zR?rjHxJ}a(5Mda`LtRi{{*D3=JgQ#_fo}|2k%F`8jfHn8x&kzXEqELj6s+MeX21|B zEI`skcrXmvmU;e5@I2@U0TPzMS|B#;W1ftX4W-K2<57N8@kq`B4w(w9je#ILGJ)RE zD95TcgDjXKzw#^?+6N%hi{nXBG&(M$xb@e;?$=})1NFs4u|%7X}b z$a02g++*|hsY-`TF4K}zM3Bcx@~=s@H7o*|>1iw(V#GYa7|H__C$e|+I<#U{;}J{~ zjmWPit_CPLmSY;&UVjb$V;Cb@#v|zJ5GO{mb81G5WY<Ud1Jf~p zo)!u6Yx+#fP*C@R{}84v1r8O)p#rtGf$~<&*38t*iJ-UY7GGR=HC?jv&6AVg&syEn zo3F)}-6c0hr$#gGhLpP@X=})uoQXHyJU!VBfqL+31`@HG$8VjQKecFDJfE)aNO?N$ zmfjt>SC!mvg7JEuBt*5C5a`!iL|sjBAt&gNxrUD8#T zE%M%Yb?VidM{f1Z_hhPDQq?W@i&~bw)k#-%L0@aCy7hih>$0~d2^TAC@!C?=ZTE}X zmc8{!SN-Q+h?wuY+x~IqN1dta<7sbq($&3ebtKv|&gzu2dQN~GjI_00c3AFPcP_gj znx3^fescNE%ZeLRns6cD>YG>R%(p!Ap8M9uWp`E5R<-OXnQodpvQP$Je&O!!d*-y` zIM@KPLzo!9F)=lCyl6ez;oo%klDm*2 zlD;e#y@IO>9X+o~us$j;Mx{Ga|LR25?jY0X*xRz{3%QjMhM1>no~UZc=42eC$7qsE zj>6<4D6&A7P)`{%0j~x_4EzzSP}A@)V1?mk$lgP*CNFzWc7V_eI)Gfi2O7lrw>fc7 zZ`$r#nPtz0X<;@t6H9yQlD4|fJ>_%8TaJ0h!jbpRyn80?*_E`(mkS|4){0_$QLA+T z%4LLc#zFXJJNPFUFrwg&o#cecxnO)33=EmH3G(*+dq7&g-4~~GyRGg6zbVg-(b@0e ze2Y?fNzPZr3oE!lR`e9)Txt9lH|V_UNtfG(Z;L*voOjiLnPj;u^SpN&b>8_j-=|$c zV#eZM(}gwXckIj$v((OM+2DXELjHNUnM%=#)pM}G+{XsM3XFXcuD3f zPhSdS2NE;fJ66&6im8& zHip@-pjZN|Jp@aKK>}0N%)DJw?qJ`73G?q@ zB}*W{y#eWZ4tcY%-G^KCA`G0QK8MTv@tdKvt8Rrd7VXLwm(JQ}>~k;O>YwjV6*sOJ z0r(jJoion4@mulvc&d2YiWz_q;VLO3c~=eZZGU%r3Ua3GX1n8YfsX+RvJ;;{1O8DL zZ=#`9SmZ1VaI7djM-X~Vzcvzw3-R-JTA3&(Fo?Ol2ky1axiE*`y23nYZWR>uKG*#|oIgvZn3q!N}J3_|QXpsj~|_4^Ti`8}*~QwX-4AJ~PDSm8f=2!tR}ERw&&wm9l`4NP)T-(#KG-E3{rH&@F$`3)|9Um!EFdVl<2$h+SF^AqQ+EFBSH@$ zw7qa_J3^1s)5h7NnW9W_bE>!*2OlRhZ=bHjhwU=v1h&ce9n*en??Ctodd`jyC+LSR z2*TgkH?=S0tVua*lC}HN&i#`%ywf-_bt2=cO}T34uB2VtChb|U{D-E7GHxd2W)^DF z?(LIKU6{9QwrQp*$Dq`iA5J^u#-o$egnbHiz;f713@>82!A0%F(fPViZi zXWFY&zUdeBTdu@F*5pcr9+o+a43qY+s;lWuUr(G8HJ7prm9K8shRuS#&5>Q%WkXm`Mo7Tya@9~Z!<4nC zVzY*cZH~CKS@3BKg~)j^4|%b@g+O6)v}}G%PK+uxf6eh&+#x2%#2_b?#2d;RtLH@c zvY}ujoEkNhMA+1?YsQG0iWo^b5g8l>0E%H0yMxgY*&Ni(QU|e11YN@*85>iyvG4_T zFl@l)f+;zAO&(T^Fl_NNg1>+?luBbiAfeTMg6l3+6DVJW#9b3-p6_XzxjB6^x99L{ zB=Eh=ZT}=UsjOGm%=o5#Yt=2;>XwC$Ty^UtzwWP_QXcd_^Eav9dp)*8CA4c(JxrcU4Kd(m<@ThTQ6^{lYfu6|{2uA#@V$ybuK076C0MBfJBL3kHb zTWEyDo6^WYkw#+7Eue^hC6n(8WWr|P>c$IKiY($5mv}O~Nmil7;!O7$DA7fQ2U#AA zbLj=HB~P#V(arPXKG#P{?%=D&@wJn+>pvrCbtXv`Ef)7~TNvMo!rrl8OBnJL_wapz zT0HW}_o0?;bg;w4qxY^rE9730_hUP$yiHI=&MMxXcIs#=acv?VAgzC3ziki)DPvJ* zTPW}Sv2DfRaNpbXvgOp0=-0rv5rb+5nT3Z5emsiL8ci;(EaYZBUOb1TQTLSY^yIl? zAb#Bnj6+ZI=ypb~rO`>@#)BhM}&6 z9+eYDcc?hl38uy&s3y?5ShprNXT|1)PjX^lO+1nnkL1LoYvS>&cswVbT=kvAkl zz)J;hFD6F^bEL=To@7k^W}GIWbSG#uJREtIaTS?D{#hJ~Cqa0K=qfZcZ+u-zJvN$9 zx^V!8NaBrgsPZhsFn=CIOv_7D`8)dfB@%x_VqOrLnptkPf8oIFmAR@sg63k!GW*!~ z$oIJNQRRy9bo|NqQ|pPfdgOfe(+eBKAb)TVQ#*AD;?z3VIv<=1F4nB>YtPmN^9Tl( S4=x)kJdLVuAaMfNua)2y%n literal 0 HcmV?d00001 diff --git a/venv/Lib/site-packages/dns/__pycache__/ttl.cpython-312.pyc b/venv/Lib/site-packages/dns/__pycache__/ttl.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1c8690186c7bfa7a9aed295ce56f3291feed213e GIT binary patch literal 2487 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 literal 0 HcmV?d00001 diff --git a/venv/Lib/site-packages/dns/__pycache__/version.cpython-312.pyc b/venv/Lib/site-packages/dns/__pycache__/version.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7bba43fd5cf4f13aec0adca55a65978decee3af0 GIT binary patch literal 819 zcmZWlzi-+=6h347Fb=lUq*0Rokcd=JGC(7>>Cje{XoOHI8Y*nUUy33Fx<0~m;j$t{V7Hm67b`1X?4`YA}ZuIy?Z_hk`72=$$c-lsOlWI_Dx&b$?si zTI@uSMQZJOv1)~ET*fBBp(eITiJnVVp=wvmI_0s6kB&NFgHJdH8Uy-)jub|*>)la2 zhHx{CCPAhKy_}s7qnlH>MGb4L$07rXfk0t@OLF|#EUM=V(vIM&wfodKBy=XD)*!fS)T^H?QY_W7 zq+*t{xdtg&n2>oonlGC5T(y3aV7_wHGe()##8%Ttm2w6t)p4|D=8wz|m>BdeAG&{! z)!J9m^GX#p%J_x7N-x6RjSMMFd5+^Q9sn*ng&-hz#jzCdKwItx$214Wyw-m_NFs3zzZO1u$P<$0bFuT_dEx+GE-=7 zQi1kwhuchFfJ}!P>?Ws6Kwa+VzPvx)IeGc**+7Y$uAFb2-5Ds4&dWc{!IR`bN&bus Mls7%uUZG?C1wGK^(EtDd literal 0 HcmV?d00001 diff --git a/venv/Lib/site-packages/dns/__pycache__/versioned.cpython-312.pyc b/venv/Lib/site-packages/dns/__pycache__/versioned.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a7c3a4ea4543bb8ab1c5a84460542b1e203268e5 GIT binary patch literal 14729 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 IW&-m60ilxu&Hw-a literal 0 HcmV?d00001 diff --git a/venv/Lib/site-packages/dns/__pycache__/wire.cpython-312.pyc b/venv/Lib/site-packages/dns/__pycache__/wire.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..518bfaae758d57ddffb930ce6f324ea433733585 GIT binary patch literal 5493 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 OWp>wnH+*bw{Qm(`n{D_2 literal 0 HcmV?d00001 diff --git a/venv/Lib/site-packages/dns/__pycache__/zonefile.cpython-312.pyc b/venv/Lib/site-packages/dns/__pycache__/zonefile.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..db13074fb2df3d35cee9f18b28d875a22b9875a5 GIT binary patch literal 33874 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 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/venv/Lib/site-packages/dns/_features.py b/venv/Lib/site-packages/dns/_features.py new file mode 100644 index 00000000..03ccaa77 --- /dev/null +++ b/venv/Lib/site-packages/dns/_features.py @@ -0,0 +1,92 @@ +# 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) + 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>=41"], + "doh": ["httpcore>=1.0.0", "httpx>=0.26.0", "h2>=4.1.0"], + "doq": ["aioquic>=0.9.25"], + "idna": ["idna>=3.6"], + "trio": ["trio>=0.23"], + "wmi": ["wmi>=1.5.1"], + ### END generated requirements +} diff --git a/venv/Lib/site-packages/dns/_immutable_ctx.py b/venv/Lib/site-packages/dns/_immutable_ctx.py new file mode 100644 index 00000000..ae7a33bf --- /dev/null +++ b/venv/Lib/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) + 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/venv/Lib/site-packages/dns/_trio_backend.py b/venv/Lib/site-packages/dns/_trio_backend.py new file mode 100644 index 00000000..398e3276 --- /dev/null +++ b/venv/Lib/site-packages/dns/_trio_backend.py @@ -0,0 +1,250 @@ +# 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, socket): + super().__init__(socket.family) + self.socket = socket + + async def sendto(self, what, destination, timeout): + with _maybe_timeout(timeout): + 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): + self.family = family + 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, local_address, 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 + ) + return _CoreTrioStream(sock.stream) + except Exception: + continue + raise httpcore.ConnectError + + async def connect_unix_socket( + self, path, timeout, 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: + # 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: + connected = False + with _maybe_timeout(timeout): + 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/venv/Lib/site-packages/dns/asyncbackend.py b/venv/Lib/site-packages/dns/asyncbackend.py new file mode 100644 index 00000000..0ec58b06 --- /dev/null +++ b/venv/Lib/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/venv/Lib/site-packages/dns/asyncquery.py b/venv/Lib/site-packages/dns/asyncquery.py new file mode 100644 index 00000000..4d9ab9ae --- /dev/null +++ b/venv/Lib/site-packages/dns/asyncquery.py @@ -0,0 +1,780 @@ +# 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 socket +import struct +import time +from typing import Any, Dict, Optional, Tuple, Union + +import dns.asyncbackend +import dns.exception +import dns.inet +import dns.message +import dns.name +import dns.quic +import dns.rcode +import dns.rdataclass +import dns.rdatatype +import dns.transaction +from dns._asyncbackend import NullContext +from dns.query import ( + BadResponse, + NoDOH, + NoDOQ, + UDPMode, + _compute_times, + _make_dot_ssl_context, + _matches_destination, + _remaining, + have_doh, + ssl, +) + +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: Union[dns.message.Message, bytes], + destination: Any, + expiration: Optional[float] = 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: Optional[Any] = None, + expiration: Optional[float] = None, + ignore_unexpected: bool = False, + one_rr_per_rrset: bool = False, + keyring: Optional[Dict[dns.name.Name, dns.tsig.Key]] = None, + request_mac: Optional[bytes] = b"", + ignore_trailing: bool = False, + raise_on_truncation: bool = False, + ignore_errors: bool = False, + query: Optional[dns.message.Message] = 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: Optional[float] = None, + port: int = 53, + source: Optional[str] = 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: Optional[dns.asyncbackend.DatagramSocket] = None, + backend: Optional[dns.asyncbackend.Backend] = 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) + (r, received_time, _) = await 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 + + +async def udp_with_fallback( + q: dns.message.Message, + where: str, + timeout: Optional[float] = None, + port: int = 53, + source: Optional[str] = None, + source_port: int = 0, + ignore_unexpected: bool = False, + one_rr_per_rrset: bool = False, + ignore_trailing: bool = False, + udp_sock: Optional[dns.asyncbackend.DatagramSocket] = None, + tcp_sock: Optional[dns.asyncbackend.StreamSocket] = None, + backend: Optional[dns.asyncbackend.Backend] = 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: Union[dns.message.Message, bytes], + expiration: Optional[float] = 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 + count = count - len(n) + s = s + n + return s + + +async def receive_tcp( + sock: dns.asyncbackend.StreamSocket, + expiration: Optional[float] = None, + one_rr_per_rrset: bool = False, + keyring: Optional[Dict[dns.name.Name, dns.tsig.Key]] = None, + request_mac: Optional[bytes] = 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: Optional[float] = None, + port: int = 53, + source: Optional[str] = None, + source_port: int = 0, + one_rr_per_rrset: bool = False, + ignore_trailing: bool = False, + sock: Optional[dns.asyncbackend.StreamSocket] = None, + backend: Optional[dns.asyncbackend.Backend] = 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) + (r, received_time) = await 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 + + +async def tls( + q: dns.message.Message, + where: str, + timeout: Optional[float] = None, + port: int = 853, + source: Optional[str] = None, + source_port: int = 0, + one_rr_per_rrset: bool = False, + ignore_trailing: bool = False, + sock: Optional[dns.asyncbackend.StreamSocket] = None, + backend: Optional[dns.asyncbackend.Backend] = None, + ssl_context: Optional[ssl.SSLContext] = None, + server_hostname: Optional[str] = None, + verify: Union[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_dot_ssl_context(server_hostname, verify) + 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 + + +async def https( + q: dns.message.Message, + where: str, + timeout: Optional[float] = None, + port: int = 443, + source: Optional[str] = None, + source_port: int = 0, # pylint: disable=W0613 + one_rr_per_rrset: bool = False, + ignore_trailing: bool = False, + client: Optional["httpx.AsyncClient"] = None, + path: str = "/dns-query", + post: bool = True, + verify: Union[bool, str] = True, + bootstrap_address: Optional[str] = None, + resolver: Optional["dns.asyncresolver.Resolver"] = None, + family: Optional[int] = socket.AF_UNSPEC, +) -> 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. + """ + + if not have_doh: + raise NoDOH # pragma: no cover + if client and not isinstance(client, httpx.AsyncClient): + raise ValueError("session parameter must be an httpx.AsyncClient") + + wire = q.to_wire() + try: + af = dns.inet.af_for_address(where) + except ValueError: + af = None + transport = None + headers = {"accept": "application/dns-message"} + if af is not None and dns.inet.is_address(where): + if af == socket.AF_INET: + url = "https://{}:{}{}".format(where, port, path) + elif af == socket.AF_INET6: + url = "https://[{}]:{}{}".format(where, port, path) + else: + url = where + + backend = dns.asyncbackend.get_default_backend() + + if source is None: + local_address = None + local_port = 0 + else: + local_address = source + local_port = source_port + transport = backend.get_transport_class()( + local_address=local_address, + http1=True, + http2=True, + verify=verify, + local_port=local_port, + bootstrap_address=bootstrap_address, + resolver=resolver, + family=family, + ) + + if client: + cm: contextlib.AbstractAsyncContextManager = NullContext(client) + else: + cm = httpx.AsyncClient( + http1=True, http2=True, verify=verify, transport=transport + ) + + 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(url, headers=headers, content=wire), 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(url, headers=headers, params={"dns": twire}), 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( + "{} responded with status code {}" + "\nResponse body: {!r}".format( + where, response.status_code, 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 + + +async def inbound_xfr( + where: str, + txn_manager: dns.transaction.TransactionManager, + query: Optional[dns.message.Message] = None, + port: int = 53, + timeout: Optional[float] = None, + lifetime: Optional[float] = None, + source: Optional[str] = None, + source_port: int = 0, + udp_mode: UDPMode = UDPMode.NEVER, + backend: Optional[dns.asyncbackend.Backend] = 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) + rdtype = query.question[0].rdtype + is_ixfr = rdtype == dns.rdatatype.IXFR + origin = txn_manager.from_wire_origin() + wire = query.to_wire() + af = dns.inet.af_for_address(where) + stuple = _source_tuple(af, source, source_port) + dtuple = (where, port) + (_, expiration) = _compute_times(lifetime) + retry = True + while retry: + retry = False + if is_ixfr and udp_mode != UDPMode.NEVER: + sock_type = socket.SOCK_DGRAM + is_udp = True + else: + sock_type = socket.SOCK_STREAM + is_udp = False + if not backend: + backend = dns.asyncbackend.get_default_backend() + s = await backend.make_socket( + af, sock_type, 0, stuple, dtuple, _timeout(expiration) + ) + async with s: + if is_udp: + await s.sendto(wire, dtuple, _timeout(expiration)) + else: + tcpmsg = struct.pack("!H", len(wire)) + wire + await s.sendall(tcpmsg, expiration) + with dns.xfr.Inbound(txn_manager, rdtype, serial, is_udp) as inbound: + done = False + tsig_ctx = 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: + destination = _lltuple((where, port), af) + while True: + timeout = _timeout(mexpiration) + (rwire, from_address) = await s.recvfrom(65535, timeout) + if _matches_destination( + af, from_address, destination, True + ): + break + else: + ldata = await _read_exactly(s, 2, mexpiration) + (l,) = struct.unpack("!H", ldata) + rwire = await _read_exactly(s, l, mexpiration) + is_ixfr = rdtype == dns.rdatatype.IXFR + 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, + ) + try: + done = inbound.process_message(r) + except dns.xfr.UseTCP: + assert is_udp # should not happen if we used TCP! + if udp_mode == UDPMode.ONLY: + raise + done = True + retry = True + udp_mode = UDPMode.NEVER + continue + tsig_ctx = r.tsig_ctx + if not retry and query.keyring and not r.had_tsig: + raise dns.exception.FormError("missing TSIG") + + +async def quic( + q: dns.message.Message, + where: str, + timeout: Optional[float] = None, + port: int = 853, + source: Optional[str] = None, + source_port: int = 0, + one_rr_per_rrset: bool = False, + ignore_trailing: bool = False, + connection: Optional[dns.quic.AsyncQuicConnection] = None, + verify: Union[bool, str] = True, + backend: Optional[dns.asyncbackend.Backend] = None, + server_hostname: Optional[str] = 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 + + 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(where, port, source, source_port) + (start, expiration) = _compute_times(timeout) + stream = await the_connection.make_stream(timeout) + 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 diff --git a/venv/Lib/site-packages/dns/asyncresolver.py b/venv/Lib/site-packages/dns/asyncresolver.py new file mode 100644 index 00000000..8f5e062a --- /dev/null +++ b/venv/Lib/site-packages/dns/asyncresolver.py @@ -0,0 +1,475 @@ +# 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, Optional, Union + +import dns._ddr +import dns.asyncbackend +import dns.asyncquery +import dns.exception +import dns.name +import dns.query +import dns.rdataclass +import dns.rdatatype +import dns.resolver # lgtm[py/import-and-import-from] + +# 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: Union[dns.name.Name, str], + rdtype: Union[dns.rdatatype.RdataType, str] = dns.rdatatype.A, + rdclass: Union[dns.rdataclass.RdataClass, str] = dns.rdataclass.IN, + tcp: bool = False, + source: Optional[str] = None, + raise_on_no_answer: bool = True, + source_port: int = 0, + lifetime: Optional[float] = None, + search: Optional[bool] = None, + backend: Optional[dns.asyncbackend.Backend] = 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: Union[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: Union[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: Union[dns.name.Name, str], + rdtype: Union[dns.rdatatype.RdataType, str] = dns.rdatatype.A, + rdclass: Union[dns.rdataclass.RdataClass, str] = dns.rdataclass.IN, + tcp: bool = False, + source: Optional[str] = None, + raise_on_no_answer: bool = True, + source_port: int = 0, + lifetime: Optional[float] = None, + search: Optional[bool] = None, + backend: Optional[dns.asyncbackend.Backend] = 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: Union[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: Union[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: Union[dns.name.Name, str], + rdclass: dns.rdataclass.RdataClass = dns.rdataclass.IN, + tcp: bool = False, + resolver: Optional[Resolver] = None, + backend: Optional[dns.asyncbackend.Backend] = 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: Union[dns.name.Name, str], + port: int = 53, + family: int = socket.AF_UNSPEC, + resolver: Optional[Resolver] = 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[Union[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 = dns.asyncresolver.Resolver(configure=False) + res.nameservers = nameservers + return res + + +async def resolve_at( + where: Union[dns.name.Name, str], + qname: Union[dns.name.Name, str], + rdtype: Union[dns.rdatatype.RdataType, str] = dns.rdatatype.A, + rdclass: Union[dns.rdataclass.RdataClass, str] = dns.rdataclass.IN, + tcp: bool = False, + source: Optional[str] = None, + raise_on_no_answer: bool = True, + source_port: int = 0, + lifetime: Optional[float] = None, + search: Optional[bool] = None, + backend: Optional[dns.asyncbackend.Backend] = None, + port: int = 53, + family: int = socket.AF_UNSPEC, + resolver: Optional[Resolver] = 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/venv/Lib/site-packages/dns/dnssec.py b/venv/Lib/site-packages/dns/dnssec.py new file mode 100644 index 00000000..e49c3b79 --- /dev/null +++ b/venv/Lib/site-packages/dns/dnssec.py @@ -0,0 +1,1223 @@ +# 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.""" + + +import base64 +import contextlib +import functools +import hashlib +import struct +import time +from datetime import datetime +from typing import Callable, Dict, List, Optional, Set, Tuple, Union, cast + +import dns._features +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.transaction +import dns.zone +from dns.dnssectypes import Algorithm, DSDigest, NSEC3Hash +from dns.exception import ( # pylint: disable=W0611 + AlgorithmKeyMismatch, + 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: Union[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: Union[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: Union[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() + 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, _: DNSKEY) -> bool: # pragma: no cover + return False + + def ok_to_validate(self, _: DNSKEY) -> bool: # pragma: no cover + return False + + def ok_to_create_ds(self, _: DSDigest) -> bool: # pragma: no cover + return False + + def ok_to_validate_ds(self, _: 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: Union[dns.name.Name, str], + key: dns.rdata.Rdata, + algorithm: Union[DSDigest, str], + origin: Optional[dns.name.Name] = None, + policy: Optional[Policy] = 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('unsupported algorithm "%s"' % 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('unsupported algorithm "%s"' % algorithm) + + if isinstance(name, str): + name = dns.name.from_text(name, origin) + wire = name.canonicalize().to_wire() + assert wire is not None + dshash.update(wire) + dshash.update(key.to_wire(origin=origin)) + 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: Union[dns.name.Name, str], + key: dns.rdata.Rdata, + algorithm: Union[DSDigest, str], + origin: Optional[dns.name.Name] = 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, Union[dns.rdataset.Rdataset, dns.node.Node]], rrsig: RRSIG +) -> Optional[List[DNSKEY]]: + 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: Union[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: + 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: Union[dns.rrset.RRset, Tuple[dns.name.Name, dns.rdataset.Rdataset]], + rrsig: RRSIG, + keys: Dict[dns.name.Name, Union[dns.node.Node, dns.rdataset.Rdataset]], + origin: Optional[dns.name.Name] = None, + now: Optional[float] = None, + policy: Optional[Policy] = 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) + + 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: Union[dns.rrset.RRset, Tuple[dns.name.Name, dns.rdataset.Rdataset]], + rrsigset: Union[dns.rrset.RRset, Tuple[dns.name.Name, dns.rdataset.Rdataset]], + keys: Dict[dns.name.Name, Union[dns.node.Node, dns.rdataset.Rdataset]], + origin: Optional[dns.name.Name] = None, + now: Optional[float] = None, + policy: Optional[Policy] = 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: Union[dns.rrset.RRset, Tuple[dns.name.Name, dns.rdataset.Rdataset]], + private_key: PrivateKey, + signer: dns.name.Name, + dnskey: DNSKEY, + inception: Optional[Union[datetime, str, int, float]] = None, + expiration: Optional[Union[datetime, str, int, float]] = None, + lifetime: Optional[int] = None, + verify: bool = False, + policy: Optional[Policy] = None, + origin: Optional[dns.name.Name] = None, +) -> 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. + + 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 = dns.dnssec._make_rrsig_signature_data(rrset, rrsig_template, origin) + + 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) + + return cast(RRSIG, rrsig_template.replace(signature=signature)) + + +def _make_rrsig_signature_data( + rrset: Union[dns.rrset.RRset, Tuple[dns.name.Name, dns.rdataset.Rdataset]], + rrsig: RRSIG, + origin: Optional[dns.name.Name] = 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"" + data += rrsig.to_wire(origin=signer)[: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: Union[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) + + 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: Union[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: Union[dns.name.Name, str], + salt: Optional[Union[str, bytes]], + iterations: int, + algorithm: Union[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: Union[dns.rrset.RRset, Tuple[dns.name.Name, dns.rdataset.Rdataset]], + algorithms: Set[Union[DSDigest, str]], + origin: Optional[dns.name.Name] = 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('unsupported algorithm "%s"' % 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: Union[dns.name.Name, str], + rdataset: dns.rdataset.Rdataset, + algorithm: Union[DSDigest, str], + origin: Optional[dns.name.Name] = 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: Optional[Union[datetime, str, int, float]] = None, + expiration: Optional[Union[datetime, str, int, float]] = None, + lifetime: Optional[int] = None, + policy: Optional[Policy] = None, + origin: Optional[dns.name.Name] = None, +) -> 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 = dns.dnssec.sign( + rrset=rrset, + private_key=private_key, + dnskey=dnskey, + inception=inception, + expiration=expiration, + lifetime=lifetime, + signer=signer, + policy=policy, + origin=origin, + ) + txn.add(rrset.name, rrset.ttl, rrsig) + + +def sign_zone( + zone: dns.zone.Zone, + txn: Optional[dns.transaction.Transaction] = None, + keys: Optional[List[Tuple[PrivateKey, DNSKEY]]] = None, + add_dnskey: bool = True, + dnskey_ttl: Optional[int] = None, + inception: Optional[Union[datetime, str, int, float]] = None, + expiration: Optional[Union[datetime, str, int, float]] = None, + lifetime: Optional[int] = None, + nsec3: Optional[NSEC3PARAM] = None, + rrset_signer: Optional[RRsetSigner] = None, + policy: Optional[Policy] = None, +) -> 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. + + 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() + + 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, + ) + return _sign_zone_nsec(zone, _txn, _rrset_signer) + + +def _sign_zone_nsec( + zone: dns.zone.Zone, + txn: dns.transaction.Transaction, + rrset_signer: Optional[RRsetSigner] = None, +) -> None: + """NSEC zone signer""" + + def _txn_add_nsec( + txn: dns.transaction.Transaction, + name: dns.name.Name, + next_secure: Optional[dns.name.Name], + rdclass: dns.rdataclass.RdataClass, + ttl: int, + rrset_signer: Optional[RRsetSigner] = 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().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 dsa # pylint: disable=W0611 + 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/venv/Lib/site-packages/dns/dnssecalgs/__init__.py b/venv/Lib/site-packages/dns/dnssecalgs/__init__.py new file mode 100644 index 00000000..3d9181a7 --- /dev/null +++ b/venv/Lib/site-packages/dns/dnssecalgs/__init__.py @@ -0,0 +1,120 @@ +from typing import Dict, Optional, Tuple, Type, Union + +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 + +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 = Optional[Union[bytes, dns.name.Name]] + +algorithms: Dict[Tuple[Algorithm, AlgorithmPrefix], Type[GenericPrivateKey]] = {} +if _have_cryptography: + 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: Union[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( + 'algorithm "%s" not supported by dnspython' % Algorithm.to_text(algorithm) + ) + + +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: Union[int, str], + algorithm_cls: Type[GenericPrivateKey], + name: Optional[Union[dns.name.Name, str]] = None, + oid: Optional[bytes] = 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/venv/Lib/site-packages/dns/dnssecalgs/__pycache__/__init__.cpython-312.pyc b/venv/Lib/site-packages/dns/dnssecalgs/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f9233e9f028b26cfc544110236303ea903485fa4 GIT binary patch literal 5560 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

DUFCJ#z1e#&H61bk$Fz8_^wJcDLnc4r|>G=TA%( zoXR=s_3J+}KH6IF1~(^n+BV8I<&y(&`P-in*%$6FX}$8`=aUd nrd>v^zn}!V6=%<(jWF0Lr+>43qvenYJ9JW3=T9zxG#mLBg1u)b literal 0 HcmV?d00001 diff --git a/venv/Lib/site-packages/dns/dnssecalgs/__pycache__/base.cpython-312.pyc b/venv/Lib/site-packages/dns/dnssecalgs/__pycache__/base.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..21a35e7f085649f443440ecbd2be4bfb141c630c GIT binary patch literal 4604 zcmb_gO>7&-6`tiTm;WLqQ9oAVh=!vdI-(LdNag$}fuhK+p_(?VI;aT&+vV<1T6wu- zXID-n7^qvghhEwmC?Mrj_f!k+!G|7vDspcRc73q0wGjj9B{$k~FFEzSS#r4~<+_Is z!1;SW^JeDF_ulevsZ^Xm*^tjyeiI|)TO71XxY;@QJ#==6PISQ}RiPwQL#2>_%KB=b4ZjFh546U`WJB{N=4loFg4Hj~v4sMD0oq+u!nC#J zM$gwBqh_h5EB$z@PK$ZLjV+jKHD);LRTyOb!7|-CXV_KM(bi#J{;asNymT3Q(rHs& z(>!auI?@~*Bn3PgN=kxGN})@Hid52tkAO_WdWg!ps7n<|58sgPG(}1gpvW|;N8pE4 z88+Oe#(*l)XeFe_0<&?N0By;pws?S&qJ0~0G*t&0Jpme$G^3~BC(Zb{A z7iuD4SGujqw{L*+WVm(6(|-uhpl{_sn_9|lN%=uHbB0=!8QO)-vT0~=rfy7a9k(l9 zfw9b}v&pQY77AF5UA*xF3922kMfQZ^9@z`Il1ZENa{SbU|BJopQ}-PavOJXfYO$c9mLJSAS%bS8c9*&MB}=R6GzZt1Q|(-N%b|9z zQe$WtJQmNWJ%Jl#qq3$U8Or4pkW;wO_OTw5x>-g*oS|=frUjh^``olop|eA-2^-LM zmuuv(*(-CPPlRX=Ir3}Bgqp&WxN}}(#^S_?oUHrqjug?7=k$*fec|SSX5J~a`KZfsc+iti5 zh-ACbI;%M~t!8%k5pIAV&R?LjLmdB3+ne6NN7hv~PBo)q zP(2P{#*1O)E}mY@y?64|J5B5hu{Y7Uce^(i<>rHhhoEzd^iIdNUWt33clcwW34i~m zjj$Z#y2OY-ehcF&lYLzOA8&{NSP2V)JpH!5eZK|e6VfcXEw4$h% zRdYC0*^0st2pC2whH&eKIXE-KB8G3mP4IW-WmNnCuPM=+p3EBP4`VyNeA1&8^R#V{;%9Im_o8 zaAgR9*3WcZco^B9m?tw5g$l=X5qGa;K zpDzFL^4G#M5(~$7?FKgYj`Qa0$^niV!@$1p**mjaK{ICV=Rd^7X82;BJ_&|=Ma~)u zkKCHTMMv(>e2lA(1X*DSOXF6dw;OKLpW%defOvKq{QQC#kLBR6P<1Du7|9_)^U_cm z6356PX=oIZ(>_Q5e2cNmEpg5j~^FK`KG}Xbe~4sidqY zZ}_sljx^}V1UX?9Nl&#oOBqNR``!K)05Rh#VA(R{h%KCHMPZhKrCyOS@-XhXT^6{5 zfXBVNjobb@ezx`r2(VGCa8Epy;;*11vI3=o_$fcD<;ea+(8|t2 z)p2+nVh+2@7Pc>=So{3^RIh1cw|Ud8^9z^<5$L7Hg}juT+ZBkZ9Q3wFd#}{P?28>n z<%QFbZ=z3oxX2UGes&5c-pA?#tnf3_qko9KPON75mFY_5TFqojNamL{k3Gz!UJk?y zNW}^j*nfu#fH;%AKLi=bxybwV|d^E>|7ZVKEqn`;vGLEJYlyF(z@Apg2-lYGUZQV zmguH&2tW7|#Ify#Q_e)c)F}Upfqy!=TlILZ)8G Z1!3fO_6q{di=PUk!rZR-ZvqWh{U5o~B}o7P literal 0 HcmV?d00001 diff --git a/venv/Lib/site-packages/dns/dnssecalgs/__pycache__/cryptography.cpython-312.pyc b/venv/Lib/site-packages/dns/dnssecalgs/__pycache__/cryptography.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6788e3d2d892a1accf230f1402c264c204a93620 GIT binary patch literal 3869 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 S+Htb%NbRM60Y5^&_kRE-IUub7 literal 0 HcmV?d00001 diff --git a/venv/Lib/site-packages/dns/dnssecalgs/__pycache__/ecdsa.cpython-312.pyc b/venv/Lib/site-packages/dns/dnssecalgs/__pycache__/ecdsa.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3f3329c78d1aa9d2d477eb81fa8beca40f6b0383 GIT binary patch literal 6021 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 PK(poe9RHulr?B`po2a!* literal 0 HcmV?d00001 diff --git a/venv/Lib/site-packages/dns/dnssecalgs/__pycache__/eddsa.cpython-312.pyc b/venv/Lib/site-packages/dns/dnssecalgs/__pycache__/eddsa.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..252a392571f74e00b555f1495266c97b81726b62 GIT binary patch literal 4184 zcmcgv&2JmW6`$oUm%Ajzr9K$hZ6s5&Y-KBxN*p;*S|fmK$#LUG4y-zWSpka`cSup; zS7#|DWK=)~T%bN#I?0C|bhP0D&9R662fdh(3o?ruZBZ0OZ%m|Ka_W1tTxz8<0RjXa zfNy5rym>qGoA-V*{L{cdm_S*VZ|52zLjHjruLQe|!!|Vbh(#=+NJ>IR5U?F621;T^ z!Kssw3uT9V8vn}Z1j^%CcnOa{l_bJ0e5?)x&bCDwaUk3d#zA4 z?2=hDRyXWInc8+woyxA6wbe@PSb=uMlxLX2u}xauT={unb;T~!XsT*vKQi;wUa`vd zEc|SmHH&$BmRgo=rmJ-}1WRcQ`2b71tB&G2M?h?owi11$JX9WO54FeY{@kCH*A78X zvI5F(>EZVoD837RHJr)Ix)%(d0_t_e*XcHWGk6l8wCFeEcY=39%R~<}E-jX`6^ka# zWR=HB5)La_r7U^p#`Wa<>+`Q)N&-xn1<5zlaF_~3`$9SI$hitDnKfNzgYZ=+>drS- z>osaK+=!!a-)Jk|F+X;qF0eP;X#yJt8yj&;GQ`%2bJx^>6}Vm+ulkST^r(i@e*$8g z{7oBv^vT0dTG~`ooBB&)vXz)=CT3cR*=Az)j{~jwJI(n!t@#g|^B;CZ62EXr0`YK% z1k^A~z`Y&UG7PYALePk=3G@B{Q8FIqs^5}tLn$YWcWCVn5T5n1;bwA*)Q_B&qB z8yryA?=pEBSkeW)K3#Gq3|h7!bqxqwmq<8N*-B}(07(r;}+DFR~kH z#Z%2VD1V=Snm>rYbtIlZ8t6zdjIvpxhT((_qg1grir9`C#;-QaVs|9y#t>#B8w24) zvPILjOSHCDu^7fVn*#BaxW|XV!!Sj;h(N=B55xn~2?#-bKiDBqJ-N%P&ufR+>&Q|t zybpl@RVPa1!A<~Lk8>SVJQ@26t3wg=U!vaq41OUCzg~g6l9y9_eV0&?N@rn+1}y<8 z)KY-IW#H_PB~h7kcrXJuo~0s#_xL(o59wjZ68A1~2|L_9LOe8NOHNS?L%%;`Z^OuU zVVvV2{$1sX);e!D04;xtqt`0Yb*(~%LgMRqZ=q!nrb08pUN6zfs+`YN? z{_gwxQwJl{zg54847Q{3N4Fo|K8TLBqEpT2)c(ECr5DjFZSBl+j+<;GE{IE+P^a#6v?TaYT2}9cBdE{t@VV294 z8fO;UFxYD_}7mr^iz z_Hm*^pn4+l>JN!S>~&(0a$iB#&UTXzTCQJoc_R66omdW6Kpa1oF!}o3dR{8_AI-=# zv>xaFPKbD7rX;@^pH43gmD0(y8GOi!co4(d?M!_S@s zfvCh<>R3}9JCMfOvEf#1vKgD?!Lg_9YF=pIr2fc!&!KUAgB`>0KY_tv&|Db$g$3Vd zzp(fzY=fadi(Xg&0OL({{6HG#0Gw~e&SO}->V*YPSkgxvxmK^jrNn8@8PEFu!ll>D z20Ulea9NcKwZf+Nh}FN{C~eyH61+@UAxSS;7ua&Fr3+o+*44=g6(tIPVUTLJBjVFovZ(W_83pa{{pYPTBZO1 literal 0 HcmV?d00001 diff --git a/venv/Lib/site-packages/dns/dnssecalgs/__pycache__/rsa.cpython-312.pyc b/venv/Lib/site-packages/dns/dnssecalgs/__pycache__/rsa.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..393e8da35a98f54720c119fc7df62d3008182430 GIT binary patch literal 7240 zcmcf`TWl0Za$Y<8e);V+w#Q(=OR$Lv2fV? zb$4}jb#+x&b@l9@-EId1Wmr5t@SU4szQc}LEQLls&N9p_BQr7^XF^8JhFIuxaW27! zcouPdTu4|#7TOl#V!|4-CTt-a?OWpZgd^lgI73d_7vrvkJLINqYuuCYhP*7pF|RYS z?K&gd73V$PSUcpSSO;L8ild12Q>+WHZpBx`R#B`6uwKPq#8y+R53v3c+8T*GpAg5c?5m}BU2X&sP5$M+)B4cSSHAo`q zp|LY0HX6~ClggN0TZSL$kH?~b23Gc-_+W~}w4ns@J>1)O^2o(#!RoXU3!VHQpfSrR z44OT}oo5t&*j7qtv=xiY9)M=$(U7QEfr6tHw&ALZ^LZSV1;rs-;3vw~0TB*`5OT_* z;uzp$JG5PjTV@pxj4}g)?6@x6E3gWA$E-o8Za+i!_-0>^&R>T8+au;Sb!T)ar7FoV z>QJ|p(0L(HI%rHflgW=K{g`o*{g40YV z0v9Wvfjv`h;8{VN_XJ}G%ZxEWwl`=Y;Gv8zjw&QJFh-EGZaZ`GP+#|G_io~aZjjdn zRf!J}A9RQxfQoWSlJwL2hxT1MuPQ{nlpX<+OIqsPq&gH!ht)(R9_}AeV@X9-i|Eei zP(z9LjGr_&t;Qj~amR~uQdF9k! zY`u9PWug#|iUrj`&2xc!KJ%E$zsf${NLtY}!_MNW#l{3X>?(GOje7+NWp*6qn7yl9 z2`{wQx>A|f1SfO%c{83}<)JTJJ`DB^$QBK4OiLwqlEn$`9%u#2IQ*^($+~CEu&&Lx zDu6s_wRNSE7EE2rw#%@d+WcS4w9`E~wR&?o*tlkh@$E;F(Uh!65h+chiv*EHN-IQq^XMVzrS9$*w@Yy1wOA@C?d*DtXGZ7y zV}lTu#ki6j)F9mSk7-JZ$O?JaB?Kk500%1i3qGBM3q=v=VM7tcl5`!aA9#)(QR5Pj3TJI zsA^;+s_6pytj;EN$6Jy3h;oFG6oD{Phr)B|q;3l<@1>E0&O<|YU~fo?zz$S&JBTZi zM9^TmPodfggHnt&#sh1vzJ3h(tB(L&WB%s$-?%V!VcFfDakppPTPC@O)%CM`XZC*5 zck7*-?<^er^zhxo+2-!Wo=o%ZZ1u~Nwuc`7jrdf2Zs)CCH+L=A9vqpBXFUfe`JA(Q zI{MM8xyF{?*WGEl-L!CSaZk4GK&J7))QNvIw%%&G*))G{VNbSk*W`(u(|u#t)UF$^ zO}&^|RJ&_rWjDX1b4l*`0aujW3n##&a3(xh2~<(f~V*7Tgb- z3}c5M4OPFR2^B>B(m+N@@Q9nop2g!{I=?^xVdSZ|iVXLT%*lL!F`fSstV06_FEat= zDmTH)+~p1kA<{EA36zti0w+s>6Rb1=D?zY8uE^cO!5yjLUTGPo38vXIYONSDhby_R zAd9Bt&!DudQJOP`iy?7sO%1OZTFgLY>iE9ZY=M*8dPy5l5!Cr(G!2okgcep>lV0P!-`QQRa}C4Wx*|-lr3DwFl)Sa z%_B?Dn_!Ci1pshHn{(OLnXz>)M8CG}%=v3(>u2gG#fLS4*;6y8=Gzy2+3?@oIsE#G*n9{Q@6eeD@vJ2YlJGoHDTc_r)Hen0wY z@^13M`7e%SyH4bJ&hPo&(=avsNo%%oTh{Z!s#WkgRv5wVp#B#0LC^{(BMBuO)*az+ zA|;Q+vF!?nuZ%?E#)x4GSJ4zFAxZRx!;xe%rBUmtFvAKu3!~ogC=&sI?mh--V=-se zEz|}yvmrZi+!>8WR5hV!Ln)cyxhFjU?lM#~8Yl^#d&$mHy0hUexlmzaly<=h^wq zc?Rp103-TWIFH3KH?YD$wb1k#fwIGFVMj1Te7=+1w1eh}g1)2fS_k*Ekd`_t7 z0T^41b9@?lU}8=4DrV#??N5aZ&i%wHwa=lC6QBQr(8msK&;XvI`Zn zER2wO8Ne)LP;l{J+@__{)d^O>ZLBL3tY#ki;*;z?YufKVS8_%0z3|5J76LS+L#d>a z)W}(2s9Ot9bn-IvK!-Hk*9W_ zcy)>1P2@qKRCfTl#{9hyOgl2Zj-0eh+$JKgiZL= za5U&6$b%e0a2NsIfg7V_@mm5piUY4Bpponhw%$T;9DpwLr&4i(XM>zTum!=}2=G*t zguzqTMX(!w>TduP!eAg{4`hV^4TG&2Pb-8$f8g4QT+5~DuvJ)rRRUv)g92eZ2jHlg37oxNM`QU*yr5OE(%4sL1ZMGeu>#te2U}K<&Lw zxG*pz2;PLBik<|*`I|pDVMu}_Ws>|dDt8xE-gm60`$;;tg3fWgMCk(USJHX}Bqs>) zfR}2$E#qrLtv7?#UzmB}<4#lSNL;qpXKCHDrgcN?pQ)V+QAzCzN{0XqQ&fA4sdgMG z6XlmtdtJHO`;HuX>1q1M^p}pCglmQ%C~d{lKZpYe@FKKogeQoy#a~KcKj?W_{yw6h5woQsTh^ie+A`3 zkU;sY05A+mruuQDOqlMfN>(j=CM zX|Yj7HLk?q(}}tw>R19ETqG7Xa$B0SG{K9vYwZ(sS1AvICxg)MbKW&FZ7dBd(rdX6 zu|%eruAbhDUB>?+#(U0%lkpO!w=uo-=nX*wF^y3)0#HAphmqQcYHVE!^pov?@i={z zybt`)YU(0@6`p0;ZyD#ejQgL=o<~ggUzvm7Gv^;Mn;$X3RdF-xSn_RNWuRU?!`4F2 zzjc*?di6z)ZJpk`%0RX1VA+=G6RQkVt2U06rjH|DLVW9V2=T2HKX! literal 0 HcmV?d00001 diff --git a/venv/Lib/site-packages/dns/dnssecalgs/base.py b/venv/Lib/site-packages/dns/dnssecalgs/base.py new file mode 100644 index 00000000..e990575a --- /dev/null +++ b/venv/Lib/site-packages/dns/dnssecalgs/base.py @@ -0,0 +1,84 @@ +from abc import ABC, abstractmethod # pylint: disable=no-name-in-module +from typing import Any, Optional, 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) -> bytes: + """Sign DNSSEC data""" + + @abstractmethod + def public_key(self) -> "GenericPublicKey": + """Return public key instance""" + + @classmethod + @abstractmethod + def from_pem( + cls, private_pem: bytes, password: Optional[bytes] = None + ) -> "GenericPrivateKey": + """Create private key from PEM-encoded PKCS#8""" + + @abstractmethod + def to_pem(self, password: Optional[bytes] = None) -> bytes: + """Return private key as PEM-encoded PKCS#8""" diff --git a/venv/Lib/site-packages/dns/dnssecalgs/cryptography.py b/venv/Lib/site-packages/dns/dnssecalgs/cryptography.py new file mode 100644 index 00000000..5a31a812 --- /dev/null +++ b/venv/Lib/site-packages/dns/dnssecalgs/cryptography.py @@ -0,0 +1,68 @@ +from typing import Any, Optional, 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] + + 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: Optional[bytes] = None + ) -> "GenericPrivateKey": + key = serialization.load_pem_private_key(private_pem, password=password) + return cls(key=key) + + def to_pem(self, password: Optional[bytes] = 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/venv/Lib/site-packages/dns/dnssecalgs/dsa.py b/venv/Lib/site-packages/dns/dnssecalgs/dsa.py new file mode 100644 index 00000000..0fe4690d --- /dev/null +++ b/venv/Lib/site-packages/dns/dnssecalgs/dsa.py @@ -0,0 +1,101 @@ +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) -> 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) + 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/venv/Lib/site-packages/dns/dnssecalgs/ecdsa.py b/venv/Lib/site-packages/dns/dnssecalgs/ecdsa.py new file mode 100644 index 00000000..a31d79f2 --- /dev/null +++ b/venv/Lib/site-packages/dns/dnssecalgs/ecdsa.py @@ -0,0 +1,89 @@ +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) -> bytes: + """Sign using a private key per RFC 6605, section 4.""" + der_signature = self.key.sign(data, ec.ECDSA(self.public_cls.chosen_hash)) + dsa_r, dsa_s = utils.decode_dss_signature(der_signature) + signature = int.to_bytes( + dsa_r, length=self.public_cls.octets, byteorder="big" + ) + int.to_bytes(dsa_s, length=self.public_cls.octets, byteorder="big") + 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() + ), + ) + + +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/venv/Lib/site-packages/dns/dnssecalgs/eddsa.py b/venv/Lib/site-packages/dns/dnssecalgs/eddsa.py new file mode 100644 index 00000000..70505342 --- /dev/null +++ b/venv/Lib/site-packages/dns/dnssecalgs/eddsa.py @@ -0,0 +1,65 @@ +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] + + def sign(self, data: bytes, verify: bool = False) -> 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/venv/Lib/site-packages/dns/dnssecalgs/rsa.py b/venv/Lib/site-packages/dns/dnssecalgs/rsa.py new file mode 100644 index 00000000..e95dcf1d --- /dev/null +++ b/venv/Lib/site-packages/dns/dnssecalgs/rsa.py @@ -0,0 +1,119 @@ +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) -> bytes: + """Sign using a private key per RFC 3110, section 3.""" + signature = self.key.sign(data, padding.PKCS1v15(), self.public_cls.chosen_hash) + 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/venv/Lib/site-packages/dns/dnssectypes.py b/venv/Lib/site-packages/dns/dnssectypes.py new file mode 100644 index 00000000..02131e0a --- /dev/null +++ b/venv/Lib/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/venv/Lib/site-packages/dns/e164.py b/venv/Lib/site-packages/dns/e164.py new file mode 100644 index 00000000..453736d4 --- /dev/null +++ b/venv/Lib/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, Optional, Union + +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: Optional[dns.name.Name] = 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: Optional[dns.name.Name] = 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[Union[dns.name.Name, str]], + resolver: Optional[dns.resolver.Resolver] = 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 = dns.e164.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/venv/Lib/site-packages/dns/edns.py b/venv/Lib/site-packages/dns/edns.py new file mode 100644 index 00000000..776e5eeb --- /dev/null +++ b/venv/Lib/site-packages/dns/edns.py @@ -0,0 +1,516 @@ +# 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, Optional, Union + +import dns.enum +import dns.inet +import dns.rdata +import dns.wire + + +class OptionType(dns.enum.IntEnum): + #: 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 + + @classmethod + def _maximum(cls): + return 65535 + + +class Option: + """Base class for all EDNS option types.""" + + def __init__(self, otype: Union[OptionType, str]): + """Initialize an option. + + *otype*, a ``dns.edns.OptionType``, is the option type. + """ + self.otype = OptionType.make(otype) + + def to_wire(self, file: Optional[Any] = None) -> Optional[bytes]: + """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 + + @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: Union[OptionType, str], data: Union[bytes, str]): + super().__init__(otype) + self.data = dns.rdata.Rdata._as_bytes(data, True) + + def to_wire(self, file: Optional[Any] = None) -> Optional[bytes]: + if file: + file.write(self.data) + return None + else: + return self.data + + def to_text(self) -> str: + return "Generic %d" % self.otype + + @classmethod + def from_wire_parser( + cls, otype: Union[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: Optional[int] = 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 "ECS {}/{} scope/{}".format(self.address, self.srclen, 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('could not parse ECS from "{}"'.format(text)) + ecs_text = tokens[1] + else: + raise ValueError('could not parse ECS from "{}"'.format(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('could not parse ECS from "{}"'.format(text)) + try: + scope = int(tscope) + except ValueError: + raise ValueError( + "invalid scope " + '"{}": scope must be an integer'.format(tscope) + ) + try: + srclen = int(tsrclen) + except ValueError: + raise ValueError( + "invalid srclen " + '"{}": srclen must be an integer'.format(tsrclen) + ) + return ECSOption(address, srclen, scope) + + def to_wire(self, file: Optional[Any] = None) -> Optional[bytes]: + 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: Union[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): + 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: Union[EDECode, str], text: Optional[str] = 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: Optional[Any] = None) -> Optional[bytes]: + 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: Union[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) -> Optional[bytes]: + 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: Union[OptionType, str], parser: dns.wire.Parser + ) -> Option: + return cls(parser.get_remaining()) + + +_type_to_class: Dict[OptionType, Any] = { + OptionType.ECS: ECSOption, + OptionType.EDE: EDEOption, + OptionType.NSID: NSIDOption, +} + + +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: Union[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: Union[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 + +### END generated OptionType constants diff --git a/venv/Lib/site-packages/dns/entropy.py b/venv/Lib/site-packages/dns/entropy.py new file mode 100644 index 00000000..4dcdc627 --- /dev/null +++ b/venv/Lib/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, Optional + + +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: Optional[bytes] = None): + self.pool_index = 0 + self.digest: Optional[bytearray] = 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) -> 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) -> 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: Optional[Any] +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/venv/Lib/site-packages/dns/enum.py b/venv/Lib/site-packages/dns/enum.py new file mode 100644 index 00000000..71461f17 --- /dev/null +++ b/venv/Lib/site-packages/dns/enum.py @@ -0,0 +1,116 @@ +# 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 Type, TypeVar, Union + +TIntEnum = TypeVar("TIntEnum", bound="IntEnum") + + +class IntEnum(enum.IntEnum): + @classmethod + def _missing_(cls, value): + cls._check_value(value) + val = int.__new__(cls, value) + val._name_ = cls._extra_to_text(value, None) or f"{cls._prefix()}{value}" + val._value_ = value + 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) + try: + return cls(value) + except ValueError: + return 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: Union[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): + return "" + + @classmethod + def _extra_from_text(cls, text): # 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): + return ValueError diff --git a/venv/Lib/site-packages/dns/exception.py b/venv/Lib/site-packages/dns/exception.py new file mode 100644 index 00000000..6982373d --- /dev/null +++ b/venv/Lib/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 Optional, 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: Optional[str] = None # non-parametrized message + supp_kwargs: Set[str] = set() # accepted parameters for _fmt_kwargs (sanity check) + fmt: Optional[str] = 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 + ), "following set of keyword args is required: %s" % (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/venv/Lib/site-packages/dns/flags.py b/venv/Lib/site-packages/dns/flags.py new file mode 100644 index 00000000..4c60be13 --- /dev/null +++ b/venv/Lib/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/venv/Lib/site-packages/dns/grange.py b/venv/Lib/site-packages/dns/grange.py new file mode 100644 index 00000000..3a52278f --- /dev/null +++ b/venv/Lib/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 + + +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("Could not parse %s" % (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/venv/Lib/site-packages/dns/immutable.py b/venv/Lib/site-packages/dns/immutable.py new file mode 100644 index 00000000..36b0362c --- /dev/null +++ b/venv/Lib/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/venv/Lib/site-packages/dns/inet.py b/venv/Lib/site-packages/dns/inet.py new file mode 100644 index 00000000..4a03f996 --- /dev/null +++ b/venv/Lib/site-packages/dns/inet.py @@ -0,0 +1,197 @@ +# 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, Optional, 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: Optional[int] = 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/venv/Lib/site-packages/dns/ipv4.py b/venv/Lib/site-packages/dns/ipv4.py new file mode 100644 index 00000000..65ee69c0 --- /dev/null +++ b/venv/Lib/site-packages/dns/ipv4.py @@ -0,0 +1,77 @@ +# 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 +from typing import Union + +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 "%u.%u.%u.%u" % (address[0], address[1], address[2], address[3]) + + +def inet_aton(text: Union[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: Union[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 dns.ipv4.inet_ntoa(dns.ipv4.inet_aton(text)) diff --git a/venv/Lib/site-packages/dns/ipv6.py b/venv/Lib/site-packages/dns/ipv6.py new file mode 100644 index 00000000..44a10639 --- /dev/null +++ b/venv/Lib/site-packages/dns/ipv6.py @@ -0,0 +1,219 @@ +# 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, Union + +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: Union[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 = ( + "{}:{:02x}{:02x}:{:02x}{:02x}".format( + m.group(1).decode(), b[0], b[1], b[2], b[3] + ) + ).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: Union[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 dns.ipv6.inet_ntoa(dns.ipv6.inet_aton(text)) diff --git a/venv/Lib/site-packages/dns/message.py b/venv/Lib/site-packages/dns/message.py new file mode 100644 index 00000000..44cacbd9 --- /dev/null +++ b/venv/Lib/site-packages/dns/message.py @@ -0,0 +1,1888 @@ +# 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 io +import time +from typing import Any, Dict, List, Optional, Tuple, Union + +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.TSIG +import dns.renderer +import dns.rrset +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, + Optional[dns.rdatatype.RdataType], + Optional[dns.rdataclass.RdataClass], +] +IndexType = Dict[IndexKeyType, dns.rrset.RRset] +SectionType = Union[int, str, List[dns.rrset.RRset]] + + +class Message: + """A DNS message.""" + + _section_enum = MessageSection + + def __init__(self, id: Optional[int] = 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: Optional[dns.rrset.RRset] = None + self.request_payload = 0 + self.pad = 0 + self.keyring: Any = None + self.tsig: Optional[dns.rrset.RRset] = None + self.request_mac = b"" + self.xfr = False + self.origin: Optional[dns.name.Name] = None + self.tsig_ctx: Optional[Any] = None + self.index: IndexType = {} + self.errors: List[MessageError] = [] + self.time = 0.0 + + @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: Optional[dns.name.Name] = 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("id %d\n" % self.id) + s.write("opcode %s\n" % dns.opcode.to_text(self.opcode())) + s.write("rcode %s\n" % dns.rcode.to_text(self.rcode())) + s.write("flags %s\n" % dns.flags.to_text(self.flags)) + if self.edns >= 0: + s.write("edns %s\n" % self.edns) + if self.ednsflags != 0: + s.write("eflags %s\n" % dns.flags.edns_to_text(self.ednsflags)) + s.write("payload %d\n" % self.payload) + for opt in self.options: + s.write("option %s\n" % opt.to_text()) + 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") + # + # 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: Optional[dns.rdataclass.RdataClass] = None, + create: bool = False, + force_unique: bool = False, + idna_codec: Optional[dns.name.IDNACodec] = 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: Optional[dns.rdataclass.RdataClass] = None, + create: bool = False, + force_unique: bool = False, + idna_codec: Optional[dns.name.IDNACodec] = None, + ) -> Optional[dns.rrset.RRset]: + """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. + for option in self.opt[0].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: Optional[dns.name.Name] = None, + max_size: int = 0, + multi: bool = False, + tsig_ctx: Optional[Any] = 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: + (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) + r.add_rrset(dns.renderer.ADDITIONAL, self.tsig) + r.write_header() + if multi: + self.tsig_ctx = ctx + wire = r.get_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: Optional[Union[dns.name.Name, str]] = None, + fudge: int = 300, + original_id: Optional[int] = None, + tsig_error: int = 0, + other_data: bytes = b"", + algorithm: Union[dns.name.Name, str] = dns.tsig.default_algorithm, + ) -> None: + """When sending, a TSIG signature using the specified key + should be added. + + *key*, a ``dns.tsig.Key`` is the key to use. If a key is specified, + the *keyring* and *algorithm* fields are not used. + + *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, + ) + + @property + def keyname(self) -> Optional[dns.name.Name]: + if self.tsig: + return self.tsig.name + else: + return None + + @property + def keyalgorithm(self) -> Optional[dns.name.Name]: + if self.tsig: + return self.tsig[0].algorithm + else: + return None + + @property + def mac(self) -> Optional[bytes]: + if self.tsig: + return self.tsig[0].mac + else: + return None + + @property + def tsig_error(self) -> Optional[int]: + if self.tsig: + return self.tsig[0].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: Optional[Union[int, bool]] = 0, + ednsflags: int = 0, + payload: int = DEFAULT_EDNS_PAYLOAD, + request_payload: Optional[int] = None, + options: Optional[List[dns.edns.Option]] = 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: + return self.opt[0].payload + else: + return 0 + + @property + def options(self) -> Tuple: + if self.opt: + return self.opt[0].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_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: Optional[dns.rrset.RRset], + 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 + ) + min_ttl = min(min_ttl, srrset.ttl, srrset[0].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 + 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, 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: + if self.keyring is None: + raise UnknownTSIGKey("got signed message without keyring") + if isinstance(self.keyring, dict): + key = self.keyring.get(absolute_name) + if isinstance(key, bytes): + key = dns.tsig.Key(absolute_name, key, rd.algorithm) + elif callable(self.keyring): + key = self.keyring(self.message, absolute_name) + else: + key = self.keyring + if key is None: + raise UnknownTSIGKey("key '%s' unknown" % name) + 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, + 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.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: Optional[Any] = None, + request_mac: Optional[bytes] = b"", + xfr: bool = False, + origin: Optional[dns.name.Name] = None, + tsig_ctx: Optional[Union[dns.tsig.HMACTSig, dns.tsig.GSSTSig]] = 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`` or ``dict``, the key or keyring to use if the message + is signed. + + *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, + idna_codec, + one_rr_per_rrset=False, + origin=None, + relativize=True, + relativize_to=None, + ): + self.message = None + 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.""" + + 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. + """ + + 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: Optional[dns.name.IDNACodec] = None, + one_rr_per_rrset: bool = False, + origin: Optional[dns.name.Name] = None, + relativize: bool = True, + relativize_to: Optional[dns.name.Name] = 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: Optional[dns.name.IDNACodec] = 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) + 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: Union[dns.name.Name, str], + rdtype: Union[dns.rdatatype.RdataType, str], + rdclass: Union[dns.rdataclass.RdataClass, str] = dns.rdataclass.IN, + use_edns: Optional[Union[int, bool]] = None, + want_dnssec: bool = False, + ednsflags: Optional[int] = None, + payload: Optional[int] = None, + request_payload: Optional[int] = None, + options: Optional[List[dns.edns.Option]] = None, + idna_codec: Optional[dns.name.IDNACodec] = None, + id: Optional[int] = 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) + m.want_dnssec(want_dnssec) + return m + + +def make_response( + query: Message, + recursion_available: bool = False, + our_payload: int = 8192, + fudge: int = 300, + tsig_error: int = 0, + pad: Optional[int] = 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. + + The response's question section is a shallow copy of the query's question section, + so the query's question 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. + + Returns a ``dns.message.Message`` object whose specific class is appropriate for the + query. For example, if query is a ``dns.update.UpdateMessage``, response will be + too. + """ + + if query.flags & dns.flags.QR: + raise dns.exception.FormError("specified query message is not a query") + factory = _message_factory_from_opcode(query.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(query.opcode()) + response.question = list(query.question) + 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: + 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/venv/Lib/site-packages/dns/name.py b/venv/Lib/site-packages/dns/name.py new file mode 100644 index 00000000..22ccb392 --- /dev/null +++ b/venv/Lib/site-packages/dns/name.py @@ -0,0 +1,1283 @@ +# 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, Union + +import dns._features +import dns.enum +import dns.exception +import dns.immutable +import dns.wire + +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 + + @classmethod + def _short_name(cls): + return cls.__name__ + + +# 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: Union[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 += "\\%03d" % c + 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 += "\\%03d" % ord(uc) + 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: + 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: Union[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[Union[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: Optional[IDNACodec] = 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: Optional[Any] = None, + compress: Optional[CompressType] = None, + origin: Optional["Name"] = None, + canonicalize: bool = False, + ) -> Optional[bytes]: + """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: Optional[Name] = root, idna_codec: Optional[IDNACodec] = 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: Union[bytes, str], + origin: Optional[Name] = root, + idna_codec: Optional[IDNACodec] = 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/venv/Lib/site-packages/dns/namedict.py b/venv/Lib/site-packages/dns/namedict.py new file mode 100644 index 00000000..ca8b1978 --- /dev/null +++ b/venv/Lib/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/venv/Lib/site-packages/dns/nameserver.py b/venv/Lib/site-packages/dns/nameserver.py new file mode 100644 index 00000000..5dbb4e8b --- /dev/null +++ b/venv/Lib/site-packages/dns/nameserver.py @@ -0,0 +1,359 @@ +from typing import Optional, Union +from urllib.parse import urlparse + +import dns.asyncbackend +import dns.asyncquery +import dns.inet +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: Optional[str], + 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: Optional[str], + 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: Optional[str], + 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: Optional[str], + 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: Optional[str] = None, + verify: Union[bool, str] = True, + want_get: bool = False, + ): + super().__init__() + self.url = url + self.bootstrap_address = bootstrap_address + self.verify = verify + self.want_get = want_get + + 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: Optional[str], + 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), + ) + + async def async_query( + self, + request: dns.message.QueryMessage, + timeout: float, + source: Optional[str], + 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), + ) + + +class DoTNameserver(AddressAndPortNameserver): + def __init__( + self, + address: str, + port: int = 853, + hostname: Optional[str] = None, + verify: Union[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: Optional[str], + 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: Optional[str], + 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: Union[bool, str] = True, + server_hostname: Optional[str] = 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: Optional[str], + 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: Optional[str], + 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/venv/Lib/site-packages/dns/node.py b/venv/Lib/site-packages/dns/node.py new file mode 100644 index 00000000..de85a82d --- /dev/null +++ b/venv/Lib/site-packages/dns/node.py @@ -0,0 +1,359 @@ +# 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, Optional + +import dns.immutable +import dns.name +import dns.rdataclass +import dns.rdataset +import dns.rdatatype +import dns.renderer +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, + ) -> Optional[dns.rdataset.Rdataset]: + """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, + ) -> Optional[dns.rdataset.Rdataset]: + 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/venv/Lib/site-packages/dns/opcode.py b/venv/Lib/site-packages/dns/opcode.py new file mode 100644 index 00000000..78b43d2c --- /dev/null +++ b/venv/Lib/site-packages/dns/opcode.py @@ -0,0 +1,117 @@ +# 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.""" + +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): + 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/venv/Lib/site-packages/dns/py.typed b/venv/Lib/site-packages/dns/py.typed new file mode 100644 index 00000000..e69de29b diff --git a/venv/Lib/site-packages/dns/query.py b/venv/Lib/site-packages/dns/query.py new file mode 100644 index 00000000..f0ee9161 --- /dev/null +++ b/venv/Lib/site-packages/dns/query.py @@ -0,0 +1,1578 @@ +# 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 os.path +import selectors +import socket +import struct +import time +from typing import Any, Dict, Optional, Tuple, Union + +import dns._features +import dns.exception +import dns.inet +import dns.message +import dns.name +import dns.quic +import dns.rcode +import dns.rdataclass +import dns.rdatatype +import dns.serial +import dns.transaction +import dns.tsig +import dns.xfr + + +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, local_address, 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: + source = dns.inet.low_level_address_tuple( + (local_address, self._local_port), af + ) + else: + source = None + sock = _make_socket(af, socket.SOCK_STREAM, source) + attempt_expiration = _expiration_for_this_attempt(2.0, expiration) + try: + _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, socket_options=None + ): # pylint: disable=signature-differs + raise NotImplementedError + + class _HTTPTransport(httpx.HTTPTransport): + def __init__( + self, + *args, + local_port=0, + bootstrap_address=None, + resolver=None, + family=socket.AF_UNSPEC, + **kwargs, + ): + if resolver 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 connect_tcp(self, host, port, timeout, local_address): + raise NotImplementedError + + +have_doh = _have_httpx + +try: + import ssl +except ImportError: # pragma: no cover + + class ssl: # type: ignore + CERT_NONE = 0 + + class WantReadException(Exception): + pass + + class WantWriteException(Exception): + pass + + class SSLContext: + pass + + class SSLSocket: + pass + + @classmethod + def create_default_context(cls, *args, **kwargs): + raise Exception("no ssl support") # pylint: disable=broad-exception-raised + + +# Function used to create a socket. Can be overridden if needed in special +# situations. +socket_factory = socket.socket + + +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 + sel = _selector_class() + events = 0 + if readable: + events |= selectors.EVENT_READ + if writable: + events |= selectors.EVENT_WRITE + if events: + sel.register(fd, events) + 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 _set_selector_class(selector_class): + # Internal API. Do not use. + + global _selector_class + + _selector_class = selector_class + + +if hasattr(selectors, "PollSelector"): + # Prefer poll() on platforms that support it because it has no + # limits on the maximum value of a file descriptor (plus it will + # be more efficient for high values). + # + # We ignore typing here as we can't say _selector_class is Any + # on python < 3.8 due to a bug. + _selector_class = selectors.PollSelector # type: ignore +else: + _selector_class = selectors.SelectSelector # type: ignore + + +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, type, source, ssl_context=None, server_hostname=None): + s = socket_factory(af, type) + try: + s.setblocking(False) + if source is not None: + s.bind(source) + if ssl_context: + # LGTM gets a false positive here, as our default context is OK + return ssl_context.wrap_socket( + s, + do_handshake_on_connect=False, # lgtm[py/insecure-protocol] + server_hostname=server_hostname, + ) + else: + return s + except Exception: + s.close() + raise + + +def https( + q: dns.message.Message, + where: str, + timeout: Optional[float] = None, + port: int = 443, + source: Optional[str] = None, + source_port: int = 0, + one_rr_per_rrset: bool = False, + ignore_trailing: bool = False, + session: Optional[Any] = None, + path: str = "/dns-query", + post: bool = True, + bootstrap_address: Optional[str] = None, + verify: Union[bool, str] = True, + resolver: Optional["dns.resolver.Resolver"] = None, + family: Optional[int] = socket.AF_UNSPEC, +) -> 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. + + Returns a ``dns.message.Message``. + """ + + if not have_doh: + raise NoDOH # pragma: no cover + if session and not isinstance(session, httpx.Client): + raise ValueError("session parameter must be an httpx.Client") + + wire = q.to_wire() + (af, _, the_source) = _destination_and_source( + where, port, source, source_port, False + ) + transport = None + headers = {"accept": "application/dns-message"} + if af is not None and dns.inet.is_address(where): + if af == socket.AF_INET: + url = "https://{}:{}{}".format(where, port, path) + elif af == socket.AF_INET6: + url = "https://[{}]:{}{}".format(where, port, path) + else: + url = where + + # 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] + transport = _HTTPTransport( + local_address=local_address, + http1=True, + http2=True, + verify=verify, + local_port=local_port, + bootstrap_address=bootstrap_address, + resolver=resolver, + family=family, + ) + + if session: + cm: contextlib.AbstractContextManager = contextlib.nullcontext(session) + else: + cm = httpx.Client(http1=True, http2=True, verify=verify, transport=transport) + with cm as session: + # 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 = session.post(url, headers=headers, content=wire, timeout=timeout) + 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} + ) + + # 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( + "{} responded with status code {}" + "\nResponse body: {}".format(where, response.status_code, 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 _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: Union[dns.message.Message, bytes], + destination: Any, + expiration: Optional[float] = 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: Optional[Any] = None, + expiration: Optional[float] = None, + ignore_unexpected: bool = False, + one_rr_per_rrset: bool = False, + keyring: Optional[Dict[dns.name.Name, dns.tsig.Key]] = None, + request_mac: Optional[bytes] = b"", + ignore_trailing: bool = False, + raise_on_truncation: bool = False, + ignore_errors: bool = False, + query: Optional[dns.message.Message] = 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: Optional[float] = None, + port: int = 53, + source: Optional[str] = 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: Optional[Any] = 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 + ) + (begin_time, expiration) = _compute_times(timeout) + if sock: + cm: contextlib.AbstractContextManager = contextlib.nullcontext(sock) + else: + 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: Optional[float] = None, + port: int = 53, + source: Optional[str] = None, + source_port: int = 0, + ignore_unexpected: bool = False, + one_rr_per_rrset: bool = False, + ignore_trailing: bool = False, + udp_sock: Optional[Any] = None, + tcp_sock: Optional[Any] = 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 + 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: Union[dns.message.Message, bytes], + expiration: Optional[float] = 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: Optional[float] = None, + one_rr_per_rrset: bool = False, + keyring: Optional[Dict[dns.name.Name, dns.tsig.Key]] = None, + request_mac: Optional[bytes] = 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: Optional[float] = None, + port: int = 53, + source: Optional[str] = None, + source_port: int = 0, + one_rr_per_rrset: bool = False, + ignore_trailing: bool = False, + sock: Optional[Any] = 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 + ) + cm = _make_socket(af, socket.SOCK_STREAM, source) + with cm as s: + if not sock: + _connect(s, destination, 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 _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_dot_ssl_context( + server_hostname: Optional[str], verify: Union[bool, str] +) -> ssl.SSLContext: + cafile: Optional[str] = None + capath: Optional[str] = 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") + ssl_context = ssl.create_default_context(cafile=cafile, capath=capath) + ssl_context.minimum_version = ssl.TLSVersion.TLSv1_2 + if server_hostname is None: + ssl_context.check_hostname = False + ssl_context.set_alpn_protocols(["dot"]) + if verify is False: + ssl_context.verify_mode = ssl.CERT_NONE + return ssl_context + + +def tls( + q: dns.message.Message, + where: str, + timeout: Optional[float] = None, + port: int = 853, + source: Optional[str] = None, + source_port: int = 0, + one_rr_per_rrset: bool = False, + ignore_trailing: bool = False, + sock: Optional[ssl.SSLSocket] = None, + ssl_context: Optional[ssl.SSLContext] = None, + server_hostname: Optional[str] = None, + verify: Union[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 + ) + if ssl_context is None and not sock: + ssl_context = _make_dot_ssl_context(server_hostname, verify) + + with _make_socket( + af, + socket.SOCK_STREAM, + source, + ssl_context=ssl_context, + server_hostname=server_hostname, + ) 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: Optional[float] = None, + port: int = 853, + source: Optional[str] = None, + source_port: int = 0, + one_rr_per_rrset: bool = False, + ignore_trailing: bool = False, + connection: Optional[dns.quic.SyncQuicConnection] = None, + verify: Union[bool, str] = True, + server_hostname: Optional[str] = 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. + + *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. + + Returns a ``dns.message.Message``. + """ + + if not dns.quic.have_quic: + raise NoDOQ("DNS-over-QUIC is not available.") # pragma: no cover + + 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=server_hostname + ) + the_manager = manager # for type checking happiness + + with manager: + if not connection: + the_connection = the_manager.connect(where, port, source, source_port) + (start, expiration) = _compute_times(timeout) + with the_connection.make_stream(timeout) as stream: + 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 + + +def xfr( + where: str, + zone: Union[dns.name.Name, str], + rdtype: Union[dns.rdatatype.RdataType, str] = dns.rdatatype.AXFR, + rdclass: Union[dns.rdataclass.RdataClass, str] = dns.rdataclass.IN, + timeout: Optional[float] = None, + port: int = 53, + keyring: Optional[Dict[dns.name.Name, dns.tsig.Key]] = None, + keyname: Optional[Union[dns.name.Name, str]] = None, + relativize: bool = True, + lifetime: Optional[float] = None, + source: Optional[str] = None, + source_port: int = 0, + serial: int = 0, + use_udp: bool = False, + keyalgorithm: Union[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. + """ + + 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 = dns.rrset.from_text(zone, 0, "IN", "SOA", ". . %u 0 0 0 0" % serial) + q.authority.append(rrset) + if keyring is not None: + q.use_tsig(keyring, keyname, algorithm=keyalgorithm) + wire = q.to_wire() + (af, destination, source) = _destination_and_source( + where, port, source, source_port + ) + 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: + (_, expiration) = _compute_times(lifetime) + _connect(s, destination, expiration) + l = len(wire) + if use_udp: + _udp_send(s, wire, None, expiration) + else: + tcpmsg = struct.pack("!H", l) + wire + _net_write(s, tcpmsg, expiration) + done = False + delete_mode = True + expecting_SOA = False + soa_rrset = None + if relativize: + origin = zone + oname = dns.name.empty + else: + origin = None + oname = zone + tsig_ctx = None + while not done: + (_, mexpiration) = _compute_times(timeout) + if mexpiration is None or ( + expiration is not None and mexpiration > expiration + ): + mexpiration = expiration + if use_udp: + (wire, _) = _udp_recv(s, 65535, mexpiration) + else: + ldata = _net_read(s, 2, mexpiration) + (l,) = struct.unpack("!H", ldata) + wire = _net_read(s, l, mexpiration) + is_ixfr = rdtype == dns.rdatatype.IXFR + r = dns.message.from_wire( + wire, + keyring=q.keyring, + request_mac=q.mac, + xfr=True, + origin=origin, + tsig_ctx=tsig_ctx, + multi=True, + one_rr_per_rrset=is_ixfr, + ) + rcode = r.rcode() + if rcode != dns.rcode.NOERROR: + raise TransferError(rcode) + tsig_ctx = r.tsig_ctx + answer_index = 0 + if soa_rrset is None: + if not r.answer or r.answer[0].name != oname: + raise dns.exception.FormError("No answer or RRset not for qname") + rrset = r.answer[0] + if rrset.rdtype != dns.rdatatype.SOA: + raise dns.exception.FormError("first RRset is not an SOA") + answer_index = 1 + soa_rrset = rrset.copy() + if rdtype == dns.rdatatype.IXFR: + if dns.serial.Serial(soa_rrset[0].serial) <= serial: + # + # We're already up-to-date. + # + done = True + else: + expecting_SOA = True + # + # Process SOAs in the answer section (other than the initial + # SOA in the first message). + # + for rrset in r.answer[answer_index:]: + if done: + raise dns.exception.FormError("answers after final SOA") + if rrset.rdtype == dns.rdatatype.SOA and rrset.name == oname: + if expecting_SOA: + if rrset[0].serial != serial: + raise dns.exception.FormError("IXFR base serial mismatch") + expecting_SOA = False + elif rdtype == dns.rdatatype.IXFR: + delete_mode = not delete_mode + # + # If this SOA RRset 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 rrset == soa_rrset and ( + rdtype == dns.rdatatype.AXFR + or (rdtype == dns.rdatatype.IXFR and delete_mode) + ): + done = True + elif expecting_SOA: + # + # We made an IXFR request and are expecting another + # SOA RR, but saw something else, so this must be an + # AXFR response. + # + rdtype = dns.rdatatype.AXFR + expecting_SOA = False + if done and q.keyring and not r.had_tsig: + raise dns.exception.FormError("missing TSIG") + yield 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( + where: str, + txn_manager: dns.transaction.TransactionManager, + query: Optional[dns.message.Message] = None, + port: int = 53, + timeout: Optional[float] = None, + lifetime: Optional[float] = None, + source: Optional[str] = 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.UDPMode.NEVER``, i.e. only use + TCP. Other possibilities are ``dns.UDPMode.TRY_FIRST``, which + means "try UDP but fallback to TCP if needed", and + ``dns.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) + rdtype = query.question[0].rdtype + is_ixfr = rdtype == dns.rdatatype.IXFR + origin = txn_manager.from_wire_origin() + wire = query.to_wire() + (af, destination, source) = _destination_and_source( + where, port, source, source_port + ) + (_, expiration) = _compute_times(lifetime) + retry = True + while retry: + retry = False + if is_ixfr and udp_mode != UDPMode.NEVER: + sock_type = socket.SOCK_DGRAM + is_udp = True + else: + sock_type = socket.SOCK_STREAM + is_udp = False + with _make_socket(af, sock_type, source) as s: + _connect(s, destination, expiration) + 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 + 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, + ) + try: + done = inbound.process_message(r) + except dns.xfr.UseTCP: + assert is_udp # should not happen if we used TCP! + if udp_mode == UDPMode.ONLY: + raise + done = True + retry = True + udp_mode = UDPMode.NEVER + continue + tsig_ctx = r.tsig_ctx + if not retry and query.keyring and not r.had_tsig: + raise dns.exception.FormError("missing TSIG") diff --git a/venv/Lib/site-packages/dns/quic/__init__.py b/venv/Lib/site-packages/dns/quic/__init__.py new file mode 100644 index 00000000..20aff345 --- /dev/null +++ b/venv/Lib/site-packages/dns/quic/__init__.py @@ -0,0 +1,75 @@ +# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license + +import dns._features +import dns.asyncbackend + +if dns._features.have("doq"): + import aioquic.quic.configuration # type: ignore + + from dns._asyncbackend import NullContext + from dns.quic._asyncio import ( + AsyncioQuicConnection, + AsyncioQuicManager, + AsyncioQuicStream, + ) + from dns.quic._common import AsyncQuicConnection, AsyncQuicManager + from dns.quic._sync import SyncQuicConnection, SyncQuicManager, SyncQuicStream + + 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 = {"asyncio": (null_factory, _asyncio_manager_factory)} + + if dns._features.have("trio"): + import trio + + from dns.quic._trio import ( # pylint: disable=ungrouped-imports + TrioQuicConnection, + TrioQuicManager, + 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 + + from typing import Any + + 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 diff --git a/venv/Lib/site-packages/dns/quic/__pycache__/__init__.cpython-312.pyc b/venv/Lib/site-packages/dns/quic/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..189878e116459858a673c7ad4c539d855712155c GIT binary patch literal 3130 zcmb7GTW{P%6rNdouW_=QIJ>#^vT2$?fFuwu6(t3gidMaaOA8NHB+FfUQ>SY?9eZ20 zN>PAF^a=6Sil=aqAgYk~AH3|+N~zpRR6>>DEzMR6@WeUe^=3Cj2u7N3#^0QoIdjgL zIsU1$Gew{*#NIA`nIPm>{0NKK=+tjeLT(bB=(I!>%G-!S4Z08uMYr395<#8 z0v6`**-<41tx$?v2_->sWwDgBQcB9|P&zD0kvJDEby{6Ymz7r1{5w|4Slvpu)uZ%S zvLXW)S9Mog;o{SWOrn%WIKV|8=-s8fHLMI%5+Nsu-m^k<`F4aG zfG7tz*9%;>$#roq2V7s1lR4K9+(47-uF&zJ-*KT?U(lV)Yt-+UEtg8i9NRNK^u`50 zd(^Gi1=D%AY!-mEje=)7w%^l2pVe$_-e7)r3wO?AhGvaNe3_FwW@nmYV=ipea}M^| zA(sxhaEq>x2-ZJe2y76b08JWdy&vEv@fsll<|Ls*&XF}b%PHBn>pt{=N}63t1uaq z>V@j!xt7V31j!8Ddg)F&UrpreK#@Z8knjTo_-5SzF4{4l&$AwA{eD#o0zkC_H#c=dD-WVPDhRs8Nan!mOV#xB_pj8_)78W@ z!vR0unD2`oGaYpFd1!Y!I^>#;4vE0gZGJ{ip^o)u09yUL(6|ynH_PotFFbgJq0YYK zEE=|Iml?RavPQV7!GW_X>Zvx&(md@j%$k7eqKTHt1G#TqK3J0v-jTT#inWsziYx+a%pFY_GvMdMC?t_L(I~_0rg3qIH9{{R!VD(T{p85X#&+^QVAJpWT zsx-LeG805!!waV2`l78_#yIuk4G|YFKqFSO!L*%@h#)I6zdTy+i8(YsEuH_Ej$<5q2@gTi_HU6FSmGn(lEj?9D zOmX8Khs}hecBKH5niNI?b@zc%6Er{J&xMc}dMZHzkPpPWq=;adj zB~`s#)=I$$%fnLJamXg|6X6Z0b@agsxtEXv3FO-p3M0Sq_Js=R)DVghJ+dMyVnrAi z875Od#tg5_?6!Et1wMsZ$M8E%D8?-Gg7OLdLAXl!%K;feo^`yF)?&%949F7tTZ}mj zXc3;vVyinkEbWq^x;ztH+uyXxb{3gz5jX}rxoFMb$R7~PJ)>1=^iNj=;*)ri&aOon zhB|uzVHyFQ$zDUqBj8)ZFEBqMhc|(8y8u?mMuaBER^2BAnm`l{(PqJaaVhHr;aN5X zxN|cc;lszV^CAM*1o^h*|Eq&Ka0!goHt(Qv0@OMlr`{Wmk2XO0L+ZU$H z-R*t~=fIB|J|NuaT>n{yzacvh%XLFIiIZ9_E4NsdOYtN#;&;M5pHz#62JvRNY!v4C z8RXioezGayix;&khCis84razl4)7Eg&G|CZ_@76-G3IykwObcrcuNAFPYhER!#9d2 zJccI?GzcULj%7J^5HtAW>UZI)!5HQ}#!rKB$-V3+Q9rn~W`XBshDne;jlk1N%WTXd zYzRmAtG*X|!w4e?&jR={ud-;`^Fb)%mEK|SzY;USTk$$O3!@kWZWUldpp@2Qgl6xP zzK25Wvm+lLSr@W3A^U}KSJ-o39DO8CY(>Oq>@fk@N|ERZ`iuBpO?>ZRrg!<|rYJ^- zHzgtZ;zouH99TV5ojz3^IbH2LQx$t3##5hN|M>cPe4rK|xHWb+K72nRZ;4_u1&bw9 zn-WbvwHc$yBO5U?dbs-9JJpeQtBL+wxmqIsL=>XWZ%Cv&w|w%EIJhO!Xy;=Buoa6$ R#~u@atwbBQi5w5PzX59u%qIW< literal 0 HcmV?d00001 diff --git a/venv/Lib/site-packages/dns/quic/__pycache__/_asyncio.cpython-312.pyc b/venv/Lib/site-packages/dns/quic/__pycache__/_asyncio.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..918200d78d168aa75d70529e568d985be972b3b9 GIT binary patch literal 16888 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 literal 0 HcmV?d00001 diff --git a/venv/Lib/site-packages/dns/quic/__pycache__/_sync.cpython-312.pyc b/venv/Lib/site-packages/dns/quic/__pycache__/_sync.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9288aea03af595f67114c4ea28957014b8884ea6 GIT binary patch literal 15831 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 U`qSR~FEQ2daP$`xTnH-tKkcsJyZ`_I literal 0 HcmV?d00001 diff --git a/venv/Lib/site-packages/dns/quic/__pycache__/_trio.cpython-312.pyc b/venv/Lib/site-packages/dns/quic/__pycache__/_trio.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..91631deea4830673330b918c3dd6e6e929346561 GIT binary patch literal 13723 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 literal 0 HcmV?d00001 diff --git a/venv/Lib/site-packages/dns/quic/_asyncio.py b/venv/Lib/site-packages/dns/quic/_asyncio.py new file mode 100644 index 00000000..0f44331f --- /dev/null +++ b/venv/Lib/site-packages/dns/quic/_asyncio.py @@ -0,0 +1,228 @@ +# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license + +import asyncio +import socket +import ssl +import struct +import time + +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 receive(self, timeout=None): + expiration = self._expiration_from_timeout(timeout) + 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 + + 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. + async with self._wake_timer: + self._wake_timer.notify_all() + except Exception: + pass + finally: + self._done = True + async with self._wake_timer: + self._wake_timer.notify_all() + self._handshake_complete.set() + + async def _wait_for_wake_timer(self): + async with self._wake_timer: + await self._wake_timer.wait() + + 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 + 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): + 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._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) + async with self._wake_timer: + self._wake_timer.notify_all() + + 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: + 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() + async with self._wake_timer: + self._wake_timer.notify_all() + try: + await self._receiver_task + except asyncio.CancelledError: + pass + try: + await self._sender_task + except asyncio.CancelledError: + pass + await self._socket.close() + + +class AsyncioQuicManager(AsyncQuicManager): + def __init__(self, conf=None, verify_mode=ssl.CERT_REQUIRED, server_name=None): + super().__init__(conf, verify_mode, AsyncioQuicConnection, server_name) + + 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/venv/Lib/site-packages/dns/quic/_common.py b/venv/Lib/site-packages/dns/quic/_common.py new file mode 100644 index 00000000..0eacc691 --- /dev/null +++ b/venv/Lib/site-packages/dns/quic/_common.py @@ -0,0 +1,224 @@ +# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license + +import copy +import functools +import socket +import struct +import time +from typing import Any, Optional + +import aioquic.quic.configuration # type: ignore +import aioquic.quic.connection # type: ignore + +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 + + +class BaseQuicStream: + def __init__(self, connection, stream_id): + self._connection = connection + self._stream_id = stream_id + self._buffer = Buffer() + self._expecting = 0 + + def id(self): + return self._stream_id + + 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 UnexpectedEOF. + + def _encapsulate(self, 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) + 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 = {} + 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 close_stream(self, stream_id): + del self._streams[stream_id] + + 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: Optional[float] = None) -> Any: + pass + + +class BaseQuicManager: + def __init__(self, conf, verify_mode, connection_factory, server_name=None): + self._connections = {} + self._connection_factory = connection_factory + self._session_tickets = {} + if conf is None: + verify_path = None + if isinstance(verify_mode, str): + verify_path = verify_mode + verify_mode = True + conf = aioquic.quic.configuration.QuicConfiguration( + alpn_protocols=["doq", "doq-i03"], + verify_mode=verify_mode, + server_name=server_name, + ) + if verify_path is not None: + conf.load_verify_locations(verify_path) + self._conf = conf + + def _connect( + self, address, port=853, source=None, source_port=0, want_session_ticket=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 + qconn = aioquic.quic.connection.QuicConnection( + configuration=conf, + session_ticket_handler=session_ticket_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 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 + + +class AsyncQuicManager(BaseQuicManager): + def connect(self, address, port=853, source=None, source_port=0): + raise NotImplementedError diff --git a/venv/Lib/site-packages/dns/quic/_sync.py b/venv/Lib/site-packages/dns/quic/_sync.py new file mode 100644 index 00000000..120cb5f3 --- /dev/null +++ b/venv/Lib/site-packages/dns/quic/_sync.py @@ -0,0 +1,238 @@ +# 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.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, +) + +# Avoid circularity with dns.query +if hasattr(selectors, "PollSelector"): + _selector_class = selectors.PollSelector # type: ignore +else: + _selector_class = selectors.SelectSelector # type: ignore + + +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 receive(self, timeout=None): + expiration = self._expiration_from_timeout(timeout) + 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.socket(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: + sel = _selector_class() + 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 + finally: + with self._lock: + self._done = True + # 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): + 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 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 + self._manager.closed(self._peer[0], self._peer[1]) + self._closed = True + self._connection.close() + self._send_wakeup.send(b"\x01") + self._worker_thread.join() + + +class SyncQuicManager(BaseQuicManager): + def __init__(self, conf=None, verify_mode=ssl.CERT_REQUIRED, server_name=None): + super().__init__(conf, verify_mode, SyncQuicConnection, server_name) + self._lock = threading.Lock() + + def connect( + self, address, port=853, source=None, source_port=0, want_session_ticket=True + ): + with self._lock: + (connection, start) = self._connect( + address, port, source, source_port, want_session_ticket + ) + 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 __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/venv/Lib/site-packages/dns/quic/_trio.py b/venv/Lib/site-packages/dns/quic/_trio.py new file mode 100644 index 00000000..35e36b98 --- /dev/null +++ b/venv/Lib/site-packages/dns/quic/_trio.py @@ -0,0 +1,210 @@ +# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license + +import socket +import ssl +import struct +import time + +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 receive(self, timeout=None): + if timeout is None: + context = NullContext(None) + else: + context = trio.move_on_after(timeout) + with context: + 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 + ) 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._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): + 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: + 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 + ): + super().__init__(conf, verify_mode, TrioQuicConnection, server_name) + 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/venv/Lib/site-packages/dns/rcode.py b/venv/Lib/site-packages/dns/rcode.py new file mode 100644 index 00000000..8e6386f8 --- /dev/null +++ b/venv/Lib/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 + +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): + 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/venv/Lib/site-packages/dns/rdata.py b/venv/Lib/site-packages/dns/rdata.py new file mode 100644 index 00000000..024fd8f6 --- /dev/null +++ b/venv/Lib/site-packages/dns/rdata.py @@ -0,0 +1,884 @@ +# 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 itertools +import random +from importlib import import_module +from typing import Any, Dict, Optional, Tuple, Union + +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 += "\\%03d" % c + 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, rdtype): + """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: Optional[dns.name.Name] = 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: Optional[Any], + compress: Optional[dns.name.CompressType] = None, + origin: Optional[dns.name.Name] = None, + canonicalize: bool = False, + ) -> bytes: + raise NotImplementedError # pragma: no cover + + def to_wire( + self, + file: Optional[Any] = None, + compress: Optional[dns.name.CompressType] = None, + origin: Optional[dns.name.Name] = None, + canonicalize: bool = False, + ) -> bytes: + """Convert an rdata to wire format. + + Returns a ``bytes`` or ``None``. + """ + + if file: + return self._to_wire(file, compress, origin, canonicalize) + else: + f = io.BytesIO() + self._to_wire(f, compress, origin, canonicalize) + return f.getvalue() + + def to_generic( + self, origin: Optional[dns.name.Name] = None + ) -> "dns.rdata.GenericRdata": + """Creates a dns.rdata.GenericRdata equivalent of this rdata. + + Returns a ``dns.rdata.GenericRdata``. + """ + return dns.rdata.GenericRdata( + self.rdclass, self.rdtype, self.to_wire(origin=origin) + ) + + def to_digestable(self, origin: Optional[dns.name.Name] = None) -> bytes: + """Convert rdata to a format suitable for digesting in hashes. This + is also the DNSSEC canonical form. + + Returns a ``bytes``. + """ + + return self.to_wire(origin=origin, canonicalize=True) + + 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. + """ + 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: Optional[dns.name.Name] = None, + relativize: bool = True, + relativize_to: Optional[dns.name.Name] = 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: Optional[dns.name.Name] = 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( + "'{}' object has no attribute '{}'".format( + self.__class__.__name__, key + ) + ) + if key in ("rdclass", "rdtype"): + raise AttributeError( + "Cannot overwrite '{}' attribute '{}'".format( + self.__class__.__name__, 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: Optional[int] = 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) + 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) + 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, rdtype, data): + super().__init__(rdclass, rdtype) + self.data = data + + def to_text( + self, + origin: Optional[dns.name.Name] = None, + relativize: bool = True, + **kw: Dict[str, Any], + ) -> str: + return r"\# %d " % len(self.data) + _hexify(self.data, **kw) + + @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) + + @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" + + +def get_rdata_class(rdclass, rdtype): + cls = _rdata_classes.get((rdclass, rdtype)) + if not cls: + cls = _rdata_classes.get((dns.rdatatype.ANY, rdtype)) + if not cls: + 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: + cls = GenericRdata + _rdata_classes[(rdclass, rdtype)] = cls + return cls + + +def from_text( + rdclass: Union[dns.rdataclass.RdataClass, str], + rdtype: Union[dns.rdatatype.RdataType, str], + tok: Union[dns.tokenizer.Tokenizer, str], + origin: Optional[dns.name.Name] = None, + relativize: bool = True, + relativize_to: Optional[dns.name.Name] = None, + idna_codec: Optional[dns.name.IDNACodec] = 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) + rdclass = dns.rdataclass.RdataClass.make(rdclass) + rdtype = dns.rdatatype.RdataType.make(rdtype) + cls = get_rdata_class(rdclass, rdtype) + 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: Union[dns.rdataclass.RdataClass, str], + rdtype: Union[dns.rdatatype.RdataType, str], + parser: dns.wire.Parser, + origin: Optional[dns.name.Name] = 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) + with dns.exception.ExceptionWrapper(dns.exception.FormError): + return cls.from_wire_parser(rdclass, rdtype, parser, origin) + + +def from_wire( + rdclass: Union[dns.rdataclass.RdataClass, str], + rdtype: Union[dns.rdatatype.RdataType, str], + wire: bytes, + current: int, + rdlen: int, + origin: Optional[dns.name.Name] = 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 module implementing the type in the usual dnspython + way. + + *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) + _rdata_classes[(rdclass, rdtype)] = getattr( + implementation, rdtype_text.replace("-", "_") + ) + dns.rdatatype.register_type(rdtype, rdtype_text, is_singleton) diff --git a/venv/Lib/site-packages/dns/rdataclass.py b/venv/Lib/site-packages/dns/rdataclass.py new file mode 100644 index 00000000..89b85a79 --- /dev/null +++ b/venv/Lib/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/venv/Lib/site-packages/dns/rdataset.py b/venv/Lib/site-packages/dns/rdataset.py new file mode 100644 index 00000000..8bff58d7 --- /dev/null +++ b/venv/Lib/site-packages/dns/rdataset.py @@ -0,0 +1,516 @@ +# 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, Optional, Union, 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 = 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 + + def add( # pylint: disable=arguments-differ,arguments-renamed + self, rd: dns.rdata.Rdata, ttl: Optional[int] = 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 "[%s]" % ", ".join("<%s>" % 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: Optional[dns.name.Name] = None, + origin: Optional[dns.name.Name] = None, + relativize: bool = True, + override_rdclass: Optional[dns.rdataclass.RdataClass] = 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( + "{}{}{} {}\n".format( + ntext, + pad, + dns.rdataclass.to_text(rdclass), + dns.rdatatype.to_text(self.rdtype), + ) + ) + else: + for rd in self: + extra = "" + if want_comments: + if rd.rdcomment: + extra = f" ;{rd.rdcomment}" + s.write( + "%s%s%d %s %s %s%s\n" + % ( + ntext, + pad, + self.ttl, + dns.rdataclass.to_text(rdclass), + dns.rdatatype.to_text(self.rdtype), + rd.to_text(origin=origin, relativize=relativize, **kw), + extra, + ) + ) + # + # 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: Optional[dns.name.CompressType] = None, + origin: Optional[dns.name.Name] = None, + override_rdclass: Optional[dns.rdataclass.RdataClass] = 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: Union[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)) + + +@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()) + + def copy(self): + return ImmutableRdataset(super().copy()) + + def union(self, other): + return ImmutableRdataset(super().union(other)) + + def intersection(self, other): + return ImmutableRdataset(super().intersection(other)) + + def difference(self, other): + return ImmutableRdataset(super().difference(other)) + + def symmetric_difference(self, other): + return ImmutableRdataset(super().symmetric_difference(other)) + + +def from_text_list( + rdclass: Union[dns.rdataclass.RdataClass, str], + rdtype: Union[dns.rdatatype.RdataType, str], + ttl: int, + text_rdatas: Collection[str], + idna_codec: Optional[dns.name.IDNACodec] = None, + origin: Optional[dns.name.Name] = None, + relativize: bool = True, + relativize_to: Optional[dns.name.Name] = 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: Union[dns.rdataclass.RdataClass, str], + rdtype: Union[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/venv/Lib/site-packages/dns/rdatatype.py b/venv/Lib/site-packages/dns/rdatatype.py new file mode 100644 index 00000000..e6c58186 --- /dev/null +++ b/venv/Lib/site-packages/dns/rdatatype.py @@ -0,0 +1,332 @@ +# 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 + 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 + 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: + 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 +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 +TA = RdataType.TA +DLV = RdataType.DLV + +### END generated RdataType constants diff --git a/venv/Lib/site-packages/dns/rdtypes/ANY/AFSDB.py b/venv/Lib/site-packages/dns/rdtypes/ANY/AFSDB.py new file mode 100644 index 00000000..06a3b970 --- /dev/null +++ b/venv/Lib/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/venv/Lib/site-packages/dns/rdtypes/ANY/AMTRELAY.py b/venv/Lib/site-packages/dns/rdtypes/ANY/AMTRELAY.py new file mode 100644 index 00000000..ed2b072b --- /dev/null +++ b/venv/Lib/site-packages/dns/rdtypes/ANY/AMTRELAY.py @@ -0,0 +1,91 @@ +# 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.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 "%d %d %d %s" % ( + self.precedence, + self.discovery_optional, + 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/venv/Lib/site-packages/dns/rdtypes/ANY/AVC.py b/venv/Lib/site-packages/dns/rdtypes/ANY/AVC.py new file mode 100644 index 00000000..a27ae2d6 --- /dev/null +++ b/venv/Lib/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/venv/Lib/site-packages/dns/rdtypes/ANY/CAA.py b/venv/Lib/site-packages/dns/rdtypes/ANY/CAA.py new file mode 100644 index 00000000..2e6a7e7e --- /dev/null +++ b/venv/Lib/site-packages/dns/rdtypes/ANY/CAA.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. + +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 '%u %s "%s"' % ( + 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/venv/Lib/site-packages/dns/rdtypes/ANY/CDNSKEY.py b/venv/Lib/site-packages/dns/rdtypes/ANY/CDNSKEY.py new file mode 100644 index 00000000..b613409f --- /dev/null +++ b/venv/Lib/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/venv/Lib/site-packages/dns/rdtypes/ANY/CDS.py b/venv/Lib/site-packages/dns/rdtypes/ANY/CDS.py new file mode 100644 index 00000000..8312b972 --- /dev/null +++ b/venv/Lib/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/venv/Lib/site-packages/dns/rdtypes/ANY/CERT.py b/venv/Lib/site-packages/dns/rdtypes/ANY/CERT.py new file mode 100644 index 00000000..f369cc85 --- /dev/null +++ b/venv/Lib/site-packages/dns/rdtypes/ANY/CERT.py @@ -0,0 +1,116 @@ +# 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) + return "%s %d %s %s" % ( + certificate_type, + self.key_tag, + dns.dnssectypes.Algorithm.to_text(self.algorithm), + dns.rdata._base64ify(self.certificate, **kw), + ) + + @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/venv/Lib/site-packages/dns/rdtypes/ANY/CNAME.py b/venv/Lib/site-packages/dns/rdtypes/ANY/CNAME.py new file mode 100644 index 00000000..665e407c --- /dev/null +++ b/venv/Lib/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/venv/Lib/site-packages/dns/rdtypes/ANY/CSYNC.py b/venv/Lib/site-packages/dns/rdtypes/ANY/CSYNC.py new file mode 100644 index 00000000..2f972f6e --- /dev/null +++ b/venv/Lib/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 "%d %d%s" % (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/venv/Lib/site-packages/dns/rdtypes/ANY/DLV.py b/venv/Lib/site-packages/dns/rdtypes/ANY/DLV.py new file mode 100644 index 00000000..6c134f18 --- /dev/null +++ b/venv/Lib/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/venv/Lib/site-packages/dns/rdtypes/ANY/DNAME.py b/venv/Lib/site-packages/dns/rdtypes/ANY/DNAME.py new file mode 100644 index 00000000..bbf9186c --- /dev/null +++ b/venv/Lib/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/venv/Lib/site-packages/dns/rdtypes/ANY/DNSKEY.py b/venv/Lib/site-packages/dns/rdtypes/ANY/DNSKEY.py new file mode 100644 index 00000000..6d961a9f --- /dev/null +++ b/venv/Lib/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/venv/Lib/site-packages/dns/rdtypes/ANY/DS.py b/venv/Lib/site-packages/dns/rdtypes/ANY/DS.py new file mode 100644 index 00000000..58b3108d --- /dev/null +++ b/venv/Lib/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/venv/Lib/site-packages/dns/rdtypes/ANY/EUI48.py b/venv/Lib/site-packages/dns/rdtypes/ANY/EUI48.py new file mode 100644 index 00000000..c843be50 --- /dev/null +++ b/venv/Lib/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/venv/Lib/site-packages/dns/rdtypes/ANY/EUI64.py b/venv/Lib/site-packages/dns/rdtypes/ANY/EUI64.py new file mode 100644 index 00000000..f6d7e257 --- /dev/null +++ b/venv/Lib/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/venv/Lib/site-packages/dns/rdtypes/ANY/GPOS.py b/venv/Lib/site-packages/dns/rdtypes/ANY/GPOS.py new file mode 100644 index 00000000..312338f9 --- /dev/null +++ b/venv/Lib/site-packages/dns/rdtypes/ANY/GPOS.py @@ -0,0 +1,125 @@ +# 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 "{} {} {}".format( + self.latitude.decode(), self.longitude.decode(), 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/venv/Lib/site-packages/dns/rdtypes/ANY/HINFO.py b/venv/Lib/site-packages/dns/rdtypes/ANY/HINFO.py new file mode 100644 index 00000000..c2c45de0 --- /dev/null +++ b/venv/Lib/site-packages/dns/rdtypes/ANY/HINFO.py @@ -0,0 +1,66 @@ +# 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 '"{}" "{}"'.format( + 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/venv/Lib/site-packages/dns/rdtypes/ANY/HIP.py b/venv/Lib/site-packages/dns/rdtypes/ANY/HIP.py new file mode 100644 index 00000000..91669139 --- /dev/null +++ b/venv/Lib/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 "%u %s %s%s" % (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/venv/Lib/site-packages/dns/rdtypes/ANY/ISDN.py b/venv/Lib/site-packages/dns/rdtypes/ANY/ISDN.py new file mode 100644 index 00000000..fb01eab3 --- /dev/null +++ b/venv/Lib/site-packages/dns/rdtypes/ANY/ISDN.py @@ -0,0 +1,77 @@ +# 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 '"{}" "{}"'.format( + dns.rdata._escapify(self.address), dns.rdata._escapify(self.subaddress) + ) + else: + return '"%s"' % 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/venv/Lib/site-packages/dns/rdtypes/ANY/L32.py b/venv/Lib/site-packages/dns/rdtypes/ANY/L32.py new file mode 100644 index 00000000..09804c2d --- /dev/null +++ b/venv/Lib/site-packages/dns/rdtypes/ANY/L32.py @@ -0,0 +1,41 @@ +# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license + +import struct + +import dns.immutable +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/venv/Lib/site-packages/dns/rdtypes/ANY/L64.py b/venv/Lib/site-packages/dns/rdtypes/ANY/L64.py new file mode 100644 index 00000000..fb76808e --- /dev/null +++ b/venv/Lib/site-packages/dns/rdtypes/ANY/L64.py @@ -0,0 +1,47 @@ +# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license + +import struct + +import dns.immutable +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/venv/Lib/site-packages/dns/rdtypes/ANY/LOC.py b/venv/Lib/site-packages/dns/rdtypes/ANY/LOC.py new file mode 100644 index 00000000..a36a2c10 --- /dev/null +++ b/venv/Lib/site-packages/dns/rdtypes/ANY/LOC.py @@ -0,0 +1,354 @@ +# 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("%s value out of bounds" % desc) + 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("bad %s exponent" % desc) + base = (what & 0xF0) >> 4 + if base > 9: + raise dns.exception.FormError("bad %s base" % desc) + 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) + 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) + 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 = "%d %d %d.%03d %s %d %d %d.%03d %s %0.2fm" % ( + self.latitude[0], + self.latitude[1], + self.latitude[2], + self.latitude[3], + lat_hemisphere, + self.longitude[0], + self.longitude[1], + self.longitude[2], + self.longitude[3], + long_hemisphere, + self.altitude / 100.0, + ) + + # do not print default values + if ( + self.size != _default_size + or self.horizontal_precision != _default_hprec + or self.vertical_precision != _default_vprec + ): + text += " {:0.2f}m {:0.2f}m {:0.2f}m".format( + self.size / 100.0, + self.horizontal_precision / 100.0, + self.vertical_precision / 100.0, + ) + 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/venv/Lib/site-packages/dns/rdtypes/ANY/LP.py b/venv/Lib/site-packages/dns/rdtypes/ANY/LP.py new file mode 100644 index 00000000..312663f1 --- /dev/null +++ b/venv/Lib/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 "%d %s" % (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/venv/Lib/site-packages/dns/rdtypes/ANY/MX.py b/venv/Lib/site-packages/dns/rdtypes/ANY/MX.py new file mode 100644 index 00000000..0c300c5a --- /dev/null +++ b/venv/Lib/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/venv/Lib/site-packages/dns/rdtypes/ANY/NID.py b/venv/Lib/site-packages/dns/rdtypes/ANY/NID.py new file mode 100644 index 00000000..2f649178 --- /dev/null +++ b/venv/Lib/site-packages/dns/rdtypes/ANY/NID.py @@ -0,0 +1,47 @@ +# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license + +import struct + +import dns.immutable +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/venv/Lib/site-packages/dns/rdtypes/ANY/NINFO.py b/venv/Lib/site-packages/dns/rdtypes/ANY/NINFO.py new file mode 100644 index 00000000..b177bddb --- /dev/null +++ b/venv/Lib/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/venv/Lib/site-packages/dns/rdtypes/ANY/NS.py b/venv/Lib/site-packages/dns/rdtypes/ANY/NS.py new file mode 100644 index 00000000..c3f34ce9 --- /dev/null +++ b/venv/Lib/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/venv/Lib/site-packages/dns/rdtypes/ANY/NSEC.py b/venv/Lib/site-packages/dns/rdtypes/ANY/NSEC.py new file mode 100644 index 00000000..340525a6 --- /dev/null +++ b/venv/Lib/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 "{}{}".format(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/venv/Lib/site-packages/dns/rdtypes/ANY/NSEC3.py b/venv/Lib/site-packages/dns/rdtypes/ANY/NSEC3.py new file mode 100644 index 00000000..d71302b7 --- /dev/null +++ b/venv/Lib/site-packages/dns/rdtypes/ANY/NSEC3.py @@ -0,0 +1,126 @@ +# 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.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 "%u %u %u %s %s%s" % ( + 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/venv/Lib/site-packages/dns/rdtypes/ANY/NSEC3PARAM.py b/venv/Lib/site-packages/dns/rdtypes/ANY/NSEC3PARAM.py new file mode 100644 index 00000000..d1e62ebc --- /dev/null +++ b/venv/Lib/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 "%u %u %u %s" % (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/venv/Lib/site-packages/dns/rdtypes/ANY/OPENPGPKEY.py b/venv/Lib/site-packages/dns/rdtypes/ANY/OPENPGPKEY.py new file mode 100644 index 00000000..4d7a4b6c --- /dev/null +++ b/venv/Lib/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) + + @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/venv/Lib/site-packages/dns/rdtypes/ANY/OPT.py b/venv/Lib/site-packages/dns/rdtypes/ANY/OPT.py new file mode 100644 index 00000000..d343dfa5 --- /dev/null +++ b/venv/Lib/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/venv/Lib/site-packages/dns/rdtypes/ANY/PTR.py b/venv/Lib/site-packages/dns/rdtypes/ANY/PTR.py new file mode 100644 index 00000000..98c36167 --- /dev/null +++ b/venv/Lib/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/venv/Lib/site-packages/dns/rdtypes/ANY/RP.py b/venv/Lib/site-packages/dns/rdtypes/ANY/RP.py new file mode 100644 index 00000000..9b74549d --- /dev/null +++ b/venv/Lib/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 "{} {}".format(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/venv/Lib/site-packages/dns/rdtypes/ANY/RRSIG.py b/venv/Lib/site-packages/dns/rdtypes/ANY/RRSIG.py new file mode 100644 index 00000000..8beb4237 --- /dev/null +++ b/venv/Lib/site-packages/dns/rdtypes/ANY/RRSIG.py @@ -0,0 +1,157 @@ +# 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 "%s %d %d %d %s %s %d %s %s" % ( + dns.rdatatype.to_text(self.type_covered), + self.algorithm, + self.labels, + self.original_ttl, + posixtime_to_sigtime(self.expiration), + posixtime_to_sigtime(self.inception), + self.key_tag, + self.signer.choose_relativity(origin, relativize), + dns.rdata._base64ify(self.signature, **kw), + ) + + @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) diff --git a/venv/Lib/site-packages/dns/rdtypes/ANY/RT.py b/venv/Lib/site-packages/dns/rdtypes/ANY/RT.py new file mode 100644 index 00000000..5a4d45cf --- /dev/null +++ b/venv/Lib/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/venv/Lib/site-packages/dns/rdtypes/ANY/SMIMEA.py b/venv/Lib/site-packages/dns/rdtypes/ANY/SMIMEA.py new file mode 100644 index 00000000..55d87bf8 --- /dev/null +++ b/venv/Lib/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/venv/Lib/site-packages/dns/rdtypes/ANY/SOA.py b/venv/Lib/site-packages/dns/rdtypes/ANY/SOA.py new file mode 100644 index 00000000..09aa8321 --- /dev/null +++ b/venv/Lib/site-packages/dns/rdtypes/ANY/SOA.py @@ -0,0 +1,86 @@ +# 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 "%s %s %d %d %d %d %d" % ( + 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/venv/Lib/site-packages/dns/rdtypes/ANY/SPF.py b/venv/Lib/site-packages/dns/rdtypes/ANY/SPF.py new file mode 100644 index 00000000..1df3b705 --- /dev/null +++ b/venv/Lib/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/venv/Lib/site-packages/dns/rdtypes/ANY/SSHFP.py b/venv/Lib/site-packages/dns/rdtypes/ANY/SSHFP.py new file mode 100644 index 00000000..d2c4b073 --- /dev/null +++ b/venv/Lib/site-packages/dns/rdtypes/ANY/SSHFP.py @@ -0,0 +1,68 @@ +# 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) + return "%d %d %s" % ( + self.algorithm, + self.fp_type, + dns.rdata._hexify(self.fingerprint, chunksize=chunksize, **kw), + ) + + @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/venv/Lib/site-packages/dns/rdtypes/ANY/TKEY.py b/venv/Lib/site-packages/dns/rdtypes/ANY/TKEY.py new file mode 100644 index 00000000..5b490b82 --- /dev/null +++ b/venv/Lib/site-packages/dns/rdtypes/ANY/TKEY.py @@ -0,0 +1,142 @@ +# 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) + text = "%s %u %u %u %u %s" % ( + str(_algorithm), + self.inception, + self.expiration, + self.mode, + self.error, + dns.rdata._base64ify(self.key, 0), + ) + if len(self.other) > 0: + text += " %s" % (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) + 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/venv/Lib/site-packages/dns/rdtypes/ANY/TLSA.py b/venv/Lib/site-packages/dns/rdtypes/ANY/TLSA.py new file mode 100644 index 00000000..4dffc553 --- /dev/null +++ b/venv/Lib/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/venv/Lib/site-packages/dns/rdtypes/ANY/TSIG.py b/venv/Lib/site-packages/dns/rdtypes/ANY/TSIG.py new file mode 100644 index 00000000..79423826 --- /dev/null +++ b/venv/Lib/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/venv/Lib/site-packages/dns/rdtypes/ANY/TXT.py b/venv/Lib/site-packages/dns/rdtypes/ANY/TXT.py new file mode 100644 index 00000000..6d4dae27 --- /dev/null +++ b/venv/Lib/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/venv/Lib/site-packages/dns/rdtypes/ANY/URI.py b/venv/Lib/site-packages/dns/rdtypes/ANY/URI.py new file mode 100644 index 00000000..2efbb305 --- /dev/null +++ b/venv/Lib/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 '%d %d "%s"' % (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/venv/Lib/site-packages/dns/rdtypes/ANY/X25.py b/venv/Lib/site-packages/dns/rdtypes/ANY/X25.py new file mode 100644 index 00000000..8375611d --- /dev/null +++ b/venv/Lib/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 '"%s"' % 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/venv/Lib/site-packages/dns/rdtypes/ANY/ZONEMD.py b/venv/Lib/site-packages/dns/rdtypes/ANY/ZONEMD.py new file mode 100644 index 00000000..c90e3ee1 --- /dev/null +++ b/venv/Lib/site-packages/dns/rdtypes/ANY/ZONEMD.py @@ -0,0 +1,66 @@ +# 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) + return "%d %d %d %s" % ( + self.serial, + self.scheme, + self.hash_algorithm, + dns.rdata._hexify(self.digest, chunksize=chunksize, **kw), + ) + + @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/venv/Lib/site-packages/dns/rdtypes/ANY/__init__.py b/venv/Lib/site-packages/dns/rdtypes/ANY/__init__.py new file mode 100644 index 00000000..3824a0a0 --- /dev/null +++ b/venv/Lib/site-packages/dns/rdtypes/ANY/__init__.py @@ -0,0 +1,68 @@ +# 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", + "EUI48", + "EUI64", + "GPOS", + "HINFO", + "HIP", + "ISDN", + "L32", + "L64", + "LOC", + "LP", + "MX", + "NID", + "NINFO", + "NS", + "NSEC", + "NSEC3", + "NSEC3PARAM", + "OPENPGPKEY", + "OPT", + "PTR", + "RP", + "RRSIG", + "RT", + "SMIMEA", + "SOA", + "SPF", + "SSHFP", + "TKEY", + "TLSA", + "TSIG", + "TXT", + "URI", + "X25", + "ZONEMD", +] diff --git a/venv/Lib/site-packages/dns/rdtypes/ANY/__pycache__/AFSDB.cpython-312.pyc b/venv/Lib/site-packages/dns/rdtypes/ANY/__pycache__/AFSDB.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f4d74204fb7f4e23e280161b3447cfc0c8673720 GIT binary patch literal 1105 zcmbVK%}*0S6rb7c$AVj;1PKs|9Q06HOD^WN|G-oE#nH}kzv7$&fGa<8}cbA

5Sr9v5P_A=5$H2>lh@`BjPA+c5XM;k~^-KNxVn{Vp3F`_dy z;m*=fCEuo{%x^^NR?11Eq#%9_&Iw^sbw#mhMP(UOa#JA-SRKH*cBYh#vP;i5R+l@v zG_#ypKDRA8&;flE6HmzoA4qc`*mE~XiFWRVbvB#S5v^L-4p@ms!vSYoj5E(-r!>k4 z=4=TpIeYzidQv0DjFdx2d1@SZZlD}J+Z)TR3sLb*558jk@G=2%BMm*g@j7sBM>M?VEahA zIdXUG_~7W^T$_pUxc@nv(f)Aiz6d?j#p!5l502$+GoHtmaxi=d+(_$Xbw(0tJv@hr zgWC3w_RqCxeh2vVQa;iQ1B)9*G;A2IZ?_!ub;EeqGMzM%Hw@difYt);2aJbpj_;CV z*;67*NfkVUEN$XC(=CDWw7>y#A-pstaS2G2RhWa`AG!*61*uHR!*0ToW@*dP&GOe3yR0K^pjx~^v&J(C4vxpY zGXZQ(L|UaRb@~g2<-!`(uX|q$m3RhXuPIMI^}_CrAoX7oQD-pJ!k%Gz-X&p zS$FO|=iYOD@A=Mm&;46VONcieB9 zoAmj}heY?P8T!+lPt7KcWeC?v_q1oq7&{R)da_AGD(zsQ>0r%E>rSyxcUKilF?0`FP0CiWC z+?DY&nTz9EMpp*%sr2R4q@s<>Iqe{o^+I0J4jxOKKj`jkFkdh^Yj?%4wdp~t(*yrc zLvPQK$8FmdjRj*(=&s?m5+ZL3l9WqLE0Sb}BxzceXR_FiNYbxnQdvjC;@1@NRLv_? zFHnTj*Mx@QLxWhcZq*454Xb&w5$>Kl`Iu{Y>SMT`nxF8kR?D7+6JGGDgECV40yJN~ z9D~=F@F?=fyjv1nZ#4ZMB@UDE82N*Wmb;tNu9OxQE z!nLddW6>Kg6@zUU>+%ON_-1hq)&zGtjA#<+Fan)zl>(t?PYjuyHUp=CFv{dIx+Gax zFoRM`lV&nG{ihV;AJovlsH$1ofnC-*I%EOs{6vA`+Gc>t>1;~VOx}jKHP@O!y|G|g zvIa?#hLhrUnX`e2&?~mP9vyW5ln7$&GpN3wBU?NPwk`xJ!mgV;m-jss#7EJN8&k_~ zR-^k%+(y?Mw+C+x-s`G%4VT#8wQcO#cX!~Y zfprTYZ9@L9`48*_TLia4fcO%{@SAen`Dr3=hj`r!0bP}J<(gi1k;vIjwcm-V~FJH={rXgzwQ z53wUy1a4{r1fu7Y{upTr{*vxS->t#+bqbvtsoU zxTj1E|CHw@&^X0qmf64$=mkTNnad{u)Vp=h#m(H>%$>5&jn&^Ux)*1L;7VtOA6PwT z1ZDsKy}HK~u(}7BDab)yE=JJs8?41t4sXqde8jLg%?KC*=tIr;WyRqi02h~dg2!Bp zD%bK#TF>MrpI*PmN;#4jU9HLArG=B8w@k2vD5d||ispvMfn0~kk&7g8E ztys)#hQ|syJ$3CAr7AU9i*7Ke%-{r7r){|6-o&u!PiHmS3vK!a4)E*hWx5Bu{n%|m zez&13>1rL~br#Yvv~A{Tfb#$di!l$*-l|7IuKgLRIkG8`9i8P1OBa4Gl|mc6@!J<} zUHGH4CUk9tdzYDS!+nop9p!~np9cKjzYX?cw?5H`T$V?Gt79r$jx%xFXH!w4dA92fkTy$konW16ua9(FcvU0dR}e^07*ZgjKk)=v zY29QoSF>+E?ka`k&8ANTpNY(QHA59KVDlwAk;y`Gl~$)A z>(Dd{hGrz4%Bi_bI+X?bv=%V=OG-+H{1R7okY|x7+^vqq#*5DpeFB5EFQA$u8y%fx zX-T@ruJ0eI>>qh>sIvcLrQ=j7^r(GjxqGSm=DFpQcSrAxt|qJPAC&kFA+i`+2;DgR zQ0RIjw3OsUbwRzU-!9%NR)quW!cavRS{=T>>%rk~j{ZaV%)S5{VdHV@UtsyyajVR@ z&Kn0O%yP55j!?6T!OOlPFrhmF_?MUcMQ|XDa@+xLmPS7JW?C+DEX@iYKXJfNydgBZ zC>!{RO;Ego=nz&vf=cAf5VDT#UHUf4@ei0$E5y;8Qa(i?`?u4XARWXZb$;O;twk}I z5Y;hb--}52j#J}t`)(WoQL*zyGHF<84Afd0D#+^l#r6K-O8@ZcxoZD=6(L^gx{(CN zSU#}I+&Nh79YWF&VmIW9&;{;}cC1JDRigWri>rt4AFf7^|8JZCIl);#{@DaniC@_1 zUxebjf@0KB3@kGsNwS)T2LxnkT2|FHNC+_}qkX6iTZ!AWqF+*FieyQV87c16Y;n@M zw35wAlE~2AAhq6BAK2%y4ToZdrnJ*geLqkB&2~N$Nc$OP?(`NbaDBDy0WP!|BYgYg z(XnU#RxX4ukM>O;w&E;zp4p5*|CtYlS(+_u*>4iGEw<2M_?RtlvkZC{3l%+CW_o%? zPhEtFjp7jJ88$ehXR?^ST8}c5wYwkM4@D;=B$j_E0#Sp0b58~3JQ>ujow}tcQ>MO?rKW-0@b3G*_q}_Mci&qr9q=5~U+qB+;Fl!Tsh1%C zir@kmFyethid!)VCzHxiRp5P}roane$~iCO#9LQ-(Gr zz1nZd#!jgf7b!{p0Ko jS|OzU>B*O^&s%f7Gt)bBy*typIiQ@Ypgj5`4MmT?vCp9Q literal 0 HcmV?d00001 diff --git a/venv/Lib/site-packages/dns/rdtypes/ANY/__pycache__/CAA.cpython-312.pyc b/venv/Lib/site-packages/dns/rdtypes/ANY/__pycache__/CAA.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..faa7bac640df2758395e1e0e8c078027804a1e9c GIT binary patch literal 3405 zcmbtW&2JmW6`%d&az#?B57$l;N33nhUZ;-grjQILaO)UOU?Wx9*ey_2$ab|mlvi5r zvNKD|Vkkf@1cYi6$UJ_Nb6Z+5vP!zeD0 z4#=5#AMxng`ZG)#$VN8jqCz}!1wNvM_9(VT zsla1?A(NQsjtB90^eVFXHDn90CfC(XW(!&}>yFz(Jl}4!3h8A|7auZe#%lATp{<8n zzXI}Y*gr-E&PK3Lt{}|AcmKi_#5yW+wy>rYBwMk?HLf7zR2tc7TUz5fUr=n-mO+-r zDSN<9^+?n@y2TX+t|2`W3YRWktbQN5{HaTr1WwVh%)s%z{KZOe!zWI#oXn~bU(XY4 z`NYQa`)^g68za|TK|UbPh0~9Fve|NuAz59UgK)~HXrji zR6`4FO<$`C+g!}5AtSjRrVD=WZ#B{8Y(BYb(C^r*i3{0&ZG@7$q65KTBDFi;`z`l1 zUy*utq$KN>kuJ_$fA}AutB2uA@(#^=evmia@(t6glrV8DJsnE4QpO}y48!r9z%atJ zVN#>wc)>Zsb{o@k%K@f@-Da3_sOiG`Q2jZ(;>(2igrq>Cr$Uipw-}~~ZMi0;p-k*x z8LTrq(c(xCEEkJ{=NtC!Bt{)}QmXUi3^GaL9z9=+j|4O%i?W3ECS4^!G< zpRsP3!JHp-8BEVTlPI2HvYXhuIs2M3H%pxWpDCNxqB)Q0tnJascyDy_;><55A>5uX zFOvb-O*O-ayftxO(f1BL)p&FB&8^GLL#ONFJL=xZ z;l_o{3tPvxg7>dAN6rFI*&7_*cx(NwcUB$@KXdQsRC9RxUS|5s3*S{+U>;eWNCJZ0 zGuun+@6bWUV19>Le+$}?iqL}CF9ROC+r9>NI`PpJCDHSZ>_vI6Ym3bidqtrNCjnIR zGgWoGk{_q}W8?IgF2{i-fEPlE*k)itNMLH2Wv95@jT2oYEOCW0pnl%*LX}__Ab-=T z;*eilqU_8pbjT>vN(H_V;9Eg;fcdXYoCXPf6}nZlH~92kM%%c$e)U0SaO2AQmEFwP zPG)RNd{4QnG&6cXSG}t?Gtcd(#4+_VBx>qD64NR<1h%Z+i)HgDsr(Hl4O9~r+Q|XP zPD^0Dk^*_JquUyZ{GfD`$X1m9V{Mv;1jUkDEmc!$>FstJx`saBW*~8<=W!52K2+J1 z2E_5^Lm7LPZ)05z1H4pF74%pHoSGOnR|9OH@=e_Z{|+cg*|Pgk=x8Q zwfwGjaz{Ja)bzUa(ctjy={v7%U4H+Cze>%)se8&)?6pqn1?vo8k=K3#-U6?22YD^% zciZ%{q2&#$|gd$7A?F8UruLtgO40o~UxHwcS zzf>lW@1r;iGnVQ39y|qHhJMba?B;tTN`c5TmK=iH`MXV>2Q}(K2MIvQY^dw%AEz2G zZoYUY*t-5+;clTh^1b`YS-}2nv7u}#zyEQa+Z)twzwo=Idal0IzvQRQk(2k8Q*HL` zA2iNwp1ZF+)1}XEp8rsJ_N(#M$r4rA%Kyv~(vy+CB7?j)n!zS(olKGHpk1oTwNw}F zWDL}Ff#hKbhZxc=zv2bhHlq3-W*Lj%k_k@%Z(di(aZv2Ry<9d46zCZGUoPsD)s@&G zNk-NpR5PC#@w)102kSlDmti6OA#|(g6YX2OT7E~%H??C7^P{`DiJja;Gk1!C9b_kV zv=dEj;_FckrRF-s-*w1pGc&Km=gqLkjc1lHJZ@CODEW59W%G<-{JH{9nOKrC4BNM& z_m4qcKcI#Yy$8rq*jhLcJy=Q@o}f0F01jc9icB$Cw*8#3uZ?{uV2>UnnodcX)wZ29h0=UV?64;5e>z0&%%d(dftQr$N6j sBVKy@=x>hhaz}Q!Baiv-a?|zW8~VDwF}Xh3LO{Yv`KLZ(g6Jdv1qOt=)c^nh literal 0 HcmV?d00001 diff --git a/venv/Lib/site-packages/dns/rdtypes/ANY/__pycache__/CDNSKEY.cpython-312.pyc b/venv/Lib/site-packages/dns/rdtypes/ANY/__pycache__/CDNSKEY.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d779fe933164de1a5e545f9ef2a0c77bf86d2178 GIT binary patch literal 765 zcmZuv&1=*^6rah*ZjId_g_iAM(ZgQ$u$#Mxpfz}C$%;jxAu!})wAp-`Nm}AfSWr(M z`!`7aN5s>f7RH;NytPCSdU7V4n2U3G@ArQ5d-LYayl;bn0@w|2P3W1)jJBp2Tw0PTQizofdU| zvD4ueU5XoY*$}2u!ioHgN5({?z32MN`Jqn5WBnPG%(ur1@pC*)Z8xxAV`6zR(K0VDQ%tl+#);N6FiXp<&#qFNy9^MSnI-rk4YNVZPj1rz z%b)pCROGf3Vk)wb?s2X;bH;FhOUGv2@Gr>N^ni??MQ?R!{YFidPEM80n?B~L^;P)D zY>M+ixY>HJdCiB}9t-&wkt1fVLbQ zcU){)H) zTBV^#%bdnZ#6+r7;W~n)`quV_^oxZ0J50nRVBt>OC2m5b@Ds}9lxMFu+xAnzxUi$4 zV1XSEUWTIUM@00UfVe~9hfIjybi?a9u{(&*38@PjGhsvS?Xhsre(bxp@MG499B*m>51!#AUZ!=FOASp*=~!R({&+Rcfk-qydE)emd$*2d<_$XpqlwUJpH zo9&U=o~W?<&8QsQJ1qekX}7q%^}J5qNUPAR>G%7?*l`2K<>yInNUrGLEH0^XuDK%9 z<&WakSs{K+qP*@rpfltC^eGT10m1nI literal 0 HcmV?d00001 diff --git a/venv/Lib/site-packages/dns/rdtypes/ANY/__pycache__/CERT.cpython-312.pyc b/venv/Lib/site-packages/dns/rdtypes/ANY/__pycache__/CERT.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4a6db3a79068cc5b1c653db464baffd2dc0c0c49 GIT binary patch literal 4493 zcmcgvU2Gf25#Hn7@s8y2NXgWnB|CI&xuI>zRjf#Ms`$UQmAaB)*>OtQx0*RDG&n4XDy@{c%wZsju2$q!o+e%KOm!zlBfIcEul;cADyeihV5htJ3S&lyS~J-x%`n+i)>!wL+}zECVKTRMyr5|Go4e=U8o#b< zR3D$2)wRsHk(dJu`{qt;Ce3kE>bzC{^)f`q48ZKRJ5t2UoB4&di(P>TxG&Zdx(4I|hxy*#_XR z9|X2Uo{6nxad%PN{jk3zMwiZ11aWDx5(*c@X92lj{NltW7ug2Q|H~Cq3=FvBQ0?0%b%XG2RC3<0tt@sH#mnw z7v_AL05Boc&G-xC4igWIk(gw1Xs?9;@+j4kIjY7uGn~|@k(x{;6NaXsX-(mlHm?|o zDbtt8Oyy|GxH)YG>a>y=Bo1NxSYhxN==_GM4}=vVPkseXo@er&4Rlr@hU7`wb$nZf zJkL4-k|&CDFaJrGtJB5(ErN9&=ygQU=J`5ZjBtxU7h$4-^4>aK!~l!tiR~{wlh!3_yz)YVbJx&j~>m~BaOMLQ*P!!uA zwmmxdRE$-k2mTQHU1;-rr%TZ@1@5W1r_$EB`rd>09?g~7juyCIN|la1AMX8N@0zdF z@dk|LO6O}Ip8VkC+Tl{?K!N*QY`2}*H4tosIEfd(%>lxJ`CW{JORgFnW?gcwc#%_? zJO?_i0bLbhxQ0r*W~j}kt=wB1V=s;P-$1Z7CWHoZ_BUK7PGhoYpfsk927>i4g*4D4 zlLF`ci1{@$$t}RSA<#S{3xRGu+O0;B>#+buw5XZaWC2Ib05Dy%U|u(UXWU2=9Eh=; zho@@7NPwqJBy??{KQ%dTI}uOcfHh4ogy&Q$Yx=2{Nf@cysRhmS+?uoY(lN{|f3dyg zEf_9HmbX;5xDFEiCNKc#&e+<4@}X13L#Iler#Hp+N~o(M!7qI8^2+6BQt003%4k{Y zE=t{Hsi!FQtf`N)>)Dd@HqH&N440*7QHnl_l=t-&;U^vc_$sh1A0PFAK zqTj$O#iE`3Ebn*UIp6_cL{QBOc`@(H2R5qlGFFAEfR%ntKNJlLWV2Lu%I;U8Y7jP| z$B~@?X8PQ$ZSG6vvX}?5m%VLUv8pHHz*lPX5877sQ|vapcl+d7t7HqP1HcHeU#U63vJ zbr1Ygkm*an0PIB4b!6?_%Ad%kx>6Pv$>6TnfFrDZXnvc70w$lUR=Z8FcDlaVJm&DBKYULFz&w zEK?WrJozmDh$UZZeh8Ofi|8o}o~WT7F?l$wW4nh1w&7$mNBZ$-w$;uDofWD1{`qnwR*b~f`X3Li4?T%|y7!a4+pMSAU+`{0 zLJZ#XulVl|JQa6WM9J#+mv`+i?%Mykc;Mfy=)%(^?6L|#pLWw52&$wq&K-?|#TWTS zuLVnJ{RVP_U35d3&wDY9>q7WP@KDU-MlarR5F^=*$C!^^0-d@*^UfqFd;!?8_BzgC zB~mLRbWGCrjRLnT+OQQxc&m#2^g>H5xa!0+@TiXQyF|Y18!Y#oE%u%L<3OqJoub&g z>Fann_-MF-F>heSOt|bol|Es*q0RL z=d+1Sbw*GWHJ1c26hwL^XXuJzl|K}-8dRrPNKI?T&74XRC@7{j8ppX{wSr1&nT(>u z82Tnibu{B?8(6-Js>sgc1Qt+VFO$D`7;Sm1ZiMH%w`3T<^svs;OfTS8Bf!iMWdGi}3VNDr5wI!LLZ`7bIf+!oVSpM7bgaD}q!Jpkwz# z6(L;l$?$v864>Qg3T<_fpj?rHm4H-%j@_3-@Yz=h$nXQTX3vuRvWsI{Udq7W20i@y qd+zQjGwnsD{gvk^bAu@yyLV#c#Jy81r?v@>;o3bNFRUr+E&c@`cC$hN literal 0 HcmV?d00001 diff --git a/venv/Lib/site-packages/dns/rdtypes/ANY/__pycache__/CNAME.cpython-312.pyc b/venv/Lib/site-packages/dns/rdtypes/ANY/__pycache__/CNAME.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d774f87ec90a623fe713f66483eb248c7f4cd24c GIT binary patch literal 888 zcmZuvJ#Q2-5Vd#jE5}7bAW%{yazTodxJw_QNJ&5l1(pI)a5~BI?)tKucQYkNjnEv5nvyG@gz=*AV({g86Aw zVEPfjF$z(L1sY)ModM=}f3BEUfd3!opob`Qj!@`AP3s?V1^4Y8uJZextFehr+!=RL zr3TFccWXaPAGco$J=`H;MG^_NXi41hV3?i5~w6mQWUlgh(h8QWc!hKpD-B`l|?)yk|kX8biz|j zMk*yjF_1CAWB`}zd)->uqSUA;E!&hPD$Iq!9ZFy3OjMaBrJ)Ka{e-4wWfyODHiplk zuO5zJuo!0QQ0gciQ=KqD{Tv>|wf;-@f{10l$}S0`6TP{xXL1uRC&S0FKh$x?dt(+H zusyDau(b6wGz--0Ti%OxyP^KLNZ}hIYi_wdA)Lmmrn80Z>Nqo-|c+ZeZPCwSv~Eno^{qvJ8M$}^wdSp K@=qhMGyVYUOzN=! literal 0 HcmV?d00001 diff --git a/venv/Lib/site-packages/dns/rdtypes/ANY/__pycache__/CSYNC.cpython-312.pyc b/venv/Lib/site-packages/dns/rdtypes/ANY/__pycache__/CSYNC.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0fe33b42be1e221f1c87ccff106c2a1f14b25546 GIT binary patch literal 3374 zcmb7GO>7(25q`VNT`nn6BpvI=itR{^Y=>4Qr%B+TQjnx|>{LiV1C@;+jDYQmyOLL0 za@n^_%MwYjS-J(m!4A$6mJn_SwP8u}3A)S0(GWK`vl z2jabXZ|2P#&dfKnpQlnu0%K9SK6f`k$k(_DhZGR4=Q%>^L?aq!l7hc-1)lANV2uT< z7_3sT#tXd8FJwZM?e(}bp1eskew}Cn+&R&f4XK5bA=O?N`r33z?Fv4%8ypn6exwaC zut9`71EI$Dx-i(LL+bzD>sjV<(tnVhFNQ+6f*;vvp+o9KCk0L;;3=*kEb}?RlYVJ9 zCAH#-SEk?1U&-fUo~$TkwWKSGmsFIJrBzL&yA|cPRn-hQ31_*YGqDGxh5Eny{gsKC zo3>8vnM&2x%^Am9D%-b=iei^kQ<<&WMp?J*=yN)`S#4) z#_Wu3IQm#cEiS6_x;>+n?MqZc3+zk3$iI7uSw3D_rZF&tO{amd<1nq0$C>{3r)XEm z`JQirn32|RfPMFfcjh54KH&aTh@}A1iXYE z*SfTL2Pau0cep~=G|6>S5uRD;X66o2y=YMl0{~7lRMQjZOm*J&;!8$ZvzF{)2nD!9 z@Q~b!@%ll^>amfNcc4WB1ZjcTxQNlluvH-)Q!;N*w zM!K9-DCde!Pe_OF+FedMJafhH{;KF?+o#8YER+PRB1@LZ11_ISdZJye=oFLEC>st- zO4U}XM%j7s1&S6xmIenuyG-$UFKIxs*^XK+>MR>P(WzEUogxBqaW4kxIp@WxRy0-H z_9Q=*m@amo$$1GyVSGhli8RE<8tn!h6l=mtkDqp-T^HcYz6sN}Yve#qZNwY$z;^%6 z`A2eYzi+T^ZCN{W&Azc(>_JvO`KT2Ob=sUgr&L6GX zn+N?se58D+H2dMo2i+`zi&4nn8B7Vx>ed=ekPsoxF`Pjf03#Zqi;qi-FpF1X8V9#K z6EC+}k{3g98xxLs+;{j{*si2}hX)!j!IAwUOyKTx?>((2e#k%=&cXmx_|O7gAWED-uU9_iAH+j3wgr#Cf0{=Ak6@_4n&=}!b0TF zP=t`kPjTRP#v%}j__Bl1VPm25mEf!m!_@>2@j~djaaVQ|y8+Oq3B_In#W*>yJ3+R$ zX-IT@z)Q?gtHk^ZKoN?jO|etZr;#T()*^idw?nv1wcV^ZRy!P?G|TG0jXjxpyu~Kxm&pcJ{+3IO#hHBzLwtp`>+{g~^Jlo8k zuSx$*pSbt(_O+dB_pd&@-0TQKR9Qe_6okLS27%92Epct zgOFkaQXa8>WJEBE>RemEqR_vf&^xqRbi5cgvUUWHpk>LR zdWezOoMGyoT(n9R3eA&+)9Ws(WvgryRTCg{1Sn7@0UoF#Do?@w6|mEufN72F%iWvF zjpUy$*C)3ox2rqaXNw;%K78qK6Mvm(KK1G&`LzSNr{-+B8*WoRy(f<}|qFMwVC}UdHFJ@4xY?W0+o|tpM6OV|X#-_(>u9YQ$foHug^`Ry*H71o?OJEB<%x zt8@ys=o|ZYFdYgU$F)WX*Yhe%p z@!nqQOe1w>FE!FgjkE|HgR}TkhbRDk`u?S#aFewwn^PN8o7Xq4w+L*R@PB!rn0@~P D*_yf% literal 0 HcmV?d00001 diff --git a/venv/Lib/site-packages/dns/rdtypes/ANY/__pycache__/DLV.cpython-312.pyc b/venv/Lib/site-packages/dns/rdtypes/ANY/__pycache__/DLV.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3ca19935c2b97ea14d66292a209b4663137604ae GIT binary patch literal 675 zcmZutO=}cE5Urls%x)%G5H*rO0>XM(!tUNhgv3BDVLgZehk<5hx;3+zFQ;b`){{a& zJ>?kxg7{NB?P;OCnUlAK$tfqRduI~^_Mz&%SFgH?`q^&VfbYDIaqq$?=f%$)$0lUDA6JRFxwEi*IaHMy*tB;oNa>ah&hC0s@IL$HX zHvT9!3~Eg$Qj_W)f-~THYsX;3F~>&Nl;+Of{s$YwV z9C(GF7Po@(MZLMPr)oz$IdbY`fjwqzT7`(YfHh37|Hnn>k+rPcrSneaWq<>IZUS(l_$*Q}ja z+vu6iTU4Lx2}_8sThC_adY(T(s%|f~FXkvr g%9qaR=GV=sy*{zmr*>yzcPc=+GQn88R0cicFU!}UZvX%Q literal 0 HcmV?d00001 diff --git a/venv/Lib/site-packages/dns/rdtypes/ANY/__pycache__/DNAME.cpython-312.pyc b/venv/Lib/site-packages/dns/rdtypes/ANY/__pycache__/DNAME.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ff71f9e762c929c80b29f037bfa3ba6744510392 GIT binary patch literal 951 zcmZuvzi-n(6uz?^$Fxq75L7LtRfjGf((VW%N(&4HGoTJcmQIfE+B$XY;B#6fiV&bk z(5YiOGJ?ABe=uc0LC1uc*oxe#6Ym_SKM+sK`|-W+?!Nb&ucgui0^8M}Z@tzK`X!68 z>3NbKLDEMQQ5>Ku&Z1Jq4A;ku@d=3k!xZ!cQKg5d3TKQ<O!75yd1T`+m$o zhsIg3CA2L+V8ZaCMl)ud3q6YcZ9f#I=Y~<}dv4%&7?} z-;Dbybdjan_G4IqK}hH}7$IVUkVZsXfy_-pUbWmH?-U84kw-{81qVN)_&S8RB%NGx z=B6Rx^6%6`Keg#o4HqgSnq*jpD8VGlexuP!+!`!fg;Ly*SU%ly$ZIZV;@SnLn74$U z_og<(@i$`BeldFdrc`h)=*+k`nxPWkfd+;je+ceG#Tchc2;cgFW>0ja6yDFjogdI#t?2W%zVc|po literal 0 HcmV?d00001 diff --git a/venv/Lib/site-packages/dns/rdtypes/ANY/__pycache__/DNSKEY.cpython-312.pyc b/venv/Lib/site-packages/dns/rdtypes/ANY/__pycache__/DNSKEY.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..72fdcb5e03d7dd7ea9294db6fba1a8baed73569d GIT binary patch literal 762 zcmZuvJ8#rL5Z+zecfrI6MIs!<38c_malRfQBpwz;A|K)eQXESwKNfO#e&Mx)OoxaB zRFwG*DEto!s;f>~I;iLp83`#ByMEw?EoQ#?_M4sA-T9_zD&T9`o4)ue1MpJ>?dmn4 z{KCNr@PQ8lV8P0x%A){(f~=v1oFX+v0Q0Y}O&5t$>2^`08fr@ID|X2ugHTT|2U8e!#N6O^Lb2_V(=EaXE= zDv(HjWX^Vt{Wn_eZ>7YGQ{NoPYy;!S2?@qb#W;-pJP>#j<5#&8G&_Ba{n*3!Gblx! zi+2z2*v}|Qsh#AM1a=m`j_Bb$!8CLN?B;YH5la8jWAD((+&Jq9p$o>Xa4hzYNOWZH z&0U+$GcrybZ|S@w)b=AfN&T!y2%X$F_a}A#dQzl)-brO{azGVsj=>N4daAYj=rYrI zx;_uXJagQDFo}n3TX4;hI>$>=xDLOJc>k1-4oJmW>{6H3Z`4ffwQc_31&R>Zv2 z#l;j{Y}~CM^Ip2mL-9X!kDD_ILRj7g@Y;7U{L_;mItSdGZ2_dbAHE%~;a~*^zY&Ck g53^4P9}m{r&Pv-^Yx+vlOTg(;0_euM5LDIv0)85{(f|Me literal 0 HcmV?d00001 diff --git a/venv/Lib/site-packages/dns/rdtypes/ANY/__pycache__/DS.cpython-312.pyc b/venv/Lib/site-packages/dns/rdtypes/ANY/__pycache__/DS.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..53cf563d2779fd040aeb786fdc32a1d5d9e551fb GIT binary patch literal 672 zcmZut&ubGw6n-7UY5PKE8wp1f7oQ%}B`Y^(+6@ZR@*?|VO(^LBd`@Ew}_Z>|i0D@B&itU+~( z-~>2u5`s;1rZy2|x^yf%F#k`Z@DeyZ25w+a>mPFsr}~Dw`gH9sSL_Tg)OnG>Zq7)r z@l&y}Uu!y%npF1@oPf|*+msXAn9-h5($VCDHJUumgr8)5+-sCA#$q=TjFqd5MF}rL zh1VE+SGZwaX)?wWpRw;ysrfe_cb_`1q{yU`7E**xp1hCcL69;Txgqlk8N@=$zjEI{ zaC0xoZw2KG`%7bA)s94bT8=N888xwTyZ%9R0h^OrK{9e)7#G@ncW literal 0 HcmV?d00001 diff --git a/venv/Lib/site-packages/dns/rdtypes/ANY/__pycache__/EUI48.cpython-312.pyc b/venv/Lib/site-packages/dns/rdtypes/ANY/__pycache__/EUI48.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..be5ea43fc7bc1e1fde70b73a08588c60afe2433b GIT binary patch literal 760 zcmZuuF>ljA6nVReGWakd*)Nz8(E+j&N zqDs(-vH1(AKZPk%keLuuwjeWg;+j;(M2A*iC1+sCSKvT(VPjp`B(W zvos`iZ6;Z}lWA#EGLqavZ~{D?+Ef@ZRi>C4*O)pg05ENYoz23qP%p-XN4rn%+#MNd zS`T=qAF!Fc;7oR%5e#j#>6H3*k5d{iQQGUX$dlNh^i^bgS*1iN>vvGrongqS$Ng9j z`I}IpFOc9#aQ0#Ap0z7@AgnHK1-mp0s2KeESh7|;26~hty z(5}yQSibUp`&(sYeDnD2*q&5shuc9VH`n#Jj7K)zUM~u5$K$bzG`=detWA?gu44-x zUphCHvRYiqizOUn6J3irF3a=NCd-J<^!tm)eGpWU%GVd080IP=B)JXb+ILu;7eOg} sTsvHwlInz1e=9|@{%Py;&hgIFs7;L8)YzC98wsGCsG!{VB@JnhKQk-2asU7T literal 0 HcmV?d00001 diff --git a/venv/Lib/site-packages/dns/rdtypes/ANY/__pycache__/EUI64.cpython-312.pyc b/venv/Lib/site-packages/dns/rdtypes/ANY/__pycache__/EUI64.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..bd26fea4b921d4c694865a4dc2e0e6d6703c1e17 GIT binary patch literal 760 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 literal 0 HcmV?d00001 diff --git a/venv/Lib/site-packages/dns/rdtypes/ANY/__pycache__/HINFO.cpython-312.pyc b/venv/Lib/site-packages/dns/rdtypes/ANY/__pycache__/HINFO.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c01cb119c3db45c94c1e50e5b5ef59b3c8ffa401 GIT binary patch literal 3035 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 literal 0 HcmV?d00001 diff --git a/venv/Lib/site-packages/dns/rdtypes/ANY/__pycache__/ISDN.cpython-312.pyc b/venv/Lib/site-packages/dns/rdtypes/ANY/__pycache__/ISDN.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..900b791b8605b4aaa6b5ab80adde066fc2228878 GIT binary patch literal 3495 zcmcguO>7&-6`uX&l3a?UBwM0nyD}5k-q@0AGp(c8j?&syYQbgD%0*E^#b(7_$)xzB z%q}I1redWK8p>@du}DD~2oUvD73#qO3M9w&)I%?3=z{Fl2NyZ{riMNQxwLPVyDQmH z6d;F=#5eQaym|9x-Ec<9TU6#eli-phE#e5sSK>?*wuAL>5wzp?YEisc-s~wyo;2?J|=wN>9&|cC6wN~-qtQLDy7oSmnk9HIs2FCv(MRXk!HfOesdlU7Zkuv*h zvDtU*gQ2@aTYdMge;lj#jh5MWgDqH%l5P-yA*c4kuXTO{d4T2kq<{2v9Vfg>@UXOZvXXmJ@(TULw9%V zAUe>oBcSfS_b-2N_3qWik>T2r;d|^y;)i1W$fa7>rTgL~9EQD;9w1I5vA4kDV;DdV zF=;!Vjsoc&&|31q{64v)Qh%bIxGf2W3nnGOk*d_}BwX!dB|&8(U@^Vd2YV}Lm$YCD z_H;|$9IdiTUK+BHfLF;=@^0H143XsGkBQq7W%{zpP1(rLYB?PffHMi?_+~Ui)(xD> z&6oi)##&YZ^Z}O1XgO0T046|aHItiHG6hZYnM^XH;{X`I)0iSV`dW!(_?Nfh;44VAtX5P-Upz87+I7;qFE_ zS_?6amJ^ZaQ|Ipu2p8wT_jpLvA`w z{GY+j_46BJo3Cvp>cN-p3oo^U?*<39bA!KZ0f^y-9fRp}DvTye)PyhQt2I@lWuJ>~ZS*WU7*nY{v0{K5bI1PAy~v|&kr^pzJ%Rd4&y^TlXRBgGtcyp>R5KV_ zkKI`)ca#^Jg1F{e^}Y9<>V?XM4P*01^{$f-gj2S7{l)6V%Ebr5Kzn_#60C~@u--40 z-@NnfW>@#R{@e5A`1<*#7+M=&9e;1WdaH74^Tn-jz321;@r*5AH>$TQxBnrY-0>hW zd_W4XfU{qTU)=uq#) zdeqt5=zO8p`9i((3_y!N-0=6;{QY(R(M{?T|8W45M&xWQa<<-vkRx9PArc+GP71m> zkocHH#U+tcb2%leK{e--QE@`f(UR;h&hz&+D% z)pA+QxRF=!DUjfEgq$aH0lU&mYMG2IOB4=*R3~Wlss*i>C8~rFQ~D2qe7=JI%^m_Y z?d;#Uc0VL7Uwy(d+^bZ}$8)djgb*Ko^!mhSjF%f}F~IE5WOUfeo!U7J>}ND@Pdp(b zE0&gh$%oH?bu$2U!H{+{sm&Rwd=7WJvQ#!(FqA2{ie949t{g0F>v@1{h)u7ngb$I0 z+>c3pVcGv9WFLsEQ+**^Y=6mDFo_~Lu6`NF4nt8?>v=?Ve2TjNMgBuTcX&i|?;L&m lXoKplQN2&-bJR#Vwl=alvNpOp+Csp>N$JRc$b_}Y{{UqP)2sjh literal 0 HcmV?d00001 diff --git a/venv/Lib/site-packages/dns/rdtypes/ANY/__pycache__/L32.cpython-312.pyc b/venv/Lib/site-packages/dns/rdtypes/ANY/__pycache__/L32.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..fd10c74e358d3f35f825204275f6738b1c0eaee7 GIT binary patch literal 2540 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 E0WPY1{{R30 literal 0 HcmV?d00001 diff --git a/venv/Lib/site-packages/dns/rdtypes/ANY/__pycache__/LOC.cpython-312.pyc b/venv/Lib/site-packages/dns/rdtypes/ANY/__pycache__/LOC.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e3eb6253f39c18273a13f02e0ff110bcc7e8f5e7 GIT binary patch literal 14111 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 literal 0 HcmV?d00001 diff --git a/venv/Lib/site-packages/dns/rdtypes/ANY/__pycache__/LP.cpython-312.pyc b/venv/Lib/site-packages/dns/rdtypes/ANY/__pycache__/LP.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a4abd7dfa6e8e213fd33bee2dcbe15833d95b167 GIT binary patch literal 2512 zcmah~&2JM&6rcUF*G`N>f@wkpq%F7@5dv+KO3+fOO7ug)ph6F|BBq<&b+U==H8bl3 zTTV%(NZ1l6MQW9BY130dx$uAVlE_p^w>?mC>Mg|_5U0MG-L*}Wwj=q?`y*y6@wYpNE=Xfq4t|Yod%`AlRpnkqpt0 zCdG_gLs`L)7uAenL=0t7$b`&r1Q}5yv?#RxK{dLJFvKEe*yuLYj!3kIZU~vKD<~Cn z#j(qk$k^pUV(Kx>7}VAh#-1 z#Wm4$3#f{wJJSRzw1mvzI$&~qB4ZipC88EUX(p5jI@<{^wJTQbPT z!i$?9of-dvnuLxQOHg6lvF8eO$|_=-*K&BGM6H5JY0EvNPifAC?X(>%FE=7*6|NDQjDA)8o}R>6%1@rvn+Gjn_`Wv9juh%hwVuw!G#yx~-$ zfRnH9hX6eRVgYT%`c-kQVaIOsnzGBqv0}o*E0QI4 z-b1V#5NlZnAeFuiVgYTdX#c?SwY%3EiKDgXR=jtqTCc8LSpDo_jHCBky!YF8xh*&1 zBkSsj2VLu?LXj49KY#(y3A;h(wBXu}Kb%*r&W;gunxIGUj5 zl{q`b*FIsLWsbn5bVJl3CEamlc01c(a;D8$#0*#}XRVy+s=A$r21+SE54T&_3Uh(ugP)oPG=u?5(Ku1)S_~CSe&J2C;x1Ctlr5BsUVtM&cNwW@71P z{pQMSBc80uyVDYy&()NazM2M+zQFHUs++wY>}K)T1Ltj{lw*1~#@9<)&X0sKHf$Z- z@H)_(?NE$)M<)HS6}O8&7l!5iB9_|lc9lA??@B`~%{rtJ}x z^8yb(6(v9NjA@(`ynae0Z$Nc71`o_hE1xep+60hR0@IB!A2BpXb0h7L;(3(MS%BAg zfV_IF-M&!Y^Uyj+&VtE4iM|hFTM`7J8A8IICuraAsAoHbMCI1OZw_t>{To96GjUKD at@SMp)CZOh*AF)lnD8Ouz~3yu%l`vWR2}L7 literal 0 HcmV?d00001 diff --git a/venv/Lib/site-packages/dns/rdtypes/ANY/__pycache__/MX.cpython-312.pyc b/venv/Lib/site-packages/dns/rdtypes/ANY/__pycache__/MX.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1265d0e5ffaab3f216f3e7f866f6d2d6d28e6cb2 GIT binary patch literal 672 zcmZutzi-n(6n=M3owzg-B7j;&l~Bc!A<2#qQp?b#2@_JVES+rML7nR>)Fy&VmyTrz=KpCFc7f9q;0E@z{xR2Zs&BZhPuFg9#lGT&IxiB~%^B%6 zekwNhYfUFolj<&la}fG!n{r|sGukssI-0z*Mw3UG@RN*>dyTTiSnNiEv2vBMDB(q@ z@ET)>g&WqDCSyGD8T$g2nt%QN`4i`*6q$6=LWsr6o~Y-8&nh@v8Qy-<_}((<0t>T!eUt>;QnS~s3%T`JAGW{xI1 z=vnNu)NsAw4x;PU)5UK+%Q{Ha$IBfIR|X-ZdJN>jci6Zxfv%ixe%QPq>vOXHn-X&W etn;b+v3p^4=N87TxwTaR%9R0hR_kqlU>En20KKz3D7-KVzlOCG}$JXYex%LZ*2{7_V0BJmKqd8qQl|IcN28-jrI z@SpQv&h7hef6Qi61jd4Tp{%Kde1=RsfDcCJEtoclMJ&M~#c&piV!%s9k%|>9vh154 zB@@Xvh$XHPOM*SYy?q;CS;>5vM{ZjRW!sUem{<~Y`7UyWQX~te$c}V=1niseUP_9B zMc|!6QL;o!niq@m8Yv5wys8xyD`_dKLQ$oOB(YMKx+-*U(5$qT09}$Mtc<1gX;N$C znovw%Bzo4D3a_18P5~KbwB$0&Pc1T9ri|7~)K_b+MQy93^t}+_#7B01AQ19vVS{*G z<+{=!Z-Z?^Xo%ZlC>BVARQmTv)(vSJ_Ty*}BmOk;lA#Qwve$(+K8wBNWqbN~za=b8 z6T;HoK&-RGSod?m8;qruz459yS4Mj4c-4?C$&zD-!RykoW{LEjwm+8k=E_)a9j_{f z;#F@BZjdZxTMlJ_k^JR*bhU67qj7nUUAtmBwlyAl;x6v!AaG|M2xs&GU*Yvd%6!c* z?3(QvhMzJ_Zq)6Xcj6R7*!`5v?Hcz?^rtenS~MA_M%iUm)AOi>wz}%eoH}K+EjcFVz8Y={ z?hlVi8b+Wp435W+<5+pH^75J4?{Uib>|z~GnDyMNHGbJ%G}uKdL{r_95*wNCy~S^5UcNInjz%4L1}Sxu0~lOmICf40 z*}&9-`Qh;%R5;K?3D~cFkNj4GtQQLpgmGQ$`A3)80kHE`m)Y|+#0H~|>DgE8W$KFy zS2;!GLc%wBkwU)Y*8*b+&o4LtWZfZ^a3k1{j=J~B} z-c(xy(>vO9h+2OQ#nON{bp8PZP!k@7n%70Dg8qAG1R;XxKQ!W68&^=MO^-gk+bsXr z7Fg-l=iZeNrrrA7lR}$=Mi=|9_mU-|%Y|ie;#G#QGfV{VoN|>xs{1PUSiR)=GUj~f zp&_&yMY0bGR_vf0=}H*xa@lsMua(>?^gPZ3fBTt|S#xW4399Zg<%oKpZ#D3~an)ut zDjE^@6jgVchsFm+Yv{S#+NoIeEUMZ=lg**Y_Rw^5 zX!@4?`}FV9e|Y^9;ge@uLl^$6edpg@{{JP4@?R1~?Iw!SP-CDk5_Sas0EGen<7Y8P z7_FMnS!(log6SabOA$5b1uV=G#3{g*V|E9K#pPl!NB{)Q2zM+GchjOzdKC6W+41Sf zW9^Y6&5Ng_tbyN4k&4tbg6Ku0Hkm}{6+@5V$H15peq_x*Qz_X z&KSmz>!uTF5{6;9B@jc4;*RTa!w9+pdlKH`r-QClrQT)NVqe8Sehmp$Uq2i5v=Vh3 z!_Wm5lt3(BF9n5wVb%o%qm_Rf$QP^RZ}KoC|K7u!*LDVRS+Y=`MSoitoT{za>wmzweFd74rLhr z{vgVt&UZZM;mMxGFj^?z#l9aL^f3t!#WMdudm)7E>+BU^@n8H!AonCe5IQOm_IyT$ v|4R13yG7;ALq9vz7WOxV{rANw;q=<%b$wmGo?p**2(a)Q@xVV(5gh+7AWM4x literal 0 HcmV?d00001 diff --git a/venv/Lib/site-packages/dns/rdtypes/ANY/__pycache__/NINFO.cpython-312.pyc b/venv/Lib/site-packages/dns/rdtypes/ANY/__pycache__/NINFO.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ba26183dea3af518814a2abc9d5ddc802a88a6a3 GIT binary patch literal 684 zcmZuuziZn-6n-b!mYvGb5|X-skPe05!S)_XAqgau6nROY6@o+3T^c$1Rh^W?-5LUE zr;h2rQ2eKK@?=oAcIuX*PMLalmYN1~!+YQNz3<(7++%q1Mj9q#HQ{Vo@5qRzERSu(XT z>ehY<$298)&fG~Nv4`u%vE z@Sg)2mN{T`9xmWVZ8;I|D)nwNZ}aZH7X(>qJ3h@-PV+^fl{fa&v(&K}&0Du7t*qr* zRn+!h?{9I}d}K@9FNZwf=%)F6{$?+dMNY*huq__0RfJIS4A9bdSh;S1QomgNusTDX nDeC-D5PJM^`}5wXy_vo?)z@bF`cz*pfXjsn%9G#1AZPpq$CRUH literal 0 HcmV?d00001 diff --git a/venv/Lib/site-packages/dns/rdtypes/ANY/__pycache__/NS.cpython-312.pyc b/venv/Lib/site-packages/dns/rdtypes/ANY/__pycache__/NS.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..344a740d1318a917a4a42474da336c8c6c1d40ee GIT binary patch literal 672 zcmZutzi-n(6n=M3owzg-B7j;&l~Bc!A<2#qQp>=C%n&hPSvuLigF5vue0EW~Af!sb z#2Ees#Gk^HDU#Xf#Ma84I`Qt}P%3f5d*AoH@BPsI=yo>&-;w#^^_c;1rpWr44XD2& zm;(n+B5;V#)FFaQ*N$}u=KpCFo&l$)zzyu_{9|t5RNrt@pKjgcihaimbzUWK+A}g} z{Z`Bl8ciqCkoq=)IS75VLpgDb866lEwI?T*J$aCeAkDcwXjL7?5-%2vRhx{(DK8_1 zw-|d{dQnqpGse?^v2ReT`B(3sK5}15kxMr#rHI@jeV53?Fk>?IBIcJeOoWtw z6@FS=3(6M_*T$i$9gF1HeIEL*3=1*Lyx_=tBc#g{InH@8%Y+>7+posX!{S|4<(F1C)sI4%p%k3?l4t?nqT88=F#?@3Wv*PeD$s?4Tl+LHtH ztoB)IxZZFJ(N*X1>bIWcJ*4X6i3T?bU#CZ2;Z5PzJr@4>2*HhyVZp literal 0 HcmV?d00001 diff --git a/venv/Lib/site-packages/dns/rdtypes/ANY/__pycache__/NSEC.cpython-312.pyc b/venv/Lib/site-packages/dns/rdtypes/ANY/__pycache__/NSEC.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..df7aec70aadd35d248899886718dd2fb8026218b GIT binary patch literal 3155 zcmb7G&u<$=6rTO*brNTjv}qDVL`6dzw<&6pBD#BvOQMBP|>6rdfCGH8Y#0 zNo|ozk+3CDKnNwA$bmyqF8l%f2V82o$Yq;D5hrc|_kcL@-t4Y7Ed|OP-n^Oj=I!kF zzBe;JWHLPj+9mbv`Rz0zzu+KlYNT`~1wuB6MJ&N3d4cz0)Jst>N4*mDYF@B~Vt*Ve z=^r#FlGlhOt`SRuH3NxntQCi1E$NH<6Ft_prBK_BCh|T1a}P4?L4h>~;)qFK98L6C z``h<2%R*ND70tOq?1Vkw$6E(~ZiCny#k^<XuE8eFVDW|%*`^#x2Gy*;gWgLX0ukA z9j6xVfE|A+_x^F*_;h8N%CHYE9sz}=K&+8F0|&0XPJ3g^J;+2EBb^Vx{PBI*SfV9e z6!UVO%nO#hrsWkYZ7FL)UbRzcV)a<+n$XR)X7ySru%+#k)n{pYEIoB{MacKgkZgu3 z@R^lf-0eYX7d&bOa@oG(2dO2eY}>IErJjULOJDtt!XsYqFn7W*<~au_t^9 z!RKw6d@JO#TA;ASicOJQPTBE+PnuyeP9Tbf8uU0o5ayfZg3VJHDE?x_wW$uqtQyEb zsrev9t%7SZ7N}wR`BLr@qz!|c41?j(aS+=o?E{tW1F@3fB#FQN1Wd74LHw~wcC<_* z)zn5eN45^%)Uvnsk8Zs6`CD5Pt^G&p@+avXeQfi{H_zPEkM10Jbo0G0y!vZ9BcOb3 zd}Xvort0#ieVkr}F1QdDlZOy?z5%g;se%6fR@}QHGVV<&0Nza~M2JtsJz-+Ni`{f4 z#+aKm#S)6$bSCzyTvg(P13SgsihQ+pwU$-Gd<+&AJdfE1wO!M9E<1iyUYL&{<$H#Y z%vI+-S~7iDG=lT0N1cmKIp~R3t=NHhX(@~-HF4~8_R9|Xj4CW{h9BcYw0i4QH+WN7keypWK9&YKw>$OI0^ZeG+ zEq(HaHW{X+%S4EX5b~N@00q(_730GZ#&JVns%m_^)>3{{VdOvRtEP#NXiQ)=Rn@kU z0?=j%9fS_fS?!|jhlEE{ke;VriKCcJ2U5Xh6c2eihBnFfF44zuco2t~1Wv>E64EnF zJ7+qI5Km}g$s$p9P|~;vR8Hrrt7TO4F8nF${~f5Dhd`K zBGK?PxUo|p09KWxGwbO_x~+{hwXw~KEqQDHx_wi7dPnQ8`v`bTd$g@hG_{GX^FL}w z{{>Pjg!CV{8UJuid1zj$U0x!uk$lj)yrdH_3Xj|Y&KQKvsF)P)@@$G8Mr*=MH8h0e z9BhmE3`<{iyNL=H-@t7)pC^q&+y<9Gtv< zpf&hZT}`NXxTT-Cp`GBkL88xRrQG=(h}^lbzWOL~l47+}4MdPK49hFPMFTejb3LCK z2EPd?o;X1-zbs0&zu;LEYlcpvz&!_<=qf4Lu4@=sf%4}Bo)j;JHHQ4)mpPUwdjrHD zYvgBn_|D+S=GcvaL#wafl@(>W(=RF6-5#P2-g$fGcS%uBbtD+F&MB1^eRB?pj4t>n3(}}j%k<6AhpYHV z3I;+8e=toi`i>id^jc0T&xa4g7wB0SA~x(Q zh+Rn#gw6yJhHsPow~2n63~*%x3Pe;s9=|r;7Dk)G=v`3|M(Q)`7aAAZnek?3yq%e7 rW+plWrU46aY!@Biu)25AA>m~G)cWbh>Gd;>GaUj$?);kw8sFSsso#(# literal 0 HcmV?d00001 diff --git a/venv/Lib/site-packages/dns/rdtypes/ANY/__pycache__/NSEC3.cpython-312.pyc b/venv/Lib/site-packages/dns/rdtypes/ANY/__pycache__/NSEC3.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..310e9bdbe3792d14e17f0dddba973632381509ca GIT binary patch literal 6394 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 literal 0 HcmV?d00001 diff --git a/venv/Lib/site-packages/dns/rdtypes/ANY/__pycache__/OPENPGPKEY.cpython-312.pyc b/venv/Lib/site-packages/dns/rdtypes/ANY/__pycache__/OPENPGPKEY.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9b31fbf2eae2405ea62214d690f9c4fa72a6f1dc GIT binary patch literal 2350 zcmaJ?&2JM&6rb4-d+o$)NTNcZ0ZJ&~Lx@x;MS!BB@Fle}2&h!7NTc=cI9bQub!XND zM@CAeNF)aeIaKD*Ln>9OQ2v1)d+!A+twOgPs5teOVh)H?-|Vi}E{!^h-@JM6&FswY z{oczjnM@kNm`_|Qol7G0QY^Y75hxGJ)$@7 zWRuVpWJ$}&l3|bDi|uOVU0tm=Z^)s(5t$092F@veiB;Z=Y{3-8q0X1Eybt%MsDLd5 z_rwKx1(h&-St}@3(vp{PL8XZlvNTIs#_`EXD`lzR(`dp0bR z*xnsYc>{;GCL-Nuq6T_`^Et&=dA&xNuMuK9wnvDcA_gbLhDW(C&(lUu^%YJlB|izb zs~DX731)c>*dv$#(Il`E!o{Z#!B~2GfBNjq7o0LaQ>(+?8P8pC_?%rMylPZPvCeIW zavqx}%sIm=x?ay9;wlr}zzN~IL!CP_pWDS5ZhLg1W|;HFEafwn!zaW0@W~7L+mk&+ zPt+PrgDWi#_u0CyJOmDY7{r?;^jyoVCfnNH$9vX~{;K7k4ex19txc_uZw?<_Q66X= zh(czJ8!W^_2jJH!fPkz-=ps~(G6Cjzl>0LpSs_o|Vu|CBpcoL&#!dML{0#=ONO?bH z&effHZZA?ul?;#dRc0BU;ipN_;Pmt_;4FU>D- zaSM?gmK9M+&m|tc>n#rTVH4SoKmh*`1R#?gde~TPY-PvW+41#@KVJR*YCAjqlkzk> z{j&txQ*D}wnlqsl#6zGHu(kcEdMS~Y;^&MdJl>Ueg^2AcW_>X@!Y{X__=pDLa{ zWkEq&G?(&av%;BZQ7i+(mpyl$y(gA?#4^(Zns}}+!_FWfJOy5XcrZfPQzhnBL*PgH zfRC-mU<*GBVhL?)C_A*JpKR+VH}#LV^z&`~{H8v&B0twhw)6vS{lH_ssgJM7zh{RY zee(G7`t)Y@_*3n8n2#7YI7=WLD8P_&YREaI zWLK!KnQpblpsoXO{6W)jT*o$z3Q#ca4H5q|fQl^GjQ+bgBjY4oln;SeLeI3phw0Vy zqf@OjYiEAb-v96OV!E9UpZC9XyQjuPQQsvB_A|58>k%ZEU&N+o{a4RS0Z{2QQHeZBLM+!LB!_P^F7^;k#4>P5VrK#AQiuHAek zt7@(z!(v+!nvT`6!1qd$BFAf?1!)b=%%#~Foakqu*PWnuO2tC zr9?>B4TET?OX>jGn4gM+*ofGu-Vi-AIR6O>za`q>TkU)Z*%UhmOYuZL4Psly7b8WBTTq}h-WHB=bGX2ggYp+#E1#f%x@ zMN(TgEs+_TjbA32CN5w4;cIg-=+ey0+05W!yX0DS(a9+d3gH}e68ij#t`WD!TvjXO zJ(Z9Otw^^ek4(YZi)3D@kUVhqwu)R)0?7)|g13@cDI?GQ>(!#=T6)2nGt+u8Efh0! zSMQC*(tbP6j9fu?oZ}}@Vsz9hx}&3M%SpSFW}2a}V5jYILHQ9Q(G8}zn+=G(MF!2iDoRL!5F$*KEJzaDrt&-+U=>=`93A9==gz1ccN`1^gr#O8t4PWU4==7Fmc~o~aPo=pdNYXS55T2zDt4_IOG9C#|=1~of6i#Od|4fK0@b+;Y_JPYoo}PlsT0jK0XacmF=AbVH(ia$} z6}At$xp<@M-9T}Ap!;fpJnAIT#Bz8ea^OaC{h7Ow%tJ+v9@>tP1F5yQuD`WDadUPv z`Q0Vuy*R^!lxvJf%$^v1RSw-6foO$sZyO?Sj1V%fR@6Mo?rA|znGaRylVaTs2OIK{ zU{)Uu3a5%3UZ>&AZ4TCzP=MH4?(`8%A>%yh#`+FQ-u;O}W)bDP^3hjb$%KW$dDykJ zX^WX$oo0X_9&%h(&beGcC~?(B2ywY!7GXmND8iL-t6=g-&YmhUKr29;Srb-~$8&no zE?R&h$Q)b-l)-L1*}+%)*-AJOz~Nu&d(vOM2y)Iv=oZLUdvdM+djCTrN4u5=w>pw* z7p`CU#l@wwTk+PF^ULRdd2Q{@>u;{VaI^cjnOm7ZT>fnT@3Wi9(|6-9`2wruzs3*m zgkihq=|*p1&S-*P^&E6i4QalXTY%sWVwu|55TG!Cg($#6P10C#z=(j**uBkzX?*YMi3Oev> z%f6MF7x6d%wpt?-73cNJ}W}6Sqhe56}{yi~L2?ug7fY01oEqPMJ<`G4ES|wDGgK9G$fiElg3)T>& zYQ7fD^^HG^1b2eO|MN!rF8ID8zA`FmkP9x8Zt@&)=(I%170?|p^qm>`3Yn&WPKH=v z2cgS^d6bz`Iwbz$1dmOauI7h)6U3^^tegws$*@c^%tk!v6%1{h*;67_XeFIN72_ej zR5FW3Cd%bp!D0Jx2jYr{JOKd~FRgbWS+OCFc*w=m5iz29NuhP``EwnJp}o}8u+w=T zx&`uJU&m5-E75T;(Y2B2+DxR^pZh#-H&&D@9&#uZ}%E)^VZ(kW+9=`GX-T0C1 zC~4W>aK&cg*iMYJwy(Z;dkLcAQA+JBNRM k_#Qp5K@U8V`e5UrkBXV=jM@gpcG1U8pNcK0qpP!C?hCgkIgVW63r?&z%ZmFXE}Jt+v9 zQ;y~@g#478_Ovj$CFGEsVRG`6>fTw6kUmts_v%$wQD2))6Yw1w&)@yj0e(m_eMSY! zPYAAo0V4reL}kq)oD8OpX$R*2X&USU(=LGN*wgsOT)~0b;h{d5f5;{Kf*YzlNnq87 zq+RLNPl}JU(1q9c?)z+5Ah@}s;wqEEjUcWpwF+5JW7pJV>t`!YRBPZmP7AB=( z%<@3uc}m~sPEb|ql(N{P^fQ!l!tL4Kmi_h0P+-OIbEdxajTl-}Z3%ZZb@3 zxP}Uk5QrHJ7x2BdoXU$Tz1=99*xU8PFwYz};DwH~Sd?1zUpEr2BX}`;@2OX%!l-IS z|J5#fCi77d8} literal 0 HcmV?d00001 diff --git a/venv/Lib/site-packages/dns/rdtypes/ANY/__pycache__/RP.cpython-312.pyc b/venv/Lib/site-packages/dns/rdtypes/ANY/__pycache__/RP.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7879b6a9ac8aa1a68fc581ded74c5cd6122801e3 GIT binary patch literal 2604 zcmb7F%Wo4$7@vK5?f8-408T4Js)iIJC6TB^)$kBiD^MQtvI)*PYpf z*lZw`B4JBK2B}pnwFe)A(gWxI3%wATD(TV#RZqR8m;>U}Z+6!YDCuLQ{pNel?0nyE zzWFH@iy#<_;^kaLLg;rYw1*flt#5$YKpN5*6J;20W`mgvW?ij0rgKmSb!#L~~>#G^$ zpoQM}z}w-r7@u{J+&<}kzI=}_-QOD}bdQ-z3$8$xiaK^BSvCrWEz7_uL@p?K9n*a= zEq+>bd7_&+H-t6SR0wfJtl1@q>$tdKS@uk_OlZh39c#~TzCW4$l<1gbi%YOV*0!z{ z$h=XMNnSDK*(G8WbV54nF?C+CXDz#{pnm39+i{HAmi5AN_7h_^OAK2dD=O-uGN+TQ zRv;7pUy+ITraqsTzA|1cVT#0ZAMow-4TFN51oCVR{VK((p=~KuKin97D5ZA?QyZ7= zUTTaq2j8mjUx#;-hd0jNJ=;h&lgBFjZK(xONbO`B1bWgw_*y>#*`WD{`Q@?D8xa_L zYrM)sT&FJG=zon9G-e^x^>1@_xXn48%J=AmHX7O(1N5>}W#Rhj@b%TS;FZ)JQ0Fa+ z=rYz##Wt1=yW|1E6>}EOE4IrK8>dA~@2o3Y*qAd4Zlvv8)?Ie-S{EB#Kn&x48WFNB z+16L=as&z!@W;SFE&^FYyNP72R4wf!PHZPmY@WJ5*-V_P@VoH?wfXA&PW!?QXDZxo;!y2I^~U#VW8^#IhveqWR;rme`9L~}2VgC=H$Aji-qq89fnsqB!4(3r zGaTL#9I@SGRzv^EOJbq)w4_eRk+y+n zEJ))(&8(XBQ;o^Z)n@!`h5IFO;LbbX@xkVs&BXWvY1~g;Z`bLh&VhCRAI>#MG~h5u zir3Eq9+v8iS7ZI<*pi>pH0Ohq!swcTb-zamJ{tL)Vd}1=TKOV|*5W1CjjBq)Dj2F_ z8fBf(lX=PX6DdKey>S15N;^`D z_K?B4-S})X`?ECp5BbZ&s{2E)eKS{u{b1Me5Ic~gaD5SS_;uJN9l}Ro>cN^=R4}w+ zA8wt-P3RTzve5=szXv6nSlN##C)@byQJ#V@G67@_Jx&~~-K^fMFEJ*Tv3*J3RN9h|x-yL{^BC?#fv?Y`BQCa?SNihRUNR~BA1vR`k z#I$T8%U+MiL+~qZpZ5gib$i~@@GxlbC?#oXiv&RC} kc9_&QlX}X&!Aw+=wV~=z?P&FA3xNp7V~75v25*PI064WTOaK4? literal 0 HcmV?d00001 diff --git a/venv/Lib/site-packages/dns/rdtypes/ANY/__pycache__/RRSIG.cpython-312.pyc b/venv/Lib/site-packages/dns/rdtypes/ANY/__pycache__/RRSIG.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9ee1010af14b8acde082d2b75a57510f8d62977a GIT binary patch literal 6603 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 X=U>H$7fpVPA_Uk^NAou(ldSANh3hPe literal 0 HcmV?d00001 diff --git a/venv/Lib/site-packages/dns/rdtypes/ANY/__pycache__/RT.cpython-312.pyc b/venv/Lib/site-packages/dns/rdtypes/ANY/__pycache__/RT.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7b758e7100e9c6fefc7a93abe32d86cc91d8a6d9 GIT binary patch literal 690 zcmZuuJ#Q015S_hqY+n*f2m^9tMM^>A!giA?LP&f_G+-plk>GUFYVUSQ&bcpc?*gWY zB1Hl!%J3HuKZVLwq@#n1F6f|1#q9b_h{P5%@4cDXnc4l)YOMmEgZk4KzfFMeiY!mP z0_8^pr@(=e0BoYYVG}{7%Zz0O`u}MLJOXZ<05`Fw`A>2Mr+S8~`gHv&SL`!RsQto$ z-I$SH?T2C`uhO(56)CSFI0d1nwhc~fb7u6+!Wd89SmVi7D!e%5qh76OQW~*PP+F`~ z8pb>i6kezFWzK>s(x8;b9;Kh4RQF#TJ=k$}rAVcd)AtF9Y_uU#fZVO{1ls!&m{ zVx8T{i^C+vZ3KUaTY3z4+k5hCF@dGp>7HwdE}C~1?{_zCBUN7^@1r?42_fZeAUD6l w#(5o#mA9M6n`fjmC!JpgAvfN&KXl)B&#dm;Lc2Y;wo5>{G{M;TsS9i-b*PsA(TMk4sM6ag5#vSBy#ktIw`^3N(@b= zj_$uu@~3q2WKf|)J9TqWCQrSSkLPxAFT!9bJ{CoThuP7cVQgarorrpwgYUP<5S8E;<0W>qH#c3x7UBy&G+vF zewLnu;%PIzsU{k4@7S&zN7!^|B6FH-3$3`a6FCfVaR6u1%|Pu?EnA( literal 0 HcmV?d00001 diff --git a/venv/Lib/site-packages/dns/rdtypes/ANY/__pycache__/SOA.cpython-312.pyc b/venv/Lib/site-packages/dns/rdtypes/ANY/__pycache__/SOA.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..93bad4e4a23ffd97031d598eddadcd8251314223 GIT binary patch literal 3832 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 DEa?5J literal 0 HcmV?d00001 diff --git a/venv/Lib/site-packages/dns/rdtypes/ANY/__pycache__/TLSA.cpython-312.pyc b/venv/Lib/site-packages/dns/rdtypes/ANY/__pycache__/TLSA.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f47f534ec8fcef3cfccfe9b3f74fb935c78dd0bc GIT binary patch literal 684 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 iWaZP7FB_jXre9>5uU(T( zTJ;LR*1&Kn$D)IPt9d~s`)FV-%B{X4$3r>)< zq$a{#RF7yKTBKzrrb#O#8`q-HCtxr+p7FsSdXnr6kjplB^z#$#q*s8`vOe%wyL0U{@Ji|aC0l=OTxN@PdC5* z30i3WI3e_~6&Syr$>;VpSNfC|yy$&yk;vhRNs2nC^rMnpMGofFV$rk)r}aU-P_h=~ zxGk`9NvF1?C`Qq+6vd7yDpSfv(K`7GMZHmcf*nQf^rEFR8o@E2*NZl<6`38?Z{+ln zWtc@fes!^EsW*N?sY&IC9cFrd#*R=emsc6HMXFhga8|IXyHiwAoOwlIxbvXXmY*!2 zdwuFHrc*XmD#Jcgmbp-5*Nl?F3Tj@NE;FO3GZwIibJtXB+O(Psl*T`yjr z`l&HJ#SBXyE~&YBbyjCn@QM-lZfxZ2#Lq`SUnfe76r({UfdgMR&jN>)p!;T-Jd~1a zk-F5k*?arBdy;&=@8HL&k5XGwqwnNu_*cpMJ^dTzx_Nu1(K8I=IF5_!#oKRhO*VR7 zLe9f@$EtPny|wom@dKOhei0vhnAx{6ygt00d7++pq4wffBQsu0jPLLyaby?HB)I^k zFhC_D`gZ4_gMh>Vs({+tfb01|>kM!M6gT;EsQx|8cikDj0x$r_4NX*HL)swxJu}3f zk;8kfDQe)&nhq0b*vb~cpJ$Dt9iw_)wT$aVMYluq3k;7(LsP`D9kEQs(r;LmIBMG) zya+Q)hHjbMPaW8_K3@80X-oU#{GIvFU;gXppGUVxFV#mcHAXMj4qd6IuGFL}v^AgOr+Q8evU*9gu_;X9qYV`Z`%SK>68eAzL z{PpaxJZXcs-^g6I+~1x)xc<$W#$kqx6_HByz}q#(95rSEz%(zWq{mY7i}GbduG zgUq5vaaNAnF$l_DjIep2ZK(|2m{Uu--Dxo8-Lh%vn&OJvN#GcoUbKuEL#GrIA;s%b zyoD`XSMz0Ej@f)J&nRX$+J$`HGUw@j9QNZd*#wYcnR`&jP&WuCx4EE-&QP=9!YYZd z3Wg&f%rI#zlO2h4_imh7Khx+sx+>jIcW)e8Kh#JcSrw7)UGM#H|91LlJ$kWnb@(9DzI&ns~w=85@&DTLgobbCw;($l5fKEi6D%*PUoDi*8&9&lq{#mU3pH zL?MhjhPD%soy?+fEMtlndc|Wk@oCJsNY3(YG*1RA1xlk2h@1{?OCn&~Jw<37T zyVb?Pr%D3R^={9WN%VB87gVSYinBC{djuYAN6(wI;HG{;wkl$Zz5dJ5%_CpL4+LH+*E4eM`3sHASS>O3l$SPAr)$z_7X^VA z!dXZ=9*U6&P|!BhUL|+937KdMjABoM9jdAo@Azg8mEAe&t!?t8)x5V5|*vXa^1vSsA${q9vE{j?|?eqZQR4ml9D zB@9ODbY5q8X&->;Z^>8o?bU*_%_UUA0CdabU#SchaNDWF_0-|*)Tw&v)aS=r;ylF0 zNtvIRoPciPynE{umfA(NpgX0uQZTi09_fSvX;aO6GZ976%p9i1leo?poigZ7~ztjWL`GEAoTSCGI1HTy9=KAVf-{a6r+#B5L(VOy`d~;-NWS8I= Nln~ngwKL@$^&eSOdAtAs literal 0 HcmV?d00001 diff --git a/venv/Lib/site-packages/dns/rdtypes/ANY/__pycache__/X25.cpython-312.pyc b/venv/Lib/site-packages/dns/rdtypes/ANY/__pycache__/X25.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e4827fa13582601ec68f2c509621f349b27b9c0c GIT binary patch literal 2388 zcmb7F-ES0C6u);qc4xP{Y+1fm5iJ$b1=<=vFa-myP1u?c+C;N8A+e7%eb7I^|G*cTVyL4J7@l~mEDwYiJ$JsgfyTs}_S|#sIrq-_ z_?@%AX0vGo%SxOdUrZu&p9|4SgiPlqFl|H-!4@h9J1z^pEQY%j?s8c$gi0oQqPuT( zCZIEj2(yTYaA#ZBHsUJ15!c;U#DHH3kIJcOT$CPgsicQz{ubJy&X*uw1OE+F#sq<% zxGWN30+*#08plMORmw6^h%}4K2_u<8Bt_&|9KWxSG)aIiWh6-tNp8}lTj(+__l%%o z#uGn(7tNZ= zs+y&a)tPBS)ba6veo=GB9JhPG?W}>waDeMB8TO^obLQA6GhJh#rs1FNPqygSBaz0$&;#E<|zl!wEYGUkifFfR7)&y0eeu?c6TET(P03F~& zC^e-1S6DyPMug}*nG(&_AkhZXwVF9T9h60)+!-(7P;*xepn7+lNux01i-^7HR3o?be$mOFA=ITA$6Ba9ZWnIG*hz(7C7u|nrtM!C0)ZyX+O2|5p3N!inFMzGv(7|4=E%sFb^?q|bwIfXnS1m%3B8U4sO} z5Yq7i=r+MKQPU_4xd5U3+s`DS*9RZZf*RO1cE*t0bjq73MCQGG;couWYW~n#{>awI zg!yoR&+ihD(&@n83?JXNRyAOJr&YD;kh;bBjH-TB*Q`*JR8`{W{)AVV<+w~${W(bs z@O`hxALCWSz333i+k_tCWRUB!;gHu2%Tm=Mru=u9@iuthZ#v4SjZb*`*jXTtXVG8M zPH43J-ua%HvyUV}KGspNd~73u65H>cA9*NB@<2xfVME|tMw0h$WI%i<2%+K;=lnAJ zZx_?_0KDvFVJZ$9mvy7&nvPBL@tIkz)?IB3(3;{V!QAw%VUQR$AeDNl*of}n7v)d* zboJj~M!~oE#C)Qh2(tJdeHTO?Dmw*aL&O+&4j{bkKHB*Q|LX+`F$waueP8dpiwmo` f@JM(O542vsQJgE@7@8aEAP^x^;km!L!vDy>68sBF literal 0 HcmV?d00001 diff --git a/venv/Lib/site-packages/dns/rdtypes/ANY/__pycache__/ZONEMD.cpython-312.pyc b/venv/Lib/site-packages/dns/rdtypes/ANY/__pycache__/ZONEMD.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..51d922a05a2cec3cedd3d437d8cb7a61eb4fe38c GIT binary patch literal 4208 zcmbtXOKcm*8J^`nxO|yfRi*qAML$#8vF!R4w{opGwUr9CXuxtDh!MJ7ahKv!B^8Q~HsM`jJq zJhb%_U_XTKP!KtL(j!wzdII+S9hGD%C{4-qJwD0Gyv*FAlAPl8A=xLh_o&7WyzG}b z(D@Xv9FV;&8h-`dp^||S6c3tQ@>=5Z@TKXHJ;tz-DquOzn4GR)Rm#Db$|!lo+>w#= zj40(M3RpEVd6SdX2}L(j-j>rdK$8#H(mV+LhFU~MLpjHn(XUv9%2e620$S22R7P2l zZEt0Ig|cO3B)S)Z4BnlWwLsu<_LvR6$`dXd{-&(VwAgM~e2wik58Bo?+hV(MM)=0= zHs|14<~Gg_bK3w)rY66G5Dpl7TsyMIU455=HC?W}Iks8TxYfJVYrc1H(~e7JdtItI zw%^%3*^IaB#iu-|>~$fTjWyT0lyShNz}iliZjP?C$+x!MK;s?nQd4LO#n}WVu$u0& z@ftf3Q*}Typ!k*|Pw#ydz1y^XHkM;KMVl}(F;z;&@3{HAUAO;$x|dWQ-S#rhv@L9djohBf49Pc5Cda zIzFbWhH|VZr6wf^EMste$L*u)$1f&EkK6I7uQ-JRaHQ$5!@CbaTtNR1bYIV+Cwyqm zTjh5ybuS-&%*Qt(yB7-!h2?ZDa;(DqGVmm_Ycao&uSZT)BPZ&SGu6nMRnYyw(na?@ z$luS`qXX6GKs|b?8ojjk<687ag?Y^HA%?UCZTV&`@+O)5G!UNepX*=R_jzE?lg_>C zJ*R4&r`Ln0Hv-{$AX*JXmqByD()8Cl&#wp1|4Zmv>{;lk3w>3guP&Ud3MW@j*G~;r zPYu?DcPrfA1HwZ3<4iqrq#8N$ht%r9KOOz!(OTby&m$KsGsCsc#CkBXNu%z8%GoXW zT$E}ANMOiok?Yy|H*|}rOlO@y3T&gTIMfUc`qtHsT&5trI3aq5k!f&D64^qa?JVDi4UX-#VM067K+<(AA;E?i)G1>Os|+x?x^W0%NgPn(4<% zPBPS6#Irn;w{Z-VI`MSNIdBh|>op3Zq1-X1cR1*D_=99_8oF7u5em=0KllDGMk}6; zuE^rx!r)T>L%P=WX2rJ=-n$X(n7=Z2r8(4tz13jvlVEs$Xl|$;?5PHOmgEPM_b02t z%d4a7!OLs=f!g%3G5=SH?eK3R*5}7j*y@$UOx}dN^wvM2do3=!?Qzk-4gVUw5A^fF!mhe-peh_#hG=sXqD{E-qqmj@mj@pXt|rz7YvC*F{1qEKtv~oCNM|vJ(#J#4ISh!p?h)9!k(p@%Qt@i!6zTsS>Q9HK5)G{ zaJ?psz+3addHw#-;n->~B$~8QX zl|94{%B)N~BV&zR(-%o$2DKZqs1|=CB&BPV_Z?f zP|X|lIYOKY z_(N6UP)&HF)l0tn$Vxc zS5O~nQeLs@Lor{FOF2RZMe(O4Dd%Xsq9_+qAcm?-&lL<^6s-~m$H9gfunMufVq^+3 zJ_8awK)OMq3pr(4O3CF!F-~Dpx9jAJ-?q?(i62=V`GKJiL-+MP^be-{Igf;k)a=j; zW(RwX+KTb4f3pj5!qaOb&uN~$N^Q}=ZF(%Si)Le+e&C;bXm;3Ey|75@!!7&q06szX zItQH@g3_%|&F4#oG@etim0^8EW@lMTZIPr7wP!cWWM6aPL1K)(PExbA$s=TMNK)-f z{=9vi7w~()l5f_BpxdM=irV53)$s)ieTllhKtMp>waFn5`(e+|dg|29Dz)>4=L|JW aRYvAV=SJtnIdKaS9zNT%>nlrTZS`MUYl4aZ literal 0 HcmV?d00001 diff --git a/venv/Lib/site-packages/dns/rdtypes/ANY/__pycache__/__init__.cpython-312.pyc b/venv/Lib/site-packages/dns/rdtypes/ANY/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..06a091fe93c4078dcec43e9e579f449163aa05c0 GIT binary patch literal 602 zcmYL`L2uJA6vvaeX{Vw|@C|xE+InbC{${th6y*M3z=@I#g~P z_zc{+@mcr;yenS-*lj1AY6DCDzvut^*?!Xdetg{4)XJN|yC0^e{j3iU%eYT&AFJex zmTC)at)==`eWCA-F8X+4<%yIXOy-@_wJ1cHt-75uO{%0*?YE+{s>?$5y8|%sh}m!8 z2>Vg!cz6y7Us`}HjG<-OguQi?W!VfGmJ>!`S_J!!3T#dkW_y=l+6Sw74;zX#ovAx~ zaR?VfXpCu~yopOj0icOXq2V%{0P@bylqdpufdz&~!SH8*h--sM4zxkk$AmeSGL)V} z`w%D&u@CKlI)sktp~ZkkfM^tg5mGS2khx=(M$lxw>pK__3u>VwFd3VSR1Z;ouMv3+ zE3I)9&D0yy&;>L*dk)P-KpfvDT?1O2r<)b$b&}*c=ijv3dIWc$t(Wmsic-egofLUo zZ9Ww8D%)~-o#cGElUX68{Ac%8S4p+pRR4{d%X<%CuddyQ;wJX8Wh}Ew^tQ?BeX1xX-N`4_-*MC3N^!}g5vq#3K IRykDeU)Y_(LjV8( literal 0 HcmV?d00001 diff --git a/venv/Lib/site-packages/dns/rdtypes/CH/A.py b/venv/Lib/site-packages/dns/rdtypes/CH/A.py new file mode 100644 index 00000000..583a88ac --- /dev/null +++ b/venv/Lib/site-packages/dns/rdtypes/CH/A.py @@ -0,0 +1,59 @@ +# 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.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 "%s %o" % (domain, self.address) + + @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/venv/Lib/site-packages/dns/rdtypes/CH/__init__.py b/venv/Lib/site-packages/dns/rdtypes/CH/__init__.py new file mode 100644 index 00000000..0760c26c --- /dev/null +++ b/venv/Lib/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/venv/Lib/site-packages/dns/rdtypes/CH/__pycache__/A.cpython-312.pyc b/venv/Lib/site-packages/dns/rdtypes/CH/__pycache__/A.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c542f846beab37a55118d1c2be6318d4fe2678e8 GIT binary patch literal 2558 zcmb7F&2JM&6rcU{`U_)=e377pk09IB)@&%o_X&#Z+?$P zLkN~B-pH>>2)(32dx<{NdH_rvX-H#Cl=Z$$mUU$=%j#?aBKNO@bS%1xGR8@LwO4iU71oWQqpZl)U2Xnl$?N~X;>#j7540t2cB-V^&^mjMCy1On;P?lYjEE5_s zMAyQ*`70N5-w_>?+;kZ-Tydn&fp7tu*6b)x_;s_v-1!sb9}NKD*JE-i)O;q%=-IfEobO zUK9o{>?eSM`fvpwzHJBkg|iD)v3=v`1K@Q)#WdzOwhicciG^Cz8dd{UY2B|IREZmZ zMTb(7arg;^=ucQjn$&F<)=r3W9-@;$hf__04}mX^0dY9nx{bT2*iFUgZq;Pl+J!iw z_sp#vC0_M{d2AKE`h_#8j_?t9k_$i<(2j%>UG;&Lfz^DY@N}dZzYO&QO=>08P#&e4 zi8HmppRvTF3#(&|v9+ts*yx5d>J_tdZy`~qn7@GVf5BGeU>^8|Vvya*TcBC@Er26B zXppz3)4Wh+3KaJHR#j*>h(U8MWZC1_lA?!K3bI)P>yAilTvly|r`OgS9KtMydkmf~ zbosnt>W-vZ#c4W6-L^OpRVi5|Lsd)zkj#T6zdL07PDs!z_dneG?(_7jlZ!wA$x>uF zycFJ&`ZuNi)vp_8*2bP*eF+_r;Lz*2SB$TBCf{CTF^yI{*wQwEF&nXHU692%!7w|3o zL05hts9PsuiNXy}_wG(l_ddttLoz@ocMtC|AzxwHgZ`53@j%{)Aaf$ 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/venv/Lib/site-packages/dns/rdtypes/IN/DHCID.py b/venv/Lib/site-packages/dns/rdtypes/IN/DHCID.py new file mode 100644 index 00000000..723492fa --- /dev/null +++ b/venv/Lib/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) + + @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/venv/Lib/site-packages/dns/rdtypes/IN/HTTPS.py b/venv/Lib/site-packages/dns/rdtypes/IN/HTTPS.py new file mode 100644 index 00000000..15464cbd --- /dev/null +++ b/venv/Lib/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/venv/Lib/site-packages/dns/rdtypes/IN/IPSECKEY.py b/venv/Lib/site-packages/dns/rdtypes/IN/IPSECKEY.py new file mode 100644 index 00000000..e3a66157 --- /dev/null +++ b/venv/Lib/site-packages/dns/rdtypes/IN/IPSECKEY.py @@ -0,0 +1,91 @@ +# 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.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) + return "%d %d %d %s %s" % ( + self.precedence, + self.gateway_type, + self.algorithm, + gateway, + dns.rdata._base64ify(self.key, **kw), + ) + + @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/venv/Lib/site-packages/dns/rdtypes/IN/KX.py b/venv/Lib/site-packages/dns/rdtypes/IN/KX.py new file mode 100644 index 00000000..6073df47 --- /dev/null +++ b/venv/Lib/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/venv/Lib/site-packages/dns/rdtypes/IN/NAPTR.py b/venv/Lib/site-packages/dns/rdtypes/IN/NAPTR.py new file mode 100644 index 00000000..195d1cba --- /dev/null +++ b/venv/Lib/site-packages/dns/rdtypes/IN/NAPTR.py @@ -0,0 +1,110 @@ +# 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 '%d %d "%s" "%s" "%s" %s' % ( + self.order, + self.preference, + dns.rdata._escapify(self.flags), + dns.rdata._escapify(self.service), + dns.rdata._escapify(self.regexp), + 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/venv/Lib/site-packages/dns/rdtypes/IN/NSAP.py b/venv/Lib/site-packages/dns/rdtypes/IN/NSAP.py new file mode 100644 index 00000000..a4854b3f --- /dev/null +++ b/venv/Lib/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 "0x%s" % 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/venv/Lib/site-packages/dns/rdtypes/IN/NSAP_PTR.py b/venv/Lib/site-packages/dns/rdtypes/IN/NSAP_PTR.py new file mode 100644 index 00000000..ce1c6632 --- /dev/null +++ b/venv/Lib/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/venv/Lib/site-packages/dns/rdtypes/IN/PX.py b/venv/Lib/site-packages/dns/rdtypes/IN/PX.py new file mode 100644 index 00000000..cdca1532 --- /dev/null +++ b/venv/Lib/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 "%d %s %s" % (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/venv/Lib/site-packages/dns/rdtypes/IN/SRV.py b/venv/Lib/site-packages/dns/rdtypes/IN/SRV.py new file mode 100644 index 00000000..5adef98f --- /dev/null +++ b/venv/Lib/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 "%d %d %d %s" % (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/venv/Lib/site-packages/dns/rdtypes/IN/SVCB.py b/venv/Lib/site-packages/dns/rdtypes/IN/SVCB.py new file mode 100644 index 00000000..ff3e9327 --- /dev/null +++ b/venv/Lib/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/venv/Lib/site-packages/dns/rdtypes/IN/WKS.py b/venv/Lib/site-packages/dns/rdtypes/IN/WKS.py new file mode 100644 index 00000000..881a7849 --- /dev/null +++ b/venv/Lib/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 "%s %d %s" % (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/venv/Lib/site-packages/dns/rdtypes/IN/__init__.py b/venv/Lib/site-packages/dns/rdtypes/IN/__init__.py new file mode 100644 index 00000000..dcec4dd2 --- /dev/null +++ b/venv/Lib/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/venv/Lib/site-packages/dns/rdtypes/IN/__pycache__/A.cpython-312.pyc b/venv/Lib/site-packages/dns/rdtypes/IN/__pycache__/A.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9c7889aca3496f5748cb94368951187253f76d38 GIT binary patch literal 2152 zcma)7&1)M+6rb4-X(daMBQ>#;G;K;96M^Lv^3k-EQWHoB6a|u$f-oJ{J7asDl~$cu z+p$r=1w-sZa0)HXp@;O;2LCC&#JIufriYZCdUK;3LQZ{eKV;*Y(g&LNIXmyY`MuFE z#bTZSd70~zN3(?diAqYDMCkqi!aCuEQ;#%g#I=O$2{#gMHmIO8h4d4<{2-e~t`e>- z6RyLaL%Xr5G}E6-yL>~B<&DJD$Su&S@fT`!C$U8=i6h;wK)na~izxSo#%V+6+7xXV zD`b*#ec5W5+~USEZDd3?M|h5#%e41>i|2U;d^wTjeLTCzlV2fsXru2YsTM*ywwN0` zC57WlJ{o3i&ZSVwG4^DHGA7l%0n7JvodiASs?j0$O+q@fqitxhnUW5f0o#7+=o>V) zbqGs~Bn#v*tyhiERI_a%LyIxDSs7EPp8{HXbNBMc<6kHt)p&ar_KpYsd`nHcZKj&G$0lZ#+k%pN=9)8Y z2NQmibg%#s|p$l=u>2(Vf z>|r1Ly2oKz$6f>9PdAB#p&(TnH_ec^Bh2{HopM`YUJB0++&Onqgxc+Sg%^yik|m7F z20jbKod9q#nuza%UEypjk!PjKT4%NM#5y4n`N4+|uROZCRT_C> zjl?x$ohkKd#-V_k=|MFI$!HKVgT3BIGkbfO_~2SG{e6@UP|7KcPkhm4XwFN3VyFWI zLj&g~%mJrSRV&!bDPCe}n@pkHxWz2KF!uGV?IoTp zW88PZ3=f&|{6H}ly&v)j93A#WZ(CCY(>|9cz#@lmIfK4p^71*t^B9A{$G;_o^Y>YV zeTl$}mSgC+3d`$d@`q7*arpRyzNKrg49&dQwW#^_PKIO-zqo$$rEZwDt`5qMhEjo= z7b4$FO-mfFP>QZM`f@cN76E>v;*KNQfeW~M2abttZnHTX*b|6642c1XlH_(^$1yO{ zq6PSq0Nq}=83&j*FfKagF^P}F*XoV+N!-d$Jm_)VC~r7=!g#a2L!R+xc~qF literal 0 HcmV?d00001 diff --git a/venv/Lib/site-packages/dns/rdtypes/IN/__pycache__/AAAA.cpython-312.pyc b/venv/Lib/site-packages/dns/rdtypes/IN/__pycache__/AAAA.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a1d38345b318b7acaa2ffadb27efa29bdb33489c GIT binary patch literal 2176 zcma)7&1)M+6rb4-X(dasBR8@0)uz-j5m-(kO`4R_v;-0YMS&!xAWVn#&e&OJwX4po z?bxWGf+6-HIE5DH&_jA^ga4FXV%%bP(?d#6y}3~iA*a5#AF^>n=>xraU$gVxo8KG# zTrL+0ke9nYbv#eVpQvP%ONH+DAgmEiIQ2+_#;caDdb%3vYBs2#Go|bkyX)(08o5fi zwnVrNdk*ZzX41?+Che~4dLpl^r(f!k%-*Xp{RUA#K{$*0sb;Nt?`oZ9ld3 zb(+}Pgk`0Y1@f5It43t1*_M!z#hB~6A!E@1vlVk&bLUx_lf-XU&B#!~n~L(1JD#mn zl#@JMfUW3_3XH{8#uVx&fjX}`mp_>JObMwbTC=csA`Iqzb=z$*)wDe}IjdYBO759! z&TTuK48mOl#(A|Yu!g=l;m=Kc>P}85Hx#v&?cA}ag__{LI+Nt1&RnaX!QPIy7Gwc( zSuDp|R^MA-QX{}#cF6Bmc_qJL4L=-uH2RBG-5xr&H8i#{H1^Zyn?vW8jr*l8#1W_0 zGf=RTeemlZ1-6DAhjl;QBoYRMRB6&WL*kAo7f5&7^`oK`o*lY#?xKjaJM#*!85<`1 zF)AMhEEIRc1R&$7_71odj!K7Y_YbbNSK3dkV-hj%;7J)_?_0xk3R!|*8wY*yu3xl|D zWq|`LLrSVEOv#{`)OR4hmYzNevFdGL9kOGQ!NXgXk&Vj8W@U6a|670M!TS%dJi5Bs zKla2LOUlMlGwPL%0|I5!*OeWnV?xLbc6=Yr>}_M}gUiJV_E9!W*-BxQlIu1jb6x^0 zBOTxv88|~x0WcmiI}H4Ej1nvuQ?4f>%L$q-9Q(MBQOU9Wz;_+n18DT-DD9jDU1jsG z6d9gVwFc>>ecQH5tHqV#w`bQbd~;!I@YKfOsYm=r?+0&l@a(VFd;ih5{{_EtTMb+8QrLiZdOje9wt?-eygh2Z`Fa-uO!!0!pF|Hn*tuYB4f>f&w41A82fV8 z_EJxtF&;QzhNn$=L8usupA&fqj*j}`*RCnT+X0uyz#>PGokm|deG47od5pm$fPYsC z2l3;02qeNUW=GI*71+xq@`rKw*}OFj{&KY#l>vm~;;tiFp$q7H6OKu2ZnHTX+LMSs3`t;$lH_)1 zCowSF!Uq&e0B|qdOajas7#AP&m?THy%k|3oFlpsS@*=2MueuCuN2io_M+iOil)Ukj x^gks7AVB(>dGEy6C${MD1|5E(9ig@5v#S?ZF0Ni$xzr_~Lc!XRztIsN@(&wa literal 0 HcmV?d00001 diff --git a/venv/Lib/site-packages/dns/rdtypes/IN/__pycache__/APL.cpython-312.pyc b/venv/Lib/site-packages/dns/rdtypes/IN/__pycache__/APL.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..78cf9dbbf00cf8304064c26c1c46df957faaae4c GIT binary patch literal 6946 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 C$!swI literal 0 HcmV?d00001 diff --git a/venv/Lib/site-packages/dns/rdtypes/IN/__pycache__/DHCID.cpython-312.pyc b/venv/Lib/site-packages/dns/rdtypes/IN/__pycache__/DHCID.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4319c46568bb69d38cc65cef88fd41be11bf46b7 GIT binary patch literal 2296 zcmaJ?&2JM&6rb4-d+pd>ngR_Hp9LxwHBgB_1wsgkh}Kf&NQCxaMIEho#$m&H*PYoA z92qG{kw^|SAhim|RH{^=T>5|Xf)%RJEe9%2y``7~;?y_0YsZDCqxj9dkC~nMz2AHJ ztys(>7+z+gemRTKOR4lmCS7!YfW<1Jh++>_lNnc)SXa|ot13*HiO=e7Pv)I$3i<+3 zWf4&o_7wgbFSVJ{`?hs8v8|`Bs?oxh_P5k3PHKy%G!AvY1@#`>o1rSE2=0liDpjU& zRa-)JOw~oBs#Ak%i@2I$*&L!dsxRX0^J$)Dz?WlLIz+Prp8OKJjjKbG$SOqo_?K78 zJo_z|z7ursQRjvo z)`D=`AmhBzUf`(o-D3W&sjuAHlyE~f+O(aU_B0by)EB3d`-s!!%IO%WA}+A?S8HcQON0o1&Vy# z5d6B|fPhq_kOJ>g_`%#CGlOYNt&p5vZS><1aLPq(^=JG&oyB*FK6C_gg zIQYb25P*X@ywX~3ZJI|r=FzqBU&=p~JLcH0!yuj*V|*6`Ir3V)Az_F)*@nd`Tz#ew zL@xxPUOCJR0+_mczB}1ao)wRsfN5uYti59>z--C%w$Ur-ZLp3wf$!KM^WjH0YuG?z zf10?I`Jr2PfrL?p`A$Gtlu4kBa*sK4fr~1UF8FJQz}ecS;4Y zZU=t$pvBMz64oh^KF6W5(wa|5rqx}KMTQeJnj8u?#w98^wjcPeV|&24e$l6dGXNzr z=W^Dsy;T1U?kI*qETCsbVI{wue=xRs{^9xGjRWt$tWUCY$;ObPddsV z-YlZOw7tOD;gJW%Lix3(=vLRj`q`}v%6#x*V)B)$>nFP^C|inLm9Ty=_PtV+)bU!b z;v~msTRFZTPL7Juepm@j1PQ_ zlEY=kduv=wV7tU+RmiuA%OJK?jB)oE!ljpJ_n-1_7u-_O?;W^%U=xpY@W^XrKR&s1 YWu?4aUYS^)=psqzX_)LF#rGn literal 0 HcmV?d00001 diff --git a/venv/Lib/site-packages/dns/rdtypes/IN/__pycache__/HTTPS.cpython-312.pyc b/venv/Lib/site-packages/dns/rdtypes/IN/__pycache__/HTTPS.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2e6f17301022ce64b084c911ceafc5f80d8313bb GIT binary patch literal 686 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 literal 0 HcmV?d00001 diff --git a/venv/Lib/site-packages/dns/rdtypes/IN/__pycache__/NSAP.cpython-312.pyc b/venv/Lib/site-packages/dns/rdtypes/IN/__pycache__/NSAP.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..293857575a5663a7b6deb90d3298a2e110d1c26b GIT binary patch literal 2741 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 aU}5-3kCne2i5^0q9dbq|=+DL}sQ(A-Z0ckH literal 0 HcmV?d00001 diff --git a/venv/Lib/site-packages/dns/rdtypes/IN/__pycache__/__init__.cpython-312.pyc b/venv/Lib/site-packages/dns/rdtypes/IN/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..cb2d9ab3e0df9fa700f52939f308dd988afe5b7c GIT binary patch literal 385 zcmYLEO-sW-6x>ZxTa@~Nc=Hg@OAl%9Dnd*P4J|P=wbl|?w#iz9+a%bV2Mu|fvv}@zqp=TB z&Ww}>_jJO*4`XO#q5K#cw$mUaildy6db(Iu&ZbE?;C`#dCV6Rx8D#rNh7~!LD NZD&h)DrftJ`vVg;cy0gy literal 0 HcmV?d00001 diff --git a/venv/Lib/site-packages/dns/rdtypes/__init__.py b/venv/Lib/site-packages/dns/rdtypes/__init__.py new file mode 100644 index 00000000..3997f84c --- /dev/null +++ b/venv/Lib/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/venv/Lib/site-packages/dns/rdtypes/__pycache__/__init__.cpython-312.pyc b/venv/Lib/site-packages/dns/rdtypes/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f73a7bff50e28d78863b137a49920e001fbae21f GIT binary patch literal 384 zcmYL^&q~8U5XN`Y(1dD05xfaKc*!9auOfofgM!3^c(4SPO?GXUZ2pkh#HP1Cg?Dd0 zi*JytFQE3;li9}Vz|4H}o9{65?l=R~M%=%9IyDjc(8D&2U$NS2#S3C+f~JV!S3JSB z;r14XSN{F5U{ukes&hV!Bn9B$?l!$k|FJP{{KgnxHv^VJ%Z~6>2-QuO_Kr-)o z+AVCTqHsfOC4qKkHCG#9mP$z9H5!W$mPLeUL8T<*4XyOC>*e??cmQ5Nke9$^pt9!_ zW+Eq$P)WiPM9KmFuoum!3N!T&v`>0lqo;QjPpjZogaHV}y_`ldopK1;uSNyakHd%% rkqSk~NnWq*^CV-X literal 0 HcmV?d00001 diff --git a/venv/Lib/site-packages/dns/rdtypes/__pycache__/dnskeybase.cpython-312.pyc b/venv/Lib/site-packages/dns/rdtypes/__pycache__/dnskeybase.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..171fdcbfbfbae4e62f4701006af7f78b054670ff GIT binary patch literal 3908 zcmaJ^U2GHC6~5#7@%V2NlK?Rg@IqkM+w#u>yPKt5AZf#PO-1NdYekw)X2!`lw#U6Q z0UR00QIT+@-9l7Vh9^Q*X@l}YpSpdcPp#CKwY*i_frstx6K^TuVZ~F=z2g}hEA5qi z?z!ijd+*FS-#PcrKVq>kf;J~y&b}2v=sPm-n$Tu84>1VUk%lxTk1}r0WEeUQWCFBj zGaT*tb}zL1V1}DPTHp^%Mhu{zAdS6+G)`x7+q_LW{Tl1?nXt~~c68((x0Q^p$-|lg z%o*}5JAH0fhxa&dpS89iBkAGXUQgTUGkqXWcE%o;=?D3KZ`A2C@iLPNJ|}{7ignn@ zv7bA_m9bAQe>CQBnak-hwPTAAxRka{^3-*tBjQkSCX->c0634$@MSK=JKT6)nR!mE zpH~SX6VoZd5oNik6m(g3!m?a2wfQ{Zqq6+#c_r^^FmWj62;qS|LYqL*tKVFBZ|WD8 zj;*QEyrt)-Z1Z~2nl(zYRZ#Ns^t@#hb<6U_ht*ldo;K}{fY{{^dxnRJ?wVe_HuW=O zddf0v{cuT9=ad=Un$n8aQLNeJl5QOZeoik>E0%twRK`K_RD|qw|iXI#u`^cSq88sW_V51xLZs{(1RhQg*5&aDni;u9{d&aSoq_2wILI*%H|WSJVtvL(Xgq}!^PI1ZDVv@p!#-p-XnUyG?{+Y|Rj9vr=YboF3!*RkgI<2CNHaBFDK1L?lB za-%tP492l%i9HYY-QTyKIMGO)SWld3B+h&pY9`KuPHGM8te;yxw{oC4aHz(8F4F8$ zy<~ztQ3(DdAe$>dpxiSM6ySOrNZa7+)_~LpRYz|7fXop$^t2p3dR`a8I?>l9WRztL zRAK+f{Fcdb3jjz+%s_e}a!^aQe^x5k8;4<-=kPQ`0TiU}oIYh_%Wg0^ObWOoKpM^% zMJJ5)ykZ;Ij0N2Z%w4z0tB{bsK~&ZVVG zr2Ba3W74IU($AuOcP5r5)}zTrG`TYP#n8i{M)cU~6(AcSJ{kTG;`_rJh!2I`nD{<} zRf*SVIrtWsI`|;hPG%VTZLQ7S41&HpqHVda7eVir3x7KD^&!xA@7^;%M-bA}{JCK} z;;FWZU?uFO^36y9RcH~b1S?`CREey$OUxv~Hmay$F99>jedhKygMkG`(mWSp$wYN6?F^sp*d32Hy!ypFX8QRn~MU>#Ul$@B!Ek zj{tF4+nmEGGJKm1V;w-qw%G$MhlPbGjx2&z0u-;gfC*)>S#TjU*aaD1`X0=pd0s^u zB8m^zrDdtvpRDn%fgSY^mOuF2k7{CTaA!TeoNf+|)Iu#OzAl|?NGF@pJL}SDLmF*L z=W6UTX>eWI+mQC&vzpR@8vC!lZFkSy8($e)J^T36Cv3Ct;#Z=3imKgFC*G!*`a3Xi z!1##TDz9l3AnDN4AW!{+p`!xn7J0tbo_~|CFk2os{*k}{F1EDTvMP9pVo;gXgIC-e zKRtgQ&?5ju=2hF_$UUStcKmfXd3Co62yPYNvPNEaMAa;mFx-4744kN{6wRWcDtS^A zgjrqD08`|h-B`Pk76Bddbpz|K*Ymb;46LnlK%kt*ch+|=@4olR%GED24>OMk|2+Js z;V0bRq`ykd_~n|=5~Fv*OX0hxzZQ3S{Hglc<+J}3_qb)DJA{5&D8b_Yt1M`Y=OA$W z|1S)JpOU;5AO~@U*VuN?(O&2zX^Kzd_rV5}V*ry!c}}=gz|T+O{1{1FKQEy?zeB7$ z2`7{k43|(h=gt!yIb|(Ue%i_8dbpov4RNvT&RE#pn^IlMOJt1?fmG2q@ty1O{f+qk zmDA1m+u&i6E&ChN{-!k2$(8Zd;}0iRuRT^CmA^dnB>tDwlgVb^#8=`3bvmHxYKl!? zh5LLuJ?>VgC?^rpMDH^1|{ucs@j8ua!1(w-XJ9TGtY4jxmb|Z=c{O#ez;dN%H!3@0&yv1B%?he!w w%Zd8F<$arojKPC}#0FJuWEd2T+@4*Wt+K69?9T3`-BqCllJa7Ck)x0JU+G3OyZ`_I literal 0 HcmV?d00001 diff --git a/venv/Lib/site-packages/dns/rdtypes/__pycache__/dsbase.cpython-312.pyc b/venv/Lib/site-packages/dns/rdtypes/__pycache__/dsbase.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1542e209a3b8e6b7eb2bb0be65ab485bd0d9d4f9 GIT binary patch literal 4207 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 D6GI9| literal 0 HcmV?d00001 diff --git a/venv/Lib/site-packages/dns/rdtypes/__pycache__/mxbase.cpython-312.pyc b/venv/Lib/site-packages/dns/rdtypes/__pycache__/mxbase.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..da0f89a7c018b4adbcdbcd24d96dea74fef14466 GIT binary patch literal 4438 zcmcIn-A^3X6~FVfJG-zf7%cr7L+Udw*1 z_;tvy!%4}MCL05DmAclLNIoKlv_K3Q)*Ptl2Cd1KpjD|KN?!Y+Ka{L{&F?Ajy^vY= zikBdZ8XJ9EuBd*X0q;o;--hv{?>WrABU7?p!+$v z+$7*?k`xUATq+vUxR{g|$(Ufs3u;m^>I`K;NQTUCgc$WkXhA4DLN#JW80I2o*k~}+ zs+p(}Ss>u3un}D#W&Qd^a$88ohDjpsh6Zn3gpKBVPAhDOb_O^t3hI!(8j@K^h7EEK)Kzue z%36-DyHP#G^qiG-PMx8s4O1|XO--2;uLCTRnVA`L!_?q`bVJl|=D-Fja}_tD>wHSr z87|SV%>Q`zQvb*`W>PjXodXLajy;=Y6V|lOrcxPwG{>y0$yj-~3%uuy+D>Hv^)g+7 zVi(TMnAw?;-&ms~%yP`G=~VhwYTRTaMwaz>yJ9_4w-I#R({mK>c|PvNwmUfQPhgOp z2Jyo@`I{PB3U8|It8Ht?zEcz1hu`|D=gXc4CyIyPTa^D8-)?{NtJs&ZwTWVT&!YTA ztOTn_y6k=EV07!?zcd131>*tr-ZELe!k?^;`5oC1U?tkxQws8!moqU7uE5CmnCfP|4^JKfZZRLaeA8a+B-fTYo=*+kM#pcV4>UR9#>Jjd;vm1wc zH{-nOE#e5E^f1>X!W z8TS_o&u5V_YEOAyYxKQV9zmC}cR|dP9hDqBv~u(Q&0_1(#prhP!Iks(&##3ZL>~C{i}AhM!Ky~j2{XWg2gRW?z%tF z3;SjLAmFOUTbDmu`fRm#t>v4}KX(@6rxxXHwSGCe6usB`o!XA=*ivju?buX1R*kif zAB;W9JXSCLn|LI_@PCA}vLBoSf8Pm}p^N~38Oro9d?OL29Z*%_HZ+~0K&&2g%QVa| zu0BjXkT!BiqLX!RMZT>Xt5p+yu$1+Hm?uwL-`HwBw%K~D*m@ixduZjt{R?Ynip?kX zg=-m-U;x|vw&T|KqeTCvCIpYO1dafPOZ}L(=+o3r0}QO}I5aI_GiNUUM!@AYCNIEZ z2KgMEU_0Kte0S;YhWeHlz3O{{TjSfi3XSU5xYli|#mO~qj}SyNIGF58@FwA#iBBbS z<(y+>+(W@hs!s=rgh>-3kFl%b^+=6oOop2d$foHP{d{{3S8z)gVI4!f%#(k_4oE&aHUgX1wp)D2j^2|V=vW;8@+x5Es zyId;c&xCc|u+uOMFF(uJ4%2o1+R}FTuv^EIfhp6Ounnrg2t9$K3k70~o&n*;{G=gm zW-_|IE>KLHIR<{t(M$0vaDk^`V<3K5Ab*$po;G!?<~JHo%>U|zEGa)LMMdR3;Mq{q z(@%z4w)_BL6!%ME4S3fLA)qh+A-KFQQhYmk{MS96r^j&MrzrT#i%EtTdi)L^ zP@DJSkDtr8z3jp6fa?cglHsdeoyN7SI&0S-Tzjm(>&5VWn1|1vg^D7>QG5&NITRd` zA7YCW2U3p@yI+m%Hn@x3Z2y}Ja2XC9UQXNa^{A8^2n0FWMlCHH>^ zOAV;}m!W?-2jEmfFM)gL06MW|WZ&NJ;N=)!$F;R%Jn~rWuf&-C5Ei_h|P+dIh=y{IeAhJCYy> zyGMxd<}(s|Mp~ZWU;GJad_vlQ$wcMzjz4s43GJIg`wQ`yFd!@rFMqo9>9W40mk9QO tPzEJ>ScYmBkh&^9q^Bih6b>13E0U|@D;d+z|U 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 literal 0 HcmV?d00001 diff --git a/venv/Lib/site-packages/dns/rdtypes/__pycache__/txtbase.cpython-312.pyc b/venv/Lib/site-packages/dns/rdtypes/__pycache__/txtbase.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..875557f3bdf56c045861ad552a5e8123e8007e39 GIT binary patch literal 5070 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 y7yT5q!T@Z}@^5$jw5!T=l$nl)?DrTZS``O@ literal 0 HcmV?d00001 diff --git a/venv/Lib/site-packages/dns/rdtypes/__pycache__/util.cpython-312.pyc b/venv/Lib/site-packages/dns/rdtypes/__pycache__/util.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d91827aabf40ab588b1ab7a817c47378d428ab0d GIT binary patch literal 12875 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 hTN% +# +# 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.immutable +import dns.rdata + + +@dns.immutable.immutable +class EUIBase(dns.rdata.Rdata): + """EUIxx record""" + + # see: rfc7043.txt + + __slots__ = ["eui"] + # define these in subclasses + # 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( + "EUI%s rdata has to have %s bytes" % (self.byte_len * 8, self.byte_len) + ) + + 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( + "Input text must have %s characters" % cls.text_len + ) + for i in range(2, cls.byte_len * 3 - 1, 3): + if text[i] != "-": + raise dns.exception.SyntaxError("Dash expected at position %s" % i) + text = text.replace("-", "") + try: + data = binascii.unhexlify(text.encode()) + except (ValueError, TypeError) as ex: + raise dns.exception.SyntaxError("Hex decoding error: %s" % 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/venv/Lib/site-packages/dns/rdtypes/mxbase.py b/venv/Lib/site-packages/dns/rdtypes/mxbase.py new file mode 100644 index 00000000..6d5e3d87 --- /dev/null +++ b/venv/Lib/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 "%d %s" % (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/venv/Lib/site-packages/dns/rdtypes/nsbase.py b/venv/Lib/site-packages/dns/rdtypes/nsbase.py new file mode 100644 index 00000000..904224f0 --- /dev/null +++ b/venv/Lib/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/venv/Lib/site-packages/dns/rdtypes/svcbbase.py b/venv/Lib/site-packages/dns/rdtypes/svcbbase.py new file mode 100644 index 00000000..05652413 --- /dev/null +++ b/venv/Lib/site-packages/dns/rdtypes/svcbbase.py @@ -0,0 +1,553 @@ +# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license + +import base64 +import enum +import struct + +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 + + @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 += "\\%03d" % c + return text + + +def _unescape(value): + if value == "": + return value + 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): + 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) + + +_class_for_key = { + ParamKey.MANDATORY: MandatoryParam, + ParamKey.ALPN: ALPNParam, + ParamKey.NO_DEFAULT_ALPN: NoDefaultALPNParam, + ParamKey.PORT: PortParam, + ParamKey.IPV4HINT: IPv4HintParam, + ParamKey.ECH: ECHParam, + ParamKey.IPV6HINT: IPv6HintParam, +} + + +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 "%d %s%s%s" % (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() + pcls = _class_for_key.get(key, GenericParam) + with parser.restrict_to(vlen): + value = pcls.from_wire_parser(parser, origin) + params[key] = 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/venv/Lib/site-packages/dns/rdtypes/tlsabase.py b/venv/Lib/site-packages/dns/rdtypes/tlsabase.py new file mode 100644 index 00000000..a059d2c4 --- /dev/null +++ b/venv/Lib/site-packages/dns/rdtypes/tlsabase.py @@ -0,0 +1,71 @@ +# 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) + return "%d %d %d %s" % ( + self.usage, + self.selector, + self.mtype, + dns.rdata._hexify(self.cert, chunksize=chunksize, **kw), + ) + + @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/venv/Lib/site-packages/dns/rdtypes/txtbase.py b/venv/Lib/site-packages/dns/rdtypes/txtbase.py new file mode 100644 index 00000000..44d6df57 --- /dev/null +++ b/venv/Lib/site-packages/dns/rdtypes/txtbase.py @@ -0,0 +1,104 @@ +# 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, Optional, Tuple, Union + +import dns.exception +import dns.immutable +import dns.rdata +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[Union[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) + ) + + def to_text( + self, + origin: Optional[dns.name.Name] = None, + relativize: bool = True, + **kw: Dict[str, Any], + ) -> str: + txt = "" + prefix = "" + for s in self.strings: + txt += '{}"{}"'.format(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: Optional[dns.name.Name] = None, + relativize: bool = True, + relativize_to: Optional[dns.name.Name] = 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/venv/Lib/site-packages/dns/rdtypes/util.py b/venv/Lib/site-packages/dns/rdtypes/util.py new file mode 100644 index 00000000..54908fdc --- /dev/null +++ b/venv/Lib/site-packages/dns/rdtypes/util.py @@ -0,0 +1,257 @@ +# 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, List + +import dns.exception +import dns.ipv4 +import dns.ipv6 +import dns.name +import dns.rdata + + +class Gateway: + """A helper class for the IPSECKEY gateway and AMTRELAY relay fields""" + + name = "" + + def __init__(self, type, gateway=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 + dns.ipv4.inet_aton(self.gateway) + elif self.type == 2: + # check that it's OK + 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: + 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: + file.write(dns.ipv4.inet_aton(self.gateway)) + elif self.type == 2: + file.write(dns.ipv6.inet_aton(self.gateway)) + elif self.type == 3: + 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=None): + last_window = -1 + 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 = 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): + weight = rdata._processing_weight() or _no_weight + if weight > r: + break + r -= weight + total -= weight + ordered.append(rdata) # pylint: disable=undefined-loop-variable + del rdatas[n] # pylint: disable=undefined-loop-variable + 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/venv/Lib/site-packages/dns/renderer.py b/venv/Lib/site-packages/dns/renderer.py new file mode 100644 index 00000000..a77481f6 --- /dev/null +++ b/venv/Lib/site-packages/dns/renderer.py @@ -0,0 +1,346 @@ +# 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.exception +import dns.tsig + +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(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(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( + 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( + 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/venv/Lib/site-packages/dns/resolver.py b/venv/Lib/site-packages/dns/resolver.py new file mode 100644 index 00000000..f08f824d --- /dev/null +++ b/venv/Lib/site-packages/dns/resolver.py @@ -0,0 +1,2054 @@ +# 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, Optional, Sequence, Tuple, Union +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.rdataclass +import dns.rdatatype +import dns.rdtypes.svcbbase +import dns.reversename +import dns.tsig + +if sys.platform == "win32": + 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): + 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 "{}: {}".format(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: + # 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[ + Optional[str], + bool, + int, + Union[Exception, str], + Optional[dns.message.Message], +] + + +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("Server {} answered {}".format(err[0], err[3])) + return texts + + +class LifetimeTimeout(dns.exception.Timeout): + """The resolution lifetime expired.""" + + msg = "The resolution lifetime expired." + fmt = "%s after {timeout:.3f} seconds: {errors}" % msg[:-1] + 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 = "%s {query}: {errors}" % msg[:-1] + 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: Optional[str] = None, + port: Optional[int] = 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 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 and len(self.rrset) or 0 + + def __iter__(self): + return self.rrset 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 HostAnswers(Answers): + """A dict of DNS stub resolver answers to a host name lookup, indexed by + type. + """ + + @classmethod + def make( + cls, + v6: Optional[Answer] = None, + v4: Optional[Answer] = 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, potentiallys + # 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: + 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)) + 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) -> Optional[Answer]: + """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: Optional[CacheKey] = 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) -> Optional[Answer]: + """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: Optional[CacheKey] = 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: Union[dns.name.Name, str], + rdtype: Union[dns.rdatatype.RdataType, str], + rdclass: Union[dns.rdataclass.RdataClass, str], + tcp: bool, + raise_on_no_answer: bool, + search: Optional[bool], + ) -> 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: Optional[dns.nameserver.Nameserver] = None + self.tcp_attempt = False + self.retry_with_tcp = False + self.request: Optional[dns.message.QueryMessage] = None + self.backoff = 0.0 + + def next_request( + self, + ) -> Tuple[Optional[dns.message.QueryMessage], Optional[Answer]]: + """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: Optional[dns.message.Message], ex: Optional[Exception] + ) -> Tuple[Optional[Answer], 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: Optional[Any] + keyname: Optional[Union[dns.name.Name, str]] + keyalgorithm: Union[dns.name.Name, str] + edns: int + ednsflags: int + ednsoptions: Optional[List[dns.edns.Option]] + payload: int + cache: Any + flags: Optional[int] + retry_servfail: bool + rotate: bool + ndots: Optional[int] + _nameservers: Sequence[Union[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": + 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: + 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) + 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: + """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: Optional[float] = None, + errors: Optional[List[ErrorTuple]] = 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: Optional[bool] + ) -> 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: Optional[Union[dns.name.Name, str]] = None, + algorithm: Union[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: Optional[Union[int, bool]] = 0, + ednsflags: int = 0, + payload: int = dns.message.DEFAULT_EDNS_PAYLOAD, + options: Optional[List[dns.edns.Option]] = 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[Union[str, dns.nameserver.Nameserver]], + nameserver_ports: Dict[str, int], + default_port: int, + ) -> List[dns.nameserver.Nameserver]: + enriched_nameservers = [] + if isinstance(nameservers, list): + 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( + "nameservers must be a list or tuple (not a {})".format( + type(nameservers) + ) + ) + return enriched_nameservers + + @property + def nameservers( + self, + ) -> Sequence[Union[str, dns.nameserver.Nameserver]]: + return self._nameservers + + @nameservers.setter + def nameservers( + self, nameservers: Sequence[Union[str, dns.nameserver.Nameserver]] + ) -> None: + """ + *nameservers*, a ``list`` 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: Union[dns.name.Name, str], + rdtype: Union[dns.rdatatype.RdataType, str] = dns.rdatatype.A, + rdclass: Union[dns.rdataclass.RdataClass, str] = dns.rdataclass.IN, + tcp: bool = False, + source: Optional[str] = None, + raise_on_no_answer: bool = True, + source_port: int = 0, + lifetime: Optional[float] = None, + search: Optional[bool] = 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: Union[dns.name.Name, str], + rdtype: Union[dns.rdatatype.RdataType, str] = dns.rdatatype.A, + rdclass: Union[dns.rdataclass.RdataClass, str] = dns.rdataclass.IN, + tcp: bool = False, + source: Optional[str] = None, + raise_on_no_answer: bool = True, + source_port: int = 0, + lifetime: Optional[float] = 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: Union[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: + 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: Union[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 dns.resolver.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: + pass + + +#: The default resolver. +default_resolver: Optional[Resolver] = 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: Union[dns.name.Name, str], + rdtype: Union[dns.rdatatype.RdataType, str] = dns.rdatatype.A, + rdclass: Union[dns.rdataclass.RdataClass, str] = dns.rdataclass.IN, + tcp: bool = False, + source: Optional[str] = None, + raise_on_no_answer: bool = True, + source_port: int = 0, + lifetime: Optional[float] = None, + search: Optional[bool] = 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: Union[dns.name.Name, str], + rdtype: Union[dns.rdatatype.RdataType, str] = dns.rdatatype.A, + rdclass: Union[dns.rdataclass.RdataClass, str] = dns.rdataclass.IN, + tcp: bool = False, + source: Optional[str] = None, + raise_on_no_answer: bool = True, + source_port: int = 0, + lifetime: Optional[float] = 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: Union[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: Union[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: + """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: Union[dns.name.Name, str], + rdclass: dns.rdataclass.RdataClass = dns.rdataclass.IN, + tcp: bool = False, + resolver: Optional[Resolver] = None, + lifetime: Optional[float] = None, +) -> dns.name.Name: + """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: Optional[float] + if lifetime is not None: + expiration = start + lifetime + else: + expiration = None + while 1: + try: + rlifetime: Optional[float] + 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 (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer) as e: + if isinstance(e, dns.resolver.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: Union[dns.name.Name, str], + port: int = 53, + family: int = socket.AF_UNSPEC, + resolver: Optional[Resolver] = 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[Union[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 = dns.resolver.Resolver(configure=False) + res.nameservers = nameservers + return res + + +def resolve_at( + where: Union[dns.name.Name, str], + qname: Union[dns.name.Name, str], + rdtype: Union[dns.rdatatype.RdataType, str] = dns.rdatatype.A, + rdclass: Union[dns.rdataclass.RdataClass, str] = dns.rdataclass.IN, + tcp: bool = False, + source: Optional[str] = None, + raise_on_no_answer: bool = True, + source_port: int = 0, + lifetime: Optional[float] = None, + search: Optional[bool] = None, + port: int = 53, + family: int = socket.AF_UNSPEC, + resolver: Optional[Resolver] = 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 = { + socket.SOCK_DGRAM: [socket.SOL_UDP], + socket.SOCK_STREAM: [socket.SOL_TCP], +} + +_resolver = 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: + answers = _resolver.resolve_name(host, family) + addrs = answers.addresses_and_families() + canonical_name = answers.canonical_name().to_text(True) + except dns.resolver.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) + 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 proto in _protocols_for_socktype[socktype]: + 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 socket.error("sockaddr resolved to multiple addresses") + addr = tuples[0][4][0] + if flags & socket.NI_DGRAM: + pname = "udp" + else: + pname = "tcp" + qname = dns.reversename.from_address(addr) + if flags & socket.NI_NUMERICHOST == 0: + try: + answer = _resolver.resolve(qname, "PTR") + hostname = answer.rrset[0].target.to_text(True) + except (dns.resolver.NXDOMAIN, dns.resolver.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: + 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] + 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: Optional[Resolver] = 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/venv/Lib/site-packages/dns/reversename.py b/venv/Lib/site-packages/dns/reversename.py new file mode 100644 index 00000000..8236c711 --- /dev/null +++ b/venv/Lib/site-packages/dns/reversename.py @@ -0,0 +1,105 @@ +# 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.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 = ["%d" % 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 = ["%d" % 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/venv/Lib/site-packages/dns/rrset.py b/venv/Lib/site-packages/dns/rrset.py new file mode 100644 index 00000000..6f39b108 --- /dev/null +++ b/venv/Lib/site-packages/dns/rrset.py @@ -0,0 +1,285 @@ +# 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, Optional, Union, cast + +import dns.name +import dns.rdataclass +import dns.rdataset +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: Optional[dns.rdataclass.RdataClass] = None, + ): + """Create a new RRset.""" + + super().__init__(rdclass, rdtype, covers) + self.name = name + self.deleting = deleting + + def _clone(self): + obj = 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: Optional[dns.rdataclass.RdataClass] = 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: Optional[dns.name.Name] = 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: Optional[dns.name.CompressType] = None, # type: ignore + origin: Optional[dns.name.Name] = 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: Union[dns.name.Name, str], + ttl: int, + rdclass: Union[dns.rdataclass.RdataClass, str], + rdtype: Union[dns.rdatatype.RdataType, str], + text_rdatas: Collection[str], + idna_codec: Optional[dns.name.IDNACodec] = None, + origin: Optional[dns.name.Name] = None, + relativize: bool = True, + relativize_to: Optional[dns.name.Name] = 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: Union[dns.name.Name, str], + ttl: int, + rdclass: Union[dns.rdataclass.RdataClass, str], + rdtype: Union[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: Union[dns.name.Name, str], + ttl: int, + rdatas: Collection[dns.rdata.Rdata], + idna_codec: Optional[dns.name.IDNACodec] = 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: Union[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/venv/Lib/site-packages/dns/serial.py b/venv/Lib/site-packages/dns/serial.py new file mode 100644 index 00000000..3417299b --- /dev/null +++ b/venv/Lib/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/venv/Lib/site-packages/dns/set.py b/venv/Lib/site-packages/dns/set.py new file mode 100644 index 00000000..f0fb0d50 --- /dev/null +++ b/venv/Lib/site-packages/dns/set.py @@ -0,0 +1,307 @@ +# 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 sets being missing in + ancient versions of python, but dnspython will continue to use it + as these sets are based on lists and are thus indexable, and this + ability is widely used in dnspython applications. + """ + + __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 "dns.set.Set(%s)" % repr(list(self.items.keys())) + + 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/venv/Lib/site-packages/dns/tokenizer.py b/venv/Lib/site-packages/dns/tokenizer.py new file mode 100644 index 00000000..454cac4a --- /dev/null +++ b/venv/Lib/site-packages/dns/tokenizer.py @@ -0,0 +1,708 @@ +# 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, Optional, 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: Optional[str] = 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 '%d "%s"' % (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: Optional[str] = None, + idna_codec: Optional[dns.name.IDNACodec] = 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: Optional[str] = None + self.ungotten_token: Optional[Token] = 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( + "%d is not an unsigned 8-bit integer" % value + ) + 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( + "%o is not an octal unsigned 16-bit integer" % value + ) + else: + raise dns.exception.SyntaxError( + "%d is not an unsigned 16-bit integer" % value + ) + 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( + "%d is not an unsigned 32-bit integer" % value + ) + 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( + "%d is not an unsigned 48-bit integer" % value + ) + return value + + def get_string(self, max_length: Optional[int] = 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: Optional[int] = 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: Optional[dns.name.Name] = None, + relativize: bool = False, + relativize_to: Optional[dns.name.Name] = 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: Optional[dns.name.Name] = None, + relativize: bool = False, + relativize_to: Optional[dns.name.Name] = 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( + 'expected EOL or EOF, got %d "%s"' % (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/venv/Lib/site-packages/dns/transaction.py b/venv/Lib/site-packages/dns/transaction.py new file mode 100644 index 00000000..84e54f7d --- /dev/null +++ b/venv/Lib/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, Optional, Tuple, Union + +import dns.exception +import dns.name +import dns.node +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[Optional[dns.name.Name], bool, Optional[dns.name.Name]]: + """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) -> Optional[dns.name.Name]: + """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: Optional[Union[dns.name.Name, str]], + rdtype: Union[dns.rdatatype.RdataType, str], + covers: Union[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) -> Optional[dns.node.Node]: + """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: Union[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): + try: + args = collections.deque(args) + if replace: + method = "replace()" + else: + method = "add()" + 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" + ) + 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): + try: + args = collections.deque(args) + if exact: + method = "delete_exact()" + else: + method = "delete()" + 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._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() + if self._ended: + raise AlreadyEnded + 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/venv/Lib/site-packages/dns/tsig.py b/venv/Lib/site-packages/dns/tsig.py new file mode 100644 index 00000000..780852e8 --- /dev/null +++ b/venv/Lib/site-packages/dns/tsig.py @@ -0,0 +1,352 @@ +# 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 + + +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) + 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("unknown TSIG error code %d" % 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, secret, algorithm=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, dns.tsig.Key]: + """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 = {} + 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/venv/Lib/site-packages/dns/ttl.py b/venv/Lib/site-packages/dns/ttl.py new file mode 100644 index 00000000..264b0338 --- /dev/null +++ b/venv/Lib/site-packages/dns/ttl.py @@ -0,0 +1,92 @@ +# 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.""" + +from typing import Union + +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("unknown unit '%s'" % 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: Union[int, str]) -> int: + if isinstance(value, int): + return value + elif isinstance(value, str): + return dns.ttl.from_text(value) + else: + raise ValueError("cannot convert value to TTL") diff --git a/venv/Lib/site-packages/dns/update.py b/venv/Lib/site-packages/dns/update.py new file mode 100644 index 00000000..bf1157ac --- /dev/null +++ b/venv/Lib/site-packages/dns/update.py @@ -0,0 +1,386 @@ +# 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, Optional, Union + +import dns.message +import dns.name +import dns.opcode +import dns.rdata +import dns.rdataclass +import dns.rdataset +import dns.rdatatype +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: Optional[Union[dns.name.Name, str]] = None, + rdclass: dns.rdataclass.RdataClass = dns.rdataclass.IN, + keyring: Optional[Any] = None, + keyname: Optional[dns.name.Name] = None, + keyalgorithm: Union[dns.name.Name, str] = dns.tsig.default_algorithm, + id: Optional[int] = 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: Union[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: Union[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: Union[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: Union[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: Union[dns.name.Name, str], + rdtype: Optional[Union[dns.rdatatype.RdataType, str]] = 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): + 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/venv/Lib/site-packages/dns/version.py b/venv/Lib/site-packages/dns/version.py new file mode 100644 index 00000000..251f2583 --- /dev/null +++ b/venv/Lib/site-packages/dns/version.py @@ -0,0 +1,58 @@ +# 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 = 6 +#: MICRO +MICRO = 1 +#: RELEASELEVEL +RELEASELEVEL = 0x0F +#: SERIAL +SERIAL = 0 + +if RELEASELEVEL == 0x0F: # pragma: no cover lgtm[py/unreachable-statement] + #: version + version = "%d.%d.%d" % (MAJOR, MINOR, MICRO) # lgtm[py/unreachable-statement] +elif RELEASELEVEL == 0x00: # pragma: no cover lgtm[py/unreachable-statement] + version = "%d.%d.%ddev%d" % ( + MAJOR, + MINOR, + MICRO, + SERIAL, + ) # lgtm[py/unreachable-statement] +elif RELEASELEVEL == 0x0C: # pragma: no cover lgtm[py/unreachable-statement] + version = "%d.%d.%drc%d" % ( + MAJOR, + MINOR, + MICRO, + SERIAL, + ) # lgtm[py/unreachable-statement] +else: # pragma: no cover lgtm[py/unreachable-statement] + version = "%d.%d.%d%x%d" % ( + MAJOR, + MINOR, + MICRO, + RELEASELEVEL, + SERIAL, + ) # lgtm[py/unreachable-statement] + +#: hexversion +hexversion = MAJOR << 24 | MINOR << 16 | MICRO << 8 | RELEASELEVEL << 4 | SERIAL diff --git a/venv/Lib/site-packages/dns/versioned.py b/venv/Lib/site-packages/dns/versioned.py new file mode 100644 index 00000000..fd78e674 --- /dev/null +++ b/venv/Lib/site-packages/dns/versioned.py @@ -0,0 +1,318 @@ +# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license + +"""DNS Versioned Zones.""" + +import collections +import threading +from typing import Callable, Deque, Optional, Set, Union + +import dns.exception +import dns.immutable +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 = Node + + def __init__( + self, + origin: Optional[Union[dns.name.Name, str]], + rdclass: dns.rdataclass.RdataClass = dns.rdataclass.IN, + relativize: bool = True, + pruning_policy: Optional[Callable[["Zone", Version], Optional[bool]]] = 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: Optional[Transaction] = None + self._write_event: Optional[threading.Event] = 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: Optional[int] = None, serial: Optional[int] = 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 and rds[0].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) + 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: Optional[int]) -> 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: + + def policy(zone, _): # pylint: disable=unused-argument + return False + + else: + + def policy(zone, _): + return len(zone._versions) > max_versions + + self.set_pruning_policy(policy) + + def set_pruning_policy( + self, policy: Optional[Callable[["Zone", Version], Optional[bool]]] + ) -> 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: Union[dns.name.Name, str], create: bool = False + ) -> dns.node.Node: + if create: + raise UseTransaction + return super().find_node(name) + + def delete_node(self, name: Union[dns.name.Name, str]) -> None: + raise UseTransaction + + def find_rdataset( + self, + name: Union[dns.name.Name, str], + rdtype: Union[dns.rdatatype.RdataType, str], + covers: Union[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: Union[dns.name.Name, str], + rdtype: Union[dns.rdatatype.RdataType, str], + covers: Union[dns.rdatatype.RdataType, str] = dns.rdatatype.NONE, + create: bool = False, + ) -> Optional[dns.rdataset.Rdataset]: + 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: Union[dns.name.Name, str], + rdtype: Union[dns.rdatatype.RdataType, str], + covers: Union[dns.rdatatype.RdataType, str] = dns.rdatatype.NONE, + ) -> None: + raise UseTransaction + + def replace_rdataset( + self, name: Union[dns.name.Name, str], replacement: dns.rdataset.Rdataset + ) -> None: + raise UseTransaction diff --git a/venv/Lib/site-packages/dns/win32util.py b/venv/Lib/site-packages/dns/win32util.py new file mode 100644 index 00000000..aaa7e93e --- /dev/null +++ b/venv/Lib/site-packages/dns/win32util.py @@ -0,0 +1,252 @@ +import sys + +import dns._features + +if sys.platform == "win32": + from typing import Any + + import dns.name + + _prefer_wmi = True + + import winreg # pylint: disable=import-error + + # Keep pylint quiet on non-windows. + try: + WindowsError is None # pylint: disable=used-before-assignment + except KeyError: + WindowsError = Exception + + if dns._features.have("wmi"): + import threading + + import pythoncom # pylint: disable=import-error + import wmi # pylint: disable=import-error + + _have_wmi = True + else: + _have_wmi = False + + 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 DnsInfo: + def __init__(self): + self.domain = None + self.nameservers = [] + self.search = [] + + if _have_wmi: + + class _WMIGetter(threading.Thread): + 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 + + class _RegistryGetter: + def __init__(self): + self.info = DnsInfo() + + def _determine_split_char(self, entry): + # + # The windows registry irritatingly changes the list element + # delimiter in between ' ' and ',' (and vice-versa) in various + # versions of windows. + # + if entry.find(" ") >= 0: + split_char = " " + elif entry.find(",") >= 0: + split_char = "," + else: + # probably a singleton; treat as a space-separated list. + split_char = " " + return split_char + + def _config_nameservers(self, nameservers): + split_char = self._determine_split_char(nameservers) + ns_list = nameservers.split(split_char) + for ns in ns_list: + if ns not in self.info.nameservers: + self.info.nameservers.append(ns) + + def _config_search(self, search): + split_char = self._determine_split_char(search) + search_list = search.split(split_char) + for s in search_list: + 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}" + r"\%s\Connection" % guid, + ) + + 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, r"SYSTEM\CurrentControlSet\Enum\%s" % 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 EnvironmentError: + break + finally: + interfaces.Close() + finally: + lm.Close() + return self.info + + _getter_class: Any + if _have_wmi and _prefer_wmi: + _getter_class = _WMIGetter + else: + _getter_class = _RegistryGetter + + def get_dns_info(): + """Extract resolver configuration.""" + getter = _getter_class() + return getter.get() diff --git a/venv/Lib/site-packages/dns/wire.py b/venv/Lib/site-packages/dns/wire.py new file mode 100644 index 00000000..9f9b1573 --- /dev/null +++ b/venv/Lib/site-packages/dns/wire.py @@ -0,0 +1,89 @@ +# 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: + def __init__(self, wire: bytes, current: int = 0): + 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/venv/Lib/site-packages/dns/xfr.py b/venv/Lib/site-packages/dns/xfr.py new file mode 100644 index 00000000..dd247d33 --- /dev/null +++ b/venv/Lib/site-packages/dns/xfr.py @@ -0,0 +1,343 @@ +# 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, Optional, Tuple, Union + +import dns.exception +import dns.message +import dns.name +import dns.rcode +import dns.rdataset +import dns.rdatatype +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 = "Zone transfer error: %s" % 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: Optional[int] = 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: Optional[dns.transaction.Transaction] = None + self.rdtype = rdtype + if rdtype == dns.rdatatype.IXFR: + if serial is None: + raise ValueError("a starting serial must be supplied for IXFRs") + elif is_udp: + raise ValueError("is_udp specified for AXFR") + self.serial = serial + self.is_udp = is_udp + (_, _, self.origin) = txn_manager.origin_information() + self.soa_rdataset: Optional[dns.rdataset.Rdataset] = 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: + replacement = self.rdtype == dns.rdatatype.AXFR + self.txn = self.txn_manager.writer(replacement) + 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() + if self.rdtype == dns.rdatatype.IXFR: + if self.soa_rdataset[0].serial == self.serial: + # + # We're already up-to-date. + # + self.done = True + elif dns.serial.Serial(self.soa_rdataset[0].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.rdtype == dns.rdatatype.IXFR: + 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 ( + self.rdtype == dns.rdatatype.AXFR + or (self.rdtype == dns.rdatatype.IXFR and self.delete_mode) + ): + # + # This is the final SOA + # + if self.expecting_SOA: + # We got an empty IXFR sequence! + raise dns.exception.FormError("empty IXFR sequence") + if ( + self.rdtype == dns.rdatatype.IXFR + and self.serial != rdataset[0].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 + if self.rdtype == dns.rdatatype.IXFR: + if self.delete_mode: + # This is the start of an IXFR deletion set + if rdataset[0].serial != self.serial: + raise dns.exception.FormError( + "IXFR base serial mismatch" + ) + else: + # This is the start of an IXFR addition set + self.serial = rdataset[0].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.rdtype = dns.rdatatype.AXFR + 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: Optional[int] = 0, + use_edns: Optional[Union[int, bool]] = None, + ednsflags: Optional[int] = None, + payload: Optional[int] = None, + request_payload: Optional[int] = None, + options: Optional[List[dns.edns.Option]] = None, + keyring: Any = None, + keyname: Optional[dns.name.Name] = None, + keyalgorithm: Union[dns.name.Name, str] = dns.tsig.default_algorithm, +) -> Tuple[dns.message.QueryMessage, Optional[int]]: + """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: + serial = rdataset[0].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) -> Optional[int]: + """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 = query.find_rrset( + query.authority, question.name, question.rdclass, dns.rdatatype.SOA + ) + return soa[0].serial diff --git a/venv/Lib/site-packages/dns/zone.py b/venv/Lib/site-packages/dns/zone.py new file mode 100644 index 00000000..844919e4 --- /dev/null +++ b/venv/Lib/site-packages/dns/zone.py @@ -0,0 +1,1434 @@ +# 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, + Optional, + Set, + Tuple, + Union, +) + +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: Optional[dns.name.Name], + 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 + writable_version_factory: Optional[Callable[[], "WritableVersion"]] = None + immutable_version_factory: Optional[Callable[[], "ImmutableVersion"]] = None + + __slots__ = ["rdclass", "origin", "nodes", "relativize"] + + def __init__( + self, + origin: Optional[Union[dns.name.Name, str]], + 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: Union[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: Union[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: Union[dns.name.Name, str], create: bool = False + ) -> Optional[dns.node.Node]: + """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: Union[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: Union[dns.name.Name, str], + rdtype: Union[dns.rdatatype.RdataType, str], + covers: Union[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: Union[dns.name.Name, str], + rdtype: Union[dns.rdatatype.RdataType, str], + covers: Union[dns.rdatatype.RdataType, str] = dns.rdatatype.NONE, + create: bool = False, + ) -> Optional[dns.rdataset.Rdataset]: + """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: Union[dns.name.Name, str], + rdtype: Union[dns.rdatatype.RdataType, str], + covers: Union[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: Union[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: Union[dns.name.Name, str], + rdtype: Union[dns.rdatatype.RdataType, str], + covers: Union[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: Union[dns.name.Name, str], + rdtype: Union[dns.rdatatype.RdataType, str], + covers: Union[dns.rdatatype.RdataType, str] = dns.rdatatype.NONE, + ) -> Optional[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 ``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: Union[dns.rdatatype.RdataType, str] = dns.rdatatype.ANY, + covers: Union[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: Union[dns.rdatatype.RdataType, str] = dns.rdatatype.ANY, + covers: Union[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: Optional[str] = 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, + relativize=relativize, + want_comments=want_comments, + ) + 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: Optional[str] = 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: Optional[dns.transaction.Transaction] = 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: Optional[dns.rdataset.Rdataset] + if txn: + soa = txn.get(origin_name, dns.rdatatype.SOA) + else: + soa = self.get_rdataset(origin_name, dns.rdatatype.SOA) + if soa is None: + raise NoSOA + return soa[0] + + 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: Optional[dns.rdtypes.ANY.ZONEMD.ZONEMD] = None + ) -> None: + digests: Union[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[Optional[dns.name.Name], bool, Optional[dns.name.Name]]: + effective: Optional[dns.name.Name] + 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, _, version, origin): + self.nodes = version.nodes + if self.origin is None: + self.origin = origin + + def _get_next_version_id(self): + # 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, + ) -> Optional[dns.rdataset.Rdataset]: + 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: Optional[MutableMapping[dns.name.Name, dns.node.Node]] = None, + origin: Optional[dns.name.Name] = 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) -> Optional[dns.node.Node]: + 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, + ) -> Optional[dns.rdataset.Rdataset]: + 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(self, name: dns.name.Name) -> dns.node.Node: + 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 + else: + return node + + 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: 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 + if factory is None: + factory = WritableVersion + self.version = factory(self.zone, self.replacement) + + def _get_rdataset(self, name, rdtype, covers): + return self.version.get_rdataset(name, rdtype, covers) + + def _put_rdataset(self, name, rdataset): + assert not self.read_only + self.version.put_rdataset(name, rdataset) + + def _delete_name(self, name): + assert not self.read_only + self.version.delete_node(name) + + def _delete_rdataset(self, name, rdtype, covers): + assert not self.read_only + self.version.delete_rdataset(name, rdtype, covers) + + def _name_exists(self, name): + return self.version.get_node(name) is not None + + def _changed(self): + if self.read_only: + return False + else: + return len(self.version.changed) > 0 + + def _end_transaction(self, commit): + if self.read_only: + self.zone._end_read(self) + elif commit and len(self.version.changed) > 0: + if self.make_immutable: + factory = self.manager.immutable_version_factory + if factory is None: + factory = ImmutableVersion + version = factory(self.version) + else: + version = self.version + self.zone._commit_version(self, version, self.version.origin) + else: + # rollback + self.zone._end_write(self) + + def _set_origin(self, origin): + if self.version.origin is None: + self.version.origin = origin + + def _iterate_rdatasets(self): + for name, node in self.version.items(): + for rdataset in node: + yield (name, rdataset) + + def _iterate_names(self): + return self.version.keys() + + def _get_node(self, name): + return self.version.get_node(name) + + def _origin_information(self): + (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: Optional[Union[dns.name.Name, str]] = None, + rdclass: dns.rdataclass.RdataClass = dns.rdataclass.IN, + relativize: bool = True, + zone_factory: Any = Zone, + filename: Optional[str] = None, + allow_include: bool = False, + check_origin: bool = True, + idna_codec: Optional[dns.name.IDNACodec] = None, + allow_directives: Union[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 dns.zone.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: Optional[Union[dns.name.Name, str]] = None, + rdclass: dns.rdataclass.RdataClass = dns.rdataclass.IN, + relativize: bool = True, + zone_factory: Any = Zone, + filename: Optional[str] = None, + allow_include: bool = False, + check_origin: bool = True, + idna_codec: Optional[dns.name.IDNACodec] = None, + allow_directives: Union[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: Optional[Union[dns.name.Name, str]] = None, + rdclass: dns.rdataclass.RdataClass = dns.rdataclass.IN, + relativize: bool = True, + zone_factory: Any = Zone, + filename: Optional[str] = None, + allow_include: bool = True, + check_origin: bool = True, + idna_codec: Optional[dns.name.IDNACodec] = None, + allow_directives: Union[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) + 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/venv/Lib/site-packages/dns/zonefile.py b/venv/Lib/site-packages/dns/zonefile.py new file mode 100644 index 00000000..af064e73 --- /dev/null +++ b/venv/Lib/site-packages/dns/zonefile.py @@ -0,0 +1,746 @@ +# 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, Optional, Set, Tuple, Union + +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, + Optional[dns.name.Name], # current_origin + Optional[dns.name.Name], # last_name + Optional[Any], # 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: Union[bool, Iterable[str]] = True, + force_name: Optional[dns.name.Name] = None, + force_ttl: Optional[int] = None, + force_rdclass: Optional[dns.rdataclass.RdataClass] = None, + force_rdtype: Optional[dns.rdatatype.RdataType] = None, + default_ttl: Optional[int] = 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: Optional[Any] = 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 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("unknown rdatatype '%s'" % 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( + "caught exception {}: {}".format(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. + self.default_ttl = rd.minimum + self.default_ttl_known = True + if ttl is None: + # if we didn't have a TTL on the SOA, set it! + ttl = 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 + g1 = is_generate1.match(side) + if g1: + mod, sign, offset, width, base = g1.groups() + if sign == "": + sign = "+" + g2 = is_generate2.match(side) + if g2: + mod, sign, offset = g2.groups() + if sign == "": + sign = "+" + width = 0 + base = "d" + g3 = is_generate3.match(side) + if g3: + mod, sign, offset, width = g3.groups() + if sign == "": + sign = "+" + base = "d" + + if not (g1 or g2 or g3): + mod = "" + sign = "+" + offset = 0 + width = 0 + base = "d" + + offset = int(offset) + width = int(width) + + if sign not in ["+", "-"]: + raise dns.exception.SyntaxError("invalid offset sign %s" % sign) + if base not in ["d", "o", "x", "X", "n", "N"]: + raise dns.exception.SyntaxError("invalid type %s" % base) + + return mod, sign, offset, width, 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 + # 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("unknown rdatatype '%s'" % 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("$%s" % (lmod), lzfindex) + rdata = rhs.replace("$%s" % (rmod), rzfindex) + + self.last_name = dns.name.from_text( + name, self.current_origin, self.tok.idna_codec + ) + name = self.last_name + 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( + "caught exception %s: %s" % (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: Optional[dns.name.Name] + 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, "r") + 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( + "%s:%d: %s" % (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) + + 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.root, relativize=False, rdclass=dns.rdataclass.IN + ): + self.origin = origin + self.relativize = relativize + self.rdclass = rdclass + self.rrsets = [] + + 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): + self.rrsets = rrsets + + +def read_rrsets( + text: Any, + name: Optional[Union[dns.name.Name, str]] = None, + ttl: Optional[int] = None, + rdclass: Optional[Union[dns.rdataclass.RdataClass, str]] = dns.rdataclass.IN, + default_rdclass: Union[dns.rdataclass.RdataClass, str] = dns.rdataclass.IN, + rdtype: Optional[Union[dns.rdatatype.RdataType, str]] = None, + default_ttl: Optional[Union[int, str]] = None, + idna_codec: Optional[dns.name.IDNACodec] = None, + origin: Optional[Union[dns.name.Name, str]] = 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/venv/Lib/site-packages/dns/zonetypes.py b/venv/Lib/site-packages/dns/zonetypes.py new file mode 100644 index 00000000..195ee2ec --- /dev/null +++ b/venv/Lib/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/venv/Lib/site-packages/dnspython-2.6.1.dist-info/INSTALLER b/venv/Lib/site-packages/dnspython-2.6.1.dist-info/INSTALLER new file mode 100644 index 00000000..a1b589e3 --- /dev/null +++ b/venv/Lib/site-packages/dnspython-2.6.1.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/venv/Lib/site-packages/dnspython-2.6.1.dist-info/METADATA b/venv/Lib/site-packages/dnspython-2.6.1.dist-info/METADATA new file mode 100644 index 00000000..129184e3 --- /dev/null +++ b/venv/Lib/site-packages/dnspython-2.6.1.dist-info/METADATA @@ -0,0 +1,147 @@ +Metadata-Version: 2.1 +Name: dnspython +Version: 2.6.1 +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-Expression: 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.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 :: Name Service (DNS) +Classifier: Topic :: Software Development :: Libraries :: Python Modules +Requires-Python: >=3.8 +Provides-Extra: dev +Requires-Dist: black>=23.1.0; extra == 'dev' +Requires-Dist: coverage>=7.0; extra == 'dev' +Requires-Dist: flake8>=7; extra == 'dev' +Requires-Dist: mypy>=1.8; extra == 'dev' +Requires-Dist: pylint>=3; extra == 'dev' +Requires-Dist: pytest-cov>=4.1.0; extra == 'dev' +Requires-Dist: pytest>=7.4; extra == 'dev' +Requires-Dist: sphinx>=7.2.0; extra == 'dev' +Requires-Dist: twine>=4.0.0; extra == 'dev' +Requires-Dist: wheel>=0.42.0; extra == 'dev' +Provides-Extra: dnssec +Requires-Dist: cryptography>=41; extra == 'dnssec' +Provides-Extra: doh +Requires-Dist: h2>=4.1.0; extra == 'doh' +Requires-Dist: httpcore>=1.0.0; extra == 'doh' +Requires-Dist: httpx>=0.26.0; extra == 'doh' +Provides-Extra: doq +Requires-Dist: aioquic>=0.9.25; extra == 'doq' +Provides-Extra: idna +Requires-Dist: idna>=3.6; extra == 'idna' +Provides-Extra: trio +Requires-Dist: trio>=0.23; extra == 'trio' +Provides-Extra: wmi +Requires-Dist: wmi>=1.5.1; extra == 'wmi' +Description-Content-Type: text/markdown + +# dnspython + +[![Build Status](https://github.com/rthalley/dnspython/actions/workflows/python-package.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) +[![Coverage](https://codecov.io/github/rthalley/dnspython/coverage.svg?branch=master)](https://codecov.io/github/rthalley/dnspython) +[![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 dnspython 2.6.1. +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 master 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 2.0.0 through +2.2.x support Python 3.6 and later. For dnspython 2.3.x, the minimum +supported Python version is 3.7, and for 2.4.x the minimum supported verison is 3.8. +We plan to align future support with the lifetime of the Python 3 versions. + +Documentation has moved to +[dnspython.readthedocs.io](https://dnspython.readthedocs.io). diff --git a/venv/Lib/site-packages/dnspython-2.6.1.dist-info/RECORD b/venv/Lib/site-packages/dnspython-2.6.1.dist-info/RECORD new file mode 100644 index 00000000..c6529618 --- /dev/null +++ b/venv/Lib/site-packages/dnspython-2.6.1.dist-info/RECORD @@ -0,0 +1,290 @@ +dns/__init__.py,sha256=YJZtDG14Idw5ui3h1nWooSwPM9gsxQgB8M0GBZ3aly0,1663 +dns/__pycache__/__init__.cpython-312.pyc,, +dns/__pycache__/_asyncbackend.cpython-312.pyc,, +dns/__pycache__/_asyncio_backend.cpython-312.pyc,, +dns/__pycache__/_ddr.cpython-312.pyc,, +dns/__pycache__/_features.cpython-312.pyc,, +dns/__pycache__/_immutable_ctx.cpython-312.pyc,, +dns/__pycache__/_trio_backend.cpython-312.pyc,, +dns/__pycache__/asyncbackend.cpython-312.pyc,, +dns/__pycache__/asyncquery.cpython-312.pyc,, +dns/__pycache__/asyncresolver.cpython-312.pyc,, +dns/__pycache__/dnssec.cpython-312.pyc,, +dns/__pycache__/dnssectypes.cpython-312.pyc,, +dns/__pycache__/e164.cpython-312.pyc,, +dns/__pycache__/edns.cpython-312.pyc,, +dns/__pycache__/entropy.cpython-312.pyc,, +dns/__pycache__/enum.cpython-312.pyc,, +dns/__pycache__/exception.cpython-312.pyc,, +dns/__pycache__/flags.cpython-312.pyc,, +dns/__pycache__/grange.cpython-312.pyc,, +dns/__pycache__/immutable.cpython-312.pyc,, +dns/__pycache__/inet.cpython-312.pyc,, +dns/__pycache__/ipv4.cpython-312.pyc,, +dns/__pycache__/ipv6.cpython-312.pyc,, +dns/__pycache__/message.cpython-312.pyc,, +dns/__pycache__/name.cpython-312.pyc,, +dns/__pycache__/namedict.cpython-312.pyc,, +dns/__pycache__/nameserver.cpython-312.pyc,, +dns/__pycache__/node.cpython-312.pyc,, +dns/__pycache__/opcode.cpython-312.pyc,, +dns/__pycache__/query.cpython-312.pyc,, +dns/__pycache__/rcode.cpython-312.pyc,, +dns/__pycache__/rdata.cpython-312.pyc,, +dns/__pycache__/rdataclass.cpython-312.pyc,, +dns/__pycache__/rdataset.cpython-312.pyc,, +dns/__pycache__/rdatatype.cpython-312.pyc,, +dns/__pycache__/renderer.cpython-312.pyc,, +dns/__pycache__/resolver.cpython-312.pyc,, +dns/__pycache__/reversename.cpython-312.pyc,, +dns/__pycache__/rrset.cpython-312.pyc,, +dns/__pycache__/serial.cpython-312.pyc,, +dns/__pycache__/set.cpython-312.pyc,, +dns/__pycache__/tokenizer.cpython-312.pyc,, +dns/__pycache__/transaction.cpython-312.pyc,, +dns/__pycache__/tsig.cpython-312.pyc,, +dns/__pycache__/tsigkeyring.cpython-312.pyc,, +dns/__pycache__/ttl.cpython-312.pyc,, +dns/__pycache__/update.cpython-312.pyc,, +dns/__pycache__/version.cpython-312.pyc,, +dns/__pycache__/versioned.cpython-312.pyc,, +dns/__pycache__/win32util.cpython-312.pyc,, +dns/__pycache__/wire.cpython-312.pyc,, +dns/__pycache__/xfr.cpython-312.pyc,, +dns/__pycache__/zone.cpython-312.pyc,, +dns/__pycache__/zonefile.cpython-312.pyc,, +dns/__pycache__/zonetypes.cpython-312.pyc,, +dns/_asyncbackend.py,sha256=Ny0kGesm9wbLBnt-0u-tANOKsxcYt2jbMuRoRz_JZUA,2360 +dns/_asyncio_backend.py,sha256=q58xPdqAOLmOYOux8GFRyiH-fSZ7jiwZF-Jg2vHjYSU,8971 +dns/_ddr.py,sha256=rHXKC8kncCTT9N4KBh1flicl79nyDjQ-DDvq30MJ3B8,5247 +dns/_features.py,sha256=MUeyfM_nMYAYkasGfbY7I_15JmwftaZjseuP1L43MT0,2384 +dns/_immutable_ctx.py,sha256=gtoCLMmdHXI23zt5lRSIS3A4Ca3jZJngebdoFFOtiwU,2459 +dns/_trio_backend.py,sha256=Vab_wR2CxDgy2Jz3iM_64FZmP_kMUN9j8LS4eNl-Oig,8269 +dns/asyncbackend.py,sha256=82fXTFls_m7F_ekQbgUGOkoBbs4BI-GBLDZAWNGUvJ0,2796 +dns/asyncquery.py,sha256=Q7u04mbbqCoe9VxsqRcsWTPxgH2Cx49eWWgi2wUyZHU,26850 +dns/asyncresolver.py,sha256=GD86dCyW9YGKs6SggWXwBKEXifW7Qdx4cEAGFKY6fA4,17852 +dns/dnssec.py,sha256=xyYW1cf6eeFNXROrEs1pyY4TgC8jlmUiiootaPbVjjY,40693 +dns/dnssecalgs/__init__.py,sha256=DcnGIbL6m-USPSiLWHSw511awB7dytlljvCOOmzchS0,4279 +dns/dnssecalgs/__pycache__/__init__.cpython-312.pyc,, +dns/dnssecalgs/__pycache__/base.cpython-312.pyc,, +dns/dnssecalgs/__pycache__/cryptography.cpython-312.pyc,, +dns/dnssecalgs/__pycache__/dsa.cpython-312.pyc,, +dns/dnssecalgs/__pycache__/ecdsa.cpython-312.pyc,, +dns/dnssecalgs/__pycache__/eddsa.cpython-312.pyc,, +dns/dnssecalgs/__pycache__/rsa.cpython-312.pyc,, +dns/dnssecalgs/base.py,sha256=hsFHFr_eCYeDcI0eU6_WiLlOYL0GR4QJ__sXoMrIAfE,2446 +dns/dnssecalgs/cryptography.py,sha256=3uqMfRm-zCkJPOrxUqlu9CmdxIMy71dVor9eAHi0wZM,2425 +dns/dnssecalgs/dsa.py,sha256=hklh_HkT_ZffQBHQ7t6pKUStTH4x5nXlz8R9RUP72aY,3497 +dns/dnssecalgs/ecdsa.py,sha256=GWrJgEXAK08MCdbLk7LQcD2ajKqW_dbONWXh3wieLzw,3016 +dns/dnssecalgs/eddsa.py,sha256=9lQQZ92f2PiIhhylieInO-19aSTDQiyoY8X2kTkGlcs,1914 +dns/dnssecalgs/rsa.py,sha256=jWkhWKByylIo7Y9gAiiO8t8bowF8IZ0siVjgZpdhLSE,3555 +dns/dnssectypes.py,sha256=CyeuGTS_rM3zXr8wD9qMT9jkzvVfTY2JWckUcogG83E,1799 +dns/e164.py,sha256=EsK8cnOtOx7kQ0DmSwibcwkzp6efMWjbRiTyHZO8Q-M,3978 +dns/edns.py,sha256=d8QWhmRd6qlaGfO-tY6iDQZt9XUiyfJfKdjoGjvwOU4,15263 +dns/entropy.py,sha256=qkG8hXDLzrJS6R5My26iA59c0RhPwJNzuOhOCAZU5Bw,4242 +dns/enum.py,sha256=EepaunPKixTSrascy7iAe9UQEXXxP_MB5Gx4jUpHIhg,3691 +dns/exception.py,sha256=FphWy-JLRG06UUUq2VmUGwdPA1xWja_8YfrcffRFlQs,5957 +dns/flags.py,sha256=cQ3kTFyvcKiWHAxI5AwchNqxVOrsIrgJ6brgrH42Wq8,2750 +dns/grange.py,sha256=HA623Mv2mZDmOK_BZNDDakT0L6EHsMQU9lFFkE8dKr0,2148 +dns/immutable.py,sha256=InrtpKvPxl-74oYbzsyneZwAuX78hUqeG22f2aniZbk,2017 +dns/inet.py,sha256=j6jQs3K_ehVhDv-i4jwCKePr5HpEiSzvOXQ4uhgn1sU,5772 +dns/ipv4.py,sha256=qEUXtlqWDH_blicj6VMvyQhfX7-BF0gB_lWJliV-2FI,2552 +dns/ipv6.py,sha256=EyiF5T8t2oww9-W4ZA5Zk2GGnOjTy_uZ50CI7maed_8,6600 +dns/message.py,sha256=DyUtBHArPX-WGj_AtcngyIXZNpLppLZX-6q9TryL_wI,65993 +dns/name.py,sha256=eaR1wVR0rErnD3EPANquCuyqpbxy5VfFVhMenWlBPDE,42672 +dns/namedict.py,sha256=hJRYpKeQv6Bd2LaUOPV0L_a0eXEIuqgggPXaH4c3Tow,4000 +dns/nameserver.py,sha256=VkYRnX5wQ7RihAD6kYqidI_hb9NgKJSAE0GaYulNpHY,9909 +dns/node.py,sha256=NGZa0AUMq-CNledJ6wn1Rx6TFYc703cH2OraLysoNWM,12663 +dns/opcode.py,sha256=I6JyuFUL0msja_BYm6bzXHfbbfqUod_69Ss4xcv8xWQ,2730 +dns/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +dns/query.py,sha256=vB8C5u6HyjPWrEx9kUdTSg3kxrOoWbPGu7brC0eetIM,54832 +dns/quic/__init__.py,sha256=F6BybmRKnMGc4W8nX7K98PeyXiSwy1FHb_bJeA2lQSw,2202 +dns/quic/__pycache__/__init__.cpython-312.pyc,, +dns/quic/__pycache__/_asyncio.cpython-312.pyc,, +dns/quic/__pycache__/_common.cpython-312.pyc,, +dns/quic/__pycache__/_sync.cpython-312.pyc,, +dns/quic/__pycache__/_trio.cpython-312.pyc,, +dns/quic/_asyncio.py,sha256=vv4RR3Ol0Y1ZOj7rPAzXxy1UcWjPvhTGQvVkMidPs-o,8159 +dns/quic/_common.py,sha256=06TfauL2VciPYSfrL4gif1eR1rm-TRkQhS2Puuk5URU,7282 +dns/quic/_sync.py,sha256=kE0PRavzd27GPQ9UgYApXZ6SGSW2LwCt8k6XWUvrbVE,8133 +dns/quic/_trio.py,sha256=9zCCBtDs6GAtY_b8ck-A17QMiLZ0njjhVtfFT5qMP7s,7670 +dns/rcode.py,sha256=N6JjrIQjCdJy0boKIp8Hcky5tm__LSDscpDz3rE_sgU,4156 +dns/rdata.py,sha256=9cXM9Y9MK2hy9w5mYqmP-r7_aKjHosigfNn_SfqfGGw,29456 +dns/rdataclass.py,sha256=TK4W4ywB1L_X7EZqk2Gmwnu7vdQpolQF5DtQWyNk5xo,2984 +dns/rdataset.py,sha256=96gTaEIcYEL348VKtTOMAazXBVNtk7m0Xez0mF1eg4I,16756 +dns/rdatatype.py,sha256=gIdYZ0iHRlgiTEO-ftobUANmaAmjTnNc4JljMaP1OnQ,7339 +dns/rdtypes/ANY/AFSDB.py,sha256=k75wMwreF1DAfDymu4lHh16BUx7ulVP3PLeQBZnkurY,1661 +dns/rdtypes/ANY/AMTRELAY.py,sha256=19jfS61mT1CQT-8vf67ZylhDS9JVRVp4WCbFE-7l0jM,3381 +dns/rdtypes/ANY/AVC.py,sha256=SpsXYzlBirRWN0mGnQe0MdN6H8fvlgXPJX5PjOHnEak,1024 +dns/rdtypes/ANY/CAA.py,sha256=AHh59Is-4WiVWd26yovnPM3hXqKS-yx7IWfXSS0NZhE,2511 +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=2Cu2LQM6-K4darqhHv1EM_blmpYpnrBIIX1GnL_rxKE,3533 +dns/rdtypes/ANY/CNAME.py,sha256=IHGGq2BDpeKUahTr1pvyBQgm0NGBI_vQ3Vs5mKTXO4w,1206 +dns/rdtypes/ANY/CSYNC.py,sha256=KkZ_rG6PfeL14il97nmJGWWmUGGS5o9nd2EqbJqOuYo,2439 +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/EUI48.py,sha256=x0BkK0sY_tgzuCwfDYpw6tyuChHjjtbRpAgYhO0Y44o,1151 +dns/rdtypes/ANY/EUI64.py,sha256=1jCff2-SXHJLDnNDnMW8Cd_o-ok0P3x6zKy_bcCU5h4,1161 +dns/rdtypes/ANY/GPOS.py,sha256=pM3i6Tn4qwHWOGOuIuW9FENPlSXT_R4xsNJeGrrABc8,4433 +dns/rdtypes/ANY/HINFO.py,sha256=vYGCHGZmYOhtmxHlvPqrK7m4pBg3MSY5herBsKJTbKQ,2249 +dns/rdtypes/ANY/HIP.py,sha256=Ucrnndu3xDyHFB93AVUA3xW-r61GR50kpRHLyLacvZY,3228 +dns/rdtypes/ANY/ISDN.py,sha256=uymYB-ayZSBob6jQgXe4EefNB8-JMLW6VfxXn7ncwPg,2713 +dns/rdtypes/ANY/L32.py,sha256=TMz2kdGCd0siiQZyiocVDCSnvkOdjhUuYRFyf8o622M,1286 +dns/rdtypes/ANY/L64.py,sha256=sb2BjuPA0PQt67nEyT9rBt759C9e6lH71d3EJHGGnww,1592 +dns/rdtypes/ANY/LOC.py,sha256=hLkzgCxqEhg6fn5Uf-DJigKEIE6oavQ8rLpajp3HDLs,12024 +dns/rdtypes/ANY/LP.py,sha256=wTsKIjtK6vh66qZRLSsiE0k54GO8ieVBGZH8dzVvFnE,1338 +dns/rdtypes/ANY/MX.py,sha256=qQk83idY0-SbRMDmB15JOpJi7cSyiheF-ALUD0Ev19E,995 +dns/rdtypes/ANY/NID.py,sha256=N7Xx4kXf3yVAocTlCXQeJ3BtiQNPFPQVdL1iMuyl5W4,1544 +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=6uRn1SxNuLRNumeoc76BkpECF8ztuqyaYviLjFe7FkQ,2475 +dns/rdtypes/ANY/NSEC3.py,sha256=696h-Zz30bmcT0n1rqoEtS5wqE6jIgsVGzaw5TfdGJo,4331 +dns/rdtypes/ANY/NSEC3PARAM.py,sha256=08p6NWS4DiLav1wOuPbxUxB9MtY2IPjfOMCtJwzzMuA,2635 +dns/rdtypes/ANY/OPENPGPKEY.py,sha256=Va0FGo_8vm1OeX62N5iDTWukAdLwrjTXIZeQ6oanE78,1851 +dns/rdtypes/ANY/OPT.py,sha256=W36RslT_Psp95OPUC70knumOYjKpaRHvGT27I-NV2qc,2561 +dns/rdtypes/ANY/PTR.py,sha256=5HcR1D77Otyk91vVY4tmqrfZfSxSXWyWvwIW-rIH5gc,997 +dns/rdtypes/ANY/RP.py,sha256=5Dgaava9mbLKr87XgbfKZPrunYPBaN8ejNzpmbW6r4s,2184 +dns/rdtypes/ANY/RRSIG.py,sha256=O8vwzS7ldfaj_x8DypvEGFsDSb7al-D7OEnprA3QQoo,4922 +dns/rdtypes/ANY/RT.py,sha256=2t9q3FZQ28iEyceeU25KU2Ur0T5JxELAu8BTwfOUgVw,1013 +dns/rdtypes/ANY/SMIMEA.py,sha256=6yjHuVDfIEodBU9wxbCGCDZ5cWYwyY6FCk-aq2VNU0s,222 +dns/rdtypes/ANY/SOA.py,sha256=Cn8yrag1YvrvwivQgWg-KXmOCaVQVdFHSkFF77w-CE0,3145 +dns/rdtypes/ANY/SPF.py,sha256=rA3Srs9ECQx-37lqm7Zf7aYmMpp_asv4tGS8_fSQ-CU,1022 +dns/rdtypes/ANY/SSHFP.py,sha256=l6TZH2R0kytiZGWez_g-Lq94o5a2xMuwLKwUwsPMx5w,2530 +dns/rdtypes/ANY/TKEY.py,sha256=HjJMIMl4Qb1Nt1JXS6iAymzd2nv_zdLWTt887PJU_5w,4931 +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=dpcS8KwcJ2WJ7BkOp4CZYaUyRuw7U2S9GzvVwKUihQg,2921 +dns/rdtypes/ANY/X25.py,sha256=PxjYTKIuoq44LT2S2JHWOV8BOFD0ASqjq0S5VBeGkFM,1944 +dns/rdtypes/ANY/ZONEMD.py,sha256=JQicv69EvUxh4FCT7eZSLzzU5L5brw_dSM65Um2t5lQ,2393 +dns/rdtypes/ANY/__init__.py,sha256=Pox71HfsEnGGB1PGU44pwrrmjxPLQlA-IbX6nQRoA2M,1497 +dns/rdtypes/ANY/__pycache__/AFSDB.cpython-312.pyc,, +dns/rdtypes/ANY/__pycache__/AMTRELAY.cpython-312.pyc,, +dns/rdtypes/ANY/__pycache__/AVC.cpython-312.pyc,, +dns/rdtypes/ANY/__pycache__/CAA.cpython-312.pyc,, +dns/rdtypes/ANY/__pycache__/CDNSKEY.cpython-312.pyc,, +dns/rdtypes/ANY/__pycache__/CDS.cpython-312.pyc,, +dns/rdtypes/ANY/__pycache__/CERT.cpython-312.pyc,, +dns/rdtypes/ANY/__pycache__/CNAME.cpython-312.pyc,, +dns/rdtypes/ANY/__pycache__/CSYNC.cpython-312.pyc,, +dns/rdtypes/ANY/__pycache__/DLV.cpython-312.pyc,, +dns/rdtypes/ANY/__pycache__/DNAME.cpython-312.pyc,, +dns/rdtypes/ANY/__pycache__/DNSKEY.cpython-312.pyc,, +dns/rdtypes/ANY/__pycache__/DS.cpython-312.pyc,, +dns/rdtypes/ANY/__pycache__/EUI48.cpython-312.pyc,, +dns/rdtypes/ANY/__pycache__/EUI64.cpython-312.pyc,, +dns/rdtypes/ANY/__pycache__/GPOS.cpython-312.pyc,, +dns/rdtypes/ANY/__pycache__/HINFO.cpython-312.pyc,, +dns/rdtypes/ANY/__pycache__/HIP.cpython-312.pyc,, +dns/rdtypes/ANY/__pycache__/ISDN.cpython-312.pyc,, +dns/rdtypes/ANY/__pycache__/L32.cpython-312.pyc,, +dns/rdtypes/ANY/__pycache__/L64.cpython-312.pyc,, +dns/rdtypes/ANY/__pycache__/LOC.cpython-312.pyc,, +dns/rdtypes/ANY/__pycache__/LP.cpython-312.pyc,, +dns/rdtypes/ANY/__pycache__/MX.cpython-312.pyc,, +dns/rdtypes/ANY/__pycache__/NID.cpython-312.pyc,, +dns/rdtypes/ANY/__pycache__/NINFO.cpython-312.pyc,, +dns/rdtypes/ANY/__pycache__/NS.cpython-312.pyc,, +dns/rdtypes/ANY/__pycache__/NSEC.cpython-312.pyc,, +dns/rdtypes/ANY/__pycache__/NSEC3.cpython-312.pyc,, +dns/rdtypes/ANY/__pycache__/NSEC3PARAM.cpython-312.pyc,, +dns/rdtypes/ANY/__pycache__/OPENPGPKEY.cpython-312.pyc,, +dns/rdtypes/ANY/__pycache__/OPT.cpython-312.pyc,, +dns/rdtypes/ANY/__pycache__/PTR.cpython-312.pyc,, +dns/rdtypes/ANY/__pycache__/RP.cpython-312.pyc,, +dns/rdtypes/ANY/__pycache__/RRSIG.cpython-312.pyc,, +dns/rdtypes/ANY/__pycache__/RT.cpython-312.pyc,, +dns/rdtypes/ANY/__pycache__/SMIMEA.cpython-312.pyc,, +dns/rdtypes/ANY/__pycache__/SOA.cpython-312.pyc,, +dns/rdtypes/ANY/__pycache__/SPF.cpython-312.pyc,, +dns/rdtypes/ANY/__pycache__/SSHFP.cpython-312.pyc,, +dns/rdtypes/ANY/__pycache__/TKEY.cpython-312.pyc,, +dns/rdtypes/ANY/__pycache__/TLSA.cpython-312.pyc,, +dns/rdtypes/ANY/__pycache__/TSIG.cpython-312.pyc,, +dns/rdtypes/ANY/__pycache__/TXT.cpython-312.pyc,, +dns/rdtypes/ANY/__pycache__/URI.cpython-312.pyc,, +dns/rdtypes/ANY/__pycache__/X25.cpython-312.pyc,, +dns/rdtypes/ANY/__pycache__/ZONEMD.cpython-312.pyc,, +dns/rdtypes/ANY/__pycache__/__init__.cpython-312.pyc,, +dns/rdtypes/CH/A.py,sha256=3S3OhOkSc7_ZsZBVB4GhTS19LPrrZ-yQ8sAp957qEgI,2216 +dns/rdtypes/CH/__init__.py,sha256=GD9YeDKb9VBDo-J5rrChX1MWEGyQXuR9Htnbhg_iYLc,923 +dns/rdtypes/CH/__pycache__/A.cpython-312.pyc,, +dns/rdtypes/CH/__pycache__/__init__.cpython-312.pyc,, +dns/rdtypes/IN/A.py,sha256=FfFn3SqbpneL9Ky63COP50V2ZFxqS1ldCKJh39Enwug,1814 +dns/rdtypes/IN/AAAA.py,sha256=AxrOlYy-1TTTWeQypDKeXrDCrdHGor0EKCE4fxzSQGo,1820 +dns/rdtypes/IN/APL.py,sha256=ppyFwn0KYMdyDzphxd0BUhgTmZv0QnDMRLjzQQM793U,5097 +dns/rdtypes/IN/DHCID.py,sha256=zRUh_EOxUPVpJjWY5m7taX8q4Oz5K70785ZtKv5OTCU,1856 +dns/rdtypes/IN/HTTPS.py,sha256=P-IjwcvDQMmtoBgsDHglXF7KgLX73G6jEDqCKsnaGpQ,220 +dns/rdtypes/IN/IPSECKEY.py,sha256=RyIy9K0Yt0uJRjdr6cj5S95ELHHbl--0xV-Qq9O3QQk,3290 +dns/rdtypes/IN/KX.py,sha256=K1JwItL0n5G-YGFCjWeh0C9DyDD8G8VzicsBeQiNAv0,1013 +dns/rdtypes/IN/NAPTR.py,sha256=SaOK-0hIYImwLtb5Hqewi-e49ykJaQiLNvk8ZzNoG7Q,3750 +dns/rdtypes/IN/NSAP.py,sha256=3OUpPOSOxU8fcdi0Oe6Ex2ERXcQ-U3iNf6FftZMtNOw,2165 +dns/rdtypes/IN/NSAP_PTR.py,sha256=iTxlV6fr_Y9lqivLLncSHxEhmFqz5UEElDW3HMBtuCU,1015 +dns/rdtypes/IN/PX.py,sha256=vHDNN2rfLObuUKwpYDIvpPB482BqXlHA-ZQpQn9Sb_E,2756 +dns/rdtypes/IN/SRV.py,sha256=a0zGaUwzvih_a4Q9BViUTFs7NZaCqgl7mls3-KRVHm8,2769 +dns/rdtypes/IN/SVCB.py,sha256=HeFmi2v01F00Hott8FlvQ4R7aPxFmT7RF-gt45R5K_M,218 +dns/rdtypes/IN/WKS.py,sha256=kErSG5AO2qIuot_hkMHnQuZB1_uUzUirNdqBoCp97rk,3652 +dns/rdtypes/IN/__init__.py,sha256=HbI8aw9HWroI6SgEvl8Sx6FdkDswCCXMbSRuJy5o8LQ,1083 +dns/rdtypes/IN/__pycache__/A.cpython-312.pyc,, +dns/rdtypes/IN/__pycache__/AAAA.cpython-312.pyc,, +dns/rdtypes/IN/__pycache__/APL.cpython-312.pyc,, +dns/rdtypes/IN/__pycache__/DHCID.cpython-312.pyc,, +dns/rdtypes/IN/__pycache__/HTTPS.cpython-312.pyc,, +dns/rdtypes/IN/__pycache__/IPSECKEY.cpython-312.pyc,, +dns/rdtypes/IN/__pycache__/KX.cpython-312.pyc,, +dns/rdtypes/IN/__pycache__/NAPTR.cpython-312.pyc,, +dns/rdtypes/IN/__pycache__/NSAP.cpython-312.pyc,, +dns/rdtypes/IN/__pycache__/NSAP_PTR.cpython-312.pyc,, +dns/rdtypes/IN/__pycache__/PX.cpython-312.pyc,, +dns/rdtypes/IN/__pycache__/SRV.cpython-312.pyc,, +dns/rdtypes/IN/__pycache__/SVCB.cpython-312.pyc,, +dns/rdtypes/IN/__pycache__/WKS.cpython-312.pyc,, +dns/rdtypes/IN/__pycache__/__init__.cpython-312.pyc,, +dns/rdtypes/__init__.py,sha256=NYizfGglJfhqt_GMtSSXf7YQXIEHHCiJ_Y_qaLVeiOI,1073 +dns/rdtypes/__pycache__/__init__.cpython-312.pyc,, +dns/rdtypes/__pycache__/dnskeybase.cpython-312.pyc,, +dns/rdtypes/__pycache__/dsbase.cpython-312.pyc,, +dns/rdtypes/__pycache__/euibase.cpython-312.pyc,, +dns/rdtypes/__pycache__/mxbase.cpython-312.pyc,, +dns/rdtypes/__pycache__/nsbase.cpython-312.pyc,, +dns/rdtypes/__pycache__/svcbbase.cpython-312.pyc,, +dns/rdtypes/__pycache__/tlsabase.cpython-312.pyc,, +dns/rdtypes/__pycache__/txtbase.cpython-312.pyc,, +dns/rdtypes/__pycache__/util.cpython-312.pyc,, +dns/rdtypes/dnskeybase.py,sha256=FoDllfa9Pz2j2rf45VyUUYUsIt3kjjrwDy6LxrlPb5s,2856 +dns/rdtypes/dsbase.py,sha256=I85Aps1lBsiItdqGpsNY1O8icosfPtkWjiUn1J1lLUQ,3427 +dns/rdtypes/euibase.py,sha256=umN9A3VNw1TziAVtePvUses2jWPcynxINvjgyndPCdQ,2630 +dns/rdtypes/mxbase.py,sha256=DzjbiKoAAgpqbhwMBIFGA081jR5_doqGAq-kLvy2mns,3196 +dns/rdtypes/nsbase.py,sha256=tueXVV6E8lelebOmrmoOPq47eeRvOpsxHVXH4cOFxcs,2323 +dns/rdtypes/svcbbase.py,sha256=TQRT52m8F2NpSJsHUkTFS-hrkyhcIoAodW6bBHED4CY,16674 +dns/rdtypes/tlsabase.py,sha256=pIiWem6sF4IwyyKmyqx5xg55IG0w3K9r502Yx8PdziA,2596 +dns/rdtypes/txtbase.py,sha256=K4v2ulFu0DxPjxyf_Ul7YRjfBpUO-Ay_ChnR_Wx-ywA,3601 +dns/rdtypes/util.py,sha256=6AGQ-k3mLNlx4Ep_FiDABj1WVumUUGs3zQ6X-2iISec,9003 +dns/renderer.py,sha256=5THf1iKql2JPL2sKZt2-b4zqHKfk_vlx0FEfPtMJysY,11254 +dns/resolver.py,sha256=wagpUIu8Oh12O-zk48U30A6VQQOspjfibU4Ls2So-kM,73552 +dns/reversename.py,sha256=zoqXEbMZXm6R13nXbJHgTsf6L2C6uReODj6mqSHrTiE,3828 +dns/rrset.py,sha256=J-oQPEPJuKueLLiz1FN08P-ys9fjHhPWuwpDdrL4UTQ,9170 +dns/serial.py,sha256=-t5rPW-TcJwzBMfIJo7Tl-uDtaYtpqOfCVYx9dMaDCY,3606 +dns/set.py,sha256=Lr1qhyqywoobNkj9sAfdovoFy9vBfkz2eHdTCc7sZRs,9088 +dns/tokenizer.py,sha256=Dcc3lQgEIHCVZBuO6FaKWEojtPSd3EuaUC4vQA-spnk,23583 +dns/transaction.py,sha256=ZlnDT-V4W01J3cS501GaRLVhE9t1jZdnEZxPyZ0Cvg4,22636 +dns/tsig.py,sha256=I-Y-c3WMBX11bVioy5puFly2BhlpptUz82ikahxuh1c,11413 +dns/tsigkeyring.py,sha256=Z0xZemcU3XjZ9HlxBYv2E2PSuIhaFreqLDlD7HcmZDA,2633 +dns/ttl.py,sha256=fWFkw8qfk6saTp7lAPxZOuD3U3TRxVRvIpljQnG-01I,2979 +dns/update.py,sha256=y9d6LOO8xrUaH2UrZhy3ssnx8bJEsxqTArw5V8XqBRs,12243 +dns/version.py,sha256=sRMqE5tzPhXEzz-SEvdN82pP77xF_i1iELxaJN0roDE,1926 +dns/versioned.py,sha256=3YQj8mzGmZEsjnuVJJjcWopVmDKYLhEj4hEGTLEwzco,11765 +dns/win32util.py,sha256=NEjd5RXQU2aV1WsBMoIGZmXyqqKCxS4WYq9HqFQoVig,9107 +dns/wire.py,sha256=vy0SolgECbO1UXB4dnhXhDeFKOJT29nQxXvSfKOgA5s,2830 +dns/xfr.py,sha256=FKkKO-kSpyE1vHU5mnoPIP4YxiCl5gG7E5wOgY_4GO8,13273 +dns/zone.py,sha256=lLAarSxPtpx4Sw29OQ0ifPshD4QauGu8RnPh2dEropA,52086 +dns/zonefile.py,sha256=9pgkO0pV8Js53Oq9ZKOSbpFkGS5r_orU-25tmufGP9M,27929 +dns/zonetypes.py,sha256=HrQNZxZ_gWLWI9dskix71msi9wkYK5pgrBBbPb1T74Y,690 +dnspython-2.6.1.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +dnspython-2.6.1.dist-info/METADATA,sha256=2GJFv-NqkwIytog5VQe0wPtZKoS016uyYfG76lqftto,5808 +dnspython-2.6.1.dist-info/RECORD,, +dnspython-2.6.1.dist-info/WHEEL,sha256=TJPnKdtrSue7xZ_AVGkp9YXcvDrobsjBds1du3Nx6dc,87 +dnspython-2.6.1.dist-info/licenses/LICENSE,sha256=w-o_9WVLMpwZ07xfdIGvYjw93tSmFFWFSZ-EOtPXQc0,1526 diff --git a/venv/Lib/site-packages/dnspython-2.6.1.dist-info/WHEEL b/venv/Lib/site-packages/dnspython-2.6.1.dist-info/WHEEL new file mode 100644 index 00000000..5998f3aa --- /dev/null +++ b/venv/Lib/site-packages/dnspython-2.6.1.dist-info/WHEEL @@ -0,0 +1,4 @@ +Wheel-Version: 1.0 +Generator: hatchling 1.21.1 +Root-Is-Purelib: true +Tag: py3-none-any diff --git a/venv/Lib/site-packages/dnspython-2.6.1.dist-info/licenses/LICENSE b/venv/Lib/site-packages/dnspython-2.6.1.dist-info/licenses/LICENSE new file mode 100644 index 00000000..390a726d --- /dev/null +++ b/venv/Lib/site-packages/dnspython-2.6.1.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/venv/Lib/site-packages/gridfs/__init__.py b/venv/Lib/site-packages/gridfs/__init__.py new file mode 100644 index 00000000..8d01fefc --- /dev/null +++ b/venv/Lib/site-packages/gridfs/__init__.py @@ -0,0 +1,1000 @@ +# Copyright 2009-present MongoDB, 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. + +"""GridFS is a specification for storing large objects in Mongo. + +The :mod:`gridfs` package is an implementation of GridFS on top of +:mod:`pymongo`, exposing a file-like interface. + +.. seealso:: The MongoDB documentation on `gridfs `_. +""" +from __future__ import annotations + +from collections import abc +from typing import Any, Mapping, Optional, cast + +from bson.objectid import ObjectId +from gridfs.errors import NoFile +from gridfs.grid_file import ( + DEFAULT_CHUNK_SIZE, + GridIn, + GridOut, + GridOutCursor, + _clear_entity_type_registry, + _disallow_transactions, +) +from pymongo import ASCENDING, DESCENDING, _csot +from pymongo.client_session import ClientSession +from pymongo.collection import Collection +from pymongo.common import validate_string +from pymongo.database import Database +from pymongo.errors import ConfigurationError +from pymongo.read_preferences import _ServerMode +from pymongo.write_concern import WriteConcern + +__all__ = [ + "GridFS", + "GridFSBucket", + "NoFile", + "DEFAULT_CHUNK_SIZE", + "GridIn", + "GridOut", + "GridOutCursor", +] + + +class GridFS: + """An instance of GridFS on top of a single Database.""" + + def __init__(self, database: Database, collection: str = "fs"): + """Create a new instance of :class:`GridFS`. + + Raises :class:`TypeError` if `database` is not an instance of + :class:`~pymongo.database.Database`. + + :param database: database to use + :param collection: root collection to use + + .. versionchanged:: 4.0 + Removed the `disable_md5` parameter. See + :ref:`removed-gridfs-checksum` for details. + + .. versionchanged:: 3.11 + Running a GridFS operation in a transaction now always raises an + error. GridFS does not support multi-document transactions. + + .. versionchanged:: 3.7 + Added the `disable_md5` parameter. + + .. versionchanged:: 3.1 + Indexes are only ensured on the first write to the DB. + + .. versionchanged:: 3.0 + `database` must use an acknowledged + :attr:`~pymongo.database.Database.write_concern` + + .. seealso:: The MongoDB documentation on `gridfs `_. + """ + if not isinstance(database, Database): + raise TypeError("database must be an instance of Database") + + database = _clear_entity_type_registry(database) + + if not database.write_concern.acknowledged: + raise ConfigurationError("database must use acknowledged write_concern") + + self.__collection = database[collection] + self.__files = self.__collection.files + self.__chunks = self.__collection.chunks + + def new_file(self, **kwargs: Any) -> GridIn: + """Create a new file in GridFS. + + Returns a new :class:`~gridfs.grid_file.GridIn` instance to + which data can be written. Any keyword arguments will be + passed through to :meth:`~gridfs.grid_file.GridIn`. + + If the ``"_id"`` of the file is manually specified, it must + not already exist in GridFS. Otherwise + :class:`~gridfs.errors.FileExists` is raised. + + :param kwargs: keyword arguments for file creation + """ + return GridIn(self.__collection, **kwargs) + + def put(self, data: Any, **kwargs: Any) -> Any: + """Put data in GridFS as a new file. + + Equivalent to doing:: + + with fs.new_file(**kwargs) as f: + f.write(data) + + `data` can be either an instance of :class:`bytes` or a file-like + object providing a :meth:`read` method. If an `encoding` keyword + argument is passed, `data` can also be a :class:`str` instance, which + will be encoded as `encoding` before being written. Any keyword + arguments will be passed through to the created file - see + :meth:`~gridfs.grid_file.GridIn` for possible arguments. Returns the + ``"_id"`` of the created file. + + If the ``"_id"`` of the file is manually specified, it must + not already exist in GridFS. Otherwise + :class:`~gridfs.errors.FileExists` is raised. + + :param data: data to be written as a file. + :param kwargs: keyword arguments for file creation + + .. versionchanged:: 3.0 + w=0 writes to GridFS are now prohibited. + """ + with GridIn(self.__collection, **kwargs) as grid_file: + grid_file.write(data) + return grid_file._id + + def get(self, file_id: Any, session: Optional[ClientSession] = None) -> GridOut: + """Get a file from GridFS by ``"_id"``. + + Returns an instance of :class:`~gridfs.grid_file.GridOut`, + which provides a file-like interface for reading. + + :param file_id: ``"_id"`` of the file to get + :param session: a + :class:`~pymongo.client_session.ClientSession` + + .. versionchanged:: 3.6 + Added ``session`` parameter. + """ + gout = GridOut(self.__collection, file_id, session=session) + + # Raise NoFile now, instead of on first attribute access. + gout._ensure_file() + return gout + + def get_version( + self, + filename: Optional[str] = None, + version: Optional[int] = -1, + session: Optional[ClientSession] = None, + **kwargs: Any, + ) -> GridOut: + """Get a file from GridFS by ``"filename"`` or metadata fields. + + Returns a version of the file in GridFS whose filename matches + `filename` and whose metadata fields match the supplied keyword + arguments, as an instance of :class:`~gridfs.grid_file.GridOut`. + + Version numbering is a convenience atop the GridFS API provided + by MongoDB. If more than one file matches the query (either by + `filename` alone, by metadata fields, or by a combination of + both), then version ``-1`` will be the most recently uploaded + matching file, ``-2`` the second most recently + uploaded, etc. Version ``0`` will be the first version + uploaded, ``1`` the second version, etc. So if three versions + have been uploaded, then version ``0`` is the same as version + ``-3``, version ``1`` is the same as version ``-2``, and + version ``2`` is the same as version ``-1``. + + Raises :class:`~gridfs.errors.NoFile` if no such version of + that file exists. + + :param filename: ``"filename"`` of the file to get, or `None` + :param version: version of the file to get (defaults + to -1, the most recent version uploaded) + :param session: a + :class:`~pymongo.client_session.ClientSession` + :param kwargs: find files by custom metadata. + + .. versionchanged:: 3.6 + Added ``session`` parameter. + + .. versionchanged:: 3.1 + ``get_version`` no longer ensures indexes. + """ + query = kwargs + if filename is not None: + query["filename"] = filename + + _disallow_transactions(session) + cursor = self.__files.find(query, session=session) + if version is None: + version = -1 + if version < 0: + skip = abs(version) - 1 + cursor.limit(-1).skip(skip).sort("uploadDate", DESCENDING) + else: + cursor.limit(-1).skip(version).sort("uploadDate", ASCENDING) + try: + doc = next(cursor) + return GridOut(self.__collection, file_document=doc, session=session) + except StopIteration: + raise NoFile("no version %d for filename %r" % (version, filename)) from None + + def get_last_version( + self, filename: Optional[str] = None, session: Optional[ClientSession] = None, **kwargs: Any + ) -> GridOut: + """Get the most recent version of a file in GridFS by ``"filename"`` + or metadata fields. + + Equivalent to calling :meth:`get_version` with the default + `version` (``-1``). + + :param filename: ``"filename"`` of the file to get, or `None` + :param session: a + :class:`~pymongo.client_session.ClientSession` + :param kwargs: find files by custom metadata. + + .. versionchanged:: 3.6 + Added ``session`` parameter. + """ + return self.get_version(filename=filename, session=session, **kwargs) + + # TODO add optional safe mode for chunk removal? + def delete(self, file_id: Any, session: Optional[ClientSession] = None) -> None: + """Delete a file from GridFS by ``"_id"``. + + Deletes all data belonging to the file with ``"_id"``: + `file_id`. + + .. warning:: Any processes/threads reading from the file while + this method is executing will likely see an invalid/corrupt + file. Care should be taken to avoid concurrent reads to a file + while it is being deleted. + + .. note:: Deletes of non-existent files are considered successful + since the end result is the same: no file with that _id remains. + + :param file_id: ``"_id"`` of the file to delete + :param session: a + :class:`~pymongo.client_session.ClientSession` + + .. versionchanged:: 3.6 + Added ``session`` parameter. + + .. versionchanged:: 3.1 + ``delete`` no longer ensures indexes. + """ + _disallow_transactions(session) + self.__files.delete_one({"_id": file_id}, session=session) + self.__chunks.delete_many({"files_id": file_id}, session=session) + + def list(self, session: Optional[ClientSession] = None) -> list[str]: + """List the names of all files stored in this instance of + :class:`GridFS`. + + :param session: a + :class:`~pymongo.client_session.ClientSession` + + .. versionchanged:: 3.6 + Added ``session`` parameter. + + .. versionchanged:: 3.1 + ``list`` no longer ensures indexes. + """ + _disallow_transactions(session) + # With an index, distinct includes documents with no filename + # as None. + return [ + name for name in self.__files.distinct("filename", session=session) if name is not None + ] + + def find_one( + self, + filter: Optional[Any] = None, + session: Optional[ClientSession] = None, + *args: Any, + **kwargs: Any, + ) -> Optional[GridOut]: + """Get a single file from gridfs. + + All arguments to :meth:`find` are also valid arguments for + :meth:`find_one`, although any `limit` argument will be + ignored. Returns a single :class:`~gridfs.grid_file.GridOut`, + or ``None`` if no matching file is found. For example: + + .. code-block: python + + file = fs.find_one({"filename": "lisa.txt"}) + + :param filter: a dictionary specifying + the query to be performing OR any other type to be used as + the value for a query for ``"_id"`` in the file collection. + :param args: any additional positional arguments are + the same as the arguments to :meth:`find`. + :param session: a + :class:`~pymongo.client_session.ClientSession` + :param kwargs: any additional keyword arguments + are the same as the arguments to :meth:`find`. + + .. versionchanged:: 3.6 + Added ``session`` parameter. + """ + if filter is not None and not isinstance(filter, abc.Mapping): + filter = {"_id": filter} + + _disallow_transactions(session) + for f in self.find(filter, *args, session=session, **kwargs): + return f + + return None + + def find(self, *args: Any, **kwargs: Any) -> GridOutCursor: + """Query GridFS for files. + + Returns a cursor that iterates across files matching + arbitrary queries on the files collection. Can be combined + with other modifiers for additional control. For example:: + + for grid_out in fs.find({"filename": "lisa.txt"}, + no_cursor_timeout=True): + data = grid_out.read() + + would iterate through all versions of "lisa.txt" stored in GridFS. + Note that setting no_cursor_timeout to True may be important to + prevent the cursor from timing out during long multi-file processing + work. + + As another example, the call:: + + most_recent_three = fs.find().sort("uploadDate", -1).limit(3) + + would return a cursor to the three most recently uploaded files + in GridFS. + + Follows a similar interface to + :meth:`~pymongo.collection.Collection.find` + in :class:`~pymongo.collection.Collection`. + + If a :class:`~pymongo.client_session.ClientSession` is passed to + :meth:`find`, all returned :class:`~gridfs.grid_file.GridOut` instances + are associated with that session. + + :param filter: A query document that selects which files + to include in the result set. Can be an empty document to include + all files. + :param skip: the number of files to omit (from + the start of the result set) when returning the results + :param limit: the maximum number of results to + return + :param no_cursor_timeout: if False (the default), any + returned cursor is closed by the server after 10 minutes of + inactivity. If set to True, the returned cursor will never + time out on the server. Care should be taken to ensure that + cursors with no_cursor_timeout turned on are properly closed. + :param sort: a list of (key, direction) pairs + specifying the sort order for this query. See + :meth:`~pymongo.cursor.Cursor.sort` for details. + + Raises :class:`TypeError` if any of the arguments are of + improper type. Returns an instance of + :class:`~gridfs.grid_file.GridOutCursor` + corresponding to this query. + + .. versionchanged:: 3.0 + Removed the read_preference, tag_sets, and + secondary_acceptable_latency_ms options. + .. versionadded:: 2.7 + .. seealso:: The MongoDB documentation on `find `_. + """ + return GridOutCursor(self.__collection, *args, **kwargs) + + def exists( + self, + document_or_id: Optional[Any] = None, + session: Optional[ClientSession] = None, + **kwargs: Any, + ) -> bool: + """Check if a file exists in this instance of :class:`GridFS`. + + The file to check for can be specified by the value of its + ``_id`` key, or by passing in a query document. A query + document can be passed in as dictionary, or by using keyword + arguments. Thus, the following three calls are equivalent: + + >>> fs.exists(file_id) + >>> fs.exists({"_id": file_id}) + >>> fs.exists(_id=file_id) + + As are the following two calls: + + >>> fs.exists({"filename": "mike.txt"}) + >>> fs.exists(filename="mike.txt") + + And the following two: + + >>> fs.exists({"foo": {"$gt": 12}}) + >>> fs.exists(foo={"$gt": 12}) + + Returns ``True`` if a matching file exists, ``False`` + otherwise. Calls to :meth:`exists` will not automatically + create appropriate indexes; application developers should be + sure to create indexes if needed and as appropriate. + + :param document_or_id: query document, or _id of the + document to check for + :param session: a + :class:`~pymongo.client_session.ClientSession` + :param kwargs: keyword arguments are used as a + query document, if they're present. + + .. versionchanged:: 3.6 + Added ``session`` parameter. + """ + _disallow_transactions(session) + if kwargs: + f = self.__files.find_one(kwargs, ["_id"], session=session) + else: + f = self.__files.find_one(document_or_id, ["_id"], session=session) + + return f is not None + + +class GridFSBucket: + """An instance of GridFS on top of a single Database.""" + + def __init__( + self, + db: Database, + bucket_name: str = "fs", + chunk_size_bytes: int = DEFAULT_CHUNK_SIZE, + write_concern: Optional[WriteConcern] = None, + read_preference: Optional[_ServerMode] = None, + ) -> None: + """Create a new instance of :class:`GridFSBucket`. + + Raises :exc:`TypeError` if `database` is not an instance of + :class:`~pymongo.database.Database`. + + Raises :exc:`~pymongo.errors.ConfigurationError` if `write_concern` + is not acknowledged. + + :param database: database to use. + :param bucket_name: The name of the bucket. Defaults to 'fs'. + :param chunk_size_bytes: The chunk size in bytes. Defaults + to 255KB. + :param write_concern: The + :class:`~pymongo.write_concern.WriteConcern` to use. If ``None`` + (the default) db.write_concern is used. + :param read_preference: The read preference to use. If + ``None`` (the default) db.read_preference is used. + + .. versionchanged:: 4.0 + Removed the `disable_md5` parameter. See + :ref:`removed-gridfs-checksum` for details. + + .. versionchanged:: 3.11 + Running a GridFSBucket operation in a transaction now always raises + an error. GridFSBucket does not support multi-document transactions. + + .. versionchanged:: 3.7 + Added the `disable_md5` parameter. + + .. versionadded:: 3.1 + + .. seealso:: The MongoDB documentation on `gridfs `_. + """ + if not isinstance(db, Database): + raise TypeError("database must be an instance of Database") + + db = _clear_entity_type_registry(db) + + wtc = write_concern if write_concern is not None else db.write_concern + if not wtc.acknowledged: + raise ConfigurationError("write concern must be acknowledged") + + self._bucket_name = bucket_name + self._collection = db[bucket_name] + self._chunks: Collection = self._collection.chunks.with_options( + write_concern=write_concern, read_preference=read_preference + ) + + self._files: Collection = self._collection.files.with_options( + write_concern=write_concern, read_preference=read_preference + ) + + self._chunk_size_bytes = chunk_size_bytes + self._timeout = db.client.options.timeout + + def open_upload_stream( + self, + filename: str, + chunk_size_bytes: Optional[int] = None, + metadata: Optional[Mapping[str, Any]] = None, + session: Optional[ClientSession] = None, + ) -> GridIn: + """Opens a Stream that the application can write the contents of the + file to. + + The user must specify the filename, and can choose to add any + additional information in the metadata field of the file document or + modify the chunk size. + For example:: + + my_db = MongoClient().test + fs = GridFSBucket(my_db) + with fs.open_upload_stream( + "test_file", chunk_size_bytes=4, + metadata={"contentType": "text/plain"}) as grid_in: + grid_in.write("data I want to store!") + # uploaded on close + + Returns an instance of :class:`~gridfs.grid_file.GridIn`. + + Raises :exc:`~gridfs.errors.NoFile` if no such version of + that file exists. + Raises :exc:`~ValueError` if `filename` is not a string. + + :param filename: The name of the file to upload. + :param chunk_size_bytes` (options): The number of bytes per chunk of this + file. Defaults to the chunk_size_bytes in :class:`GridFSBucket`. + :param metadata: User data for the 'metadata' field of the + files collection document. If not provided the metadata field will + be omitted from the files collection document. + :param session: a + :class:`~pymongo.client_session.ClientSession` + + .. versionchanged:: 3.6 + Added ``session`` parameter. + """ + validate_string("filename", filename) + + opts = { + "filename": filename, + "chunk_size": ( + chunk_size_bytes if chunk_size_bytes is not None else self._chunk_size_bytes + ), + } + if metadata is not None: + opts["metadata"] = metadata + + return GridIn(self._collection, session=session, **opts) + + def open_upload_stream_with_id( + self, + file_id: Any, + filename: str, + chunk_size_bytes: Optional[int] = None, + metadata: Optional[Mapping[str, Any]] = None, + session: Optional[ClientSession] = None, + ) -> GridIn: + """Opens a Stream that the application can write the contents of the + file to. + + The user must specify the file id and filename, and can choose to add + any additional information in the metadata field of the file document + or modify the chunk size. + For example:: + + my_db = MongoClient().test + fs = GridFSBucket(my_db) + with fs.open_upload_stream_with_id( + ObjectId(), + "test_file", + chunk_size_bytes=4, + metadata={"contentType": "text/plain"}) as grid_in: + grid_in.write("data I want to store!") + # uploaded on close + + Returns an instance of :class:`~gridfs.grid_file.GridIn`. + + Raises :exc:`~gridfs.errors.NoFile` if no such version of + that file exists. + Raises :exc:`~ValueError` if `filename` is not a string. + + :param file_id: The id to use for this file. The id must not have + already been used for another file. + :param filename: The name of the file to upload. + :param chunk_size_bytes` (options): The number of bytes per chunk of this + file. Defaults to the chunk_size_bytes in :class:`GridFSBucket`. + :param metadata: User data for the 'metadata' field of the + files collection document. If not provided the metadata field will + be omitted from the files collection document. + :param session: a + :class:`~pymongo.client_session.ClientSession` + + .. versionchanged:: 3.6 + Added ``session`` parameter. + """ + validate_string("filename", filename) + + opts = { + "_id": file_id, + "filename": filename, + "chunk_size": ( + chunk_size_bytes if chunk_size_bytes is not None else self._chunk_size_bytes + ), + } + if metadata is not None: + opts["metadata"] = metadata + + return GridIn(self._collection, session=session, **opts) + + @_csot.apply + def upload_from_stream( + self, + filename: str, + source: Any, + chunk_size_bytes: Optional[int] = None, + metadata: Optional[Mapping[str, Any]] = None, + session: Optional[ClientSession] = None, + ) -> ObjectId: + """Uploads a user file to a GridFS bucket. + + Reads the contents of the user file from `source` and uploads + it to the file `filename`. Source can be a string or file-like object. + For example:: + + my_db = MongoClient().test + fs = GridFSBucket(my_db) + file_id = fs.upload_from_stream( + "test_file", + "data I want to store!", + chunk_size_bytes=4, + metadata={"contentType": "text/plain"}) + + Returns the _id of the uploaded file. + + Raises :exc:`~gridfs.errors.NoFile` if no such version of + that file exists. + Raises :exc:`~ValueError` if `filename` is not a string. + + :param filename: The name of the file to upload. + :param source: The source stream of the content to be uploaded. Must be + a file-like object that implements :meth:`read` or a string. + :param chunk_size_bytes` (options): The number of bytes per chunk of this + file. Defaults to the chunk_size_bytes of :class:`GridFSBucket`. + :param metadata: User data for the 'metadata' field of the + files collection document. If not provided the metadata field will + be omitted from the files collection document. + :param session: a + :class:`~pymongo.client_session.ClientSession` + + .. versionchanged:: 3.6 + Added ``session`` parameter. + """ + with self.open_upload_stream(filename, chunk_size_bytes, metadata, session=session) as gin: + gin.write(source) + + return cast(ObjectId, gin._id) + + @_csot.apply + def upload_from_stream_with_id( + self, + file_id: Any, + filename: str, + source: Any, + chunk_size_bytes: Optional[int] = None, + metadata: Optional[Mapping[str, Any]] = None, + session: Optional[ClientSession] = None, + ) -> None: + """Uploads a user file to a GridFS bucket with a custom file id. + + Reads the contents of the user file from `source` and uploads + it to the file `filename`. Source can be a string or file-like object. + For example:: + + my_db = MongoClient().test + fs = GridFSBucket(my_db) + file_id = fs.upload_from_stream( + ObjectId(), + "test_file", + "data I want to store!", + chunk_size_bytes=4, + metadata={"contentType": "text/plain"}) + + Raises :exc:`~gridfs.errors.NoFile` if no such version of + that file exists. + Raises :exc:`~ValueError` if `filename` is not a string. + + :param file_id: The id to use for this file. The id must not have + already been used for another file. + :param filename: The name of the file to upload. + :param source: The source stream of the content to be uploaded. Must be + a file-like object that implements :meth:`read` or a string. + :param chunk_size_bytes` (options): The number of bytes per chunk of this + file. Defaults to the chunk_size_bytes of :class:`GridFSBucket`. + :param metadata: User data for the 'metadata' field of the + files collection document. If not provided the metadata field will + be omitted from the files collection document. + :param session: a + :class:`~pymongo.client_session.ClientSession` + + .. versionchanged:: 3.6 + Added ``session`` parameter. + """ + with self.open_upload_stream_with_id( + file_id, filename, chunk_size_bytes, metadata, session=session + ) as gin: + gin.write(source) + + def open_download_stream( + self, file_id: Any, session: Optional[ClientSession] = None + ) -> GridOut: + """Opens a Stream from which the application can read the contents of + the stored file specified by file_id. + + For example:: + + my_db = MongoClient().test + fs = GridFSBucket(my_db) + # get _id of file to read. + file_id = fs.upload_from_stream("test_file", "data I want to store!") + grid_out = fs.open_download_stream(file_id) + contents = grid_out.read() + + Returns an instance of :class:`~gridfs.grid_file.GridOut`. + + Raises :exc:`~gridfs.errors.NoFile` if no file with file_id exists. + + :param file_id: The _id of the file to be downloaded. + :param session: a + :class:`~pymongo.client_session.ClientSession` + + .. versionchanged:: 3.6 + Added ``session`` parameter. + """ + gout = GridOut(self._collection, file_id, session=session) + + # Raise NoFile now, instead of on first attribute access. + gout._ensure_file() + return gout + + @_csot.apply + def download_to_stream( + self, file_id: Any, destination: Any, session: Optional[ClientSession] = None + ) -> None: + """Downloads the contents of the stored file specified by file_id and + writes the contents to `destination`. + + For example:: + + my_db = MongoClient().test + fs = GridFSBucket(my_db) + # Get _id of file to read + file_id = fs.upload_from_stream("test_file", "data I want to store!") + # Get file to write to + file = open('myfile','wb+') + fs.download_to_stream(file_id, file) + file.seek(0) + contents = file.read() + + Raises :exc:`~gridfs.errors.NoFile` if no file with file_id exists. + + :param file_id: The _id of the file to be downloaded. + :param destination: a file-like object implementing :meth:`write`. + :param session: a + :class:`~pymongo.client_session.ClientSession` + + .. versionchanged:: 3.6 + Added ``session`` parameter. + """ + with self.open_download_stream(file_id, session=session) as gout: + while True: + chunk = gout.readchunk() + if not len(chunk): + break + destination.write(chunk) + + @_csot.apply + def delete(self, file_id: Any, session: Optional[ClientSession] = None) -> None: + """Given an file_id, delete this stored file's files collection document + and associated chunks from a GridFS bucket. + + For example:: + + my_db = MongoClient().test + fs = GridFSBucket(my_db) + # Get _id of file to delete + file_id = fs.upload_from_stream("test_file", "data I want to store!") + fs.delete(file_id) + + Raises :exc:`~gridfs.errors.NoFile` if no file with file_id exists. + + :param file_id: The _id of the file to be deleted. + :param session: a + :class:`~pymongo.client_session.ClientSession` + + .. versionchanged:: 3.6 + Added ``session`` parameter. + """ + _disallow_transactions(session) + res = self._files.delete_one({"_id": file_id}, session=session) + self._chunks.delete_many({"files_id": file_id}, session=session) + if not res.deleted_count: + raise NoFile("no file could be deleted because none matched %s" % file_id) + + def find(self, *args: Any, **kwargs: Any) -> GridOutCursor: + """Find and return the files collection documents that match ``filter`` + + Returns a cursor that iterates across files matching + arbitrary queries on the files collection. Can be combined + with other modifiers for additional control. + + For example:: + + for grid_data in fs.find({"filename": "lisa.txt"}, + no_cursor_timeout=True): + data = grid_data.read() + + would iterate through all versions of "lisa.txt" stored in GridFS. + Note that setting no_cursor_timeout to True may be important to + prevent the cursor from timing out during long multi-file processing + work. + + As another example, the call:: + + most_recent_three = fs.find().sort("uploadDate", -1).limit(3) + + would return a cursor to the three most recently uploaded files + in GridFS. + + Follows a similar interface to + :meth:`~pymongo.collection.Collection.find` + in :class:`~pymongo.collection.Collection`. + + If a :class:`~pymongo.client_session.ClientSession` is passed to + :meth:`find`, all returned :class:`~gridfs.grid_file.GridOut` instances + are associated with that session. + + :param filter: Search query. + :param batch_size: The number of documents to return per + batch. + :param limit: The maximum number of documents to return. + :param no_cursor_timeout: The server normally times out idle + cursors after an inactivity period (10 minutes) to prevent excess + memory use. Set this option to True prevent that. + :param skip: The number of documents to skip before + returning. + :param sort: The order by which to sort results. Defaults to + None. + """ + return GridOutCursor(self._collection, *args, **kwargs) + + def open_download_stream_by_name( + self, filename: str, revision: int = -1, session: Optional[ClientSession] = None + ) -> GridOut: + """Opens a Stream from which the application can read the contents of + `filename` and optional `revision`. + + For example:: + + my_db = MongoClient().test + fs = GridFSBucket(my_db) + grid_out = fs.open_download_stream_by_name("test_file") + contents = grid_out.read() + + Returns an instance of :class:`~gridfs.grid_file.GridOut`. + + Raises :exc:`~gridfs.errors.NoFile` if no such version of + that file exists. + + Raises :exc:`~ValueError` filename is not a string. + + :param filename: The name of the file to read from. + :param revision: Which revision (documents with the same + filename and different uploadDate) of the file to retrieve. + Defaults to -1 (the most recent revision). + :param session: a + :class:`~pymongo.client_session.ClientSession` + + :Note: Revision numbers are defined as follows: + + - 0 = the original stored file + - 1 = the first revision + - 2 = the second revision + - etc... + - -2 = the second most recent revision + - -1 = the most recent revision + + .. versionchanged:: 3.6 + Added ``session`` parameter. + """ + validate_string("filename", filename) + query = {"filename": filename} + _disallow_transactions(session) + cursor = self._files.find(query, session=session) + if revision < 0: + skip = abs(revision) - 1 + cursor.limit(-1).skip(skip).sort("uploadDate", DESCENDING) + else: + cursor.limit(-1).skip(revision).sort("uploadDate", ASCENDING) + try: + grid_file = next(cursor) + return GridOut(self._collection, file_document=grid_file, session=session) + except StopIteration: + raise NoFile("no version %d for filename %r" % (revision, filename)) from None + + @_csot.apply + def download_to_stream_by_name( + self, + filename: str, + destination: Any, + revision: int = -1, + session: Optional[ClientSession] = None, + ) -> None: + """Write the contents of `filename` (with optional `revision`) to + `destination`. + + For example:: + + my_db = MongoClient().test + fs = GridFSBucket(my_db) + # Get file to write to + file = open('myfile','wb') + fs.download_to_stream_by_name("test_file", file) + + Raises :exc:`~gridfs.errors.NoFile` if no such version of + that file exists. + + Raises :exc:`~ValueError` if `filename` is not a string. + + :param filename: The name of the file to read from. + :param destination: A file-like object that implements :meth:`write`. + :param revision: Which revision (documents with the same + filename and different uploadDate) of the file to retrieve. + Defaults to -1 (the most recent revision). + :param session: a + :class:`~pymongo.client_session.ClientSession` + + :Note: Revision numbers are defined as follows: + + - 0 = the original stored file + - 1 = the first revision + - 2 = the second revision + - etc... + - -2 = the second most recent revision + - -1 = the most recent revision + + .. versionchanged:: 3.6 + Added ``session`` parameter. + """ + with self.open_download_stream_by_name(filename, revision, session=session) as gout: + while True: + chunk = gout.readchunk() + if not len(chunk): + break + destination.write(chunk) + + def rename( + self, file_id: Any, new_filename: str, session: Optional[ClientSession] = None + ) -> None: + """Renames the stored file with the specified file_id. + + For example:: + + my_db = MongoClient().test + fs = GridFSBucket(my_db) + # Get _id of file to rename + file_id = fs.upload_from_stream("test_file", "data I want to store!") + fs.rename(file_id, "new_test_name") + + Raises :exc:`~gridfs.errors.NoFile` if no file with file_id exists. + + :param file_id: The _id of the file to be renamed. + :param new_filename: The new name of the file. + :param session: a + :class:`~pymongo.client_session.ClientSession` + + .. versionchanged:: 3.6 + Added ``session`` parameter. + """ + _disallow_transactions(session) + result = self._files.update_one( + {"_id": file_id}, {"$set": {"filename": new_filename}}, session=session + ) + if not result.matched_count: + raise NoFile( + "no files could be renamed %r because none " + "matched file_id %i" % (new_filename, file_id) + ) diff --git a/venv/Lib/site-packages/gridfs/__pycache__/__init__.cpython-312.pyc b/venv/Lib/site-packages/gridfs/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8cdfcd073b4286876380e8c684ddc2f754efa398 GIT binary patch literal 41030 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 literal 0 HcmV?d00001 diff --git a/venv/Lib/site-packages/gridfs/__pycache__/errors.cpython-312.pyc b/venv/Lib/site-packages/gridfs/__pycache__/errors.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f0b27447aaa041f26a7670721ab448841440ce28 GIT binary patch literal 1242 zcmb7^zi$&U6vyr4@}v1t5CS2DKn4k+NF-$d2?f+j4M`UW2*eU1U7R~7IrZ***uJ)x zfvE#*-TF6F{3-kgk(dwz8>nUJgx|RdL4u(-Jiqs~U;Od&b3RwAC8WQP`4=N*A@mi3 z`Ij>eKHk&LA#zaedOg<}wn#b?9uMeQ6ShI&|^HRIx5Hp$eov+|bS zpz}ISoZi`_k`c$JQj!rDg!(=)^Moxh8{H#oRTMXonN0X zti#+63wQd@ykTE@iq+4GB>SdnYYfUE8&M+i8Xz@Sv?Jm2_tDH+6VwxSp^TS zD>c`&vjA?J6)<@&;Cz>Nfb+pJrxJ-5j)~$#FzS+#;DKfjd01zAo>VMU=|TPjcnJW( z+*k%&H-Mm@3$O%uZP->$AN?EB(UgkKtE+T#=>7!qL^mfS{s;6jfP%TT47zDRLGRX9 zL|Ko}%A;7tLbtNWg0LDT0X(aQX(XZlL`)Z%>q$$H{`}vUcUpX6p2WJisx#mT%BR}= z$YP9t-bDD}7j*j@x_gqx`2NAY6QqZeBE}tjunj>6L$vYXEd(3jxA4LBUpi>v6#F-I C)iC1# literal 0 HcmV?d00001 diff --git a/venv/Lib/site-packages/gridfs/__pycache__/grid_file.cpython-312.pyc b/venv/Lib/site-packages/gridfs/__pycache__/grid_file.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d3ee67b7c492bad97fb6795f0e68ca77af43cb2b GIT binary patch literal 44280 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 R-G+X{Z2vC>`em#C{{g%Dk@Wxo literal 0 HcmV?d00001 diff --git a/venv/Lib/site-packages/gridfs/errors.py b/venv/Lib/site-packages/gridfs/errors.py new file mode 100644 index 00000000..e8c02cef --- /dev/null +++ b/venv/Lib/site-packages/gridfs/errors.py @@ -0,0 +1,34 @@ +# Copyright 2009-2015 MongoDB, 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. + +"""Exceptions raised by the :mod:`gridfs` package""" +from __future__ import annotations + +from pymongo.errors import PyMongoError + + +class GridFSError(PyMongoError): + """Base class for all GridFS exceptions.""" + + +class CorruptGridFile(GridFSError): + """Raised when a file in :class:`~gridfs.GridFS` is malformed.""" + + +class NoFile(GridFSError): + """Raised when trying to read from a non-existent file.""" + + +class FileExists(GridFSError): + """Raised when trying to create a file that already exists.""" diff --git a/venv/Lib/site-packages/gridfs/grid_file.py b/venv/Lib/site-packages/gridfs/grid_file.py new file mode 100644 index 00000000..ac72c144 --- /dev/null +++ b/venv/Lib/site-packages/gridfs/grid_file.py @@ -0,0 +1,964 @@ +# Copyright 2009-present MongoDB, 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. + +"""Tools for representing files stored in GridFS.""" +from __future__ import annotations + +import datetime +import io +import math +import os +import warnings +from typing import Any, Iterable, Mapping, NoReturn, Optional + +from bson.int64 import Int64 +from bson.objectid import ObjectId +from gridfs.errors import CorruptGridFile, FileExists, NoFile +from pymongo import ASCENDING +from pymongo.client_session import ClientSession +from pymongo.collection import Collection +from pymongo.common import MAX_MESSAGE_SIZE +from pymongo.cursor import Cursor +from pymongo.errors import ( + BulkWriteError, + ConfigurationError, + CursorNotFound, + DuplicateKeyError, + InvalidOperation, + OperationFailure, +) +from pymongo.helpers import _check_write_command_response +from pymongo.read_preferences import ReadPreference + +_SEEK_SET = os.SEEK_SET +_SEEK_CUR = os.SEEK_CUR +_SEEK_END = os.SEEK_END + +EMPTY = b"" +NEWLN = b"\n" + +"""Default chunk size, in bytes.""" +# Slightly under a power of 2, to work well with server's record allocations. +DEFAULT_CHUNK_SIZE = 255 * 1024 +# The number of chunked bytes to buffer before calling insert_many. +_UPLOAD_BUFFER_SIZE = MAX_MESSAGE_SIZE +# The number of chunk documents to buffer before calling insert_many. +_UPLOAD_BUFFER_CHUNKS = 100000 +# Rough BSON overhead of a chunk document not including the chunk data itself. +# Essentially len(encode({"_id": ObjectId(), "files_id": ObjectId(), "n": 1, "data": ""})) +_CHUNK_OVERHEAD = 60 + +_C_INDEX: dict[str, Any] = {"files_id": ASCENDING, "n": ASCENDING} +_F_INDEX: dict[str, Any] = {"filename": ASCENDING, "uploadDate": ASCENDING} + + +def _grid_in_property( + field_name: str, + docstring: str, + read_only: Optional[bool] = False, + closed_only: Optional[bool] = False, +) -> Any: + """Create a GridIn property.""" + warn_str = "" + if docstring.startswith("DEPRECATED,"): + warn_str = ( + f"GridIn property '{field_name}' is deprecated and will be removed in PyMongo 5.0" + ) + + def getter(self: Any) -> Any: + if warn_str: + warnings.warn(warn_str, stacklevel=2, category=DeprecationWarning) + if closed_only and not self._closed: + raise AttributeError("can only get %r on a closed file" % field_name) + # Protect against PHP-237 + if field_name == "length": + return self._file.get(field_name, 0) + return self._file.get(field_name, None) + + def setter(self: Any, value: Any) -> Any: + if warn_str: + warnings.warn(warn_str, stacklevel=2, category=DeprecationWarning) + if self._closed: + self._coll.files.update_one({"_id": self._file["_id"]}, {"$set": {field_name: value}}) + self._file[field_name] = value + + if read_only: + docstring += "\n\nThis attribute is read-only." + elif closed_only: + docstring = "{}\n\n{}".format( + docstring, + "This attribute is read-only and " + "can only be read after :meth:`close` " + "has been called.", + ) + + if not read_only and not closed_only: + return property(getter, setter, doc=docstring) + return property(getter, doc=docstring) + + +def _grid_out_property(field_name: str, docstring: str) -> Any: + """Create a GridOut property.""" + warn_str = "" + if docstring.startswith("DEPRECATED,"): + warn_str = ( + f"GridOut property '{field_name}' is deprecated and will be removed in PyMongo 5.0" + ) + + def getter(self: Any) -> Any: + if warn_str: + warnings.warn(warn_str, stacklevel=2, category=DeprecationWarning) + self._ensure_file() + + # Protect against PHP-237 + if field_name == "length": + return self._file.get(field_name, 0) + return self._file.get(field_name, None) + + docstring += "\n\nThis attribute is read-only." + return property(getter, doc=docstring) + + +def _clear_entity_type_registry(entity: Any, **kwargs: Any) -> Any: + """Clear the given database/collection object's type registry.""" + codecopts = entity.codec_options.with_options(type_registry=None) + return entity.with_options(codec_options=codecopts, **kwargs) + + +def _disallow_transactions(session: Optional[ClientSession]) -> None: + if session and session.in_transaction: + raise InvalidOperation("GridFS does not support multi-document transactions") + + +class GridIn: + """Class to write data to GridFS.""" + + def __init__( + self, root_collection: Collection, session: Optional[ClientSession] = None, **kwargs: Any + ) -> None: + """Write a file to GridFS + + Application developers should generally not need to + instantiate this class directly - instead see the methods + provided by :class:`~gridfs.GridFS`. + + Raises :class:`TypeError` if `root_collection` is not an + instance of :class:`~pymongo.collection.Collection`. + + Any of the file level options specified in the `GridFS Spec + `_ may be passed as + keyword arguments. Any additional keyword arguments will be + set as additional fields on the file document. Valid keyword + arguments include: + + - ``"_id"``: unique ID for this file (default: + :class:`~bson.objectid.ObjectId`) - this ``"_id"`` must + not have already been used for another file + + - ``"filename"``: human name for the file + + - ``"contentType"`` or ``"content_type"``: valid mime-type + for the file + + - ``"chunkSize"`` or ``"chunk_size"``: size of each of the + chunks, in bytes (default: 255 kb) + + - ``"encoding"``: encoding used for this file. Any :class:`str` + that is written to the file will be converted to :class:`bytes`. + + :param root_collection: root collection to write to + :param session: a + :class:`~pymongo.client_session.ClientSession` to use for all + commands + :param kwargs: Any: file level options (see above) + + .. versionchanged:: 4.0 + Removed the `disable_md5` parameter. See + :ref:`removed-gridfs-checksum` for details. + + .. versionchanged:: 3.7 + Added the `disable_md5` parameter. + + .. versionchanged:: 3.6 + Added ``session`` parameter. + + .. versionchanged:: 3.0 + `root_collection` must use an acknowledged + :attr:`~pymongo.collection.Collection.write_concern` + """ + if not isinstance(root_collection, Collection): + raise TypeError("root_collection must be an instance of Collection") + + if not root_collection.write_concern.acknowledged: + raise ConfigurationError("root_collection must use acknowledged write_concern") + _disallow_transactions(session) + + # Handle alternative naming + if "content_type" in kwargs: + kwargs["contentType"] = kwargs.pop("content_type") + if "chunk_size" in kwargs: + kwargs["chunkSize"] = kwargs.pop("chunk_size") + + coll = _clear_entity_type_registry(root_collection, read_preference=ReadPreference.PRIMARY) + + # Defaults + kwargs["_id"] = kwargs.get("_id", ObjectId()) + kwargs["chunkSize"] = kwargs.get("chunkSize", DEFAULT_CHUNK_SIZE) + object.__setattr__(self, "_session", session) + object.__setattr__(self, "_coll", coll) + object.__setattr__(self, "_chunks", coll.chunks) + object.__setattr__(self, "_file", kwargs) + object.__setattr__(self, "_buffer", io.BytesIO()) + object.__setattr__(self, "_position", 0) + object.__setattr__(self, "_chunk_number", 0) + object.__setattr__(self, "_closed", False) + object.__setattr__(self, "_ensured_index", False) + object.__setattr__(self, "_buffered_docs", []) + object.__setattr__(self, "_buffered_docs_size", 0) + + def __create_index(self, collection: Collection, index_key: Any, unique: bool) -> None: + doc = collection.find_one(projection={"_id": 1}, session=self._session) + if doc is None: + try: + index_keys = [ + index_spec["key"] + for index_spec in collection.list_indexes(session=self._session) + ] + except OperationFailure: + index_keys = [] + if index_key not in index_keys: + collection.create_index(index_key.items(), unique=unique, session=self._session) + + def __ensure_indexes(self) -> None: + if not object.__getattribute__(self, "_ensured_index"): + _disallow_transactions(self._session) + self.__create_index(self._coll.files, _F_INDEX, False) + self.__create_index(self._coll.chunks, _C_INDEX, True) + object.__setattr__(self, "_ensured_index", True) + + def abort(self) -> None: + """Remove all chunks/files that may have been uploaded and close.""" + self._coll.chunks.delete_many({"files_id": self._file["_id"]}, session=self._session) + self._coll.files.delete_one({"_id": self._file["_id"]}, session=self._session) + object.__setattr__(self, "_closed", True) + + @property + def closed(self) -> bool: + """Is this file closed?""" + return self._closed + + _id: Any = _grid_in_property("_id", "The ``'_id'`` value for this file.", read_only=True) + filename: Optional[str] = _grid_in_property("filename", "Name of this file.") + name: Optional[str] = _grid_in_property("filename", "Alias for `filename`.") + content_type: Optional[str] = _grid_in_property( + "contentType", "DEPRECATED, will be removed in PyMongo 5.0. Mime-type for this file." + ) + length: int = _grid_in_property("length", "Length (in bytes) of this file.", closed_only=True) + chunk_size: int = _grid_in_property("chunkSize", "Chunk size for this file.", read_only=True) + upload_date: datetime.datetime = _grid_in_property( + "uploadDate", "Date that this file was uploaded.", closed_only=True + ) + md5: Optional[str] = _grid_in_property( + "md5", + "DEPRECATED, will be removed in PyMongo 5.0. MD5 of the contents of this file if an md5 sum was created.", + closed_only=True, + ) + + _buffer: io.BytesIO + _closed: bool + _buffered_docs: list[dict[str, Any]] + _buffered_docs_size: int + + def __getattr__(self, name: str) -> Any: + if name in self._file: + return self._file[name] + raise AttributeError("GridIn object has no attribute '%s'" % name) + + def __setattr__(self, name: str, value: Any) -> None: + # For properties of this instance like _buffer, or descriptors set on + # the class like filename, use regular __setattr__ + if name in self.__dict__ or name in self.__class__.__dict__: + object.__setattr__(self, name, value) + else: + # All other attributes are part of the document in db.fs.files. + # Store them to be sent to server on close() or if closed, send + # them now. + self._file[name] = value + if self._closed: + self._coll.files.update_one({"_id": self._file["_id"]}, {"$set": {name: value}}) + + def __flush_data(self, data: Any, force: bool = False) -> None: + """Flush `data` to a chunk.""" + self.__ensure_indexes() + assert len(data) <= self.chunk_size + if data: + self._buffered_docs.append( + {"files_id": self._file["_id"], "n": self._chunk_number, "data": data} + ) + self._buffered_docs_size += len(data) + _CHUNK_OVERHEAD + if not self._buffered_docs: + return + # Limit to 100,000 chunks or 32MB (+1 chunk) of data. + if ( + force + or self._buffered_docs_size >= _UPLOAD_BUFFER_SIZE + or len(self._buffered_docs) >= _UPLOAD_BUFFER_CHUNKS + ): + try: + self._chunks.insert_many(self._buffered_docs, session=self._session) + except BulkWriteError as exc: + # For backwards compatibility, raise an insert_one style exception. + write_errors = exc.details["writeErrors"] + for err in write_errors: + if err.get("code") in (11000, 11001, 12582): # Duplicate key errors + self._raise_file_exists(self._file["_id"]) + result = {"writeErrors": write_errors} + wces = exc.details["writeConcernErrors"] + if wces: + result["writeConcernError"] = wces[-1] + _check_write_command_response(result) + raise + self._buffered_docs = [] + self._buffered_docs_size = 0 + self._chunk_number += 1 + self._position += len(data) + + def __flush_buffer(self, force: bool = False) -> None: + """Flush the buffer contents out to a chunk.""" + self.__flush_data(self._buffer.getvalue(), force=force) + self._buffer.close() + self._buffer = io.BytesIO() + + def __flush(self) -> Any: + """Flush the file to the database.""" + try: + self.__flush_buffer(force=True) + # The GridFS spec says length SHOULD be an Int64. + self._file["length"] = Int64(self._position) + self._file["uploadDate"] = datetime.datetime.now(tz=datetime.timezone.utc) + + return self._coll.files.insert_one(self._file, session=self._session) + except DuplicateKeyError: + self._raise_file_exists(self._id) + + def _raise_file_exists(self, file_id: Any) -> NoReturn: + """Raise a FileExists exception for the given file_id.""" + raise FileExists("file with _id %r already exists" % file_id) + + def close(self) -> None: + """Flush the file and close it. + + A closed file cannot be written any more. Calling + :meth:`close` more than once is allowed. + """ + if not self._closed: + self.__flush() + object.__setattr__(self, "_closed", True) + + def read(self, size: int = -1) -> NoReturn: + raise io.UnsupportedOperation("read") + + def readable(self) -> bool: + return False + + def seekable(self) -> bool: + return False + + def write(self, data: Any) -> None: + """Write data to the file. There is no return value. + + `data` can be either a string of bytes or a file-like object + (implementing :meth:`read`). If the file has an + :attr:`encoding` attribute, `data` can also be a + :class:`str` instance, which will be encoded as + :attr:`encoding` before being written. + + Due to buffering, the data may not actually be written to the + database until the :meth:`close` method is called. Raises + :class:`ValueError` if this file is already closed. Raises + :class:`TypeError` if `data` is not an instance of + :class:`bytes`, a file-like object, or an instance of :class:`str`. + Unicode data is only allowed if the file has an :attr:`encoding` + attribute. + + :param data: string of bytes or file-like object to be written + to the file + """ + if self._closed: + raise ValueError("cannot write to a closed file") + + try: + # file-like + read = data.read + except AttributeError: + # string + if not isinstance(data, (str, bytes)): + raise TypeError("can only write strings or file-like objects") from None + if isinstance(data, str): + try: + data = data.encode(self.encoding) + except AttributeError: + raise TypeError( + "must specify an encoding for file in order to write str" + ) from None + read = io.BytesIO(data).read + + if self._buffer.tell() > 0: + # Make sure to flush only when _buffer is complete + space = self.chunk_size - self._buffer.tell() + if space: + try: + to_write = read(space) + except BaseException: + self.abort() + raise + self._buffer.write(to_write) + if len(to_write) < space: + return # EOF or incomplete + self.__flush_buffer() + to_write = read(self.chunk_size) + while to_write and len(to_write) == self.chunk_size: + self.__flush_data(to_write) + to_write = read(self.chunk_size) + self._buffer.write(to_write) + + def writelines(self, sequence: Iterable[Any]) -> None: + """Write a sequence of strings to the file. + + Does not add separators. + """ + for line in sequence: + self.write(line) + + def writeable(self) -> bool: + return True + + def __enter__(self) -> GridIn: + """Support for the context manager protocol.""" + return self + + def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> Any: + """Support for the context manager protocol. + + Close the file if no exceptions occur and allow exceptions to propagate. + """ + if exc_type is None: + # No exceptions happened. + self.close() + else: + # Something happened, at minimum mark as closed. + object.__setattr__(self, "_closed", True) + + # propagate exceptions + return False + + +class GridOut(io.IOBase): + """Class to read data out of GridFS.""" + + def __init__( + self, + root_collection: Collection, + file_id: Optional[int] = None, + file_document: Optional[Any] = None, + session: Optional[ClientSession] = None, + ) -> None: + """Read a file from GridFS + + Application developers should generally not need to + instantiate this class directly - instead see the methods + provided by :class:`~gridfs.GridFS`. + + Either `file_id` or `file_document` must be specified, + `file_document` will be given priority if present. Raises + :class:`TypeError` if `root_collection` is not an instance of + :class:`~pymongo.collection.Collection`. + + :param root_collection: root collection to read from + :param file_id: value of ``"_id"`` for the file to read + :param file_document: file document from + `root_collection.files` + :param session: a + :class:`~pymongo.client_session.ClientSession` to use for all + commands + + .. versionchanged:: 3.8 + For better performance and to better follow the GridFS spec, + :class:`GridOut` now uses a single cursor to read all the chunks in + the file. + + .. versionchanged:: 3.6 + Added ``session`` parameter. + + .. versionchanged:: 3.0 + Creating a GridOut does not immediately retrieve the file metadata + from the server. Metadata is fetched when first needed. + """ + if not isinstance(root_collection, Collection): + raise TypeError("root_collection must be an instance of Collection") + _disallow_transactions(session) + + root_collection = _clear_entity_type_registry(root_collection) + + super().__init__() + + self.__chunks = root_collection.chunks + self.__files = root_collection.files + self.__file_id = file_id + self.__buffer = EMPTY + # Start position within the current buffered chunk. + self.__buffer_pos = 0 + self.__chunk_iter = None + # Position within the total file. + self.__position = 0 + self._file = file_document + self._session = session + + _id: Any = _grid_out_property("_id", "The ``'_id'`` value for this file.") + filename: str = _grid_out_property("filename", "Name of this file.") + name: str = _grid_out_property("filename", "Alias for `filename`.") + content_type: Optional[str] = _grid_out_property( + "contentType", "DEPRECATED, will be removed in PyMongo 5.0. Mime-type for this file." + ) + length: int = _grid_out_property("length", "Length (in bytes) of this file.") + chunk_size: int = _grid_out_property("chunkSize", "Chunk size for this file.") + upload_date: datetime.datetime = _grid_out_property( + "uploadDate", "Date that this file was first uploaded." + ) + aliases: Optional[list[str]] = _grid_out_property( + "aliases", "DEPRECATED, will be removed in PyMongo 5.0. List of aliases for this file." + ) + metadata: Optional[Mapping[str, Any]] = _grid_out_property( + "metadata", "Metadata attached to this file." + ) + md5: Optional[str] = _grid_out_property( + "md5", + "DEPRECATED, will be removed in PyMongo 5.0. MD5 of the contents of this file if an md5 sum was created.", + ) + + _file: Any + __chunk_iter: Any + + def _ensure_file(self) -> None: + if not self._file: + _disallow_transactions(self._session) + self._file = self.__files.find_one({"_id": self.__file_id}, session=self._session) + if not self._file: + raise NoFile( + f"no file in gridfs collection {self.__files!r} with _id {self.__file_id!r}" + ) + + def __getattr__(self, name: str) -> Any: + self._ensure_file() + if name in self._file: + return self._file[name] + raise AttributeError("GridOut object has no attribute '%s'" % name) + + def readable(self) -> bool: + return True + + def readchunk(self) -> bytes: + """Reads a chunk at a time. If the current position is within a + chunk the remainder of the chunk is returned. + """ + received = len(self.__buffer) - self.__buffer_pos + chunk_data = EMPTY + chunk_size = int(self.chunk_size) + + if received > 0: + chunk_data = self.__buffer[self.__buffer_pos :] + elif self.__position < int(self.length): + chunk_number = int((received + self.__position) / chunk_size) + if self.__chunk_iter is None: + self.__chunk_iter = _GridOutChunkIterator( + self, self.__chunks, self._session, chunk_number + ) + + chunk = self.__chunk_iter.next() + chunk_data = chunk["data"][self.__position % chunk_size :] + + if not chunk_data: + raise CorruptGridFile("truncated chunk") + + self.__position += len(chunk_data) + self.__buffer = EMPTY + self.__buffer_pos = 0 + return chunk_data + + def _read_size_or_line(self, size: int = -1, line: bool = False) -> bytes: + """Internal read() and readline() helper.""" + self._ensure_file() + remainder = int(self.length) - self.__position + if size < 0 or size > remainder: + size = remainder + + if size == 0: + return EMPTY + + received = 0 + data = [] + while received < size: + needed = size - received + if self.__buffer: + # Optimization: Read the buffer with zero byte copies. + buf = self.__buffer + chunk_start = self.__buffer_pos + chunk_data = memoryview(buf)[self.__buffer_pos :] + self.__buffer = EMPTY + self.__buffer_pos = 0 + self.__position += len(chunk_data) + else: + buf = self.readchunk() + chunk_start = 0 + chunk_data = memoryview(buf) + if line: + pos = buf.find(NEWLN, chunk_start, chunk_start + needed) - chunk_start + if pos >= 0: + # Decrease size to exit the loop. + size = received + pos + 1 + needed = pos + 1 + if len(chunk_data) > needed: + data.append(chunk_data[:needed]) + # Optimization: Save the buffer with zero byte copies. + self.__buffer = buf + self.__buffer_pos = chunk_start + needed + self.__position -= len(self.__buffer) - self.__buffer_pos + else: + data.append(chunk_data) + received += len(chunk_data) + + # Detect extra chunks after reading the entire file. + if size == remainder and self.__chunk_iter: + try: + self.__chunk_iter.next() + except StopIteration: + pass + + return b"".join(data) + + def read(self, size: int = -1) -> bytes: + """Read at most `size` bytes from the file (less if there + isn't enough data). + + The bytes are returned as an instance of :class:`bytes` + If `size` is negative or omitted all data is read. + + :param size: the number of bytes to read + + .. versionchanged:: 3.8 + This method now only checks for extra chunks after reading the + entire file. Previously, this method would check for extra chunks + on every call. + """ + return self._read_size_or_line(size=size) + + def readline(self, size: int = -1) -> bytes: # type: ignore[override] + """Read one line or up to `size` bytes from the file. + + :param size: the maximum number of bytes to read + """ + return self._read_size_or_line(size=size, line=True) + + def tell(self) -> int: + """Return the current position of this file.""" + return self.__position + + def seek(self, pos: int, whence: int = _SEEK_SET) -> int: + """Set the current position of this file. + + :param pos: the position (or offset if using relative + positioning) to seek to + :param whence: where to seek + from. :attr:`os.SEEK_SET` (``0``) for absolute file + positioning, :attr:`os.SEEK_CUR` (``1``) to seek relative + to the current position, :attr:`os.SEEK_END` (``2``) to + seek relative to the file's end. + + .. versionchanged:: 4.1 + The method now returns the new position in the file, to + conform to the behavior of :meth:`io.IOBase.seek`. + """ + if whence == _SEEK_SET: + new_pos = pos + elif whence == _SEEK_CUR: + new_pos = self.__position + pos + elif whence == _SEEK_END: + new_pos = int(self.length) + pos + else: + raise OSError(22, "Invalid value for `whence`") + + if new_pos < 0: + raise OSError(22, "Invalid value for `pos` - must be positive") + + # Optimization, continue using the same buffer and chunk iterator. + if new_pos == self.__position: + return new_pos + + self.__position = new_pos + self.__buffer = EMPTY + self.__buffer_pos = 0 + if self.__chunk_iter: + self.__chunk_iter.close() + self.__chunk_iter = None + return new_pos + + def seekable(self) -> bool: + return True + + def __iter__(self) -> GridOut: + """Return an iterator over all of this file's data. + + The iterator will return lines (delimited by ``b'\\n'``) of + :class:`bytes`. This can be useful when serving files + using a webserver that handles such an iterator efficiently. + + .. versionchanged:: 3.8 + The iterator now raises :class:`CorruptGridFile` when encountering + any truncated, missing, or extra chunk in a file. The previous + behavior was to only raise :class:`CorruptGridFile` on a missing + chunk. + + .. versionchanged:: 4.0 + The iterator now iterates over *lines* in the file, instead + of chunks, to conform to the base class :py:class:`io.IOBase`. + Use :meth:`GridOut.readchunk` to read chunk by chunk instead + of line by line. + """ + return self + + def close(self) -> None: + """Make GridOut more generically file-like.""" + if self.__chunk_iter: + self.__chunk_iter.close() + self.__chunk_iter = None + super().close() + + def write(self, value: Any) -> NoReturn: + raise io.UnsupportedOperation("write") + + def writelines(self, lines: Any) -> NoReturn: + raise io.UnsupportedOperation("writelines") + + def writable(self) -> bool: + return False + + def __enter__(self) -> GridOut: + """Makes it possible to use :class:`GridOut` files + with the context manager protocol. + """ + return self + + def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> Any: + """Makes it possible to use :class:`GridOut` files + with the context manager protocol. + """ + self.close() + return False + + def fileno(self) -> NoReturn: + raise io.UnsupportedOperation("fileno") + + def flush(self) -> None: + # GridOut is read-only, so flush does nothing. + pass + + def isatty(self) -> bool: + return False + + def truncate(self, size: Optional[int] = None) -> NoReturn: + # See https://docs.python.org/3/library/io.html#io.IOBase.writable + # for why truncate has to raise. + raise io.UnsupportedOperation("truncate") + + # Override IOBase.__del__ otherwise it will lead to __getattr__ on + # __IOBase_closed which calls _ensure_file and potentially performs I/O. + # We cannot do I/O in __del__ since it can lead to a deadlock. + def __del__(self) -> None: + pass + + +class _GridOutChunkIterator: + """Iterates over a file's chunks using a single cursor. + + Raises CorruptGridFile when encountering any truncated, missing, or extra + chunk in a file. + """ + + def __init__( + self, + grid_out: GridOut, + chunks: Collection, + session: Optional[ClientSession], + next_chunk: Any, + ) -> None: + self._id = grid_out._id + self._chunk_size = int(grid_out.chunk_size) + self._length = int(grid_out.length) + self._chunks = chunks + self._session = session + self._next_chunk = next_chunk + self._num_chunks = math.ceil(float(self._length) / self._chunk_size) + self._cursor = None + + _cursor: Optional[Cursor] + + def expected_chunk_length(self, chunk_n: int) -> int: + if chunk_n < self._num_chunks - 1: + return self._chunk_size + return self._length - (self._chunk_size * (self._num_chunks - 1)) + + def __iter__(self) -> _GridOutChunkIterator: + return self + + def _create_cursor(self) -> None: + filter = {"files_id": self._id} + if self._next_chunk > 0: + filter["n"] = {"$gte": self._next_chunk} + _disallow_transactions(self._session) + self._cursor = self._chunks.find(filter, sort=[("n", 1)], session=self._session) + + def _next_with_retry(self) -> Mapping[str, Any]: + """Return the next chunk and retry once on CursorNotFound. + + We retry on CursorNotFound to maintain backwards compatibility in + cases where two calls to read occur more than 10 minutes apart (the + server's default cursor timeout). + """ + if self._cursor is None: + self._create_cursor() + assert self._cursor is not None + try: + return self._cursor.next() + except CursorNotFound: + self._cursor.close() + self._create_cursor() + return self._cursor.next() + + def next(self) -> Mapping[str, Any]: + try: + chunk = self._next_with_retry() + except StopIteration: + if self._next_chunk >= self._num_chunks: + raise + raise CorruptGridFile("no chunk #%d" % self._next_chunk) from None + + if chunk["n"] != self._next_chunk: + self.close() + raise CorruptGridFile( + "Missing chunk: expected chunk #%d but found " + "chunk with n=%d" % (self._next_chunk, chunk["n"]) + ) + + if chunk["n"] >= self._num_chunks: + # According to spec, ignore extra chunks if they are empty. + if len(chunk["data"]): + self.close() + raise CorruptGridFile( + "Extra chunk found: expected %d chunks but found " + "chunk with n=%d" % (self._num_chunks, chunk["n"]) + ) + + expected_length = self.expected_chunk_length(chunk["n"]) + if len(chunk["data"]) != expected_length: + self.close() + raise CorruptGridFile( + "truncated chunk #%d: expected chunk length to be %d but " + "found chunk with length %d" % (chunk["n"], expected_length, len(chunk["data"])) + ) + + self._next_chunk += 1 + return chunk + + __next__ = next + + def close(self) -> None: + if self._cursor: + self._cursor.close() + self._cursor = None + + +class GridOutIterator: + def __init__(self, grid_out: GridOut, chunks: Collection, session: ClientSession): + self.__chunk_iter = _GridOutChunkIterator(grid_out, chunks, session, 0) + + def __iter__(self) -> GridOutIterator: + return self + + def next(self) -> bytes: + chunk = self.__chunk_iter.next() + return bytes(chunk["data"]) + + __next__ = next + + +class GridOutCursor(Cursor): + """A cursor / iterator for returning GridOut objects as the result + of an arbitrary query against the GridFS files collection. + """ + + def __init__( + self, + collection: Collection, + filter: Optional[Mapping[str, Any]] = None, + skip: int = 0, + limit: int = 0, + no_cursor_timeout: bool = False, + sort: Optional[Any] = None, + batch_size: int = 0, + session: Optional[ClientSession] = None, + ) -> None: + """Create a new cursor, similar to the normal + :class:`~pymongo.cursor.Cursor`. + + Should not be called directly by application developers - see + the :class:`~gridfs.GridFS` method :meth:`~gridfs.GridFS.find` instead. + + .. versionadded 2.7 + + .. seealso:: The MongoDB documentation on `cursors `_. + """ + _disallow_transactions(session) + collection = _clear_entity_type_registry(collection) + + # Hold on to the base "fs" collection to create GridOut objects later. + self.__root_collection = collection + + super().__init__( + collection.files, + filter, + skip=skip, + limit=limit, + no_cursor_timeout=no_cursor_timeout, + sort=sort, + batch_size=batch_size, + session=session, + ) + + def next(self) -> GridOut: + """Get next GridOut object from cursor.""" + _disallow_transactions(self.session) + next_file = super().next() + return GridOut(self.__root_collection, file_document=next_file, session=self.session) + + __next__ = next + + def add_option(self, *args: Any, **kwargs: Any) -> NoReturn: + raise NotImplementedError("Method does not exist for GridOutCursor") + + def remove_option(self, *args: Any, **kwargs: Any) -> NoReturn: + raise NotImplementedError("Method does not exist for GridOutCursor") + + def _clone_base(self, session: Optional[ClientSession]) -> GridOutCursor: + """Creates an empty GridOutCursor for information to be copied into.""" + return GridOutCursor(self.__root_collection, session=session) diff --git a/venv/Lib/site-packages/gridfs/py.typed b/venv/Lib/site-packages/gridfs/py.typed new file mode 100644 index 00000000..0f405706 --- /dev/null +++ b/venv/Lib/site-packages/gridfs/py.typed @@ -0,0 +1,2 @@ +# PEP-561 Support File. +# "Package maintainers who wish to support type checking of their code MUST add a marker file named py.typed to their package supporting typing". diff --git a/venv/Lib/site-packages/pymongo-4.7.2.dist-info/INSTALLER b/venv/Lib/site-packages/pymongo-4.7.2.dist-info/INSTALLER new file mode 100644 index 00000000..a1b589e3 --- /dev/null +++ b/venv/Lib/site-packages/pymongo-4.7.2.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/venv/Lib/site-packages/pymongo-4.7.2.dist-info/LICENSE b/venv/Lib/site-packages/pymongo-4.7.2.dist-info/LICENSE new file mode 100644 index 00000000..261eeb9e --- /dev/null +++ b/venv/Lib/site-packages/pymongo-4.7.2.dist-info/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. diff --git a/venv/Lib/site-packages/pymongo-4.7.2.dist-info/METADATA b/venv/Lib/site-packages/pymongo-4.7.2.dist-info/METADATA new file mode 100644 index 00000000..da000892 --- /dev/null +++ b/venv/Lib/site-packages/pymongo-4.7.2.dist-info/METADATA @@ -0,0 +1,485 @@ +Metadata-Version: 2.1 +Name: pymongo +Version: 4.7.2 +Summary: Python driver for MongoDB +Author: The MongoDB Python Team +License: Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. + +Project-URL: Homepage, https://www.mongodb.org +Project-URL: Documentation, https://pymongo.readthedocs.io +Project-URL: Source, https://github.com/mongodb/mongo-python-driver +Project-URL: Tracker, https://jira.mongodb.org/projects/PYTHON/issues +Keywords: bson,gridfs,mongo,mongodb,pymongo +Classifier: Development Status :: 5 - Production/Stable +Classifier: Intended Audience :: Developers +Classifier: License :: OSI Approved :: Apache Software License +Classifier: Operating System :: MacOS :: MacOS X +Classifier: Operating System :: Microsoft :: Windows +Classifier: Operating System :: POSIX +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.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: Topic :: Database +Classifier: Typing :: Typed +Requires-Python: >=3.7 +Description-Content-Type: text/markdown +License-File: LICENSE +Requires-Dist: dnspython <3.0.0,>=1.16.0 +Provides-Extra: aws +Requires-Dist: pymongo-auth-aws <2.0.0,>=1.1.0 ; extra == 'aws' +Provides-Extra: encryption +Requires-Dist: pymongo-auth-aws <2.0.0,>=1.1.0 ; extra == 'encryption' +Requires-Dist: pymongocrypt <2.0.0,>=1.6.0 ; extra == 'encryption' +Requires-Dist: certifi ; (os_name == "nt" or sys_platform == "darwin") and extra == 'encryption' +Provides-Extra: gssapi +Requires-Dist: pykerberos ; (os_name != "nt") and extra == 'gssapi' +Requires-Dist: winkerberos >=0.5.0 ; (os_name == "nt") and extra == 'gssapi' +Provides-Extra: ocsp +Requires-Dist: pyopenssl >=17.2.0 ; extra == 'ocsp' +Requires-Dist: requests <3.0.0 ; extra == 'ocsp' +Requires-Dist: cryptography >=2.5 ; extra == 'ocsp' +Requires-Dist: service-identity >=18.1.0 ; extra == 'ocsp' +Requires-Dist: certifi ; (os_name == "nt" or sys_platform == "darwin") and extra == 'ocsp' +Provides-Extra: snappy +Requires-Dist: python-snappy ; extra == 'snappy' +Provides-Extra: srv +Provides-Extra: test +Requires-Dist: pytest >=7 ; extra == 'test' +Provides-Extra: tls +Provides-Extra: zstd +Requires-Dist: zstandard ; extra == 'zstd' + +# PyMongo + +[![PyPI Version](https://img.shields.io/pypi/v/pymongo)](https://pypi.org/project/pymongo) +[![Python Versions](https://img.shields.io/pypi/pyversions/pymongo)](https://pypi.org/project/pymongo) +[![Monthly Downloads](https://static.pepy.tech/badge/pymongo/month)](https://pepy.tech/project/pymongo) +[![Documentation Status](https://readthedocs.org/projects/pymongo/badge/?version=stable)](http://pymongo.readthedocs.io/en/stable/?badge=stable) + +## About + +The PyMongo distribution contains tools for interacting with MongoDB +database from Python. The `bson` package is an implementation of the +[BSON format](http://bsonspec.org) for Python. The `pymongo` package is +a native Python driver for MongoDB. The `gridfs` package is a +[gridfs](https://github.com/mongodb/specifications/blob/master/source/gridfs/gridfs-spec.rst/) +implementation on top of `pymongo`. + +PyMongo supports MongoDB 3.6, 4.0, 4.2, 4.4, 5.0, 6.0, and 7.0. + +## Support / Feedback + +For issues with, questions about, or feedback for PyMongo, please look +into our [support channels](https://support.mongodb.com/welcome). Please +do not email any of the PyMongo developers directly with issues or +questions - you're more likely to get an answer on +[StackOverflow](https://stackoverflow.com/questions/tagged/mongodb) +(using a "mongodb" tag). + +## Bugs / Feature Requests + +Think you've found a bug? Want to see a new feature in PyMongo? Please +open a case in our issue management tool, JIRA: + +- [Create an account and login](https://jira.mongodb.org). +- Navigate to [the PYTHON + project](https://jira.mongodb.org/browse/PYTHON). +- Click **Create Issue** - Please provide as much information as + possible about the issue type and how to reproduce it. + +Bug reports in JIRA for all driver projects (i.e. PYTHON, CSHARP, JAVA) +and the Core Server (i.e. SERVER) project are **public**. + +### How To Ask For Help + +Please include all of the following information when opening an issue: + +- Detailed steps to reproduce the problem, including full traceback, + if possible. + +- The exact python version used, with patch level: + +```bash +python -c "import sys; print(sys.version)" +``` + +- The exact version of PyMongo used, with patch level: + +```bash +python -c "import pymongo; print(pymongo.version); print(pymongo.has_c())" +``` + +- The operating system and version (e.g. Windows 7, OSX 10.8, ...) + +- Web framework or asynchronous network library used, if any, with + version (e.g. Django 1.7, mod_wsgi 4.3.0, gevent 1.0.1, Tornado + 4.0.2, ...) + +### Security Vulnerabilities + +If you've identified a security vulnerability in a driver or any other +MongoDB project, please report it according to the [instructions +here](https://www.mongodb.com/docs/manual/tutorial/create-a-vulnerability-report/). + +## Installation + +PyMongo can be installed with [pip](http://pypi.python.org/pypi/pip): + +```bash +python -m pip install pymongo +``` + +Or `easy_install` from [setuptools](http://pypi.python.org/pypi/setuptools): + +```bash +python -m easy_install pymongo +``` + +You can also download the project source and do: + +```bash +pip install . +``` + +Do **not** install the "bson" package from pypi. PyMongo comes with +its own bson package; running "pip install bson" installs a third-party +package that is incompatible with PyMongo. + +## Dependencies + +PyMongo supports CPython 3.7+ and PyPy3.7+. + +Required dependencies: + +Support for `mongodb+srv://` URIs requires [dnspython](https://pypi.python.org/pypi/dnspython) + +Optional dependencies: + +GSSAPI authentication requires +[pykerberos](https://pypi.python.org/pypi/pykerberos) on Unix or +[WinKerberos](https://pypi.python.org/pypi/winkerberos) on Windows. The +correct dependency can be installed automatically along with PyMongo: + +```bash +python -m pip install "pymongo[gssapi]" +``` + +MONGODB-AWS authentication requires +[pymongo-auth-aws](https://pypi.org/project/pymongo-auth-aws/): + +```bash +python -m pip install "pymongo[aws]" +``` + +OCSP (Online Certificate Status Protocol) requires +[PyOpenSSL](https://pypi.org/project/pyOpenSSL/), +[requests](https://pypi.org/project/requests/), +[service_identity](https://pypi.org/project/service_identity/) and may +require [certifi](https://pypi.python.org/pypi/certifi): + +```bash +python -m pip install "pymongo[ocsp]" +``` + +Wire protocol compression with snappy requires +[python-snappy](https://pypi.org/project/python-snappy): + +```bash +python -m pip install "pymongo[snappy]" +``` + +Wire protocol compression with zstandard requires +[zstandard](https://pypi.org/project/zstandard): + +```bash +python -m pip install "pymongo[zstd]" +``` + +Client-Side Field Level Encryption requires +[pymongocrypt](https://pypi.org/project/pymongocrypt/) and +[pymongo-auth-aws](https://pypi.org/project/pymongo-auth-aws/): + +```bash +python -m pip install "pymongo[encryption]" +``` +You can install all dependencies automatically with the following +command: + +```bash +python -m pip install "pymongo[gssapi,aws,ocsp,snappy,zstd,encryption]" +``` + +Additional dependencies are: + +- (to generate documentation or run tests) + [tox](https://tox.wiki/en/latest/index.html) + +## Examples + +Here's a basic example (for more see the *examples* section of the +docs): + +```pycon +>>> import pymongo +>>> client = pymongo.MongoClient("localhost", 27017) +>>> db = client.test +>>> db.name +'test' +>>> db.my_collection +Collection(Database(MongoClient('localhost', 27017), 'test'), 'my_collection') +>>> db.my_collection.insert_one({"x": 10}).inserted_id +ObjectId('4aba15ebe23f6b53b0000000') +>>> db.my_collection.insert_one({"x": 8}).inserted_id +ObjectId('4aba160ee23f6b543e000000') +>>> db.my_collection.insert_one({"x": 11}).inserted_id +ObjectId('4aba160ee23f6b543e000002') +>>> db.my_collection.find_one() +{'x': 10, '_id': ObjectId('4aba15ebe23f6b53b0000000')} +>>> for item in db.my_collection.find(): +... print(item["x"]) +... +10 +8 +11 +>>> db.my_collection.create_index("x") +'x_1' +>>> for item in db.my_collection.find().sort("x", pymongo.ASCENDING): +... print(item["x"]) +... +8 +10 +11 +>>> [item["x"] for item in db.my_collection.find().limit(2).skip(1)] +[8, 11] +``` + +## Documentation + +Documentation is available at +[pymongo.readthedocs.io](https://pymongo.readthedocs.io/en/stable/). + +Documentation can be generated by running **tox -m doc**. Generated +documentation can be found in the `doc/build/html/` directory. + +## Learning Resources + +- MongoDB Learn - [Python +courses](https://learn.mongodb.com/catalog?labels=%5B%22Language%22%5D&values=%5B%22Python%22%5D). +- [Python Articles on Developer +Center](https://www.mongodb.com/developer/languages/python/). + +## Testing + +The easiest way to run the tests is to run **tox -m test** in the root +of the distribution. For example, + +```bash +tox -e test +``` diff --git a/venv/Lib/site-packages/pymongo-4.7.2.dist-info/RECORD b/venv/Lib/site-packages/pymongo-4.7.2.dist-info/RECORD new file mode 100644 index 00000000..b13ab34c --- /dev/null +++ b/venv/Lib/site-packages/pymongo-4.7.2.dist-info/RECORD @@ -0,0 +1,194 @@ +bson/__init__.py,sha256=hHfukHTrBEVUPnw0Qxs2F-7IpRD64Ita0MYYBMyv8nM,51326 +bson/__pycache__/__init__.cpython-312.pyc,, +bson/__pycache__/_helpers.cpython-312.pyc,, +bson/__pycache__/binary.cpython-312.pyc,, +bson/__pycache__/code.cpython-312.pyc,, +bson/__pycache__/codec_options.cpython-312.pyc,, +bson/__pycache__/datetime_ms.cpython-312.pyc,, +bson/__pycache__/dbref.cpython-312.pyc,, +bson/__pycache__/decimal128.cpython-312.pyc,, +bson/__pycache__/errors.cpython-312.pyc,, +bson/__pycache__/int64.cpython-312.pyc,, +bson/__pycache__/json_util.cpython-312.pyc,, +bson/__pycache__/max_key.cpython-312.pyc,, +bson/__pycache__/min_key.cpython-312.pyc,, +bson/__pycache__/objectid.cpython-312.pyc,, +bson/__pycache__/raw_bson.cpython-312.pyc,, +bson/__pycache__/regex.cpython-312.pyc,, +bson/__pycache__/son.cpython-312.pyc,, +bson/__pycache__/timestamp.cpython-312.pyc,, +bson/__pycache__/typings.cpython-312.pyc,, +bson/__pycache__/tz_util.cpython-312.pyc,, +bson/_cbson.cp312-win_amd64.pyd,sha256=s3QJ5f3_AzDJSJvJUbREExnjRuHRpv2NmszSuF_3dGk,46080 +bson/_cbsonmodule.c,sha256=BFegVXVaYhj1HnZ61GPGFjtAbNsFpl9iRVLU51jTfjE,105766 +bson/_cbsonmodule.h,sha256=va4oA4jASv494cr68ewG6kXDWFX4C6wVm5dUryCtSVU,8263 +bson/_helpers.py,sha256=MqOBDkyRx78lR1RYii4kNZcifMNPIEYceWSKIaU7f-w,1409 +bson/binary.py,sha256=H68Zpp9wxQkUShNOG3z3RDIj7PR7JbelOSHHw1Jfojg,12703 +bson/bson-endian.h,sha256=RoU1Fkefn_pUMdmCIG13o-hoIwFtj2qZnopOzJgXZM8,6806 +bson/buffer.c,sha256=b_r58Ua6ad6nTuQh9WCWm9-0SUY3__QCZ4P4aus9x_Y,4607 +bson/buffer.h,sha256=bLqJy7Jxdl_TeJUljzB0Up2fuBvyvUH1CbhpoU8NBok,1879 +bson/code.py,sha256=_Q1_tzdHdWzp3QWT-53xmYX9j4tmA4yj0uxGoXsVZk4,3533 +bson/codec_options.py,sha256=rrYBl0gHlaTFlybPCSOcboFG-wJyzsR4U3NKCWqmIQQ,20156 +bson/datetime_ms.py,sha256=Fow-BeU3LMmL8W-bhMMpDwv8BzRdtZQ5CyTOXhShOVY,6732 +bson/dbref.py,sha256=SSnerWSEhsgRnwz95ydZi6kvkMizGseSmFaZS8YPrLQ,4861 +bson/decimal128.py,sha256=F8MxNstyb0hMelanANkbvaBqViZ0smhw9DUBczzpkFM,10525 +bson/errors.py,sha256=buM2qPmur-9_rLKM_xkXVGJw0RkryGSUWhjbKHKhTfI,1205 +bson/int64.py,sha256=uJO22QL4tW0eX8m3lPnpVbZ2NhdodOQlEdd7kSf6NwY,1217 +bson/json_util.py,sha256=LFwl1apeEuZ8mcqZ3U_sZj-VbPI0QaSVMzim7EuVHa8,43794 +bson/max_key.py,sha256=XxkXM0i2eRlIqb8Wt8Z2bJZBd-lY8KKSWaS2aotb5kY,1560 +bson/min_key.py,sha256=35AkN147moOGGi4eMyE8zPeIqQ0aeKCaPvFhk4KAhpw,1560 +bson/objectid.py,sha256=L7WPv1WBbSaixdYG2lGxwieK-MjaD9j5fMc8pTEc2a0,9419 +bson/py.typed,sha256=SEaNgPmH3E8kUVMaKTOYBxODVTUDutfGVGupZE0IkZQ,172 +bson/raw_bson.py,sha256=b0vyQTv98LnulIfu-ArW08q6ZMHXpUtgAYpbsGBfH5o,7497 +bson/regex.py,sha256=eP0mvQi1G4XkV2UwSa8iDC4xxHQjk1yyipihd7GkQj8,4721 +bson/son.py,sha256=GwLUl_4SryoRXnP_WqN1SX4ItSItbSpBj8k9-3JV2Dk,6722 +bson/time64.c,sha256=HBaC09Oz721fxpeFTHm2xR9opxadFDih2LCkPtuXsm8,22308 +bson/time64.h,sha256=RaXMBNtMoFQyaJUijGIXkbM5oLkgGL4l2VSfZGEkhEQ,1628 +bson/time64_config.h,sha256=jOirHsEcXTlAaGF8r2iY5Tgrq0TN3xEtG45uVk8NCmY,1760 +bson/time64_limits.h,sha256=UfzyW78wagp1puS6YjKBDFmPI4gdQGZceHfxMTguqRI,1587 +bson/timestamp.py,sha256=T1LmQDvbTWZMAv8RZgoWpHghCRzRbzVYmKOEc3BtJ0Q,4356 +bson/typings.py,sha256=0zJOM3KQ7oDhbOZwVTosWEfysNIDWITO8Pwjk8Mh4Ek,1169 +bson/tz_util.py,sha256=qlv0MvZox__cI0armrEAaKqDIaf5YIcLeviQA28-VlE,1814 +gridfs/__init__.py,sha256=vW_U2MBgttj4RvXP-XS05sdrYIcRXAV8eap5NoIEqd0,38774 +gridfs/__pycache__/__init__.cpython-312.pyc,, +gridfs/__pycache__/errors.cpython-312.pyc,, +gridfs/__pycache__/grid_file.cpython-312.pyc,, +gridfs/errors.py,sha256=YIIhfEMrQV9SuAS-nNfxQtCxHf_4Bj4_yQ52uL67vaw,1125 +gridfs/grid_file.py,sha256=psYUwIhBYPEqCJc6cI3wo6gJ911zbiuMTe5N7i3MVXk,36921 +gridfs/py.typed,sha256=SEaNgPmH3E8kUVMaKTOYBxODVTUDutfGVGupZE0IkZQ,172 +pymongo-4.7.2.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +pymongo-4.7.2.dist-info/LICENSE,sha256=HrhfyXIkWY2tGFK11kg7vPCqhgh5DcxleloqdhrpyMY,11558 +pymongo-4.7.2.dist-info/METADATA,sha256=uBVZsvS9irxPFB8GqtxPbdPkqdYgFqLzY7CSu7QDnnA,22681 +pymongo-4.7.2.dist-info/RECORD,, +pymongo-4.7.2.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +pymongo-4.7.2.dist-info/WHEEL,sha256=fZWyj_84lK0cA-ZNCsdwhbJl0OTrpWkxInEn424qrSs,102 +pymongo-4.7.2.dist-info/top_level.txt,sha256=OinVojDdOfo1Dsp-NRfrZdp6gcJJ4bPRq61vSg5vyAs,20 +pymongo/__init__.py,sha256=vOV6Mo8Xo3y_ATcLsPEqk1c_iWh_0zVKbH4jfOV65TA,5402 +pymongo/__pycache__/__init__.cpython-312.pyc,, +pymongo/__pycache__/_azure_helpers.cpython-312.pyc,, +pymongo/__pycache__/_csot.cpython-312.pyc,, +pymongo/__pycache__/_gcp_helpers.cpython-312.pyc,, +pymongo/__pycache__/_lazy_import.cpython-312.pyc,, +pymongo/__pycache__/_version.cpython-312.pyc,, +pymongo/__pycache__/aggregation.cpython-312.pyc,, +pymongo/__pycache__/auth.cpython-312.pyc,, +pymongo/__pycache__/auth_aws.cpython-312.pyc,, +pymongo/__pycache__/auth_oidc.cpython-312.pyc,, +pymongo/__pycache__/bulk.cpython-312.pyc,, +pymongo/__pycache__/change_stream.cpython-312.pyc,, +pymongo/__pycache__/client_options.cpython-312.pyc,, +pymongo/__pycache__/client_session.cpython-312.pyc,, +pymongo/__pycache__/collation.cpython-312.pyc,, +pymongo/__pycache__/collection.cpython-312.pyc,, +pymongo/__pycache__/command_cursor.cpython-312.pyc,, +pymongo/__pycache__/common.cpython-312.pyc,, +pymongo/__pycache__/compression_support.cpython-312.pyc,, +pymongo/__pycache__/cursor.cpython-312.pyc,, +pymongo/__pycache__/daemon.cpython-312.pyc,, +pymongo/__pycache__/database.cpython-312.pyc,, +pymongo/__pycache__/driver_info.cpython-312.pyc,, +pymongo/__pycache__/encryption.cpython-312.pyc,, +pymongo/__pycache__/encryption_options.cpython-312.pyc,, +pymongo/__pycache__/errors.cpython-312.pyc,, +pymongo/__pycache__/event_loggers.cpython-312.pyc,, +pymongo/__pycache__/hello.cpython-312.pyc,, +pymongo/__pycache__/helpers.cpython-312.pyc,, +pymongo/__pycache__/lock.cpython-312.pyc,, +pymongo/__pycache__/logger.cpython-312.pyc,, +pymongo/__pycache__/max_staleness_selectors.cpython-312.pyc,, +pymongo/__pycache__/message.cpython-312.pyc,, +pymongo/__pycache__/mongo_client.cpython-312.pyc,, +pymongo/__pycache__/monitor.cpython-312.pyc,, +pymongo/__pycache__/monitoring.cpython-312.pyc,, +pymongo/__pycache__/network.cpython-312.pyc,, +pymongo/__pycache__/ocsp_cache.cpython-312.pyc,, +pymongo/__pycache__/ocsp_support.cpython-312.pyc,, +pymongo/__pycache__/operations.cpython-312.pyc,, +pymongo/__pycache__/periodic_executor.cpython-312.pyc,, +pymongo/__pycache__/pool.cpython-312.pyc,, +pymongo/__pycache__/pyopenssl_context.cpython-312.pyc,, +pymongo/__pycache__/read_concern.cpython-312.pyc,, +pymongo/__pycache__/read_preferences.cpython-312.pyc,, +pymongo/__pycache__/response.cpython-312.pyc,, +pymongo/__pycache__/results.cpython-312.pyc,, +pymongo/__pycache__/saslprep.cpython-312.pyc,, +pymongo/__pycache__/server.cpython-312.pyc,, +pymongo/__pycache__/server_api.cpython-312.pyc,, +pymongo/__pycache__/server_description.cpython-312.pyc,, +pymongo/__pycache__/server_selectors.cpython-312.pyc,, +pymongo/__pycache__/server_type.cpython-312.pyc,, +pymongo/__pycache__/settings.cpython-312.pyc,, +pymongo/__pycache__/socket_checker.cpython-312.pyc,, +pymongo/__pycache__/srv_resolver.cpython-312.pyc,, +pymongo/__pycache__/ssl_context.cpython-312.pyc,, +pymongo/__pycache__/ssl_support.cpython-312.pyc,, +pymongo/__pycache__/topology.cpython-312.pyc,, +pymongo/__pycache__/topology_description.cpython-312.pyc,, +pymongo/__pycache__/typings.cpython-312.pyc,, +pymongo/__pycache__/uri_parser.cpython-312.pyc,, +pymongo/__pycache__/write_concern.cpython-312.pyc,, +pymongo/_azure_helpers.py,sha256=XtnVfYnNlKpzrGGU3k7t1GzIOaKa9JYG46W9O8c9VNk,2062 +pymongo/_cmessage.cp312-win_amd64.pyd,sha256=J4LvTujxC5Uv9QbvSkCJmhgyy21ZhXIm5tU6XVrStTA,56832 +pymongo/_cmessagemodule.c,sha256=y0-IuLinu01Ob43cAATu95HT9Q1lNT2Dzc-v3y_GEz4,33527 +pymongo/_csot.py,sha256=R2qx1vm2486VLBDXoIHZ6kJbissKiVf9FOLncIpSh4s,4810 +pymongo/_gcp_helpers.py,sha256=N2nVDeOJKPiKuUOD4GD8gU6rZfU6eRG15Z5v18-uB48,1487 +pymongo/_lazy_import.py,sha256=BAKpfey3JukKjFoMWpGORlNtmPbfgNmn7nqd5Rfffgg,1596 +pymongo/_version.py,sha256=d1i5IPn8OYjtaympWfdkeHnpW92jPO9EUxF3bWHFRZ0,1031 +pymongo/aggregation.py,sha256=R1fsDf_qVHYsyyTsryZnoE-knLm7U3ndlcL8q_iBPKw,9588 +pymongo/auth.py,sha256=-5JnaEvBOro9IwuZA3rc3W7dpLuCBhVZiipJKiJ2_yg,24973 +pymongo/auth_aws.py,sha256=OYDzwWO3wgw6LwbKBW4QggMmW9e2SpcaGKSuyJA-lnM,3923 +pymongo/auth_oidc.py,sha256=OgyWbtNkRmPXJnFz4iYyCnNMv3wTfc8BEvQnRa1qN0k,14427 +pymongo/bulk.py,sha256=ZNL96EC7MB_AIPjiwkYSCXIqVeiJ-JPC_MbLBDMb8bQ,21840 +pymongo/change_stream.py,sha256=BzCkg2JtGCeS4AuIfPS1QMXXPqKRlU3aY1Rqb6_Jrz0,19186 +pymongo/client_options.py,sha256=8OmbOU4HDiPGsxflouttHQggiSmFyreYHN8PUK7vmAU,12788 +pymongo/client_session.py,sha256=AJ6pPgjZS717XlIYbNkeCqxBjEff4zf07fYkvudmbt4,45652 +pymongo/collation.py,sha256=qEfmvs6u5SdkiAB_0UndlSeownqd-mIpDg3OVtFDxdA,8129 +pymongo/collection.py,sha256=RrTTVsIXfmdznrtvzLY8qaDKphLPwBrswKmIKbhjt2U,143913 +pymongo/command_cursor.py,sha256=EeKLTkmfhULY7TzByMJVIRqin2mCur3GjRmqCHN_Ht0,14737 +pymongo/common.py,sha256=-Z6zZAPeO78jGMlDFIdMx_DMZEPmxvvUw7gYABbEuJc,38365 +pymongo/compression_support.py,sha256=6dF0HGN3BcTyqO6X1N_mfW2m716wVXM7x93xbSjisJU,5378 +pymongo/cursor.py,sha256=F4ptj3gIxg9op4xTb38YEnoVKuwIa0m5u0rhwqW-AIQ,51762 +pymongo/daemon.py,sha256=CKFmDOP47KU9LxazaN3rv6YN2bN_mnNB3qWx1db_ELE,6039 +pymongo/database.py,sha256=oiaiN7aNsKcWfn2scvbrwH2oX3ioy6hzOM3SRQc85TM,57266 +pymongo/driver_info.py,sha256=yc4DzOrIzSD9-HGvvzy6wWqQ1oxaapk5cH4y3sxL51g,1747 +pymongo/encryption.py,sha256=vgKjPRNET9nKTlemqzhX1H_f-6jAweW_jU9ccTr2cK8,45695 +pymongo/encryption_options.py,sha256=HJaohNu8FGOkNQxFjiFGOmBOH_na6ARFNMAn0nJ8y7o,13277 +pymongo/errors.py,sha256=2ntDbN7SV5bbLH5WwEDL4pLbtuWCujmF5yspyEhqav0,12034 +pymongo/event_loggers.py,sha256=TLA8rLQdvZ5SmaSFQGZnRY7CM-6TuDjnjYf8HIjmwEU,9356 +pymongo/hello.py,sha256=zWh_dwooFhgIYSz4rmGCoflUF1jqyKbUTUBkHACsd7w,6936 +pymongo/helpers.py,sha256=XjTdc_3IT41boldhUi0CShXhqMY1goXY3ksfz4aSoY4,11983 +pymongo/lock.py,sha256=atG82ip9uANlz9ZNU9quQoaVQuQTOOd17JWDgJbBBAU,1317 +pymongo/logger.py,sha256=F1_MXxc4qApoMNVZZeBWcS3mI8qMaALfvNO49oa2U5w,6536 +pymongo/max_staleness_selectors.py,sha256=NIjaxeoXSI-I-MTw_AGm3SscgdUe7VTMJBDCsHsVXjE,4795 +pymongo/message.py,sha256=Ab-1RJCHklpKIwyJsONjQ3f-RvR8bTAYGgrTDRMCMns,60690 +pymongo/mongo_client.py,sha256=MhIs4JRiwoeyD1IZf21zaWr8UH0O99rcIWXaz3_-a1s,111914 +pymongo/monitor.py,sha256=3r-opKIYC9KndGxOuBAWLFeDnnpAPCcvhnWHRpwBnys,18125 +pymongo/monitoring.py,sha256=ZttTuK3M3_RblVF-SQKen5fP0E9jAKwqHH0KmvX4PV8,66499 +pymongo/network.py,sha256=TOS1vw1MYgldse0DKGSSDAtFovD184L1HeWXAAFMmeI,16523 +pymongo/ocsp_cache.py,sha256=auQMBkffkewFDCRjJv9SQ8tvb1iLcu3H17ewr7DbVfc,3946 +pymongo/ocsp_support.py,sha256=Wr2qDb1h-_lPTGOBfaxiXCu3QBs3MSwIkH8w8dI8X5A,18233 +pymongo/operations.py,sha256=zr28SZQjPGrCtm33tpI6HSOKTfh67NZDGHs0FirGwEI,22543 +pymongo/periodic_executor.py,sha256=_u_coW3VpfV6IdptWCJMdOPYlGFxh7RNakXvhWvbTUE,6905 +pymongo/pool.py,sha256=bll23VpjGi1Z0UVfPn2Wywp_72I3RVibuD3oJsMzrrA,84845 +pymongo/py.typed,sha256=SEaNgPmH3E8kUVMaKTOYBxODVTUDutfGVGupZE0IkZQ,172 +pymongo/pyopenssl_context.py,sha256=xsL8JbrzpVJMjqN5rYsP6jUh-BBb2RyzjuaE-6e_tM4,17050 +pymongo/read_concern.py,sha256=U68-UKvYXIl7l9t-EgyKuCbUpslLsxvkl3kxJjWMiBM,2488 +pymongo/read_preferences.py,sha256=GQxqa0T337wkYGN0eIAc4S3C-LVxPKUvTWy8p81ATUA,21995 +pymongo/response.py,sha256=Q1uF4jtcVM1AgPNQnjYV7Bn9qvjzL2dZIGAayrrjw4E,4424 +pymongo/results.py,sha256=SmIJ9pZIhJ_qpHXCghVWl7DSmLhrKDtulYm3-JVoQMU,8757 +pymongo/saslprep.py,sha256=VIfmL_K5ml0BDZ1vSb7DrnGoqAzFKdWHAjTzvm69UlI,4499 +pymongo/server.py,sha256=jw6sOb4rUE8uGOy7fZzwH7ZX4MCSgBM0zUM0gtd7RyY,13194 +pymongo/server_api.py,sha256=4t_Ob9AFZnAx6EPsTyXC8iWlUtZrHcRMXvi8weMjfpM,6248 +pymongo/server_description.py,sha256=LROXcelRbnwXnw17e97tMRPvVVpG81AQ6ASteSuLmV0,9905 +pymongo/server_selectors.py,sha256=hTD9FsvpPXy5bZtm0ACt52TOUtTTlsvDHqHSkEVsPc8,6252 +pymongo/server_type.py,sha256=Iz7XtKaIyxm2TbXYMBqFD7FkEnxvo3p-IOUFqYh4iSc,956 +pymongo/settings.py,sha256=oIHdlJNLnKHImqChYLjpZTdkY5qcWHg8I6VyHA8DhUI,6249 +pymongo/socket_checker.py,sha256=mjM1ImCkdMl6BEho5q5sXsadoI1fIKTZJODseIfY0cA,4329 +pymongo/srv_resolver.py,sha256=cwo9MRNzuvFqTE-r1kAuIMxsxCikCCTeYdYu6X6sbzs,4956 +pymongo/ssl_context.py,sha256=FWsu0TZjZ8_0LQ3F1L7Hp1ifGEdmnhZvJq5j0VicuXI,1465 +pymongo/ssl_support.py,sha256=20oziqw2n2V31nqpAzpmvbyNDH4I_IVurHk_MaiRfxw,4013 +pymongo/topology.py,sha256=ECjaNLsVz9GEzgR6Gpdli-tSOVrKmpLRTGlvDVK6NFw,42423 +pymongo/topology_description.py,sha256=md8xx_GJTNqVif46ONejw36DTl7Yqt16SZABuIyM-JM,27542 +pymongo/typings.py,sha256=22e-oAWcZCUkgpbHUTeXQoQ9MF35zHQD5X1ztsH_Hck,1580 +pymongo/uri_parser.py,sha256=bx9F8KVcPnu1B8XGih09bjWrFYFVH_IfqhrvYfXkMFM,24300 +pymongo/write_concern.py,sha256=nRG1kVZmGgdjw8ZIYaYvciElsc6vQL61RQo2FBhRfHA,5441 diff --git a/venv/Lib/site-packages/pymongo-4.7.2.dist-info/REQUESTED b/venv/Lib/site-packages/pymongo-4.7.2.dist-info/REQUESTED new file mode 100644 index 00000000..e69de29b diff --git a/venv/Lib/site-packages/pymongo-4.7.2.dist-info/WHEEL b/venv/Lib/site-packages/pymongo-4.7.2.dist-info/WHEEL new file mode 100644 index 00000000..8e45f0d7 --- /dev/null +++ b/venv/Lib/site-packages/pymongo-4.7.2.dist-info/WHEEL @@ -0,0 +1,5 @@ +Wheel-Version: 1.0 +Generator: bdist_wheel (0.43.0) +Root-Is-Purelib: false +Tag: cp312-cp312-win_amd64 + diff --git a/venv/Lib/site-packages/pymongo-4.7.2.dist-info/top_level.txt b/venv/Lib/site-packages/pymongo-4.7.2.dist-info/top_level.txt new file mode 100644 index 00000000..7b660e26 --- /dev/null +++ b/venv/Lib/site-packages/pymongo-4.7.2.dist-info/top_level.txt @@ -0,0 +1,3 @@ +bson +gridfs +pymongo diff --git a/venv/Lib/site-packages/pymongo/__init__.py b/venv/Lib/site-packages/pymongo/__init__.py new file mode 100644 index 00000000..758bb33a --- /dev/null +++ b/venv/Lib/site-packages/pymongo/__init__.py @@ -0,0 +1,176 @@ +# Copyright 2009-present MongoDB, 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. + +"""Python driver for MongoDB.""" +from __future__ import annotations + +from typing import ContextManager, Optional + +__all__ = [ + "ASCENDING", + "DESCENDING", + "GEO2D", + "GEOSPHERE", + "HASHED", + "TEXT", + "version_tuple", + "get_version_string", + "__version__", + "version", + "ReturnDocument", + "MAX_SUPPORTED_WIRE_VERSION", + "MIN_SUPPORTED_WIRE_VERSION", + "CursorType", + "MongoClient", + "DeleteMany", + "DeleteOne", + "IndexModel", + "InsertOne", + "ReplaceOne", + "UpdateMany", + "UpdateOne", + "ReadPreference", + "WriteConcern", + "has_c", + "timeout", +] + +ASCENDING = 1 +"""Ascending sort order.""" +DESCENDING = -1 +"""Descending sort order.""" + +GEO2D = "2d" +"""Index specifier for a 2-dimensional `geospatial index`_. + +.. _geospatial index: http://mongodb.com/docs/manual/core/2d/ +""" + +GEOSPHERE = "2dsphere" +"""Index specifier for a `spherical geospatial index`_. + +.. versionadded:: 2.5 + +.. _spherical geospatial index: http://mongodb.com/docs/manual/core/2dsphere/ +""" + +HASHED = "hashed" +"""Index specifier for a `hashed index`_. + +.. versionadded:: 2.5 + +.. _hashed index: http://mongodb.com/docs/manual/core/index-hashed/ +""" + +TEXT = "text" +"""Index specifier for a `text index`_. + +.. seealso:: MongoDB's `Atlas Search + `_ which offers more advanced + text search functionality. + +.. versionadded:: 2.7.1 + +.. _text index: http://mongodb.com/docs/manual/core/index-text/ +""" + +from pymongo import _csot +from pymongo._version import __version__, get_version_string, version_tuple +from pymongo.collection import ReturnDocument +from pymongo.common import MAX_SUPPORTED_WIRE_VERSION, MIN_SUPPORTED_WIRE_VERSION +from pymongo.cursor import CursorType +from pymongo.mongo_client import MongoClient +from pymongo.operations import ( + DeleteMany, + DeleteOne, + IndexModel, + InsertOne, + ReplaceOne, + UpdateMany, + UpdateOne, +) +from pymongo.read_preferences import ReadPreference +from pymongo.write_concern import WriteConcern + +version = __version__ +"""Current version of PyMongo.""" + + +def has_c() -> bool: + """Is the C extension installed?""" + try: + from pymongo import _cmessage # type: ignore[attr-defined] # noqa: F401 + + return True + except ImportError: + return False + + +def timeout(seconds: Optional[float]) -> ContextManager[None]: + """**(Provisional)** Apply the given timeout for a block of operations. + + .. note:: :func:`~pymongo.timeout` is currently provisional. Backwards + incompatible changes may occur before becoming officially supported. + + Use :func:`~pymongo.timeout` in a with-statement:: + + with pymongo.timeout(5): + client.db.coll.insert_one({}) + client.db.coll2.insert_one({}) + + When the with-statement is entered, a deadline is set for the entire + block. When that deadline is exceeded, any blocking pymongo operation + will raise a timeout exception. For example:: + + try: + with pymongo.timeout(5): + client.db.coll.insert_one({}) + time.sleep(5) + # The deadline has now expired, the next operation will raise + # a timeout exception. + client.db.coll2.insert_one({}) + except PyMongoError as exc: + if exc.timeout: + print(f"block timed out: {exc!r}") + else: + print(f"failed with non-timeout error: {exc!r}") + + When nesting :func:`~pymongo.timeout`, the nested deadline is capped by + the outer deadline. The deadline can only be shortened, not extended. + When exiting the block, the previous deadline is restored:: + + with pymongo.timeout(5): + coll.find_one() # Uses the 5 second deadline. + with pymongo.timeout(3): + coll.find_one() # Uses the 3 second deadline. + coll.find_one() # Uses the original 5 second deadline. + with pymongo.timeout(10): + coll.find_one() # Still uses the original 5 second deadline. + coll.find_one() # Uses the original 5 second deadline. + + :param seconds: A non-negative floating point number expressing seconds, or None. + + :raises: :py:class:`ValueError`: When `seconds` is negative. + + See :ref:`timeout-example` for more examples. + + .. versionadded:: 4.2 + """ + if not isinstance(seconds, (int, float, type(None))): + raise TypeError("timeout must be None, an int, or a float") + if seconds and seconds < 0: + raise ValueError("timeout cannot be negative") + if seconds is not None: + seconds = float(seconds) + return _csot._TimeoutContext(seconds) diff --git a/venv/Lib/site-packages/pymongo/__pycache__/__init__.cpython-312.pyc b/venv/Lib/site-packages/pymongo/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..556af1eed119b08a1d6f0f389ba364f81933fcb7 GIT binary patch literal 4275 zcmbUk%WfRUv1cFL7rCS;zC^uRl4WygapTCAWNd;261lP%MH#_LvqznS>Onc*XCHD7tB0K< z>JjItdeosRb&jdWoa5?o=Y)Df()8QUIVaVVa=zbw-g!ZNK_RS99RsO`&4Hh%)RH-3 z4#GIz94Z~SIXc%AE1pYDZmlv->mH|gmX6F~HQ`*((`KtD2b$(%bb_Uj_XP&l}pKS2J}AAM>I&Rn{%Ai8E3A+ z!tSfw5)4#oFz$xw6+O_5Fe5C7c@6PM0Si3(7kpvjv>9ekn}NRq8^bQ3TVZCHLPLOw z(wY%?qLk!C*uZ-bbW7v$J{%H!AENpr)c$A~z#+pSEC?S$FpOXX!2txL2o3@$Dg010 z9EQPw-_DnUS&|Ut@k=ndL!{>?Y~m=li9(vm6GR||{GKB`sgSoxGqpz6lW&tXrIfyT zcsihBh0#e0_h2si0JYpe=(f$wA94)QkCiZ|84e2q@V3Q^rDT}#n-01tFTrwHn09=R ziz??H=VO2gP-*nZ?a9jZD`1%5y59(xeO-8KZm?qcTHxrmR%-;73p{tw6UK@zYMy8# zkj|cn2`6xEmAR|ee{9vR2Vk=aUpH=mOmILX&v!J6&URRF1pBqg6RvAnhc#rtY!bB?iPcXSd z#Ln6+B3g-zbSPV7;*F+qw?$h|-fd9^1(DlXPsHo*DYppkyG6|84WQ!%QP`nLb<+H- zAky*5EfTNXCIa0~em_Ak-1D_57f30s{|W>gA0M0J-l~OGhwL#vPS5+k-INZo4B5z~ ztuT;TNvAd2Gj33?PCcLTC>@pac?w^-Od&(DN`+SH4cDkF{j_D0a*M+fwE}81IK-TQ z6a4nEWqJY3xTbS6kgR~4Djz$zT8~$V?zm-7+lj zl_m`uKKddvTXKQ_eTzgc)#;igRwlsT1;hMRsYLgo5>NMBV(d(*63Yl;8Ztwb&05*; zY`Y9$16fG(TsHRZ?b1`}PVXg@lvh_!+YpGmlUa`-*E)wAYYp@y$CNA zTh|?UFxqMpyxFNDyiLqLc7;7!ME7}CO(HEiR5`F2^MO%^mT~O_T?B3HXbEyOxaAs1 z>4SPegQzPP5?%YIpzrvW-{AG1ZoP+Sx8UC}Ey_gUc+?r4Ysz;`=1B^Y0lHIFRvj_1 zk~&wAhYxSC7^@$TLJ)OeQh1*#^j+9=jNd-qsS0KX>}d?@x&@g%a&OmjC*lZan7%vN z$(u?yf;27AO`aCov0ej+ySDN%-gVyxRIMp_q4GgwsBvNgm3N!V&|T`e5XCh{gB1*8 z7mWsq1vCSHz;VlMso1(Dr9!q+E0GvJXRDUi2zCy^SmAI3NEv3^PoKdtWl+~G*OVb$ zLhpif5Y7`GIJoG zulURshXn?23)GR7rB)10w6fun%*zhGQ7uf+ekNOmvu=Vn!*lsc`Lw*{%+0Yl5}igM zFa%IoNcvch2`RlET~-!lH%DV~h#HI#*bvKC%7%G()3J)f0yhc!!?e^Bl&e@^=djF{ z_YCvxJM-tEVhDS+#i;acH5pLbW?D*!>N{5!K0sPJ0 zxc#{%@AwWshuDGLP0}^g<~yYq!;Gexo}p=BUeoFg=v|nmaVYFa*af-)hv8T-b1bls z7ODzZ??Mz-bR5e-jWvz<@Ln{L)4$?%#lNlp+WSt`lCR$9^qiTyoTM9 z2DuO0C64_F??oWXZS;zd2|tSnbdx99?>nB^u-OmzMfi(#aBv3z6x@pP`4OTV+akyR zPG0;Xb+C}w$ZS_gPjMr6pB%j3)4P$sPYylIc5kG&6G{4hVb Vk-1NfJ?_dVgI^B3s*Fp){|#Xk+jIZ` literal 0 HcmV?d00001 diff --git a/venv/Lib/site-packages/pymongo/__pycache__/_azure_helpers.cpython-312.pyc b/venv/Lib/site-packages/pymongo/__pycache__/_azure_helpers.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..249b22fc86a26b27fc1b3808230e3e08738c2975 GIT binary patch literal 2150 zcma)7O>7%Q6rTO{`froixD?VBH%Zf2iR~mxX$gc9oF;DjZxWB2^Ey1V}xC+5_UmfeYkPiHoCF!d(?nsi)pdTd7ie;jI%VP^iRMo_X`l zoA6}ygW`QPZgOwU1Wh5Q{)^@VeK$Nb%}G( zSLhphb53CaR(Xfo#(DDO0rx)z!u0AUG#~u;ni9H7ZJ@<~0&!MQ!w-ed`W12EAVN5-G!;Xnsj=JH+H;Qb zBhu2q=OYbidpsN1vL9AjM$oDKa4yja&&RfP8j>S6bTM@yrbLP%C*+7uV1~Bgw9jI| zA}2h7+EE`esYR%blZb-f1!;*ni597LL_!Vw9SH9z29>ZAe6~FWoud_041OG3q!!VC z+6Yr+)AS&@mH))ZAI8d**ovPI_l)5KVSZO|kd$r@?MXQ-)lYU`w@|G8KGj*rJ@44juq3J8Zbd`Wd*Za`fs01H#nbi4PN zhO&lEWH7bQ?1@iXB)%w{Ag2;RSA$D z>iUl?c|n4>yMYhnoTVPY*uXC9MbqVShOC$_GpmwBlPffG;rh2Sf#Eeke{;fsd+b%E z>)T{d-&T(Sj(RvdZ79nwrMgVsoOM~RDqLnkT{gWt82@K`{7GhT>U|S3H&w7qH8(|! zMctg!3X++ZbJDbBYC5E83r=R|WHN1#ZNSsZB^!!KZ|#h#pP70`o1QW?q9zM+c0rz1 z!NGFg&}WUbBzq63)Obh>%WhPH^KZh9eBw@Ey955_QFvok&?5nHy>|k`E14?ms{}jN z*^atcnk);eb|u)h&bF;&Zuy$4LU6UUe6oDB($uvsbd{KD>)uN8@VfZM%GfP2wwk?i z@TSa`FGHLo0f z)a^O+xIKBuqfqQVqPecBc<{%)>*A|h&aVtqg#Axy9IQQg7^$mK9u;wC<%Q83;@I`~ zD#E)p#={<)gAmjo0+|Th(FWEthKgedYH8y z&XWgrj0BhjJ${b^3kdnGn)@@PIvcy~N ziNj~|R6=lllB5_}Npb^{G-H91DoNP;XyToob)GC2G<_Dc9^$#Jm&JGJ0f-3igYL2L zTHa8soccQMg{k*FG7m!sr=_Sr_aG|vJ8HX&f`6bmqiRe$)m#-@HpV^O8{^QM Q_3;iDeaOK&PJ@lV0QXQmEC2ui literal 0 HcmV?d00001 diff --git a/venv/Lib/site-packages/pymongo/__pycache__/_csot.cpython-312.pyc b/venv/Lib/site-packages/pymongo/__pycache__/_csot.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..54c3428d040974350048a5b1c1d3b33c509151a8 GIT binary patch literal 6920 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 iQ9N8s%yuASj9*xI*`e{`OW-h-kaHv$z&XX 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 literal 0 HcmV?d00001 diff --git a/venv/Lib/site-packages/pymongo/__pycache__/auth_aws.cpython-312.pyc b/venv/Lib/site-packages/pymongo/__pycache__/auth_aws.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ee2b19640325d26195b54dd639d54d70ed4da1cc GIT binary patch literal 3950 zcmb_f+iw$B8b9M1-^aH&_k=JIQ))JGPz4B41UQ#cLH?iK?(ns`o#!(7anF^OHf zst^y+ZXZDV09AKY_N^5!{R8?!)s-ski_;(_9ThF8^ntfPBDKoC?0057wrN?Zm3CyG z`OZ1t{haT2&ipG92_bm?>^nO*6GP~G+HoJwA(+iCfVhohltVHjdlW|XMso`8$<#W6(8Bij*2+RkSs2a<~?6Hs% zR};B}+L7yE5Dt4?&!_U3wlj>*v*T&}Mi7)IEiLc|MW zzMSto`tZaeT4K+mMJ64u44*kWee&$&_-OWzGeWjx&ST9iRyg4;-`qcDE zi_L0f%YQ~H77N;Ci#t~?;_1&A%Qs%oBvMX$td0p?n=4!{5!=WVAv&?*XTb@(|Atgh zN(6)HnD`EsWNB8xT0cQ)su zuBJ(J=`cdI(911WgL147gk0_#*sTWSI2{A)39ugipVq@o>p{@dL1hf)!@~?J2RZl; zqqbZNF!{3wYHGB`!OsRDB)2$KMEy$ zL?~#6DJhDuSTN_0z`QvRurR6$#X_+;x3?j70VFQ%C3HcdlDiFTig_tNk7Y66EK0-b zr{g69zhv&*(;&!JPW&2I?P8xC7p?(5QK&kQIUwjnP$UzRyqh@sZtO2+lP3Q*!YIc8 zUVCJ(%m6yFvQPHFo0I)A3vX|kO9!^oHi$JcgejP1jw*UyQjBAn?4mIv844haiI>cL z1J-SelulAup;g=tD>DF|mEm`=St6PMsTXDqUCYecpqFv5PcQ)sGlZfD02gOqWzL}b zgzehIk&C}KAk2$JNSkue)E6~lzEBhmRZ_%Ry4Kh*n!}^{dC8pB&DH?rQ$}6JQ95@G zYu7HmRhYeK6ihr?l=4@k%NPte5I*KYc&1pk!j79F^{;a9S0|k%r&#R&{#ey;Xc(SFxz<@oYL~Tj0H)M3pUilNg|lKFg|m3 zIs+S#qsg{>*yc)m2!%I`mG!(qbqfNaWe;Hjm*_D4rZ8J#CqR!uA(@8RbNlsMuir6N zlf!j>m{6$v{{=jW?SN^3G|=|IAz(t0*7C_VlN&E5OQ!UafE}*i9Izuc`OARK)nuy9 zr|f{IA^ARHZ~6*PkNOMFR=8Es8R>E!QlxD4<0bnp=N^k=Tsu*fdD;TQfFG^)a zm*h$?1t5WmCMi#8X_NiXSkb($QQWfcG^b?H!c#32QVLoLlh>M@oUCiOG9bMAb7XGf z-Zqm?cUlSY&Fmkh#D;4kT;&M8Q&?RMx#p!TLra^`8v=oPLsndsmc&H>f3X3i#7pOB zyjBDf)w)hu0YD!JM8#I1C>h40PGqaI8M0U;I$YDt0yeA!0H$aFC;>2wrhWx$0B9yI zTK>GQs*)yKULt9iv6XzgY~y$qoC0I^b&o`-?Xc)N1WOy7TFT=hy~U*?R=E8z;UJS* zD;&pfHg7ILj+FucxY$?-i*NMj3dArW-j8O-_ zuiszk`O`AL-ZOB!daHW(^yh=CJ;$IQ?O$e}B)V^szxF*xpUv5uRE z{_@`Ac-ONoQ1#PRHv<2|V=LjY=g8;nd>j~pR0L8_qC>#CjO)qFgR$dlW2aZfPS-PU zeVwiI)0?a(7J3v*Zg@Sx&i`!r5a0D2vEHaFyB>&a_@TdTP}I2JJ^tEc1br1`XdAZM zNOEe3{YrRsD!~3T&<%9jM<{3!_@F~%iZ*fD*r>xzL#?IzEx#zj;ozXlTnUbBKo>#} zD66wkO&PK6!-sHGgIzU;C{y^9{XOJ-y7=JKgsT|wQ672%k^pV!HOGNz``Brml_U(q zR@60u>Bo;@*RfL*ynSnMG#WN6TD;3_mo<&5HB{7f#R-q@QSv+5&~wP%%G-gX4ssqN zIT)kJW=PO%unfaI8$!&!hiLZ?Xk>%qn4xcbcGrgwuJ#)3k8zT5yK3v_wM+H>!PZMwr!NX8OkQm{e(X| Kz~1j?p#3kr^95)C literal 0 HcmV?d00001 diff --git a/venv/Lib/site-packages/pymongo/__pycache__/auth_oidc.cpython-312.pyc b/venv/Lib/site-packages/pymongo/__pycache__/auth_oidc.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f6d4f31b899d9a0c0476c1c7c4635450d0d42684 GIT binary patch literal 16236 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 literal 0 HcmV?d00001 diff --git a/venv/Lib/site-packages/pymongo/__pycache__/change_stream.cpython-312.pyc b/venv/Lib/site-packages/pymongo/__pycache__/change_stream.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..fb2e78c384cf17afa46c70d205d8a9368a3246b1 GIT binary patch literal 20608 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 W?7M6|`a**i?gyU!I)F74=RX3yyVB7B literal 0 HcmV?d00001 diff --git a/venv/Lib/site-packages/pymongo/__pycache__/collection.cpython-312.pyc b/venv/Lib/site-packages/pymongo/__pycache__/collection.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b977eafe4ea15149931e0544414b4b042da3031d GIT binary patch literal 143437 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%0AVYlDEFj@qH} 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 JfnM4A|3Amp@4x^6 literal 0 HcmV?d00001 diff --git a/venv/Lib/site-packages/pymongo/__pycache__/common.cpython-312.pyc b/venv/Lib/site-packages/pymongo/__pycache__/common.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..04882b7492421a9adbba72f9daf82879ec585767 GIT binary patch literal 40537 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 literal 0 HcmV?d00001 diff --git a/venv/Lib/site-packages/pymongo/__pycache__/compression_support.cpython-312.pyc b/venv/Lib/site-packages/pymongo/__pycache__/compression_support.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..48ec575d8d0594d693e4d077a4ee590a5969e107 GIT binary patch literal 6256 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 F`oF8PiDUo( literal 0 HcmV?d00001 diff --git a/venv/Lib/site-packages/pymongo/__pycache__/cursor.cpython-312.pyc b/venv/Lib/site-packages/pymongo/__pycache__/cursor.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5b6548e0077ff2eb54adf791dff80a3340f25527 GIT binary patch literal 52786 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

R?&Zd5a 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 s10U0!cN>vv$LuN)029f~bgWDL2 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 literal 0 HcmV?d00001 diff --git a/venv/Lib/site-packages/pymongo/__pycache__/ocsp_cache.cpython-312.pyc b/venv/Lib/site-packages/pymongo/__pycache__/ocsp_cache.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..548c80e3642417d57954bd3abbfba752d7b734ea GIT binary patch literal 4069 zcmbtXO>7&-6`tiT|0$9ZX<0I5du>ay=pRaMY}b-w!z%p~|CA2wB!CH{3yM3EmRc^E z*`;j?4C(|v$VCm;=t0gYX;C0H>_d({wrPPLdJ!uFWETnIpv|E-IdUPur@psKk(5Fg zDLNA0&b&82Z@!uN-ppUSx_kuEFI<%$2kQS`GpgQx;w9CpnA@5GRt-MS1&8N4swpj-aAD0J#pBhr^97`JTO1>uA!)kp-^3%E-*2h$<8X;jCeVD^=*++y;#t6 zswe)1ZQ`8i&uCh~$QTN!q3F{xc`6&FqDrCUkuwH0l)RP4?|ea{afjLU&b7DCNXfU( zBrl#%oiq7KtqipTQkGF@AgP7y9MtmTkXPvYC8`@{z%JOsXKjaV>9jxG>RHGv5=zn> z;OCIJH18l(kR3Bb=6}mIXX%u^vT#R8yJVm2gs)rn%P#nO3nR`LV;R%Y{J2eUe4;pD-B$%`WI>Wv0w&gGy1HQQD+KFF=q2-Skd(oWfB4;q0J06 zG;_3UE4p;bl#D8ArTi3SS*~rCxI{c5-0GM$NrhC1)HEAlvD1^r7p#p%C8P|?l#27F zQ-amVFfUYryoE7m3OZG%bsR~2_4DM!^>@Jq{d%zk>gz_~rl!v-MM=+tQ&S~f(WtIB z)d#b)8Dpwov=mrRJ-A-+AlhzF?Z)*>%G7mTG3dc!COemzp)f!>U(jX>qXocD!W~8v z#j@#jn)&NLU1$IM`OK60lHZ z_QBC{hN8&&&=QnpsA1zR!LFO{Z-;qseT*3BsjVcxHNn>3<;46+!D|dSsTH+|1QV!3-rc~kO zCQWd}i?j=W>%_s4G6!RHSXHBIy`x-kJ=gjgxNcU;;Zki%PaW$s$ZgqVf}+el<=5?5 z$1TSk0LVg}XF3N6>H&>=jpvHstBEs{cxnS{`KCW=bcxh!oLY# zd>`*UwI5)(m~evRHT zBzyi0caF&y3q{j!%qn_AZF-UMt@vy5THz+cV_@)T)L+y5xJ+&8vxe#J+^TWTS(8WNX6BlU1}(f7c&7<>?XSbjA4EIe8d z_pWtAyA^D_(nq>`YvEnh@UAZ-1B)jfocP^apI!RRrDu^twa9oiGX96?Qe=ENa`q+f z=pKCDv$fW#- z9v-NLN2=kGrSRc;RIEi0RHFwTm1?7>s-vg=7CrTRdsP_f?D^9Wd#{xfN*L;(VjYkd>ylA94SMmp(m5jg5pPD$}B3i1e1C8+toC;j#JQ z1Y_NUTl{h*qrg2}yhzJu7=wE{3}OJyV4ui%a48oR%~IL-s}`@Y-GDS31kuhY7KyMC zG!3JOqu7T6SDoR7fE_@w7li3qA2%-8F-C`B`B9mp$7ajxYBNEd_?w zc48f@_{zGg-up>UGR%Jw-wX0jVP~@6or0H*i&3M*G*d|7dq`kAVb%;U7VO6;20+AJ zEYZ{=q6`n3Z9j%~fgNeutyXA{`|uKNJuxJB!btgoTvDkd!P`oDzm!oM7PlnHg{&ko zTqSEbhTLF97WN~|9R=~2Sobw+1sPs(EU^PJI_`@*$;s< z_3_TZRiDf0TMh4WCSIQ9dY!{|H`qfe-pw%5qi@qCzyN$>c&{`4*#d;Wto4qx-NHy? z??NkXxgdbUJCxx~oFPe>fyOH$fo~c6UDMk+z}whb09x}nfqmdz)SBBX&zm+*Cs`77 zc%jr0`xTz!xEDJJci>yH8~(o{`@SOEza~T9lA&+-@YlkrHCKw`hQA{yt^WQ4Io5kY literal 0 HcmV?d00001 diff --git a/venv/Lib/site-packages/pymongo/__pycache__/ocsp_support.cpython-312.pyc b/venv/Lib/site-packages/pymongo/__pycache__/ocsp_support.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..348d0bf384f8b3160920d62ff32df3ad1d5ad776 GIT binary patch literal 17308 zcmbt*ZEzdOmDmh000!S6`27u1B1P~6qP{<@kL9PnDOr?c36d5DF^~ufAN0&n5@68F z-Q2~LZ)0gIu9AGUuG3v@)p4_`SQY0g>x!$q-XuP!u2LZg>;alcF)x;raw^VeHzrqLQ$I1ZxOvg~v zU5cap6i0KqC>_)Jbu_~IsNT35PkQ`8l!@Vg1jMk`}g{%Qi7qcyQwe;t7>(fU|}zmdRJfSdfy1hz$6Vy%7; zf$h<@Si64%fgRC}u?~MHft}G!u`a)tz^-U_tjFI=;0l2I{5}G^qnl&>{w)Nq1bC}| z8-c5$+haTYI|*D3@Gk#u0@nb%$G?}rwE%y`zmLFm0Ppu7AaFgv2mNmlxFI?a8}ttm zxDnuC{|JGb03P)pB5*Unhy6zg+!8$+JLW%5;8uW7_{RwBiJpv|@_&`UZPC-QasL?t zw@1&$&iT*LRM^V@IjqBm=+|QB{pVvB{1;+=zdv@-e=&B+e<^m^e>wK1|4q_nV>A%^ zy8r70?f_l{{XqhE!nh~=6S0s#M4mTAxmegAj$QFzAFR-!xOJ|8aXcACxCom&#AvSGdhkmnyB}_Hq4C_Dya-w*~Ta zX&u7c0d6al&%RWCklP03-{RijwnP3FH^A+Hd=8#>LjLwzd_rrG+XZ#+a6{Z~$p2YV z?;TEUAD@|?PVl1VN`m+B;cv`@1u+u8>Iud<&-E}Lxst@ElSAXDpjMbp#D%cn`wgT;@#GFY^hb6<=I6OjY*OB=3U^K#wN3O<$;tY=zom}`za3(4SCW4`B;W!6H zw&C%CQ!^9MNa$EN3D1U+At(W$W+WPo0PWDw41Yabio%{cHZ;EF`j#Eo{PgPP_Ve4f z?=D4lp5MA-$Cf>%Ma=l&fi2LMIUE!wA!kS8p#%p^LV7adXo8OgMR;x)m=Pxvd_+th zi35M35N0&{l6{Ji6w6TOZnwTN8w46CUJ{nfo;Lx6s zXV9oVnh0`%>2NG?b4UN4Kq$l9tOZm$aP5YmXdD)52K#km$!U412WKKtF3fAC z@)E1KHH0)^1QH=(I-qO;z;Ax_+)f@D287DB)02yt>4bp1aTP}}ax)Yrn*hoTfsl|8 zy(US|2|-C04oQYE2TLNE!W>x$NzX$Ca}&#qH%SpRGoRwaldu|L+!A(Nmx1V2Wcy^y ztCOra`653gW-JK1Qzw~KJ{Ok(S&r;H7v!-`35z6~{2+4%daw*7;_+|@*Akxsgu%hk zBy!sx7z#$CxIu@5Vo=xv?@1s221gDZ8S@N{oIZ18^vKY_nURZc+AWuRd_4zA^~l)p z%atIkwVw$56@=s|&Q&*{3y=YMI zRXj$-9?5U2s5L0hn2SWmKOe zrS;>~25Ji>&^JIExj@nI@7=H7)L)=(&|ZD2|5T6{!X9xF{-VOrz-S~I_C(@h!V~mF zBZBBjT=A4_e!(Z{_=zzuD=~@ba9m>eaFCN+fi+@8(!;ivOjjat4wg=`25^-Er$-Kr zoR^HjX=uqw4S`p`Vn)dr3SNPsNc0tn0SOqCTp~Xsh+!@e3X-=`kW59HC7E%60mKrF zzz`4ly?A?Q@1?Usm=`Wh&j{h@B{6X$E=)$I141kq4NS}kkvPZ)- z(LM#S59_XnRkcGc*Iw{r2Q(QZ89*@{b z_!F)}GDkf%+vl&`;j-q&6^b_0-TOwt>AHRW*7ZNTInVq~4<#!sRoAe{R6TaIWF4Nl zLyxQL?p}QF;s=cjT()}K7trVwJ#Q|sb$PZm!?xx;dq3Xu$@)V{3J^F&FIyWI zna1B$ATjLYgO*=Aq=+#h2s2?`G6^#iQy?(l zBxG>;B?GKqB+ie*03@r76o`Xhg53e78gp}U}`l*%Je ztBOD6E$5qISyUOL)KXNMPU|%}qsX4L?xSK~G?k=RQC209O1bLQlz~ohqr5~%m0l{H z*Nsq>W1Fq}c9YK>%+)i}u*$^2{f!UIA6Xw>$k`6AG*CAC^LolrcTfNR<($2z z1ZlyXvu$0eL4<08kZazav+pRucwk&eX1n&}Yu9Z9L)3mZl@h}yU68Wi{h*CBYe;g{jddU(- zMHDq4L8?VDqdo^fYYl{=K|zq*0aayERZ|^*5-3Q9YltD>_?5+U5OJl_;ZZ3675oWk zHKMT68>$M<%G+<Q>x@B zsl@@3-<10HDvRjJo8793i5V8EdD0T~*J{C=choSDY+Tw+1N1dBEm_A=zH z;iIH>S{{;-i-2+_t`Ui@fHs=|S~&bSp%Knp_yCeQs!&mVxAnc&Y(@Lr(dQP5b>HcJ zKbAA~7AhL^6L}U}vjhPA;k6I1eSGexZ~pkrCEu~v z@Ndf=Q1gM9@>6%^_h4@V|LAhun$KIFSz%d(| zL|pKJ6Ss3oBO=ay#Lh}}4Hs#9!0qh`%Ff+Xm?O?}bOUdF`ycE zQnffClzO4zFhsS>jm5J37s7Ix*CHPatWqp7vLA$3CMTIADp+!O;HrQd1&>@UZbnzlZSe6nY0=kdp^^S1q#eX*t|$M!DQ zw|>X*R4LhU-SM|FHUF<0nkEWn}*3V_W6z6Sqz* zHuQgH+w!z!`@^#zkA89_+cH*C)t$5TJaV?Er~^6MV4<=K#@^svsL0g!%sZYNDQEM& zH*to@k$sZ>(HHX#q!#Udsw14dM6m0IitvO?B&e~cF zwuZcIW5%|T0NXOQZ3O7g*!rKF4KB-b8)dJO$2yp^4FQ3tmc{~GlV{s9Y+IJyP+(no zwjskd5Wt&Zy#&~hVK+Q88?45;q0eoU&8hP3K#o0FusY_B${J+psjHsVxEV`*;ff@M zb6Qu{G)AjW^+PGx>ZydS%2SdiD^-S;u%a<+!+M=*C+xu_r|FeUOOvB4eV}JrtyT1sVAKw)q%LXlvSTUtIraKfku0jKNJ$EgSEjTR5M!?3^2VWOcx8!E=_fR^9Hq)%X7Hzu5Qv zeK|{4-qN43^nc7OTKcn={c}UhCTHH%m@zfp3oV)2REe-H$8LxAa_y3-@jjdPY|nVM zKNOZcdsVD0Id&^x6|6S-9l(9B{8YDf7g6#{49(J=&49NJl=7kysS2KQ4(!tRByAx6 zHdWEzNZq0N%QPjLmF`8W0+o9kI8|Ne^dA*_oi&1MU41vekYJ`%m0F<%Z<)HEr&Kjp zt5tR;$8d%Soi;u&s%n@i-Ul8+RRsnQVcRULx$n!)*pz0K(Vi)j+Ai3t?d$YnF5kMe=Om@MngF{=o3R2ZGY3=P^{Z$Bb-G1qrR7?0H>^-T=f!r?PIA%S!oX8EjPD18pQV-lrUHS--n+sotQXvFlB?=aquRKeR!Lm zs)Gwuh|VB7FS&B;E5?|>i8uo7B-12Z%EHB`WI~aZm=RO8htHhBU=wlz7WL%4o(|zR zzX1GH-Ktv9@n-lq45@>AQAZSTMbvuWG6cdMpdp{?J6()i@xT>pBpCI?XJQjz4{(XF zKs0%x|0`XlYKCB%Xbr3)=mQ-bk7G^VO6;+4a)jp-yrho>Z}MpHNKWvU$1qSL1~+^_ z1bl%Q@W98gX&jRTqE!UehCfJFdBg#TCJDopO!>eI$rO^=t7yHTNz3h2rKB{1L$ZvY z9U2-LA0It?T(Zlv56H78Qrl}T(M$9*d_<~T73TpL*s$V~tyoBwM6$0!;UD0nCNM#J7hUC+YtOx>P& zyDHo91XwvtQ?~j=rtbI8AvgUi?d`(ZLre|RwTjP6Z$hvpVn^tsY zbKT?Grh==oP~DcV_GPMl50XEe{ctv0y}wYuY0*2hST|g#YXv2?&T;GT??I1swc)L3 zovILTdpMr6?RipJ`+xCDwC+XGDrn7ETkmgMvUaM7+aFHmcOK2`Jeso|dsJEbxNh@9 zHsAk7rvHs>-9X7S`XBb?x1P*wJ(*)q6|8l6YgfkFmFqp2?>(C7J-TE)3XQ=@Xsgd# zJ2TeKh2DJkaHe~B$vUidbtGp)tjEsQ`^E)(&bjj+;6X7kd^vV=p?A+B+j#Hz66;;I z)&aX#4$;7<7s6>^{C69=sek1it@$37s~PMveA>=nNtXdj z`nCf6AKC`okp0IUK1lylhYizh4Uqoag^gGFBGqH|4S>LxrW15Wsdw<^6V`g|PtmT? zLpfKoj<`~fNVw5Ok0$oC=BLtQw1XF7WYmZgz7ocv9>iK>wVi>ZXN~bDz_M$#3Z!9y zt>92fwH!QZ2JpVT{|2G`uvnDvqUNbN7W>+zA=z=FsRRz*anoS#3 zJI2iEXDub(FQ$z5%bK>ny+3W8QV)M^4iNC*gK^l>Hc|D7mGgrU|0VtIdWThsYRs8t zE#McVF|?xGfjw(a)1V-Lryo?Og>|@C>L0Rzg8q7*TeO1?c8;#xalh`ml^nZC(<=UY z6z(a9@2n&3Sa&y<9KK0Y+EIQjZaa&6V+X8>Gwm#=YKW>LQO@f|yvsGv68O6ISFTUk zVY^_5xu7@Kx;(L$@YR$yuRCW)3BG=vHFIjXX&3CuZvzJ^)@kEPS14-$s&|hvUwA{z z>58;nRejxQ_ZC+ty(Ju zD!z5#pi1f)YMt@BH4eg#ef=9!sp0<7>j-;O{%OGnv+i~JtAg3@RN86zD&Cf<7k4YT zQ+pLi%Zd6-C-oxUy^eP@DQ%d~I5FqHe0kN%~lMtFXK7QPXe?;lNDf$G6-XMxrC>jAJ z9P~Fi8r3(JvV)_0It*$r`lt?$N^XqwAR#vqj8w#kv{Yp&j)#;TOtqcDR(Sgj6hYSx zdSVdW*avFoG{j#bwKK%jPHiNJ5Why0>SBE`zFLn??KrWfwLF`60#$aP$Q7OMLbw+^ z^gKA%!By@{`G__S!I;x{{|;jdhpwTL4i^DjM8IfruGb?Zu0SL^!S9Bt6g~7}(i0>> z*AOhKhGCZ=r)mI~kUx9+NNKqf%4KJ}%(PThQEOMN0}SL$na}VfO1?u#RS`1ClTri3 z3v?jr6bu3}ay=|}Rf1F6Wi#QS!ilxav^aAf9XpbJj0t+Qh_&afgpf@L z_m(W=tt!Tkz$?g;7+%RZoy0DQ6(yPAK@05ELj`{d2@$Wa6<-=06KrvaX9u4f#4tnH zu^iXW6W)>HU<4xExv&tD7%<2L{@){s??NKreWq$!X@KlMb`;_av*4_P0FJ!1En{uVS~tuM zKQ%e##l>oG&eUCS*W}&ajN6-a_sp9LjS#?Fw~S@o8FzQq-HT-#^Nl+)jXM_Ex<^*G zYLehB2vYX#_hYFqBkzBdbj5aycsLj`Be`!LQDG{>myh7iWxA$n1XRN?8-FkT4ZX$euF@n@vNy%9p1hiyZ@23Mm4X#nzNlQ zRMwHO*xn^)uUdho-Ew8^Q@7`SRc_N@);$Dswz}sJsu-g=+abV!PU<~>VgJ4z0zF)o zWfw-f<(+LAXIrjgDBp27({Xsod3eP@!D`q%8Jj0>>&)6ZSM-2A&p^0`%ksH}a#g8x zx8`iyfG(`f`~kJFCui%0ajNs^&9QyZb5-3Jl+D~cKU74#*K=QhTcXFc-i3`1jUVe5 z_n*qves%tE!CAZLY$bP5D#`wbUHPpiGFwmNY-3QLcXnr--47CZ-(bc!xa1ssRNJ_! zcqHQ+$vQ_K)i$lvL!Zx^D6>nx`5MTvgD~r-iaU5qj@eqe-4<&$<=8IR6_1^r_fuKt z*15x~>$rDeGVdGB_(pTgAzUw4-qe~gwdOYL%x@UTY#3NF4PZkEo*BJ=IA`sHhvoev zu(&z!8Vo<|%+~G6Fnd*UT?<|L?vYIQNRELW58mbWjH&&DM80!>rgQ(2X+LgFtMdJ66N|ynXLjBLmLyA$X@GJ$Q2@qwINlQGwqbTpZG*&Sftv zyHrr|Bd`C9YUOjMRd_tg+f<|S0o28p2wmp?2VzvK7^`}*@If3*Yf0e|av!b+q@jow z2;7#tS+jb%=CINWKI2Grq?qD-)vnb&N=>nLIW}iNe^eP*H|*M2-2#- zJ}!Y{vM!It=eSOw?^`mBxWL! zh09CcG>?&v?Sli@u)fQZ(`eyb=qP~ z42TW9McyzV!{|3t{m9o-}-+|RX!fMb4$(L60y?}RBU&Ys; z?Dpn2A&D+byqI)jf{qLxJrR5#B+}+DP0cqWMxp}m!={@tA%b)Z5?TFb;%dM|ln()T zG*tO*n4pv+LJ>ZT)J#qyntap6o@gjbMPW0HjQ818HZ1v8908Ne*;R% z*J`p6!{Or;Cf~$_1VO!n&|OS^fXM7 zey3v%#@|tpe2&R)-IU=d{e&6%CA009%;8@$Z#-c-f6eqgVRkKd_7qrao~;8-y#%nx zVL1iXnP)v2)>B~ZdA2FTHWgS$o^8pnEd|ylhwH4^jpnA$_3S3&BC}zog|ar!!-aiQ z#?o{TZuL9oOv{e?`Oxiax2_eK;ZH(;JN2`vXL_AeH)nljvCvH`4KCVMuvD+;F>l!> z=Z7XpzHDfCW`OdQ&RW`DFn6u!AzyY@tr#E&wa>L<#e@(<47gUz2w5nj`I!}=?MAwa z(8auE`-&d&WkdHf1LV)#4!U`z-9#G;?oBIt%(v{5^V30+FB@u}8K8WnsvOeTrBz7J zszLo0i{@j=81wsb_4y6S4b1P)HRKzUjrpcz6MGNjn)5Bmmi)%# z#(ZnCm4yX!ZTa?Od%h#tk>8Zu#KJt__pCHdYPVNoP{+YtkDeH$-MP7>zQb}s zRz!&5k9Z8=*Fgl#*FJ`9m za-oDE-)T9AI;rrsolEDkQo1CkGR0hum7gjqsY045-7Dye>PRY3}D&Wv! zW?2)(Vopv|gFLAN)48)5>Nw+ZaF1hc4xY<`EF>M6d``)M$>vPDC0EiTxszVWlk`d6 zq+jwS1Cl=(lmf|+6ikMtP%!v zk?)jTI#yJW;A}RRJ2J|v&7WdKmr7~DR4QMTrgJj<;Z*9&)9D=L zgf~y$f#H!cEzo!P@W{a7zOez#dvN&JvB5FTb8z@XVhpb%0~B1xe^Y~r{(%<pRdlI-rHwtC3d&d#lPR zJdo&5jSh^C4h|gpR6CbqfyO!;y7MVHU7(6q&4Vo6-m7#1Bt$417JG?W5~7-Ou}`*??7gmK z{xFXjYJ0t3dBLb<1^3d$m}Bi}e6GR00+Qd&E~B7z>o^V!mAu{ST5PWO)g ziavlnBycz{>9N>K&jfgT>b!y>AGc-HJC#<_dC?Fkz2ewu*?bdAMZI-B;^3rMvVY2| zVm=G@Ryc*qn)RaUrLvMZnU!;rNL_LsJVu<5#k3^JQjZxip`1jkAqm}}>PhfgU81Sf zN1~_z0Sd%{NS>!svm4Cb=rO{^5JL^YnHN@LLer)N_tyiA4l25w&<*SIUU;a>&*&lz zujVr+oUsp+9)8sjegX~6-ADT*30;78o1;wqL!H9pra4UFICXV#;>>tY0u2nJcs znNU4EJZu8-)FwFd!b*MnV*j@W-x^$e=8B^n-C6eUTt83*QQ1Hh;JGZ6%ofZ!jDfI) z2}ao2F0iLo3~P-6tUauhVpJ$mdF&Y#mK+IyEq4hZiH(p}mnfD7^8kAU>tx9oF;5YL ztyGMcH>5zdf#dM zoz~xKU2fP}4)44f-Bk8(THh(uDAp-;@PK+jzf}D&8ldD5q&!bf5l(fj3Xsl~wW1Ij z_q8Y-3`#@G;$xSdDM!V!Ut}dG;%MQjn!A&^R`xnfC8rIc~{I!O*yQ28=B zDmkoqjJFd%P?Cl-43D}E&KtsiaqqqrYn>llbNfA;)*79jht~WK&#pDE%hP!~=SpnK22F_LtfaIsMg;FA8QY$J;enu3(&1yd9cs*>* z49SW4qR$0~Fz0%GS4nTFfH#ZkjW}w1$%ky%bH;D@tlv48G1DNcXmHt6@XfjG?@W?; zjjwY~d^2+e^K_-}ph|{l6V0A6S*A@OecF9A?AF#4z(g^cqR@ylH&(Y_TRGi9&K6 zKkD?<6qY)yj;!itO3DEMM9O9W`$ct1&SW8PB~b<#kQH%aM$7`4U{aEBCZOIe0xo0= zEGrf76*)~76?BJ~R>di-p^;6gSn>Vip$oo{;;Yedu}khbWwn&*J3fB0>tTip9_|+V zMh^m=WBDC_d3-$H1I)psDqG2uuaKKDnvePv3l;S_Mj*BSfVjVBKahtcVnKM$4vl(x z#B<=%Sa$&n7f#7iZ?E`h&)&WEOd!gsp%C5U<1_%{;}GgpyjAJi**tcSTucgCpHV{d zavEInE5v%WU>2x}BRXXU0ZN1%f`rN&n(EqigD=mOmUkk6aZ3D`Ru9s6Z*%?dXYJvwr zS(2k8toregjTo$-{vd zpW(cZNvN`p=KU_=)h?So^(U@PI+n?o0dR>F~+&5yj>_=C;g z-F&^dZ>7HJa@(c0RmhR{p9-EpkawSnZ;*%CeI)Nb8llz3G=$yoMehh?Vm|od`lb)4 z&J9#&Hl^#Xvs+Y7BbM?yIaxS1WXHsCB{gwS_pc~kHyi`h4ZSr~Zr^of-xcXQ#ie3- z_ZP1@ukHVn-rw)N)>A(IrE+wn>>pWAE>urOE`J8k17f|y_rQ9NTCGRi8U!^7*>w-R zSfd9Kw`LwXjxPxMlheu9^Rt3LN3N8N{lyRmLna7XHH z2ZdPYr-(BdMysqofy{4HQ%mGf8s+_07ULj|e9AZM%&~cu^$f1EyN8u23?7kJ-H$S)< z6n5`j>FQn$h@S1YVp~={_+9l1jXPI;G_#KO?_S;OBZG;brS~G%;Sr-YVBcj{udup{@?YOlDgnC1u6kC zKS!0Hqe>GsetM{qaa85k!B>VsE7lp}kK@{wX=ErGF22D`eHIOZn$illvg9$^qk$>(8mt2IT5k z9Q)QwZ@pCR=)Q8`O6fbVExlIm>ASY!TK}IM{r#iYo-KcAq#PYB`$yLg?muM6rTh!( z+ah%ok&Lke3_heGk{VvT>3!jHV%U-3nCo!*HngygOs=E^6 zo}7<{E8n6h4a* z(ixeQWEGn#B50pJRB6fL9uBKXjShX;^b}L7{~F#H$WCPJK;5MHo2(i4QImx&HJRjs z(u+s|4^(I#sL-qxs;6X@VZ?z7Ex=<7!kM7qvwr71)(SRdriKd512viX&Qxf`g4x4V zXy(#_Dcj64=G;hUmIL*gJN zzv4YM6#qU{e)~QLR%@SpaqI{JcQg5lmMu@Hr**A{&*#?UfnD}pvVrSt%ANcfoePBJ)4|XiR_|Cca z!aWbHA8gzVHDTa>+Ct)MU|5c|e+|!r?a9_wa@AJRSX>$3O@4_Y^~d7MXTnC9_4~Vy zt2)<*UeOtMF})&3qj4Xs%CJqVkB23b_`nf(xUX8V#XdGR)Y=K0sy>4pbiHExvcKKb zE5_a&yfCQ0)lxBrgh0nn1+SrE)MXfJ6A=jO^Q|c00ab45Cs(;42UD7{`Y;EfHO6HU za`BU@=a9n#l&7omOe)Y-d2ydjl~*}2@2k|Sw~sH|5$>XqCDgZGt9L8bbUAVp6x<)*3Ztf4x28(=9%Ei}?jx=_+GZ`WzkY@$Z`E^E$7oQZ zI}@|wS6=P<$`<9-Zn3r-hmEP1@2SxiH`^1$7Wd=Gw3<;6hv$Su40)aYy^?){*}&4` zX(FNl#D)Ssy7xx(iRI`M#1Ik}5;wxzmc!d>@(KzZzA#KbM=u;*i}+%}Rl%pTiMZ=f zA`uUBeZt#RrEkNT-NDRUPgatBHDdw+`GfvJVIhGy38G5ZU1|Z>~kn_Ls&qSJT3@`wVZdlm3X{GxSQtC$H zx>6UvQ0g{6wdy0E9}{UcK)xU;dPC$3lcJa6W12zH8-;J}d50g0-o~h>b8RbBytlo0 zWh+Nkb_QjekFjQ0>qpSN7EZ=O$@-Oix*3{ZH$w|>Gqf3RJO~mx!K^!nr)J~FmFzj% z@g@6H-QpR9CUQ$pOTVl305<$8s=$m{EHI(frP7L$p3&_LR7QFkbIE}OZu~PL;RTtI zV6Lz%a8y|q*q2PsswGp{S~6id!6Xe6-3<|Ycpb5U?l#;X-EAy~w%-i5 zF7CPV%zNP{KQ`LCrSAqewp63NuSHincP+Tz_qVTCu23TwaUEsEWtptZIjpt!oN3r) zkb`moD1ivG>OrW^m+c=O1ZN^Yw>fHM3Y_}sSvTT$3Q9pPI}hP zV-;K>TDvc1iP;H?jMyfLoEDIjAWPFl`5R;JY=1lXUa0H7faOMP`*Li%fm|X!K#|x- zBbUY-v2Dw-ZN}DR#0Llyts@9i2ME(?hcKv>`Ou_hR8@xO7Y$i}rp%EvTO?f7*(1jr z58VfEa2yhk2b6b_h4Ni;?g=(5GN_3-DmV%U95X}$31muuF5sN)AHLVY=BIM&(gnU3SPp(GDR|izG7KLwZe?2yu9|{-%gi5IZAUd0Ltc~Hz zuYUvtkvtassP;+T1z{2g>fVUJ($U8u$%62TBS}^yel4Kmk!qD~LB}CgDqDuyDnN$0 z7U)4Gf75}rD1P~K!8a#-9jk5#ZvYAiX3w10K;Kw}!wNh71GEuKE1UT}hd))t{_HH} zAIP4N*KM70kq2{4Cl1!YdJmR-bWn(Nq&N(go2ik66iL6hI?oE=MAn!zLt#OQlLBP) z3Uftf#yCSJi|0>gGp8%mwJfy=!&%Z0vqKlW3q|#GaT*5=YWLaarj>tZ)5=dp&&n(- zKc->jllJlMGtD#86WLt01gl-li%dqIf}N^3rJrIXlT~KI$!Wo6T3dUU{R_2=7_T7B z_4b>!GyA(3f-X5V53G7+g{^U66lFqxMr<}{TmmX-)X+m{BG+HkNs%>`~KtW!MO>eeh+Cs4V5fAWl{s6 ze=?7{y!OsJo$Z7q}}A*^Cgc@hD$&9;8G zscyeZ0)G$@YMF%Us@r-(La-67(B8In-e}vs+_u}W6pb=VQ3E9{xuKs)EY;Z*G>lqE zl~z;z!W#VV>Fd1|F(5q#5ucz)bSk47-B2@ee3)ymGIB1LO5ywuVN0f^`XVPnq7{Xl ze@rbWH1)T-3TA^%1@qKDA}vgGhv!QU27Sl_>{Gvg)yKRX{4g&^Kg`SF5AzcK;pwr4 z2-f|yo=96Hg zOl|ryM8oG>4M;=BmOn>9pQE6UbA0s|cDMO+6jX7pCZSvd>r?&!&b@F=gT>5DVDpC% zG0GoO$`{~N*e(9U!!BdWAEA*6<&UYowJ}JGy^m}l;Bl<=z+JyFBYF7-jO^w5j;ky-uD|ik zc%bpXVQ8Gp!_^&fZnm>h(r4)lElxOhqEr94E5k53s-RNB!q{bStleaFu&cj~{ft+k z_Bf;0Rj&g3k<1g&^L5s~ighk}4)?JBl1vUpa;!3RZ5b9F)sVxU+Mk<<1FTQx0zRU28RcR_w}O| zHNr=a9~l@K_=vPe`bLio)Ji-y@ZuN_e_EQl=jp;EU0tApUNX692}f5arm^T(WQ;-y zqloK$*b&uUE1f9r7RTwB>v*r2L1z~8?9eMsOFBwRTcqsT20mGLbGtxvRn{7AT3qX+ zn&yd(Yb$U86WuXoA3IjwbRmmFxhN4D-ME;%bW)VSk7(~z=^j-W6E~|-! zhgpUbxQ6AFg3F4e>P&5w*VSF17G9EsZX&ucM~)g|){B&N#HCfY~hKL_fcc*Jj15A&KLTin1 zKK%-1oJJi#-Hhc|rof*1CZR_il|gNiigKO?y*(?Q;0L;Oav>hWA~Oy+HZ~Nms5BWg z>i5Qo#wlwy57N_^ROqb?t`Y)aunP=qLqo(pGsRnu#v@*0w6Ql~b9ei$dL z$cMP1cw+3JNDIYMHV^hja~W(9oR=|K#ua$`tA-J@UdpC(hnRYK;1$|2pflms*~n_Rr&ZXTxVx7`U)Q52hOR+Xr=X8t-$ zt12EtBxTZybX+OYPL|%tT&{RtB?buHIyj-uPN@UsR~TnCE)K|46wfd2nM#*VtDGsM z#c`tm-uUqpawP(ys(nS=x_#%)tzxgZ_f-;T6?q{SjC-L+9KjWOpitGMs%M|U@%?>O z?Kqu<+Ls2Xf~DHHpTU!PM2lzDxE&#fTmL|EBsN(p5+0x?nWi&wbAH_cZdU-0;rBw| zyg{L&_PCeGm<+Y+zuzXiMrGXDfc`M1jls9rP&4ia+M32qNXE8@e!Bx|!Y>|BaczTH zJO&L{7#PjK^41Hy|38C4Xi`r#y$>)NWX*vx_wmZd{L1qGC`s2u0;q8 zGrYEf{-Z8@V089zHU0a{g3t zQfW%<-Lr>X%_dF26hFTQ#HH@Zr&Ua@JsDi&w})l5+eo^{%usU^EJ|oDbR|=U!`!F= zFkN#&gevv$@oOjQSlATqIBZG{nu{H4A%ng0U#2K-q7==0mW@5ju;#HtD+_b^{{Lu2 zNGG(gEivm5V(p9juSdlNH!gpBv-d*p#mVcTP36#zo140?j9uTfZ{g6zFI-66-1zVn z_w|jt7rYD87a}(sd#;@O&g|0c4|iQFUEecwz46#WV5JS2!0IT}SPqFRyY?^ilcD3{ zb3YDut-6FTKapKwii(PGgON6P=t?Ep#IJs^)9si&!R~sY1)edRF`QXk|A=)3;TfV- z#s~Q&U>uRLw#yc>q#G&0g)CMVcPJW)TX*FG-Mlp0VCy=g&;w3%9gPrdx^RHM@y*vR zyjJ#iGCfEFmVW;gpP;K}DWBtK;}u$CT30bK3(PpbtZB9m*T$5r1Nisb3efyORoJpB z&G5USNQupg=34+irdM+A&;aj%hjsv9AGDcnWa@W#9$Ou93f{WEak@R5*Ie*@NIsqe zrOV*od|ce;SUUvI`gJ;4DoEaHdj*7%l>+W%!us7$rjxeyyDJ{hVzx^#`6|9bUoWcc z_Rn=3Pq~gtRGyNj`mr_qw^{wNpdK1F{>(XFkJ5U%7ClK$S{;*3V$H8?g^tyAl&!9f z3~j%BbzM7kEsepP`zp~n9%eS-Nq-+ibWp!qP~`m~GKxX7KaI-;Iq|1>R~okfk@S6_ z+k@=MD_8fr)Uhu3)h{YEFZ^XQ(!vVXeA81BBh-(U!B-W#n;?g7Q?f24T?@xklCLTy zw^aZno=i{YV3`1xzm>OQYmeARmJJ0!fO9fmN0_TxaqBr?iz47UT|B#WU74K2_KKL# zBm%Mi)x{nYC^=>&Q6+hG7*Q!s0DELyf5eO76b=-oa5x44VZfD2z#%@~^T;l-uY^05 zVP=dKwX9e*w4^P>KD%6Le~+~pZhf^bU)taExG9k;L@4V|A`;$QtCd!13FSY7h_#?K z@a#wiu2pPL@m5(J(~?(gI~|8bNoXnmogB)AALby|sKUlbAWUMvJgK!hKT~)O69*t&%qE2s2FgS&zO= znNzpkjhI$t$ldX^J8ExEP|+;(OT^ zIB9Pur*ZEgj7&LM{f==Peq)%<{LbsD zDveZ~CUTl7eUp)%-2sa`263aTQebBiwXpGxt{vsKJkkk8%>^X_`h?A~AsB2ae@>z9 z0rUyK7&uAU&yz#P$k~wy<$L6#J$belz|a93(W;@o>wlVGD*deTe-N3-9`~jT!m7*R zaD3P)IJSKtZ2e0ibjKNRIPVB>*2wv3v*6hIfzbYe@WfAqXMQ3KE(?P{5e_U12R;z? z{e>&`fqN@RqOM`Z9bPzb@zF)kV&YFcS7JDximtzmjM{avN z9PJC+7D{i|k zsq?4j+~Eu3}8_pM#|Ittaj(>iR=J0g7ir@)T1tNMXXm39h8 zlz-CcdOT9;`)(3WTq$WquO?AN&(W-OIiAibQc9DO@wBGwV`G$(=p;>7r?i3c8qZ9{ zX*?sPvbsWH;xp12C81_zO`1^jX+_CO3E)MObX9uQ)V@~-Z2G0SnN&7bo_{KpO=WT! zNvpa}Dk;4>9lxZ!3jEpY;C7IVB1@MQ3L8)AN_HC=ll(K%^S1tP87dB`Be6TWg|B6W1|tT;fyJlm8@>K?Tr}zSmnNqnwUx7aK%uBQ;LjD%-#$! zh6D|7OuLktifL&@nKFW<&e(J+osKD2QaVMIfw5>|5z!DdC7m=}mf#HECUVXaH33)F zP!ZXgKmM!XXU@H>DO5W*m4gM&>FRV=yNFaX@pNn=r=_xrrrFK?iHmW4LeNRKBHzYsDEbZBy7Sozkw<6A~BFuN{_{6TDDr} zfYr4VxYk~TWS$iJNYjo5VbNP?>4dDdursnCth!nYO*^6LD>P%~FC2WD)xs^1c?xY3 zj%hA*?#8_RLAdkhuPg{}de&h{lCX^$DrkPp1x!K#oHv*vRAgWB!C&~%CQ_LUWrgK#|SnC!56%DJ^C zj^w#K*F|(YUUd`7b7Y(>emYIA@Z)3}dJlj$P6Or{hx6vh;>T8dm053MG&^z0g1vYJgO zYP}Cbkm{e0)qxQ6tRHyADh1oBejEIu;A@Iq_+Ng^jh>G@w;~w>KfMSdOoY`DSE_U zOQEgf{lRw!m)j05hYmto2sJOAym@jh)Uy)mSq=3TU1ay(g^^X?uELJag^|TmkJ@03 z;tm2KqNeB&+`&R)>r(n=`m@HN5A?sE{pIXh@8C-B;Jx0Vh2sxBB+&FA+`bm>Sqb-i zIQH@Qt?^IC{w?~?(be#Yg=2*s(!#ODQxDp9y?^B0BOf;2YugVLPCoFpzoW1Ex{FSy zzp)T#T=L!Y{Y`B#2y1<{(L$Pce@Ps{mM=ohMG^AHj~b96+}YO~yGWoNdfmYvkx|!hOO~37 zjMhx!Ha%tSDHy4t^94oqkrqH6Z~D!MSScb@d?uoKC_E`5zP6J^9T>6on}oSrzgf=m zg5ZbY2Ywi-uur#d7fFZZlk>##Z@0KnFj90Rv=@^-m^=lEC4?HZay6${ih&KSVyx(i zeh725=OF?3;H|qpcx`am+XdF;4;KO<)c>_W=SraSgWmgrzM@Ec-Cu1uNx19pPA)eb z0Dbp|zX*gt-~Hjonh#d_S=(N6yY2}8uiO!S6R!Bb6|K{})R{2d1id!Cmo!z)4zLr) z^P=6zbp=oo@Fr1ZhuECQv2IA_$?qFm;f%f8&v*-W?5+qi%r8&q5R^dMaEF$leOYDN zz9Uph+o;P;$#{Xb0R4=#P5rivn1gjoEEZaNfW_MFz-$YPEF_xzL7E4)M#q3dG~;d3 z@sw{PGi}>=DR<7pWDT#VpbKps3&MR@^L9RLfOr)KKFJ)u0U!o92XlBmU_T2IAuy@Z z=GY2G@+FXRiys|37U9h(i&4}JwObX$AB$-h)m&O;cea`KZBp3O{}U9%bEY8tK{I{{ z?iPq&*XQ1tD@6Kl2&;kJg{HPgHPBonn7Bx&<&hgokFfd>6VoK#3?By2bD00NI8?LA zBoOp6$1(3X% zTCxc%SkM!&dc<$E$F{2bCT*v+Fw8`t{_n&!<>-}!!rlWbpey0aAxB)^nwJpuwoPGPph3-87nobo$Ef9^GcNaq>5L&D2_^hs@(6#4=a6cgZH{i;l zcZcq^?fp>xID0F*(l&U*RcLL0zyELhKN|UX^w#LTeS@D~`E1|u2Y@(VxcS06`dY_< zm5u|ep{J0{kKXzbyh0Rf2&}Vt%DsKZ0nox@#K_|JihD^zlgZ;+{qC=kQu_LnpFT|Jd5iIm|!yuhycW`LVKRsb zLpq32=m;jqF!>%Ps7$vAi&5-aLNUy<1m88Z2}oW?XxGtI^tOxLzprm9ijWtb@CH?^ z!4e!sSJ90n52@R+j-%GU&2eIsTX*(~jT@&qr?_(?;1t8_0f*SO?&8GV>rPIT)}5l* zvQgV8N+x-vyve(x?3i~)E|!RHdx~n$CbJa)(29g}Tp0>Z;__R*%^=cL43Q4ND1*mK zVJN0%?R#KMR?>jTWsv;Q9~>QidF;j0XAC|r%b*4>3!BrkFnN=;?uQ17$w%0O?+Nqd zfhTx<@3p-*F0M+4?s*Q+4?hrU|9bSb(HjTv3r*Yc0!Az*2jmRo0IB&^1;!QL+=5*W zdLAaAvS!rF3Sb<@C5qF$i_&vYWXZ3fprIcJCWYEHA-p1lZzS#uttMB{VSo1N{0lTy zay8H8iH`2TEZ z{Q2Ppm*H(F_(Io5uZ`aKfrUXASl<%{ow|1Folbig+}A^|onQwAn?Wv3W3Kc*calCkbbLcJ z4Q+UK+HpvVJjZbl+X;8*k7U;$i05+>_?&n@C$;}Y`c_EaA4$(6=ZJ%o9uk&D1yURQ Y>C5w;b*F=CUiWZZ+lH%w3o%0f3rNOTRR910 literal 0 HcmV?d00001 diff --git a/venv/Lib/site-packages/pymongo/__pycache__/pool.cpython-312.pyc b/venv/Lib/site-packages/pymongo/__pycache__/pool.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4e170e91c2c842b815a29ff01fe771909448b8a5 GIT binary patch literal 92438 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 b8xVeA;Ey;gAEZ(K4>D{=3QZpr7!dw%6RJ=X literal 0 HcmV?d00001 diff --git a/venv/Lib/site-packages/pymongo/__pycache__/pyopenssl_context.cpython-312.pyc b/venv/Lib/site-packages/pymongo/__pycache__/pyopenssl_context.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d5447bb4b44b6fe90ab45532c0ac4cfe89e666e2 GIT binary patch literal 18420 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 YANM!@%A@7hzujj$YBT?xl}Gr00AlejZvX%Q literal 0 HcmV?d00001 diff --git a/venv/Lib/site-packages/pymongo/__pycache__/read_concern.cpython-312.pyc b/venv/Lib/site-packages/pymongo/__pycache__/read_concern.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d895b6150298c991723f201c8960d3ad4ad3f1cb GIT binary patch literal 3013 zcmai0&2JM&6rc51jGe@RK!GNZ0U8Obi4hd3(l()uLa3-xBZY7=LF5>NR>GCycMhb*!EMjYwNFkyj9krvCSRq!47vdU; zkdFzA-6kylBogc_BqD?-p0xB8y6TB+S7UhAb!|z@uAtXkam8{j(rcEwLS9L!2()^sq#TlVrWD>vpWLjb&I8{!{fxw$Dq?fWec2 zz2=u1A))V7io(#VrZ6ki<_p}`>0zk^IGd%jm-q%}0|6?lykwOvE~x?me-m}fz9Ljc zsU=<82g1kBhs&*U5nz*}^b{bww$&_@Sb0?(m8thWsp~{8h+giW}>bQ){96enQxn@Z? zv#e;%X*38Jk}`^nMNZuKb$SumQK>d3ZJS0tNSlNcR-ov5z1Jky@E_I{>iBl*Tq5Uff~ zDu4uh4M_afvKRMP zB-%mg%UhHB+&O?n&Q$>gdrrC69C^vA8nR;A#yrNDOW9$MmLO8|u6o5lIeWA%IErfv z+*z2Lw&v%grTA#oEL|}#ayXz~ah;3q5zG>!RhGFb6rW*Oj-?Evu`}dBuFX9NhRQ=Q z-6X5&zQytHK6z04q4ad{*y;dXJo&}+pF0MY2giTiabmgq#5w|6>VyG4koLp$HV{js zNiM_x>*Fl+g~skTS5Ypji8O3@tcf1@lc+ z5pOp_D0(0(RVea#oBNHjQy{L*fFU@o3Rk%$*KXH>%yne)kU(z9v;hzFQS`$U0>$fb zuNcsYA^$F#rFvs;7@iGFqqy+^OaM~~VQSoId^5Y8Iq*wHe=z%~@UZaX`zyoxa#G)n z9H16P<IUwB8h1kpX_Ez*QAo0u}T|Xg8Zs zY;_B4qR=tmKH<9Ffe89^$u_0biy`MB8j@O-N==!7dyakzgu(`jy?Md3YkXsG)=i~^ zUThmx1oNPg!$}lXC?44Krh{I^%8yVs;^%Lh#w7NlptjWk{8aXm?Q=HPV4Hg!Hc2c* zh*clSY+sD7rZe}(?vAYv41KSEt1nL8@47R+n%S|K3W61eJO=yxG%o_NME<8s?uCHi zj@L$0dlNTEeU)enpM^5bXJZ219YkPv6@fO4Iat4U_D8yaBUTnZ2VHo@> zbfO)h$U!GU-UMip!!X?>&(gc@^Y6|-O=nm8cQ1{9HU41gQT}0mrT^G+_pxW0eap#x zn{&2P?K57eW^!^TSg(dF)=tC{`2Q(_Q=0AsBFmp$!=(|NdpE_jLHQuktb zO99M>Kt^ERv}i9j4^Oq$N28ZnC50PDC_KV0fX@9WkI@fyBHC>&_^8aL@v?Bt3O5Wd zWf&Eg)oi4@4dZIfv|B4(h5?BS?)JeRFA2>I9yO{S^sC*d9kW6V;e-w7F$o;=o4TJh zIPHR;oPp^!c^*l{lP~tf;vcSclSJ=7kw|=SJqpwsQfo<{7mI(;;sMo?OU3(JJf!wS zf!)nZsCpIJLx4scIDS5RJ5NrXnm9K-Ys^ed zoHQoS!hwV~$( RCr2ZD4t;%Nh3xdz{09-#>cRj3 literal 0 HcmV?d00001 diff --git a/venv/Lib/site-packages/pymongo/__pycache__/read_preferences.cpython-312.pyc b/venv/Lib/site-packages/pymongo/__pycache__/read_preferences.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5aa08606bac91a1fa6ca1b723e00fdb2394ca29b GIT binary patch literal 25568 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 literal 0 HcmV?d00001 diff --git a/venv/Lib/site-packages/pymongo/__pycache__/response.cpython-312.pyc b/venv/Lib/site-packages/pymongo/__pycache__/response.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d44a0f6731fb04db5be824423e7240658f2e9b29 GIT binary patch literal 5208 zcmc&&+ix6K89%c-yF0tyn{SCPv3=|+Z8z9%xTRnsNSc<$G_ga|qIPX-G@d!Ohs=?-rk*jg^{knz=S-Nd;Qe`+QjeWL>Q9PCAtKw~DpW#_=~$Qn z4cl@UUE#J#-8DuX#;-Cy{ZAZI6TB|PvTRpzb?8;gdZoACz4V%V?)+=#-k6a8Ldl8Z;5xp`RZAa6g4p+$$_M#WfBqzBxNm|kmNy9(Q*k%0VGxMMrY=( zR=(ElNpFUhzMb_lvZlBSY*7if*d=qcm*dL1=H+Fr$$f)+B^li!tF~z>mWJbO)lqW^ z8=%9%48q z*Urr>z76T@EH;`j>!NF~Th5x^kR4Mo%#897(7b5!^dxmpGpI}gX7RF?~c^4Ikek9cvJkaum@v^8pkv=koPlS2$)-B zll&Z>`+t|Alx>V(_DfqFfiF0eE9>-?%ky(|x#covx+d^m7CB}Y-oPQXSJ{1{bC8uI$~^&M&CSrnX}Jy-gQu_>MIGENTzSKk402@rw2pcSM3~r9r=NLm?TDM$|j(XG^A&!L@T`Y05h$k-F zw!tZO8jiroLI_kA9G6d0fG$f*h($Pao801)(6^T2--L(1R>V&wU)*edSQ-=M!2 ze;2m{TLsk!z$f8I&@j%QfNYW7>cWlLws<5n^tbZyb_SkMm3oHSIpieLKhVx2S0KI5 zx4V!llB2`z5^`lyIk8tk?j*^U|CLH*hW66HwUL9dfovp}jl?n<%V;bcjb)>;Y$BFT z#Ij(G2e<~uFz`2|0u%qz0VnLO0rRV;M0gSI(=s>73Lx;UqL22n2hd*bakO`nyf4%x zI4C5qd`Sl+szo^kF_HlcW?6v8Oq(;=wGomrH6LqAhy*<7EA0IQnBNJvh#SA$&To9|yFa>wRyrs1Kh zD+UEX$y?J;z+?X-K!#Qe@KgufQvf3xU9q_jjP_}}4?K3xi2oa)JETfK4WNtAFI2l3 zdO-&V0rrc^rPBd^VZUTt&Cpk%c7Y!?U4?sqwMy657_7-dA5*}ToGDt1LIf+h-XIUBNxr}r@RmlG6<1wjaD}iSa={0VtzKbp zL0Q+`HM{B3bp;X+yak~hPqfP1i%>G0&iz_7bhye+(x&C=M)GdpFc&==dyobh!aELz zx0vvp+q~mmICg!H?gxZFLMQa@a{~Go3sVGTs{b9_`U^}F-ei-J(UK8*&~{*3-*g=t z#P$MHA^drwEzm-T+5iLk04P3#HW=P#gJdLMM_YslsQ|XPS4kqIS~bWGLp*#_#j9ZLius@QY!N2oMAYUR%IDsf(Fmu933GfV2 z0&)_m47T&g6%a3UAy-7aP(rSZc%gz^l~l&t-N^NT?2BIHPWdPy7b6GY;4LBtF%z8r z50MK_demyJcEroc_(datODj!4VE~snp88(aZ8ad5IDV7h_$h+pfywcD%kik?c=p0w zSaBI5KaOR{tAy9k>G0D6@4}u%d{vBIH4qk}y*s^wrJMj@R&a=quYCJ`51Y>+!E4|n z>2!eVe#kFC6OHYBA4oeb2*QI=B23;V6Q7gWdt~FRxP;&bujees3Q#8dyyzAOwq UAV40v!jv%a+jqVo@bIVp5B0PQjQ{`u literal 0 HcmV?d00001 diff --git a/venv/Lib/site-packages/pymongo/__pycache__/results.cpython-312.pyc b/venv/Lib/site-packages/pymongo/__pycache__/results.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..20764e4bf161b9a4ec5e2c7e4e32e0ebc6a6365d GIT binary patch literal 12568 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 literal 0 HcmV?d00001 diff --git a/venv/Lib/site-packages/pymongo/__pycache__/saslprep.cpython-312.pyc b/venv/Lib/site-packages/pymongo/__pycache__/saslprep.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f7ac0ab259313f82855b7cd261b0572b834540d3 GIT binary patch literal 3487 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 literal 0 HcmV?d00001 diff --git a/venv/Lib/site-packages/pymongo/__pycache__/server_selectors.cpython-312.pyc b/venv/Lib/site-packages/pymongo/__pycache__/server_selectors.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e67b8fe888cd3fe3e5f49a71fae239e4c41bd59c GIT binary patch literal 8060 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 literal 0 HcmV?d00001 diff --git a/venv/Lib/site-packages/pymongo/__pycache__/server_type.cpython-312.pyc b/venv/Lib/site-packages/pymongo/__pycache__/server_type.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..82a50e2d09a25a602cda3de68b46d4ce4a084ac8 GIT binary patch literal 864 zcmYk4&ubGw6vt<>o6XOfwzgFSYY-74rs6>mDT39~gE1}HRxkvH$?h~+x;qnRb}R9u zL{EZvE8@kg^k48l@FM6T40sX`y;<5*PtNQ%TL<>d=e>FR=FNBJ+tgGMsPWEP-~MU< z@LM~Rkxc*|H5BXt7qo#3-HZ=|OgjTL8@>^kZ4-hFSOc#42Dn*b?yEX_w6hsN^k=!& zK1^coK|43h)$%W|Yz_yc>M)mx>NexmCye&kqm`;4e3x*sbfEKVP?j*IEX1M5s8DXP zi38$pMgyNXNyWsTZV8Y2%HIP7w4rJ@v!$9OhMQSd(U^>s%#^YzSt;dG%BNIFsiS`AG}7>BD05qMhq4z`TJe#DEVS$m?gf|+WvOM`#9`Fcj*36vT`weD=34f8 z*iT@s&qOGTc8FD)&nS`Q2E*yyN~ zUfU&fx6|;t9idKBA7Ez(_lVkH80bsAm0T$bl|)N}A-7Z;v6)vOZY%Q^jHjkYjd8g$ zdOWVoj+*0{xgX|Sg7Hdhrq7So$FuW4&3V2ssc}$H)U4%Y4k4F0i6q-m7;z;D*RMia zYGaS~xH*;C>Z9dyr<+_=q>;*+9`Q=2{`+QNk`tgdJ%KSATgold& oUjGHI{sEVdtpZ&5aN`)L>F^?`T>5zD^USBa-_KopU5cyy1MzI~ivR!s literal 0 HcmV?d00001 diff --git a/venv/Lib/site-packages/pymongo/__pycache__/settings.cpython-312.pyc b/venv/Lib/site-packages/pymongo/__pycache__/settings.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..beeadf5aa9c05a1bdf27697be129546ba9de2b5c GIT binary patch literal 8201 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 literal 0 HcmV?d00001 diff --git a/venv/Lib/site-packages/pymongo/__pycache__/srv_resolver.cpython-312.pyc b/venv/Lib/site-packages/pymongo/__pycache__/srv_resolver.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4ecb5d4b46526ff90d7354dc64b08c457bcae08c GIT binary patch literal 6338 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 XS-evR*GHBhhc-BK(8X*xDX9MkZ;Yln literal 0 HcmV?d00001 diff --git a/venv/Lib/site-packages/pymongo/__pycache__/ssl_context.cpython-312.pyc b/venv/Lib/site-packages/pymongo/__pycache__/ssl_context.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..540c4eeee5a44045eb08023587ad624de68c02ff GIT binary patch literal 1061 zcmZXT&rj1}7{}kPV;$Z2u?;YSnn^e?$uNk7V8jGVfo5f0+9{z~@}{gWFzMRab|{{9 z)Oe7aJ9@-$;i!Lumy(&FO*A3IgEt5_PQIlB7I_cfywB%(-sk$0s;JxDbjM3eIUuj}@Y6s3H$SpF}`R49Sa#h(tNlD@vL$}duJ6S4M zG*m{qt=J_)AN}jCP7!WI(RIUTR@P1AL?JIQ0uS@N1R0)Vvw1fnv5)^pz*K_c<$abIXtsdB`lVOa}7o z3@$Pwp1B^Ef>1Sz5V|ykt5v&dA_z_I7G2V^G&cx6rPdJkZ<}t{rTKHKBgF5$t>B(NAah;z+|}WJmdt(ilQJ& zZWx!HXHDS|uG> z)+J6C*A;uku&;nur=>3n(_U^9>6-<vw>ZYXOAZ?IK1^flu?{wT2M-}-1} z>n!}ZY^YTYJuwHcA^iUUYY)pYNf5-z9Y75K0)s~&ei8zS{`!!UoY=DuCNuk!nMRzi zb^Yw@-B#<1J7bN`bS?5<)H6_@bp|K)UN(BB91uI~yIohEp{XxpjlO9IBo67=_E5e3 z(?o+#)uFufT96&p literal 0 HcmV?d00001 diff --git a/venv/Lib/site-packages/pymongo/__pycache__/ssl_support.cpython-312.pyc b/venv/Lib/site-packages/pymongo/__pycache__/ssl_support.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0be9d3b87f7cc637a88fea84b5f85b5601f1d9a3 GIT binary patch literal 3571 zcmbVO&2JmW6`$pj+$FjEFe%HT70WBhvQ67!Vyi`4!%iVnl%+}`RZ?*)*2QAQT}dl1 zcbVDMC#XU;e2^0tP7j3x)B$o#U0TP$3LGtA*-KQHm3LO#y z>oSFy#1#%o!JCLk_9HoP4B;*cblZF2NnL(O+1+CIEXoZ?Vx7Qt< z2(NeNm&#=woBW)P`FwtoS2TWV?Hyg4*OUJyYbPixAZnU!il(A#1`OG3nLttfjIyFB zU7J(pm$1!EW31!p%xJ=8`BLfJw2;YW(pJ!hx%4mJ9nYmlicI@d5V=LzA+m5Eg)5Y%BWh+Q>S18B#DO91aU)pzG{OdDtz&z?u zC9iYeHz>2;p|*VuIthNBjsvKY4O**ECRC)piN~_Jgf)O9@F9>*dDZ0g*+sc%CP%GUaX~I#6ZE1{7GzB->xyPtjBdxi zky$@6g_UKZ>6*O8OS)`mCrrL1n#BdaC}UHZQ;NVn&##{}RU?&~992}AFWQ-epOyI~ zLzegz#aw`FlP${{SYREzlFp5f&Ilkaq{h;z31KolJc>!kB|0&QG)w}*atnr`TK=}t zR(L9xozAARlL8Qz&kb3jhVcu!VQOMYwwMLc5KR+fQc-Y#XbEJeAU`!G>>mtS2O6AI z_MNF*I-eiUW~^9)luKvQm$TF3!_$NqR&`Ml$Zmz=f~aT~3#;2X5R(*TapQSmY9>3C z&XCXTNYKNVQ}+$4C~OVJmL z5R678OP1HHFq}1U5~0}eYD`LX5`G4TQm~1hb)aK!|55qd#jU`TQ1p&|OTQ=W_MF@9 zIrlg;v_;nfo%a%t0{yjt3)S8WTd6O&o~OOXw^BP?&yzs(!=Y-|;7%Y}JC&{;PZMgt zP5sEX6X>h^Q8036;{AzxKicg+y$%1|>EBP(xbSz;&Te;~-R91|A~&?%J+#dY0WA=# z29DNUbWoydk=Smee>>9u$=IjTpMS9v`FY)oINDdU5!kC zb@cd?NJq7E@YDD9Xcr$Mw!Q%~f{|TrV4EBGWPXQBHuQsqP1?d2+?%yv*S+ya!2z4v zx6SqKaK~zg2e#Y>qG-1CqNJ04P`Pe9f9x}1fB>nJkcr-yjOn3knf!F$)m73b)fOHqhs6fHn&b3A* zT=)=>G6~vZ1%U*$AXt7ufM8exFBL3K5Z+r7)duG?v`188lF;pWb|b*ZAeH_ms<9j< z4$)u^0(!*^c#a@QIMg07{4O$^W+8ZHIf?!Tn6!Q}qx0Zre8Y+}JQfv0=3jplCD-}s z1$mz+uNXW$s(5i(R8$gYNlXlb=aolVDwWm@vLfHGpE$4W!E*qN5Hy?Us}Nu9O+5~c zRM`=GBZ&y^0nW>y^E&&DVdCFnQWf!8((oKpcD_w?S&Se^da-c@b4yT1;0iD~HOp(Rl@)CslNXs4Y6fUhCQn17kr&yq;VWs9 zc&)g@F0biO1olO&b>KA;CND87FkT|>RXfdEF=zh%nSuQS8V2AdhAFAaEZ}`*gc%#o z3;E1AsGJ%e8OaIx;nB414_5TjWHvQ1p1CZHXN7bwm(Atv3$T3Y)uL>dKumn5;X=g8 zUa}V&KJYdkfg!m)<2f`C02KAC2T^@rqQ0+D_t$9fX&}7G*4q85(b*|@c_IaZ4w*^R%o9e=Cp?yS29J+WH!Xq|@sX-99} z13iRvjIT2Uu?ToRg8XP8Q4bKrp}ynwAVDE?{8T+mPz3qo&kq24K1)%a;DU7j@cqCVq7Z0sB4rs9x^Jn?_9r(Qsogy{FgYq75PS=gs1{@*$S w0xyjp!^S`I4qrfjx!}1Jrysbymwc`VEJd)7xfJs}h`0!jQUu3ng1ZR!f8obnSO5S3 literal 0 HcmV?d00001 diff --git a/venv/Lib/site-packages/pymongo/__pycache__/topology.cpython-312.pyc b/venv/Lib/site-packages/pymongo/__pycache__/topology.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..004730854db7d03873c1cae89f31769cb3dbe699 GIT binary patch literal 44411 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 X?|%DWqR#Z9()g!gnCjQ1x!!*PrQ>a5 literal 0 HcmV?d00001 diff --git a/venv/Lib/site-packages/pymongo/__pycache__/uri_parser.cpython-312.pyc b/venv/Lib/site-packages/pymongo/__pycache__/uri_parser.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..efd5724860ab799c61cae6549b496a49dc84b210 GIT binary patch literal 23754 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%0AWylg3EhMfJ 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 dict[str, Any]: + # Deferred import to save overall import time. + from urllib.request import Request, urlopen + + url = "http://169.254.169.254/metadata/identity/oauth2/token" + url += "?api-version=2018-02-01" + url += f"&resource={resource}" + if client_id: + url += f"&client_id={client_id}" + headers = {"Metadata": "true", "Accept": "application/json"} + request = Request(url, headers=headers) # noqa: S310 + try: + with urlopen(request, timeout=timeout) as response: # noqa: S310 + status = response.status + body = response.read().decode("utf8") + except Exception as e: + msg = "Failed to acquire IMDS access token: %s" % e + raise ValueError(msg) from None + + if status != 200: + msg = "Failed to acquire IMDS access token." + raise ValueError(msg) + try: + data = json.loads(body) + except Exception: + raise ValueError("Azure IMDS response must be in JSON format.") from None + + for key in ["access_token", "expires_in"]: + if not data.get(key): + msg = "Azure IMDS response must contain %s, but was %s." + msg = msg % (key, body) + raise ValueError(msg) + + return data diff --git a/venv/Lib/site-packages/pymongo/_cmessage.cp312-win_amd64.pyd b/venv/Lib/site-packages/pymongo/_cmessage.cp312-win_amd64.pyd new file mode 100644 index 0000000000000000000000000000000000000000..ab0666d01c71b3d98d0d6dfb2e6daa80ffbdd05b GIT binary patch literal 56832 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, options_obj, &options))) { + return NULL; + } + buffer = pymongo_buffer_new(); + if (!buffer) { + goto fail; + } + + // save space for message length + length_location = pymongo_buffer_save_space(buffer, 4); + if (length_location == -1) { + goto fail; + } + + if (!buffer_write_int32(buffer, (int32_t)request_id) || + !buffer_write_bytes(buffer, "\x00\x00\x00\x00\xd4\x07\x00\x00", 8) || + !buffer_write_int32(buffer, (int32_t)flags) || + !buffer_write_bytes_ssize_t(buffer, collection_name, + collection_name_length + 1) || + !buffer_write_int32(buffer, (int32_t)num_to_skip) || + !buffer_write_int32(buffer, (int32_t)num_to_return)) { + goto fail; + } + + begin = pymongo_buffer_get_position(buffer); + if (!write_dict(state->_cbson, buffer, query, 0, &options, 1)) { + goto fail; + } + + max_size = pymongo_buffer_get_position(buffer) - begin; + + if (field_selector != Py_None) { + begin = pymongo_buffer_get_position(buffer); + if (!write_dict(state->_cbson, buffer, field_selector, 0, + &options, 1)) { + goto fail; + } + cur_size = pymongo_buffer_get_position(buffer) - begin; + max_size = (cur_size > max_size) ? cur_size : max_size; + } + + message_length = pymongo_buffer_get_position(buffer) - length_location; + buffer_write_int32_at_position( + buffer, length_location, (int32_t)message_length); + + /* objectify buffer */ + result = Py_BuildValue("iy#i", request_id, + pymongo_buffer_get_buffer(buffer), + (Py_ssize_t)pymongo_buffer_get_position(buffer), + max_size); +fail: + PyMem_Free(collection_name); + destroy_codec_options(&options); + if (buffer) { + pymongo_buffer_free(buffer); + } + return result; +} + +static PyObject* _cbson_get_more_message(PyObject* self, PyObject* args) { + /* NOTE just using a random number as the request_id */ + int request_id = rand(); + char* collection_name = NULL; + Py_ssize_t collection_name_length; + int num_to_return; + long long cursor_id; + buffer_t buffer = NULL; + int length_location, message_length; + PyObject* result = NULL; + + if (!PyArg_ParseTuple(args, "et#iL", + "utf-8", + &collection_name, + &collection_name_length, + &num_to_return, + &cursor_id)) { + return NULL; + } + buffer = pymongo_buffer_new(); + if (!buffer) { + goto fail; + } + + // save space for message length + length_location = pymongo_buffer_save_space(buffer, 4); + if (length_location == -1) { + goto fail; + } + if (!buffer_write_int32(buffer, (int32_t)request_id) || + !buffer_write_bytes(buffer, + "\x00\x00\x00\x00" + "\xd5\x07\x00\x00" + "\x00\x00\x00\x00", 12) || + !buffer_write_bytes_ssize_t(buffer, + collection_name, + collection_name_length + 1) || + !buffer_write_int32(buffer, (int32_t)num_to_return) || + !buffer_write_int64(buffer, (int64_t)cursor_id)) { + goto fail; + } + + message_length = pymongo_buffer_get_position(buffer) - length_location; + buffer_write_int32_at_position( + buffer, length_location, (int32_t)message_length); + + /* objectify buffer */ + result = Py_BuildValue("iy#", request_id, + pymongo_buffer_get_buffer(buffer), + (Py_ssize_t)pymongo_buffer_get_position(buffer)); +fail: + PyMem_Free(collection_name); + if (buffer) { + pymongo_buffer_free(buffer); + } + return result; +} + +/* + * NOTE this method handles multiple documents in a type one payload but + * it does not perform batch splitting and the total message size is + * only checked *after* generating the entire message. + */ +static PyObject* _cbson_op_msg(PyObject* self, PyObject* args) { + /* NOTE just using a random number as the request_id */ + int request_id = rand(); + unsigned int flags; + PyObject* command; + char* identifier = NULL; + Py_ssize_t identifier_length = 0; + PyObject* docs; + PyObject* doc; + PyObject* options_obj; + codec_options_t options; + buffer_t buffer = NULL; + int length_location, message_length; + int total_size = 0; + int max_doc_size = 0; + PyObject* result = NULL; + PyObject* iterator = NULL; + struct module_state *state = GETSTATE(self); + if (!state) { + return NULL; + } + + /*flags, command, identifier, docs, opts*/ + if (!(PyArg_ParseTuple(args, "IOet#OO", + &flags, + &command, + "utf-8", + &identifier, + &identifier_length, + &docs, + &options_obj) && + convert_codec_options(state->_cbson, options_obj, &options))) { + return NULL; + } + buffer = pymongo_buffer_new(); + if (!buffer) { + goto fail; + } + + // save space for message length + length_location = pymongo_buffer_save_space(buffer, 4); + if (length_location == -1) { + goto fail; + } + if (!buffer_write_int32(buffer, (int32_t)request_id) || + !buffer_write_bytes(buffer, + "\x00\x00\x00\x00" /* responseTo */ + "\xdd\x07\x00\x00" /* 2013 */, 8)) { + goto fail; + } + + if (!buffer_write_int32(buffer, (int32_t)flags) || + !buffer_write_bytes(buffer, "\x00", 1) /* Payload type 0 */) { + goto fail; + } + total_size = write_dict(state->_cbson, buffer, command, 0, + &options, 1); + if (!total_size) { + goto fail; + } + + if (identifier_length) { + int payload_one_length_location, payload_length; + /* Payload type 1 */ + if (!buffer_write_bytes(buffer, "\x01", 1)) { + goto fail; + } + /* save space for payload 0 length */ + payload_one_length_location = pymongo_buffer_save_space(buffer, 4); + /* C string identifier */ + if (!buffer_write_bytes_ssize_t(buffer, identifier, identifier_length + 1)) { + goto fail; + } + iterator = PyObject_GetIter(docs); + if (iterator == NULL) { + goto fail; + } + while ((doc = PyIter_Next(iterator)) != NULL) { + int encoded_doc_size = write_dict( + state->_cbson, buffer, doc, 0, &options, 1); + if (!encoded_doc_size) { + Py_CLEAR(doc); + goto fail; + } + if (encoded_doc_size > max_doc_size) { + max_doc_size = encoded_doc_size; + } + Py_CLEAR(doc); + } + + payload_length = pymongo_buffer_get_position(buffer) - payload_one_length_location; + buffer_write_int32_at_position( + buffer, payload_one_length_location, (int32_t)payload_length); + total_size += payload_length; + } + + message_length = pymongo_buffer_get_position(buffer) - length_location; + buffer_write_int32_at_position( + buffer, length_location, (int32_t)message_length); + + /* objectify buffer */ + result = Py_BuildValue("iy#ii", request_id, + pymongo_buffer_get_buffer(buffer), + (Py_ssize_t)pymongo_buffer_get_position(buffer), + total_size, + max_doc_size); +fail: + Py_XDECREF(iterator); + if (buffer) { + pymongo_buffer_free(buffer); + } + PyMem_Free(identifier); + destroy_codec_options(&options); + return result; +} + + +static void +_set_document_too_large(int size, long max) { + PyObject* DocumentTooLarge = _error("DocumentTooLarge"); + if (DocumentTooLarge) { + PyObject* error = PyUnicode_FromFormat(DOC_TOO_LARGE_FMT, size, max); + if (error) { + PyErr_SetObject(DocumentTooLarge, error); + Py_DECREF(error); + } + Py_DECREF(DocumentTooLarge); + } +} + +#define _INSERT 0 +#define _UPDATE 1 +#define _DELETE 2 + +/* OP_MSG ----------------------------------------------- */ + +static int +_batched_op_msg( + unsigned char op, unsigned char ack, + PyObject* command, PyObject* docs, PyObject* ctx, + PyObject* to_publish, codec_options_t options, + buffer_t buffer, struct module_state *state) { + + long max_bson_size; + long max_write_batch_size; + long max_message_size; + int idx = 0; + int size_location; + int position; + int length; + PyObject* max_bson_size_obj = NULL; + PyObject* max_write_batch_size_obj = NULL; + PyObject* max_message_size_obj = NULL; + PyObject* doc = NULL; + PyObject* iterator = NULL; + char* flags = ack ? "\x00\x00\x00\x00" : "\x02\x00\x00\x00"; + + max_bson_size_obj = PyObject_GetAttr(ctx, state->_max_bson_size_str); + max_bson_size = PyLong_AsLong(max_bson_size_obj); + Py_XDECREF(max_bson_size_obj); + if (max_bson_size == -1) { + return 0; + } + + max_write_batch_size_obj = PyObject_GetAttr(ctx, state->_max_write_batch_size_str); + max_write_batch_size = PyLong_AsLong(max_write_batch_size_obj); + Py_XDECREF(max_write_batch_size_obj); + if (max_write_batch_size == -1) { + return 0; + } + + max_message_size_obj = PyObject_GetAttr(ctx, state->_max_message_size_str); + max_message_size = PyLong_AsLong(max_message_size_obj); + Py_XDECREF(max_message_size_obj); + if (max_message_size == -1) { + return 0; + } + + if (!buffer_write_bytes(buffer, flags, 4)) { + return 0; + } + /* Type 0 Section */ + if (!buffer_write_bytes(buffer, "\x00", 1)) { + return 0; + } + if (!write_dict(state->_cbson, buffer, command, 0, + &options, 0)) { + return 0; + } + + /* Type 1 Section */ + if (!buffer_write_bytes(buffer, "\x01", 1)) { + return 0; + } + /* Save space for size */ + size_location = pymongo_buffer_save_space(buffer, 4); + if (size_location == -1) { + return 0; + } + + switch (op) { + case _INSERT: + { + if (!buffer_write_bytes(buffer, "documents\x00", 10)) + goto fail; + break; + } + case _UPDATE: + { + if (!buffer_write_bytes(buffer, "updates\x00", 8)) + goto fail; + break; + } + case _DELETE: + { + if (!buffer_write_bytes(buffer, "deletes\x00", 8)) + goto fail; + break; + } + default: + { + PyObject* InvalidOperation = _error("InvalidOperation"); + if (InvalidOperation) { + PyErr_SetString(InvalidOperation, "Unknown command"); + Py_DECREF(InvalidOperation); + } + return 0; + } + } + + iterator = PyObject_GetIter(docs); + if (iterator == NULL) { + PyObject* InvalidOperation = _error("InvalidOperation"); + if (InvalidOperation) { + PyErr_SetString(InvalidOperation, "input is not iterable"); + Py_DECREF(InvalidOperation); + } + return 0; + } + while ((doc = PyIter_Next(iterator)) != NULL) { + int cur_doc_begin = pymongo_buffer_get_position(buffer); + int cur_size; + int doc_too_large = 0; + int unacked_doc_too_large = 0; + if (!write_dict(state->_cbson, buffer, doc, 0, &options, 1)) { + goto fail; + } + cur_size = pymongo_buffer_get_position(buffer) - cur_doc_begin; + + /* Does the first document exceed max_message_size? */ + doc_too_large = (idx == 0 && (pymongo_buffer_get_position(buffer) > max_message_size)); + /* When OP_MSG is used unacknowledged we have to check + * document size client side or applications won't be notified. + * Otherwise we let the server deal with documents that are too large + * since ordered=False causes those documents to be skipped instead of + * halting the bulk write operation. + * */ + unacked_doc_too_large = (!ack && cur_size > max_bson_size); + if (doc_too_large || unacked_doc_too_large) { + if (op == _INSERT) { + _set_document_too_large(cur_size, max_bson_size); + } else { + PyObject* DocumentTooLarge = _error("DocumentTooLarge"); + if (DocumentTooLarge) { + /* + * There's nothing intelligent we can say + * about size for update and delete. + */ + PyErr_Format( + DocumentTooLarge, + "%s command document too large", + (op == _UPDATE) ? "update": "delete"); + Py_DECREF(DocumentTooLarge); + } + } + goto fail; + } + /* We have enough data, return this batch. */ + if (pymongo_buffer_get_position(buffer) > max_message_size) { + /* + * Roll the existing buffer back to the beginning + * of the last document encoded. + */ + pymongo_buffer_update_position(buffer, cur_doc_begin); + Py_CLEAR(doc); + break; + } + if (PyList_Append(to_publish, doc) < 0) { + goto fail; + } + Py_CLEAR(doc); + idx += 1; + /* We have enough documents, return this batch. */ + if (idx == max_write_batch_size) { + break; + } + } + Py_CLEAR(iterator); + + if (PyErr_Occurred()) { + goto fail; + } + + position = pymongo_buffer_get_position(buffer); + length = position - size_location; + buffer_write_int32_at_position(buffer, size_location, (int32_t)length); + return 1; + +fail: + Py_XDECREF(doc); + Py_XDECREF(iterator); + return 0; +} + +static PyObject* +_cbson_encode_batched_op_msg(PyObject* self, PyObject* args) { + unsigned char op; + unsigned char ack; + PyObject* command; + PyObject* docs; + PyObject* ctx = NULL; + PyObject* to_publish = NULL; + PyObject* result = NULL; + PyObject* options_obj; + codec_options_t options; + buffer_t buffer; + struct module_state *state = GETSTATE(self); + if (!state) { + return NULL; + } + + if (!(PyArg_ParseTuple(args, "bOObOO", + &op, &command, &docs, &ack, + &options_obj, &ctx) && + convert_codec_options(state->_cbson, options_obj, &options))) { + return NULL; + } + if (!(buffer = pymongo_buffer_new())) { + destroy_codec_options(&options); + return NULL; + } + if (!(to_publish = PyList_New(0))) { + goto fail; + } + + if (!_batched_op_msg( + op, + ack, + command, + docs, + ctx, + to_publish, + options, + buffer, + state)) { + goto fail; + } + + result = Py_BuildValue("y#O", + pymongo_buffer_get_buffer(buffer), + (Py_ssize_t)pymongo_buffer_get_position(buffer), + to_publish); +fail: + destroy_codec_options(&options); + pymongo_buffer_free(buffer); + Py_XDECREF(to_publish); + return result; +} + +static PyObject* +_cbson_batched_op_msg(PyObject* self, PyObject* args) { + unsigned char op; + unsigned char ack; + int request_id; + int position; + PyObject* command; + PyObject* docs; + PyObject* ctx = NULL; + PyObject* to_publish = NULL; + PyObject* result = NULL; + PyObject* options_obj; + codec_options_t options; + buffer_t buffer; + struct module_state *state = GETSTATE(self); + if (!state) { + return NULL; + } + + if (!(PyArg_ParseTuple(args, "bOObOO", + &op, &command, &docs, &ack, + &options_obj, &ctx) && + convert_codec_options(state->_cbson, options_obj, &options))) { + return NULL; + } + if (!(buffer = pymongo_buffer_new())) { + destroy_codec_options(&options); + return NULL; + } + /* Save space for message length and request id */ + if ((pymongo_buffer_save_space(buffer, 8)) == -1) { + goto fail; + } + if (!buffer_write_bytes(buffer, + "\x00\x00\x00\x00" /* responseTo */ + "\xdd\x07\x00\x00", /* opcode */ + 8)) { + goto fail; + } + if (!(to_publish = PyList_New(0))) { + goto fail; + } + + if (!_batched_op_msg( + op, + ack, + command, + docs, + ctx, + to_publish, + options, + buffer, + state)) { + goto fail; + } + + request_id = rand(); + position = pymongo_buffer_get_position(buffer); + buffer_write_int32_at_position(buffer, 0, (int32_t)position); + buffer_write_int32_at_position(buffer, 4, (int32_t)request_id); + result = Py_BuildValue("iy#O", request_id, + pymongo_buffer_get_buffer(buffer), + (Py_ssize_t)pymongo_buffer_get_position(buffer), + to_publish); +fail: + destroy_codec_options(&options); + pymongo_buffer_free(buffer); + Py_XDECREF(to_publish); + return result; +} + +/* End OP_MSG -------------------------------------------- */ + +static int +_batched_write_command( + char* ns, Py_ssize_t ns_len, unsigned char op, + PyObject* command, PyObject* docs, PyObject* ctx, + PyObject* to_publish, codec_options_t options, + buffer_t buffer, struct module_state *state) { + + long max_bson_size; + long max_cmd_size; + long max_write_batch_size; + long max_split_size; + int idx = 0; + int cmd_len_loc; + int lst_len_loc; + int position; + int length; + PyObject* max_bson_size_obj = NULL; + PyObject* max_write_batch_size_obj = NULL; + PyObject* max_split_size_obj = NULL; + PyObject* doc = NULL; + PyObject* iterator = NULL; + + max_bson_size_obj = PyObject_GetAttr(ctx, state->_max_bson_size_str); + max_bson_size = PyLong_AsLong(max_bson_size_obj); + Py_XDECREF(max_bson_size_obj); + if (max_bson_size == -1) { + return 0; + } + /* + * Max BSON object size + 16k - 2 bytes for ending NUL bytes + * XXX: This should come from the server - SERVER-10643 + */ + max_cmd_size = max_bson_size + 16382; + + max_write_batch_size_obj = PyObject_GetAttr(ctx, state->_max_write_batch_size_str); + max_write_batch_size = PyLong_AsLong(max_write_batch_size_obj); + Py_XDECREF(max_write_batch_size_obj); + if (max_write_batch_size == -1) { + return 0; + } + + // max_split_size is the size at which to perform a batch split. + // Normally this this value is equal to max_bson_size (16MiB). However, + // when auto encryption is enabled max_split_size is reduced to 2MiB. + max_split_size_obj = PyObject_GetAttr(ctx, state->_max_split_size_str); + max_split_size = PyLong_AsLong(max_split_size_obj); + Py_XDECREF(max_split_size_obj); + if (max_split_size == -1) { + return 0; + } + + if (!buffer_write_bytes(buffer, + "\x00\x00\x00\x00", /* flags */ + 4) || + !buffer_write_bytes_ssize_t(buffer, ns, ns_len + 1) || /* namespace */ + !buffer_write_bytes(buffer, + "\x00\x00\x00\x00" /* skip */ + "\xFF\xFF\xFF\xFF", /* limit (-1) */ + 8)) { + return 0; + } + + /* Position of command document length */ + cmd_len_loc = pymongo_buffer_get_position(buffer); + if (!write_dict(state->_cbson, buffer, command, 0, + &options, 0)) { + return 0; + } + + /* Write type byte for array */ + *(pymongo_buffer_get_buffer(buffer) + (pymongo_buffer_get_position(buffer) - 1)) = 0x4; + + switch (op) { + case _INSERT: + { + if (!buffer_write_bytes(buffer, "documents\x00", 10)) + goto fail; + break; + } + case _UPDATE: + { + if (!buffer_write_bytes(buffer, "updates\x00", 8)) + goto fail; + break; + } + case _DELETE: + { + if (!buffer_write_bytes(buffer, "deletes\x00", 8)) + goto fail; + break; + } + default: + { + PyObject* InvalidOperation = _error("InvalidOperation"); + if (InvalidOperation) { + PyErr_SetString(InvalidOperation, "Unknown command"); + Py_DECREF(InvalidOperation); + } + return 0; + } + } + + /* Save space for list document */ + lst_len_loc = pymongo_buffer_save_space(buffer, 4); + if (lst_len_loc == -1) { + return 0; + } + + iterator = PyObject_GetIter(docs); + if (iterator == NULL) { + PyObject* InvalidOperation = _error("InvalidOperation"); + if (InvalidOperation) { + PyErr_SetString(InvalidOperation, "input is not iterable"); + Py_DECREF(InvalidOperation); + } + return 0; + } + while ((doc = PyIter_Next(iterator)) != NULL) { + int sub_doc_begin = pymongo_buffer_get_position(buffer); + int cur_doc_begin; + int cur_size; + int enough_data = 0; + char key[BUF_SIZE]; + int res = LL2STR(key, (long long)idx); + if (res == -1) { + return 0; + } + if (!buffer_write_bytes(buffer, "\x03", 1) || + !buffer_write_bytes(buffer, key, (int)strlen(key) + 1)) { + goto fail; + } + cur_doc_begin = pymongo_buffer_get_position(buffer); + if (!write_dict(state->_cbson, buffer, doc, 0, &options, 1)) { + goto fail; + } + + /* We have enough data, return this batch. + * max_cmd_size accounts for the two trailing null bytes. + */ + cur_size = pymongo_buffer_get_position(buffer) - cur_doc_begin; + /* This single document is too large for the command. */ + if (cur_size > max_cmd_size) { + if (op == _INSERT) { + _set_document_too_large(cur_size, max_bson_size); + } else { + PyObject* DocumentTooLarge = _error("DocumentTooLarge"); + if (DocumentTooLarge) { + /* + * There's nothing intelligent we can say + * about size for update and delete. + */ + PyErr_Format( + DocumentTooLarge, + "%s command document too large", + (op == _UPDATE) ? "update": "delete"); + Py_DECREF(DocumentTooLarge); + } + } + goto fail; + } + enough_data = (idx >= 1 && + (pymongo_buffer_get_position(buffer) > max_split_size)); + if (enough_data) { + /* + * Roll the existing buffer back to the beginning + * of the last document encoded. + */ + pymongo_buffer_update_position(buffer, sub_doc_begin); + Py_CLEAR(doc); + break; + } + if (PyList_Append(to_publish, doc) < 0) { + goto fail; + } + Py_CLEAR(doc); + idx += 1; + /* We have enough documents, return this batch. */ + if (idx == max_write_batch_size) { + break; + } + } + Py_CLEAR(iterator); + + if (PyErr_Occurred()) { + goto fail; + } + + if (!buffer_write_bytes(buffer, "\x00\x00", 2)) { + goto fail; + } + + position = pymongo_buffer_get_position(buffer); + length = position - lst_len_loc - 1; + buffer_write_int32_at_position(buffer, lst_len_loc, (int32_t)length); + length = position - cmd_len_loc; + buffer_write_int32_at_position(buffer, cmd_len_loc, (int32_t)length); + return 1; + +fail: + Py_XDECREF(doc); + Py_XDECREF(iterator); + return 0; +} + +static PyObject* +_cbson_encode_batched_write_command(PyObject* self, PyObject* args) { + char *ns = NULL; + unsigned char op; + Py_ssize_t ns_len; + PyObject* command; + PyObject* docs; + PyObject* ctx = NULL; + PyObject* to_publish = NULL; + PyObject* result = NULL; + PyObject* options_obj; + codec_options_t options; + buffer_t buffer; + struct module_state *state = GETSTATE(self); + if (!state) { + return NULL; + } + + if (!(PyArg_ParseTuple(args, "et#bOOOO", "utf-8", + &ns, &ns_len, &op, &command, &docs, + &options_obj, &ctx) && + convert_codec_options(state->_cbson, options_obj, &options))) { + return NULL; + } + if (!(buffer = pymongo_buffer_new())) { + PyMem_Free(ns); + destroy_codec_options(&options); + return NULL; + } + if (!(to_publish = PyList_New(0))) { + goto fail; + } + + if (!_batched_write_command( + ns, + ns_len, + op, + command, + docs, + ctx, + to_publish, + options, + buffer, + state)) { + goto fail; + } + + result = Py_BuildValue("y#O", + pymongo_buffer_get_buffer(buffer), + (Py_ssize_t)pymongo_buffer_get_position(buffer), + to_publish); +fail: + PyMem_Free(ns); + destroy_codec_options(&options); + pymongo_buffer_free(buffer); + Py_XDECREF(to_publish); + return result; +} + +static PyMethodDef _CMessageMethods[] = { + {"_query_message", _cbson_query_message, METH_VARARGS, + "create a query message to be sent to MongoDB"}, + {"_get_more_message", _cbson_get_more_message, METH_VARARGS, + "create a get more message to be sent to MongoDB"}, + {"_op_msg", _cbson_op_msg, METH_VARARGS, + "create an OP_MSG message to be sent to MongoDB"}, + {"_encode_batched_write_command", _cbson_encode_batched_write_command, METH_VARARGS, + "Encode the next batched insert, update, or delete command"}, + {"_batched_op_msg", _cbson_batched_op_msg, METH_VARARGS, + "Create the next batched insert, update, or delete using OP_MSG"}, + {"_encode_batched_op_msg", _cbson_encode_batched_op_msg, METH_VARARGS, + "Encode the next batched insert, update, or delete using OP_MSG"}, + {NULL, NULL, 0, NULL} +}; + +#define INITERROR return -1; +static int _cmessage_traverse(PyObject *m, visitproc visit, void *arg) { + struct module_state *state = GETSTATE(m); + if (!state) { + return 0; + } + Py_VISIT(state->_cbson); + Py_VISIT(state->_max_bson_size_str); + Py_VISIT(state->_max_message_size_str); + Py_VISIT(state->_max_split_size_str); + Py_VISIT(state->_max_write_batch_size_str); + return 0; +} + +static int _cmessage_clear(PyObject *m) { + struct module_state *state = GETSTATE(m); + if (!state) { + return 0; + } + Py_CLEAR(state->_cbson); + Py_CLEAR(state->_max_bson_size_str); + Py_CLEAR(state->_max_message_size_str); + Py_CLEAR(state->_max_split_size_str); + Py_CLEAR(state->_max_write_batch_size_str); + return 0; +} + +/* Multi-phase extension module initialization code. + * See https://peps.python.org/pep-0489/. +*/ +static int +_cmessage_exec(PyObject *m) +{ + PyObject *_cbson = NULL; + PyObject *c_api_object = NULL; + struct module_state* state = NULL; + + /* Store a reference to the _cbson module since it's needed to call some + * of its functions + */ + _cbson = PyImport_ImportModule("bson._cbson"); + if (_cbson == NULL) { + goto fail; + } + + /* Import C API of _cbson + * The header file accesses _cbson_API to call the functions + */ + c_api_object = PyObject_GetAttrString(_cbson, "_C_API"); + if (c_api_object == NULL) { + goto fail; + } + _cbson_API = (void **)PyCapsule_GetPointer(c_api_object, "_cbson._C_API"); + if (_cbson_API == NULL) { + goto fail; + } + + state = GETSTATE(m); + if (state == NULL) { + goto fail; + } + state->_cbson = _cbson; + if (!((state->_max_bson_size_str = PyUnicode_FromString("max_bson_size")) && + (state->_max_message_size_str = PyUnicode_FromString("max_message_size")) && + (state->_max_write_batch_size_str = PyUnicode_FromString("max_write_batch_size")) && + (state->_max_split_size_str = PyUnicode_FromString("max_split_size")))) { + goto fail; + } + + Py_DECREF(c_api_object); + return 0; + +fail: + Py_XDECREF(m); + Py_XDECREF(c_api_object); + Py_XDECREF(_cbson); + INITERROR; +} + + +static PyModuleDef_Slot _cmessage_slots[] = { + {Py_mod_exec, _cmessage_exec}, +#ifdef Py_MOD_MULTIPLE_INTERPRETERS_SUPPORTED + {Py_mod_multiple_interpreters, Py_MOD_MULTIPLE_INTERPRETERS_SUPPORTED}, +#endif + {0, NULL}, +}; + + +static struct PyModuleDef moduledef = { + PyModuleDef_HEAD_INIT, + "_cmessage", + NULL, + sizeof(struct module_state), + _CMessageMethods, + _cmessage_slots, + _cmessage_traverse, + _cmessage_clear, + NULL +}; + +PyMODINIT_FUNC +PyInit__cmessage(void) +{ + return PyModuleDef_Init(&moduledef); +} diff --git a/venv/Lib/site-packages/pymongo/_csot.py b/venv/Lib/site-packages/pymongo/_csot.py new file mode 100644 index 00000000..194cbad4 --- /dev/null +++ b/venv/Lib/site-packages/pymongo/_csot.py @@ -0,0 +1,153 @@ +# Copyright 2022-present MongoDB, 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. + +"""Internal helpers for CSOT.""" + +from __future__ import annotations + +import functools +import time +from collections import deque +from contextlib import AbstractContextManager +from contextvars import ContextVar, Token +from typing import TYPE_CHECKING, Any, Callable, Deque, MutableMapping, Optional, TypeVar, cast + +if TYPE_CHECKING: + from pymongo.write_concern import WriteConcern + +TIMEOUT: ContextVar[Optional[float]] = ContextVar("TIMEOUT", default=None) +RTT: ContextVar[float] = ContextVar("RTT", default=0.0) +DEADLINE: ContextVar[float] = ContextVar("DEADLINE", default=float("inf")) + + +def get_timeout() -> Optional[float]: + return TIMEOUT.get(None) + + +def get_rtt() -> float: + return RTT.get() + + +def get_deadline() -> float: + return DEADLINE.get() + + +def set_rtt(rtt: float) -> None: + RTT.set(rtt) + + +def remaining() -> Optional[float]: + if not get_timeout(): + return None + return DEADLINE.get() - time.monotonic() + + +def clamp_remaining(max_timeout: float) -> float: + """Return the remaining timeout clamped to a max value.""" + timeout = remaining() + if timeout is None: + return max_timeout + return min(timeout, max_timeout) + + +class _TimeoutContext(AbstractContextManager): + """Internal timeout context manager. + + Use :func:`pymongo.timeout` instead:: + + with pymongo.timeout(0.5): + client.test.test.insert_one({}) + """ + + def __init__(self, timeout: Optional[float]): + self._timeout = timeout + self._tokens: Optional[tuple[Token[Optional[float]], Token[float], Token[float]]] = None + + def __enter__(self) -> _TimeoutContext: + timeout_token = TIMEOUT.set(self._timeout) + prev_deadline = DEADLINE.get() + next_deadline = time.monotonic() + self._timeout if self._timeout else float("inf") + deadline_token = DEADLINE.set(min(prev_deadline, next_deadline)) + rtt_token = RTT.set(0.0) + self._tokens = (timeout_token, deadline_token, rtt_token) + return self + + def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None: + if self._tokens: + timeout_token, deadline_token, rtt_token = self._tokens + TIMEOUT.reset(timeout_token) + DEADLINE.reset(deadline_token) + RTT.reset(rtt_token) + + +# See https://mypy.readthedocs.io/en/stable/generics.html?#decorator-factories +F = TypeVar("F", bound=Callable[..., Any]) + + +def apply(func: F) -> F: + """Apply the client's timeoutMS to this operation.""" + + @functools.wraps(func) + def csot_wrapper(self: Any, *args: Any, **kwargs: Any) -> Any: + if get_timeout() is None: + timeout = self._timeout + if timeout is not None: + with _TimeoutContext(timeout): + return func(self, *args, **kwargs) + return func(self, *args, **kwargs) + + return cast(F, csot_wrapper) + + +def apply_write_concern( + cmd: MutableMapping[str, Any], write_concern: Optional[WriteConcern] +) -> None: + """Apply the given write concern to a command.""" + if not write_concern or write_concern.is_server_default: + return + wc = write_concern.document + if get_timeout() is not None: + wc.pop("wtimeout", None) + if wc: + cmd["writeConcern"] = wc + + +_MAX_RTT_SAMPLES: int = 10 +_MIN_RTT_SAMPLES: int = 2 + + +class MovingMinimum: + """Tracks a minimum RTT within the last 10 RTT samples.""" + + samples: Deque[float] + + def __init__(self) -> None: + self.samples = deque(maxlen=_MAX_RTT_SAMPLES) + + def add_sample(self, sample: float) -> None: + if sample < 0: + # Likely system time change while waiting for hello response + # and not using time.monotonic. Ignore it, the next one will + # probably be valid. + return + self.samples.append(sample) + + def get(self) -> float: + """Get the min, or 0.0 if there aren't enough samples yet.""" + if len(self.samples) >= _MIN_RTT_SAMPLES: + return min(self.samples) + return 0.0 + + def reset(self) -> None: + self.samples.clear() diff --git a/venv/Lib/site-packages/pymongo/_gcp_helpers.py b/venv/Lib/site-packages/pymongo/_gcp_helpers.py new file mode 100644 index 00000000..46f02ba1 --- /dev/null +++ b/venv/Lib/site-packages/pymongo/_gcp_helpers.py @@ -0,0 +1,39 @@ +# Copyright 2024-present MongoDB, 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. + +"""GCP helpers.""" +from __future__ import annotations + +from typing import Any +from urllib.request import Request, urlopen + + +def _get_gcp_response(resource: str, timeout: float = 5) -> dict[str, Any]: + url = "http://metadata/computeMetadata/v1/instance/service-accounts/default/identity" + url += f"?audience={resource}" + headers = {"Metadata-Flavor": "Google"} + request = Request(url, headers=headers) # noqa: S310 + try: + with urlopen(request, timeout=timeout) as response: # noqa: S310 + status = response.status + body = response.read().decode("utf8") + except Exception as e: + msg = "Failed to acquire IMDS access token: %s" % e + raise ValueError(msg) from None + + if status != 200: + msg = "Failed to acquire IMDS access token." + raise ValueError(msg) + + return dict(access_token=body) diff --git a/venv/Lib/site-packages/pymongo/_lazy_import.py b/venv/Lib/site-packages/pymongo/_lazy_import.py new file mode 100644 index 00000000..888339d0 --- /dev/null +++ b/venv/Lib/site-packages/pymongo/_lazy_import.py @@ -0,0 +1,43 @@ +# Copyright 2024-present MongoDB, 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. +from __future__ import annotations + +import importlib.util +import sys +from types import ModuleType + + +def lazy_import(name: str) -> ModuleType: + """Lazily import a module by name + + From https://docs.python.org/3/library/importlib.html#implementing-lazy-imports + """ + # Workaround for PYTHON-4424. + if "__compiled__" in globals(): + return importlib.import_module(name) + try: + spec = importlib.util.find_spec(name) + except ValueError: + # Note: this cannot be ModuleNotFoundError, see PYTHON-4424. + raise ImportError(name=name) from None + if spec is None: + # Note: this cannot be ModuleNotFoundError, see PYTHON-4424. + raise ImportError(name=name) + assert spec is not None + loader = importlib.util.LazyLoader(spec.loader) # type:ignore[arg-type] + spec.loader = loader + module = importlib.util.module_from_spec(spec) + sys.modules[name] = module + loader.exec_module(module) + return module diff --git a/venv/Lib/site-packages/pymongo/_version.py b/venv/Lib/site-packages/pymongo/_version.py new file mode 100644 index 00000000..65caa084 --- /dev/null +++ b/venv/Lib/site-packages/pymongo/_version.py @@ -0,0 +1,30 @@ +# Copyright 2022-present MongoDB, 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. + +"""Current version of PyMongo.""" +from __future__ import annotations + +from typing import Tuple, Union + +version_tuple: Tuple[Union[int, str], ...] = (4, 7, 2) + + +def get_version_string() -> str: + if isinstance(version_tuple[-1], str): + return ".".join(map(str, version_tuple[:-1])) + version_tuple[-1] + return ".".join(map(str, version_tuple)) + + +__version__: str = get_version_string() +version = __version__ diff --git a/venv/Lib/site-packages/pymongo/aggregation.py b/venv/Lib/site-packages/pymongo/aggregation.py new file mode 100644 index 00000000..574db10a --- /dev/null +++ b/venv/Lib/site-packages/pymongo/aggregation.py @@ -0,0 +1,255 @@ +# Copyright 2019-present MongoDB, 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. + +"""Perform aggregation operations on a collection or database.""" +from __future__ import annotations + +from collections.abc import Callable, Mapping, MutableMapping +from typing import TYPE_CHECKING, Any, Optional, Union + +from pymongo import common +from pymongo.collation import validate_collation_or_none +from pymongo.errors import ConfigurationError +from pymongo.read_preferences import ReadPreference, _AggWritePref + +if TYPE_CHECKING: + from pymongo.client_session import ClientSession + from pymongo.collection import Collection + from pymongo.command_cursor import CommandCursor + from pymongo.database import Database + from pymongo.pool import Connection + from pymongo.read_preferences import _ServerMode + from pymongo.server import Server + from pymongo.typings import _DocumentType, _Pipeline + + +class _AggregationCommand: + """The internal abstract base class for aggregation cursors. + + Should not be called directly by application developers. Use + :meth:`pymongo.collection.Collection.aggregate`, or + :meth:`pymongo.database.Database.aggregate` instead. + """ + + def __init__( + self, + target: Union[Database, Collection], + cursor_class: type[CommandCursor], + pipeline: _Pipeline, + options: MutableMapping[str, Any], + explicit_session: bool, + let: Optional[Mapping[str, Any]] = None, + user_fields: Optional[MutableMapping[str, Any]] = None, + result_processor: Optional[Callable[[Mapping[str, Any], Connection], None]] = None, + comment: Any = None, + ) -> None: + if "explain" in options: + raise ConfigurationError( + "The explain option is not supported. Use Database.command instead." + ) + + self._target = target + + pipeline = common.validate_list("pipeline", pipeline) + self._pipeline = pipeline + self._performs_write = False + if pipeline and ("$out" in pipeline[-1] or "$merge" in pipeline[-1]): + self._performs_write = True + + common.validate_is_mapping("options", options) + if let is not None: + common.validate_is_mapping("let", let) + options["let"] = let + if comment is not None: + options["comment"] = comment + + self._options = options + + # This is the batchSize that will be used for setting the initial + # batchSize for the cursor, as well as the subsequent getMores. + self._batch_size = common.validate_non_negative_integer_or_none( + "batchSize", self._options.pop("batchSize", None) + ) + + # If the cursor option is already specified, avoid overriding it. + self._options.setdefault("cursor", {}) + # If the pipeline performs a write, we ignore the initial batchSize + # since the server doesn't return results in this case. + if self._batch_size is not None and not self._performs_write: + self._options["cursor"]["batchSize"] = self._batch_size + + self._cursor_class = cursor_class + self._explicit_session = explicit_session + self._user_fields = user_fields + self._result_processor = result_processor + + self._collation = validate_collation_or_none(options.pop("collation", None)) + + self._max_await_time_ms = options.pop("maxAwaitTimeMS", None) + self._write_preference: Optional[_AggWritePref] = None + + @property + def _aggregation_target(self) -> Union[str, int]: + """The argument to pass to the aggregate command.""" + raise NotImplementedError + + @property + def _cursor_namespace(self) -> str: + """The namespace in which the aggregate command is run.""" + raise NotImplementedError + + def _cursor_collection(self, cursor_doc: Mapping[str, Any]) -> Collection: + """The Collection used for the aggregate command cursor.""" + raise NotImplementedError + + @property + def _database(self) -> Database: + """The database against which the aggregation command is run.""" + raise NotImplementedError + + def get_read_preference( + self, session: Optional[ClientSession] + ) -> Union[_AggWritePref, _ServerMode]: + if self._write_preference: + return self._write_preference + pref = self._target._read_preference_for(session) + if self._performs_write and pref != ReadPreference.PRIMARY: + self._write_preference = pref = _AggWritePref(pref) # type: ignore[assignment] + return pref + + def get_cursor( + self, + session: Optional[ClientSession], + server: Server, + conn: Connection, + read_preference: _ServerMode, + ) -> CommandCursor[_DocumentType]: + # Serialize command. + cmd = {"aggregate": self._aggregation_target, "pipeline": self._pipeline} + cmd.update(self._options) + + # Apply this target's read concern if: + # readConcern has not been specified as a kwarg and either + # - server version is >= 4.2 or + # - server version is >= 3.2 and pipeline doesn't use $out + if ("readConcern" not in cmd) and ( + not self._performs_write or (conn.max_wire_version >= 8) + ): + read_concern = self._target.read_concern + else: + read_concern = None + + # Apply this target's write concern if: + # writeConcern has not been specified as a kwarg and pipeline doesn't + # perform a write operation + if "writeConcern" not in cmd and self._performs_write: + write_concern = self._target._write_concern_for(session) + else: + write_concern = None + + # Run command. + result = conn.command( + self._database.name, + cmd, + read_preference, + self._target.codec_options, + parse_write_concern_error=True, + read_concern=read_concern, + write_concern=write_concern, + collation=self._collation, + session=session, + client=self._database.client, + user_fields=self._user_fields, + ) + + if self._result_processor: + self._result_processor(result, conn) + + # Extract cursor from result or mock/fake one if necessary. + if "cursor" in result: + cursor = result["cursor"] + else: + # Unacknowledged $out/$merge write. Fake a cursor. + cursor = { + "id": 0, + "firstBatch": result.get("result", []), + "ns": self._cursor_namespace, + } + + # Create and return cursor instance. + cmd_cursor = self._cursor_class( + self._cursor_collection(cursor), + cursor, + conn.address, + batch_size=self._batch_size or 0, + max_await_time_ms=self._max_await_time_ms, + session=session, + explicit_session=self._explicit_session, + comment=self._options.get("comment"), + ) + cmd_cursor._maybe_pin_connection(conn) + return cmd_cursor + + +class _CollectionAggregationCommand(_AggregationCommand): + _target: Collection + + @property + def _aggregation_target(self) -> str: + return self._target.name + + @property + def _cursor_namespace(self) -> str: + return self._target.full_name + + def _cursor_collection(self, cursor: Mapping[str, Any]) -> Collection: + """The Collection used for the aggregate command cursor.""" + return self._target + + @property + def _database(self) -> Database: + return self._target.database + + +class _CollectionRawAggregationCommand(_CollectionAggregationCommand): + def __init__(self, *args: Any, **kwargs: Any) -> None: + super().__init__(*args, **kwargs) + + # For raw-batches, we set the initial batchSize for the cursor to 0. + if not self._performs_write: + self._options["cursor"]["batchSize"] = 0 + + +class _DatabaseAggregationCommand(_AggregationCommand): + _target: Database + + @property + def _aggregation_target(self) -> int: + return 1 + + @property + def _cursor_namespace(self) -> str: + return f"{self._target.name}.$cmd.aggregate" + + @property + def _database(self) -> Database: + return self._target + + def _cursor_collection(self, cursor: Mapping[str, Any]) -> Collection: + """The Collection used for the aggregate command cursor.""" + # Collection level aggregate may not always return the "ns" field + # according to our MockupDB tests. Let's handle that case for db level + # aggregate too by defaulting to the .$cmd.aggregate namespace. + _, collname = cursor.get("ns", self._cursor_namespace).split(".", 1) + return self._database[collname] diff --git a/venv/Lib/site-packages/pymongo/auth.py b/venv/Lib/site-packages/pymongo/auth.py new file mode 100644 index 00000000..8bc4145a --- /dev/null +++ b/venv/Lib/site-packages/pymongo/auth.py @@ -0,0 +1,656 @@ +# Copyright 2013-present MongoDB, 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. + +"""Authentication helpers.""" +from __future__ import annotations + +import functools +import hashlib +import hmac +import os +import socket +import typing +from base64 import standard_b64decode, standard_b64encode +from collections import namedtuple +from typing import ( + TYPE_CHECKING, + Any, + Callable, + Dict, + Mapping, + MutableMapping, + Optional, + cast, +) +from urllib.parse import quote + +from bson.binary import Binary +from pymongo.auth_aws import _authenticate_aws +from pymongo.auth_oidc import ( + _authenticate_oidc, + _get_authenticator, + _OIDCAzureCallback, + _OIDCGCPCallback, + _OIDCProperties, + _OIDCTestCallback, +) +from pymongo.errors import ConfigurationError, OperationFailure +from pymongo.saslprep import saslprep + +if TYPE_CHECKING: + from pymongo.hello import Hello + from pymongo.pool import Connection + +HAVE_KERBEROS = True +_USE_PRINCIPAL = False +try: + import winkerberos as kerberos # type:ignore[import] + + if tuple(map(int, kerberos.__version__.split(".")[:2])) >= (0, 5): + _USE_PRINCIPAL = True +except ImportError: + try: + import kerberos # type:ignore[import] + except ImportError: + HAVE_KERBEROS = False + + +MECHANISMS = frozenset( + [ + "GSSAPI", + "MONGODB-CR", + "MONGODB-OIDC", + "MONGODB-X509", + "MONGODB-AWS", + "PLAIN", + "SCRAM-SHA-1", + "SCRAM-SHA-256", + "DEFAULT", + ] +) +"""The authentication mechanisms supported by PyMongo.""" + + +class _Cache: + __slots__ = ("data",) + + _hash_val = hash("_Cache") + + def __init__(self) -> None: + self.data = None + + def __eq__(self, other: object) -> bool: + # Two instances must always compare equal. + if isinstance(other, _Cache): + return True + return NotImplemented + + def __ne__(self, other: object) -> bool: + if isinstance(other, _Cache): + return False + return NotImplemented + + def __hash__(self) -> int: + return self._hash_val + + +MongoCredential = namedtuple( + "MongoCredential", + ["mechanism", "source", "username", "password", "mechanism_properties", "cache"], +) +"""A hashable namedtuple of values used for authentication.""" + + +GSSAPIProperties = namedtuple( + "GSSAPIProperties", ["service_name", "canonicalize_host_name", "service_realm"] +) +"""Mechanism properties for GSSAPI authentication.""" + + +_AWSProperties = namedtuple("_AWSProperties", ["aws_session_token"]) +"""Mechanism properties for MONGODB-AWS authentication.""" + + +def _build_credentials_tuple( + mech: str, + source: Optional[str], + user: str, + passwd: str, + extra: Mapping[str, Any], + database: Optional[str], +) -> MongoCredential: + """Build and return a mechanism specific credentials tuple.""" + if mech not in ("MONGODB-X509", "MONGODB-AWS", "MONGODB-OIDC") and user is None: + raise ConfigurationError(f"{mech} requires a username.") + if mech == "GSSAPI": + if source is not None and source != "$external": + raise ValueError("authentication source must be $external or None for GSSAPI") + properties = extra.get("authmechanismproperties", {}) + service_name = properties.get("SERVICE_NAME", "mongodb") + canonicalize = bool(properties.get("CANONICALIZE_HOST_NAME", False)) + service_realm = properties.get("SERVICE_REALM") + props = GSSAPIProperties( + service_name=service_name, + canonicalize_host_name=canonicalize, + service_realm=service_realm, + ) + # Source is always $external. + return MongoCredential(mech, "$external", user, passwd, props, None) + elif mech == "MONGODB-X509": + if passwd is not None: + raise ConfigurationError("Passwords are not supported by MONGODB-X509") + if source is not None and source != "$external": + raise ValueError("authentication source must be $external or None for MONGODB-X509") + # Source is always $external, user can be None. + return MongoCredential(mech, "$external", user, None, None, None) + elif mech == "MONGODB-AWS": + if user is not None and passwd is None: + raise ConfigurationError("username without a password is not supported by MONGODB-AWS") + if source is not None and source != "$external": + raise ConfigurationError( + "authentication source must be $external or None for MONGODB-AWS" + ) + + properties = extra.get("authmechanismproperties", {}) + aws_session_token = properties.get("AWS_SESSION_TOKEN") + aws_props = _AWSProperties(aws_session_token=aws_session_token) + # user can be None for temporary link-local EC2 credentials. + return MongoCredential(mech, "$external", user, passwd, aws_props, None) + elif mech == "MONGODB-OIDC": + properties = extra.get("authmechanismproperties", {}) + callback = properties.get("OIDC_CALLBACK") + human_callback = properties.get("OIDC_HUMAN_CALLBACK") + environ = properties.get("ENVIRONMENT") + token_resource = properties.get("TOKEN_RESOURCE", "") + default_allowed = [ + "*.mongodb.net", + "*.mongodb-dev.net", + "*.mongodb-qa.net", + "*.mongodbgov.net", + "localhost", + "127.0.0.1", + "::1", + ] + allowed_hosts = properties.get("ALLOWED_HOSTS", default_allowed) + msg = ( + "authentication with MONGODB-OIDC requires providing either a callback or a environment" + ) + if passwd is not None: + msg = "password is not supported by MONGODB-OIDC" + raise ConfigurationError(msg) + if callback or human_callback: + if environ is not None: + raise ConfigurationError(msg) + if callback and human_callback: + msg = "cannot set both OIDC_CALLBACK and OIDC_HUMAN_CALLBACK" + raise ConfigurationError(msg) + elif environ is not None: + if environ == "test": + if user is not None: + msg = "test environment for MONGODB-OIDC does not support username" + raise ConfigurationError(msg) + callback = _OIDCTestCallback() + elif environ == "azure": + passwd = None + if not token_resource: + raise ConfigurationError( + "Azure environment for MONGODB-OIDC requires a TOKEN_RESOURCE auth mechanism property" + ) + callback = _OIDCAzureCallback(token_resource) + elif environ == "gcp": + passwd = None + if not token_resource: + raise ConfigurationError( + "GCP provider for MONGODB-OIDC requires a TOKEN_RESOURCE auth mechanism property" + ) + callback = _OIDCGCPCallback(token_resource) + else: + raise ConfigurationError(f"unrecognized ENVIRONMENT for MONGODB-OIDC: {environ}") + else: + raise ConfigurationError(msg) + + oidc_props = _OIDCProperties( + callback=callback, + human_callback=human_callback, + environment=environ, + allowed_hosts=allowed_hosts, + token_resource=token_resource, + username=user, + ) + return MongoCredential(mech, "$external", user, passwd, oidc_props, _Cache()) + + elif mech == "PLAIN": + source_database = source or database or "$external" + return MongoCredential(mech, source_database, user, passwd, None, None) + else: + source_database = source or database or "admin" + if passwd is None: + raise ConfigurationError("A password is required.") + return MongoCredential(mech, source_database, user, passwd, None, _Cache()) + + +def _xor(fir: bytes, sec: bytes) -> bytes: + """XOR two byte strings together.""" + return b"".join([bytes([x ^ y]) for x, y in zip(fir, sec)]) + + +def _parse_scram_response(response: bytes) -> Dict[bytes, bytes]: + """Split a scram response into key, value pairs.""" + return dict( + typing.cast(typing.Tuple[bytes, bytes], item.split(b"=", 1)) + for item in response.split(b",") + ) + + +def _authenticate_scram_start( + credentials: MongoCredential, mechanism: str +) -> tuple[bytes, bytes, MutableMapping[str, Any]]: + username = credentials.username + user = username.encode("utf-8").replace(b"=", b"=3D").replace(b",", b"=2C") + nonce = standard_b64encode(os.urandom(32)) + first_bare = b"n=" + user + b",r=" + nonce + + cmd = { + "saslStart": 1, + "mechanism": mechanism, + "payload": Binary(b"n,," + first_bare), + "autoAuthorize": 1, + "options": {"skipEmptyExchange": True}, + } + return nonce, first_bare, cmd + + +def _authenticate_scram(credentials: MongoCredential, conn: Connection, mechanism: str) -> None: + """Authenticate using SCRAM.""" + username = credentials.username + if mechanism == "SCRAM-SHA-256": + digest = "sha256" + digestmod = hashlib.sha256 + data = saslprep(credentials.password).encode("utf-8") + else: + digest = "sha1" + digestmod = hashlib.sha1 + data = _password_digest(username, credentials.password).encode("utf-8") + source = credentials.source + cache = credentials.cache + + # Make local + _hmac = hmac.HMAC + + ctx = conn.auth_ctx + if ctx and ctx.speculate_succeeded(): + assert isinstance(ctx, _ScramContext) + assert ctx.scram_data is not None + nonce, first_bare = ctx.scram_data + res = ctx.speculative_authenticate + else: + nonce, first_bare, cmd = _authenticate_scram_start(credentials, mechanism) + res = conn.command(source, cmd) + + assert res is not None + server_first = res["payload"] + parsed = _parse_scram_response(server_first) + iterations = int(parsed[b"i"]) + if iterations < 4096: + raise OperationFailure("Server returned an invalid iteration count.") + salt = parsed[b"s"] + rnonce = parsed[b"r"] + if not rnonce.startswith(nonce): + raise OperationFailure("Server returned an invalid nonce.") + + without_proof = b"c=biws,r=" + rnonce + if cache.data: + client_key, server_key, csalt, citerations = cache.data + else: + client_key, server_key, csalt, citerations = None, None, None, None + + # Salt and / or iterations could change for a number of different + # reasons. Either changing invalidates the cache. + if not client_key or salt != csalt or iterations != citerations: + salted_pass = hashlib.pbkdf2_hmac(digest, data, standard_b64decode(salt), iterations) + client_key = _hmac(salted_pass, b"Client Key", digestmod).digest() + server_key = _hmac(salted_pass, b"Server Key", digestmod).digest() + cache.data = (client_key, server_key, salt, iterations) + stored_key = digestmod(client_key).digest() + auth_msg = b",".join((first_bare, server_first, without_proof)) + client_sig = _hmac(stored_key, auth_msg, digestmod).digest() + client_proof = b"p=" + standard_b64encode(_xor(client_key, client_sig)) + client_final = b",".join((without_proof, client_proof)) + + server_sig = standard_b64encode(_hmac(server_key, auth_msg, digestmod).digest()) + + cmd = { + "saslContinue": 1, + "conversationId": res["conversationId"], + "payload": Binary(client_final), + } + res = conn.command(source, cmd) + + parsed = _parse_scram_response(res["payload"]) + if not hmac.compare_digest(parsed[b"v"], server_sig): + raise OperationFailure("Server returned an invalid signature.") + + # A third empty challenge may be required if the server does not support + # skipEmptyExchange: SERVER-44857. + if not res["done"]: + cmd = { + "saslContinue": 1, + "conversationId": res["conversationId"], + "payload": Binary(b""), + } + res = conn.command(source, cmd) + if not res["done"]: + raise OperationFailure("SASL conversation failed to complete.") + + +def _password_digest(username: str, password: str) -> str: + """Get a password digest to use for authentication.""" + if not isinstance(password, str): + raise TypeError("password must be an instance of str") + if len(password) == 0: + raise ValueError("password can't be empty") + if not isinstance(username, str): + raise TypeError("username must be an instance of str") + + md5hash = hashlib.md5() # noqa: S324 + data = f"{username}:mongo:{password}" + md5hash.update(data.encode("utf-8")) + return md5hash.hexdigest() + + +def _auth_key(nonce: str, username: str, password: str) -> str: + """Get an auth key to use for authentication.""" + digest = _password_digest(username, password) + md5hash = hashlib.md5() # noqa: S324 + data = f"{nonce}{username}{digest}" + md5hash.update(data.encode("utf-8")) + return md5hash.hexdigest() + + +def _canonicalize_hostname(hostname: str) -> str: + """Canonicalize hostname following MIT-krb5 behavior.""" + # https://github.com/krb5/krb5/blob/d406afa363554097ac48646a29249c04f498c88e/src/util/k5test.py#L505-L520 + af, socktype, proto, canonname, sockaddr = socket.getaddrinfo( + hostname, None, 0, 0, socket.IPPROTO_TCP, socket.AI_CANONNAME + )[0] + + try: + name = socket.getnameinfo(sockaddr, socket.NI_NAMEREQD) + except socket.gaierror: + return canonname.lower() + + return name[0].lower() + + +def _authenticate_gssapi(credentials: MongoCredential, conn: Connection) -> None: + """Authenticate using GSSAPI.""" + if not HAVE_KERBEROS: + raise ConfigurationError( + 'The "kerberos" module must be installed to use GSSAPI authentication.' + ) + + try: + username = credentials.username + password = credentials.password + props = credentials.mechanism_properties + # Starting here and continuing through the while loop below - establish + # the security context. See RFC 4752, Section 3.1, first paragraph. + host = conn.address[0] + if props.canonicalize_host_name: + host = _canonicalize_hostname(host) + service = props.service_name + "@" + host + if props.service_realm is not None: + service = service + "@" + props.service_realm + + if password is not None: + if _USE_PRINCIPAL: + # Note that, though we use unquote_plus for unquoting URI + # options, we use quote here. Microsoft's UrlUnescape (used + # by WinKerberos) doesn't support +. + principal = ":".join((quote(username), quote(password))) + result, ctx = kerberos.authGSSClientInit( + service, principal, gssflags=kerberos.GSS_C_MUTUAL_FLAG + ) + else: + if "@" in username: + user, domain = username.split("@", 1) + else: + user, domain = username, None + result, ctx = kerberos.authGSSClientInit( + service, + gssflags=kerberos.GSS_C_MUTUAL_FLAG, + user=user, + domain=domain, + password=password, + ) + else: + result, ctx = kerberos.authGSSClientInit(service, gssflags=kerberos.GSS_C_MUTUAL_FLAG) + + if result != kerberos.AUTH_GSS_COMPLETE: + raise OperationFailure("Kerberos context failed to initialize.") + + try: + # pykerberos uses a weird mix of exceptions and return values + # to indicate errors. + # 0 == continue, 1 == complete, -1 == error + # Only authGSSClientStep can return 0. + if kerberos.authGSSClientStep(ctx, "") != 0: + raise OperationFailure("Unknown kerberos failure in step function.") + + # Start a SASL conversation with mongod/s + # Note: pykerberos deals with base64 encoded byte strings. + # Since mongo accepts base64 strings as the payload we don't + # have to use bson.binary.Binary. + payload = kerberos.authGSSClientResponse(ctx) + cmd = { + "saslStart": 1, + "mechanism": "GSSAPI", + "payload": payload, + "autoAuthorize": 1, + } + response = conn.command("$external", cmd) + + # Limit how many times we loop to catch protocol / library issues + for _ in range(10): + result = kerberos.authGSSClientStep(ctx, str(response["payload"])) + if result == -1: + raise OperationFailure("Unknown kerberos failure in step function.") + + payload = kerberos.authGSSClientResponse(ctx) or "" + + cmd = { + "saslContinue": 1, + "conversationId": response["conversationId"], + "payload": payload, + } + response = conn.command("$external", cmd) + + if result == kerberos.AUTH_GSS_COMPLETE: + break + else: + raise OperationFailure("Kerberos authentication failed to complete.") + + # Once the security context is established actually authenticate. + # See RFC 4752, Section 3.1, last two paragraphs. + if kerberos.authGSSClientUnwrap(ctx, str(response["payload"])) != 1: + raise OperationFailure("Unknown kerberos failure during GSS_Unwrap step.") + + if kerberos.authGSSClientWrap(ctx, kerberos.authGSSClientResponse(ctx), username) != 1: + raise OperationFailure("Unknown kerberos failure during GSS_Wrap step.") + + payload = kerberos.authGSSClientResponse(ctx) + cmd = { + "saslContinue": 1, + "conversationId": response["conversationId"], + "payload": payload, + } + conn.command("$external", cmd) + + finally: + kerberos.authGSSClientClean(ctx) + + except kerberos.KrbError as exc: + raise OperationFailure(str(exc)) from None + + +def _authenticate_plain(credentials: MongoCredential, conn: Connection) -> None: + """Authenticate using SASL PLAIN (RFC 4616)""" + source = credentials.source + username = credentials.username + password = credentials.password + payload = (f"\x00{username}\x00{password}").encode() + cmd = { + "saslStart": 1, + "mechanism": "PLAIN", + "payload": Binary(payload), + "autoAuthorize": 1, + } + conn.command(source, cmd) + + +def _authenticate_x509(credentials: MongoCredential, conn: Connection) -> None: + """Authenticate using MONGODB-X509.""" + ctx = conn.auth_ctx + if ctx and ctx.speculate_succeeded(): + # MONGODB-X509 is done after the speculative auth step. + return + + cmd = _X509Context(credentials, conn.address).speculate_command() + conn.command("$external", cmd) + + +def _authenticate_mongo_cr(credentials: MongoCredential, conn: Connection) -> None: + """Authenticate using MONGODB-CR.""" + source = credentials.source + username = credentials.username + password = credentials.password + # Get a nonce + response = conn.command(source, {"getnonce": 1}) + nonce = response["nonce"] + key = _auth_key(nonce, username, password) + + # Actually authenticate + query = {"authenticate": 1, "user": username, "nonce": nonce, "key": key} + conn.command(source, query) + + +def _authenticate_default(credentials: MongoCredential, conn: Connection) -> None: + if conn.max_wire_version >= 7: + if conn.negotiated_mechs: + mechs = conn.negotiated_mechs + else: + source = credentials.source + cmd = conn.hello_cmd() + cmd["saslSupportedMechs"] = source + "." + credentials.username + mechs = conn.command(source, cmd, publish_events=False).get("saslSupportedMechs", []) + if "SCRAM-SHA-256" in mechs: + return _authenticate_scram(credentials, conn, "SCRAM-SHA-256") + else: + return _authenticate_scram(credentials, conn, "SCRAM-SHA-1") + else: + return _authenticate_scram(credentials, conn, "SCRAM-SHA-1") + + +_AUTH_MAP: Mapping[str, Callable[..., None]] = { + "GSSAPI": _authenticate_gssapi, + "MONGODB-CR": _authenticate_mongo_cr, + "MONGODB-X509": _authenticate_x509, + "MONGODB-AWS": _authenticate_aws, + "MONGODB-OIDC": _authenticate_oidc, # type:ignore[dict-item] + "PLAIN": _authenticate_plain, + "SCRAM-SHA-1": functools.partial(_authenticate_scram, mechanism="SCRAM-SHA-1"), + "SCRAM-SHA-256": functools.partial(_authenticate_scram, mechanism="SCRAM-SHA-256"), + "DEFAULT": _authenticate_default, +} + + +class _AuthContext: + def __init__(self, credentials: MongoCredential, address: tuple[str, int]) -> None: + self.credentials = credentials + self.speculative_authenticate: Optional[Mapping[str, Any]] = None + self.address = address + + @staticmethod + def from_credentials( + creds: MongoCredential, address: tuple[str, int] + ) -> Optional[_AuthContext]: + spec_cls = _SPECULATIVE_AUTH_MAP.get(creds.mechanism) + if spec_cls: + return cast(_AuthContext, spec_cls(creds, address)) + return None + + def speculate_command(self) -> Optional[MutableMapping[str, Any]]: + raise NotImplementedError + + def parse_response(self, hello: Hello[Mapping[str, Any]]) -> None: + self.speculative_authenticate = hello.speculative_authenticate + + def speculate_succeeded(self) -> bool: + return bool(self.speculative_authenticate) + + +class _ScramContext(_AuthContext): + def __init__( + self, credentials: MongoCredential, address: tuple[str, int], mechanism: str + ) -> None: + super().__init__(credentials, address) + self.scram_data: Optional[tuple[bytes, bytes]] = None + self.mechanism = mechanism + + def speculate_command(self) -> Optional[MutableMapping[str, Any]]: + nonce, first_bare, cmd = _authenticate_scram_start(self.credentials, self.mechanism) + # The 'db' field is included only on the speculative command. + cmd["db"] = self.credentials.source + # Save for later use. + self.scram_data = (nonce, first_bare) + return cmd + + +class _X509Context(_AuthContext): + def speculate_command(self) -> MutableMapping[str, Any]: + cmd = {"authenticate": 1, "mechanism": "MONGODB-X509"} + if self.credentials.username is not None: + cmd["user"] = self.credentials.username + return cmd + + +class _OIDCContext(_AuthContext): + def speculate_command(self) -> Optional[MutableMapping[str, Any]]: + authenticator = _get_authenticator(self.credentials, self.address) + cmd = authenticator.get_spec_auth_cmd() + if cmd is None: + return None + cmd["db"] = self.credentials.source + return cmd + + +_SPECULATIVE_AUTH_MAP: Mapping[str, Any] = { + "MONGODB-X509": _X509Context, + "SCRAM-SHA-1": functools.partial(_ScramContext, mechanism="SCRAM-SHA-1"), + "SCRAM-SHA-256": functools.partial(_ScramContext, mechanism="SCRAM-SHA-256"), + "MONGODB-OIDC": _OIDCContext, + "DEFAULT": functools.partial(_ScramContext, mechanism="SCRAM-SHA-256"), +} + + +def authenticate( + credentials: MongoCredential, conn: Connection, reauthenticate: bool = False +) -> None: + """Authenticate connection.""" + mechanism = credentials.mechanism + auth_func = _AUTH_MAP[mechanism] + if mechanism == "MONGODB-OIDC": + _authenticate_oidc(credentials, conn, reauthenticate) + else: + auth_func(credentials, conn) diff --git a/venv/Lib/site-packages/pymongo/auth_aws.py b/venv/Lib/site-packages/pymongo/auth_aws.py new file mode 100644 index 00000000..0d253cea --- /dev/null +++ b/venv/Lib/site-packages/pymongo/auth_aws.py @@ -0,0 +1,106 @@ +# Copyright 2020-present MongoDB, 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. + +"""MONGODB-AWS Authentication helpers.""" +from __future__ import annotations + +from pymongo._lazy_import import lazy_import + +try: + pymongo_auth_aws = lazy_import("pymongo_auth_aws") + _HAVE_MONGODB_AWS = True +except ImportError: + _HAVE_MONGODB_AWS = False + + +from typing import TYPE_CHECKING, Any, Mapping, Type + +import bson +from bson.binary import Binary +from pymongo.errors import ConfigurationError, OperationFailure + +if TYPE_CHECKING: + from bson.typings import _ReadableBuffer + from pymongo.auth import MongoCredential + from pymongo.pool import Connection + + +def _authenticate_aws(credentials: MongoCredential, conn: Connection) -> None: + """Authenticate using MONGODB-AWS.""" + if not _HAVE_MONGODB_AWS: + raise ConfigurationError( + "MONGODB-AWS authentication requires pymongo-auth-aws: " + "install with: python -m pip install 'pymongo[aws]'" + ) + + # Delayed import. + from pymongo_auth_aws.auth import ( # type:ignore[import] + set_cached_credentials, + set_use_cached_credentials, + ) + + set_use_cached_credentials(True) + + if conn.max_wire_version < 9: + raise ConfigurationError("MONGODB-AWS authentication requires MongoDB version 4.4 or later") + + class AwsSaslContext(pymongo_auth_aws.AwsSaslContext): # type: ignore + # Dependency injection: + def binary_type(self) -> Type[Binary]: + """Return the bson.binary.Binary type.""" + return Binary + + def bson_encode(self, doc: Mapping[str, Any]) -> bytes: + """Encode a dictionary to BSON.""" + return bson.encode(doc) + + def bson_decode(self, data: _ReadableBuffer) -> Mapping[str, Any]: + """Decode BSON to a dictionary.""" + return bson.decode(data) + + try: + ctx = AwsSaslContext( + pymongo_auth_aws.AwsCredential( + credentials.username, + credentials.password, + credentials.mechanism_properties.aws_session_token, + ) + ) + client_payload = ctx.step(None) + client_first = {"saslStart": 1, "mechanism": "MONGODB-AWS", "payload": client_payload} + server_first = conn.command("$external", client_first) + res = server_first + # Limit how many times we loop to catch protocol / library issues + for _ in range(10): + client_payload = ctx.step(res["payload"]) + cmd = { + "saslContinue": 1, + "conversationId": server_first["conversationId"], + "payload": client_payload, + } + res = conn.command("$external", cmd) + if res["done"]: + # SASL complete. + break + except pymongo_auth_aws.PyMongoAuthAwsError as exc: + # Clear the cached credentials if we hit a failure in auth. + set_cached_credentials(None) + # Convert to OperationFailure and include pymongo-auth-aws version. + raise OperationFailure( + f"{exc} (pymongo-auth-aws version {pymongo_auth_aws.__version__})" + ) from None + except Exception: + # Clear the cached credentials if we hit a failure in auth. + set_cached_credentials(None) + raise diff --git a/venv/Lib/site-packages/pymongo/auth_oidc.py b/venv/Lib/site-packages/pymongo/auth_oidc.py new file mode 100644 index 00000000..bfe2340f --- /dev/null +++ b/venv/Lib/site-packages/pymongo/auth_oidc.py @@ -0,0 +1,365 @@ +# Copyright 2023-present MongoDB, 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. + +"""MONGODB-OIDC Authentication helpers.""" +from __future__ import annotations + +import abc +import os +import threading +import time +from dataclasses import dataclass, field +from typing import TYPE_CHECKING, Any, Mapping, MutableMapping, Optional, Union +from urllib.parse import quote + +import bson +from bson.binary import Binary +from pymongo._azure_helpers import _get_azure_response +from pymongo._csot import remaining +from pymongo._gcp_helpers import _get_gcp_response +from pymongo.errors import ConfigurationError, OperationFailure +from pymongo.helpers import _AUTHENTICATION_FAILURE_CODE + +if TYPE_CHECKING: + from pymongo.auth import MongoCredential + from pymongo.pool import Connection + + +@dataclass +class OIDCIdPInfo: + issuer: str + clientId: Optional[str] = field(default=None) + requestScopes: Optional[list[str]] = field(default=None) + + +@dataclass +class OIDCCallbackContext: + timeout_seconds: float + username: str + version: int + refresh_token: Optional[str] = field(default=None) + idp_info: Optional[OIDCIdPInfo] = field(default=None) + + +@dataclass +class OIDCCallbackResult: + access_token: str + expires_in_seconds: Optional[float] = field(default=None) + refresh_token: Optional[str] = field(default=None) + + +class OIDCCallback(abc.ABC): + """A base class for defining OIDC callbacks.""" + + @abc.abstractmethod + def fetch(self, context: OIDCCallbackContext) -> OIDCCallbackResult: + """Convert the given BSON value into our own type.""" + + +@dataclass +class _OIDCProperties: + callback: Optional[OIDCCallback] = field(default=None) + human_callback: Optional[OIDCCallback] = field(default=None) + environment: Optional[str] = field(default=None) + allowed_hosts: list[str] = field(default_factory=list) + token_resource: Optional[str] = field(default=None) + username: str = "" + + +"""Mechanism properties for MONGODB-OIDC authentication.""" + +TOKEN_BUFFER_MINUTES = 5 +HUMAN_CALLBACK_TIMEOUT_SECONDS = 5 * 60 +CALLBACK_VERSION = 1 +MACHINE_CALLBACK_TIMEOUT_SECONDS = 60 +TIME_BETWEEN_CALLS_SECONDS = 0.1 + + +def _get_authenticator( + credentials: MongoCredential, address: tuple[str, int] +) -> _OIDCAuthenticator: + if credentials.cache.data: + return credentials.cache.data + + # Extract values. + principal_name = credentials.username + properties = credentials.mechanism_properties + + # Validate that the address is allowed. + if not properties.environment: + found = False + allowed_hosts = properties.allowed_hosts + for patt in allowed_hosts: + if patt == address[0]: + found = True + elif patt.startswith("*.") and address[0].endswith(patt[1:]): + found = True + if not found: + raise ConfigurationError( + f"Refusing to connect to {address[0]}, which is not in authOIDCAllowedHosts: {allowed_hosts}" + ) + + # Get or create the cache data. + credentials.cache.data = _OIDCAuthenticator(username=principal_name, properties=properties) + return credentials.cache.data + + +class _OIDCTestCallback(OIDCCallback): + def fetch(self, context: OIDCCallbackContext) -> OIDCCallbackResult: + token_file = os.environ.get("OIDC_TOKEN_FILE") + if not token_file: + raise RuntimeError( + 'MONGODB-OIDC with an "test" provider requires "OIDC_TOKEN_FILE" to be set' + ) + with open(token_file) as fid: + return OIDCCallbackResult(access_token=fid.read().strip()) + + +class _OIDCAzureCallback(OIDCCallback): + def __init__(self, token_resource: str) -> None: + self.token_resource = quote(token_resource) + + def fetch(self, context: OIDCCallbackContext) -> OIDCCallbackResult: + resp = _get_azure_response(self.token_resource, context.username, context.timeout_seconds) + return OIDCCallbackResult( + access_token=resp["access_token"], expires_in_seconds=resp["expires_in"] + ) + + +class _OIDCGCPCallback(OIDCCallback): + def __init__(self, token_resource: str) -> None: + self.token_resource = quote(token_resource) + + def fetch(self, context: OIDCCallbackContext) -> OIDCCallbackResult: + resp = _get_gcp_response(self.token_resource, context.timeout_seconds) + return OIDCCallbackResult(access_token=resp["access_token"]) + + +@dataclass +class _OIDCAuthenticator: + username: str + properties: _OIDCProperties + refresh_token: Optional[str] = field(default=None) + access_token: Optional[str] = field(default=None) + idp_info: Optional[OIDCIdPInfo] = field(default=None) + token_gen_id: int = field(default=0) + lock: threading.Lock = field(default_factory=threading.Lock) + last_call_time: float = field(default=0) + + def reauthenticate(self, conn: Connection) -> Optional[Mapping[str, Any]]: + """Handle a reauthenticate from the server.""" + # Invalidate the token for the connection. + self._invalidate(conn) + # Call the appropriate auth logic for the callback type. + if self.properties.callback: + return self._authenticate_machine(conn) + return self._authenticate_human(conn) + + def authenticate(self, conn: Connection) -> Optional[Mapping[str, Any]]: + """Handle an initial authenticate request.""" + # First handle speculative auth. + # If it succeeded, we are done. + ctx = conn.auth_ctx + if ctx and ctx.speculate_succeeded(): + resp = ctx.speculative_authenticate + if resp and resp["done"]: + conn.oidc_token_gen_id = self.token_gen_id + return resp + + # If spec auth failed, call the appropriate auth logic for the callback type. + # We cannot assume that the token is invalid, because a proxy may have been + # involved that stripped the speculative auth information. + if self.properties.callback: + return self._authenticate_machine(conn) + return self._authenticate_human(conn) + + def get_spec_auth_cmd(self) -> Optional[MutableMapping[str, Any]]: + """Get the appropriate speculative auth command.""" + if not self.access_token: + return None + return self._get_start_command({"jwt": self.access_token}) + + def _authenticate_machine(self, conn: Connection) -> Mapping[str, Any]: + # If there is a cached access token, try to authenticate with it. If + # authentication fails with error code 18, invalidate the access token, + # fetch a new access token, and try to authenticate again. If authentication + # fails for any other reason, raise the error to the user. + if self.access_token: + try: + return self._sasl_start_jwt(conn) + except OperationFailure as e: + if self._is_auth_error(e): + return self._authenticate_machine(conn) + raise + return self._sasl_start_jwt(conn) + + def _authenticate_human(self, conn: Connection) -> Optional[Mapping[str, Any]]: + # If we have a cached access token, try a JwtStepRequest. + # authentication fails with error code 18, invalidate the access token, + # and try to authenticate again. If authentication fails for any other + # reason, raise the error to the user. + if self.access_token: + try: + return self._sasl_start_jwt(conn) + except OperationFailure as e: + if self._is_auth_error(e): + return self._authenticate_human(conn) + raise + + # If we have a cached refresh token, try a JwtStepRequest with that. + # If authentication fails with error code 18, invalidate the access and + # refresh tokens, and try to authenticate again. If authentication fails for + # any other reason, raise the error to the user. + if self.refresh_token: + try: + return self._sasl_start_jwt(conn) + except OperationFailure as e: + if self._is_auth_error(e): + self.refresh_token = None + return self._authenticate_human(conn) + raise + + # Start a new Two-Step SASL conversation. + # Run a PrincipalStepRequest to get the IdpInfo. + cmd = self._get_start_command(None) + start_resp = self._run_command(conn, cmd) + # Attempt to authenticate with a JwtStepRequest. + return self._sasl_continue_jwt(conn, start_resp) + + def _get_access_token(self) -> Optional[str]: + properties = self.properties + cb: Union[None, OIDCCallback] + resp: OIDCCallbackResult + + is_human = properties.human_callback is not None + if is_human and self.idp_info is None: + return None + + if properties.callback: + cb = properties.callback + if properties.human_callback: + cb = properties.human_callback + + prev_token = self.access_token + if prev_token: + return prev_token + + if cb is None and not prev_token: + return None + + if not prev_token and cb is not None: + with self.lock: + # See if the token was changed while we were waiting for the + # lock. + new_token = self.access_token + if new_token != prev_token: + return new_token + + # Ensure that we are waiting a min time between callback invocations. + delta = time.time() - self.last_call_time + if delta < TIME_BETWEEN_CALLS_SECONDS: + time.sleep(TIME_BETWEEN_CALLS_SECONDS - delta) + self.last_call_time = time.time() + + if is_human: + timeout = HUMAN_CALLBACK_TIMEOUT_SECONDS + assert self.idp_info is not None + else: + timeout = int(remaining() or MACHINE_CALLBACK_TIMEOUT_SECONDS) + context = OIDCCallbackContext( + timeout_seconds=timeout, + version=CALLBACK_VERSION, + refresh_token=self.refresh_token, + idp_info=self.idp_info, + username=self.properties.username, + ) + resp = cb.fetch(context) + if not isinstance(resp, OIDCCallbackResult): + raise ValueError("Callback result must be of type OIDCCallbackResult") + self.refresh_token = resp.refresh_token + self.access_token = resp.access_token + self.token_gen_id += 1 + + return self.access_token + + def _run_command(self, conn: Connection, cmd: MutableMapping[str, Any]) -> Mapping[str, Any]: + try: + return conn.command("$external", cmd, no_reauth=True) # type: ignore[call-arg] + except OperationFailure as e: + if self._is_auth_error(e): + self._invalidate(conn) + raise + + def _is_auth_error(self, err: Exception) -> bool: + if not isinstance(err, OperationFailure): + return False + return err.code == _AUTHENTICATION_FAILURE_CODE + + def _invalidate(self, conn: Connection) -> None: + # Ignore the invalidation if a token gen id is given and is less than our + # current token gen id. + token_gen_id = conn.oidc_token_gen_id or 0 + if token_gen_id is not None and token_gen_id < self.token_gen_id: + return + self.access_token = None + + def _sasl_continue_jwt( + self, conn: Connection, start_resp: Mapping[str, Any] + ) -> Mapping[str, Any]: + self.access_token = None + self.refresh_token = None + start_payload: dict = bson.decode(start_resp["payload"]) + if "issuer" in start_payload: + self.idp_info = OIDCIdPInfo(**start_payload) + access_token = self._get_access_token() + conn.oidc_token_gen_id = self.token_gen_id + cmd = self._get_continue_command({"jwt": access_token}, start_resp) + return self._run_command(conn, cmd) + + def _sasl_start_jwt(self, conn: Connection) -> Mapping[str, Any]: + access_token = self._get_access_token() + conn.oidc_token_gen_id = self.token_gen_id + cmd = self._get_start_command({"jwt": access_token}) + return self._run_command(conn, cmd) + + def _get_start_command(self, payload: Optional[Mapping[str, Any]]) -> MutableMapping[str, Any]: + if payload is None: + principal_name = self.username + if principal_name: + payload = {"n": principal_name} + else: + payload = {} + bin_payload = Binary(bson.encode(payload)) + return {"saslStart": 1, "mechanism": "MONGODB-OIDC", "payload": bin_payload} + + def _get_continue_command( + self, payload: Mapping[str, Any], start_resp: Mapping[str, Any] + ) -> MutableMapping[str, Any]: + bin_payload = Binary(bson.encode(payload)) + return { + "saslContinue": 1, + "payload": bin_payload, + "conversationId": start_resp["conversationId"], + } + + +def _authenticate_oidc( + credentials: MongoCredential, conn: Connection, reauthenticate: bool +) -> Optional[Mapping[str, Any]]: + """Authenticate using MONGODB-OIDC.""" + authenticator = _get_authenticator(credentials, conn.address) + if reauthenticate: + return authenticator.reauthenticate(conn) + else: + return authenticator.authenticate(conn) diff --git a/venv/Lib/site-packages/pymongo/bulk.py b/venv/Lib/site-packages/pymongo/bulk.py new file mode 100644 index 00000000..e1c46105 --- /dev/null +++ b/venv/Lib/site-packages/pymongo/bulk.py @@ -0,0 +1,595 @@ +# Copyright 2014-present MongoDB, 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. + +"""The bulk write operations interface. + +.. versionadded:: 2.7 +""" +from __future__ import annotations + +import copy +from collections.abc import MutableMapping +from itertools import islice +from typing import ( + TYPE_CHECKING, + Any, + Iterator, + Mapping, + NoReturn, + Optional, + Type, + Union, +) + +from bson.objectid import ObjectId +from bson.raw_bson import RawBSONDocument +from pymongo import _csot, common +from pymongo.client_session import ClientSession, _validate_session_write_concern +from pymongo.common import ( + validate_is_document_type, + validate_ok_for_replace, + validate_ok_for_update, +) +from pymongo.errors import ( + BulkWriteError, + ConfigurationError, + InvalidOperation, + OperationFailure, +) +from pymongo.helpers import _RETRYABLE_ERROR_CODES, _get_wce_doc +from pymongo.message import ( + _DELETE, + _INSERT, + _UPDATE, + _BulkWriteContext, + _EncryptedBulkWriteContext, + _randint, +) +from pymongo.read_preferences import ReadPreference +from pymongo.write_concern import WriteConcern + +if TYPE_CHECKING: + from pymongo.collection import Collection + from pymongo.pool import Connection + from pymongo.typings import _DocumentOut, _DocumentType, _Pipeline + +_DELETE_ALL: int = 0 +_DELETE_ONE: int = 1 + +# For backwards compatibility. See MongoDB src/mongo/base/error_codes.err +_BAD_VALUE: int = 2 +_UNKNOWN_ERROR: int = 8 +_WRITE_CONCERN_ERROR: int = 64 + +_COMMANDS: tuple[str, str, str] = ("insert", "update", "delete") + + +class _Run: + """Represents a batch of write operations.""" + + def __init__(self, op_type: int) -> None: + """Initialize a new Run object.""" + self.op_type: int = op_type + self.index_map: list[int] = [] + self.ops: list[Any] = [] + self.idx_offset: int = 0 + + def index(self, idx: int) -> int: + """Get the original index of an operation in this run. + + :param idx: The Run index that maps to the original index. + """ + return self.index_map[idx] + + def add(self, original_index: int, operation: Any) -> None: + """Add an operation to this Run instance. + + :param original_index: The original index of this operation + within a larger bulk operation. + :param operation: The operation document. + """ + self.index_map.append(original_index) + self.ops.append(operation) + + +def _merge_command( + run: _Run, + full_result: MutableMapping[str, Any], + offset: int, + result: Mapping[str, Any], +) -> None: + """Merge a write command result into the full bulk result.""" + affected = result.get("n", 0) + + if run.op_type == _INSERT: + full_result["nInserted"] += affected + + elif run.op_type == _DELETE: + full_result["nRemoved"] += affected + + elif run.op_type == _UPDATE: + upserted = result.get("upserted") + if upserted: + n_upserted = len(upserted) + for doc in upserted: + doc["index"] = run.index(doc["index"] + offset) + full_result["upserted"].extend(upserted) + full_result["nUpserted"] += n_upserted + full_result["nMatched"] += affected - n_upserted + else: + full_result["nMatched"] += affected + full_result["nModified"] += result["nModified"] + + write_errors = result.get("writeErrors") + if write_errors: + for doc in write_errors: + # Leave the server response intact for APM. + replacement = doc.copy() + idx = doc["index"] + offset + replacement["index"] = run.index(idx) + # Add the failed operation to the error document. + replacement["op"] = run.ops[idx] + full_result["writeErrors"].append(replacement) + + wce = _get_wce_doc(result) + if wce: + full_result["writeConcernErrors"].append(wce) + + +def _raise_bulk_write_error(full_result: _DocumentOut) -> NoReturn: + """Raise a BulkWriteError from the full bulk api result.""" + # retryWrites on MMAPv1 should raise an actionable error. + if full_result["writeErrors"]: + full_result["writeErrors"].sort(key=lambda error: error["index"]) + err = full_result["writeErrors"][0] + code = err["code"] + msg = err["errmsg"] + if code == 20 and msg.startswith("Transaction numbers"): + errmsg = ( + "This MongoDB deployment does not support " + "retryable writes. Please add retryWrites=false " + "to your connection string." + ) + raise OperationFailure(errmsg, code, full_result) + raise BulkWriteError(full_result) + + +class _Bulk: + """The private guts of the bulk write API.""" + + def __init__( + self, + collection: Collection[_DocumentType], + ordered: bool, + bypass_document_validation: bool, + comment: Optional[str] = None, + let: Optional[Any] = None, + ) -> None: + """Initialize a _Bulk instance.""" + self.collection = collection.with_options( + codec_options=collection.codec_options._replace( + unicode_decode_error_handler="replace", document_class=dict + ) + ) + self.let = let + if self.let is not None: + common.validate_is_document_type("let", self.let) + self.comment: Optional[str] = comment + self.ordered = ordered + self.ops: list[tuple[int, Mapping[str, Any]]] = [] + self.executed = False + self.bypass_doc_val = bypass_document_validation + self.uses_collation = False + self.uses_array_filters = False + self.uses_hint_update = False + self.uses_hint_delete = False + self.is_retryable = True + self.retrying = False + self.started_retryable_write = False + # Extra state so that we know where to pick up on a retry attempt. + self.current_run = None + self.next_run = None + + @property + def bulk_ctx_class(self) -> Type[_BulkWriteContext]: + encrypter = self.collection.database.client._encrypter + if encrypter and not encrypter._bypass_auto_encryption: + return _EncryptedBulkWriteContext + else: + return _BulkWriteContext + + def add_insert(self, document: _DocumentOut) -> None: + """Add an insert document to the list of ops.""" + validate_is_document_type("document", document) + # Generate ObjectId client side. + if not (isinstance(document, RawBSONDocument) or "_id" in document): + document["_id"] = ObjectId() + self.ops.append((_INSERT, document)) + + def add_update( + self, + selector: Mapping[str, Any], + update: Union[Mapping[str, Any], _Pipeline], + multi: bool = False, + upsert: bool = False, + collation: Optional[Mapping[str, Any]] = None, + array_filters: Optional[list[Mapping[str, Any]]] = None, + hint: Union[str, dict[str, Any], None] = None, + ) -> None: + """Create an update document and add it to the list of ops.""" + validate_ok_for_update(update) + cmd: dict[str, Any] = dict( # noqa: C406 + [("q", selector), ("u", update), ("multi", multi), ("upsert", upsert)] + ) + if collation is not None: + self.uses_collation = True + cmd["collation"] = collation + if array_filters is not None: + self.uses_array_filters = True + cmd["arrayFilters"] = array_filters + if hint is not None: + self.uses_hint_update = True + cmd["hint"] = hint + if multi: + # A bulk_write containing an update_many is not retryable. + self.is_retryable = False + self.ops.append((_UPDATE, cmd)) + + def add_replace( + self, + selector: Mapping[str, Any], + replacement: Mapping[str, Any], + upsert: bool = False, + collation: Optional[Mapping[str, Any]] = None, + hint: Union[str, dict[str, Any], None] = None, + ) -> None: + """Create a replace document and add it to the list of ops.""" + validate_ok_for_replace(replacement) + cmd = {"q": selector, "u": replacement, "multi": False, "upsert": upsert} + if collation is not None: + self.uses_collation = True + cmd["collation"] = collation + if hint is not None: + self.uses_hint_update = True + cmd["hint"] = hint + self.ops.append((_UPDATE, cmd)) + + def add_delete( + self, + selector: Mapping[str, Any], + limit: int, + collation: Optional[Mapping[str, Any]] = None, + hint: Union[str, dict[str, Any], None] = None, + ) -> None: + """Create a delete document and add it to the list of ops.""" + cmd = {"q": selector, "limit": limit} + if collation is not None: + self.uses_collation = True + cmd["collation"] = collation + if hint is not None: + self.uses_hint_delete = True + cmd["hint"] = hint + if limit == _DELETE_ALL: + # A bulk_write containing a delete_many is not retryable. + self.is_retryable = False + self.ops.append((_DELETE, cmd)) + + def gen_ordered(self) -> Iterator[Optional[_Run]]: + """Generate batches of operations, batched by type of + operation, in the order **provided**. + """ + run = None + for idx, (op_type, operation) in enumerate(self.ops): + if run is None: + run = _Run(op_type) + elif run.op_type != op_type: + yield run + run = _Run(op_type) + run.add(idx, operation) + yield run + + def gen_unordered(self) -> Iterator[_Run]: + """Generate batches of operations, batched by type of + operation, in arbitrary order. + """ + operations = [_Run(_INSERT), _Run(_UPDATE), _Run(_DELETE)] + for idx, (op_type, operation) in enumerate(self.ops): + operations[op_type].add(idx, operation) + + for run in operations: + if run.ops: + yield run + + def _execute_command( + self, + generator: Iterator[Any], + write_concern: WriteConcern, + session: Optional[ClientSession], + conn: Connection, + op_id: int, + retryable: bool, + full_result: MutableMapping[str, Any], + final_write_concern: Optional[WriteConcern] = None, + ) -> None: + db_name = self.collection.database.name + client = self.collection.database.client + listeners = client._event_listeners + + if not self.current_run: + self.current_run = next(generator) + self.next_run = None + run = self.current_run + + # Connection.command validates the session, but we use + # Connection.write_command + conn.validate_session(client, session) + last_run = False + + while run: + if not self.retrying: + self.next_run = next(generator, None) + if self.next_run is None: + last_run = True + + cmd_name = _COMMANDS[run.op_type] + bwc = self.bulk_ctx_class( + db_name, + cmd_name, + conn, + op_id, + listeners, + session, + run.op_type, + self.collection.codec_options, + ) + + while run.idx_offset < len(run.ops): + # If this is the last possible operation, use the + # final write concern. + if last_run and (len(run.ops) - run.idx_offset) == 1: + write_concern = final_write_concern or write_concern + + cmd = {cmd_name: self.collection.name, "ordered": self.ordered} + if self.comment: + cmd["comment"] = self.comment + _csot.apply_write_concern(cmd, write_concern) + if self.bypass_doc_val: + cmd["bypassDocumentValidation"] = True + if self.let is not None and run.op_type in (_DELETE, _UPDATE): + cmd["let"] = self.let + if session: + # Start a new retryable write unless one was already + # started for this command. + if retryable and not self.started_retryable_write: + session._start_retryable_write() + self.started_retryable_write = True + session._apply_to(cmd, retryable, ReadPreference.PRIMARY, conn) + conn.send_cluster_time(cmd, session, client) + conn.add_server_api(cmd) + # CSOT: apply timeout before encoding the command. + conn.apply_timeout(client, cmd) + ops = islice(run.ops, run.idx_offset, None) + + # Run as many ops as possible in one command. + if write_concern.acknowledged: + result, to_send = bwc.execute(cmd, ops, client) + + # Retryable writeConcernErrors halt the execution of this run. + wce = result.get("writeConcernError", {}) + if wce.get("code", 0) in _RETRYABLE_ERROR_CODES: + # Synthesize the full bulk result without modifying the + # current one because this write operation may be retried. + full = copy.deepcopy(full_result) + _merge_command(run, full, run.idx_offset, result) + _raise_bulk_write_error(full) + + _merge_command(run, full_result, run.idx_offset, result) + + # We're no longer in a retry once a command succeeds. + self.retrying = False + self.started_retryable_write = False + + if self.ordered and "writeErrors" in result: + break + else: + to_send = bwc.execute_unack(cmd, ops, client) + + run.idx_offset += len(to_send) + + # We're supposed to continue if errors are + # at the write concern level (e.g. wtimeout) + if self.ordered and full_result["writeErrors"]: + break + # Reset our state + self.current_run = run = self.next_run + + def execute_command( + self, + generator: Iterator[Any], + write_concern: WriteConcern, + session: Optional[ClientSession], + operation: str, + ) -> dict[str, Any]: + """Execute using write commands.""" + # nModified is only reported for write commands, not legacy ops. + full_result = { + "writeErrors": [], + "writeConcernErrors": [], + "nInserted": 0, + "nUpserted": 0, + "nMatched": 0, + "nModified": 0, + "nRemoved": 0, + "upserted": [], + } + op_id = _randint() + + def retryable_bulk( + session: Optional[ClientSession], conn: Connection, retryable: bool + ) -> None: + self._execute_command( + generator, + write_concern, + session, + conn, + op_id, + retryable, + full_result, + ) + + client = self.collection.database.client + client._retryable_write( + self.is_retryable, + retryable_bulk, + session, + operation, + bulk=self, + operation_id=op_id, + ) + + if full_result["writeErrors"] or full_result["writeConcernErrors"]: + _raise_bulk_write_error(full_result) + return full_result + + def execute_op_msg_no_results(self, conn: Connection, generator: Iterator[Any]) -> None: + """Execute write commands with OP_MSG and w=0 writeConcern, unordered.""" + db_name = self.collection.database.name + client = self.collection.database.client + listeners = client._event_listeners + op_id = _randint() + + if not self.current_run: + self.current_run = next(generator) + run = self.current_run + + while run: + cmd_name = _COMMANDS[run.op_type] + bwc = self.bulk_ctx_class( + db_name, + cmd_name, + conn, + op_id, + listeners, + None, + run.op_type, + self.collection.codec_options, + ) + + while run.idx_offset < len(run.ops): + cmd = { + cmd_name: self.collection.name, + "ordered": False, + "writeConcern": {"w": 0}, + } + conn.add_server_api(cmd) + ops = islice(run.ops, run.idx_offset, None) + # Run as many ops as possible. + to_send = bwc.execute_unack(cmd, ops, client) + run.idx_offset += len(to_send) + self.current_run = run = next(generator, None) + + def execute_command_no_results( + self, + conn: Connection, + generator: Iterator[Any], + write_concern: WriteConcern, + ) -> None: + """Execute write commands with OP_MSG and w=0 WriteConcern, ordered.""" + full_result = { + "writeErrors": [], + "writeConcernErrors": [], + "nInserted": 0, + "nUpserted": 0, + "nMatched": 0, + "nModified": 0, + "nRemoved": 0, + "upserted": [], + } + # Ordered bulk writes have to be acknowledged so that we stop + # processing at the first error, even when the application + # specified unacknowledged writeConcern. + initial_write_concern = WriteConcern() + op_id = _randint() + try: + self._execute_command( + generator, + initial_write_concern, + None, + conn, + op_id, + False, + full_result, + write_concern, + ) + except OperationFailure: + pass + + def execute_no_results( + self, + conn: Connection, + generator: Iterator[Any], + write_concern: WriteConcern, + ) -> None: + """Execute all operations, returning no results (w=0).""" + if self.uses_collation: + raise ConfigurationError("Collation is unsupported for unacknowledged writes.") + if self.uses_array_filters: + raise ConfigurationError("arrayFilters is unsupported for unacknowledged writes.") + # Guard against unsupported unacknowledged writes. + unack = write_concern and not write_concern.acknowledged + if unack and self.uses_hint_delete and conn.max_wire_version < 9: + raise ConfigurationError( + "Must be connected to MongoDB 4.4+ to use hint on unacknowledged delete commands." + ) + if unack and self.uses_hint_update and conn.max_wire_version < 8: + raise ConfigurationError( + "Must be connected to MongoDB 4.2+ to use hint on unacknowledged update commands." + ) + # Cannot have both unacknowledged writes and bypass document validation. + if self.bypass_doc_val: + raise OperationFailure( + "Cannot set bypass_document_validation with unacknowledged write concern" + ) + + if self.ordered: + return self.execute_command_no_results(conn, generator, write_concern) + return self.execute_op_msg_no_results(conn, generator) + + def execute( + self, + write_concern: WriteConcern, + session: Optional[ClientSession], + operation: str, + ) -> Any: + """Execute operations.""" + if not self.ops: + raise InvalidOperation("No operations to execute") + if self.executed: + raise InvalidOperation("Bulk operations can only be executed once.") + self.executed = True + write_concern = write_concern or self.collection.write_concern + session = _validate_session_write_concern(session, write_concern) + + if self.ordered: + generator = self.gen_ordered() + else: + generator = self.gen_unordered() + + client = self.collection.database.client + if not write_concern.acknowledged: + with client._conn_for_writes(session, operation) as connection: + self.execute_no_results(connection, generator, write_concern) + return None + else: + return self.execute_command(generator, write_concern, session, operation) diff --git a/venv/Lib/site-packages/pymongo/change_stream.py b/venv/Lib/site-packages/pymongo/change_stream.py new file mode 100644 index 00000000..dc2f6bf2 --- /dev/null +++ b/venv/Lib/site-packages/pymongo/change_stream.py @@ -0,0 +1,490 @@ +# Copyright 2017 MongoDB, 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. + +"""Watch changes on a collection, a database, or the entire cluster.""" +from __future__ import annotations + +import copy +from typing import TYPE_CHECKING, Any, Generic, Mapping, Optional, Type, Union + +from bson import CodecOptions, _bson_to_dict +from bson.raw_bson import RawBSONDocument +from bson.timestamp import Timestamp +from pymongo import _csot, common +from pymongo.aggregation import ( + _AggregationCommand, + _CollectionAggregationCommand, + _DatabaseAggregationCommand, +) +from pymongo.collation import validate_collation_or_none +from pymongo.command_cursor import CommandCursor +from pymongo.errors import ( + ConnectionFailure, + CursorNotFound, + InvalidOperation, + OperationFailure, + PyMongoError, +) +from pymongo.operations import _Op +from pymongo.typings import _CollationIn, _DocumentType, _Pipeline + +# The change streams spec considers the following server errors from the +# getMore command non-resumable. All other getMore errors are resumable. +_RESUMABLE_GETMORE_ERRORS = frozenset( + [ + 6, # HostUnreachable + 7, # HostNotFound + 89, # NetworkTimeout + 91, # ShutdownInProgress + 189, # PrimarySteppedDown + 262, # ExceededTimeLimit + 9001, # SocketException + 10107, # NotWritablePrimary + 11600, # InterruptedAtShutdown + 11602, # InterruptedDueToReplStateChange + 13435, # NotPrimaryNoSecondaryOk + 13436, # NotPrimaryOrSecondary + 63, # StaleShardVersion + 150, # StaleEpoch + 13388, # StaleConfig + 234, # RetryChangeStream + 133, # FailedToSatisfyReadPreference + ] +) + + +if TYPE_CHECKING: + from pymongo.client_session import ClientSession + from pymongo.collection import Collection + from pymongo.database import Database + from pymongo.mongo_client import MongoClient + from pymongo.pool import Connection + + +def _resumable(exc: PyMongoError) -> bool: + """Return True if given a resumable change stream error.""" + if isinstance(exc, (ConnectionFailure, CursorNotFound)): + return True + if isinstance(exc, OperationFailure): + if exc._max_wire_version is None: + return False + return ( + exc._max_wire_version >= 9 and exc.has_error_label("ResumableChangeStreamError") + ) or (exc._max_wire_version < 9 and exc.code in _RESUMABLE_GETMORE_ERRORS) + return False + + +class ChangeStream(Generic[_DocumentType]): + """The internal abstract base class for change stream cursors. + + Should not be called directly by application developers. Use + :meth:`pymongo.collection.Collection.watch`, + :meth:`pymongo.database.Database.watch`, or + :meth:`pymongo.mongo_client.MongoClient.watch` instead. + + .. versionadded:: 3.6 + .. seealso:: The MongoDB documentation on `changeStreams `_. + """ + + def __init__( + self, + target: Union[ + MongoClient[_DocumentType], Database[_DocumentType], Collection[_DocumentType] + ], + pipeline: Optional[_Pipeline], + full_document: Optional[str], + resume_after: Optional[Mapping[str, Any]], + max_await_time_ms: Optional[int], + batch_size: Optional[int], + collation: Optional[_CollationIn], + start_at_operation_time: Optional[Timestamp], + session: Optional[ClientSession], + start_after: Optional[Mapping[str, Any]], + comment: Optional[Any] = None, + full_document_before_change: Optional[str] = None, + show_expanded_events: Optional[bool] = None, + ) -> None: + if pipeline is None: + pipeline = [] + pipeline = common.validate_list("pipeline", pipeline) + common.validate_string_or_none("full_document", full_document) + validate_collation_or_none(collation) + common.validate_non_negative_integer_or_none("batchSize", batch_size) + + self._decode_custom = False + self._orig_codec_options: CodecOptions[_DocumentType] = target.codec_options + if target.codec_options.type_registry._decoder_map: + self._decode_custom = True + # Keep the type registry so that we support encoding custom types + # in the pipeline. + self._target = target.with_options( # type: ignore + codec_options=target.codec_options.with_options(document_class=RawBSONDocument) + ) + else: + self._target = target + + self._pipeline = copy.deepcopy(pipeline) + self._full_document = full_document + self._full_document_before_change = full_document_before_change + self._uses_start_after = start_after is not None + self._uses_resume_after = resume_after is not None + self._resume_token = copy.deepcopy(start_after or resume_after) + self._max_await_time_ms = max_await_time_ms + self._batch_size = batch_size + self._collation = collation + self._start_at_operation_time = start_at_operation_time + self._session = session + self._comment = comment + self._closed = False + self._timeout = self._target._timeout + self._show_expanded_events = show_expanded_events + # Initialize cursor. + self._cursor = self._create_cursor() + + @property + def _aggregation_command_class(self) -> Type[_AggregationCommand]: + """The aggregation command class to be used.""" + raise NotImplementedError + + @property + def _client(self) -> MongoClient: + """The client against which the aggregation commands for + this ChangeStream will be run. + """ + raise NotImplementedError + + def _change_stream_options(self) -> dict[str, Any]: + """Return the options dict for the $changeStream pipeline stage.""" + options: dict[str, Any] = {} + if self._full_document is not None: + options["fullDocument"] = self._full_document + + if self._full_document_before_change is not None: + options["fullDocumentBeforeChange"] = self._full_document_before_change + + resume_token = self.resume_token + if resume_token is not None: + if self._uses_start_after: + options["startAfter"] = resume_token + else: + options["resumeAfter"] = resume_token + + if self._start_at_operation_time is not None: + options["startAtOperationTime"] = self._start_at_operation_time + + if self._show_expanded_events: + options["showExpandedEvents"] = self._show_expanded_events + + return options + + def _command_options(self) -> dict[str, Any]: + """Return the options dict for the aggregation command.""" + options = {} + if self._max_await_time_ms is not None: + options["maxAwaitTimeMS"] = self._max_await_time_ms + if self._batch_size is not None: + options["batchSize"] = self._batch_size + return options + + def _aggregation_pipeline(self) -> list[dict[str, Any]]: + """Return the full aggregation pipeline for this ChangeStream.""" + options = self._change_stream_options() + full_pipeline: list = [{"$changeStream": options}] + full_pipeline.extend(self._pipeline) + return full_pipeline + + def _process_result(self, result: Mapping[str, Any], conn: Connection) -> None: + """Callback that caches the postBatchResumeToken or + startAtOperationTime from a changeStream aggregate command response + containing an empty batch of change documents. + + This is implemented as a callback because we need access to the wire + version in order to determine whether to cache this value. + """ + if not result["cursor"]["firstBatch"]: + if "postBatchResumeToken" in result["cursor"]: + self._resume_token = result["cursor"]["postBatchResumeToken"] + elif ( + self._start_at_operation_time is None + and self._uses_resume_after is False + and self._uses_start_after is False + and conn.max_wire_version >= 7 + ): + self._start_at_operation_time = result.get("operationTime") + # PYTHON-2181: informative error on missing operationTime. + if self._start_at_operation_time is None: + raise OperationFailure( + "Expected field 'operationTime' missing from command " + f"response : {result!r}" + ) + + def _run_aggregation_cmd( + self, session: Optional[ClientSession], explicit_session: bool + ) -> CommandCursor: + """Run the full aggregation pipeline for this ChangeStream and return + the corresponding CommandCursor. + """ + cmd = self._aggregation_command_class( + self._target, + CommandCursor, + self._aggregation_pipeline(), + self._command_options(), + explicit_session, + result_processor=self._process_result, + comment=self._comment, + ) + return self._client._retryable_read( + cmd.get_cursor, + self._target._read_preference_for(session), + session, + operation=_Op.AGGREGATE, + ) + + def _create_cursor(self) -> CommandCursor: + with self._client._tmp_session(self._session, close=False) as s: + return self._run_aggregation_cmd(session=s, explicit_session=self._session is not None) + + def _resume(self) -> None: + """Reestablish this change stream after a resumable error.""" + try: + self._cursor.close() + except PyMongoError: + pass + self._cursor = self._create_cursor() + + def close(self) -> None: + """Close this ChangeStream.""" + self._closed = True + self._cursor.close() + + def __iter__(self) -> ChangeStream[_DocumentType]: + return self + + @property + def resume_token(self) -> Optional[Mapping[str, Any]]: + """The cached resume token that will be used to resume after the most + recently returned change. + + .. versionadded:: 3.9 + """ + return copy.deepcopy(self._resume_token) + + @_csot.apply + def next(self) -> _DocumentType: + """Advance the cursor. + + This method blocks until the next change document is returned or an + unrecoverable error is raised. This method is used when iterating over + all changes in the cursor. For example:: + + try: + resume_token = None + pipeline = [{'$match': {'operationType': 'insert'}}] + with db.collection.watch(pipeline) as stream: + for insert_change in stream: + print(insert_change) + resume_token = stream.resume_token + except pymongo.errors.PyMongoError: + # The ChangeStream encountered an unrecoverable error or the + # resume attempt failed to recreate the cursor. + if resume_token is None: + # There is no usable resume token because there was a + # failure during ChangeStream initialization. + logging.error('...') + else: + # Use the interrupted ChangeStream's resume token to create + # a new ChangeStream. The new stream will continue from the + # last seen insert change without missing any events. + with db.collection.watch( + pipeline, resume_after=resume_token) as stream: + for insert_change in stream: + print(insert_change) + + Raises :exc:`StopIteration` if this ChangeStream is closed. + """ + while self.alive: + doc = self.try_next() + if doc is not None: + return doc + + raise StopIteration + + __next__ = next + + @property + def alive(self) -> bool: + """Does this cursor have the potential to return more data? + + .. note:: Even if :attr:`alive` is ``True``, :meth:`next` can raise + :exc:`StopIteration` and :meth:`try_next` can return ``None``. + + .. versionadded:: 3.8 + """ + return not self._closed + + @_csot.apply + def try_next(self) -> Optional[_DocumentType]: + """Advance the cursor without blocking indefinitely. + + This method returns the next change document without waiting + indefinitely for the next change. For example:: + + with db.collection.watch() as stream: + while stream.alive: + change = stream.try_next() + # Note that the ChangeStream's resume token may be updated + # even when no changes are returned. + print("Current resume token: %r" % (stream.resume_token,)) + if change is not None: + print("Change document: %r" % (change,)) + continue + # We end up here when there are no recent changes. + # Sleep for a while before trying again to avoid flooding + # the server with getMore requests when no changes are + # available. + time.sleep(10) + + If no change document is cached locally then this method runs a single + getMore command. If the getMore yields any documents, the next + document is returned, otherwise, if the getMore returns no documents + (because there have been no changes) then ``None`` is returned. + + :return: The next change document or ``None`` when no document is available + after running a single getMore or when the cursor is closed. + + .. versionadded:: 3.8 + """ + if not self._closed and not self._cursor.alive: + self._resume() + + # Attempt to get the next change with at most one getMore and at most + # one resume attempt. + try: + try: + change = self._cursor._try_next(True) + except PyMongoError as exc: + if not _resumable(exc): + raise + self._resume() + change = self._cursor._try_next(False) + except PyMongoError as exc: + # Close the stream after a fatal error. + if not _resumable(exc) and not exc.timeout: + self.close() + raise + except Exception: + self.close() + raise + + # Check if the cursor was invalidated. + if not self._cursor.alive: + self._closed = True + + # If no changes are available. + if change is None: + # We have either iterated over all documents in the cursor, + # OR the most-recently returned batch is empty. In either case, + # update the cached resume token with the postBatchResumeToken if + # one was returned. We also clear the startAtOperationTime. + if self._cursor._post_batch_resume_token is not None: + self._resume_token = self._cursor._post_batch_resume_token + self._start_at_operation_time = None + return change + + # Else, changes are available. + try: + resume_token = change["_id"] + except KeyError: + self.close() + raise InvalidOperation( + "Cannot provide resume functionality when the resume token is missing." + ) from None + + # If this is the last change document from the current batch, cache the + # postBatchResumeToken. + if not self._cursor._has_next() and self._cursor._post_batch_resume_token: + resume_token = self._cursor._post_batch_resume_token + + # Hereafter, don't use startAfter; instead use resumeAfter. + self._uses_start_after = False + self._uses_resume_after = True + + # Cache the resume token and clear startAtOperationTime. + self._resume_token = resume_token + self._start_at_operation_time = None + + if self._decode_custom: + return _bson_to_dict(change.raw, self._orig_codec_options) + return change + + def __enter__(self) -> ChangeStream[_DocumentType]: + return self + + def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None: + self.close() + + +class CollectionChangeStream(ChangeStream[_DocumentType]): + """A change stream that watches changes on a single collection. + + Should not be called directly by application developers. Use + helper method :meth:`pymongo.collection.Collection.watch` instead. + + .. versionadded:: 3.7 + """ + + _target: Collection[_DocumentType] + + @property + def _aggregation_command_class(self) -> Type[_CollectionAggregationCommand]: + return _CollectionAggregationCommand + + @property + def _client(self) -> MongoClient[_DocumentType]: + return self._target.database.client + + +class DatabaseChangeStream(ChangeStream[_DocumentType]): + """A change stream that watches changes on all collections in a database. + + Should not be called directly by application developers. Use + helper method :meth:`pymongo.database.Database.watch` instead. + + .. versionadded:: 3.7 + """ + + _target: Database[_DocumentType] + + @property + def _aggregation_command_class(self) -> Type[_DatabaseAggregationCommand]: + return _DatabaseAggregationCommand + + @property + def _client(self) -> MongoClient[_DocumentType]: + return self._target.client + + +class ClusterChangeStream(DatabaseChangeStream[_DocumentType]): + """A change stream that watches changes on all collections in the cluster. + + Should not be called directly by application developers. Use + helper method :meth:`pymongo.mongo_client.MongoClient.watch` instead. + + .. versionadded:: 3.7 + """ + + def _change_stream_options(self) -> dict[str, Any]: + options = super()._change_stream_options() + options["allChangesForCluster"] = True + return options diff --git a/venv/Lib/site-packages/pymongo/client_options.py b/venv/Lib/site-packages/pymongo/client_options.py new file mode 100644 index 00000000..60332605 --- /dev/null +++ b/venv/Lib/site-packages/pymongo/client_options.py @@ -0,0 +1,330 @@ +# Copyright 2014-present MongoDB, 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. + +"""Tools to parse mongo client options.""" +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, Mapping, Optional, Sequence, cast + +from bson.codec_options import _parse_codec_options +from pymongo import common +from pymongo.auth import MongoCredential, _build_credentials_tuple +from pymongo.compression_support import CompressionSettings +from pymongo.errors import ConfigurationError +from pymongo.monitoring import _EventListener, _EventListeners +from pymongo.pool import PoolOptions +from pymongo.read_concern import ReadConcern +from pymongo.read_preferences import ( + _ServerMode, + make_read_preference, + read_pref_mode_from_name, +) +from pymongo.server_selectors import any_server_selector +from pymongo.ssl_support import get_ssl_context +from pymongo.write_concern import WriteConcern, validate_boolean + +if TYPE_CHECKING: + from bson.codec_options import CodecOptions + from pymongo.encryption_options import AutoEncryptionOpts + from pymongo.pyopenssl_context import SSLContext + from pymongo.topology_description import _ServerSelector + + +def _parse_credentials( + username: str, password: str, database: Optional[str], options: Mapping[str, Any] +) -> Optional[MongoCredential]: + """Parse authentication credentials.""" + mechanism = options.get("authmechanism", "DEFAULT" if username else None) + source = options.get("authsource") + if username or mechanism: + return _build_credentials_tuple(mechanism, source, username, password, options, database) + return None + + +def _parse_read_preference(options: Mapping[str, Any]) -> _ServerMode: + """Parse read preference options.""" + if "read_preference" in options: + return options["read_preference"] + + name = options.get("readpreference", "primary") + mode = read_pref_mode_from_name(name) + tags = options.get("readpreferencetags") + max_staleness = options.get("maxstalenessseconds", -1) + return make_read_preference(mode, tags, max_staleness) + + +def _parse_write_concern(options: Mapping[str, Any]) -> WriteConcern: + """Parse write concern options.""" + concern = options.get("w") + wtimeout = options.get("wtimeoutms") + j = options.get("journal") + fsync = options.get("fsync") + return WriteConcern(concern, wtimeout, j, fsync) + + +def _parse_read_concern(options: Mapping[str, Any]) -> ReadConcern: + """Parse read concern options.""" + concern = options.get("readconcernlevel") + return ReadConcern(concern) + + +def _parse_ssl_options(options: Mapping[str, Any]) -> tuple[Optional[SSLContext], bool]: + """Parse ssl options.""" + use_tls = options.get("tls") + if use_tls is not None: + validate_boolean("tls", use_tls) + + certfile = options.get("tlscertificatekeyfile") + passphrase = options.get("tlscertificatekeyfilepassword") + ca_certs = options.get("tlscafile") + crlfile = options.get("tlscrlfile") + allow_invalid_certificates = options.get("tlsallowinvalidcertificates", False) + allow_invalid_hostnames = options.get("tlsallowinvalidhostnames", False) + disable_ocsp_endpoint_check = options.get("tlsdisableocspendpointcheck", False) + + enabled_tls_opts = [] + for opt in ( + "tlscertificatekeyfile", + "tlscertificatekeyfilepassword", + "tlscafile", + "tlscrlfile", + ): + # Any non-null value of these options implies tls=True. + if opt in options and options[opt]: + enabled_tls_opts.append(opt) + for opt in ( + "tlsallowinvalidcertificates", + "tlsallowinvalidhostnames", + "tlsdisableocspendpointcheck", + ): + # A value of False for these options implies tls=True. + if opt in options and not options[opt]: + enabled_tls_opts.append(opt) + + if enabled_tls_opts: + if use_tls is None: + # Implicitly enable TLS when one of the tls* options is set. + use_tls = True + elif not use_tls: + # Error since tls is explicitly disabled but a tls option is set. + raise ConfigurationError( + "TLS has not been enabled but the " + "following tls parameters have been set: " + "%s. Please set `tls=True` or remove." % ", ".join(enabled_tls_opts) + ) + + if use_tls: + ctx = get_ssl_context( + certfile, + passphrase, + ca_certs, + crlfile, + allow_invalid_certificates, + allow_invalid_hostnames, + disable_ocsp_endpoint_check, + ) + return ctx, allow_invalid_hostnames + return None, allow_invalid_hostnames + + +def _parse_pool_options( + username: str, password: str, database: Optional[str], options: Mapping[str, Any] +) -> PoolOptions: + """Parse connection pool options.""" + credentials = _parse_credentials(username, password, database, options) + max_pool_size = options.get("maxpoolsize", common.MAX_POOL_SIZE) + min_pool_size = options.get("minpoolsize", common.MIN_POOL_SIZE) + max_idle_time_seconds = options.get("maxidletimems", common.MAX_IDLE_TIME_SEC) + if max_pool_size is not None and min_pool_size > max_pool_size: + raise ValueError("minPoolSize must be smaller or equal to maxPoolSize") + connect_timeout = options.get("connecttimeoutms", common.CONNECT_TIMEOUT) + socket_timeout = options.get("sockettimeoutms") + wait_queue_timeout = options.get("waitqueuetimeoutms", common.WAIT_QUEUE_TIMEOUT) + event_listeners = cast(Optional[Sequence[_EventListener]], options.get("event_listeners")) + appname = options.get("appname") + driver = options.get("driver") + server_api = options.get("server_api") + compression_settings = CompressionSettings( + options.get("compressors", []), options.get("zlibcompressionlevel", -1) + ) + ssl_context, tls_allow_invalid_hostnames = _parse_ssl_options(options) + load_balanced = options.get("loadbalanced") + max_connecting = options.get("maxconnecting", common.MAX_CONNECTING) + return PoolOptions( + max_pool_size, + min_pool_size, + max_idle_time_seconds, + connect_timeout, + socket_timeout, + wait_queue_timeout, + ssl_context, + tls_allow_invalid_hostnames, + _EventListeners(event_listeners), + appname, + driver, + compression_settings, + max_connecting=max_connecting, + server_api=server_api, + load_balanced=load_balanced, + credentials=credentials, + ) + + +class ClientOptions: + """Read only configuration options for a MongoClient. + + Should not be instantiated directly by application developers. Access + a client's options via :attr:`pymongo.mongo_client.MongoClient.options` + instead. + """ + + def __init__( + self, username: str, password: str, database: Optional[str], options: Mapping[str, Any] + ): + self.__options = options + self.__codec_options = _parse_codec_options(options) + self.__direct_connection = options.get("directconnection") + self.__local_threshold_ms = options.get("localthresholdms", common.LOCAL_THRESHOLD_MS) + # self.__server_selection_timeout is in seconds. Must use full name for + # common.SERVER_SELECTION_TIMEOUT because it is set directly by tests. + self.__server_selection_timeout = options.get( + "serverselectiontimeoutms", common.SERVER_SELECTION_TIMEOUT + ) + self.__pool_options = _parse_pool_options(username, password, database, options) + self.__read_preference = _parse_read_preference(options) + self.__replica_set_name = options.get("replicaset") + self.__write_concern = _parse_write_concern(options) + self.__read_concern = _parse_read_concern(options) + self.__connect = options.get("connect") + self.__heartbeat_frequency = options.get("heartbeatfrequencyms", common.HEARTBEAT_FREQUENCY) + self.__retry_writes = options.get("retrywrites", common.RETRY_WRITES) + self.__retry_reads = options.get("retryreads", common.RETRY_READS) + self.__server_selector = options.get("server_selector", any_server_selector) + self.__auto_encryption_opts = options.get("auto_encryption_opts") + self.__load_balanced = options.get("loadbalanced") + self.__timeout = options.get("timeoutms") + self.__server_monitoring_mode = options.get( + "servermonitoringmode", common.SERVER_MONITORING_MODE + ) + + @property + def _options(self) -> Mapping[str, Any]: + """The original options used to create this ClientOptions.""" + return self.__options + + @property + def connect(self) -> Optional[bool]: + """Whether to begin discovering a MongoDB topology automatically.""" + return self.__connect + + @property + def codec_options(self) -> CodecOptions: + """A :class:`~bson.codec_options.CodecOptions` instance.""" + return self.__codec_options + + @property + def direct_connection(self) -> Optional[bool]: + """Whether to connect to the deployment in 'Single' topology.""" + return self.__direct_connection + + @property + def local_threshold_ms(self) -> int: + """The local threshold for this instance.""" + return self.__local_threshold_ms + + @property + def server_selection_timeout(self) -> int: + """The server selection timeout for this instance in seconds.""" + return self.__server_selection_timeout + + @property + def server_selector(self) -> _ServerSelector: + return self.__server_selector + + @property + def heartbeat_frequency(self) -> int: + """The monitoring frequency in seconds.""" + return self.__heartbeat_frequency + + @property + def pool_options(self) -> PoolOptions: + """A :class:`~pymongo.pool.PoolOptions` instance.""" + return self.__pool_options + + @property + def read_preference(self) -> _ServerMode: + """A read preference instance.""" + return self.__read_preference + + @property + def replica_set_name(self) -> Optional[str]: + """Replica set name or None.""" + return self.__replica_set_name + + @property + def write_concern(self) -> WriteConcern: + """A :class:`~pymongo.write_concern.WriteConcern` instance.""" + return self.__write_concern + + @property + def read_concern(self) -> ReadConcern: + """A :class:`~pymongo.read_concern.ReadConcern` instance.""" + return self.__read_concern + + @property + def timeout(self) -> Optional[float]: + """The configured timeoutMS converted to seconds, or None. + + .. versionadded:: 4.2 + """ + return self.__timeout + + @property + def retry_writes(self) -> bool: + """If this instance should retry supported write operations.""" + return self.__retry_writes + + @property + def retry_reads(self) -> bool: + """If this instance should retry supported read operations.""" + return self.__retry_reads + + @property + def auto_encryption_opts(self) -> Optional[AutoEncryptionOpts]: + """A :class:`~pymongo.encryption.AutoEncryptionOpts` or None.""" + return self.__auto_encryption_opts + + @property + def load_balanced(self) -> Optional[bool]: + """True if the client was configured to connect to a load balancer.""" + return self.__load_balanced + + @property + def event_listeners(self) -> list[_EventListeners]: + """The event listeners registered for this client. + + See :mod:`~pymongo.monitoring` for details. + + .. versionadded:: 4.0 + """ + assert self.__pool_options._event_listeners is not None + return self.__pool_options._event_listeners.event_listeners() + + @property + def server_monitoring_mode(self) -> str: + """The configured serverMonitoringMode option. + + .. versionadded:: 4.5 + """ + return self.__server_monitoring_mode diff --git a/venv/Lib/site-packages/pymongo/client_session.py b/venv/Lib/site-packages/pymongo/client_session.py new file mode 100644 index 00000000..3efc624c --- /dev/null +++ b/venv/Lib/site-packages/pymongo/client_session.py @@ -0,0 +1,1155 @@ +# Copyright 2017 MongoDB, 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. + +"""Logical sessions for ordering sequential operations. + +.. versionadded:: 3.6 + +Causally Consistent Reads +========================= + +.. code-block:: python + + with client.start_session(causal_consistency=True) as session: + collection = client.db.collection + collection.update_one({"_id": 1}, {"$set": {"x": 10}}, session=session) + secondary_c = collection.with_options(read_preference=ReadPreference.SECONDARY) + + # A secondary read waits for replication of the write. + secondary_c.find_one({"_id": 1}, session=session) + +If `causal_consistency` is True (the default), read operations that use +the session are causally after previous read and write operations. Using a +causally consistent session, an application can read its own writes and is +guaranteed monotonic reads, even when reading from replica set secondaries. + +.. seealso:: The MongoDB documentation on `causal-consistency `_. + +.. _transactions-ref: + +Transactions +============ + +.. versionadded:: 3.7 + +MongoDB 4.0 adds support for transactions on replica set primaries. A +transaction is associated with a :class:`ClientSession`. To start a transaction +on a session, use :meth:`ClientSession.start_transaction` in a with-statement. +Then, execute an operation within the transaction by passing the session to the +operation: + +.. code-block:: python + + orders = client.db.orders + inventory = client.db.inventory + with client.start_session() as session: + with session.start_transaction(): + orders.insert_one({"sku": "abc123", "qty": 100}, session=session) + inventory.update_one( + {"sku": "abc123", "qty": {"$gte": 100}}, + {"$inc": {"qty": -100}}, + session=session, + ) + +Upon normal completion of ``with session.start_transaction()`` block, the +transaction automatically calls :meth:`ClientSession.commit_transaction`. +If the block exits with an exception, the transaction automatically calls +:meth:`ClientSession.abort_transaction`. + +In general, multi-document transactions only support read/write (CRUD) +operations on existing collections. However, MongoDB 4.4 adds support for +creating collections and indexes with some limitations, including an +insert operation that would result in the creation of a new collection. +For a complete description of all the supported and unsupported operations +see the `MongoDB server's documentation for transactions +`_. + +A session may only have a single active transaction at a time, multiple +transactions on the same session can be executed in sequence. + +Sharded Transactions +^^^^^^^^^^^^^^^^^^^^ + +.. versionadded:: 3.9 + +PyMongo 3.9 adds support for transactions on sharded clusters running MongoDB +>=4.2. Sharded transactions have the same API as replica set transactions. +When running a transaction against a sharded cluster, the session is +pinned to the mongos server selected for the first operation in the +transaction. All subsequent operations that are part of the same transaction +are routed to the same mongos server. When the transaction is completed, by +running either commitTransaction or abortTransaction, the session is unpinned. + +.. seealso:: The MongoDB documentation on `transactions `_. + +.. _snapshot-reads-ref: + +Snapshot Reads +============== + +.. versionadded:: 3.12 + +MongoDB 5.0 adds support for snapshot reads. Snapshot reads are requested by +passing the ``snapshot`` option to +:meth:`~pymongo.mongo_client.MongoClient.start_session`. +If ``snapshot`` is True, all read operations that use this session read data +from the same snapshot timestamp. The server chooses the latest +majority-committed snapshot timestamp when executing the first read operation +using the session. Subsequent reads on this session read from the same +snapshot timestamp. Snapshot reads are also supported when reading from +replica set secondaries. + +.. code-block:: python + + # Each read using this session reads data from the same point in time. + with client.start_session(snapshot=True) as session: + order = orders.find_one({"sku": "abc123"}, session=session) + inventory = inventory.find_one({"sku": "abc123"}, session=session) + +Snapshot Reads Limitations +^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Snapshot reads sessions are incompatible with ``causal_consistency=True``. +Only the following read operations are supported in a snapshot reads session: + +- :meth:`~pymongo.collection.Collection.find` +- :meth:`~pymongo.collection.Collection.find_one` +- :meth:`~pymongo.collection.Collection.aggregate` +- :meth:`~pymongo.collection.Collection.count_documents` +- :meth:`~pymongo.collection.Collection.distinct` (on unsharded collections) + +Classes +======= +""" + +from __future__ import annotations + +import collections +import time +import uuid +from collections.abc import Mapping as _Mapping +from typing import ( + TYPE_CHECKING, + Any, + Callable, + ContextManager, + Mapping, + MutableMapping, + NoReturn, + Optional, + Type, + TypeVar, +) + +from bson.binary import Binary +from bson.int64 import Int64 +from bson.timestamp import Timestamp +from pymongo import _csot +from pymongo.cursor import _ConnectionManager +from pymongo.errors import ( + ConfigurationError, + ConnectionFailure, + InvalidOperation, + OperationFailure, + PyMongoError, + WTimeoutError, +) +from pymongo.helpers import _RETRYABLE_ERROR_CODES +from pymongo.operations import _Op +from pymongo.read_concern import ReadConcern +from pymongo.read_preferences import ReadPreference, _ServerMode +from pymongo.server_type import SERVER_TYPE +from pymongo.write_concern import WriteConcern + +if TYPE_CHECKING: + from types import TracebackType + + from pymongo.pool import Connection + from pymongo.server import Server + from pymongo.typings import ClusterTime, _Address + + +class SessionOptions: + """Options for a new :class:`ClientSession`. + + :param causal_consistency: If True, read operations are causally + ordered within the session. Defaults to True when the ``snapshot`` + option is ``False``. + :param default_transaction_options: The default + TransactionOptions to use for transactions started on this session. + :param snapshot: If True, then all reads performed using this + session will read from the same snapshot. This option is incompatible + with ``causal_consistency=True``. Defaults to ``False``. + + .. versionchanged:: 3.12 + Added the ``snapshot`` parameter. + """ + + def __init__( + self, + causal_consistency: Optional[bool] = None, + default_transaction_options: Optional[TransactionOptions] = None, + snapshot: Optional[bool] = False, + ) -> None: + if snapshot: + if causal_consistency: + raise ConfigurationError("snapshot reads do not support causal_consistency=True") + causal_consistency = False + elif causal_consistency is None: + causal_consistency = True + self._causal_consistency = causal_consistency + if default_transaction_options is not None: + if not isinstance(default_transaction_options, TransactionOptions): + raise TypeError( + "default_transaction_options must be an instance of " + "pymongo.client_session.TransactionOptions, not: {!r}".format( + default_transaction_options + ) + ) + self._default_transaction_options = default_transaction_options + self._snapshot = snapshot + + @property + def causal_consistency(self) -> bool: + """Whether causal consistency is configured.""" + return self._causal_consistency + + @property + def default_transaction_options(self) -> Optional[TransactionOptions]: + """The default TransactionOptions to use for transactions started on + this session. + + .. versionadded:: 3.7 + """ + return self._default_transaction_options + + @property + def snapshot(self) -> Optional[bool]: + """Whether snapshot reads are configured. + + .. versionadded:: 3.12 + """ + return self._snapshot + + +class TransactionOptions: + """Options for :meth:`ClientSession.start_transaction`. + + :param read_concern: The + :class:`~pymongo.read_concern.ReadConcern` to use for this transaction. + If ``None`` (the default) the :attr:`read_preference` of + the :class:`MongoClient` is used. + :param write_concern: The + :class:`~pymongo.write_concern.WriteConcern` to use for this + transaction. If ``None`` (the default) the :attr:`read_preference` of + the :class:`MongoClient` is used. + :param read_preference: The read preference to use. If + ``None`` (the default) the :attr:`read_preference` of this + :class:`MongoClient` is used. See :mod:`~pymongo.read_preferences` + for options. Transactions which read must use + :attr:`~pymongo.read_preferences.ReadPreference.PRIMARY`. + :param max_commit_time_ms: The maximum amount of time to allow a + single commitTransaction command to run. This option is an alias for + maxTimeMS option on the commitTransaction command. If ``None`` (the + default) maxTimeMS is not used. + + .. versionchanged:: 3.9 + Added the ``max_commit_time_ms`` option. + + .. versionadded:: 3.7 + """ + + def __init__( + self, + read_concern: Optional[ReadConcern] = None, + write_concern: Optional[WriteConcern] = None, + read_preference: Optional[_ServerMode] = None, + max_commit_time_ms: Optional[int] = None, + ) -> None: + self._read_concern = read_concern + self._write_concern = write_concern + self._read_preference = read_preference + self._max_commit_time_ms = max_commit_time_ms + if read_concern is not None: + if not isinstance(read_concern, ReadConcern): + raise TypeError( + "read_concern must be an instance of " + f"pymongo.read_concern.ReadConcern, not: {read_concern!r}" + ) + if write_concern is not None: + if not isinstance(write_concern, WriteConcern): + raise TypeError( + "write_concern must be an instance of " + f"pymongo.write_concern.WriteConcern, not: {write_concern!r}" + ) + if not write_concern.acknowledged: + raise ConfigurationError( + "transactions do not support unacknowledged write concern" + f": {write_concern!r}" + ) + if read_preference is not None: + if not isinstance(read_preference, _ServerMode): + raise TypeError( + f"{read_preference!r} is not valid for read_preference. See " + "pymongo.read_preferences for valid " + "options." + ) + if max_commit_time_ms is not None: + if not isinstance(max_commit_time_ms, int): + raise TypeError("max_commit_time_ms must be an integer or None") + + @property + def read_concern(self) -> Optional[ReadConcern]: + """This transaction's :class:`~pymongo.read_concern.ReadConcern`.""" + return self._read_concern + + @property + def write_concern(self) -> Optional[WriteConcern]: + """This transaction's :class:`~pymongo.write_concern.WriteConcern`.""" + return self._write_concern + + @property + def read_preference(self) -> Optional[_ServerMode]: + """This transaction's :class:`~pymongo.read_preferences.ReadPreference`.""" + return self._read_preference + + @property + def max_commit_time_ms(self) -> Optional[int]: + """The maxTimeMS to use when running a commitTransaction command. + + .. versionadded:: 3.9 + """ + return self._max_commit_time_ms + + +def _validate_session_write_concern( + session: Optional[ClientSession], write_concern: Optional[WriteConcern] +) -> Optional[ClientSession]: + """Validate that an explicit session is not used with an unack'ed write. + + Returns the session to use for the next operation. + """ + if session: + if write_concern is not None and not write_concern.acknowledged: + # For unacknowledged writes without an explicit session, + # drivers SHOULD NOT use an implicit session. If a driver + # creates an implicit session for unacknowledged writes + # without an explicit session, the driver MUST NOT send the + # session ID. + if session._implicit: + return None + else: + raise ConfigurationError( + "Explicit sessions are incompatible with " + f"unacknowledged write concern: {write_concern!r}" + ) + return session + + +class _TransactionContext: + """Internal transaction context manager for start_transaction.""" + + def __init__(self, session: ClientSession): + self.__session = session + + def __enter__(self) -> _TransactionContext: + return self + + def __exit__( + self, + exc_type: Optional[Type[BaseException]], + exc_val: Optional[BaseException], + exc_tb: Optional[TracebackType], + ) -> None: + if self.__session.in_transaction: + if exc_val is None: + self.__session.commit_transaction() + else: + self.__session.abort_transaction() + + +class _TxnState: + NONE = 1 + STARTING = 2 + IN_PROGRESS = 3 + COMMITTED = 4 + COMMITTED_EMPTY = 5 + ABORTED = 6 + + +class _Transaction: + """Internal class to hold transaction information in a ClientSession.""" + + def __init__(self, opts: Optional[TransactionOptions], client: MongoClient): + self.opts = opts + self.state = _TxnState.NONE + self.sharded = False + self.pinned_address: Optional[_Address] = None + self.conn_mgr: Optional[_ConnectionManager] = None + self.recovery_token = None + self.attempt = 0 + self.client = client + + def active(self) -> bool: + return self.state in (_TxnState.STARTING, _TxnState.IN_PROGRESS) + + def starting(self) -> bool: + return self.state == _TxnState.STARTING + + @property + def pinned_conn(self) -> Optional[Connection]: + if self.active() and self.conn_mgr: + return self.conn_mgr.conn + return None + + def pin(self, server: Server, conn: Connection) -> None: + self.sharded = True + self.pinned_address = server.description.address + if server.description.server_type == SERVER_TYPE.LoadBalancer: + conn.pin_txn() + self.conn_mgr = _ConnectionManager(conn, False) + + def unpin(self) -> None: + self.pinned_address = None + if self.conn_mgr: + self.conn_mgr.close() + self.conn_mgr = None + + def reset(self) -> None: + self.unpin() + self.state = _TxnState.NONE + self.sharded = False + self.recovery_token = None + self.attempt = 0 + + def __del__(self) -> None: + if self.conn_mgr: + # Reuse the cursor closing machinery to return the socket to the + # pool soon. + self.client._close_cursor_soon(0, None, self.conn_mgr) + self.conn_mgr = None + + +def _reraise_with_unknown_commit(exc: Any) -> NoReturn: + """Re-raise an exception with the UnknownTransactionCommitResult label.""" + exc._add_error_label("UnknownTransactionCommitResult") + raise + + +def _max_time_expired_error(exc: PyMongoError) -> bool: + """Return true if exc is a MaxTimeMSExpired error.""" + return isinstance(exc, OperationFailure) and exc.code == 50 + + +# From the transactions spec, all the retryable writes errors plus +# WriteConcernFailed. +_UNKNOWN_COMMIT_ERROR_CODES: frozenset = _RETRYABLE_ERROR_CODES | frozenset( + [ + 64, # WriteConcernFailed + 50, # MaxTimeMSExpired + ] +) + +# From the Convenient API for Transactions spec, with_transaction must +# halt retries after 120 seconds. +# This limit is non-configurable and was chosen to be twice the 60 second +# default value of MongoDB's `transactionLifetimeLimitSeconds` parameter. +_WITH_TRANSACTION_RETRY_TIME_LIMIT = 120 + + +def _within_time_limit(start_time: float) -> bool: + """Are we within the with_transaction retry limit?""" + return time.monotonic() - start_time < _WITH_TRANSACTION_RETRY_TIME_LIMIT + + +_T = TypeVar("_T") + +if TYPE_CHECKING: + from pymongo.mongo_client import MongoClient + + +class ClientSession: + """A session for ordering sequential operations. + + :class:`ClientSession` instances are **not thread-safe or fork-safe**. + They can only be used by one thread or process at a time. A single + :class:`ClientSession` cannot be used to run multiple operations + concurrently. + + Should not be initialized directly by application developers - to create a + :class:`ClientSession`, call + :meth:`~pymongo.mongo_client.MongoClient.start_session`. + """ + + def __init__( + self, + client: MongoClient, + server_session: Any, + options: SessionOptions, + implicit: bool, + ) -> None: + # A MongoClient, a _ServerSession, a SessionOptions, and a set. + self._client: MongoClient = client + self._server_session = server_session + self._options = options + self._cluster_time: Optional[Mapping[str, Any]] = None + self._operation_time: Optional[Timestamp] = None + self._snapshot_time = None + # Is this an implicitly created session? + self._implicit = implicit + self._transaction = _Transaction(None, client) + + def end_session(self) -> None: + """Finish this session. If a transaction has started, abort it. + + It is an error to use the session after the session has ended. + """ + self._end_session(lock=True) + + def _end_session(self, lock: bool) -> None: + if self._server_session is not None: + try: + if self.in_transaction: + self.abort_transaction() + # It's possible we're still pinned here when the transaction + # is in the committed state when the session is discarded. + self._unpin() + finally: + self._client._return_server_session(self._server_session, lock) + self._server_session = None + + def _check_ended(self) -> None: + if self._server_session is None: + raise InvalidOperation("Cannot use ended session") + + def __enter__(self) -> ClientSession: + return self + + def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None: + self._end_session(lock=True) + + @property + def client(self) -> MongoClient: + """The :class:`~pymongo.mongo_client.MongoClient` this session was + created from. + """ + return self._client + + @property + def options(self) -> SessionOptions: + """The :class:`SessionOptions` this session was created with.""" + return self._options + + @property + def session_id(self) -> Mapping[str, Any]: + """A BSON document, the opaque server session identifier.""" + self._check_ended() + self._materialize(self._client.topology_description.logical_session_timeout_minutes) + return self._server_session.session_id + + @property + def _transaction_id(self) -> Int64: + """The current transaction id for the underlying server session.""" + self._materialize(self._client.topology_description.logical_session_timeout_minutes) + return self._server_session.transaction_id + + @property + def cluster_time(self) -> Optional[ClusterTime]: + """The cluster time returned by the last operation executed + in this session. + """ + return self._cluster_time + + @property + def operation_time(self) -> Optional[Timestamp]: + """The operation time returned by the last operation executed + in this session. + """ + return self._operation_time + + def _inherit_option(self, name: str, val: _T) -> _T: + """Return the inherited TransactionOption value.""" + if val: + return val + txn_opts = self.options.default_transaction_options + parent_val = txn_opts and getattr(txn_opts, name) + if parent_val: + return parent_val + return getattr(self.client, name) + + def with_transaction( + self, + callback: Callable[[ClientSession], _T], + read_concern: Optional[ReadConcern] = None, + write_concern: Optional[WriteConcern] = None, + read_preference: Optional[_ServerMode] = None, + max_commit_time_ms: Optional[int] = None, + ) -> _T: + """Execute a callback in a transaction. + + This method starts a transaction on this session, executes ``callback`` + once, and then commits the transaction. For example:: + + def callback(session): + orders = session.client.db.orders + inventory = session.client.db.inventory + orders.insert_one({"sku": "abc123", "qty": 100}, session=session) + inventory.update_one({"sku": "abc123", "qty": {"$gte": 100}}, + {"$inc": {"qty": -100}}, session=session) + + with client.start_session() as session: + session.with_transaction(callback) + + To pass arbitrary arguments to the ``callback``, wrap your callable + with a ``lambda`` like this:: + + def callback(session, custom_arg, custom_kwarg=None): + # Transaction operations... + + with client.start_session() as session: + session.with_transaction( + lambda s: callback(s, "custom_arg", custom_kwarg=1)) + + In the event of an exception, ``with_transaction`` may retry the commit + or the entire transaction, therefore ``callback`` may be invoked + multiple times by a single call to ``with_transaction``. Developers + should be mindful of this possibility when writing a ``callback`` that + modifies application state or has any other side-effects. + Note that even when the ``callback`` is invoked multiple times, + ``with_transaction`` ensures that the transaction will be committed + at-most-once on the server. + + The ``callback`` should not attempt to start new transactions, but + should simply run operations meant to be contained within a + transaction. The ``callback`` should also not commit the transaction; + this is handled automatically by ``with_transaction``. If the + ``callback`` does commit or abort the transaction without error, + however, ``with_transaction`` will return without taking further + action. + + :class:`ClientSession` instances are **not thread-safe or fork-safe**. + Consequently, the ``callback`` must not attempt to execute multiple + operations concurrently. + + When ``callback`` raises an exception, ``with_transaction`` + automatically aborts the current transaction. When ``callback`` or + :meth:`~ClientSession.commit_transaction` raises an exception that + includes the ``"TransientTransactionError"`` error label, + ``with_transaction`` starts a new transaction and re-executes + the ``callback``. + + When :meth:`~ClientSession.commit_transaction` raises an exception with + the ``"UnknownTransactionCommitResult"`` error label, + ``with_transaction`` retries the commit until the result of the + transaction is known. + + This method will cease retrying after 120 seconds has elapsed. This + timeout is not configurable and any exception raised by the + ``callback`` or by :meth:`ClientSession.commit_transaction` after the + timeout is reached will be re-raised. Applications that desire a + different timeout duration should not use this method. + + :param callback: The callable ``callback`` to run inside a transaction. + The callable must accept a single argument, this session. Note, + under certain error conditions the callback may be run multiple + times. + :param read_concern: The + :class:`~pymongo.read_concern.ReadConcern` to use for this + transaction. + :param write_concern: The + :class:`~pymongo.write_concern.WriteConcern` to use for this + transaction. + :param read_preference: The read preference to use for this + transaction. If ``None`` (the default) the :attr:`read_preference` + of this :class:`Database` is used. See + :mod:`~pymongo.read_preferences` for options. + + :return: The return value of the ``callback``. + + .. versionadded:: 3.9 + """ + start_time = time.monotonic() + while True: + self.start_transaction(read_concern, write_concern, read_preference, max_commit_time_ms) + try: + ret = callback(self) + except Exception as exc: + if self.in_transaction: + self.abort_transaction() + if ( + isinstance(exc, PyMongoError) + and exc.has_error_label("TransientTransactionError") + and _within_time_limit(start_time) + ): + # Retry the entire transaction. + continue + raise + + if not self.in_transaction: + # Assume callback intentionally ended the transaction. + return ret + + while True: + try: + self.commit_transaction() + except PyMongoError as exc: + if ( + exc.has_error_label("UnknownTransactionCommitResult") + and _within_time_limit(start_time) + and not _max_time_expired_error(exc) + ): + # Retry the commit. + continue + + if exc.has_error_label("TransientTransactionError") and _within_time_limit( + start_time + ): + # Retry the entire transaction. + break + raise + + # Commit succeeded. + return ret + + def start_transaction( + self, + read_concern: Optional[ReadConcern] = None, + write_concern: Optional[WriteConcern] = None, + read_preference: Optional[_ServerMode] = None, + max_commit_time_ms: Optional[int] = None, + ) -> ContextManager: + """Start a multi-statement transaction. + + Takes the same arguments as :class:`TransactionOptions`. + + .. versionchanged:: 3.9 + Added the ``max_commit_time_ms`` option. + + .. versionadded:: 3.7 + """ + self._check_ended() + + if self.options.snapshot: + raise InvalidOperation("Transactions are not supported in snapshot sessions") + + if self.in_transaction: + raise InvalidOperation("Transaction already in progress") + + read_concern = self._inherit_option("read_concern", read_concern) + write_concern = self._inherit_option("write_concern", write_concern) + read_preference = self._inherit_option("read_preference", read_preference) + if max_commit_time_ms is None: + opts = self.options.default_transaction_options + if opts: + max_commit_time_ms = opts.max_commit_time_ms + + self._transaction.opts = TransactionOptions( + read_concern, write_concern, read_preference, max_commit_time_ms + ) + self._transaction.reset() + self._transaction.state = _TxnState.STARTING + self._start_retryable_write() + return _TransactionContext(self) + + def commit_transaction(self) -> None: + """Commit a multi-statement transaction. + + .. versionadded:: 3.7 + """ + self._check_ended() + state = self._transaction.state + if state is _TxnState.NONE: + raise InvalidOperation("No transaction started") + elif state in (_TxnState.STARTING, _TxnState.COMMITTED_EMPTY): + # Server transaction was never started, no need to send a command. + self._transaction.state = _TxnState.COMMITTED_EMPTY + return + elif state is _TxnState.ABORTED: + raise InvalidOperation("Cannot call commitTransaction after calling abortTransaction") + elif state is _TxnState.COMMITTED: + # We're explicitly retrying the commit, move the state back to + # "in progress" so that in_transaction returns true. + self._transaction.state = _TxnState.IN_PROGRESS + + try: + self._finish_transaction_with_retry("commitTransaction") + except ConnectionFailure as exc: + # We do not know if the commit was successfully applied on the + # server or if it satisfied the provided write concern, set the + # unknown commit error label. + exc._remove_error_label("TransientTransactionError") + _reraise_with_unknown_commit(exc) + except WTimeoutError as exc: + # We do not know if the commit has satisfied the provided write + # concern, add the unknown commit error label. + _reraise_with_unknown_commit(exc) + except OperationFailure as exc: + if exc.code not in _UNKNOWN_COMMIT_ERROR_CODES: + # The server reports errorLabels in the case. + raise + # We do not know if the commit was successfully applied on the + # server or if it satisfied the provided write concern, set the + # unknown commit error label. + _reraise_with_unknown_commit(exc) + finally: + self._transaction.state = _TxnState.COMMITTED + + def abort_transaction(self) -> None: + """Abort a multi-statement transaction. + + .. versionadded:: 3.7 + """ + self._check_ended() + + state = self._transaction.state + if state is _TxnState.NONE: + raise InvalidOperation("No transaction started") + elif state is _TxnState.STARTING: + # Server transaction was never started, no need to send a command. + self._transaction.state = _TxnState.ABORTED + return + elif state is _TxnState.ABORTED: + raise InvalidOperation("Cannot call abortTransaction twice") + elif state in (_TxnState.COMMITTED, _TxnState.COMMITTED_EMPTY): + raise InvalidOperation("Cannot call abortTransaction after calling commitTransaction") + + try: + self._finish_transaction_with_retry("abortTransaction") + except (OperationFailure, ConnectionFailure): + # The transactions spec says to ignore abortTransaction errors. + pass + finally: + self._transaction.state = _TxnState.ABORTED + self._unpin() + + def _finish_transaction_with_retry(self, command_name: str) -> dict[str, Any]: + """Run commit or abort with one retry after any retryable error. + + :param command_name: Either "commitTransaction" or "abortTransaction". + """ + + def func( + _session: Optional[ClientSession], conn: Connection, _retryable: bool + ) -> dict[str, Any]: + return self._finish_transaction(conn, command_name) + + return self._client._retry_internal(func, self, None, retryable=True, operation=_Op.ABORT) + + def _finish_transaction(self, conn: Connection, command_name: str) -> dict[str, Any]: + self._transaction.attempt += 1 + opts = self._transaction.opts + assert opts + wc = opts.write_concern + cmd = {command_name: 1} + if command_name == "commitTransaction": + if opts.max_commit_time_ms and _csot.get_timeout() is None: + cmd["maxTimeMS"] = opts.max_commit_time_ms + + # Transaction spec says that after the initial commit attempt, + # subsequent commitTransaction commands should be upgraded to use + # w:"majority" and set a default value of 10 seconds for wtimeout. + if self._transaction.attempt > 1: + assert wc + wc_doc = wc.document + wc_doc["w"] = "majority" + wc_doc.setdefault("wtimeout", 10000) + wc = WriteConcern(**wc_doc) + + if self._transaction.recovery_token: + cmd["recoveryToken"] = self._transaction.recovery_token + + return self._client.admin._command( + conn, cmd, session=self, write_concern=wc, parse_write_concern_error=True + ) + + def _advance_cluster_time(self, cluster_time: Optional[Mapping[str, Any]]) -> None: + """Internal cluster time helper.""" + if self._cluster_time is None: + self._cluster_time = cluster_time + elif cluster_time is not None: + if cluster_time["clusterTime"] > self._cluster_time["clusterTime"]: + self._cluster_time = cluster_time + + def advance_cluster_time(self, cluster_time: Mapping[str, Any]) -> None: + """Update the cluster time for this session. + + :param cluster_time: The + :data:`~pymongo.client_session.ClientSession.cluster_time` from + another `ClientSession` instance. + """ + if not isinstance(cluster_time, _Mapping): + raise TypeError("cluster_time must be a subclass of collections.Mapping") + if not isinstance(cluster_time.get("clusterTime"), Timestamp): + raise ValueError("Invalid cluster_time") + self._advance_cluster_time(cluster_time) + + def _advance_operation_time(self, operation_time: Optional[Timestamp]) -> None: + """Internal operation time helper.""" + if self._operation_time is None: + self._operation_time = operation_time + elif operation_time is not None: + if operation_time > self._operation_time: + self._operation_time = operation_time + + def advance_operation_time(self, operation_time: Timestamp) -> None: + """Update the operation time for this session. + + :param operation_time: The + :data:`~pymongo.client_session.ClientSession.operation_time` from + another `ClientSession` instance. + """ + if not isinstance(operation_time, Timestamp): + raise TypeError("operation_time must be an instance of bson.timestamp.Timestamp") + self._advance_operation_time(operation_time) + + def _process_response(self, reply: Mapping[str, Any]) -> None: + """Process a response to a command that was run with this session.""" + self._advance_cluster_time(reply.get("$clusterTime")) + self._advance_operation_time(reply.get("operationTime")) + if self._options.snapshot and self._snapshot_time is None: + if "cursor" in reply: + ct = reply["cursor"].get("atClusterTime") + else: + ct = reply.get("atClusterTime") + self._snapshot_time = ct + if self.in_transaction and self._transaction.sharded: + recovery_token = reply.get("recoveryToken") + if recovery_token: + self._transaction.recovery_token = recovery_token + + @property + def has_ended(self) -> bool: + """True if this session is finished.""" + return self._server_session is None + + @property + def in_transaction(self) -> bool: + """True if this session has an active multi-statement transaction. + + .. versionadded:: 3.10 + """ + return self._transaction.active() + + @property + def _starting_transaction(self) -> bool: + """True if this session is starting a multi-statement transaction.""" + return self._transaction.starting() + + @property + def _pinned_address(self) -> Optional[_Address]: + """The mongos address this transaction was created on.""" + if self._transaction.active(): + return self._transaction.pinned_address + return None + + @property + def _pinned_connection(self) -> Optional[Connection]: + """The connection this transaction was started on.""" + return self._transaction.pinned_conn + + def _pin(self, server: Server, conn: Connection) -> None: + """Pin this session to the given Server or to the given connection.""" + self._transaction.pin(server, conn) + + def _unpin(self) -> None: + """Unpin this session from any pinned Server.""" + self._transaction.unpin() + + def _txn_read_preference(self) -> Optional[_ServerMode]: + """Return read preference of this transaction or None.""" + if self.in_transaction: + assert self._transaction.opts + return self._transaction.opts.read_preference + return None + + def _materialize(self, logical_session_timeout_minutes: Optional[int] = None) -> None: + if isinstance(self._server_session, _EmptyServerSession): + old = self._server_session + self._server_session = self._client._topology.get_server_session( + logical_session_timeout_minutes + ) + if old.started_retryable_write: + self._server_session.inc_transaction_id() + + def _apply_to( + self, + command: MutableMapping[str, Any], + is_retryable: bool, + read_preference: _ServerMode, + conn: Connection, + ) -> None: + if not conn.supports_sessions: + if not self._implicit: + raise ConfigurationError("Sessions are not supported by this MongoDB deployment") + return + self._check_ended() + self._materialize(conn.logical_session_timeout_minutes) + if self.options.snapshot: + self._update_read_concern(command, conn) + + self._server_session.last_use = time.monotonic() + command["lsid"] = self._server_session.session_id + + if is_retryable: + command["txnNumber"] = self._server_session.transaction_id + return + + if self.in_transaction: + if read_preference != ReadPreference.PRIMARY: + raise InvalidOperation( + f"read preference in a transaction must be primary, not: {read_preference!r}" + ) + + if self._transaction.state == _TxnState.STARTING: + # First command begins a new transaction. + self._transaction.state = _TxnState.IN_PROGRESS + command["startTransaction"] = True + + assert self._transaction.opts + if self._transaction.opts.read_concern: + rc = self._transaction.opts.read_concern.document + if rc: + command["readConcern"] = rc + self._update_read_concern(command, conn) + + command["txnNumber"] = self._server_session.transaction_id + command["autocommit"] = False + + def _start_retryable_write(self) -> None: + self._check_ended() + self._server_session.inc_transaction_id() + + def _update_read_concern(self, cmd: MutableMapping[str, Any], conn: Connection) -> None: + if self.options.causal_consistency and self.operation_time is not None: + cmd.setdefault("readConcern", {})["afterClusterTime"] = self.operation_time + if self.options.snapshot: + if conn.max_wire_version < 13: + raise ConfigurationError("Snapshot reads require MongoDB 5.0 or later") + rc = cmd.setdefault("readConcern", {}) + rc["level"] = "snapshot" + if self._snapshot_time is not None: + rc["atClusterTime"] = self._snapshot_time + + def __copy__(self) -> NoReturn: + raise TypeError("A ClientSession cannot be copied, create a new session instead") + + +class _EmptyServerSession: + __slots__ = "dirty", "started_retryable_write" + + def __init__(self) -> None: + self.dirty = False + self.started_retryable_write = False + + def mark_dirty(self) -> None: + self.dirty = True + + def inc_transaction_id(self) -> None: + self.started_retryable_write = True + + +class _ServerSession: + def __init__(self, generation: int): + # Ensure id is type 4, regardless of CodecOptions.uuid_representation. + self.session_id = {"id": Binary(uuid.uuid4().bytes, 4)} + self.last_use = time.monotonic() + self._transaction_id = 0 + self.dirty = False + self.generation = generation + + def mark_dirty(self) -> None: + """Mark this session as dirty. + + A server session is marked dirty when a command fails with a network + error. Dirty sessions are later discarded from the server session pool. + """ + self.dirty = True + + def timed_out(self, session_timeout_minutes: Optional[int]) -> bool: + if session_timeout_minutes is None: + return False + + idle_seconds = time.monotonic() - self.last_use + + # Timed out if we have less than a minute to live. + return idle_seconds > (session_timeout_minutes - 1) * 60 + + @property + def transaction_id(self) -> Int64: + """Positive 64-bit integer.""" + return Int64(self._transaction_id) + + def inc_transaction_id(self) -> None: + self._transaction_id += 1 + + +class _ServerSessionPool(collections.deque): + """Pool of _ServerSession objects. + + This class is not thread-safe, access it while holding the Topology lock. + """ + + def __init__(self, *args: Any, **kwargs: Any): + super().__init__(*args, **kwargs) + self.generation = 0 + + def reset(self) -> None: + self.generation += 1 + self.clear() + + def pop_all(self) -> list[_ServerSession]: + ids = [] + while self: + ids.append(self.pop().session_id) + return ids + + def get_server_session(self, session_timeout_minutes: Optional[int]) -> _ServerSession: + # Although the Driver Sessions Spec says we only clear stale sessions + # in return_server_session, PyMongo can't take a lock when returning + # sessions from a __del__ method (like in Cursor.__die), so it can't + # clear stale sessions there. In case many sessions were returned via + # __del__, check for stale sessions here too. + self._clear_stale(session_timeout_minutes) + + # The most recently used sessions are on the left. + while self: + s = self.popleft() + if not s.timed_out(session_timeout_minutes): + return s + + return _ServerSession(self.generation) + + def return_server_session( + self, server_session: _ServerSession, session_timeout_minutes: Optional[int] + ) -> None: + if session_timeout_minutes is not None: + self._clear_stale(session_timeout_minutes) + if server_session.timed_out(session_timeout_minutes): + return + self.return_server_session_no_lock(server_session) + + def return_server_session_no_lock(self, server_session: _ServerSession) -> None: + # Discard sessions from an old pool to avoid duplicate sessions in the + # child process after a fork. + if server_session.generation == self.generation and not server_session.dirty: + self.appendleft(server_session) + + def _clear_stale(self, session_timeout_minutes: Optional[int]) -> None: + # Clear stale sessions. The least recently used are on the right. + while self: + if self[-1].timed_out(session_timeout_minutes): + self.pop() + else: + # The remaining sessions also haven't timed out. + break diff --git a/venv/Lib/site-packages/pymongo/collation.py b/venv/Lib/site-packages/pymongo/collation.py new file mode 100644 index 00000000..971628f4 --- /dev/null +++ b/venv/Lib/site-packages/pymongo/collation.py @@ -0,0 +1,224 @@ +# Copyright 2016 MongoDB, 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. + +"""Tools for working with `collations`_. + +.. _collations: https://www.mongodb.com/docs/manual/reference/collation/ +""" +from __future__ import annotations + +from typing import Any, Mapping, Optional, Union + +from pymongo import common +from pymongo.write_concern import validate_boolean + + +class CollationStrength: + """ + An enum that defines values for `strength` on a + :class:`~pymongo.collation.Collation`. + """ + + PRIMARY = 1 + """Differentiate base (unadorned) characters.""" + + SECONDARY = 2 + """Differentiate character accents.""" + + TERTIARY = 3 + """Differentiate character case.""" + + QUATERNARY = 4 + """Differentiate words with and without punctuation.""" + + IDENTICAL = 5 + """Differentiate unicode code point (characters are exactly identical).""" + + +class CollationAlternate: + """ + An enum that defines values for `alternate` on a + :class:`~pymongo.collation.Collation`. + """ + + NON_IGNORABLE = "non-ignorable" + """Spaces and punctuation are treated as base characters.""" + + SHIFTED = "shifted" + """Spaces and punctuation are *not* considered base characters. + + Spaces and punctuation are distinguished regardless when the + :class:`~pymongo.collation.Collation` strength is at least + :data:`~pymongo.collation.CollationStrength.QUATERNARY`. + + """ + + +class CollationMaxVariable: + """ + An enum that defines values for `max_variable` on a + :class:`~pymongo.collation.Collation`. + """ + + PUNCT = "punct" + """Both punctuation and spaces are ignored.""" + + SPACE = "space" + """Spaces alone are ignored.""" + + +class CollationCaseFirst: + """ + An enum that defines values for `case_first` on a + :class:`~pymongo.collation.Collation`. + """ + + UPPER = "upper" + """Sort uppercase characters first.""" + + LOWER = "lower" + """Sort lowercase characters first.""" + + OFF = "off" + """Default for locale or collation strength.""" + + +class Collation: + """Collation + + :param locale: (string) The locale of the collation. This should be a string + that identifies an `ICU locale ID` exactly. For example, ``en_US`` is + valid, but ``en_us`` and ``en-US`` are not. Consult the MongoDB + documentation for a list of supported locales. + :param caseLevel: (optional) If ``True``, turn on case sensitivity if + `strength` is 1 or 2 (case sensitivity is implied if `strength` is + greater than 2). Defaults to ``False``. + :param caseFirst: (optional) Specify that either uppercase or lowercase + characters take precedence. Must be one of the following values: + + * :data:`~CollationCaseFirst.UPPER` + * :data:`~CollationCaseFirst.LOWER` + * :data:`~CollationCaseFirst.OFF` (the default) + + :param strength: Specify the comparison strength. This is also + known as the ICU comparison level. This must be one of the following + values: + + * :data:`~CollationStrength.PRIMARY` + * :data:`~CollationStrength.SECONDARY` + * :data:`~CollationStrength.TERTIARY` (the default) + * :data:`~CollationStrength.QUATERNARY` + * :data:`~CollationStrength.IDENTICAL` + + Each successive level builds upon the previous. For example, a + `strength` of :data:`~CollationStrength.SECONDARY` differentiates + characters based both on the unadorned base character and its accents. + + :param numericOrdering: If ``True``, order numbers numerically + instead of in collation order (defaults to ``False``). + :param alternate: Specify whether spaces and punctuation are + considered base characters. This must be one of the following values: + + * :data:`~CollationAlternate.NON_IGNORABLE` (the default) + * :data:`~CollationAlternate.SHIFTED` + + :param maxVariable: When `alternate` is + :data:`~CollationAlternate.SHIFTED`, this option specifies what + characters may be ignored. This must be one of the following values: + + * :data:`~CollationMaxVariable.PUNCT` (the default) + * :data:`~CollationMaxVariable.SPACE` + + :param normalization: If ``True``, normalizes text into Unicode + NFD. Defaults to ``False``. + :param backwards: If ``True``, accents on characters are + considered from the back of the word to the front, as it is done in some + French dictionary ordering traditions. Defaults to ``False``. + :param kwargs: Keyword arguments supplying any additional options + to be sent with this Collation object. + + .. versionadded: 3.4 + + """ + + __slots__ = ("__document",) + + def __init__( + self, + locale: str, + caseLevel: Optional[bool] = None, + caseFirst: Optional[str] = None, + strength: Optional[int] = None, + numericOrdering: Optional[bool] = None, + alternate: Optional[str] = None, + maxVariable: Optional[str] = None, + normalization: Optional[bool] = None, + backwards: Optional[bool] = None, + **kwargs: Any, + ) -> None: + locale = common.validate_string("locale", locale) + self.__document: dict[str, Any] = {"locale": locale} + if caseLevel is not None: + self.__document["caseLevel"] = validate_boolean("caseLevel", caseLevel) + if caseFirst is not None: + self.__document["caseFirst"] = common.validate_string("caseFirst", caseFirst) + if strength is not None: + self.__document["strength"] = common.validate_integer("strength", strength) + if numericOrdering is not None: + self.__document["numericOrdering"] = validate_boolean( + "numericOrdering", numericOrdering + ) + if alternate is not None: + self.__document["alternate"] = common.validate_string("alternate", alternate) + if maxVariable is not None: + self.__document["maxVariable"] = common.validate_string("maxVariable", maxVariable) + if normalization is not None: + self.__document["normalization"] = validate_boolean("normalization", normalization) + if backwards is not None: + self.__document["backwards"] = validate_boolean("backwards", backwards) + self.__document.update(kwargs) + + @property + def document(self) -> dict[str, Any]: + """The document representation of this collation. + + .. note:: + :class:`Collation` is immutable. Mutating the value of + :attr:`document` does not mutate this :class:`Collation`. + """ + return self.__document.copy() + + def __repr__(self) -> str: + document = self.document + return "Collation({})".format(", ".join(f"{key}={document[key]!r}" for key in document)) + + def __eq__(self, other: Any) -> bool: + if isinstance(other, Collation): + return self.document == other.document + return NotImplemented + + def __ne__(self, other: Any) -> bool: + return not self == other + + +def validate_collation_or_none( + value: Optional[Union[Mapping[str, Any], Collation]] +) -> Optional[dict[str, Any]]: + if value is None: + return None + if isinstance(value, Collation): + return value.document + if isinstance(value, dict): + return value + raise TypeError("collation must be a dict, an instance of collation.Collation, or None.") diff --git a/venv/Lib/site-packages/pymongo/collection.py b/venv/Lib/site-packages/pymongo/collection.py new file mode 100644 index 00000000..ddfe9f1d --- /dev/null +++ b/venv/Lib/site-packages/pymongo/collection.py @@ -0,0 +1,3483 @@ +# Copyright 2009-present MongoDB, 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. + +"""Collection level utilities for Mongo.""" +from __future__ import annotations + +from collections import abc +from typing import ( + TYPE_CHECKING, + Any, + Callable, + ContextManager, + Generic, + Iterable, + Iterator, + Mapping, + MutableMapping, + NoReturn, + Optional, + Sequence, + Type, + TypeVar, + Union, + cast, +) + +from bson.codec_options import DEFAULT_CODEC_OPTIONS, CodecOptions +from bson.objectid import ObjectId +from bson.raw_bson import RawBSONDocument +from bson.son import SON +from bson.timestamp import Timestamp +from pymongo import ASCENDING, _csot, common, helpers, message +from pymongo.aggregation import ( + _CollectionAggregationCommand, + _CollectionRawAggregationCommand, +) +from pymongo.bulk import _Bulk +from pymongo.change_stream import CollectionChangeStream +from pymongo.collation import validate_collation_or_none +from pymongo.command_cursor import CommandCursor, RawBatchCommandCursor +from pymongo.common import _ecoc_coll_name, _esc_coll_name +from pymongo.cursor import Cursor, RawBatchCursor +from pymongo.errors import ( + ConfigurationError, + InvalidName, + InvalidOperation, + OperationFailure, +) +from pymongo.helpers import _check_write_command_response +from pymongo.message import _UNICODE_REPLACE_CODEC_OPTIONS +from pymongo.operations import ( + DeleteMany, + DeleteOne, + IndexModel, + InsertOne, + ReplaceOne, + SearchIndexModel, + UpdateMany, + UpdateOne, + _IndexKeyHint, + _IndexList, + _Op, +) +from pymongo.read_concern import DEFAULT_READ_CONCERN, ReadConcern +from pymongo.read_preferences import ReadPreference, _ServerMode +from pymongo.results import ( + BulkWriteResult, + DeleteResult, + InsertManyResult, + InsertOneResult, + UpdateResult, +) +from pymongo.typings import _CollationIn, _DocumentType, _DocumentTypeArg, _Pipeline +from pymongo.write_concern import DEFAULT_WRITE_CONCERN, WriteConcern, validate_boolean + +T = TypeVar("T") + +_FIND_AND_MODIFY_DOC_FIELDS = {"value": 1} + + +_WriteOp = Union[ + InsertOne[_DocumentType], + DeleteOne, + DeleteMany, + ReplaceOne[_DocumentType], + UpdateOne, + UpdateMany, +] + + +class ReturnDocument: + """An enum used with + :meth:`~pymongo.collection.Collection.find_one_and_replace` and + :meth:`~pymongo.collection.Collection.find_one_and_update`. + """ + + BEFORE = False + """Return the original document before it was updated/replaced, or + ``None`` if no document matches the query. + """ + AFTER = True + """Return the updated/replaced or inserted document.""" + + +if TYPE_CHECKING: + from pymongo.aggregation import _AggregationCommand + from pymongo.client_session import ClientSession + from pymongo.collation import Collation + from pymongo.database import Database + from pymongo.pool import Connection + from pymongo.server import Server + + +class Collection(common.BaseObject, Generic[_DocumentType]): + """A Mongo collection.""" + + def __init__( + self, + database: Database[_DocumentType], + name: str, + create: Optional[bool] = False, + codec_options: Optional[CodecOptions[_DocumentTypeArg]] = None, + read_preference: Optional[_ServerMode] = None, + write_concern: Optional[WriteConcern] = None, + read_concern: Optional[ReadConcern] = None, + session: Optional[ClientSession] = None, + **kwargs: Any, + ) -> None: + """Get / create a Mongo collection. + + Raises :class:`TypeError` if `name` is not an instance of + :class:`str`. Raises :class:`~pymongo.errors.InvalidName` if `name` is + not a valid collection name. Any additional keyword arguments will be used + as options passed to the create command. See + :meth:`~pymongo.database.Database.create_collection` for valid + options. + + If `create` is ``True``, `collation` is specified, or any additional + keyword arguments are present, a ``create`` command will be + sent, using ``session`` if specified. Otherwise, a ``create`` command + will not be sent and the collection will be created implicitly on first + use. The optional ``session`` argument is *only* used for the ``create`` + command, it is not associated with the collection afterward. + + :param database: the database to get a collection from + :param name: the name of the collection to get + :param create: if ``True``, force collection + creation even without options being set + :param codec_options: An instance of + :class:`~bson.codec_options.CodecOptions`. If ``None`` (the + default) database.codec_options is used. + :param read_preference: The read preference to use. If + ``None`` (the default) database.read_preference is used. + :param write_concern: An instance of + :class:`~pymongo.write_concern.WriteConcern`. If ``None`` (the + default) database.write_concern is used. + :param read_concern: An instance of + :class:`~pymongo.read_concern.ReadConcern`. If ``None`` (the + default) database.read_concern is used. + :param collation: An instance of + :class:`~pymongo.collation.Collation`. If a collation is provided, + it will be passed to the create collection command. + :param session: a + :class:`~pymongo.client_session.ClientSession` that is used with + the create collection command + :param kwargs: additional keyword arguments will + be passed as options for the create collection command + + .. versionchanged:: 4.2 + Added the ``clusteredIndex`` and ``encryptedFields`` parameters. + + .. versionchanged:: 4.0 + Removed the reindex, map_reduce, inline_map_reduce, + parallel_scan, initialize_unordered_bulk_op, + initialize_ordered_bulk_op, group, count, insert, save, + update, remove, find_and_modify, and ensure_index methods. See the + :ref:`pymongo4-migration-guide`. + + .. versionchanged:: 3.6 + Added ``session`` parameter. + + .. versionchanged:: 3.4 + Support the `collation` option. + + .. versionchanged:: 3.2 + Added the read_concern option. + + .. versionchanged:: 3.0 + Added the codec_options, read_preference, and write_concern options. + Removed the uuid_subtype attribute. + :class:`~pymongo.collection.Collection` no longer returns an + instance of :class:`~pymongo.collection.Collection` for attribute + names with leading underscores. You must use dict-style lookups + instead:: + + collection['__my_collection__'] + + Not: + + collection.__my_collection__ + + .. seealso:: The MongoDB documentation on `collections `_. + """ + super().__init__( + codec_options or database.codec_options, + read_preference or database.read_preference, + write_concern or database.write_concern, + read_concern or database.read_concern, + ) + if not isinstance(name, str): + raise TypeError("name must be an instance of str") + + if not name or ".." in name: + raise InvalidName("collection names cannot be empty") + if "$" in name and not (name.startswith(("oplog.$main", "$cmd"))): + raise InvalidName("collection names must not contain '$': %r" % name) + if name[0] == "." or name[-1] == ".": + raise InvalidName("collection names must not start or end with '.': %r" % name) + if "\x00" in name: + raise InvalidName("collection names must not contain the null character") + collation = validate_collation_or_none(kwargs.pop("collation", None)) + + self.__database: Database[_DocumentType] = database + self.__name = name + self.__full_name = f"{self.__database.name}.{self.__name}" + self.__write_response_codec_options = self.codec_options._replace( + unicode_decode_error_handler="replace", document_class=dict + ) + self._timeout = database.client.options.timeout + encrypted_fields = kwargs.pop("encryptedFields", None) + if create or kwargs or collation: + if encrypted_fields: + common.validate_is_mapping("encrypted_fields", encrypted_fields) + opts = {"clusteredIndex": {"key": {"_id": 1}, "unique": True}} + self.__create( + _esc_coll_name(encrypted_fields, name), opts, None, session, qev2_required=True + ) + self.__create(_ecoc_coll_name(encrypted_fields, name), opts, None, session) + self.__create(name, kwargs, collation, session, encrypted_fields=encrypted_fields) + self.create_index([("__safeContent__", ASCENDING)], session) + else: + self.__create(name, kwargs, collation, session) + + def _conn_for_writes( + self, session: Optional[ClientSession], operation: str + ) -> ContextManager[Connection]: + return self.__database.client._conn_for_writes(session, operation) + + def _command( + self, + conn: Connection, + command: MutableMapping[str, Any], + read_preference: Optional[_ServerMode] = None, + codec_options: Optional[CodecOptions] = None, + check: bool = True, + allowable_errors: Optional[Sequence[Union[str, int]]] = None, + read_concern: Optional[ReadConcern] = None, + write_concern: Optional[WriteConcern] = None, + collation: Optional[_CollationIn] = None, + session: Optional[ClientSession] = None, + retryable_write: bool = False, + user_fields: Optional[Any] = None, + ) -> Mapping[str, Any]: + """Internal command helper. + + :param conn` - A Connection instance. + :param command` - The command itself, as a :class:`~bson.son.SON` instance. + :param read_preference` (optional) - The read preference to use. + :param codec_options` (optional) - An instance of + :class:`~bson.codec_options.CodecOptions`. + :param check: raise OperationFailure if there are errors + :param allowable_errors: errors to ignore if `check` is True + :param read_concern` (optional) - An instance of + :class:`~pymongo.read_concern.ReadConcern`. + :param write_concern: An instance of + :class:`~pymongo.write_concern.WriteConcern`. + :param collation` (optional) - An instance of + :class:`~pymongo.collation.Collation`. + :param session: a + :class:`~pymongo.client_session.ClientSession`. + :param retryable_write: True if this command is a retryable + write. + :param user_fields: Response fields that should be decoded + using the TypeDecoders from codec_options, passed to + bson._decode_all_selective. + + :return: The result document. + """ + with self.__database.client._tmp_session(session) as s: + return conn.command( + self.__database.name, + command, + read_preference or self._read_preference_for(session), + codec_options or self.codec_options, + check, + allowable_errors, + read_concern=read_concern, + write_concern=write_concern, + parse_write_concern_error=True, + collation=collation, + session=s, + client=self.__database.client, + retryable_write=retryable_write, + user_fields=user_fields, + ) + + def __create( + self, + name: str, + options: MutableMapping[str, Any], + collation: Optional[_CollationIn], + session: Optional[ClientSession], + encrypted_fields: Optional[Mapping[str, Any]] = None, + qev2_required: bool = False, + ) -> None: + """Sends a create command with the given options.""" + cmd: dict[str, Any] = {"create": name} + if encrypted_fields: + cmd["encryptedFields"] = encrypted_fields + + if options: + if "size" in options: + options["size"] = float(options["size"]) + cmd.update(options) + with self._conn_for_writes(session, operation=_Op.CREATE) as conn: + if qev2_required and conn.max_wire_version < 21: + raise ConfigurationError( + "Driver support of Queryable Encryption is incompatible with server. " + "Upgrade server to use Queryable Encryption. " + f"Got maxWireVersion {conn.max_wire_version} but need maxWireVersion >= 21 (MongoDB >=7.0)" + ) + + self._command( + conn, + cmd, + read_preference=ReadPreference.PRIMARY, + write_concern=self._write_concern_for(session), + collation=collation, + session=session, + ) + + def __getattr__(self, name: str) -> Collection[_DocumentType]: + """Get a sub-collection of this collection by name. + + Raises InvalidName if an invalid collection name is used. + + :param name: the name of the collection to get + """ + if name.startswith("_"): + full_name = f"{self.__name}.{name}" + raise AttributeError( + f"Collection has no attribute {name!r}. To access the {full_name}" + f" collection, use database['{full_name}']." + ) + return self.__getitem__(name) + + def __getitem__(self, name: str) -> Collection[_DocumentType]: + return Collection( + self.__database, + f"{self.__name}.{name}", + False, + self.codec_options, + self.read_preference, + self.write_concern, + self.read_concern, + ) + + def __repr__(self) -> str: + return f"Collection({self.__database!r}, {self.__name!r})" + + def __eq__(self, other: Any) -> bool: + if isinstance(other, Collection): + return self.__database == other.database and self.__name == other.name + return NotImplemented + + def __ne__(self, other: Any) -> bool: + return not self == other + + def __hash__(self) -> int: + return hash((self.__database, self.__name)) + + def __bool__(self) -> NoReturn: + raise NotImplementedError( + "Collection objects do not implement truth " + "value testing or bool(). Please compare " + "with None instead: collection is not None" + ) + + @property + def full_name(self) -> str: + """The full name of this :class:`Collection`. + + The full name is of the form `database_name.collection_name`. + """ + return self.__full_name + + @property + def name(self) -> str: + """The name of this :class:`Collection`.""" + return self.__name + + @property + def database(self) -> Database[_DocumentType]: + """The :class:`~pymongo.database.Database` that this + :class:`Collection` is a part of. + """ + return self.__database + + def with_options( + self, + codec_options: Optional[CodecOptions[_DocumentTypeArg]] = None, + read_preference: Optional[_ServerMode] = None, + write_concern: Optional[WriteConcern] = None, + read_concern: Optional[ReadConcern] = None, + ) -> Collection[_DocumentType]: + """Get a clone of this collection changing the specified settings. + + >>> coll1.read_preference + Primary() + >>> from pymongo import ReadPreference + >>> coll2 = coll1.with_options(read_preference=ReadPreference.SECONDARY) + >>> coll1.read_preference + Primary() + >>> coll2.read_preference + Secondary(tag_sets=None) + + :param codec_options: An instance of + :class:`~bson.codec_options.CodecOptions`. If ``None`` (the + default) the :attr:`codec_options` of this :class:`Collection` + is used. + :param read_preference: The read preference to use. If + ``None`` (the default) the :attr:`read_preference` of this + :class:`Collection` is used. See :mod:`~pymongo.read_preferences` + for options. + :param write_concern: An instance of + :class:`~pymongo.write_concern.WriteConcern`. If ``None`` (the + default) the :attr:`write_concern` of this :class:`Collection` + is used. + :param read_concern: An instance of + :class:`~pymongo.read_concern.ReadConcern`. If ``None`` (the + default) the :attr:`read_concern` of this :class:`Collection` + is used. + """ + return Collection( + self.__database, + self.__name, + False, + codec_options or self.codec_options, + read_preference or self.read_preference, + write_concern or self.write_concern, + read_concern or self.read_concern, + ) + + @_csot.apply + def bulk_write( + self, + requests: Sequence[_WriteOp[_DocumentType]], + ordered: bool = True, + bypass_document_validation: bool = False, + session: Optional[ClientSession] = None, + comment: Optional[Any] = None, + let: Optional[Mapping] = None, + ) -> BulkWriteResult: + """Send a batch of write operations to the server. + + Requests are passed as a list of write operation instances ( + :class:`~pymongo.operations.InsertOne`, + :class:`~pymongo.operations.UpdateOne`, + :class:`~pymongo.operations.UpdateMany`, + :class:`~pymongo.operations.ReplaceOne`, + :class:`~pymongo.operations.DeleteOne`, or + :class:`~pymongo.operations.DeleteMany`). + + >>> for doc in db.test.find({}): + ... print(doc) + ... + {'x': 1, '_id': ObjectId('54f62e60fba5226811f634ef')} + {'x': 1, '_id': ObjectId('54f62e60fba5226811f634f0')} + >>> # DeleteMany, UpdateOne, and UpdateMany are also available. + ... + >>> from pymongo import InsertOne, DeleteOne, ReplaceOne + >>> requests = [InsertOne({'y': 1}), DeleteOne({'x': 1}), + ... ReplaceOne({'w': 1}, {'z': 1}, upsert=True)] + >>> result = db.test.bulk_write(requests) + >>> result.inserted_count + 1 + >>> result.deleted_count + 1 + >>> result.modified_count + 0 + >>> result.upserted_ids + {2: ObjectId('54f62ee28891e756a6e1abd5')} + >>> for doc in db.test.find({}): + ... print(doc) + ... + {'x': 1, '_id': ObjectId('54f62e60fba5226811f634f0')} + {'y': 1, '_id': ObjectId('54f62ee2fba5226811f634f1')} + {'z': 1, '_id': ObjectId('54f62ee28891e756a6e1abd5')} + + :param requests: A list of write operations (see examples above). + :param ordered: If ``True`` (the default) requests will be + performed on the server serially, in the order provided. If an error + occurs all remaining operations are aborted. If ``False`` requests + will be performed on the server in arbitrary order, possibly in + parallel, and all operations will be attempted. + :param bypass_document_validation: (optional) If ``True``, allows the + write to opt-out of document level validation. Default is + ``False``. + :param session: a + :class:`~pymongo.client_session.ClientSession`. + :param comment: A user-provided comment to attach to this + command. + :param let: Map of parameter names and values. Values must be + constant or closed expressions that do not reference document + fields. Parameters can then be accessed as variables in an + aggregate expression context (e.g. "$$var"). + + :return: An instance of :class:`~pymongo.results.BulkWriteResult`. + + .. seealso:: :ref:`writes-and-ids` + + .. note:: `bypass_document_validation` requires server version + **>= 3.2** + + .. versionchanged:: 4.1 + Added ``comment`` parameter. + Added ``let`` parameter. + + .. versionchanged:: 3.6 + Added ``session`` parameter. + + .. versionchanged:: 3.2 + Added bypass_document_validation support + + .. versionadded:: 3.0 + """ + common.validate_list("requests", requests) + + blk = _Bulk(self, ordered, bypass_document_validation, comment=comment, let=let) + for request in requests: + try: + request._add_to_bulk(blk) + except AttributeError: + raise TypeError(f"{request!r} is not a valid request") from None + + write_concern = self._write_concern_for(session) + bulk_api_result = blk.execute(write_concern, session, _Op.INSERT) + if bulk_api_result is not None: + return BulkWriteResult(bulk_api_result, True) + return BulkWriteResult({}, False) + + def _insert_one( + self, + doc: Mapping[str, Any], + ordered: bool, + write_concern: WriteConcern, + op_id: Optional[int], + bypass_doc_val: bool, + session: Optional[ClientSession], + comment: Optional[Any] = None, + ) -> Any: + """Internal helper for inserting a single document.""" + write_concern = write_concern or self.write_concern + acknowledged = write_concern.acknowledged + command = {"insert": self.name, "ordered": ordered, "documents": [doc]} + if comment is not None: + command["comment"] = comment + + def _insert_command( + session: Optional[ClientSession], conn: Connection, retryable_write: bool + ) -> None: + if bypass_doc_val: + command["bypassDocumentValidation"] = True + + result = conn.command( + self.__database.name, + command, + write_concern=write_concern, + codec_options=self.__write_response_codec_options, + session=session, + client=self.__database.client, + retryable_write=retryable_write, + ) + + _check_write_command_response(result) + + self.__database.client._retryable_write( + acknowledged, _insert_command, session, operation=_Op.INSERT + ) + + if not isinstance(doc, RawBSONDocument): + return doc.get("_id") + return None + + def insert_one( + self, + document: Union[_DocumentType, RawBSONDocument], + bypass_document_validation: bool = False, + session: Optional[ClientSession] = None, + comment: Optional[Any] = None, + ) -> InsertOneResult: + """Insert a single document. + + >>> db.test.count_documents({'x': 1}) + 0 + >>> result = db.test.insert_one({'x': 1}) + >>> result.inserted_id + ObjectId('54f112defba522406c9cc208') + >>> db.test.find_one({'x': 1}) + {'x': 1, '_id': ObjectId('54f112defba522406c9cc208')} + + :param document: The document to insert. Must be a mutable mapping + type. If the document does not have an _id field one will be + added automatically. + :param bypass_document_validation: (optional) If ``True``, allows the + write to opt-out of document level validation. Default is + ``False``. + :param session: a + :class:`~pymongo.client_session.ClientSession`. + :param comment: A user-provided comment to attach to this + command. + + :return: - An instance of :class:`~pymongo.results.InsertOneResult`. + + .. seealso:: :ref:`writes-and-ids` + + .. note:: `bypass_document_validation` requires server version + **>= 3.2** + + .. versionchanged:: 4.1 + Added ``comment`` parameter. + + .. versionchanged:: 3.6 + Added ``session`` parameter. + + .. versionchanged:: 3.2 + Added bypass_document_validation support + + .. versionadded:: 3.0 + """ + common.validate_is_document_type("document", document) + if not (isinstance(document, RawBSONDocument) or "_id" in document): + document["_id"] = ObjectId() # type: ignore[index] + + write_concern = self._write_concern_for(session) + return InsertOneResult( + self._insert_one( + document, + ordered=True, + write_concern=write_concern, + op_id=None, + bypass_doc_val=bypass_document_validation, + session=session, + comment=comment, + ), + write_concern.acknowledged, + ) + + @_csot.apply + def insert_many( + self, + documents: Iterable[Union[_DocumentType, RawBSONDocument]], + ordered: bool = True, + bypass_document_validation: bool = False, + session: Optional[ClientSession] = None, + comment: Optional[Any] = None, + ) -> InsertManyResult: + """Insert an iterable of documents. + + >>> db.test.count_documents({}) + 0 + >>> result = db.test.insert_many([{'x': i} for i in range(2)]) + >>> result.inserted_ids + [ObjectId('54f113fffba522406c9cc20e'), ObjectId('54f113fffba522406c9cc20f')] + >>> db.test.count_documents({}) + 2 + + :param documents: A iterable of documents to insert. + :param ordered: If ``True`` (the default) documents will be + inserted on the server serially, in the order provided. If an error + occurs all remaining inserts are aborted. If ``False``, documents + will be inserted on the server in arbitrary order, possibly in + parallel, and all document inserts will be attempted. + :param bypass_document_validation: (optional) If ``True``, allows the + write to opt-out of document level validation. Default is + ``False``. + :param session: a + :class:`~pymongo.client_session.ClientSession`. + :param comment: A user-provided comment to attach to this + command. + + :return: An instance of :class:`~pymongo.results.InsertManyResult`. + + .. seealso:: :ref:`writes-and-ids` + + .. note:: `bypass_document_validation` requires server version + **>= 3.2** + + .. versionchanged:: 4.1 + Added ``comment`` parameter. + + .. versionchanged:: 3.6 + Added ``session`` parameter. + + .. versionchanged:: 3.2 + Added bypass_document_validation support + + .. versionadded:: 3.0 + """ + if ( + not isinstance(documents, abc.Iterable) + or isinstance(documents, abc.Mapping) + or not documents + ): + raise TypeError("documents must be a non-empty list") + inserted_ids: list[ObjectId] = [] + + def gen() -> Iterator[tuple[int, Mapping[str, Any]]]: + """A generator that validates documents and handles _ids.""" + for document in documents: + common.validate_is_document_type("document", document) + if not isinstance(document, RawBSONDocument): + if "_id" not in document: + document["_id"] = ObjectId() # type: ignore[index] + inserted_ids.append(document["_id"]) + yield (message._INSERT, document) + + write_concern = self._write_concern_for(session) + blk = _Bulk(self, ordered, bypass_document_validation, comment=comment) + blk.ops = list(gen()) + blk.execute(write_concern, session, _Op.INSERT) + return InsertManyResult(inserted_ids, write_concern.acknowledged) + + def _update( + self, + conn: Connection, + criteria: Mapping[str, Any], + document: Union[Mapping[str, Any], _Pipeline], + upsert: bool = False, + multi: bool = False, + write_concern: Optional[WriteConcern] = None, + op_id: Optional[int] = None, + ordered: bool = True, + bypass_doc_val: Optional[bool] = False, + collation: Optional[_CollationIn] = None, + array_filters: Optional[Sequence[Mapping[str, Any]]] = None, + hint: Optional[_IndexKeyHint] = None, + session: Optional[ClientSession] = None, + retryable_write: bool = False, + let: Optional[Mapping[str, Any]] = None, + comment: Optional[Any] = None, + ) -> Optional[Mapping[str, Any]]: + """Internal update / replace helper.""" + validate_boolean("upsert", upsert) + collation = validate_collation_or_none(collation) + write_concern = write_concern or self.write_concern + acknowledged = write_concern.acknowledged + update_doc: dict[str, Any] = { + "q": criteria, + "u": document, + "multi": multi, + "upsert": upsert, + } + if collation is not None: + if not acknowledged: + raise ConfigurationError("Collation is unsupported for unacknowledged writes.") + else: + update_doc["collation"] = collation + if array_filters is not None: + if not acknowledged: + raise ConfigurationError("arrayFilters is unsupported for unacknowledged writes.") + else: + update_doc["arrayFilters"] = array_filters + if hint is not None: + if not acknowledged and conn.max_wire_version < 8: + raise ConfigurationError( + "Must be connected to MongoDB 4.2+ to use hint on unacknowledged update commands." + ) + if not isinstance(hint, str): + hint = helpers._index_document(hint) + update_doc["hint"] = hint + command = {"update": self.name, "ordered": ordered, "updates": [update_doc]} + if let is not None: + common.validate_is_mapping("let", let) + command["let"] = let + + if comment is not None: + command["comment"] = comment + # Update command. + if bypass_doc_val: + command["bypassDocumentValidation"] = True + + # The command result has to be published for APM unmodified + # so we make a shallow copy here before adding updatedExisting. + result = conn.command( + self.__database.name, + command, + write_concern=write_concern, + codec_options=self.__write_response_codec_options, + session=session, + client=self.__database.client, + retryable_write=retryable_write, + ).copy() + _check_write_command_response(result) + # Add the updatedExisting field for compatibility. + if result.get("n") and "upserted" not in result: + result["updatedExisting"] = True + else: + result["updatedExisting"] = False + # MongoDB >= 2.6.0 returns the upsert _id in an array + # element. Break it out for backward compatibility. + if "upserted" in result: + result["upserted"] = result["upserted"][0]["_id"] + + if not acknowledged: + return None + return result + + def _update_retryable( + self, + criteria: Mapping[str, Any], + document: Union[Mapping[str, Any], _Pipeline], + operation: str, + upsert: bool = False, + multi: bool = False, + write_concern: Optional[WriteConcern] = None, + op_id: Optional[int] = None, + ordered: bool = True, + bypass_doc_val: Optional[bool] = False, + collation: Optional[_CollationIn] = None, + array_filters: Optional[Sequence[Mapping[str, Any]]] = None, + hint: Optional[_IndexKeyHint] = None, + session: Optional[ClientSession] = None, + let: Optional[Mapping[str, Any]] = None, + comment: Optional[Any] = None, + ) -> Optional[Mapping[str, Any]]: + """Internal update / replace helper.""" + + def _update( + session: Optional[ClientSession], conn: Connection, retryable_write: bool + ) -> Optional[Mapping[str, Any]]: + return self._update( + conn, + criteria, + document, + upsert=upsert, + multi=multi, + write_concern=write_concern, + op_id=op_id, + ordered=ordered, + bypass_doc_val=bypass_doc_val, + collation=collation, + array_filters=array_filters, + hint=hint, + session=session, + retryable_write=retryable_write, + let=let, + comment=comment, + ) + + return self.__database.client._retryable_write( + (write_concern or self.write_concern).acknowledged and not multi, + _update, + session, + operation, + ) + + def replace_one( + self, + filter: Mapping[str, Any], + replacement: Mapping[str, Any], + upsert: bool = False, + bypass_document_validation: bool = False, + collation: Optional[_CollationIn] = None, + hint: Optional[_IndexKeyHint] = None, + session: Optional[ClientSession] = None, + let: Optional[Mapping[str, Any]] = None, + comment: Optional[Any] = None, + ) -> UpdateResult: + """Replace a single document matching the filter. + + >>> for doc in db.test.find({}): + ... print(doc) + ... + {'x': 1, '_id': ObjectId('54f4c5befba5220aa4d6dee7')} + >>> result = db.test.replace_one({'x': 1}, {'y': 1}) + >>> result.matched_count + 1 + >>> result.modified_count + 1 + >>> for doc in db.test.find({}): + ... print(doc) + ... + {'y': 1, '_id': ObjectId('54f4c5befba5220aa4d6dee7')} + + The *upsert* option can be used to insert a new document if a matching + document does not exist. + + >>> result = db.test.replace_one({'x': 1}, {'x': 1}, True) + >>> result.matched_count + 0 + >>> result.modified_count + 0 + >>> result.upserted_id + ObjectId('54f11e5c8891e756a6e1abd4') + >>> db.test.find_one({'x': 1}) + {'x': 1, '_id': ObjectId('54f11e5c8891e756a6e1abd4')} + + :param filter: A query that matches the document to replace. + :param replacement: The new document. + :param upsert: If ``True``, perform an insert if no documents + match the filter. + :param bypass_document_validation: (optional) If ``True``, allows the + write to opt-out of document level validation. Default is + ``False``. + :param collation: An instance of + :class:`~pymongo.collation.Collation`. + :param hint: An index to use to support the query + predicate specified either by its string name, or in the same + format as passed to + :meth:`~pymongo.collection.Collection.create_index` (e.g. + ``[('field', ASCENDING)]``). This option is only supported on + MongoDB 4.2 and above. + :param session: a + :class:`~pymongo.client_session.ClientSession`. + :param let: Map of parameter names and values. Values must be + constant or closed expressions that do not reference document + fields. Parameters can then be accessed as variables in an + aggregate expression context (e.g. "$$var"). + :param comment: A user-provided comment to attach to this + command. + :return: - An instance of :class:`~pymongo.results.UpdateResult`. + + .. versionchanged:: 4.1 + Added ``let`` parameter. + Added ``comment`` parameter. + .. versionchanged:: 3.11 + Added ``hint`` parameter. + .. versionchanged:: 3.6 + Added ``session`` parameter. + .. versionchanged:: 3.4 + Added the `collation` option. + .. versionchanged:: 3.2 + Added bypass_document_validation support. + + .. versionadded:: 3.0 + """ + common.validate_is_mapping("filter", filter) + common.validate_ok_for_replace(replacement) + if let is not None: + common.validate_is_mapping("let", let) + write_concern = self._write_concern_for(session) + return UpdateResult( + self._update_retryable( + filter, + replacement, + _Op.UPDATE, + upsert, + write_concern=write_concern, + bypass_doc_val=bypass_document_validation, + collation=collation, + hint=hint, + session=session, + let=let, + comment=comment, + ), + write_concern.acknowledged, + ) + + def update_one( + self, + filter: Mapping[str, Any], + update: Union[Mapping[str, Any], _Pipeline], + upsert: bool = False, + bypass_document_validation: bool = False, + collation: Optional[_CollationIn] = None, + array_filters: Optional[Sequence[Mapping[str, Any]]] = None, + hint: Optional[_IndexKeyHint] = None, + session: Optional[ClientSession] = None, + let: Optional[Mapping[str, Any]] = None, + comment: Optional[Any] = None, + ) -> UpdateResult: + """Update a single document matching the filter. + + >>> for doc in db.test.find(): + ... print(doc) + ... + {'x': 1, '_id': 0} + {'x': 1, '_id': 1} + {'x': 1, '_id': 2} + >>> result = db.test.update_one({'x': 1}, {'$inc': {'x': 3}}) + >>> result.matched_count + 1 + >>> result.modified_count + 1 + >>> for doc in db.test.find(): + ... print(doc) + ... + {'x': 4, '_id': 0} + {'x': 1, '_id': 1} + {'x': 1, '_id': 2} + + If ``upsert=True`` and no documents match the filter, create a + new document based on the filter criteria and update modifications. + + >>> result = db.test.update_one({'x': -10}, {'$inc': {'x': 3}}, upsert=True) + >>> result.matched_count + 0 + >>> result.modified_count + 0 + >>> result.upserted_id + ObjectId('626a678eeaa80587d4bb3fb7') + >>> db.test.find_one(result.upserted_id) + {'_id': ObjectId('626a678eeaa80587d4bb3fb7'), 'x': -7} + + :param filter: A query that matches the document to update. + :param update: The modifications to apply. + :param upsert: If ``True``, perform an insert if no documents + match the filter. + :param bypass_document_validation: (optional) If ``True``, allows the + write to opt-out of document level validation. Default is + ``False``. + :param collation: An instance of + :class:`~pymongo.collation.Collation`. + :param array_filters: A list of filters specifying which + array elements an update should apply. + :param hint: An index to use to support the query + predicate specified either by its string name, or in the same + format as passed to + :meth:`~pymongo.collection.Collection.create_index` (e.g. + ``[('field', ASCENDING)]``). This option is only supported on + MongoDB 4.2 and above. + :param session: a + :class:`~pymongo.client_session.ClientSession`. + :param let: Map of parameter names and values. Values must be + constant or closed expressions that do not reference document + fields. Parameters can then be accessed as variables in an + aggregate expression context (e.g. "$$var"). + :param comment: A user-provided comment to attach to this + command. + + :return: - An instance of :class:`~pymongo.results.UpdateResult`. + + .. versionchanged:: 4.1 + Added ``let`` parameter. + Added ``comment`` parameter. + .. versionchanged:: 3.11 + Added ``hint`` parameter. + .. versionchanged:: 3.9 + Added the ability to accept a pipeline as the ``update``. + .. versionchanged:: 3.6 + Added the ``array_filters`` and ``session`` parameters. + .. versionchanged:: 3.4 + Added the ``collation`` option. + .. versionchanged:: 3.2 + Added ``bypass_document_validation`` support. + + .. versionadded:: 3.0 + """ + common.validate_is_mapping("filter", filter) + common.validate_ok_for_update(update) + common.validate_list_or_none("array_filters", array_filters) + + write_concern = self._write_concern_for(session) + return UpdateResult( + self._update_retryable( + filter, + update, + _Op.UPDATE, + upsert, + write_concern=write_concern, + bypass_doc_val=bypass_document_validation, + collation=collation, + array_filters=array_filters, + hint=hint, + session=session, + let=let, + comment=comment, + ), + write_concern.acknowledged, + ) + + def update_many( + self, + filter: Mapping[str, Any], + update: Union[Mapping[str, Any], _Pipeline], + upsert: bool = False, + array_filters: Optional[Sequence[Mapping[str, Any]]] = None, + bypass_document_validation: Optional[bool] = None, + collation: Optional[_CollationIn] = None, + hint: Optional[_IndexKeyHint] = None, + session: Optional[ClientSession] = None, + let: Optional[Mapping[str, Any]] = None, + comment: Optional[Any] = None, + ) -> UpdateResult: + """Update one or more documents that match the filter. + + >>> for doc in db.test.find(): + ... print(doc) + ... + {'x': 1, '_id': 0} + {'x': 1, '_id': 1} + {'x': 1, '_id': 2} + >>> result = db.test.update_many({'x': 1}, {'$inc': {'x': 3}}) + >>> result.matched_count + 3 + >>> result.modified_count + 3 + >>> for doc in db.test.find(): + ... print(doc) + ... + {'x': 4, '_id': 0} + {'x': 4, '_id': 1} + {'x': 4, '_id': 2} + + :param filter: A query that matches the documents to update. + :param update: The modifications to apply. + :param upsert: If ``True``, perform an insert if no documents + match the filter. + :param bypass_document_validation: If ``True``, allows the + write to opt-out of document level validation. Default is + ``False``. + :param collation: An instance of + :class:`~pymongo.collation.Collation`. + :param array_filters: A list of filters specifying which + array elements an update should apply. + :param hint: An index to use to support the query + predicate specified either by its string name, or in the same + format as passed to + :meth:`~pymongo.collection.Collection.create_index` (e.g. + ``[('field', ASCENDING)]``). This option is only supported on + MongoDB 4.2 and above. + :param session: a + :class:`~pymongo.client_session.ClientSession`. + :param let: Map of parameter names and values. Values must be + constant or closed expressions that do not reference document + fields. Parameters can then be accessed as variables in an + aggregate expression context (e.g. "$$var"). + :param comment: A user-provided comment to attach to this + command. + + :return: - An instance of :class:`~pymongo.results.UpdateResult`. + + .. versionchanged:: 4.1 + Added ``let`` parameter. + Added ``comment`` parameter. + .. versionchanged:: 3.11 + Added ``hint`` parameter. + .. versionchanged:: 3.9 + Added the ability to accept a pipeline as the `update`. + .. versionchanged:: 3.6 + Added ``array_filters`` and ``session`` parameters. + .. versionchanged:: 3.4 + Added the `collation` option. + .. versionchanged:: 3.2 + Added bypass_document_validation support. + + .. versionadded:: 3.0 + """ + common.validate_is_mapping("filter", filter) + common.validate_ok_for_update(update) + common.validate_list_or_none("array_filters", array_filters) + + write_concern = self._write_concern_for(session) + return UpdateResult( + self._update_retryable( + filter, + update, + _Op.UPDATE, + upsert, + multi=True, + write_concern=write_concern, + bypass_doc_val=bypass_document_validation, + collation=collation, + array_filters=array_filters, + hint=hint, + session=session, + let=let, + comment=comment, + ), + write_concern.acknowledged, + ) + + def drop( + self, + session: Optional[ClientSession] = None, + comment: Optional[Any] = None, + encrypted_fields: Optional[Mapping[str, Any]] = None, + ) -> None: + """Alias for :meth:`~pymongo.database.Database.drop_collection`. + + :param session: a + :class:`~pymongo.client_session.ClientSession`. + :param comment: A user-provided comment to attach to this + command. + :param encrypted_fields: **(BETA)** Document that describes the encrypted fields for + Queryable Encryption. + + The following two calls are equivalent: + + >>> db.foo.drop() + >>> db.drop_collection("foo") + + .. versionchanged:: 4.2 + Added ``encrypted_fields`` parameter. + + .. versionchanged:: 4.1 + Added ``comment`` parameter. + + .. versionchanged:: 3.7 + :meth:`drop` now respects this :class:`Collection`'s :attr:`write_concern`. + + .. versionchanged:: 3.6 + Added ``session`` parameter. + """ + dbo = self.__database.client.get_database( + self.__database.name, + self.codec_options, + self.read_preference, + self.write_concern, + self.read_concern, + ) + dbo.drop_collection( + self.__name, session=session, comment=comment, encrypted_fields=encrypted_fields + ) + + def _delete( + self, + conn: Connection, + criteria: Mapping[str, Any], + multi: bool, + write_concern: Optional[WriteConcern] = None, + op_id: Optional[int] = None, + ordered: bool = True, + collation: Optional[_CollationIn] = None, + hint: Optional[_IndexKeyHint] = None, + session: Optional[ClientSession] = None, + retryable_write: bool = False, + let: Optional[Mapping[str, Any]] = None, + comment: Optional[Any] = None, + ) -> Mapping[str, Any]: + """Internal delete helper.""" + common.validate_is_mapping("filter", criteria) + write_concern = write_concern or self.write_concern + acknowledged = write_concern.acknowledged + delete_doc = {"q": criteria, "limit": int(not multi)} + collation = validate_collation_or_none(collation) + if collation is not None: + if not acknowledged: + raise ConfigurationError("Collation is unsupported for unacknowledged writes.") + else: + delete_doc["collation"] = collation + if hint is not None: + if not acknowledged and conn.max_wire_version < 9: + raise ConfigurationError( + "Must be connected to MongoDB 4.4+ to use hint on unacknowledged delete commands." + ) + if not isinstance(hint, str): + hint = helpers._index_document(hint) + delete_doc["hint"] = hint + command = {"delete": self.name, "ordered": ordered, "deletes": [delete_doc]} + + if let is not None: + common.validate_is_document_type("let", let) + command["let"] = let + + if comment is not None: + command["comment"] = comment + + # Delete command. + result = conn.command( + self.__database.name, + command, + write_concern=write_concern, + codec_options=self.__write_response_codec_options, + session=session, + client=self.__database.client, + retryable_write=retryable_write, + ) + _check_write_command_response(result) + return result + + def _delete_retryable( + self, + criteria: Mapping[str, Any], + multi: bool, + write_concern: Optional[WriteConcern] = None, + op_id: Optional[int] = None, + ordered: bool = True, + collation: Optional[_CollationIn] = None, + hint: Optional[_IndexKeyHint] = None, + session: Optional[ClientSession] = None, + let: Optional[Mapping[str, Any]] = None, + comment: Optional[Any] = None, + ) -> Mapping[str, Any]: + """Internal delete helper.""" + + def _delete( + session: Optional[ClientSession], conn: Connection, retryable_write: bool + ) -> Mapping[str, Any]: + return self._delete( + conn, + criteria, + multi, + write_concern=write_concern, + op_id=op_id, + ordered=ordered, + collation=collation, + hint=hint, + session=session, + retryable_write=retryable_write, + let=let, + comment=comment, + ) + + return self.__database.client._retryable_write( + (write_concern or self.write_concern).acknowledged and not multi, + _delete, + session, + operation=_Op.DELETE, + ) + + def delete_one( + self, + filter: Mapping[str, Any], + collation: Optional[_CollationIn] = None, + hint: Optional[_IndexKeyHint] = None, + session: Optional[ClientSession] = None, + let: Optional[Mapping[str, Any]] = None, + comment: Optional[Any] = None, + ) -> DeleteResult: + """Delete a single document matching the filter. + + >>> db.test.count_documents({'x': 1}) + 3 + >>> result = db.test.delete_one({'x': 1}) + >>> result.deleted_count + 1 + >>> db.test.count_documents({'x': 1}) + 2 + + :param filter: A query that matches the document to delete. + :param collation: An instance of + :class:`~pymongo.collation.Collation`. + :param hint: An index to use to support the query + predicate specified either by its string name, or in the same + format as passed to + :meth:`~pymongo.collection.Collection.create_index` (e.g. + ``[('field', ASCENDING)]``). This option is only supported on + MongoDB 4.4 and above. + :param session: a + :class:`~pymongo.client_session.ClientSession`. + :param let: Map of parameter names and values. Values must be + constant or closed expressions that do not reference document + fields. Parameters can then be accessed as variables in an + aggregate expression context (e.g. "$$var"). + :param comment: A user-provided comment to attach to this + command. + + :return: - An instance of :class:`~pymongo.results.DeleteResult`. + + .. versionchanged:: 4.1 + Added ``let`` parameter. + Added ``comment`` parameter. + .. versionchanged:: 3.11 + Added ``hint`` parameter. + .. versionchanged:: 3.6 + Added ``session`` parameter. + .. versionchanged:: 3.4 + Added the `collation` option. + .. versionadded:: 3.0 + """ + write_concern = self._write_concern_for(session) + return DeleteResult( + self._delete_retryable( + filter, + False, + write_concern=write_concern, + collation=collation, + hint=hint, + session=session, + let=let, + comment=comment, + ), + write_concern.acknowledged, + ) + + def delete_many( + self, + filter: Mapping[str, Any], + collation: Optional[_CollationIn] = None, + hint: Optional[_IndexKeyHint] = None, + session: Optional[ClientSession] = None, + let: Optional[Mapping[str, Any]] = None, + comment: Optional[Any] = None, + ) -> DeleteResult: + """Delete one or more documents matching the filter. + + >>> db.test.count_documents({'x': 1}) + 3 + >>> result = db.test.delete_many({'x': 1}) + >>> result.deleted_count + 3 + >>> db.test.count_documents({'x': 1}) + 0 + + :param filter: A query that matches the documents to delete. + :param collation: An instance of + :class:`~pymongo.collation.Collation`. + :param hint: An index to use to support the query + predicate specified either by its string name, or in the same + format as passed to + :meth:`~pymongo.collection.Collection.create_index` (e.g. + ``[('field', ASCENDING)]``). This option is only supported on + MongoDB 4.4 and above. + :param session: a + :class:`~pymongo.client_session.ClientSession`. + :param let: Map of parameter names and values. Values must be + constant or closed expressions that do not reference document + fields. Parameters can then be accessed as variables in an + aggregate expression context (e.g. "$$var"). + :param comment: A user-provided comment to attach to this + command. + + :return: - An instance of :class:`~pymongo.results.DeleteResult`. + + .. versionchanged:: 4.1 + Added ``let`` parameter. + Added ``comment`` parameter. + .. versionchanged:: 3.11 + Added ``hint`` parameter. + .. versionchanged:: 3.6 + Added ``session`` parameter. + .. versionchanged:: 3.4 + Added the `collation` option. + .. versionadded:: 3.0 + """ + write_concern = self._write_concern_for(session) + return DeleteResult( + self._delete_retryable( + filter, + True, + write_concern=write_concern, + collation=collation, + hint=hint, + session=session, + let=let, + comment=comment, + ), + write_concern.acknowledged, + ) + + def find_one( + self, filter: Optional[Any] = None, *args: Any, **kwargs: Any + ) -> Optional[_DocumentType]: + """Get a single document from the database. + + All arguments to :meth:`find` are also valid arguments for + :meth:`find_one`, although any `limit` argument will be + ignored. Returns a single document, or ``None`` if no matching + document is found. + + The :meth:`find_one` method obeys the :attr:`read_preference` of + this :class:`Collection`. + + :param filter: a dictionary specifying + the query to be performed OR any other type to be used as + the value for a query for ``"_id"``. + + :param args: any additional positional arguments + are the same as the arguments to :meth:`find`. + + :param kwargs: any additional keyword arguments + are the same as the arguments to :meth:`find`. + + :: code-block: python + + >>> collection.find_one(max_time_ms=100) + + """ + if filter is not None and not isinstance(filter, abc.Mapping): + filter = {"_id": filter} + cursor = self.find(filter, *args, **kwargs) + for result in cursor.limit(-1): + return result + return None + + def find(self, *args: Any, **kwargs: Any) -> Cursor[_DocumentType]: + """Query the database. + + The `filter` argument is a query document that all results + must match. For example: + + >>> db.test.find({"hello": "world"}) + + only matches documents that have a key "hello" with value + "world". Matches can have other keys *in addition* to + "hello". The `projection` argument is used to specify a subset + of fields that should be included in the result documents. By + limiting results to a certain subset of fields you can cut + down on network traffic and decoding time. + + Raises :class:`TypeError` if any of the arguments are of + improper type. Returns an instance of + :class:`~pymongo.cursor.Cursor` corresponding to this query. + + The :meth:`find` method obeys the :attr:`read_preference` of + this :class:`Collection`. + + :param filter: A query document that selects which documents + to include in the result set. Can be an empty document to include + all documents. + :param projection: a list of field names that should be + returned in the result set or a dict specifying the fields + to include or exclude. If `projection` is a list "_id" will + always be returned. Use a dict to exclude fields from + the result (e.g. projection={'_id': False}). + :param session: a + :class:`~pymongo.client_session.ClientSession`. + :param skip: the number of documents to omit (from + the start of the result set) when returning the results + :param limit: the maximum number of results to + return. A limit of 0 (the default) is equivalent to setting no + limit. + :param no_cursor_timeout: if False (the default), any + returned cursor is closed by the server after 10 minutes of + inactivity. If set to True, the returned cursor will never + time out on the server. Care should be taken to ensure that + cursors with no_cursor_timeout turned on are properly closed. + :param cursor_type: the type of cursor to return. The valid + options are defined by :class:`~pymongo.cursor.CursorType`: + + - :attr:`~pymongo.cursor.CursorType.NON_TAILABLE` - the result of + this find call will return a standard cursor over the result set. + - :attr:`~pymongo.cursor.CursorType.TAILABLE` - the result of this + find call will be a tailable cursor - tailable cursors are only + for use with capped collections. They are not closed when the + last data is retrieved but are kept open and the cursor location + marks the final document position. If more data is received + iteration of the cursor will continue from the last document + received. For details, see the `tailable cursor documentation + `_. + - :attr:`~pymongo.cursor.CursorType.TAILABLE_AWAIT` - the result + of this find call will be a tailable cursor with the await flag + set. The server will wait for a few seconds after returning the + full result set so that it can capture and return additional data + added during the query. + - :attr:`~pymongo.cursor.CursorType.EXHAUST` - the result of this + find call will be an exhaust cursor. MongoDB will stream batched + results to the client without waiting for the client to request + each batch, reducing latency. See notes on compatibility below. + + :param sort: a list of (key, direction) pairs + specifying the sort order for this query. See + :meth:`~pymongo.cursor.Cursor.sort` for details. + :param allow_partial_results: if True, mongos will return + partial results if some shards are down instead of returning an + error. + :param oplog_replay: **DEPRECATED** - if True, set the + oplogReplay query flag. Default: False. + :param batch_size: Limits the number of documents returned in + a single batch. + :param collation: An instance of + :class:`~pymongo.collation.Collation`. + :param return_key: If True, return only the index keys in + each document. + :param show_record_id: If True, adds a field ``$recordId`` in + each document with the storage engine's internal record identifier. + :param snapshot: **DEPRECATED** - If True, prevents the + cursor from returning a document more than once because of an + intervening write operation. + :param hint: An index, in the same format as passed to + :meth:`~pymongo.collection.Collection.create_index` (e.g. + ``[('field', ASCENDING)]``). Pass this as an alternative to calling + :meth:`~pymongo.cursor.Cursor.hint` on the cursor to tell Mongo the + proper index to use for the query. + :param max_time_ms: Specifies a time limit for a query + operation. If the specified time is exceeded, the operation will be + aborted and :exc:`~pymongo.errors.ExecutionTimeout` is raised. Pass + this as an alternative to calling + :meth:`~pymongo.cursor.Cursor.max_time_ms` on the cursor. + :param max_scan: **DEPRECATED** - The maximum number of + documents to scan. Pass this as an alternative to calling + :meth:`~pymongo.cursor.Cursor.max_scan` on the cursor. + :param min: A list of field, limit pairs specifying the + inclusive lower bound for all keys of a specific index in order. + Pass this as an alternative to calling + :meth:`~pymongo.cursor.Cursor.min` on the cursor. ``hint`` must + also be passed to ensure the query utilizes the correct index. + :param max: A list of field, limit pairs specifying the + exclusive upper bound for all keys of a specific index in order. + Pass this as an alternative to calling + :meth:`~pymongo.cursor.Cursor.max` on the cursor. ``hint`` must + also be passed to ensure the query utilizes the correct index. + :param comment: A string to attach to the query to help + interpret and trace the operation in the server logs and in profile + data. Pass this as an alternative to calling + :meth:`~pymongo.cursor.Cursor.comment` on the cursor. + :param allow_disk_use: if True, MongoDB may use temporary + disk files to store data exceeding the system memory limit while + processing a blocking sort operation. The option has no effect if + MongoDB can satisfy the specified sort using an index, or if the + blocking sort requires less memory than the 100 MiB limit. This + option is only supported on MongoDB 4.4 and above. + + .. note:: There are a number of caveats to using + :attr:`~pymongo.cursor.CursorType.EXHAUST` as cursor_type: + + - The `limit` option can not be used with an exhaust cursor. + + - Exhaust cursors are not supported by mongos and can not be + used with a sharded cluster. + + - A :class:`~pymongo.cursor.Cursor` instance created with the + :attr:`~pymongo.cursor.CursorType.EXHAUST` cursor_type requires an + exclusive :class:`~socket.socket` connection to MongoDB. If the + :class:`~pymongo.cursor.Cursor` is discarded without being + completely iterated the underlying :class:`~socket.socket` + connection will be closed and discarded without being returned to + the connection pool. + + .. versionchanged:: 4.0 + Removed the ``modifiers`` option. + Empty projections (eg {} or []) are passed to the server as-is, + rather than the previous behavior which substituted in a + projection of ``{"_id": 1}``. This means that an empty projection + will now return the entire document, not just the ``"_id"`` field. + + .. versionchanged:: 3.11 + Added the ``allow_disk_use`` option. + Deprecated the ``oplog_replay`` option. Support for this option is + deprecated in MongoDB 4.4. The query engine now automatically + optimizes queries against the oplog without requiring this + option to be set. + + .. versionchanged:: 3.7 + Deprecated the ``snapshot`` option, which is deprecated in MongoDB + 3.6 and removed in MongoDB 4.0. + Deprecated the ``max_scan`` option. Support for this option is + deprecated in MongoDB 4.0. Use ``max_time_ms`` instead to limit + server-side execution time. + + .. versionchanged:: 3.6 + Added ``session`` parameter. + + .. versionchanged:: 3.5 + Added the options ``return_key``, ``show_record_id``, ``snapshot``, + ``hint``, ``max_time_ms``, ``max_scan``, ``min``, ``max``, and + ``comment``. + Deprecated the ``modifiers`` option. + + .. versionchanged:: 3.4 + Added support for the ``collation`` option. + + .. versionchanged:: 3.0 + Changed the parameter names ``spec``, ``fields``, ``timeout``, and + ``partial`` to ``filter``, ``projection``, ``no_cursor_timeout``, + and ``allow_partial_results`` respectively. + Added the ``cursor_type``, ``oplog_replay``, and ``modifiers`` + options. + Removed the ``network_timeout``, ``read_preference``, ``tag_sets``, + ``secondary_acceptable_latency_ms``, ``max_scan``, ``snapshot``, + ``tailable``, ``await_data``, ``exhaust``, ``as_class``, and + slave_okay parameters. + Removed ``compile_re`` option: PyMongo now always + represents BSON regular expressions as :class:`~bson.regex.Regex` + objects. Use :meth:`~bson.regex.Regex.try_compile` to attempt to + convert from a BSON regular expression to a Python regular + expression object. + Soft deprecated the ``manipulate`` option. + + .. seealso:: The MongoDB documentation on `find `_. + """ + return Cursor(self, *args, **kwargs) + + def find_raw_batches(self, *args: Any, **kwargs: Any) -> RawBatchCursor[_DocumentType]: + """Query the database and retrieve batches of raw BSON. + + Similar to the :meth:`find` method but returns a + :class:`~pymongo.cursor.RawBatchCursor`. + + This example demonstrates how to work with raw batches, but in practice + raw batches should be passed to an external library that can decode + BSON into another data type, rather than used with PyMongo's + :mod:`bson` module. + + >>> import bson + >>> cursor = db.test.find_raw_batches() + >>> for batch in cursor: + ... print(bson.decode_all(batch)) + + .. note:: find_raw_batches does not support auto encryption. + + .. versionchanged:: 3.12 + Instead of ignoring the user-specified read concern, this method + now sends it to the server when connected to MongoDB 3.6+. + + Added session support. + + .. versionadded:: 3.6 + """ + # OP_MSG is required to support encryption. + if self.__database.client._encrypter: + raise InvalidOperation("find_raw_batches does not support auto encryption") + return RawBatchCursor(self, *args, **kwargs) + + def _count_cmd( + self, + session: Optional[ClientSession], + conn: Connection, + read_preference: Optional[_ServerMode], + cmd: dict[str, Any], + collation: Optional[Collation], + ) -> int: + """Internal count command helper.""" + # XXX: "ns missing" checks can be removed when we drop support for + # MongoDB 3.0, see SERVER-17051. + res = self._command( + conn, + cmd, + read_preference=read_preference, + allowable_errors=["ns missing"], + codec_options=self.__write_response_codec_options, + read_concern=self.read_concern, + collation=collation, + session=session, + ) + if res.get("errmsg", "") == "ns missing": + return 0 + return int(res["n"]) + + def _aggregate_one_result( + self, + conn: Connection, + read_preference: Optional[_ServerMode], + cmd: dict[str, Any], + collation: Optional[_CollationIn], + session: Optional[ClientSession], + ) -> Optional[Mapping[str, Any]]: + """Internal helper to run an aggregate that returns a single result.""" + result = self._command( + conn, + cmd, + read_preference, + allowable_errors=[26], # Ignore NamespaceNotFound. + codec_options=self.__write_response_codec_options, + read_concern=self.read_concern, + collation=collation, + session=session, + ) + # cursor will not be present for NamespaceNotFound errors. + if "cursor" not in result: + return None + batch = result["cursor"]["firstBatch"] + return batch[0] if batch else None + + def estimated_document_count(self, comment: Optional[Any] = None, **kwargs: Any) -> int: + """Get an estimate of the number of documents in this collection using + collection metadata. + + The :meth:`estimated_document_count` method is **not** supported in a + transaction. + + All optional parameters should be passed as keyword arguments + to this method. Valid options include: + + - `maxTimeMS` (int): The maximum amount of time to allow this + operation to run, in milliseconds. + + :param comment: A user-provided comment to attach to this + command. + :param kwargs: See list of options above. + + .. versionchanged:: 4.2 + This method now always uses the `count`_ command. Due to an oversight in versions + 5.0.0-5.0.8 of MongoDB, the count command was not included in V1 of the + :ref:`versioned-api-ref`. Users of the Stable API with estimated_document_count are + recommended to upgrade their server version to 5.0.9+ or set + :attr:`pymongo.server_api.ServerApi.strict` to ``False`` to avoid encountering errors. + + .. versionadded:: 3.7 + .. _count: https://mongodb.com/docs/manual/reference/command/count/ + """ + if "session" in kwargs: + raise ConfigurationError("estimated_document_count does not support sessions") + if comment is not None: + kwargs["comment"] = comment + + def _cmd( + session: Optional[ClientSession], + _server: Server, + conn: Connection, + read_preference: Optional[_ServerMode], + ) -> int: + cmd: dict[str, Any] = {"count": self.__name} + cmd.update(kwargs) + return self._count_cmd(session, conn, read_preference, cmd, collation=None) + + return self._retryable_non_cursor_read(_cmd, None, operation=_Op.COUNT) + + def count_documents( + self, + filter: Mapping[str, Any], + session: Optional[ClientSession] = None, + comment: Optional[Any] = None, + **kwargs: Any, + ) -> int: + """Count the number of documents in this collection. + + .. note:: For a fast count of the total documents in a collection see + :meth:`estimated_document_count`. + + The :meth:`count_documents` method is supported in a transaction. + + All optional parameters should be passed as keyword arguments + to this method. Valid options include: + + - `skip` (int): The number of matching documents to skip before + returning results. + - `limit` (int): The maximum number of documents to count. Must be + a positive integer. If not provided, no limit is imposed. + - `maxTimeMS` (int): The maximum amount of time to allow this + operation to run, in milliseconds. + - `collation` (optional): An instance of + :class:`~pymongo.collation.Collation`. + - `hint` (string or list of tuples): The index to use. Specify either + the index name as a string or the index specification as a list of + tuples (e.g. [('a', pymongo.ASCENDING), ('b', pymongo.ASCENDING)]). + + The :meth:`count_documents` method obeys the :attr:`read_preference` of + this :class:`Collection`. + + .. note:: When migrating from :meth:`count` to :meth:`count_documents` + the following query operators must be replaced: + + +-------------+-------------------------------------+ + | Operator | Replacement | + +=============+=====================================+ + | $where | `$expr`_ | + +-------------+-------------------------------------+ + | $near | `$geoWithin`_ with `$center`_ | + +-------------+-------------------------------------+ + | $nearSphere | `$geoWithin`_ with `$centerSphere`_ | + +-------------+-------------------------------------+ + + :param filter: A query document that selects which documents + to count in the collection. Can be an empty document to count all + documents. + :param session: a + :class:`~pymongo.client_session.ClientSession`. + :param comment: A user-provided comment to attach to this + command. + :param kwargs: See list of options above. + + + .. versionadded:: 3.7 + + .. _$expr: https://mongodb.com/docs/manual/reference/operator/query/expr/ + .. _$geoWithin: https://mongodb.com/docs/manual/reference/operator/query/geoWithin/ + .. _$center: https://mongodb.com/docs/manual/reference/operator/query/center/ + .. _$centerSphere: https://mongodb.com/docs/manual/reference/operator/query/centerSphere/ + """ + pipeline = [{"$match": filter}] + if "skip" in kwargs: + pipeline.append({"$skip": kwargs.pop("skip")}) + if "limit" in kwargs: + pipeline.append({"$limit": kwargs.pop("limit")}) + if comment is not None: + kwargs["comment"] = comment + pipeline.append({"$group": {"_id": 1, "n": {"$sum": 1}}}) + cmd = {"aggregate": self.__name, "pipeline": pipeline, "cursor": {}} + if "hint" in kwargs and not isinstance(kwargs["hint"], str): + kwargs["hint"] = helpers._index_document(kwargs["hint"]) + collation = validate_collation_or_none(kwargs.pop("collation", None)) + cmd.update(kwargs) + + def _cmd( + session: Optional[ClientSession], + _server: Server, + conn: Connection, + read_preference: Optional[_ServerMode], + ) -> int: + result = self._aggregate_one_result(conn, read_preference, cmd, collation, session) + if not result: + return 0 + return result["n"] + + return self._retryable_non_cursor_read(_cmd, session, _Op.COUNT) + + def _retryable_non_cursor_read( + self, + func: Callable[[Optional[ClientSession], Server, Connection, Optional[_ServerMode]], T], + session: Optional[ClientSession], + operation: str, + ) -> T: + """Non-cursor read helper to handle implicit session creation.""" + client = self.__database.client + with client._tmp_session(session) as s: + return client._retryable_read(func, self._read_preference_for(s), s, operation) + + def create_indexes( + self, + indexes: Sequence[IndexModel], + session: Optional[ClientSession] = None, + comment: Optional[Any] = None, + **kwargs: Any, + ) -> list[str]: + """Create one or more indexes on this collection. + + >>> from pymongo import IndexModel, ASCENDING, DESCENDING + >>> index1 = IndexModel([("hello", DESCENDING), + ... ("world", ASCENDING)], name="hello_world") + >>> index2 = IndexModel([("goodbye", DESCENDING)]) + >>> db.test.create_indexes([index1, index2]) + ["hello_world", "goodbye_-1"] + + :param indexes: A list of :class:`~pymongo.operations.IndexModel` + instances. + :param session: a + :class:`~pymongo.client_session.ClientSession`. + :param comment: A user-provided comment to attach to this + command. + :param kwargs: optional arguments to the createIndexes + command (like maxTimeMS) can be passed as keyword arguments. + + + + + .. note:: The :attr:`~pymongo.collection.Collection.write_concern` of + this collection is automatically applied to this operation. + + .. versionchanged:: 3.6 + Added ``session`` parameter. Added support for arbitrary keyword + arguments. + + .. versionchanged:: 3.4 + Apply this collection's write concern automatically to this operation + when connected to MongoDB >= 3.4. + .. versionadded:: 3.0 + + .. _createIndexes: https://mongodb.com/docs/manual/reference/command/createIndexes/ + """ + common.validate_list("indexes", indexes) + if comment is not None: + kwargs["comment"] = comment + return self.__create_indexes(indexes, session, **kwargs) + + @_csot.apply + def __create_indexes( + self, indexes: Sequence[IndexModel], session: Optional[ClientSession], **kwargs: Any + ) -> list[str]: + """Internal createIndexes helper. + + :param indexes: A list of :class:`~pymongo.operations.IndexModel` + instances. + :param session: a + :class:`~pymongo.client_session.ClientSession`. + :param kwargs: optional arguments to the createIndexes + command (like maxTimeMS) can be passed as keyword arguments. + """ + names = [] + with self._conn_for_writes(session, operation=_Op.CREATE_INDEXES) as conn: + supports_quorum = conn.max_wire_version >= 9 + + def gen_indexes() -> Iterator[Mapping[str, Any]]: + for index in indexes: + if not isinstance(index, IndexModel): + raise TypeError( + f"{index!r} is not an instance of pymongo.operations.IndexModel" + ) + document = index.document + names.append(document["name"]) + yield document + + cmd = {"createIndexes": self.name, "indexes": list(gen_indexes())} + cmd.update(kwargs) + if "commitQuorum" in kwargs and not supports_quorum: + raise ConfigurationError( + "Must be connected to MongoDB 4.4+ to use the " + "commitQuorum option for createIndexes" + ) + + self._command( + conn, + cmd, + read_preference=ReadPreference.PRIMARY, + codec_options=_UNICODE_REPLACE_CODEC_OPTIONS, + write_concern=self._write_concern_for(session), + session=session, + ) + return names + + def create_index( + self, + keys: _IndexKeyHint, + session: Optional[ClientSession] = None, + comment: Optional[Any] = None, + **kwargs: Any, + ) -> str: + """Creates an index on this collection. + + Takes either a single key or a list containing (key, direction) pairs + or keys. If no direction is given, :data:`~pymongo.ASCENDING` will + be assumed. + The key(s) must be an instance of :class:`str` and the direction(s) must + be one of (:data:`~pymongo.ASCENDING`, :data:`~pymongo.DESCENDING`, + :data:`~pymongo.GEO2D`, :data:`~pymongo.GEOSPHERE`, + :data:`~pymongo.HASHED`, :data:`~pymongo.TEXT`). + + To create a single key ascending index on the key ``'mike'`` we just + use a string argument:: + + >>> my_collection.create_index("mike") + + For a compound index on ``'mike'`` descending and ``'eliot'`` + ascending we need to use a list of tuples:: + + >>> my_collection.create_index([("mike", pymongo.DESCENDING), + ... "eliot"]) + + All optional index creation parameters should be passed as + keyword arguments to this method. For example:: + + >>> my_collection.create_index([("mike", pymongo.DESCENDING)], + ... background=True) + + Valid options include, but are not limited to: + + - `name`: custom name to use for this index - if none is + given, a name will be generated. + - `unique`: if ``True``, creates a uniqueness constraint on the + index. + - `background`: if ``True``, this index should be created in the + background. + - `sparse`: if ``True``, omit from the index any documents that lack + the indexed field. + - `bucketSize`: for use with geoHaystack indexes. + Number of documents to group together within a certain proximity + to a given longitude and latitude. + - `min`: minimum value for keys in a :data:`~pymongo.GEO2D` + index. + - `max`: maximum value for keys in a :data:`~pymongo.GEO2D` + index. + - `expireAfterSeconds`: Used to create an expiring (TTL) + collection. MongoDB will automatically delete documents from + this collection after seconds. The indexed field must + be a UTC datetime or the data will not expire. + - `partialFilterExpression`: A document that specifies a filter for + a partial index. + - `collation` (optional): An instance of + :class:`~pymongo.collation.Collation`. + - `wildcardProjection`: Allows users to include or exclude specific + field paths from a `wildcard index`_ using the {"$**" : 1} key + pattern. Requires MongoDB >= 4.2. + - `hidden`: if ``True``, this index will be hidden from the query + planner and will not be evaluated as part of query plan + selection. Requires MongoDB >= 4.4. + + See the MongoDB documentation for a full list of supported options by + server version. + + .. warning:: `dropDups` is not supported by MongoDB 3.0 or newer. The + option is silently ignored by the server and unique index builds + using the option will fail if a duplicate value is detected. + + .. note:: The :attr:`~pymongo.collection.Collection.write_concern` of + this collection is automatically applied to this operation. + + :param keys: a single key or a list of (key, direction) + pairs specifying the index to create + :param session: a + :class:`~pymongo.client_session.ClientSession`. + :param comment: A user-provided comment to attach to this + command. + :param kwargs: any additional index creation + options (see the above list) should be passed as keyword + arguments. + + .. versionchanged:: 4.4 + Allow passing a list containing (key, direction) pairs + or keys for the ``keys`` parameter. + .. versionchanged:: 4.1 + Added ``comment`` parameter. + .. versionchanged:: 3.11 + Added the ``hidden`` option. + .. versionchanged:: 3.6 + Added ``session`` parameter. Added support for passing maxTimeMS + in kwargs. + .. versionchanged:: 3.4 + Apply this collection's write concern automatically to this operation + when connected to MongoDB >= 3.4. Support the `collation` option. + .. versionchanged:: 3.2 + Added partialFilterExpression to support partial indexes. + .. versionchanged:: 3.0 + Renamed `key_or_list` to `keys`. Removed the `cache_for` option. + :meth:`create_index` no longer caches index names. Removed support + for the drop_dups and bucket_size aliases. + + .. seealso:: The MongoDB documentation on `indexes `_. + + .. _wildcard index: https://dochub.mongodb.org/core/index-wildcard/ + """ + cmd_options = {} + if "maxTimeMS" in kwargs: + cmd_options["maxTimeMS"] = kwargs.pop("maxTimeMS") + if comment is not None: + cmd_options["comment"] = comment + index = IndexModel(keys, **kwargs) + return self.__create_indexes([index], session, **cmd_options)[0] + + def drop_indexes( + self, + session: Optional[ClientSession] = None, + comment: Optional[Any] = None, + **kwargs: Any, + ) -> None: + """Drops all indexes on this collection. + + Can be used on non-existent collections or collections with no indexes. + Raises OperationFailure on an error. + + :param session: a + :class:`~pymongo.client_session.ClientSession`. + :param comment: A user-provided comment to attach to this + command. + :param kwargs: optional arguments to the createIndexes + command (like maxTimeMS) can be passed as keyword arguments. + + .. note:: The :attr:`~pymongo.collection.Collection.write_concern` of + this collection is automatically applied to this operation. + + .. versionchanged:: 3.6 + Added ``session`` parameter. Added support for arbitrary keyword + arguments. + + .. versionchanged:: 3.4 + Apply this collection's write concern automatically to this operation + when connected to MongoDB >= 3.4. + """ + if comment is not None: + kwargs["comment"] = comment + self.drop_index("*", session=session, **kwargs) + + @_csot.apply + def drop_index( + self, + index_or_name: _IndexKeyHint, + session: Optional[ClientSession] = None, + comment: Optional[Any] = None, + **kwargs: Any, + ) -> None: + """Drops the specified index on this collection. + + Can be used on non-existent collections or collections with no + indexes. Raises OperationFailure on an error (e.g. trying to + drop an index that does not exist). `index_or_name` + can be either an index name (as returned by `create_index`), + or an index specifier (as passed to `create_index`). An index + specifier should be a list of (key, direction) pairs. Raises + TypeError if index is not an instance of (str, unicode, list). + + .. warning:: + + if a custom name was used on index creation (by + passing the `name` parameter to :meth:`create_index`) the index + **must** be dropped by name. + + :param index_or_name: index (or name of index) to drop + :param session: a + :class:`~pymongo.client_session.ClientSession`. + :param comment: A user-provided comment to attach to this + command. + :param kwargs: optional arguments to the createIndexes + command (like maxTimeMS) can be passed as keyword arguments. + + + + .. note:: The :attr:`~pymongo.collection.Collection.write_concern` of + this collection is automatically applied to this operation. + + + .. versionchanged:: 3.6 + Added ``session`` parameter. Added support for arbitrary keyword + arguments. + + .. versionchanged:: 3.4 + Apply this collection's write concern automatically to this operation + when connected to MongoDB >= 3.4. + + """ + name = index_or_name + if isinstance(index_or_name, list): + name = helpers._gen_index_name(index_or_name) + + if not isinstance(name, str): + raise TypeError("index_or_name must be an instance of str or list") + + cmd = {"dropIndexes": self.__name, "index": name} + cmd.update(kwargs) + if comment is not None: + cmd["comment"] = comment + with self._conn_for_writes(session, operation=_Op.DROP_INDEXES) as conn: + self._command( + conn, + cmd, + read_preference=ReadPreference.PRIMARY, + allowable_errors=["ns not found", 26], + write_concern=self._write_concern_for(session), + session=session, + ) + + def list_indexes( + self, + session: Optional[ClientSession] = None, + comment: Optional[Any] = None, + ) -> CommandCursor[MutableMapping[str, Any]]: + """Get a cursor over the index documents for this collection. + + >>> for index in db.test.list_indexes(): + ... print(index) + ... + SON([('v', 2), ('key', SON([('_id', 1)])), ('name', '_id_')]) + + :param session: a + :class:`~pymongo.client_session.ClientSession`. + :param comment: A user-provided comment to attach to this + command. + + :return: An instance of :class:`~pymongo.command_cursor.CommandCursor`. + + .. versionchanged:: 4.1 + Added ``comment`` parameter. + + .. versionchanged:: 3.6 + Added ``session`` parameter. + + .. versionadded:: 3.0 + """ + codec_options: CodecOptions = CodecOptions(SON) + coll = cast( + Collection[MutableMapping[str, Any]], + self.with_options(codec_options=codec_options, read_preference=ReadPreference.PRIMARY), + ) + read_pref = (session and session._txn_read_preference()) or ReadPreference.PRIMARY + explicit_session = session is not None + + def _cmd( + session: Optional[ClientSession], + _server: Server, + conn: Connection, + read_preference: _ServerMode, + ) -> CommandCursor[MutableMapping[str, Any]]: + cmd = {"listIndexes": self.__name, "cursor": {}} + if comment is not None: + cmd["comment"] = comment + + try: + cursor = self._command(conn, cmd, read_preference, codec_options, session=session)[ + "cursor" + ] + except OperationFailure as exc: + # Ignore NamespaceNotFound errors to match the behavior + # of reading from *.system.indexes. + if exc.code != 26: + raise + cursor = {"id": 0, "firstBatch": []} + cmd_cursor = CommandCursor( + coll, + cursor, + conn.address, + session=session, + explicit_session=explicit_session, + comment=cmd.get("comment"), + ) + cmd_cursor._maybe_pin_connection(conn) + return cmd_cursor + + with self.__database.client._tmp_session(session, False) as s: + return self.__database.client._retryable_read( + _cmd, read_pref, s, operation=_Op.LIST_INDEXES + ) + + def index_information( + self, + session: Optional[ClientSession] = None, + comment: Optional[Any] = None, + ) -> MutableMapping[str, Any]: + """Get information on this collection's indexes. + + Returns a dictionary where the keys are index names (as + returned by create_index()) and the values are dictionaries + containing information about each index. The dictionary is + guaranteed to contain at least a single key, ``"key"`` which + is a list of (key, direction) pairs specifying the index (as + passed to create_index()). It will also contain any other + metadata about the indexes, except for the ``"ns"`` and + ``"name"`` keys, which are cleaned. Example output might look + like this: + + >>> db.test.create_index("x", unique=True) + 'x_1' + >>> db.test.index_information() + {'_id_': {'key': [('_id', 1)]}, + 'x_1': {'unique': True, 'key': [('x', 1)]}} + + :param session: a + :class:`~pymongo.client_session.ClientSession`. + :param comment: A user-provided comment to attach to this + command. + + .. versionchanged:: 4.1 + Added ``comment`` parameter. + + .. versionchanged:: 3.6 + Added ``session`` parameter. + """ + cursor = self.list_indexes(session=session, comment=comment) + info = {} + for index in cursor: + index["key"] = list(index["key"].items()) + index = dict(index) # noqa: PLW2901 + info[index.pop("name")] = index + return info + + def list_search_indexes( + self, + name: Optional[str] = None, + session: Optional[ClientSession] = None, + comment: Optional[Any] = None, + **kwargs: Any, + ) -> CommandCursor[Mapping[str, Any]]: + """Return a cursor over search indexes for the current collection. + + :param name: If given, the name of the index to search + for. Only indexes with matching index names will be returned. + If not given, all search indexes for the current collection + will be returned. + :param session: a :class:`~pymongo.client_session.ClientSession`. + :param comment: A user-provided comment to attach to this + command. + + :return: A :class:`~pymongo.command_cursor.CommandCursor` over the result + set. + + .. note:: requires a MongoDB server version 7.0+ Atlas cluster. + + .. versionadded:: 4.5 + """ + if name is None: + pipeline: _Pipeline = [{"$listSearchIndexes": {}}] + else: + pipeline = [{"$listSearchIndexes": {"name": name}}] + + coll = self.with_options( + codec_options=DEFAULT_CODEC_OPTIONS, + read_preference=ReadPreference.PRIMARY, + write_concern=DEFAULT_WRITE_CONCERN, + read_concern=DEFAULT_READ_CONCERN, + ) + cmd = _CollectionAggregationCommand( + coll, + CommandCursor, + pipeline, + kwargs, + explicit_session=session is not None, + comment=comment, + user_fields={"cursor": {"firstBatch": 1}}, + ) + + return self.__database.client._retryable_read( + cmd.get_cursor, + cmd.get_read_preference(session), # type: ignore[arg-type] + session, + retryable=not cmd._performs_write, + operation=_Op.LIST_SEARCH_INDEX, + ) + + def create_search_index( + self, + model: Union[Mapping[str, Any], SearchIndexModel], + session: Optional[ClientSession] = None, + comment: Any = None, + **kwargs: Any, + ) -> str: + """Create a single search index for the current collection. + + :param model: The model for the new search index. + It can be given as a :class:`~pymongo.operations.SearchIndexModel` + instance or a dictionary with a model "definition" and optional + "name". + :param session: a + :class:`~pymongo.client_session.ClientSession`. + :param comment: A user-provided comment to attach to this + command. + :param kwargs: optional arguments to the createSearchIndexes + command (like maxTimeMS) can be passed as keyword arguments. + + :return: The name of the new search index. + + .. note:: requires a MongoDB server version 7.0+ Atlas cluster. + + .. versionadded:: 4.5 + """ + if not isinstance(model, SearchIndexModel): + model = SearchIndexModel(**model) + return self.create_search_indexes([model], session, comment, **kwargs)[0] + + def create_search_indexes( + self, + models: list[SearchIndexModel], + session: Optional[ClientSession] = None, + comment: Optional[Any] = None, + **kwargs: Any, + ) -> list[str]: + """Create multiple search indexes for the current collection. + + :param models: A list of :class:`~pymongo.operations.SearchIndexModel` instances. + :param session: a :class:`~pymongo.client_session.ClientSession`. + :param comment: A user-provided comment to attach to this + command. + :param kwargs: optional arguments to the createSearchIndexes + command (like maxTimeMS) can be passed as keyword arguments. + + :return: A list of the newly created search index names. + + .. note:: requires a MongoDB server version 7.0+ Atlas cluster. + + .. versionadded:: 4.5 + """ + if comment is not None: + kwargs["comment"] = comment + + def gen_indexes() -> Iterator[Mapping[str, Any]]: + for index in models: + if not isinstance(index, SearchIndexModel): + raise TypeError( + f"{index!r} is not an instance of pymongo.operations.SearchIndexModel" + ) + yield index.document + + cmd = {"createSearchIndexes": self.name, "indexes": list(gen_indexes())} + cmd.update(kwargs) + + with self._conn_for_writes(session, operation=_Op.CREATE_SEARCH_INDEXES) as conn: + resp = self._command( + conn, + cmd, + read_preference=ReadPreference.PRIMARY, + codec_options=_UNICODE_REPLACE_CODEC_OPTIONS, + ) + return [index["name"] for index in resp["indexesCreated"]] + + def drop_search_index( + self, + name: str, + session: Optional[ClientSession] = None, + comment: Optional[Any] = None, + **kwargs: Any, + ) -> None: + """Delete a search index by index name. + + :param name: The name of the search index to be deleted. + :param session: a + :class:`~pymongo.client_session.ClientSession`. + :param comment: A user-provided comment to attach to this + command. + :param kwargs: optional arguments to the dropSearchIndexes + command (like maxTimeMS) can be passed as keyword arguments. + + .. note:: requires a MongoDB server version 7.0+ Atlas cluster. + + .. versionadded:: 4.5 + """ + cmd = {"dropSearchIndex": self.__name, "name": name} + cmd.update(kwargs) + if comment is not None: + cmd["comment"] = comment + with self._conn_for_writes(session, operation=_Op.DROP_SEARCH_INDEXES) as conn: + self._command( + conn, + cmd, + read_preference=ReadPreference.PRIMARY, + allowable_errors=["ns not found", 26], + codec_options=_UNICODE_REPLACE_CODEC_OPTIONS, + ) + + def update_search_index( + self, + name: str, + definition: Mapping[str, Any], + session: Optional[ClientSession] = None, + comment: Optional[Any] = None, + **kwargs: Any, + ) -> None: + """Update a search index by replacing the existing index definition with the provided definition. + + :param name: The name of the search index to be updated. + :param definition: The new search index definition. + :param session: a + :class:`~pymongo.client_session.ClientSession`. + :param comment: A user-provided comment to attach to this + command. + :param kwargs: optional arguments to the updateSearchIndexes + command (like maxTimeMS) can be passed as keyword arguments. + + .. note:: requires a MongoDB server version 7.0+ Atlas cluster. + + .. versionadded:: 4.5 + """ + cmd = {"updateSearchIndex": self.__name, "name": name, "definition": definition} + cmd.update(kwargs) + if comment is not None: + cmd["comment"] = comment + with self._conn_for_writes(session, operation=_Op.UPDATE_SEARCH_INDEX) as conn: + self._command( + conn, + cmd, + read_preference=ReadPreference.PRIMARY, + allowable_errors=["ns not found", 26], + codec_options=_UNICODE_REPLACE_CODEC_OPTIONS, + ) + + def options( + self, + session: Optional[ClientSession] = None, + comment: Optional[Any] = None, + ) -> MutableMapping[str, Any]: + """Get the options set on this collection. + + Returns a dictionary of options and their values - see + :meth:`~pymongo.database.Database.create_collection` for more + information on the possible options. Returns an empty + dictionary if the collection has not been created yet. + + :param session: a + :class:`~pymongo.client_session.ClientSession`. + :param comment: A user-provided comment to attach to this + command. + + .. versionchanged:: 3.6 + Added ``session`` parameter. + """ + dbo = self.__database.client.get_database( + self.__database.name, + self.codec_options, + self.read_preference, + self.write_concern, + self.read_concern, + ) + cursor = dbo.list_collections( + session=session, filter={"name": self.__name}, comment=comment + ) + + result = None + for doc in cursor: + result = doc + break + + if not result: + return {} + + options = result.get("options", {}) + assert options is not None + if "create" in options: + del options["create"] + + return options + + @_csot.apply + def _aggregate( + self, + aggregation_command: Type[_AggregationCommand], + pipeline: _Pipeline, + cursor_class: Type[CommandCursor], + session: Optional[ClientSession], + explicit_session: bool, + let: Optional[Mapping[str, Any]] = None, + comment: Optional[Any] = None, + **kwargs: Any, + ) -> CommandCursor[_DocumentType]: + if comment is not None: + kwargs["comment"] = comment + cmd = aggregation_command( + self, + cursor_class, + pipeline, + kwargs, + explicit_session, + let, + user_fields={"cursor": {"firstBatch": 1}}, + ) + + return self.__database.client._retryable_read( + cmd.get_cursor, + cmd.get_read_preference(session), # type: ignore[arg-type] + session, + retryable=not cmd._performs_write, + operation=_Op.AGGREGATE, + ) + + def aggregate( + self, + pipeline: _Pipeline, + session: Optional[ClientSession] = None, + let: Optional[Mapping[str, Any]] = None, + comment: Optional[Any] = None, + **kwargs: Any, + ) -> CommandCursor[_DocumentType]: + """Perform an aggregation using the aggregation framework on this + collection. + + The :meth:`aggregate` method obeys the :attr:`read_preference` of this + :class:`Collection`, except when ``$out`` or ``$merge`` are used on + MongoDB <5.0, in which case + :attr:`~pymongo.read_preferences.ReadPreference.PRIMARY` is used. + + .. note:: This method does not support the 'explain' option. Please + use `PyMongoExplain `_ + instead. An example is included in the :ref:`aggregate-examples` + documentation. + + .. note:: The :attr:`~pymongo.collection.Collection.write_concern` of + this collection is automatically applied to this operation. + + :param pipeline: a list of aggregation pipeline stages + :param session: a + :class:`~pymongo.client_session.ClientSession`. + :param let: A dict of parameter names and values. Values must be + constant or closed expressions that do not reference document + fields. Parameters can then be accessed as variables in an + aggregate expression context (e.g. ``"$$var"``). This option is + only supported on MongoDB >= 5.0. + :param comment: A user-provided comment to attach to this + command. + :param kwargs: extra `aggregate command`_ parameters. + + All optional `aggregate command`_ parameters should be passed as + keyword arguments to this method. Valid options include, but are not + limited to: + + - `allowDiskUse` (bool): Enables writing to temporary files. When set + to True, aggregation stages can write data to the _tmp subdirectory + of the --dbpath directory. The default is False. + - `maxTimeMS` (int): The maximum amount of time to allow the operation + to run in milliseconds. + - `batchSize` (int): The maximum number of documents to return per + batch. Ignored if the connected mongod or mongos does not support + returning aggregate results using a cursor. + - `collation` (optional): An instance of + :class:`~pymongo.collation.Collation`. + + + :return: A :class:`~pymongo.command_cursor.CommandCursor` over the result + set. + + .. versionchanged:: 4.1 + Added ``comment`` parameter. + Added ``let`` parameter. + Support $merge and $out executing on secondaries according to the + collection's :attr:`read_preference`. + .. versionchanged:: 4.0 + Removed the ``useCursor`` option. + .. versionchanged:: 3.9 + Apply this collection's read concern to pipelines containing the + `$out` stage when connected to MongoDB >= 4.2. + Added support for the ``$merge`` pipeline stage. + Aggregations that write always use read preference + :attr:`~pymongo.read_preferences.ReadPreference.PRIMARY`. + .. versionchanged:: 3.6 + Added the `session` parameter. Added the `maxAwaitTimeMS` option. + Deprecated the `useCursor` option. + .. versionchanged:: 3.4 + Apply this collection's write concern automatically to this operation + when connected to MongoDB >= 3.4. Support the `collation` option. + .. versionchanged:: 3.0 + The :meth:`aggregate` method always returns a CommandCursor. The + pipeline argument must be a list. + + .. seealso:: :doc:`/examples/aggregation` + + .. _aggregate command: + https://mongodb.com/docs/manual/reference/command/aggregate + """ + with self.__database.client._tmp_session(session, close=False) as s: + return self._aggregate( + _CollectionAggregationCommand, + pipeline, + CommandCursor, + session=s, + explicit_session=session is not None, + let=let, + comment=comment, + **kwargs, + ) + + def aggregate_raw_batches( + self, + pipeline: _Pipeline, + session: Optional[ClientSession] = None, + comment: Optional[Any] = None, + **kwargs: Any, + ) -> RawBatchCursor[_DocumentType]: + """Perform an aggregation and retrieve batches of raw BSON. + + Similar to the :meth:`aggregate` method but returns a + :class:`~pymongo.cursor.RawBatchCursor`. + + This example demonstrates how to work with raw batches, but in practice + raw batches should be passed to an external library that can decode + BSON into another data type, rather than used with PyMongo's + :mod:`bson` module. + + >>> import bson + >>> cursor = db.test.aggregate_raw_batches([ + ... {'$project': {'x': {'$multiply': [2, '$x']}}}]) + >>> for batch in cursor: + ... print(bson.decode_all(batch)) + + .. note:: aggregate_raw_batches does not support auto encryption. + + .. versionchanged:: 3.12 + Added session support. + + .. versionadded:: 3.6 + """ + # OP_MSG is required to support encryption. + if self.__database.client._encrypter: + raise InvalidOperation("aggregate_raw_batches does not support auto encryption") + if comment is not None: + kwargs["comment"] = comment + with self.__database.client._tmp_session(session, close=False) as s: + return cast( + RawBatchCursor[_DocumentType], + self._aggregate( + _CollectionRawAggregationCommand, + pipeline, + RawBatchCommandCursor, + session=s, + explicit_session=session is not None, + **kwargs, + ), + ) + + def watch( + self, + pipeline: Optional[_Pipeline] = None, + full_document: Optional[str] = None, + resume_after: Optional[Mapping[str, Any]] = None, + max_await_time_ms: Optional[int] = None, + batch_size: Optional[int] = None, + collation: Optional[_CollationIn] = None, + start_at_operation_time: Optional[Timestamp] = None, + session: Optional[ClientSession] = None, + start_after: Optional[Mapping[str, Any]] = None, + comment: Optional[Any] = None, + full_document_before_change: Optional[str] = None, + show_expanded_events: Optional[bool] = None, + ) -> CollectionChangeStream[_DocumentType]: + """Watch changes on this collection. + + Performs an aggregation with an implicit initial ``$changeStream`` + stage and returns a + :class:`~pymongo.change_stream.CollectionChangeStream` cursor which + iterates over changes on this collection. + + .. code-block:: python + + with db.collection.watch() as stream: + for change in stream: + print(change) + + The :class:`~pymongo.change_stream.CollectionChangeStream` iterable + blocks until the next change document is returned or an error is + raised. If the + :meth:`~pymongo.change_stream.CollectionChangeStream.next` method + encounters a network error when retrieving a batch from the server, + it will automatically attempt to recreate the cursor such that no + change events are missed. Any error encountered during the resume + attempt indicates there may be an outage and will be raised. + + .. code-block:: python + + try: + with db.collection.watch([{"$match": {"operationType": "insert"}}]) as stream: + for insert_change in stream: + print(insert_change) + except pymongo.errors.PyMongoError: + # The ChangeStream encountered an unrecoverable error or the + # resume attempt failed to recreate the cursor. + logging.error("...") + + For a precise description of the resume process see the + `change streams specification`_. + + .. note:: Using this helper method is preferred to directly calling + :meth:`~pymongo.collection.Collection.aggregate` with a + ``$changeStream`` stage, for the purpose of supporting + resumability. + + .. warning:: This Collection's :attr:`read_concern` must be + ``ReadConcern("majority")`` in order to use the ``$changeStream`` + stage. + + :param pipeline: A list of aggregation pipeline stages to + append to an initial ``$changeStream`` stage. Not all + pipeline stages are valid after a ``$changeStream`` stage, see the + MongoDB documentation on change streams for the supported stages. + :param full_document: The fullDocument to pass as an option + to the ``$changeStream`` stage. Allowed values: 'updateLookup', + 'whenAvailable', 'required'. When set to 'updateLookup', the + change notification for partial updates will include both a delta + describing the changes to the document, as well as a copy of the + entire document that was changed from some time after the change + occurred. + :param full_document_before_change: Allowed values: 'whenAvailable' + and 'required'. Change events may now result in a + 'fullDocumentBeforeChange' response field. + :param resume_after: A resume token. If provided, the + change stream will start returning changes that occur directly + after the operation specified in the resume token. A resume token + is the _id value of a change document. + :param max_await_time_ms: The maximum time in milliseconds + for the server to wait for changes before responding to a getMore + operation. + :param batch_size: The maximum number of documents to return + per batch. + :param collation: The :class:`~pymongo.collation.Collation` + to use for the aggregation. + :param start_at_operation_time: If provided, the resulting + change stream will only return changes that occurred at or after + the specified :class:`~bson.timestamp.Timestamp`. Requires + MongoDB >= 4.0. + :param session: a + :class:`~pymongo.client_session.ClientSession`. + :param start_after: The same as `resume_after` except that + `start_after` can resume notifications after an invalidate event. + This option and `resume_after` are mutually exclusive. + :param comment: A user-provided comment to attach to this + command. + :param show_expanded_events: Include expanded events such as DDL events like `dropIndexes`. + + :return: A :class:`~pymongo.change_stream.CollectionChangeStream` cursor. + + .. versionchanged:: 4.3 + Added `show_expanded_events` parameter. + + .. versionchanged:: 4.2 + Added ``full_document_before_change`` parameter. + + .. versionchanged:: 4.1 + Added ``comment`` parameter. + + .. versionchanged:: 3.9 + Added the ``start_after`` parameter. + + .. versionchanged:: 3.7 + Added the ``start_at_operation_time`` parameter. + + .. versionadded:: 3.6 + + .. seealso:: The MongoDB documentation on `changeStreams `_. + + .. _change streams specification: + https://github.com/mongodb/specifications/blob/master/source/change-streams/change-streams.md + """ + return CollectionChangeStream( + self, + pipeline, + full_document, + resume_after, + max_await_time_ms, + batch_size, + collation, + start_at_operation_time, + session, + start_after, + comment, + full_document_before_change, + show_expanded_events, + ) + + @_csot.apply + def rename( + self, + new_name: str, + session: Optional[ClientSession] = None, + comment: Optional[Any] = None, + **kwargs: Any, + ) -> MutableMapping[str, Any]: + """Rename this collection. + + If operating in auth mode, client must be authorized as an + admin to perform this operation. Raises :class:`TypeError` if + `new_name` is not an instance of :class:`str`. + Raises :class:`~pymongo.errors.InvalidName` + if `new_name` is not a valid collection name. + + :param new_name: new name for this collection + :param session: a + :class:`~pymongo.client_session.ClientSession`. + :param comment: A user-provided comment to attach to this + command. + :param kwargs: additional arguments to the rename command + may be passed as keyword arguments to this helper method + (i.e. ``dropTarget=True``) + + .. note:: The :attr:`~pymongo.collection.Collection.write_concern` of + this collection is automatically applied to this operation. + + .. versionchanged:: 3.6 + Added ``session`` parameter. + + .. versionchanged:: 3.4 + Apply this collection's write concern automatically to this operation + when connected to MongoDB >= 3.4. + + """ + if not isinstance(new_name, str): + raise TypeError("new_name must be an instance of str") + + if not new_name or ".." in new_name: + raise InvalidName("collection names cannot be empty") + if new_name[0] == "." or new_name[-1] == ".": + raise InvalidName("collection names must not start or end with '.'") + if "$" in new_name and not new_name.startswith("oplog.$main"): + raise InvalidName("collection names must not contain '$'") + + new_name = f"{self.__database.name}.{new_name}" + cmd = {"renameCollection": self.__full_name, "to": new_name} + cmd.update(kwargs) + if comment is not None: + cmd["comment"] = comment + write_concern = self._write_concern_for_cmd(cmd, session) + + with self._conn_for_writes(session, operation=_Op.RENAME) as conn: + with self.__database.client._tmp_session(session) as s: + return conn.command( + "admin", + cmd, + write_concern=write_concern, + parse_write_concern_error=True, + session=s, + client=self.__database.client, + ) + + def distinct( + self, + key: str, + filter: Optional[Mapping[str, Any]] = None, + session: Optional[ClientSession] = None, + comment: Optional[Any] = None, + **kwargs: Any, + ) -> list: + """Get a list of distinct values for `key` among all documents + in this collection. + + Raises :class:`TypeError` if `key` is not an instance of + :class:`str`. + + All optional distinct parameters should be passed as keyword arguments + to this method. Valid options include: + + - `maxTimeMS` (int): The maximum amount of time to allow the count + command to run, in milliseconds. + - `collation` (optional): An instance of + :class:`~pymongo.collation.Collation`. + + The :meth:`distinct` method obeys the :attr:`read_preference` of + this :class:`Collection`. + + :param key: name of the field for which we want to get the distinct + values + :param filter: A query document that specifies the documents + from which to retrieve the distinct values. + :param session: a + :class:`~pymongo.client_session.ClientSession`. + :param comment: A user-provided comment to attach to this + command. + :param kwargs: See list of options above. + + .. versionchanged:: 3.6 + Added ``session`` parameter. + + .. versionchanged:: 3.4 + Support the `collation` option. + + """ + if not isinstance(key, str): + raise TypeError("key must be an instance of str") + cmd = {"distinct": self.__name, "key": key} + if filter is not None: + if "query" in kwargs: + raise ConfigurationError("can't pass both filter and query") + kwargs["query"] = filter + collation = validate_collation_or_none(kwargs.pop("collation", None)) + cmd.update(kwargs) + if comment is not None: + cmd["comment"] = comment + + def _cmd( + session: Optional[ClientSession], + _server: Server, + conn: Connection, + read_preference: Optional[_ServerMode], + ) -> list: + return self._command( + conn, + cmd, + read_preference=read_preference, + read_concern=self.read_concern, + collation=collation, + session=session, + user_fields={"values": 1}, + )["values"] + + return self._retryable_non_cursor_read(_cmd, session, operation=_Op.DISTINCT) + + def _write_concern_for_cmd( + self, cmd: Mapping[str, Any], session: Optional[ClientSession] + ) -> WriteConcern: + raw_wc = cmd.get("writeConcern") + if raw_wc is not None: + return WriteConcern(**raw_wc) + else: + return self._write_concern_for(session) + + def __find_and_modify( + self, + filter: Mapping[str, Any], + projection: Optional[Union[Mapping[str, Any], Iterable[str]]], + sort: Optional[_IndexList], + upsert: Optional[bool] = None, + return_document: bool = ReturnDocument.BEFORE, + array_filters: Optional[Sequence[Mapping[str, Any]]] = None, + hint: Optional[_IndexKeyHint] = None, + session: Optional[ClientSession] = None, + let: Optional[Mapping] = None, + **kwargs: Any, + ) -> Any: + """Internal findAndModify helper.""" + common.validate_is_mapping("filter", filter) + if not isinstance(return_document, bool): + raise ValueError( + "return_document must be ReturnDocument.BEFORE or ReturnDocument.AFTER" + ) + collation = validate_collation_or_none(kwargs.pop("collation", None)) + cmd = {"findAndModify": self.__name, "query": filter, "new": return_document} + if let is not None: + common.validate_is_mapping("let", let) + cmd["let"] = let + cmd.update(kwargs) + if projection is not None: + cmd["fields"] = helpers._fields_list_to_dict(projection, "projection") + if sort is not None: + cmd["sort"] = helpers._index_document(sort) + if upsert is not None: + validate_boolean("upsert", upsert) + cmd["upsert"] = upsert + if hint is not None: + if not isinstance(hint, str): + hint = helpers._index_document(hint) + + write_concern = self._write_concern_for_cmd(cmd, session) + + def _find_and_modify( + session: Optional[ClientSession], conn: Connection, retryable_write: bool + ) -> Any: + acknowledged = write_concern.acknowledged + if array_filters is not None: + if not acknowledged: + raise ConfigurationError( + "arrayFilters is unsupported for unacknowledged writes." + ) + cmd["arrayFilters"] = list(array_filters) + if hint is not None: + if conn.max_wire_version < 8: + raise ConfigurationError( + "Must be connected to MongoDB 4.2+ to use hint on find and modify commands." + ) + elif not acknowledged and conn.max_wire_version < 9: + raise ConfigurationError( + "Must be connected to MongoDB 4.4+ to use hint on unacknowledged find and modify commands." + ) + cmd["hint"] = hint + out = self._command( + conn, + cmd, + read_preference=ReadPreference.PRIMARY, + write_concern=write_concern, + collation=collation, + session=session, + retryable_write=retryable_write, + user_fields=_FIND_AND_MODIFY_DOC_FIELDS, + ) + _check_write_command_response(out) + + return out.get("value") + + return self.__database.client._retryable_write( + write_concern.acknowledged, + _find_and_modify, + session, + operation=_Op.FIND_AND_MODIFY, + ) + + def find_one_and_delete( + self, + filter: Mapping[str, Any], + projection: Optional[Union[Mapping[str, Any], Iterable[str]]] = None, + sort: Optional[_IndexList] = None, + hint: Optional[_IndexKeyHint] = None, + session: Optional[ClientSession] = None, + let: Optional[Mapping[str, Any]] = None, + comment: Optional[Any] = None, + **kwargs: Any, + ) -> _DocumentType: + """Finds a single document and deletes it, returning the document. + + >>> db.test.count_documents({'x': 1}) + 2 + >>> db.test.find_one_and_delete({'x': 1}) + {'x': 1, '_id': ObjectId('54f4e12bfba5220aa4d6dee8')} + >>> db.test.count_documents({'x': 1}) + 1 + + If multiple documents match *filter*, a *sort* can be applied. + + >>> for doc in db.test.find({'x': 1}): + ... print(doc) + ... + {'x': 1, '_id': 0} + {'x': 1, '_id': 1} + {'x': 1, '_id': 2} + >>> db.test.find_one_and_delete( + ... {'x': 1}, sort=[('_id', pymongo.DESCENDING)]) + {'x': 1, '_id': 2} + + The *projection* option can be used to limit the fields returned. + + >>> db.test.find_one_and_delete({'x': 1}, projection={'_id': False}) + {'x': 1} + + :param filter: A query that matches the document to delete. + :param projection: a list of field names that should be + returned in the result document or a mapping specifying the fields + to include or exclude. If `projection` is a list "_id" will + always be returned. Use a mapping to exclude fields from + the result (e.g. projection={'_id': False}). + :param sort: a list of (key, direction) pairs + specifying the sort order for the query. If multiple documents + match the query, they are sorted and the first is deleted. + :param hint: An index to use to support the query predicate + specified either by its string name, or in the same format as + passed to :meth:`~pymongo.collection.Collection.create_index` + (e.g. ``[('field', ASCENDING)]``). This option is only supported + on MongoDB 4.4 and above. + :param session: a + :class:`~pymongo.client_session.ClientSession`. + :param let: Map of parameter names and values. Values must be + constant or closed expressions that do not reference document + fields. Parameters can then be accessed as variables in an + aggregate expression context (e.g. "$$var"). + :param comment: A user-provided comment to attach to this + command. + :param kwargs: additional command arguments can be passed + as keyword arguments (for example maxTimeMS can be used with + recent server versions). + + .. versionchanged:: 4.1 + Added ``let`` parameter. + .. versionchanged:: 3.11 + Added ``hint`` parameter. + .. versionchanged:: 3.6 + Added ``session`` parameter. + .. versionchanged:: 3.2 + Respects write concern. + + .. warning:: Starting in PyMongo 3.2, this command uses the + :class:`~pymongo.write_concern.WriteConcern` of this + :class:`~pymongo.collection.Collection` when connected to MongoDB >= + 3.2. Note that using an elevated write concern with this command may + be slower compared to using the default write concern. + + .. versionchanged:: 3.4 + Added the `collation` option. + .. versionadded:: 3.0 + """ + kwargs["remove"] = True + if comment is not None: + kwargs["comment"] = comment + return self.__find_and_modify( + filter, projection, sort, let=let, hint=hint, session=session, **kwargs + ) + + def find_one_and_replace( + self, + filter: Mapping[str, Any], + replacement: Mapping[str, Any], + projection: Optional[Union[Mapping[str, Any], Iterable[str]]] = None, + sort: Optional[_IndexList] = None, + upsert: bool = False, + return_document: bool = ReturnDocument.BEFORE, + hint: Optional[_IndexKeyHint] = None, + session: Optional[ClientSession] = None, + let: Optional[Mapping[str, Any]] = None, + comment: Optional[Any] = None, + **kwargs: Any, + ) -> _DocumentType: + """Finds a single document and replaces it, returning either the + original or the replaced document. + + The :meth:`find_one_and_replace` method differs from + :meth:`find_one_and_update` by replacing the document matched by + *filter*, rather than modifying the existing document. + + >>> for doc in db.test.find({}): + ... print(doc) + ... + {'x': 1, '_id': 0} + {'x': 1, '_id': 1} + {'x': 1, '_id': 2} + >>> db.test.find_one_and_replace({'x': 1}, {'y': 1}) + {'x': 1, '_id': 0} + >>> for doc in db.test.find({}): + ... print(doc) + ... + {'y': 1, '_id': 0} + {'x': 1, '_id': 1} + {'x': 1, '_id': 2} + + :param filter: A query that matches the document to replace. + :param replacement: The replacement document. + :param projection: A list of field names that should be + returned in the result document or a mapping specifying the fields + to include or exclude. If `projection` is a list "_id" will + always be returned. Use a mapping to exclude fields from + the result (e.g. projection={'_id': False}). + :param sort: a list of (key, direction) pairs + specifying the sort order for the query. If multiple documents + match the query, they are sorted and the first is replaced. + :param upsert: When ``True``, inserts a new document if no + document matches the query. Defaults to ``False``. + :param return_document: If + :attr:`ReturnDocument.BEFORE` (the default), + returns the original document before it was replaced, or ``None`` + if no document matches. If + :attr:`ReturnDocument.AFTER`, returns the replaced + or inserted document. + :param hint: An index to use to support the query + predicate specified either by its string name, or in the same + format as passed to + :meth:`~pymongo.collection.Collection.create_index` (e.g. + ``[('field', ASCENDING)]``). This option is only supported on + MongoDB 4.4 and above. + :param session: a + :class:`~pymongo.client_session.ClientSession`. + :param let: Map of parameter names and values. Values must be + constant or closed expressions that do not reference document + fields. Parameters can then be accessed as variables in an + aggregate expression context (e.g. "$$var"). + :param comment: A user-provided comment to attach to this + command. + :param kwargs: additional command arguments can be passed + as keyword arguments (for example maxTimeMS can be used with + recent server versions). + + .. versionchanged:: 4.1 + Added ``let`` parameter. + .. versionchanged:: 3.11 + Added the ``hint`` option. + .. versionchanged:: 3.6 + Added ``session`` parameter. + .. versionchanged:: 3.4 + Added the ``collation`` option. + .. versionchanged:: 3.2 + Respects write concern. + + .. warning:: Starting in PyMongo 3.2, this command uses the + :class:`~pymongo.write_concern.WriteConcern` of this + :class:`~pymongo.collection.Collection` when connected to MongoDB >= + 3.2. Note that using an elevated write concern with this command may + be slower compared to using the default write concern. + + .. versionadded:: 3.0 + """ + common.validate_ok_for_replace(replacement) + kwargs["update"] = replacement + if comment is not None: + kwargs["comment"] = comment + return self.__find_and_modify( + filter, + projection, + sort, + upsert, + return_document, + let=let, + hint=hint, + session=session, + **kwargs, + ) + + def find_one_and_update( + self, + filter: Mapping[str, Any], + update: Union[Mapping[str, Any], _Pipeline], + projection: Optional[Union[Mapping[str, Any], Iterable[str]]] = None, + sort: Optional[_IndexList] = None, + upsert: bool = False, + return_document: bool = ReturnDocument.BEFORE, + array_filters: Optional[Sequence[Mapping[str, Any]]] = None, + hint: Optional[_IndexKeyHint] = None, + session: Optional[ClientSession] = None, + let: Optional[Mapping[str, Any]] = None, + comment: Optional[Any] = None, + **kwargs: Any, + ) -> _DocumentType: + """Finds a single document and updates it, returning either the + original or the updated document. + + >>> db.test.find_one_and_update( + ... {'_id': 665}, {'$inc': {'count': 1}, '$set': {'done': True}}) + {'_id': 665, 'done': False, 'count': 25}} + + Returns ``None`` if no document matches the filter. + + >>> db.test.find_one_and_update( + ... {'_exists': False}, {'$inc': {'count': 1}}) + + When the filter matches, by default :meth:`find_one_and_update` + returns the original version of the document before the update was + applied. To return the updated (or inserted in the case of + *upsert*) version of the document instead, use the *return_document* + option. + + >>> from pymongo import ReturnDocument + >>> db.example.find_one_and_update( + ... {'_id': 'userid'}, + ... {'$inc': {'seq': 1}}, + ... return_document=ReturnDocument.AFTER) + {'_id': 'userid', 'seq': 1} + + You can limit the fields returned with the *projection* option. + + >>> db.example.find_one_and_update( + ... {'_id': 'userid'}, + ... {'$inc': {'seq': 1}}, + ... projection={'seq': True, '_id': False}, + ... return_document=ReturnDocument.AFTER) + {'seq': 2} + + The *upsert* option can be used to create the document if it doesn't + already exist. + + >>> db.example.delete_many({}).deleted_count + 1 + >>> db.example.find_one_and_update( + ... {'_id': 'userid'}, + ... {'$inc': {'seq': 1}}, + ... projection={'seq': True, '_id': False}, + ... upsert=True, + ... return_document=ReturnDocument.AFTER) + {'seq': 1} + + If multiple documents match *filter*, a *sort* can be applied. + + >>> for doc in db.test.find({'done': True}): + ... print(doc) + ... + {'_id': 665, 'done': True, 'result': {'count': 26}} + {'_id': 701, 'done': True, 'result': {'count': 17}} + >>> db.test.find_one_and_update( + ... {'done': True}, + ... {'$set': {'final': True}}, + ... sort=[('_id', pymongo.DESCENDING)]) + {'_id': 701, 'done': True, 'result': {'count': 17}} + + :param filter: A query that matches the document to update. + :param update: The update operations to apply. + :param projection: A list of field names that should be + returned in the result document or a mapping specifying the fields + to include or exclude. If `projection` is a list "_id" will + always be returned. Use a dict to exclude fields from + the result (e.g. projection={'_id': False}). + :param sort: a list of (key, direction) pairs + specifying the sort order for the query. If multiple documents + match the query, they are sorted and the first is updated. + :param upsert: When ``True``, inserts a new document if no + document matches the query. Defaults to ``False``. + :param return_document: If + :attr:`ReturnDocument.BEFORE` (the default), + returns the original document before it was updated. If + :attr:`ReturnDocument.AFTER`, returns the updated + or inserted document. + :param array_filters: A list of filters specifying which + array elements an update should apply. + :param hint: An index to use to support the query + predicate specified either by its string name, or in the same + format as passed to + :meth:`~pymongo.collection.Collection.create_index` (e.g. + ``[('field', ASCENDING)]``). This option is only supported on + MongoDB 4.4 and above. + :param session: a + :class:`~pymongo.client_session.ClientSession`. + :param let: Map of parameter names and values. Values must be + constant or closed expressions that do not reference document + fields. Parameters can then be accessed as variables in an + aggregate expression context (e.g. "$$var"). + :param comment: A user-provided comment to attach to this + command. + :param kwargs: additional command arguments can be passed + as keyword arguments (for example maxTimeMS can be used with + recent server versions). + + .. versionchanged:: 3.11 + Added the ``hint`` option. + .. versionchanged:: 3.9 + Added the ability to accept a pipeline as the ``update``. + .. versionchanged:: 3.6 + Added the ``array_filters`` and ``session`` options. + .. versionchanged:: 3.4 + Added the ``collation`` option. + .. versionchanged:: 3.2 + Respects write concern. + + .. warning:: Starting in PyMongo 3.2, this command uses the + :class:`~pymongo.write_concern.WriteConcern` of this + :class:`~pymongo.collection.Collection` when connected to MongoDB >= + 3.2. Note that using an elevated write concern with this command may + be slower compared to using the default write concern. + + .. versionadded:: 3.0 + """ + common.validate_ok_for_update(update) + common.validate_list_or_none("array_filters", array_filters) + kwargs["update"] = update + if comment is not None: + kwargs["comment"] = comment + return self.__find_and_modify( + filter, + projection, + sort, + upsert, + return_document, + array_filters, + hint=hint, + let=let, + session=session, + **kwargs, + ) + + # See PYTHON-3084. + __iter__ = None + + def __next__(self) -> NoReturn: + raise TypeError("'Collection' object is not iterable") + + next = __next__ + + def __call__(self, *args: Any, **kwargs: Any) -> NoReturn: + """This is only here so that some API misusages are easier to debug.""" + if "." not in self.__name: + raise TypeError( + "'Collection' object is not callable. If you " + "meant to call the '%s' method on a 'Database' " + "object it is failing because no such method " + "exists." % self.__name + ) + raise TypeError( + "'Collection' object is not callable. If you meant to " + "call the '%s' method on a 'Collection' object it is " + "failing because no such method exists." % self.__name.split(".")[-1] + ) diff --git a/venv/Lib/site-packages/pymongo/command_cursor.py b/venv/Lib/site-packages/pymongo/command_cursor.py new file mode 100644 index 00000000..0411a45a --- /dev/null +++ b/venv/Lib/site-packages/pymongo/command_cursor.py @@ -0,0 +1,401 @@ +# Copyright 2014-present MongoDB, 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. + +"""CommandCursor class to iterate over command results.""" +from __future__ import annotations + +from collections import deque +from typing import ( + TYPE_CHECKING, + Any, + Generic, + Iterator, + Mapping, + NoReturn, + Optional, + Sequence, + Union, +) + +from bson import CodecOptions, _convert_raw_document_lists_to_streams +from pymongo.cursor import _CURSOR_CLOSED_ERRORS, _ConnectionManager +from pymongo.errors import ConnectionFailure, InvalidOperation, OperationFailure +from pymongo.message import _CursorAddress, _GetMore, _OpMsg, _OpReply, _RawBatchGetMore +from pymongo.response import PinnedResponse +from pymongo.typings import _Address, _DocumentOut, _DocumentType + +if TYPE_CHECKING: + from pymongo.client_session import ClientSession + from pymongo.collection import Collection + from pymongo.pool import Connection + + +class CommandCursor(Generic[_DocumentType]): + """A cursor / iterator over command cursors.""" + + _getmore_class = _GetMore + + def __init__( + self, + collection: Collection[_DocumentType], + cursor_info: Mapping[str, Any], + address: Optional[_Address], + batch_size: int = 0, + max_await_time_ms: Optional[int] = None, + session: Optional[ClientSession] = None, + explicit_session: bool = False, + comment: Any = None, + ) -> None: + """Create a new command cursor.""" + self.__sock_mgr: Any = None + self.__collection: Collection[_DocumentType] = collection + self.__id = cursor_info["id"] + self.__data = deque(cursor_info["firstBatch"]) + self.__postbatchresumetoken: Optional[Mapping[str, Any]] = cursor_info.get( + "postBatchResumeToken" + ) + self.__address = address + self.__batch_size = batch_size + self.__max_await_time_ms = max_await_time_ms + self.__session = session + self.__explicit_session = explicit_session + self.__killed = self.__id == 0 + self.__comment = comment + if self.__killed: + self.__end_session(True) + + if "ns" in cursor_info: # noqa: SIM401 + self.__ns = cursor_info["ns"] + else: + self.__ns = collection.full_name + + self.batch_size(batch_size) + + if not isinstance(max_await_time_ms, int) and max_await_time_ms is not None: + raise TypeError("max_await_time_ms must be an integer or None") + + def __del__(self) -> None: + self.__die() + + def __die(self, synchronous: bool = False) -> None: + """Closes this cursor.""" + already_killed = self.__killed + self.__killed = True + if self.__id and not already_killed: + cursor_id = self.__id + assert self.__address is not None + address = _CursorAddress(self.__address, self.__ns) + else: + # Skip killCursors. + cursor_id = 0 + address = None + self.__collection.database.client._cleanup_cursor( + synchronous, + cursor_id, + address, + self.__sock_mgr, + self.__session, + self.__explicit_session, + ) + if not self.__explicit_session: + self.__session = None + self.__sock_mgr = None + + def __end_session(self, synchronous: bool) -> None: + if self.__session and not self.__explicit_session: + self.__session._end_session(lock=synchronous) + self.__session = None + + def close(self) -> None: + """Explicitly close / kill this cursor.""" + self.__die(True) + + def batch_size(self, batch_size: int) -> CommandCursor[_DocumentType]: + """Limits the number of documents returned in one batch. Each batch + requires a round trip to the server. It can be adjusted to optimize + performance and limit data transfer. + + .. note:: batch_size can not override MongoDB's internal limits on the + amount of data it will return to the client in a single batch (i.e + if you set batch size to 1,000,000,000, MongoDB will currently only + return 4-16MB of results per batch). + + Raises :exc:`TypeError` if `batch_size` is not an integer. + Raises :exc:`ValueError` if `batch_size` is less than ``0``. + + :param batch_size: The size of each batch of results requested. + """ + if not isinstance(batch_size, int): + raise TypeError("batch_size must be an integer") + if batch_size < 0: + raise ValueError("batch_size must be >= 0") + + self.__batch_size = batch_size == 1 and 2 or batch_size + return self + + def _has_next(self) -> bool: + """Returns `True` if the cursor has documents remaining from the + previous batch. + """ + return len(self.__data) > 0 + + @property + def _post_batch_resume_token(self) -> Optional[Mapping[str, Any]]: + """Retrieve the postBatchResumeToken from the response to a + changeStream aggregate or getMore. + """ + return self.__postbatchresumetoken + + def _maybe_pin_connection(self, conn: Connection) -> None: + client = self.__collection.database.client + if not client._should_pin_cursor(self.__session): + return + if not self.__sock_mgr: + conn.pin_cursor() + conn_mgr = _ConnectionManager(conn, False) + # Ensure the connection gets returned when the entire result is + # returned in the first batch. + if self.__id == 0: + conn_mgr.close() + else: + self.__sock_mgr = conn_mgr + + def __send_message(self, operation: _GetMore) -> None: + """Send a getmore message and handle the response.""" + client = self.__collection.database.client + try: + response = client._run_operation( + operation, self._unpack_response, address=self.__address + ) + except OperationFailure as exc: + if exc.code in _CURSOR_CLOSED_ERRORS: + # Don't send killCursors because the cursor is already closed. + self.__killed = True + if exc.timeout: + self.__die(False) + else: + # Return the session and pinned connection, if necessary. + self.close() + raise + except ConnectionFailure: + # Don't send killCursors because the cursor is already closed. + self.__killed = True + # Return the session and pinned connection, if necessary. + self.close() + raise + except Exception: + self.close() + raise + + if isinstance(response, PinnedResponse): + if not self.__sock_mgr: + self.__sock_mgr = _ConnectionManager(response.conn, response.more_to_come) + if response.from_command: + cursor = response.docs[0]["cursor"] + documents = cursor["nextBatch"] + self.__postbatchresumetoken = cursor.get("postBatchResumeToken") + self.__id = cursor["id"] + else: + documents = response.docs + assert isinstance(response.data, _OpReply) + self.__id = response.data.cursor_id + + if self.__id == 0: + self.close() + self.__data = deque(documents) + + def _unpack_response( + self, + response: Union[_OpReply, _OpMsg], + cursor_id: Optional[int], + codec_options: CodecOptions[Mapping[str, Any]], + user_fields: Optional[Mapping[str, Any]] = None, + legacy_response: bool = False, + ) -> Sequence[_DocumentOut]: + return response.unpack_response(cursor_id, codec_options, user_fields, legacy_response) + + def _refresh(self) -> int: + """Refreshes the cursor with more data from the server. + + Returns the length of self.__data after refresh. Will exit early if + self.__data is already non-empty. Raises OperationFailure when the + cursor cannot be refreshed due to an error on the query. + """ + if len(self.__data) or self.__killed: + return len(self.__data) + + if self.__id: # Get More + dbname, collname = self.__ns.split(".", 1) + read_pref = self.__collection._read_preference_for(self.session) + self.__send_message( + self._getmore_class( + dbname, + collname, + self.__batch_size, + self.__id, + self.__collection.codec_options, + read_pref, + self.__session, + self.__collection.database.client, + self.__max_await_time_ms, + self.__sock_mgr, + False, + self.__comment, + ) + ) + else: # Cursor id is zero nothing else to return + self.__die(True) + + return len(self.__data) + + @property + def alive(self) -> bool: + """Does this cursor have the potential to return more data? + + Even if :attr:`alive` is ``True``, :meth:`next` can raise + :exc:`StopIteration`. Best to use a for loop:: + + for doc in collection.aggregate(pipeline): + print(doc) + + .. note:: :attr:`alive` can be True while iterating a cursor from + a failed server. In this case :attr:`alive` will return False after + :meth:`next` fails to retrieve the next batch of results from the + server. + """ + return bool(len(self.__data) or (not self.__killed)) + + @property + def cursor_id(self) -> int: + """Returns the id of the cursor.""" + return self.__id + + @property + def address(self) -> Optional[_Address]: + """The (host, port) of the server used, or None. + + .. versionadded:: 3.0 + """ + return self.__address + + @property + def session(self) -> Optional[ClientSession]: + """The cursor's :class:`~pymongo.client_session.ClientSession`, or None. + + .. versionadded:: 3.6 + """ + if self.__explicit_session: + return self.__session + return None + + def __iter__(self) -> Iterator[_DocumentType]: + return self + + def next(self) -> _DocumentType: + """Advance the cursor.""" + # Block until a document is returnable. + while self.alive: + doc = self._try_next(True) + if doc is not None: + return doc + + raise StopIteration + + __next__ = next + + def _try_next(self, get_more_allowed: bool) -> Optional[_DocumentType]: + """Advance the cursor blocking for at most one getMore command.""" + if not len(self.__data) and not self.__killed and get_more_allowed: + self._refresh() + if len(self.__data): + return self.__data.popleft() + else: + return None + + def try_next(self) -> Optional[_DocumentType]: + """Advance the cursor without blocking indefinitely. + + This method returns the next document without waiting + indefinitely for data. + + If no document is cached locally then this method runs a single + getMore command. If the getMore yields any documents, the next + document is returned, otherwise, if the getMore returns no documents + (because there is no additional data) then ``None`` is returned. + + :return: The next document or ``None`` when no document is available + after running a single getMore or when the cursor is closed. + + .. versionadded:: 4.5 + """ + return self._try_next(get_more_allowed=True) + + def __enter__(self) -> CommandCursor[_DocumentType]: + return self + + def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None: + self.close() + + +class RawBatchCommandCursor(CommandCursor, Generic[_DocumentType]): + _getmore_class = _RawBatchGetMore + + def __init__( + self, + collection: Collection[_DocumentType], + cursor_info: Mapping[str, Any], + address: Optional[_Address], + batch_size: int = 0, + max_await_time_ms: Optional[int] = None, + session: Optional[ClientSession] = None, + explicit_session: bool = False, + comment: Any = None, + ) -> None: + """Create a new cursor / iterator over raw batches of BSON data. + + Should not be called directly by application developers - + see :meth:`~pymongo.collection.Collection.aggregate_raw_batches` + instead. + + .. seealso:: The MongoDB documentation on `cursors `_. + """ + assert not cursor_info.get("firstBatch") + super().__init__( + collection, + cursor_info, + address, + batch_size, + max_await_time_ms, + session, + explicit_session, + comment, + ) + + def _unpack_response( # type: ignore[override] + self, + response: Union[_OpReply, _OpMsg], + cursor_id: Optional[int], + codec_options: CodecOptions, + user_fields: Optional[Mapping[str, Any]] = None, + legacy_response: bool = False, + ) -> list[Mapping[str, Any]]: + raw_response = response.raw_response(cursor_id, user_fields=user_fields) + if not legacy_response: + # OP_MSG returns firstBatch/nextBatch documents as a BSON array + # Re-assemble the array of documents into a document stream + _convert_raw_document_lists_to_streams(raw_response[0]) + return raw_response # type: ignore[return-value] + + def __getitem__(self, index: int) -> NoReturn: + raise InvalidOperation("Cannot call __getitem__ on RawBatchCursor") diff --git a/venv/Lib/site-packages/pymongo/common.py b/venv/Lib/site-packages/pymongo/common.py new file mode 100644 index 00000000..7f1245b7 --- /dev/null +++ b/venv/Lib/site-packages/pymongo/common.py @@ -0,0 +1,1055 @@ +# Copyright 2011-present MongoDB, 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. + + +"""Functions and classes common to multiple pymongo modules.""" +from __future__ import annotations + +import datetime +import warnings +from collections import OrderedDict, abc +from difflib import get_close_matches +from typing import ( + TYPE_CHECKING, + Any, + Callable, + Iterator, + Mapping, + MutableMapping, + NoReturn, + Optional, + Sequence, + Type, + Union, + overload, +) +from urllib.parse import unquote_plus + +from bson import SON +from bson.binary import UuidRepresentation +from bson.codec_options import CodecOptions, DatetimeConversion, TypeRegistry +from bson.raw_bson import RawBSONDocument +from pymongo.auth import MECHANISMS +from pymongo.auth_oidc import OIDCCallback +from pymongo.compression_support import ( + validate_compressors, + validate_zlib_compression_level, +) +from pymongo.driver_info import DriverInfo +from pymongo.errors import ConfigurationError +from pymongo.monitoring import _validate_event_listeners +from pymongo.read_concern import ReadConcern +from pymongo.read_preferences import _MONGOS_MODES, _ServerMode +from pymongo.server_api import ServerApi +from pymongo.write_concern import DEFAULT_WRITE_CONCERN, WriteConcern, validate_boolean + +if TYPE_CHECKING: + from pymongo.client_session import ClientSession + +ORDERED_TYPES: Sequence[Type] = (SON, OrderedDict) + +# Defaults until we connect to a server and get updated limits. +MAX_BSON_SIZE = 16 * (1024**2) +MAX_MESSAGE_SIZE: int = 2 * MAX_BSON_SIZE +MIN_WIRE_VERSION = 0 +MAX_WIRE_VERSION = 0 +MAX_WRITE_BATCH_SIZE = 1000 + +# What this version of PyMongo supports. +MIN_SUPPORTED_SERVER_VERSION = "3.6" +MIN_SUPPORTED_WIRE_VERSION = 6 +MAX_SUPPORTED_WIRE_VERSION = 21 + +# Frequency to call hello on servers, in seconds. +HEARTBEAT_FREQUENCY = 10 + +# Frequency to clean up unclosed cursors, in seconds. +# See MongoClient._process_kill_cursors. +KILL_CURSOR_FREQUENCY = 1 + +# Frequency to process events queue, in seconds. +EVENTS_QUEUE_FREQUENCY = 1 + +# How long to wait, in seconds, for a suitable server to be found before +# aborting an operation. For example, if the client attempts an insert +# during a replica set election, SERVER_SELECTION_TIMEOUT governs the +# longest it is willing to wait for a new primary to be found. +SERVER_SELECTION_TIMEOUT = 30 + +# Spec requires at least 500ms between hello calls. +MIN_HEARTBEAT_INTERVAL = 0.5 + +# Spec requires at least 60s between SRV rescans. +MIN_SRV_RESCAN_INTERVAL = 60 + +# Default connectTimeout in seconds. +CONNECT_TIMEOUT = 20.0 + +# Default value for maxPoolSize. +MAX_POOL_SIZE = 100 + +# Default value for minPoolSize. +MIN_POOL_SIZE = 0 + +# The maximum number of concurrent connection creation attempts per pool. +MAX_CONNECTING = 2 + +# Default value for maxIdleTimeMS. +MAX_IDLE_TIME_MS: Optional[int] = None + +# Default value for maxIdleTimeMS in seconds. +MAX_IDLE_TIME_SEC: Optional[int] = None + +# Default value for waitQueueTimeoutMS in seconds. +WAIT_QUEUE_TIMEOUT: Optional[int] = None + +# Default value for localThresholdMS. +LOCAL_THRESHOLD_MS = 15 + +# Default value for retryWrites. +RETRY_WRITES = True + +# Default value for retryReads. +RETRY_READS = True + +# The error code returned when a command doesn't exist. +COMMAND_NOT_FOUND_CODES: Sequence[int] = (59,) + +# Error codes to ignore if GridFS calls createIndex on a secondary +UNAUTHORIZED_CODES: Sequence[int] = (13, 16547, 16548) + +# Maximum number of sessions to send in a single endSessions command. +# From the driver sessions spec. +_MAX_END_SESSIONS = 10000 + +# Default value for srvServiceName +SRV_SERVICE_NAME = "mongodb" + +# Default value for serverMonitoringMode +SERVER_MONITORING_MODE = "auto" # poll/stream/auto + + +def partition_node(node: str) -> tuple[str, int]: + """Split a host:port string into (host, int(port)) pair.""" + host = node + port = 27017 + idx = node.rfind(":") + if idx != -1: + host, port = node[:idx], int(node[idx + 1 :]) + if host.startswith("["): + host = host[1:-1] + return host, port + + +def clean_node(node: str) -> tuple[str, int]: + """Split and normalize a node name from a hello response.""" + host, port = partition_node(node) + + # Normalize hostname to lowercase, since DNS is case-insensitive: + # http://tools.ietf.org/html/rfc4343 + # This prevents useless rediscovery if "foo.com" is in the seed list but + # "FOO.com" is in the hello response. + return host.lower(), port + + +def raise_config_error(key: str, suggestions: Optional[list] = None) -> NoReturn: + """Raise ConfigurationError with the given key name.""" + msg = f"Unknown option: {key}." + if suggestions: + msg += f" Did you mean one of ({', '.join(suggestions)}) or maybe a camelCase version of one? Refer to docstring." + raise ConfigurationError(msg) + + +# Mapping of URI uuid representation options to valid subtypes. +_UUID_REPRESENTATIONS = { + "unspecified": UuidRepresentation.UNSPECIFIED, + "standard": UuidRepresentation.STANDARD, + "pythonLegacy": UuidRepresentation.PYTHON_LEGACY, + "javaLegacy": UuidRepresentation.JAVA_LEGACY, + "csharpLegacy": UuidRepresentation.CSHARP_LEGACY, +} + + +def validate_boolean_or_string(option: str, value: Any) -> bool: + """Validates that value is True, False, 'true', or 'false'.""" + if isinstance(value, str): + if value not in ("true", "false"): + raise ValueError(f"The value of {option} must be 'true' or 'false'") + return value == "true" + return validate_boolean(option, value) + + +def validate_integer(option: str, value: Any) -> int: + """Validates that 'value' is an integer (or basestring representation).""" + if isinstance(value, int): + return value + elif isinstance(value, str): + try: + return int(value) + except ValueError: + raise ValueError(f"The value of {option} must be an integer") from None + raise TypeError(f"Wrong type for {option}, value must be an integer") + + +def validate_positive_integer(option: str, value: Any) -> int: + """Validate that 'value' is a positive integer, which does not include 0.""" + val = validate_integer(option, value) + if val <= 0: + raise ValueError(f"The value of {option} must be a positive integer") + return val + + +def validate_non_negative_integer(option: str, value: Any) -> int: + """Validate that 'value' is a positive integer or 0.""" + val = validate_integer(option, value) + if val < 0: + raise ValueError(f"The value of {option} must be a non negative integer") + return val + + +def validate_readable(option: str, value: Any) -> Optional[str]: + """Validates that 'value' is file-like and readable.""" + if value is None: + return value + # First make sure its a string py3.3 open(True, 'r') succeeds + # Used in ssl cert checking due to poor ssl module error reporting + value = validate_string(option, value) + open(value).close() + return value + + +def validate_positive_integer_or_none(option: str, value: Any) -> Optional[int]: + """Validate that 'value' is a positive integer or None.""" + if value is None: + return value + return validate_positive_integer(option, value) + + +def validate_non_negative_integer_or_none(option: str, value: Any) -> Optional[int]: + """Validate that 'value' is a positive integer or 0 or None.""" + if value is None: + return value + return validate_non_negative_integer(option, value) + + +def validate_string(option: str, value: Any) -> str: + """Validates that 'value' is an instance of `str`.""" + if isinstance(value, str): + return value + raise TypeError(f"Wrong type for {option}, value must be an instance of str") + + +def validate_string_or_none(option: str, value: Any) -> Optional[str]: + """Validates that 'value' is an instance of `basestring` or `None`.""" + if value is None: + return value + return validate_string(option, value) + + +def validate_int_or_basestring(option: str, value: Any) -> Union[int, str]: + """Validates that 'value' is an integer or string.""" + if isinstance(value, int): + return value + elif isinstance(value, str): + try: + return int(value) + except ValueError: + return value + raise TypeError(f"Wrong type for {option}, value must be an integer or a string") + + +def validate_non_negative_int_or_basestring(option: Any, value: Any) -> Union[int, str]: + """Validates that 'value' is an integer or string.""" + if isinstance(value, int): + return value + elif isinstance(value, str): + try: + val = int(value) + except ValueError: + return value + return validate_non_negative_integer(option, val) + raise TypeError(f"Wrong type for {option}, value must be an non negative integer or a string") + + +def validate_positive_float(option: str, value: Any) -> float: + """Validates that 'value' is a float, or can be converted to one, and is + positive. + """ + errmsg = f"{option} must be an integer or float" + try: + value = float(value) + except ValueError: + raise ValueError(errmsg) from None + except TypeError: + raise TypeError(errmsg) from None + + # float('inf') doesn't work in 2.4 or 2.5 on Windows, so just cap floats at + # one billion - this is a reasonable approximation for infinity + if not 0 < value < 1e9: + raise ValueError(f"{option} must be greater than 0 and less than one billion") + return value + + +def validate_positive_float_or_zero(option: str, value: Any) -> float: + """Validates that 'value' is 0 or a positive float, or can be converted to + 0 or a positive float. + """ + if value == 0 or value == "0": + return 0 + return validate_positive_float(option, value) + + +def validate_timeout_or_none(option: str, value: Any) -> Optional[float]: + """Validates a timeout specified in milliseconds returning + a value in floating point seconds. + """ + if value is None: + return value + return validate_positive_float(option, value) / 1000.0 + + +def validate_timeout_or_zero(option: str, value: Any) -> float: + """Validates a timeout specified in milliseconds returning + a value in floating point seconds for the case where None is an error + and 0 is valid. Setting the timeout to nothing in the URI string is a + config error. + """ + if value is None: + raise ConfigurationError(f"{option} cannot be None") + if value == 0 or value == "0": + return 0 + return validate_positive_float(option, value) / 1000.0 + + +def validate_timeout_or_none_or_zero(option: Any, value: Any) -> Optional[float]: + """Validates a timeout specified in milliseconds returning + a value in floating point seconds. value=0 and value="0" are treated the + same as value=None which means unlimited timeout. + """ + if value is None or value == 0 or value == "0": + return None + return validate_positive_float(option, value) / 1000.0 + + +def validate_timeoutms(option: Any, value: Any) -> Optional[float]: + """Validates a timeout specified in milliseconds returning + a value in floating point seconds. + """ + if value is None: + return None + return validate_positive_float_or_zero(option, value) / 1000.0 + + +def validate_max_staleness(option: str, value: Any) -> int: + """Validates maxStalenessSeconds according to the Max Staleness Spec.""" + if value == -1 or value == "-1": + # Default: No maximum staleness. + return -1 + return validate_positive_integer(option, value) + + +def validate_read_preference(dummy: Any, value: Any) -> _ServerMode: + """Validate a read preference.""" + if not isinstance(value, _ServerMode): + raise TypeError(f"{value!r} is not a read preference.") + return value + + +def validate_read_preference_mode(dummy: Any, value: Any) -> _ServerMode: + """Validate read preference mode for a MongoClient. + + .. versionchanged:: 3.5 + Returns the original ``value`` instead of the validated read preference + mode. + """ + if value not in _MONGOS_MODES: + raise ValueError(f"{value} is not a valid read preference") + return value + + +def validate_auth_mechanism(option: str, value: Any) -> str: + """Validate the authMechanism URI option.""" + if value not in MECHANISMS: + raise ValueError(f"{option} must be in {tuple(MECHANISMS)}") + return value + + +def validate_uuid_representation(dummy: Any, value: Any) -> int: + """Validate the uuid representation option selected in the URI.""" + try: + return _UUID_REPRESENTATIONS[value] + except KeyError: + raise ValueError( + f"{value} is an invalid UUID representation. " + "Must be one of " + f"{tuple(_UUID_REPRESENTATIONS)}" + ) from None + + +def validate_read_preference_tags(name: str, value: Any) -> list[dict[str, str]]: + """Parse readPreferenceTags if passed as a client kwarg.""" + if not isinstance(value, list): + value = [value] + + tag_sets: list = [] + for tag_set in value: + if tag_set == "": + tag_sets.append({}) + continue + try: + tags = {} + for tag in tag_set.split(","): + key, val = tag.split(":") + tags[unquote_plus(key)] = unquote_plus(val) + tag_sets.append(tags) + except Exception: + raise ValueError(f"{tag_set!r} not a valid value for {name}") from None + return tag_sets + + +_MECHANISM_PROPS = frozenset( + [ + "SERVICE_NAME", + "CANONICALIZE_HOST_NAME", + "SERVICE_REALM", + "AWS_SESSION_TOKEN", + "ENVIRONMENT", + "TOKEN_RESOURCE", + ] +) + + +def validate_auth_mechanism_properties(option: str, value: Any) -> dict[str, Union[bool, str]]: + """Validate authMechanismProperties.""" + props: dict[str, Any] = {} + if not isinstance(value, str): + if not isinstance(value, dict): + raise ValueError("Auth mechanism properties must be given as a string or a dictionary") + for key, value in value.items(): # noqa: B020 + if isinstance(value, str): + props[key] = value + elif isinstance(value, bool): + props[key] = str(value).lower() + elif key in ["ALLOWED_HOSTS"] and isinstance(value, list): + props[key] = value + elif key in ["OIDC_CALLBACK", "OIDC_HUMAN_CALLBACK"]: + if not isinstance(value, OIDCCallback): + raise ValueError("callback must be an OIDCCallback object") + props[key] = value + else: + raise ValueError(f"Invalid type for auth mechanism property {key}, {type(value)}") + return props + + value = validate_string(option, value) + for opt in value.split(","): + key, _, val = opt.partition(":") + if key not in _MECHANISM_PROPS: + # Try not to leak the token. + if "AWS_SESSION_TOKEN" in key: + raise ValueError( + "auth mechanism properties must be " + "key:value pairs like AWS_SESSION_TOKEN:" + ) + + raise ValueError( + f"{key} is not a supported auth " + "mechanism property. Must be one of " + f"{tuple(_MECHANISM_PROPS)}." + ) + + if key == "CANONICALIZE_HOST_NAME": + props[key] = validate_boolean_or_string(key, val) + else: + props[key] = unquote_plus(val) + + return props + + +def validate_document_class( + option: str, value: Any +) -> Union[Type[MutableMapping], Type[RawBSONDocument]]: + """Validate the document_class option.""" + # issubclass can raise TypeError for generic aliases like SON[str, Any]. + # In that case we can use the base class for the comparison. + is_mapping = False + try: + is_mapping = issubclass(value, abc.MutableMapping) + except TypeError: + if hasattr(value, "__origin__"): + is_mapping = issubclass(value.__origin__, abc.MutableMapping) + if not is_mapping and not issubclass(value, RawBSONDocument): + raise TypeError( + f"{option} must be dict, bson.son.SON, " + "bson.raw_bson.RawBSONDocument, or a " + "subclass of collections.MutableMapping" + ) + return value + + +def validate_type_registry(option: Any, value: Any) -> Optional[TypeRegistry]: + """Validate the type_registry option.""" + if value is not None and not isinstance(value, TypeRegistry): + raise TypeError(f"{option} must be an instance of {TypeRegistry}") + return value + + +def validate_list(option: str, value: Any) -> list: + """Validates that 'value' is a list.""" + if not isinstance(value, list): + raise TypeError(f"{option} must be a list") + return value + + +def validate_list_or_none(option: Any, value: Any) -> Optional[list]: + """Validates that 'value' is a list or None.""" + if value is None: + return value + return validate_list(option, value) + + +def validate_list_or_mapping(option: Any, value: Any) -> None: + """Validates that 'value' is a list or a document.""" + if not isinstance(value, (abc.Mapping, list)): + raise TypeError( + f"{option} must either be a list or an instance of dict, " + "bson.son.SON, or any other type that inherits from " + "collections.Mapping" + ) + + +def validate_is_mapping(option: str, value: Any) -> None: + """Validate the type of method arguments that expect a document.""" + if not isinstance(value, abc.Mapping): + raise TypeError( + f"{option} must be an instance of dict, bson.son.SON, or " + "any other type that inherits from " + "collections.Mapping" + ) + + +def validate_is_document_type(option: str, value: Any) -> None: + """Validate the type of method arguments that expect a MongoDB document.""" + if not isinstance(value, (abc.MutableMapping, RawBSONDocument)): + raise TypeError( + f"{option} must be an instance of dict, bson.son.SON, " + "bson.raw_bson.RawBSONDocument, or " + "a type that inherits from " + "collections.MutableMapping" + ) + + +def validate_appname_or_none(option: str, value: Any) -> Optional[str]: + """Validate the appname option.""" + if value is None: + return value + validate_string(option, value) + # We need length in bytes, so encode utf8 first. + if len(value.encode("utf-8")) > 128: + raise ValueError(f"{option} must be <= 128 bytes") + return value + + +def validate_driver_or_none(option: Any, value: Any) -> Optional[DriverInfo]: + """Validate the driver keyword arg.""" + if value is None: + return value + if not isinstance(value, DriverInfo): + raise TypeError(f"{option} must be an instance of DriverInfo") + return value + + +def validate_server_api_or_none(option: Any, value: Any) -> Optional[ServerApi]: + """Validate the server_api keyword arg.""" + if value is None: + return value + if not isinstance(value, ServerApi): + raise TypeError(f"{option} must be an instance of ServerApi") + return value + + +def validate_is_callable_or_none(option: Any, value: Any) -> Optional[Callable]: + """Validates that 'value' is a callable.""" + if value is None: + return value + if not callable(value): + raise ValueError(f"{option} must be a callable") + return value + + +def validate_ok_for_replace(replacement: Mapping[str, Any]) -> None: + """Validate a replacement document.""" + validate_is_mapping("replacement", replacement) + # Replacement can be {} + if replacement and not isinstance(replacement, RawBSONDocument): + first = next(iter(replacement)) + if first.startswith("$"): + raise ValueError("replacement can not include $ operators") + + +def validate_ok_for_update(update: Any) -> None: + """Validate an update document.""" + validate_list_or_mapping("update", update) + # Update cannot be {}. + if not update: + raise ValueError("update cannot be empty") + + is_document = not isinstance(update, list) + first = next(iter(update)) + if is_document and not first.startswith("$"): + raise ValueError("update only works with $ operators") + + +_UNICODE_DECODE_ERROR_HANDLERS = frozenset(["strict", "replace", "ignore"]) + + +def validate_unicode_decode_error_handler(dummy: Any, value: str) -> str: + """Validate the Unicode decode error handler option of CodecOptions.""" + if value not in _UNICODE_DECODE_ERROR_HANDLERS: + raise ValueError( + f"{value} is an invalid Unicode decode error handler. " + "Must be one of " + f"{tuple(_UNICODE_DECODE_ERROR_HANDLERS)}" + ) + return value + + +def validate_tzinfo(dummy: Any, value: Any) -> Optional[datetime.tzinfo]: + """Validate the tzinfo option""" + if value is not None and not isinstance(value, datetime.tzinfo): + raise TypeError("%s must be an instance of datetime.tzinfo" % value) + return value + + +def validate_auto_encryption_opts_or_none(option: Any, value: Any) -> Optional[Any]: + """Validate the driver keyword arg.""" + if value is None: + return value + from pymongo.encryption_options import AutoEncryptionOpts + + if not isinstance(value, AutoEncryptionOpts): + raise TypeError(f"{option} must be an instance of AutoEncryptionOpts") + + return value + + +def validate_datetime_conversion(option: Any, value: Any) -> Optional[DatetimeConversion]: + """Validate a DatetimeConversion string.""" + if value is None: + return DatetimeConversion.DATETIME + + if isinstance(value, str): + if value.isdigit(): + return DatetimeConversion(int(value)) + return DatetimeConversion[value] + elif isinstance(value, int): + return DatetimeConversion(value) + + raise TypeError(f"{option} must be a str or int representing DatetimeConversion") + + +def validate_server_monitoring_mode(option: str, value: str) -> str: + """Validate the serverMonitoringMode option.""" + if value not in {"auto", "stream", "poll"}: + raise ValueError( + f'{option}={value!r} is invalid. Must be one of "auto", "stream", or "poll"' + ) + return value + + +# Dictionary where keys are the names of public URI options, and values +# are lists of aliases for that option. +URI_OPTIONS_ALIAS_MAP: dict[str, list[str]] = { + "tls": ["ssl"], +} + +# Dictionary where keys are the names of URI options, and values +# are functions that validate user-input values for that option. If an option +# alias uses a different validator than its public counterpart, it should be +# included here as a key, value pair. +URI_OPTIONS_VALIDATOR_MAP: dict[str, Callable[[Any, Any], Any]] = { + "appname": validate_appname_or_none, + "authmechanism": validate_auth_mechanism, + "authmechanismproperties": validate_auth_mechanism_properties, + "authsource": validate_string, + "compressors": validate_compressors, + "connecttimeoutms": validate_timeout_or_none_or_zero, + "directconnection": validate_boolean_or_string, + "heartbeatfrequencyms": validate_timeout_or_none, + "journal": validate_boolean_or_string, + "localthresholdms": validate_positive_float_or_zero, + "maxidletimems": validate_timeout_or_none, + "maxconnecting": validate_positive_integer, + "maxpoolsize": validate_non_negative_integer_or_none, + "maxstalenessseconds": validate_max_staleness, + "readconcernlevel": validate_string_or_none, + "readpreference": validate_read_preference_mode, + "readpreferencetags": validate_read_preference_tags, + "replicaset": validate_string_or_none, + "retryreads": validate_boolean_or_string, + "retrywrites": validate_boolean_or_string, + "loadbalanced": validate_boolean_or_string, + "serverselectiontimeoutms": validate_timeout_or_zero, + "sockettimeoutms": validate_timeout_or_none_or_zero, + "tls": validate_boolean_or_string, + "tlsallowinvalidcertificates": validate_boolean_or_string, + "tlsallowinvalidhostnames": validate_boolean_or_string, + "tlscafile": validate_readable, + "tlscertificatekeyfile": validate_readable, + "tlscertificatekeyfilepassword": validate_string_or_none, + "tlsdisableocspendpointcheck": validate_boolean_or_string, + "tlsinsecure": validate_boolean_or_string, + "w": validate_non_negative_int_or_basestring, + "wtimeoutms": validate_non_negative_integer, + "zlibcompressionlevel": validate_zlib_compression_level, + "srvservicename": validate_string, + "srvmaxhosts": validate_non_negative_integer, + "timeoutms": validate_timeoutms, + "servermonitoringmode": validate_server_monitoring_mode, +} + +# Dictionary where keys are the names of URI options specific to pymongo, +# and values are functions that validate user-input values for those options. +NONSPEC_OPTIONS_VALIDATOR_MAP: dict[str, Callable[[Any, Any], Any]] = { + "connect": validate_boolean_or_string, + "driver": validate_driver_or_none, + "server_api": validate_server_api_or_none, + "fsync": validate_boolean_or_string, + "minpoolsize": validate_non_negative_integer, + "tlscrlfile": validate_readable, + "tz_aware": validate_boolean_or_string, + "unicode_decode_error_handler": validate_unicode_decode_error_handler, + "uuidrepresentation": validate_uuid_representation, + "waitqueuemultiple": validate_non_negative_integer_or_none, + "waitqueuetimeoutms": validate_timeout_or_none, + "datetime_conversion": validate_datetime_conversion, +} + +# Dictionary where keys are the names of keyword-only options for the +# MongoClient constructor, and values are functions that validate user-input +# values for those options. +KW_VALIDATORS: dict[str, Callable[[Any, Any], Any]] = { + "document_class": validate_document_class, + "type_registry": validate_type_registry, + "read_preference": validate_read_preference, + "event_listeners": _validate_event_listeners, + "tzinfo": validate_tzinfo, + "username": validate_string_or_none, + "password": validate_string_or_none, + "server_selector": validate_is_callable_or_none, + "auto_encryption_opts": validate_auto_encryption_opts_or_none, + "authoidcallowedhosts": validate_list, +} + +# Dictionary where keys are any URI option name, and values are the +# internally-used names of that URI option. Options with only one name +# variant need not be included here. Options whose public and internal +# names are the same need not be included here. +INTERNAL_URI_OPTION_NAME_MAP: dict[str, str] = { + "ssl": "tls", +} + +# Map from deprecated URI option names to a tuple indicating the method of +# their deprecation and any additional information that may be needed to +# construct the warning message. +URI_OPTIONS_DEPRECATION_MAP: dict[str, tuple[str, str]] = { + # format: : (, ), + # Supported values: + # - 'renamed': should be the new option name. Note that case is + # preserved for renamed options as they are part of user warnings. + # - 'removed': may suggest the rationale for deprecating the + # option and/or recommend remedial action. + # For example: + # 'wtimeout': ('renamed', 'wTimeoutMS'), +} + +# Augment the option validator map with pymongo-specific option information. +URI_OPTIONS_VALIDATOR_MAP.update(NONSPEC_OPTIONS_VALIDATOR_MAP) +for optname, aliases in URI_OPTIONS_ALIAS_MAP.items(): + for alias in aliases: + if alias not in URI_OPTIONS_VALIDATOR_MAP: + URI_OPTIONS_VALIDATOR_MAP[alias] = URI_OPTIONS_VALIDATOR_MAP[optname] + +# Map containing all URI option and keyword argument validators. +VALIDATORS: dict[str, Callable[[Any, Any], Any]] = URI_OPTIONS_VALIDATOR_MAP.copy() +VALIDATORS.update(KW_VALIDATORS) + +# List of timeout-related options. +TIMEOUT_OPTIONS: list[str] = [ + "connecttimeoutms", + "heartbeatfrequencyms", + "maxidletimems", + "maxstalenessseconds", + "serverselectiontimeoutms", + "sockettimeoutms", + "waitqueuetimeoutms", +] + +_AUTH_OPTIONS = frozenset(["authmechanismproperties"]) + + +def validate_auth_option(option: str, value: Any) -> tuple[str, Any]: + """Validate optional authentication parameters.""" + lower, value = validate(option, value) + if lower not in _AUTH_OPTIONS: + raise ConfigurationError(f"Unknown option: {option}. Must be in {_AUTH_OPTIONS}") + return option, value + + +def _get_validator( + key: str, validators: dict[str, Callable[[Any, Any], Any]], normed_key: Optional[str] = None +) -> Callable: + normed_key = normed_key or key + try: + return validators[normed_key] + except KeyError: + suggestions = get_close_matches(normed_key, validators, cutoff=0.2) + raise_config_error(key, suggestions) + + +def validate(option: str, value: Any) -> tuple[str, Any]: + """Generic validation function.""" + validator = _get_validator(option, VALIDATORS, normed_key=option.lower()) + value = validator(option, value) + return option, value + + +def get_validated_options( + options: Mapping[str, Any], warn: bool = True +) -> MutableMapping[str, Any]: + """Validate each entry in options and raise a warning if it is not valid. + Returns a copy of options with invalid entries removed. + + :param opts: A dict containing MongoDB URI options. + :param warn: If ``True`` then warnings will be logged and + invalid options will be ignored. Otherwise, invalid options will + cause errors. + """ + validated_options: MutableMapping[str, Any] + if isinstance(options, _CaseInsensitiveDictionary): + validated_options = _CaseInsensitiveDictionary() + + def get_normed_key(x: str) -> str: + return x + + def get_setter_key(x: str) -> str: + return options.cased_key(x) # type: ignore[attr-defined] + + else: + validated_options = {} + + def get_normed_key(x: str) -> str: + return x.lower() + + def get_setter_key(x: str) -> str: + return x + + for opt, value in options.items(): + normed_key = get_normed_key(opt) + try: + validator = _get_validator(opt, URI_OPTIONS_VALIDATOR_MAP, normed_key=normed_key) + validated = validator(opt, value) + except (ValueError, TypeError, ConfigurationError) as exc: + if warn: + warnings.warn(str(exc), stacklevel=2) + else: + raise + else: + validated_options[get_setter_key(normed_key)] = validated + return validated_options + + +def _esc_coll_name(encrypted_fields: Mapping[str, Any], name: str) -> Any: + return encrypted_fields.get("escCollection", f"enxcol_.{name}.esc") + + +def _ecoc_coll_name(encrypted_fields: Mapping[str, Any], name: str) -> Any: + return encrypted_fields.get("ecocCollection", f"enxcol_.{name}.ecoc") + + +# List of write-concern-related options. +WRITE_CONCERN_OPTIONS = frozenset(["w", "wtimeout", "wtimeoutms", "fsync", "j", "journal"]) + + +class BaseObject: + """A base class that provides attributes and methods common + to multiple pymongo classes. + + SHOULD NOT BE USED BY DEVELOPERS EXTERNAL TO MONGODB. + """ + + def __init__( + self, + codec_options: CodecOptions, + read_preference: _ServerMode, + write_concern: WriteConcern, + read_concern: ReadConcern, + ) -> None: + if not isinstance(codec_options, CodecOptions): + raise TypeError("codec_options must be an instance of bson.codec_options.CodecOptions") + self.__codec_options = codec_options + + if not isinstance(read_preference, _ServerMode): + raise TypeError( + f"{read_preference!r} is not valid for read_preference. See " + "pymongo.read_preferences for valid " + "options." + ) + self.__read_preference = read_preference + + if not isinstance(write_concern, WriteConcern): + raise TypeError( + "write_concern must be an instance of pymongo.write_concern.WriteConcern" + ) + self.__write_concern = write_concern + + if not isinstance(read_concern, ReadConcern): + raise TypeError("read_concern must be an instance of pymongo.read_concern.ReadConcern") + self.__read_concern = read_concern + + @property + def codec_options(self) -> CodecOptions: + """Read only access to the :class:`~bson.codec_options.CodecOptions` + of this instance. + """ + return self.__codec_options + + @property + def write_concern(self) -> WriteConcern: + """Read only access to the :class:`~pymongo.write_concern.WriteConcern` + of this instance. + + .. versionchanged:: 3.0 + The :attr:`write_concern` attribute is now read only. + """ + return self.__write_concern + + def _write_concern_for(self, session: Optional[ClientSession]) -> WriteConcern: + """Read only access to the write concern of this instance or session.""" + # Override this operation's write concern with the transaction's. + if session and session.in_transaction: + return DEFAULT_WRITE_CONCERN + return self.write_concern + + @property + def read_preference(self) -> _ServerMode: + """Read only access to the read preference of this instance. + + .. versionchanged:: 3.0 + The :attr:`read_preference` attribute is now read only. + """ + return self.__read_preference + + def _read_preference_for(self, session: Optional[ClientSession]) -> _ServerMode: + """Read only access to the read preference of this instance or session.""" + # Override this operation's read preference with the transaction's. + if session: + return session._txn_read_preference() or self.__read_preference + return self.__read_preference + + @property + def read_concern(self) -> ReadConcern: + """Read only access to the :class:`~pymongo.read_concern.ReadConcern` + of this instance. + + .. versionadded:: 3.2 + """ + return self.__read_concern + + +class _CaseInsensitiveDictionary(MutableMapping[str, Any]): + def __init__(self, *args: Any, **kwargs: Any): + self.__casedkeys: dict[str, Any] = {} + self.__data: dict[str, Any] = {} + self.update(dict(*args, **kwargs)) + + def __contains__(self, key: str) -> bool: # type: ignore[override] + return key.lower() in self.__data + + def __len__(self) -> int: + return len(self.__data) + + def __iter__(self) -> Iterator[str]: + return (key for key in self.__casedkeys) + + def __repr__(self) -> str: + return str({self.__casedkeys[k]: self.__data[k] for k in self}) + + def __setitem__(self, key: str, value: Any) -> None: + lc_key = key.lower() + self.__casedkeys[lc_key] = key + self.__data[lc_key] = value + + def __getitem__(self, key: str) -> Any: + return self.__data[key.lower()] + + def __delitem__(self, key: str) -> None: + lc_key = key.lower() + del self.__casedkeys[lc_key] + del self.__data[lc_key] + + def __eq__(self, other: Any) -> bool: + if not isinstance(other, abc.Mapping): + return NotImplemented + if len(self) != len(other): + return False + for key in other: # noqa: SIM110 + if self[key] != other[key]: + return False + + return True + + def get(self, key: str, default: Optional[Any] = None) -> Any: + return self.__data.get(key.lower(), default) + + def pop(self, key: str, *args: Any, **kwargs: Any) -> Any: + lc_key = key.lower() + self.__casedkeys.pop(lc_key, None) + return self.__data.pop(lc_key, *args, **kwargs) + + def popitem(self) -> tuple[str, Any]: + lc_key, cased_key = self.__casedkeys.popitem() + value = self.__data.pop(lc_key) + return cased_key, value + + def clear(self) -> None: + self.__casedkeys.clear() + self.__data.clear() + + @overload + def setdefault(self, key: str, default: None = None) -> Optional[Any]: + ... + + @overload + def setdefault(self, key: str, default: Any) -> Any: + ... + + def setdefault(self, key: str, default: Optional[Any] = None) -> Optional[Any]: + lc_key = key.lower() + if key in self: + return self.__data[lc_key] + else: + self.__casedkeys[lc_key] = key + self.__data[lc_key] = default + return default + + def update(self, other: Mapping[str, Any]) -> None: # type: ignore[override] + if isinstance(other, _CaseInsensitiveDictionary): + for key in other: + self[other.cased_key(key)] = other[key] + else: + for key in other: + self[key] = other[key] + + def cased_key(self, key: str) -> Any: + return self.__casedkeys[key.lower()] diff --git a/venv/Lib/site-packages/pymongo/compression_support.py b/venv/Lib/site-packages/pymongo/compression_support.py new file mode 100644 index 00000000..7daad210 --- /dev/null +++ b/venv/Lib/site-packages/pymongo/compression_support.py @@ -0,0 +1,157 @@ +# Copyright 2018 MongoDB, 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. +from __future__ import annotations + +import warnings +from typing import Any, Iterable, Optional, Union + +from pymongo._lazy_import import lazy_import +from pymongo.hello import HelloCompat +from pymongo.monitoring import _SENSITIVE_COMMANDS + +try: + snappy = lazy_import("snappy") + _HAVE_SNAPPY = True +except ImportError: + # python-snappy isn't available. + _HAVE_SNAPPY = False + +try: + zlib = lazy_import("zlib") + + _HAVE_ZLIB = True +except ImportError: + # Python built without zlib support. + _HAVE_ZLIB = False + +try: + zstandard = lazy_import("zstandard") + _HAVE_ZSTD = True +except ImportError: + _HAVE_ZSTD = False + +_SUPPORTED_COMPRESSORS = {"snappy", "zlib", "zstd"} +_NO_COMPRESSION = {HelloCompat.CMD, HelloCompat.LEGACY_CMD} +_NO_COMPRESSION.update(_SENSITIVE_COMMANDS) + + +def validate_compressors(dummy: Any, value: Union[str, Iterable[str]]) -> list[str]: + try: + # `value` is string. + compressors = value.split(",") # type: ignore[union-attr] + except AttributeError: + # `value` is an iterable. + compressors = list(value) + + for compressor in compressors[:]: + if compressor not in _SUPPORTED_COMPRESSORS: + compressors.remove(compressor) + warnings.warn(f"Unsupported compressor: {compressor}", stacklevel=2) + elif compressor == "snappy" and not _HAVE_SNAPPY: + compressors.remove(compressor) + warnings.warn( + "Wire protocol compression with snappy is not available. " + "You must install the python-snappy module for snappy support.", + stacklevel=2, + ) + elif compressor == "zlib" and not _HAVE_ZLIB: + compressors.remove(compressor) + warnings.warn( + "Wire protocol compression with zlib is not available. " + "The zlib module is not available.", + stacklevel=2, + ) + elif compressor == "zstd" and not _HAVE_ZSTD: + compressors.remove(compressor) + warnings.warn( + "Wire protocol compression with zstandard is not available. " + "You must install the zstandard module for zstandard support.", + stacklevel=2, + ) + return compressors + + +def validate_zlib_compression_level(option: str, value: Any) -> int: + try: + level = int(value) + except Exception: + raise TypeError(f"{option} must be an integer, not {value!r}.") from None + if level < -1 or level > 9: + raise ValueError("%s must be between -1 and 9, not %d." % (option, level)) + return level + + +class CompressionSettings: + def __init__(self, compressors: list[str], zlib_compression_level: int): + self.compressors = compressors + self.zlib_compression_level = zlib_compression_level + + def get_compression_context( + self, compressors: Optional[list[str]] + ) -> Union[SnappyContext, ZlibContext, ZstdContext, None]: + if compressors: + chosen = compressors[0] + if chosen == "snappy": + return SnappyContext() + elif chosen == "zlib": + return ZlibContext(self.zlib_compression_level) + elif chosen == "zstd": + return ZstdContext() + return None + return None + + +class SnappyContext: + compressor_id = 1 + + @staticmethod + def compress(data: bytes) -> bytes: + return snappy.compress(data) + + +class ZlibContext: + compressor_id = 2 + + def __init__(self, level: int): + self.level = level + + def compress(self, data: bytes) -> bytes: + return zlib.compress(data, self.level) + + +class ZstdContext: + compressor_id = 3 + + @staticmethod + def compress(data: bytes) -> bytes: + # ZstdCompressor is not thread safe. + # TODO: Use a pool? + return zstandard.ZstdCompressor().compress(data) + + +def decompress(data: bytes, compressor_id: int) -> bytes: + if compressor_id == SnappyContext.compressor_id: + # python-snappy doesn't support the buffer interface. + # https://github.com/andrix/python-snappy/issues/65 + # This only matters when data is a memoryview since + # id(bytes(data)) == id(data) when data is a bytes. + return snappy.uncompress(bytes(data)) + elif compressor_id == ZlibContext.compressor_id: + return zlib.decompress(data) + elif compressor_id == ZstdContext.compressor_id: + # ZstdDecompressor is not thread safe. + # TODO: Use a pool? + return zstandard.ZstdDecompressor().decompress(data) + else: + raise ValueError("Unknown compressorId %d" % (compressor_id,)) diff --git a/venv/Lib/site-packages/pymongo/cursor.py b/venv/Lib/site-packages/pymongo/cursor.py new file mode 100644 index 00000000..3151fcaf --- /dev/null +++ b/venv/Lib/site-packages/pymongo/cursor.py @@ -0,0 +1,1357 @@ +# Copyright 2009-present MongoDB, 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. + +"""Cursor class to iterate over Mongo query results.""" +from __future__ import annotations + +import copy +import warnings +from collections import deque +from typing import ( + TYPE_CHECKING, + Any, + Generic, + Iterable, + List, + Mapping, + NoReturn, + Optional, + Sequence, + Tuple, + Union, + cast, + overload, +) + +from bson import RE_TYPE, _convert_raw_document_lists_to_streams +from bson.code import Code +from bson.son import SON +from pymongo import helpers +from pymongo.collation import validate_collation_or_none +from pymongo.common import ( + validate_is_document_type, + validate_is_mapping, +) +from pymongo.errors import ConnectionFailure, InvalidOperation, OperationFailure +from pymongo.lock import _create_lock +from pymongo.message import ( + _CursorAddress, + _GetMore, + _OpMsg, + _OpReply, + _Query, + _RawBatchGetMore, + _RawBatchQuery, +) +from pymongo.response import PinnedResponse +from pymongo.typings import _Address, _CollationIn, _DocumentOut, _DocumentType +from pymongo.write_concern import validate_boolean + +if TYPE_CHECKING: + from _typeshed import SupportsItems + + from bson.codec_options import CodecOptions + from pymongo.client_session import ClientSession + from pymongo.collection import Collection + from pymongo.pool import Connection + from pymongo.read_preferences import _ServerMode + + +# These errors mean that the server has already killed the cursor so there is +# no need to send killCursors. +_CURSOR_CLOSED_ERRORS = frozenset( + [ + 43, # CursorNotFound + 175, # QueryPlanKilled + 237, # CursorKilled + # On a tailable cursor, the following errors mean the capped collection + # rolled over. + # MongoDB 2.6: + # {'$err': 'Runner killed during getMore', 'code': 28617, 'ok': 0} + 28617, + # MongoDB 3.0: + # {'$err': 'getMore executor error: UnknownError no details available', + # 'code': 17406, 'ok': 0} + 17406, + # MongoDB 3.2 + 3.4: + # {'ok': 0.0, 'errmsg': 'GetMore command executor error: + # CappedPositionLost: CollectionScan died due to failure to restore + # tailable cursor position. Last seen record id: RecordId(3)', + # 'code': 96} + 96, + # MongoDB 3.6+: + # {'ok': 0.0, 'errmsg': 'errmsg: "CollectionScan died due to failure to + # restore tailable cursor position. Last seen record id: RecordId(3)"', + # 'code': 136, 'codeName': 'CappedPositionLost'} + 136, + ] +) + +_QUERY_OPTIONS = { + "tailable_cursor": 2, + "secondary_okay": 4, + "oplog_replay": 8, + "no_timeout": 16, + "await_data": 32, + "exhaust": 64, + "partial": 128, +} + + +class CursorType: + NON_TAILABLE = 0 + """The standard cursor type.""" + + TAILABLE = _QUERY_OPTIONS["tailable_cursor"] + """The tailable cursor type. + + Tailable cursors are only for use with capped collections. They are not + closed when the last data is retrieved but are kept open and the cursor + location marks the final document position. If more data is received + iteration of the cursor will continue from the last document received. + """ + + TAILABLE_AWAIT = TAILABLE | _QUERY_OPTIONS["await_data"] + """A tailable cursor with the await option set. + + Creates a tailable cursor that will wait for a few seconds after returning + the full result set so that it can capture and return additional data added + during the query. + """ + + EXHAUST = _QUERY_OPTIONS["exhaust"] + """An exhaust cursor. + + MongoDB will stream batched results to the client without waiting for the + client to request each batch, reducing latency. + """ + + +class _ConnectionManager: + """Used with exhaust cursors to ensure the connection is returned.""" + + def __init__(self, conn: Connection, more_to_come: bool): + self.conn: Optional[Connection] = conn + self.more_to_come = more_to_come + self.lock = _create_lock() + + def update_exhaust(self, more_to_come: bool) -> None: + self.more_to_come = more_to_come + + def close(self) -> None: + """Return this instance's connection to the connection pool.""" + if self.conn: + self.conn.unpin() + self.conn = None + + +_Sort = Union[ + Sequence[Union[str, Tuple[str, Union[int, str, Mapping[str, Any]]]]], Mapping[str, Any] +] +_Hint = Union[str, _Sort] + + +class Cursor(Generic[_DocumentType]): + """A cursor / iterator over Mongo query results.""" + + _query_class = _Query + _getmore_class = _GetMore + + def __init__( + self, + collection: Collection[_DocumentType], + filter: Optional[Mapping[str, Any]] = None, + projection: Optional[Union[Mapping[str, Any], Iterable[str]]] = None, + skip: int = 0, + limit: int = 0, + no_cursor_timeout: bool = False, + cursor_type: int = CursorType.NON_TAILABLE, + sort: Optional[_Sort] = None, + allow_partial_results: bool = False, + oplog_replay: bool = False, + batch_size: int = 0, + collation: Optional[_CollationIn] = None, + hint: Optional[_Hint] = None, + max_scan: Optional[int] = None, + max_time_ms: Optional[int] = None, + max: Optional[_Sort] = None, + min: Optional[_Sort] = None, + return_key: Optional[bool] = None, + show_record_id: Optional[bool] = None, + snapshot: Optional[bool] = None, + comment: Optional[Any] = None, + session: Optional[ClientSession] = None, + allow_disk_use: Optional[bool] = None, + let: Optional[bool] = None, + ) -> None: + """Create a new cursor. + + Should not be called directly by application developers - see + :meth:`~pymongo.collection.Collection.find` instead. + + .. seealso:: The MongoDB documentation on `cursors `_. + """ + # Initialize all attributes used in __del__ before possibly raising + # an error to avoid attribute errors during garbage collection. + self.__collection: Collection[_DocumentType] = collection + self.__id: Any = None + self.__exhaust = False + self.__sock_mgr: Any = None + self.__killed = False + self.__session: Optional[ClientSession] + + if session: + self.__session = session + self.__explicit_session = True + else: + self.__session = None + self.__explicit_session = False + + spec: Mapping[str, Any] = filter or {} + validate_is_mapping("filter", spec) + if not isinstance(skip, int): + raise TypeError("skip must be an instance of int") + if not isinstance(limit, int): + raise TypeError("limit must be an instance of int") + validate_boolean("no_cursor_timeout", no_cursor_timeout) + if no_cursor_timeout and not self.__explicit_session: + warnings.warn( + "use an explicit session with no_cursor_timeout=True " + "otherwise the cursor may still timeout after " + "30 minutes, for more info see " + "https://mongodb.com/docs/v4.4/reference/method/" + "cursor.noCursorTimeout/" + "#session-idle-timeout-overrides-nocursortimeout", + UserWarning, + stacklevel=2, + ) + if cursor_type not in ( + CursorType.NON_TAILABLE, + CursorType.TAILABLE, + CursorType.TAILABLE_AWAIT, + CursorType.EXHAUST, + ): + raise ValueError("not a valid value for cursor_type") + validate_boolean("allow_partial_results", allow_partial_results) + validate_boolean("oplog_replay", oplog_replay) + if not isinstance(batch_size, int): + raise TypeError("batch_size must be an integer") + if batch_size < 0: + raise ValueError("batch_size must be >= 0") + # Only set if allow_disk_use is provided by the user, else None. + if allow_disk_use is not None: + allow_disk_use = validate_boolean("allow_disk_use", allow_disk_use) + + if projection is not None: + projection = helpers._fields_list_to_dict(projection, "projection") + + if let is not None: + validate_is_document_type("let", let) + + self.__let = let + self.__spec = spec + self.__has_filter = filter is not None + self.__projection = projection + self.__skip = skip + self.__limit = limit + self.__batch_size = batch_size + self.__ordering = sort and helpers._index_document(sort) or None + self.__max_scan = max_scan + self.__explain = False + self.__comment = comment + self.__max_time_ms = max_time_ms + self.__max_await_time_ms: Optional[int] = None + self.__max: Optional[Union[dict[Any, Any], _Sort]] = max + self.__min: Optional[Union[dict[Any, Any], _Sort]] = min + self.__collation = validate_collation_or_none(collation) + self.__return_key = return_key + self.__show_record_id = show_record_id + self.__allow_disk_use = allow_disk_use + self.__snapshot = snapshot + self.__hint: Union[str, dict[str, Any], None] + self.__set_hint(hint) + + # Exhaust cursor support + if cursor_type == CursorType.EXHAUST: + if self.__collection.database.client.is_mongos: + raise InvalidOperation("Exhaust cursors are not supported by mongos") + if limit: + raise InvalidOperation("Can't use limit and exhaust together.") + self.__exhaust = True + + # This is ugly. People want to be able to do cursor[5:5] and + # get an empty result set (old behavior was an + # exception). It's hard to do that right, though, because the + # server uses limit(0) to mean 'no limit'. So we set __empty + # in that case and check for it when iterating. We also unset + # it anytime we change __limit. + self.__empty = False + + self.__data: deque = deque() + self.__address: Optional[_Address] = None + self.__retrieved = 0 + + self.__codec_options = collection.codec_options + # Read preference is set when the initial find is sent. + self.__read_preference: Optional[_ServerMode] = None + self.__read_concern = collection.read_concern + + self.__query_flags = cursor_type + if no_cursor_timeout: + self.__query_flags |= _QUERY_OPTIONS["no_timeout"] + if allow_partial_results: + self.__query_flags |= _QUERY_OPTIONS["partial"] + if oplog_replay: + self.__query_flags |= _QUERY_OPTIONS["oplog_replay"] + + # The namespace to use for find/getMore commands. + self.__dbname = collection.database.name + self.__collname = collection.name + + @property + def collection(self) -> Collection[_DocumentType]: + """The :class:`~pymongo.collection.Collection` that this + :class:`Cursor` is iterating. + """ + return self.__collection + + @property + def retrieved(self) -> int: + """The number of documents retrieved so far.""" + return self.__retrieved + + def __del__(self) -> None: + self.__die() + + def rewind(self) -> Cursor[_DocumentType]: + """Rewind this cursor to its unevaluated state. + + Reset this cursor if it has been partially or completely evaluated. + Any options that are present on the cursor will remain in effect. + Future iterating performed on this cursor will cause new queries to + be sent to the server, even if the resultant data has already been + retrieved by this cursor. + """ + self.close() + self.__data = deque() + self.__id = None + self.__address = None + self.__retrieved = 0 + self.__killed = False + + return self + + def clone(self) -> Cursor[_DocumentType]: + """Get a clone of this cursor. + + Returns a new Cursor instance with options matching those that have + been set on the current instance. The clone will be completely + unevaluated, even if the current instance has been partially or + completely evaluated. + """ + return self._clone(True) + + def _clone(self, deepcopy: bool = True, base: Optional[Cursor] = None) -> Cursor: + """Internal clone helper.""" + if not base: + if self.__explicit_session: + base = self._clone_base(self.__session) + else: + base = self._clone_base(None) + + values_to_clone = ( + "spec", + "projection", + "skip", + "limit", + "max_time_ms", + "max_await_time_ms", + "comment", + "max", + "min", + "ordering", + "explain", + "hint", + "batch_size", + "max_scan", + "query_flags", + "collation", + "empty", + "show_record_id", + "return_key", + "allow_disk_use", + "snapshot", + "exhaust", + "has_filter", + ) + data = { + k: v + for k, v in self.__dict__.items() + if k.startswith("_Cursor__") and k[9:] in values_to_clone + } + if deepcopy: + data = self._deepcopy(data) + base.__dict__.update(data) + return base + + def _clone_base(self, session: Optional[ClientSession]) -> Cursor: + """Creates an empty Cursor object for information to be copied into.""" + return self.__class__(self.__collection, session=session) + + def __die(self, synchronous: bool = False) -> None: + """Closes this cursor.""" + try: + already_killed = self.__killed + except AttributeError: + # __init__ did not run to completion (or at all). + return + + self.__killed = True + if self.__id and not already_killed: + cursor_id = self.__id + assert self.__address is not None + address = _CursorAddress(self.__address, f"{self.__dbname}.{self.__collname}") + else: + # Skip killCursors. + cursor_id = 0 + address = None + self.__collection.database.client._cleanup_cursor( + synchronous, + cursor_id, + address, + self.__sock_mgr, + self.__session, + self.__explicit_session, + ) + if not self.__explicit_session: + self.__session = None + self.__sock_mgr = None + + def close(self) -> None: + """Explicitly close / kill this cursor.""" + self.__die(True) + + def __query_spec(self) -> Mapping[str, Any]: + """Get the spec to use for a query.""" + operators: dict[str, Any] = {} + if self.__ordering: + operators["$orderby"] = self.__ordering + if self.__explain: + operators["$explain"] = True + if self.__hint: + operators["$hint"] = self.__hint + if self.__let: + operators["let"] = self.__let + if self.__comment: + operators["$comment"] = self.__comment + if self.__max_scan: + operators["$maxScan"] = self.__max_scan + if self.__max_time_ms is not None: + operators["$maxTimeMS"] = self.__max_time_ms + if self.__max: + operators["$max"] = self.__max + if self.__min: + operators["$min"] = self.__min + if self.__return_key is not None: + operators["$returnKey"] = self.__return_key + if self.__show_record_id is not None: + # This is upgraded to showRecordId for MongoDB 3.2+ "find" command. + operators["$showDiskLoc"] = self.__show_record_id + if self.__snapshot is not None: + operators["$snapshot"] = self.__snapshot + + if operators: + # Make a shallow copy so we can cleanly rewind or clone. + spec = dict(self.__spec) + + # Allow-listed commands must be wrapped in $query. + if "$query" not in spec: + # $query has to come first + spec = {"$query": spec} + + spec.update(operators) + return spec + # Have to wrap with $query if "query" is the first key. + # We can't just use $query anytime "query" is a key as + # that breaks commands like count and find_and_modify. + # Checking spec.keys()[0] covers the case that the spec + # was passed as an instance of SON or OrderedDict. + elif "query" in self.__spec and ( + len(self.__spec) == 1 or next(iter(self.__spec)) == "query" + ): + return {"$query": self.__spec} + + return self.__spec + + def __check_okay_to_chain(self) -> None: + """Check if it is okay to chain more options onto this cursor.""" + if self.__retrieved or self.__id is not None: + raise InvalidOperation("cannot set options after executing query") + + def add_option(self, mask: int) -> Cursor[_DocumentType]: + """Set arbitrary query flags using a bitmask. + + To set the tailable flag: + cursor.add_option(2) + """ + if not isinstance(mask, int): + raise TypeError("mask must be an int") + self.__check_okay_to_chain() + + if mask & _QUERY_OPTIONS["exhaust"]: + if self.__limit: + raise InvalidOperation("Can't use limit and exhaust together.") + if self.__collection.database.client.is_mongos: + raise InvalidOperation("Exhaust cursors are not supported by mongos") + self.__exhaust = True + + self.__query_flags |= mask + return self + + def remove_option(self, mask: int) -> Cursor[_DocumentType]: + """Unset arbitrary query flags using a bitmask. + + To unset the tailable flag: + cursor.remove_option(2) + """ + if not isinstance(mask, int): + raise TypeError("mask must be an int") + self.__check_okay_to_chain() + + if mask & _QUERY_OPTIONS["exhaust"]: + self.__exhaust = False + + self.__query_flags &= ~mask + return self + + def allow_disk_use(self, allow_disk_use: bool) -> Cursor[_DocumentType]: + """Specifies whether MongoDB can use temporary disk files while + processing a blocking sort operation. + + Raises :exc:`TypeError` if `allow_disk_use` is not a boolean. + + .. note:: `allow_disk_use` requires server version **>= 4.4** + + :param allow_disk_use: if True, MongoDB may use temporary + disk files to store data exceeding the system memory limit while + processing a blocking sort operation. + + .. versionadded:: 3.11 + """ + if not isinstance(allow_disk_use, bool): + raise TypeError("allow_disk_use must be a bool") + self.__check_okay_to_chain() + + self.__allow_disk_use = allow_disk_use + return self + + def limit(self, limit: int) -> Cursor[_DocumentType]: + """Limits the number of results to be returned by this cursor. + + Raises :exc:`TypeError` if `limit` is not an integer. Raises + :exc:`~pymongo.errors.InvalidOperation` if this :class:`Cursor` + has already been used. The last `limit` applied to this cursor + takes precedence. A limit of ``0`` is equivalent to no limit. + + :param limit: the number of results to return + + .. seealso:: The MongoDB documentation on `limit `_. + """ + if not isinstance(limit, int): + raise TypeError("limit must be an integer") + if self.__exhaust: + raise InvalidOperation("Can't use limit and exhaust together.") + self.__check_okay_to_chain() + + self.__empty = False + self.__limit = limit + return self + + def batch_size(self, batch_size: int) -> Cursor[_DocumentType]: + """Limits the number of documents returned in one batch. Each batch + requires a round trip to the server. It can be adjusted to optimize + performance and limit data transfer. + + .. note:: batch_size can not override MongoDB's internal limits on the + amount of data it will return to the client in a single batch (i.e + if you set batch size to 1,000,000,000, MongoDB will currently only + return 4-16MB of results per batch). + + Raises :exc:`TypeError` if `batch_size` is not an integer. + Raises :exc:`ValueError` if `batch_size` is less than ``0``. + Raises :exc:`~pymongo.errors.InvalidOperation` if this + :class:`Cursor` has already been used. The last `batch_size` + applied to this cursor takes precedence. + + :param batch_size: The size of each batch of results requested. + """ + if not isinstance(batch_size, int): + raise TypeError("batch_size must be an integer") + if batch_size < 0: + raise ValueError("batch_size must be >= 0") + self.__check_okay_to_chain() + + self.__batch_size = batch_size + return self + + def skip(self, skip: int) -> Cursor[_DocumentType]: + """Skips the first `skip` results of this cursor. + + Raises :exc:`TypeError` if `skip` is not an integer. Raises + :exc:`ValueError` if `skip` is less than ``0``. Raises + :exc:`~pymongo.errors.InvalidOperation` if this :class:`Cursor` has + already been used. The last `skip` applied to this cursor takes + precedence. + + :param skip: the number of results to skip + """ + if not isinstance(skip, int): + raise TypeError("skip must be an integer") + if skip < 0: + raise ValueError("skip must be >= 0") + self.__check_okay_to_chain() + + self.__skip = skip + return self + + def max_time_ms(self, max_time_ms: Optional[int]) -> Cursor[_DocumentType]: + """Specifies a time limit for a query operation. If the specified + time is exceeded, the operation will be aborted and + :exc:`~pymongo.errors.ExecutionTimeout` is raised. If `max_time_ms` + is ``None`` no limit is applied. + + Raises :exc:`TypeError` if `max_time_ms` is not an integer or ``None``. + Raises :exc:`~pymongo.errors.InvalidOperation` if this :class:`Cursor` + has already been used. + + :param max_time_ms: the time limit after which the operation is aborted + """ + if not isinstance(max_time_ms, int) and max_time_ms is not None: + raise TypeError("max_time_ms must be an integer or None") + self.__check_okay_to_chain() + + self.__max_time_ms = max_time_ms + return self + + def max_await_time_ms(self, max_await_time_ms: Optional[int]) -> Cursor[_DocumentType]: + """Specifies a time limit for a getMore operation on a + :attr:`~pymongo.cursor.CursorType.TAILABLE_AWAIT` cursor. For all other + types of cursor max_await_time_ms is ignored. + + Raises :exc:`TypeError` if `max_await_time_ms` is not an integer or + ``None``. Raises :exc:`~pymongo.errors.InvalidOperation` if this + :class:`Cursor` has already been used. + + .. note:: `max_await_time_ms` requires server version **>= 3.2** + + :param max_await_time_ms: the time limit after which the operation is + aborted + + .. versionadded:: 3.2 + """ + if not isinstance(max_await_time_ms, int) and max_await_time_ms is not None: + raise TypeError("max_await_time_ms must be an integer or None") + self.__check_okay_to_chain() + + # Ignore max_await_time_ms if not tailable or await_data is False. + if self.__query_flags & CursorType.TAILABLE_AWAIT: + self.__max_await_time_ms = max_await_time_ms + + return self + + @overload + def __getitem__(self, index: int) -> _DocumentType: + ... + + @overload + def __getitem__(self, index: slice) -> Cursor[_DocumentType]: + ... + + def __getitem__(self, index: Union[int, slice]) -> Union[_DocumentType, Cursor[_DocumentType]]: + """Get a single document or a slice of documents from this cursor. + + .. warning:: A :class:`~Cursor` is not a Python :class:`list`. Each + index access or slice requires that a new query be run using skip + and limit. Do not iterate the cursor using index accesses. + The following example is **extremely inefficient** and may return + surprising results:: + + cursor = db.collection.find() + # Warning: This runs a new query for each document. + # Don't do this! + for idx in range(10): + print(cursor[idx]) + + Raises :class:`~pymongo.errors.InvalidOperation` if this + cursor has already been used. + + To get a single document use an integral index, e.g.:: + + >>> db.test.find()[50] + + An :class:`IndexError` will be raised if the index is negative + or greater than the amount of documents in this cursor. Any + limit previously applied to this cursor will be ignored. + + To get a slice of documents use a slice index, e.g.:: + + >>> db.test.find()[20:25] + + This will return this cursor with a limit of ``5`` and skip of + ``20`` applied. Using a slice index will override any prior + limits or skips applied to this cursor (including those + applied through previous calls to this method). Raises + :class:`IndexError` when the slice has a step, a negative + start value, or a stop value less than or equal to the start + value. + + :param index: An integer or slice index to be applied to this cursor + """ + self.__check_okay_to_chain() + self.__empty = False + if isinstance(index, slice): + if index.step is not None: + raise IndexError("Cursor instances do not support slice steps") + + skip = 0 + if index.start is not None: + if index.start < 0: + raise IndexError("Cursor instances do not support negative indices") + skip = index.start + + if index.stop is not None: + limit = index.stop - skip + if limit < 0: + raise IndexError( + "stop index must be greater than start index for slice %r" % index + ) + if limit == 0: + self.__empty = True + else: + limit = 0 + + self.__skip = skip + self.__limit = limit + return self + + if isinstance(index, int): + if index < 0: + raise IndexError("Cursor instances do not support negative indices") + clone = self.clone() + clone.skip(index + self.__skip) + clone.limit(-1) # use a hard limit + clone.__query_flags &= ~CursorType.TAILABLE_AWAIT # PYTHON-1371 + for doc in clone: + return doc + raise IndexError("no such item for Cursor instance") + raise TypeError("index %r cannot be applied to Cursor instances" % index) + + def max_scan(self, max_scan: Optional[int]) -> Cursor[_DocumentType]: + """**DEPRECATED** - Limit the number of documents to scan when + performing the query. + + Raises :class:`~pymongo.errors.InvalidOperation` if this + cursor has already been used. Only the last :meth:`max_scan` + applied to this cursor has any effect. + + :param max_scan: the maximum number of documents to scan + + .. versionchanged:: 3.7 + Deprecated :meth:`max_scan`. Support for this option is deprecated in + MongoDB 4.0. Use :meth:`max_time_ms` instead to limit server side + execution time. + """ + self.__check_okay_to_chain() + self.__max_scan = max_scan + return self + + def max(self, spec: _Sort) -> Cursor[_DocumentType]: + """Adds ``max`` operator that specifies upper bound for specific index. + + When using ``max``, :meth:`~hint` should also be configured to ensure + the query uses the expected index and starting in MongoDB 4.2 + :meth:`~hint` will be required. + + :param spec: a list of field, limit pairs specifying the exclusive + upper bound for all keys of a specific index in order. + + .. versionchanged:: 3.8 + Deprecated cursors that use ``max`` without a :meth:`~hint`. + + .. versionadded:: 2.7 + """ + if not isinstance(spec, (list, tuple)): + raise TypeError("spec must be an instance of list or tuple") + + self.__check_okay_to_chain() + self.__max = dict(spec) + return self + + def min(self, spec: _Sort) -> Cursor[_DocumentType]: + """Adds ``min`` operator that specifies lower bound for specific index. + + When using ``min``, :meth:`~hint` should also be configured to ensure + the query uses the expected index and starting in MongoDB 4.2 + :meth:`~hint` will be required. + + :param spec: a list of field, limit pairs specifying the inclusive + lower bound for all keys of a specific index in order. + + .. versionchanged:: 3.8 + Deprecated cursors that use ``min`` without a :meth:`~hint`. + + .. versionadded:: 2.7 + """ + if not isinstance(spec, (list, tuple)): + raise TypeError("spec must be an instance of list or tuple") + + self.__check_okay_to_chain() + self.__min = dict(spec) + return self + + def sort( + self, key_or_list: _Hint, direction: Optional[Union[int, str]] = None + ) -> Cursor[_DocumentType]: + """Sorts this cursor's results. + + Pass a field name and a direction, either + :data:`~pymongo.ASCENDING` or :data:`~pymongo.DESCENDING`.:: + + for doc in collection.find().sort('field', pymongo.ASCENDING): + print(doc) + + To sort by multiple fields, pass a list of (key, direction) pairs. + If just a name is given, :data:`~pymongo.ASCENDING` will be inferred:: + + for doc in collection.find().sort([ + 'field1', + ('field2', pymongo.DESCENDING)]): + print(doc) + + Text search results can be sorted by relevance:: + + cursor = db.test.find( + {'$text': {'$search': 'some words'}}, + {'score': {'$meta': 'textScore'}}) + + # Sort by 'score' field. + cursor.sort([('score', {'$meta': 'textScore'})]) + + for doc in cursor: + print(doc) + + For more advanced text search functionality, see MongoDB's + `Atlas Search `_. + + Raises :class:`~pymongo.errors.InvalidOperation` if this cursor has + already been used. Only the last :meth:`sort` applied to this + cursor has any effect. + + :param key_or_list: a single key or a list of (key, direction) + pairs specifying the keys to sort on + :param direction: only used if `key_or_list` is a single + key, if not given :data:`~pymongo.ASCENDING` is assumed + """ + self.__check_okay_to_chain() + keys = helpers._index_list(key_or_list, direction) + self.__ordering = helpers._index_document(keys) + return self + + def distinct(self, key: str) -> list: + """Get a list of distinct values for `key` among all documents + in the result set of this query. + + Raises :class:`TypeError` if `key` is not an instance of + :class:`str`. + + The :meth:`distinct` method obeys the + :attr:`~pymongo.collection.Collection.read_preference` of the + :class:`~pymongo.collection.Collection` instance on which + :meth:`~pymongo.collection.Collection.find` was called. + + :param key: name of key for which we want to get the distinct values + + .. seealso:: :meth:`pymongo.collection.Collection.distinct` + """ + options: dict[str, Any] = {} + if self.__spec: + options["query"] = self.__spec + if self.__max_time_ms is not None: + options["maxTimeMS"] = self.__max_time_ms + if self.__comment: + options["comment"] = self.__comment + if self.__collation is not None: + options["collation"] = self.__collation + + return self.__collection.distinct(key, session=self.__session, **options) + + def explain(self) -> _DocumentType: + """Returns an explain plan record for this cursor. + + .. note:: This method uses the default verbosity mode of the + `explain command + `_, + ``allPlansExecution``. To use a different verbosity use + :meth:`~pymongo.database.Database.command` to run the explain + command directly. + + .. seealso:: The MongoDB documentation on `explain `_. + """ + c = self.clone() + c.__explain = True + + # always use a hard limit for explains + if c.__limit: + c.__limit = -abs(c.__limit) + return next(c) + + def __set_hint(self, index: Optional[_Hint]) -> None: + if index is None: + self.__hint = None + return + + if isinstance(index, str): + self.__hint = index + else: + self.__hint = helpers._index_document(index) + + def hint(self, index: Optional[_Hint]) -> Cursor[_DocumentType]: + """Adds a 'hint', telling Mongo the proper index to use for the query. + + Judicious use of hints can greatly improve query + performance. When doing a query on multiple fields (at least + one of which is indexed) pass the indexed field as a hint to + the query. Raises :class:`~pymongo.errors.OperationFailure` if the + provided hint requires an index that does not exist on this collection, + and raises :class:`~pymongo.errors.InvalidOperation` if this cursor has + already been used. + + `index` should be an index as passed to + :meth:`~pymongo.collection.Collection.create_index` + (e.g. ``[('field', ASCENDING)]``) or the name of the index. + If `index` is ``None`` any existing hint for this query is + cleared. The last hint applied to this cursor takes precedence + over all others. + + :param index: index to hint on (as an index specifier) + """ + self.__check_okay_to_chain() + self.__set_hint(index) + return self + + def comment(self, comment: Any) -> Cursor[_DocumentType]: + """Adds a 'comment' to the cursor. + + http://mongodb.com/docs/manual/reference/operator/comment/ + + :param comment: A string to attach to the query to help interpret and + trace the operation in the server logs and in profile data. + + .. versionadded:: 2.7 + """ + self.__check_okay_to_chain() + self.__comment = comment + return self + + def where(self, code: Union[str, Code]) -> Cursor[_DocumentType]: + """Adds a `$where`_ clause to this query. + + The `code` argument must be an instance of :class:`str` or + :class:`~bson.code.Code` containing a JavaScript expression. + This expression will be evaluated for each document scanned. + Only those documents for which the expression evaluates to + *true* will be returned as results. The keyword *this* refers + to the object currently being scanned. For example:: + + # Find all documents where field "a" is less than "b" plus "c". + for doc in db.test.find().where('this.a < (this.b + this.c)'): + print(doc) + + Raises :class:`TypeError` if `code` is not an instance of + :class:`str`. Raises :class:`~pymongo.errors.InvalidOperation` if this + :class:`Cursor` has already been used. Only the last call to + :meth:`where` applied to a :class:`Cursor` has any effect. + + .. note:: MongoDB 4.4 drops support for :class:`~bson.code.Code` + with scope variables. Consider using `$expr`_ instead. + + :param code: JavaScript expression to use as a filter + + .. _$expr: https://mongodb.com/docs/manual/reference/operator/query/expr/ + .. _$where: https://mongodb.com/docs/manual/reference/operator/query/where/ + """ + self.__check_okay_to_chain() + if not isinstance(code, Code): + code = Code(code) + + # Avoid overwriting a filter argument that was given by the user + # when updating the spec. + spec: dict[str, Any] + if self.__has_filter: + spec = dict(self.__spec) + else: + spec = cast(dict, self.__spec) + spec["$where"] = code + self.__spec = spec + return self + + def collation(self, collation: Optional[_CollationIn]) -> Cursor[_DocumentType]: + """Adds a :class:`~pymongo.collation.Collation` to this query. + + Raises :exc:`TypeError` if `collation` is not an instance of + :class:`~pymongo.collation.Collation` or a ``dict``. Raises + :exc:`~pymongo.errors.InvalidOperation` if this :class:`Cursor` has + already been used. Only the last collation applied to this cursor has + any effect. + + :param collation: An instance of :class:`~pymongo.collation.Collation`. + """ + self.__check_okay_to_chain() + self.__collation = validate_collation_or_none(collation) + return self + + def __send_message(self, operation: Union[_Query, _GetMore]) -> None: + """Send a query or getmore operation and handles the response. + + If operation is ``None`` this is an exhaust cursor, which reads + the next result batch off the exhaust socket instead of + sending getMore messages to the server. + + Can raise ConnectionFailure. + """ + client = self.__collection.database.client + # OP_MSG is required to support exhaust cursors with encryption. + if client._encrypter and self.__exhaust: + raise InvalidOperation("exhaust cursors do not support auto encryption") + + try: + response = client._run_operation( + operation, self._unpack_response, address=self.__address + ) + except OperationFailure as exc: + if exc.code in _CURSOR_CLOSED_ERRORS or self.__exhaust: + # Don't send killCursors because the cursor is already closed. + self.__killed = True + if exc.timeout: + self.__die(False) + else: + self.close() + # If this is a tailable cursor the error is likely + # due to capped collection roll over. Setting + # self.__killed to True ensures Cursor.alive will be + # False. No need to re-raise. + if ( + exc.code in _CURSOR_CLOSED_ERRORS + and self.__query_flags & _QUERY_OPTIONS["tailable_cursor"] + ): + return + raise + except ConnectionFailure: + self.__killed = True + self.close() + raise + except Exception: + self.close() + raise + + self.__address = response.address + if isinstance(response, PinnedResponse): + if not self.__sock_mgr: + self.__sock_mgr = _ConnectionManager(response.conn, response.more_to_come) + + cmd_name = operation.name + docs = response.docs + if response.from_command: + if cmd_name != "explain": + cursor = docs[0]["cursor"] + self.__id = cursor["id"] + if cmd_name == "find": + documents = cursor["firstBatch"] + # Update the namespace used for future getMore commands. + ns = cursor.get("ns") + if ns: + self.__dbname, self.__collname = ns.split(".", 1) + else: + documents = cursor["nextBatch"] + self.__data = deque(documents) + self.__retrieved += len(documents) + else: + self.__id = 0 + self.__data = deque(docs) + self.__retrieved += len(docs) + else: + assert isinstance(response.data, _OpReply) + self.__id = response.data.cursor_id + self.__data = deque(docs) + self.__retrieved += response.data.number_returned + + if self.__id == 0: + # Don't wait for garbage collection to call __del__, return the + # socket and the session to the pool now. + self.close() + + if self.__limit and self.__id and self.__limit <= self.__retrieved: + self.close() + + def _unpack_response( + self, + response: Union[_OpReply, _OpMsg], + cursor_id: Optional[int], + codec_options: CodecOptions, + user_fields: Optional[Mapping[str, Any]] = None, + legacy_response: bool = False, + ) -> Sequence[_DocumentOut]: + return response.unpack_response(cursor_id, codec_options, user_fields, legacy_response) + + def _read_preference(self) -> _ServerMode: + if self.__read_preference is None: + # Save the read preference for getMore commands. + self.__read_preference = self.__collection._read_preference_for(self.session) + return self.__read_preference + + def _refresh(self) -> int: + """Refreshes the cursor with more data from Mongo. + + Returns the length of self.__data after refresh. Will exit early if + self.__data is already non-empty. Raises OperationFailure when the + cursor cannot be refreshed due to an error on the query. + """ + if len(self.__data) or self.__killed: + return len(self.__data) + + if not self.__session: + self.__session = self.__collection.database.client._ensure_session() + + if self.__id is None: # Query + if (self.__min or self.__max) and not self.__hint: + raise InvalidOperation( + "Passing a 'hint' is required when using the min/max query" + " option to ensure the query utilizes the correct index" + ) + q = self._query_class( + self.__query_flags, + self.__collection.database.name, + self.__collection.name, + self.__skip, + self.__query_spec(), + self.__projection, + self.__codec_options, + self._read_preference(), + self.__limit, + self.__batch_size, + self.__read_concern, + self.__collation, + self.__session, + self.__collection.database.client, + self.__allow_disk_use, + self.__exhaust, + ) + self.__send_message(q) + elif self.__id: # Get More + if self.__limit: + limit = self.__limit - self.__retrieved + if self.__batch_size: + limit = min(limit, self.__batch_size) + else: + limit = self.__batch_size + # Exhaust cursors don't send getMore messages. + g = self._getmore_class( + self.__dbname, + self.__collname, + limit, + self.__id, + self.__codec_options, + self._read_preference(), + self.__session, + self.__collection.database.client, + self.__max_await_time_ms, + self.__sock_mgr, + self.__exhaust, + self.__comment, + ) + self.__send_message(g) + + return len(self.__data) + + @property + def alive(self) -> bool: + """Does this cursor have the potential to return more data? + + This is mostly useful with `tailable cursors + `_ + since they will stop iterating even though they *may* return more + results in the future. + + With regular cursors, simply use a for loop instead of :attr:`alive`:: + + for doc in collection.find(): + print(doc) + + .. note:: Even if :attr:`alive` is True, :meth:`next` can raise + :exc:`StopIteration`. :attr:`alive` can also be True while iterating + a cursor from a failed server. In this case :attr:`alive` will + return False after :meth:`next` fails to retrieve the next batch + of results from the server. + """ + return bool(len(self.__data) or (not self.__killed)) + + @property + def cursor_id(self) -> Optional[int]: + """Returns the id of the cursor + + .. versionadded:: 2.2 + """ + return self.__id + + @property + def address(self) -> Optional[tuple[str, Any]]: + """The (host, port) of the server used, or None. + + .. versionchanged:: 3.0 + Renamed from "conn_id". + """ + return self.__address + + @property + def session(self) -> Optional[ClientSession]: + """The cursor's :class:`~pymongo.client_session.ClientSession`, or None. + + .. versionadded:: 3.6 + """ + if self.__explicit_session: + return self.__session + return None + + def __iter__(self) -> Cursor[_DocumentType]: + return self + + def next(self) -> _DocumentType: + """Advance the cursor.""" + if self.__empty: + raise StopIteration + if len(self.__data) or self._refresh(): + return self.__data.popleft() + else: + raise StopIteration + + __next__ = next + + def __enter__(self) -> Cursor[_DocumentType]: + return self + + def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None: + self.close() + + def __copy__(self) -> Cursor[_DocumentType]: + """Support function for `copy.copy()`. + + .. versionadded:: 2.4 + """ + return self._clone(deepcopy=False) + + def __deepcopy__(self, memo: Any) -> Any: + """Support function for `copy.deepcopy()`. + + .. versionadded:: 2.4 + """ + return self._clone(deepcopy=True) + + @overload + def _deepcopy(self, x: Iterable, memo: Optional[dict[int, Union[list, dict]]] = None) -> list: + ... + + @overload + def _deepcopy( + self, x: SupportsItems, memo: Optional[dict[int, Union[list, dict]]] = None + ) -> dict: + ... + + def _deepcopy( + self, x: Union[Iterable, SupportsItems], memo: Optional[dict[int, Union[list, dict]]] = None + ) -> Union[list, dict]: + """Deepcopy helper for the data dictionary or list. + + Regular expressions cannot be deep copied but as they are immutable we + don't have to copy them when cloning. + """ + y: Union[list, dict] + iterator: Iterable[tuple[Any, Any]] + if not hasattr(x, "items"): + y, is_list, iterator = [], True, enumerate(x) + else: + y, is_list, iterator = {}, False, cast("SupportsItems", x).items() + if memo is None: + memo = {} + val_id = id(x) + if val_id in memo: + return memo[val_id] + memo[val_id] = y + + for key, value in iterator: + if isinstance(value, (dict, list)) and not isinstance(value, SON): + value = self._deepcopy(value, memo) # noqa: PLW2901 + elif not isinstance(value, RE_TYPE): + value = copy.deepcopy(value, memo) # noqa: PLW2901 + + if is_list: + y.append(value) # type: ignore[union-attr] + else: + if not isinstance(key, RE_TYPE): + key = copy.deepcopy(key, memo) # noqa: PLW2901 + y[key] = value + return y + + +class RawBatchCursor(Cursor, Generic[_DocumentType]): + """A cursor / iterator over raw batches of BSON data from a query result.""" + + _query_class = _RawBatchQuery + _getmore_class = _RawBatchGetMore + + def __init__(self, collection: Collection[_DocumentType], *args: Any, **kwargs: Any) -> None: + """Create a new cursor / iterator over raw batches of BSON data. + + Should not be called directly by application developers - + see :meth:`~pymongo.collection.Collection.find_raw_batches` + instead. + + .. seealso:: The MongoDB documentation on `cursors `_. + """ + super().__init__(collection, *args, **kwargs) + + def _unpack_response( + self, + response: Union[_OpReply, _OpMsg], + cursor_id: Optional[int], + codec_options: CodecOptions[Mapping[str, Any]], + user_fields: Optional[Mapping[str, Any]] = None, + legacy_response: bool = False, + ) -> list[_DocumentOut]: + raw_response = response.raw_response(cursor_id, user_fields=user_fields) + if not legacy_response: + # OP_MSG returns firstBatch/nextBatch documents as a BSON array + # Re-assemble the array of documents into a document stream + _convert_raw_document_lists_to_streams(raw_response[0]) + return cast(List["_DocumentOut"], raw_response) + + def explain(self) -> _DocumentType: + """Returns an explain plan record for this cursor. + + .. seealso:: The MongoDB documentation on `explain `_. + """ + clone = self._clone(deepcopy=True, base=Cursor(self.collection)) + return clone.explain() + + def __getitem__(self, index: Any) -> NoReturn: + raise InvalidOperation("Cannot call __getitem__ on RawBatchCursor") diff --git a/venv/Lib/site-packages/pymongo/daemon.py b/venv/Lib/site-packages/pymongo/daemon.py new file mode 100644 index 00000000..b40384df --- /dev/null +++ b/venv/Lib/site-packages/pymongo/daemon.py @@ -0,0 +1,148 @@ +# Copyright 2019-present MongoDB, 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. + +"""Support for spawning a daemon process. + +PyMongo only attempts to spawn the mongocryptd daemon process when automatic +client-side field level encryption is enabled. See +:ref:`automatic-client-side-encryption` for more info. +""" +from __future__ import annotations + +import os +import subprocess +import sys +import warnings +from typing import Any, Optional, Sequence + +# The maximum amount of time to wait for the intermediate subprocess. +_WAIT_TIMEOUT = 10 +_THIS_FILE = os.path.realpath(__file__) + + +def _popen_wait(popen: subprocess.Popen[Any], timeout: Optional[float]) -> Optional[int]: + """Implement wait timeout support for Python 3.""" + try: + return popen.wait(timeout=timeout) + except subprocess.TimeoutExpired: + # Silence TimeoutExpired errors. + return None + + +def _silence_resource_warning(popen: Optional[subprocess.Popen[Any]]) -> None: + """Silence Popen's ResourceWarning. + + Note this should only be used if the process was created as a daemon. + """ + # Set the returncode to avoid this warning when popen is garbage collected: + # "ResourceWarning: subprocess XXX is still running". + # See https://bugs.python.org/issue38890 and + # https://bugs.python.org/issue26741. + # popen is None when mongocryptd spawning fails + if popen is not None: + popen.returncode = 0 + + +if sys.platform == "win32": + # On Windows we spawn the daemon process simply by using DETACHED_PROCESS. + _DETACHED_PROCESS = getattr(subprocess, "DETACHED_PROCESS", 0x00000008) + + def _spawn_daemon(args: Sequence[str]) -> None: + """Spawn a daemon process (Windows).""" + try: + with open(os.devnull, "r+b") as devnull: + popen = subprocess.Popen( + args, # noqa: S603 + creationflags=_DETACHED_PROCESS, + stdin=devnull, + stderr=devnull, + stdout=devnull, + ) + _silence_resource_warning(popen) + except FileNotFoundError as exc: + warnings.warn( + f"Failed to start {args[0]}: is it on your $PATH?\nOriginal exception: {exc}", + RuntimeWarning, + stacklevel=2, + ) + +else: + # On Unix we spawn the daemon process with a double Popen. + # 1) The first Popen runs this file as a Python script using the current + # interpreter. + # 2) The script then decouples itself and performs the second Popen to + # spawn the daemon process. + # 3) The original process waits up to 10 seconds for the script to exit. + # + # Note that we do not call fork() directly because we want this procedure + # to be safe to call from any thread. Using Popen instead of fork also + # avoids triggering the application's os.register_at_fork() callbacks when + # we spawn the mongocryptd daemon process. + def _spawn(args: Sequence[str]) -> Optional[subprocess.Popen[Any]]: + """Spawn the process and silence stdout/stderr.""" + try: + with open(os.devnull, "r+b") as devnull: + return subprocess.Popen( + args, # noqa: S603 + close_fds=True, + stdin=devnull, + stderr=devnull, + stdout=devnull, + ) + except FileNotFoundError as exc: + warnings.warn( + f"Failed to start {args[0]}: is it on your $PATH?\nOriginal exception: {exc}", + RuntimeWarning, + stacklevel=2, + ) + return None + + def _spawn_daemon_double_popen(args: Sequence[str]) -> None: + """Spawn a daemon process using a double subprocess.Popen.""" + spawner_args = [sys.executable, _THIS_FILE] + spawner_args.extend(args) + temp_proc = subprocess.Popen(spawner_args, close_fds=True) # noqa: S603 + # Reap the intermediate child process to avoid creating zombie + # processes. + _popen_wait(temp_proc, _WAIT_TIMEOUT) + + def _spawn_daemon(args: Sequence[str]) -> None: + """Spawn a daemon process (Unix).""" + # "If Python is unable to retrieve the real path to its executable, + # sys.executable will be an empty string or None". + if sys.executable: + _spawn_daemon_double_popen(args) + else: + # Fallback to spawn a non-daemon process without silencing the + # resource warning. We do not use fork here because it is not + # safe to call from a thread on all systems. + # Unfortunately, this means that: + # 1) If the parent application is killed via Ctrl-C, the + # non-daemon process will also be killed. + # 2) Each non-daemon process will hang around as a zombie process + # until the main application exits. + _spawn(args) + + if __name__ == "__main__": + # Attempt to start a new session to decouple from the parent. + if hasattr(os, "setsid"): + try: + os.setsid() + except OSError: + pass + + # We are performing a double fork (Popen) to spawn the process as a + # daemon so it is safe to ignore the resource warning. + _silence_resource_warning(_spawn(sys.argv[1:])) + os._exit(0) diff --git a/venv/Lib/site-packages/pymongo/database.py b/venv/Lib/site-packages/pymongo/database.py new file mode 100644 index 00000000..70580694 --- /dev/null +++ b/venv/Lib/site-packages/pymongo/database.py @@ -0,0 +1,1388 @@ +# Copyright 2009-present MongoDB, 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. + +"""Database level operations.""" +from __future__ import annotations + +from copy import deepcopy +from typing import ( + TYPE_CHECKING, + Any, + Generic, + Mapping, + MutableMapping, + NoReturn, + Optional, + Sequence, + TypeVar, + Union, + cast, + overload, +) + +from bson.codec_options import DEFAULT_CODEC_OPTIONS, CodecOptions +from bson.dbref import DBRef +from bson.timestamp import Timestamp +from pymongo import _csot, common +from pymongo.aggregation import _DatabaseAggregationCommand +from pymongo.change_stream import DatabaseChangeStream +from pymongo.collection import Collection +from pymongo.command_cursor import CommandCursor +from pymongo.common import _ecoc_coll_name, _esc_coll_name +from pymongo.errors import CollectionInvalid, InvalidName, InvalidOperation +from pymongo.operations import _Op +from pymongo.read_preferences import ReadPreference, _ServerMode +from pymongo.typings import _CollationIn, _DocumentType, _DocumentTypeArg, _Pipeline + +if TYPE_CHECKING: + import bson + import bson.codec_options + from pymongo.client_session import ClientSession + from pymongo.mongo_client import MongoClient + from pymongo.pool import Connection + from pymongo.read_concern import ReadConcern + from pymongo.server import Server + from pymongo.write_concern import WriteConcern + + +def _check_name(name: str) -> None: + """Check if a database name is valid.""" + if not name: + raise InvalidName("database name cannot be the empty string") + + for invalid_char in [" ", ".", "$", "/", "\\", "\x00", '"']: + if invalid_char in name: + raise InvalidName("database names cannot contain the character %r" % invalid_char) + + +_CodecDocumentType = TypeVar("_CodecDocumentType", bound=Mapping[str, Any]) + + +class Database(common.BaseObject, Generic[_DocumentType]): + """A Mongo database.""" + + def __init__( + self, + client: MongoClient[_DocumentType], + name: str, + codec_options: Optional[CodecOptions[_DocumentTypeArg]] = None, + read_preference: Optional[_ServerMode] = None, + write_concern: Optional[WriteConcern] = None, + read_concern: Optional[ReadConcern] = None, + ) -> None: + """Get a database by client and name. + + Raises :class:`TypeError` if `name` is not an instance of + :class:`str`. Raises :class:`~pymongo.errors.InvalidName` if + `name` is not a valid database name. + + :param client: A :class:`~pymongo.mongo_client.MongoClient` instance. + :param name: The database name. + :param codec_options: An instance of + :class:`~bson.codec_options.CodecOptions`. If ``None`` (the + default) client.codec_options is used. + :param read_preference: The read preference to use. If + ``None`` (the default) client.read_preference is used. + :param write_concern: An instance of + :class:`~pymongo.write_concern.WriteConcern`. If ``None`` (the + default) client.write_concern is used. + :param read_concern: An instance of + :class:`~pymongo.read_concern.ReadConcern`. If ``None`` (the + default) client.read_concern is used. + + .. seealso:: The MongoDB documentation on `databases `_. + + .. versionchanged:: 4.0 + Removed the eval, system_js, error, last_status, previous_error, + reset_error_history, authenticate, logout, collection_names, + current_op, add_user, remove_user, profiling_level, + set_profiling_level, and profiling_info methods. + See the :ref:`pymongo4-migration-guide`. + + .. versionchanged:: 3.2 + Added the read_concern option. + + .. versionchanged:: 3.0 + Added the codec_options, read_preference, and write_concern options. + :class:`~pymongo.database.Database` no longer returns an instance + of :class:`~pymongo.collection.Collection` for attribute names + with leading underscores. You must use dict-style lookups instead:: + + db['__my_collection__'] + + Not: + + db.__my_collection__ + """ + super().__init__( + codec_options or client.codec_options, + read_preference or client.read_preference, + write_concern or client.write_concern, + read_concern or client.read_concern, + ) + + if not isinstance(name, str): + raise TypeError("name must be an instance of str") + + if name != "$external": + _check_name(name) + + self.__name = name + self.__client: MongoClient[_DocumentType] = client + self._timeout = client.options.timeout + + @property + def client(self) -> MongoClient[_DocumentType]: + """The client instance for this :class:`Database`.""" + return self.__client + + @property + def name(self) -> str: + """The name of this :class:`Database`.""" + return self.__name + + def with_options( + self, + codec_options: Optional[CodecOptions[_DocumentTypeArg]] = None, + read_preference: Optional[_ServerMode] = None, + write_concern: Optional[WriteConcern] = None, + read_concern: Optional[ReadConcern] = None, + ) -> Database[_DocumentType]: + """Get a clone of this database changing the specified settings. + + >>> db1.read_preference + Primary() + >>> from pymongo.read_preferences import Secondary + >>> db2 = db1.with_options(read_preference=Secondary([{'node': 'analytics'}])) + >>> db1.read_preference + Primary() + >>> db2.read_preference + Secondary(tag_sets=[{'node': 'analytics'}], max_staleness=-1, hedge=None) + + :param codec_options: An instance of + :class:`~bson.codec_options.CodecOptions`. If ``None`` (the + default) the :attr:`codec_options` of this :class:`Collection` + is used. + :param read_preference: The read preference to use. If + ``None`` (the default) the :attr:`read_preference` of this + :class:`Collection` is used. See :mod:`~pymongo.read_preferences` + for options. + :param write_concern: An instance of + :class:`~pymongo.write_concern.WriteConcern`. If ``None`` (the + default) the :attr:`write_concern` of this :class:`Collection` + is used. + :param read_concern: An instance of + :class:`~pymongo.read_concern.ReadConcern`. If ``None`` (the + default) the :attr:`read_concern` of this :class:`Collection` + is used. + + .. versionadded:: 3.8 + """ + return Database( + self.client, + self.__name, + codec_options or self.codec_options, + read_preference or self.read_preference, + write_concern or self.write_concern, + read_concern or self.read_concern, + ) + + def __eq__(self, other: Any) -> bool: + if isinstance(other, Database): + return self.__client == other.client and self.__name == other.name + return NotImplemented + + def __ne__(self, other: Any) -> bool: + return not self == other + + def __hash__(self) -> int: + return hash((self.__client, self.__name)) + + def __repr__(self) -> str: + return f"Database({self.__client!r}, {self.__name!r})" + + def __getattr__(self, name: str) -> Collection[_DocumentType]: + """Get a collection of this database by name. + + Raises InvalidName if an invalid collection name is used. + + :param name: the name of the collection to get + """ + if name.startswith("_"): + raise AttributeError( + f"Database has no attribute {name!r}. To access the {name}" + f" collection, use database[{name!r}]." + ) + return self.__getitem__(name) + + def __getitem__(self, name: str) -> Collection[_DocumentType]: + """Get a collection of this database by name. + + Raises InvalidName if an invalid collection name is used. + + :param name: the name of the collection to get + """ + return Collection(self, name) + + def get_collection( + self, + name: str, + codec_options: Optional[CodecOptions[_DocumentTypeArg]] = None, + read_preference: Optional[_ServerMode] = None, + write_concern: Optional[WriteConcern] = None, + read_concern: Optional[ReadConcern] = None, + ) -> Collection[_DocumentType]: + """Get a :class:`~pymongo.collection.Collection` with the given name + and options. + + Useful for creating a :class:`~pymongo.collection.Collection` with + different codec options, read preference, and/or write concern from + this :class:`Database`. + + >>> db.read_preference + Primary() + >>> coll1 = db.test + >>> coll1.read_preference + Primary() + >>> from pymongo import ReadPreference + >>> coll2 = db.get_collection( + ... 'test', read_preference=ReadPreference.SECONDARY) + >>> coll2.read_preference + Secondary(tag_sets=None) + + :param name: The name of the collection - a string. + :param codec_options: An instance of + :class:`~bson.codec_options.CodecOptions`. If ``None`` (the + default) the :attr:`codec_options` of this :class:`Database` is + used. + :param read_preference: The read preference to use. If + ``None`` (the default) the :attr:`read_preference` of this + :class:`Database` is used. See :mod:`~pymongo.read_preferences` + for options. + :param write_concern: An instance of + :class:`~pymongo.write_concern.WriteConcern`. If ``None`` (the + default) the :attr:`write_concern` of this :class:`Database` is + used. + :param read_concern: An instance of + :class:`~pymongo.read_concern.ReadConcern`. If ``None`` (the + default) the :attr:`read_concern` of this :class:`Database` is + used. + """ + return Collection( + self, + name, + False, + codec_options, + read_preference, + write_concern, + read_concern, + ) + + def _get_encrypted_fields( + self, kwargs: Mapping[str, Any], coll_name: str, ask_db: bool + ) -> Optional[Mapping[str, Any]]: + encrypted_fields = kwargs.get("encryptedFields") + if encrypted_fields: + return cast(Mapping[str, Any], deepcopy(encrypted_fields)) + if ( + self.client.options.auto_encryption_opts + and self.client.options.auto_encryption_opts._encrypted_fields_map + and self.client.options.auto_encryption_opts._encrypted_fields_map.get( + f"{self.name}.{coll_name}" + ) + ): + return cast( + Mapping[str, Any], + deepcopy( + self.client.options.auto_encryption_opts._encrypted_fields_map[ + f"{self.name}.{coll_name}" + ] + ), + ) + if ask_db and self.client.options.auto_encryption_opts: + options = self[coll_name].options() + if options.get("encryptedFields"): + return cast(Mapping[str, Any], deepcopy(options["encryptedFields"])) + return None + + @_csot.apply + def create_collection( + self, + name: str, + codec_options: Optional[CodecOptions[_DocumentTypeArg]] = None, + read_preference: Optional[_ServerMode] = None, + write_concern: Optional[WriteConcern] = None, + read_concern: Optional[ReadConcern] = None, + session: Optional[ClientSession] = None, + check_exists: Optional[bool] = True, + **kwargs: Any, + ) -> Collection[_DocumentType]: + """Create a new :class:`~pymongo.collection.Collection` in this + database. + + Normally collection creation is automatic. This method should + only be used to specify options on + creation. :class:`~pymongo.errors.CollectionInvalid` will be + raised if the collection already exists. + + :param name: the name of the collection to create + :param codec_options: An instance of + :class:`~bson.codec_options.CodecOptions`. If ``None`` (the + default) the :attr:`codec_options` of this :class:`Database` is + used. + :param read_preference: The read preference to use. If + ``None`` (the default) the :attr:`read_preference` of this + :class:`Database` is used. + :param write_concern: An instance of + :class:`~pymongo.write_concern.WriteConcern`. If ``None`` (the + default) the :attr:`write_concern` of this :class:`Database` is + used. + :param read_concern: An instance of + :class:`~pymongo.read_concern.ReadConcern`. If ``None`` (the + default) the :attr:`read_concern` of this :class:`Database` is + used. + :param collation: An instance of + :class:`~pymongo.collation.Collation`. + :param session: a + :class:`~pymongo.client_session.ClientSession`. + :param `check_exists`: if True (the default), send a listCollections command to + check if the collection already exists before creation. + :param kwargs: additional keyword arguments will + be passed as options for the `create collection command`_ + + All optional `create collection command`_ parameters should be passed + as keyword arguments to this method. Valid options include, but are not + limited to: + + - ``size`` (int): desired initial size for the collection (in + bytes). For capped collections this size is the max + size of the collection. + - ``capped`` (bool): if True, this is a capped collection + - ``max`` (int): maximum number of objects if capped (optional) + - ``timeseries`` (dict): a document specifying configuration options for + timeseries collections + - ``expireAfterSeconds`` (int): the number of seconds after which a + document in a timeseries collection expires + - ``validator`` (dict): a document specifying validation rules or expressions + for the collection + - ``validationLevel`` (str): how strictly to apply the + validation rules to existing documents during an update. The default level + is "strict" + - ``validationAction`` (str): whether to "error" on invalid documents + (the default) or just "warn" about the violations but allow invalid + documents to be inserted + - ``indexOptionDefaults`` (dict): a document specifying a default configuration + for indexes when creating a collection + - ``viewOn`` (str): the name of the source collection or view from which + to create the view + - ``pipeline`` (list): a list of aggregation pipeline stages + - ``comment`` (str): a user-provided comment to attach to this command. + This option is only supported on MongoDB >= 4.4. + - ``encryptedFields`` (dict): **(BETA)** Document that describes the encrypted fields for + Queryable Encryption. For example:: + + { + "escCollection": "enxcol_.encryptedCollection.esc", + "ecocCollection": "enxcol_.encryptedCollection.ecoc", + "fields": [ + { + "path": "firstName", + "keyId": Binary.from_uuid(UUID('00000000-0000-0000-0000-000000000000')), + "bsonType": "string", + "queries": {"queryType": "equality"} + }, + { + "path": "ssn", + "keyId": Binary.from_uuid(UUID('04104104-1041-0410-4104-104104104104')), + "bsonType": "string" + } + ] + } + - ``clusteredIndex`` (dict): Document that specifies the clustered index + configuration. It must have the following form:: + + { + // key pattern must be {_id: 1} + key: , // required + unique: , // required, must be `true` + name: , // optional, otherwise automatically generated + v: , // optional, must be `2` if provided + } + - ``changeStreamPreAndPostImages`` (dict): a document with a boolean field ``enabled`` for + enabling pre- and post-images. + + .. versionchanged:: 4.2 + Added the ``check_exists``, ``clusteredIndex``, and ``encryptedFields`` parameters. + + .. versionchanged:: 3.11 + This method is now supported inside multi-document transactions + with MongoDB 4.4+. + + .. versionchanged:: 3.6 + Added ``session`` parameter. + + .. versionchanged:: 3.4 + Added the collation option. + + .. versionchanged:: 3.0 + Added the codec_options, read_preference, and write_concern options. + + .. _create collection command: + https://mongodb.com/docs/manual/reference/command/create + """ + encrypted_fields = self._get_encrypted_fields(kwargs, name, False) + if encrypted_fields: + common.validate_is_mapping("encryptedFields", encrypted_fields) + kwargs["encryptedFields"] = encrypted_fields + + clustered_index = kwargs.get("clusteredIndex") + if clustered_index: + common.validate_is_mapping("clusteredIndex", clustered_index) + + with self.__client._tmp_session(session) as s: + # Skip this check in a transaction where listCollections is not + # supported. + if ( + check_exists + and (not s or not s.in_transaction) + and name in self.list_collection_names(filter={"name": name}, session=s) + ): + raise CollectionInvalid("collection %s already exists" % name) + return Collection( + self, + name, + True, + codec_options, + read_preference, + write_concern, + read_concern, + session=s, + **kwargs, + ) + + def aggregate( + self, pipeline: _Pipeline, session: Optional[ClientSession] = None, **kwargs: Any + ) -> CommandCursor[_DocumentType]: + """Perform a database-level aggregation. + + See the `aggregation pipeline`_ documentation for a list of stages + that are supported. + + .. code-block:: python + + # Lists all operations currently running on the server. + with client.admin.aggregate([{"$currentOp": {}}]) as cursor: + for operation in cursor: + print(operation) + + The :meth:`aggregate` method obeys the :attr:`read_preference` of this + :class:`Database`, except when ``$out`` or ``$merge`` are used, in + which case :attr:`~pymongo.read_preferences.ReadPreference.PRIMARY` + is used. + + .. note:: This method does not support the 'explain' option. Please + use :meth:`~pymongo.database.Database.command` instead. + + .. note:: The :attr:`~pymongo.database.Database.write_concern` of + this collection is automatically applied to this operation. + + :param pipeline: a list of aggregation pipeline stages + :param session: a + :class:`~pymongo.client_session.ClientSession`. + :param kwargs: extra `aggregate command`_ parameters. + + All optional `aggregate command`_ parameters should be passed as + keyword arguments to this method. Valid options include, but are not + limited to: + + - `allowDiskUse` (bool): Enables writing to temporary files. When set + to True, aggregation stages can write data to the _tmp subdirectory + of the --dbpath directory. The default is False. + - `maxTimeMS` (int): The maximum amount of time to allow the operation + to run in milliseconds. + - `batchSize` (int): The maximum number of documents to return per + batch. Ignored if the connected mongod or mongos does not support + returning aggregate results using a cursor. + - `collation` (optional): An instance of + :class:`~pymongo.collation.Collation`. + - `let` (dict): A dict of parameter names and values. Values must be + constant or closed expressions that do not reference document + fields. Parameters can then be accessed as variables in an + aggregate expression context (e.g. ``"$$var"``). This option is + only supported on MongoDB >= 5.0. + + :return: A :class:`~pymongo.command_cursor.CommandCursor` over the result + set. + + .. versionadded:: 3.9 + + .. _aggregation pipeline: + https://mongodb.com/docs/manual/reference/operator/aggregation-pipeline + + .. _aggregate command: + https://mongodb.com/docs/manual/reference/command/aggregate + """ + with self.client._tmp_session(session, close=False) as s: + cmd = _DatabaseAggregationCommand( + self, + CommandCursor, + pipeline, + kwargs, + session is not None, + user_fields={"cursor": {"firstBatch": 1}}, + ) + return self.client._retryable_read( + cmd.get_cursor, + cmd.get_read_preference(s), # type: ignore[arg-type] + s, + retryable=not cmd._performs_write, + operation=_Op.AGGREGATE, + ) + + def watch( + self, + pipeline: Optional[_Pipeline] = None, + full_document: Optional[str] = None, + resume_after: Optional[Mapping[str, Any]] = None, + max_await_time_ms: Optional[int] = None, + batch_size: Optional[int] = None, + collation: Optional[_CollationIn] = None, + start_at_operation_time: Optional[Timestamp] = None, + session: Optional[ClientSession] = None, + start_after: Optional[Mapping[str, Any]] = None, + comment: Optional[Any] = None, + full_document_before_change: Optional[str] = None, + show_expanded_events: Optional[bool] = None, + ) -> DatabaseChangeStream[_DocumentType]: + """Watch changes on this database. + + Performs an aggregation with an implicit initial ``$changeStream`` + stage and returns a + :class:`~pymongo.change_stream.DatabaseChangeStream` cursor which + iterates over changes on all collections in this database. + + Introduced in MongoDB 4.0. + + .. code-block:: python + + with db.watch() as stream: + for change in stream: + print(change) + + The :class:`~pymongo.change_stream.DatabaseChangeStream` iterable + blocks until the next change document is returned or an error is + raised. If the + :meth:`~pymongo.change_stream.DatabaseChangeStream.next` method + encounters a network error when retrieving a batch from the server, + it will automatically attempt to recreate the cursor such that no + change events are missed. Any error encountered during the resume + attempt indicates there may be an outage and will be raised. + + .. code-block:: python + + try: + with db.watch([{"$match": {"operationType": "insert"}}]) as stream: + for insert_change in stream: + print(insert_change) + except pymongo.errors.PyMongoError: + # The ChangeStream encountered an unrecoverable error or the + # resume attempt failed to recreate the cursor. + logging.error("...") + + For a precise description of the resume process see the + `change streams specification`_. + + :param pipeline: A list of aggregation pipeline stages to + append to an initial ``$changeStream`` stage. Not all + pipeline stages are valid after a ``$changeStream`` stage, see the + MongoDB documentation on change streams for the supported stages. + :param full_document: The fullDocument to pass as an option + to the ``$changeStream`` stage. Allowed values: 'updateLookup', + 'whenAvailable', 'required'. When set to 'updateLookup', the + change notification for partial updates will include both a delta + describing the changes to the document, as well as a copy of the + entire document that was changed from some time after the change + occurred. + :param full_document_before_change: Allowed values: 'whenAvailable' + and 'required'. Change events may now result in a + 'fullDocumentBeforeChange' response field. + :param resume_after: A resume token. If provided, the + change stream will start returning changes that occur directly + after the operation specified in the resume token. A resume token + is the _id value of a change document. + :param max_await_time_ms: The maximum time in milliseconds + for the server to wait for changes before responding to a getMore + operation. + :param batch_size: The maximum number of documents to return + per batch. + :param collation: The :class:`~pymongo.collation.Collation` + to use for the aggregation. + :param start_at_operation_time: If provided, the resulting + change stream will only return changes that occurred at or after + the specified :class:`~bson.timestamp.Timestamp`. Requires + MongoDB >= 4.0. + :param session: a + :class:`~pymongo.client_session.ClientSession`. + :param start_after: The same as `resume_after` except that + `start_after` can resume notifications after an invalidate event. + This option and `resume_after` are mutually exclusive. + :param comment: A user-provided comment to attach to this + command. + :param show_expanded_events: Include expanded events such as DDL events like `dropIndexes`. + + :return: A :class:`~pymongo.change_stream.DatabaseChangeStream` cursor. + + .. versionchanged:: 4.3 + Added `show_expanded_events` parameter. + + .. versionchanged:: 4.2 + Added ``full_document_before_change`` parameter. + + .. versionchanged:: 4.1 + Added ``comment`` parameter. + + .. versionchanged:: 3.9 + Added the ``start_after`` parameter. + + .. versionadded:: 3.7 + + .. seealso:: The MongoDB documentation on `changeStreams `_. + + .. _change streams specification: + https://github.com/mongodb/specifications/blob/master/source/change-streams/change-streams.md + """ + return DatabaseChangeStream( + self, + pipeline, + full_document, + resume_after, + max_await_time_ms, + batch_size, + collation, + start_at_operation_time, + session, + start_after, + comment, + full_document_before_change, + show_expanded_events=show_expanded_events, + ) + + @overload + def _command( + self, + conn: Connection, + command: Union[str, MutableMapping[str, Any]], + value: int = 1, + check: bool = True, + allowable_errors: Optional[Sequence[Union[str, int]]] = None, + read_preference: _ServerMode = ReadPreference.PRIMARY, + codec_options: CodecOptions[dict[str, Any]] = DEFAULT_CODEC_OPTIONS, + write_concern: Optional[WriteConcern] = None, + parse_write_concern_error: bool = False, + session: Optional[ClientSession] = None, + **kwargs: Any, + ) -> dict[str, Any]: + ... + + @overload + def _command( + self, + conn: Connection, + command: Union[str, MutableMapping[str, Any]], + value: int = 1, + check: bool = True, + allowable_errors: Optional[Sequence[Union[str, int]]] = None, + read_preference: _ServerMode = ReadPreference.PRIMARY, + codec_options: CodecOptions[_CodecDocumentType] = ..., + write_concern: Optional[WriteConcern] = None, + parse_write_concern_error: bool = False, + session: Optional[ClientSession] = None, + **kwargs: Any, + ) -> _CodecDocumentType: + ... + + def _command( + self, + conn: Connection, + command: Union[str, MutableMapping[str, Any]], + value: int = 1, + check: bool = True, + allowable_errors: Optional[Sequence[Union[str, int]]] = None, + read_preference: _ServerMode = ReadPreference.PRIMARY, + codec_options: Union[ + CodecOptions[dict[str, Any]], CodecOptions[_CodecDocumentType] + ] = DEFAULT_CODEC_OPTIONS, + write_concern: Optional[WriteConcern] = None, + parse_write_concern_error: bool = False, + session: Optional[ClientSession] = None, + **kwargs: Any, + ) -> Union[dict[str, Any], _CodecDocumentType]: + """Internal command helper.""" + if isinstance(command, str): + command = {command: value} + + command.update(kwargs) + with self.__client._tmp_session(session) as s: + return conn.command( + self.__name, + command, + read_preference, + codec_options, + check, + allowable_errors, + write_concern=write_concern, + parse_write_concern_error=parse_write_concern_error, + session=s, + client=self.__client, + ) + + @overload + def command( + self, + command: Union[str, MutableMapping[str, Any]], + value: Any = 1, + check: bool = True, + allowable_errors: Optional[Sequence[Union[str, int]]] = None, + read_preference: Optional[_ServerMode] = None, + codec_options: None = None, + session: Optional[ClientSession] = None, + comment: Optional[Any] = None, + **kwargs: Any, + ) -> dict[str, Any]: + ... + + @overload + def command( + self, + command: Union[str, MutableMapping[str, Any]], + value: Any = 1, + check: bool = True, + allowable_errors: Optional[Sequence[Union[str, int]]] = None, + read_preference: Optional[_ServerMode] = None, + codec_options: CodecOptions[_CodecDocumentType] = ..., + session: Optional[ClientSession] = None, + comment: Optional[Any] = None, + **kwargs: Any, + ) -> _CodecDocumentType: + ... + + @_csot.apply + def command( + self, + command: Union[str, MutableMapping[str, Any]], + value: Any = 1, + check: bool = True, + allowable_errors: Optional[Sequence[Union[str, int]]] = None, + read_preference: Optional[_ServerMode] = None, + codec_options: Optional[bson.codec_options.CodecOptions[_CodecDocumentType]] = None, + session: Optional[ClientSession] = None, + comment: Optional[Any] = None, + **kwargs: Any, + ) -> Union[dict[str, Any], _CodecDocumentType]: + """Issue a MongoDB command. + + Send command `command` to the database and return the + response. If `command` is an instance of :class:`str` + then the command {`command`: `value`} will be sent. + Otherwise, `command` must be an instance of + :class:`dict` and will be sent as is. + + Any additional keyword arguments will be added to the final + command document before it is sent. + + For example, a command like ``{buildinfo: 1}`` can be sent + using: + + >>> db.command("buildinfo") + OR + >>> db.command({"buildinfo": 1}) + + For a command where the value matters, like ``{count: + collection_name}`` we can do: + + >>> db.command("count", collection_name) + OR + >>> db.command({"count": collection_name}) + + For commands that take additional arguments we can use + kwargs. So ``{count: collection_name, query: query}`` becomes: + + >>> db.command("count", collection_name, query=query) + OR + >>> db.command({"count": collection_name, "query": query}) + + :param command: document representing the command to be issued, + or the name of the command (for simple commands only). + + .. note:: the order of keys in the `command` document is + significant (the "verb" must come first), so commands + which require multiple keys (e.g. `findandmodify`) + should be done with this in mind. + + :param value: value to use for the command verb when + `command` is passed as a string + :param check: check the response for errors, raising + :class:`~pymongo.errors.OperationFailure` if there are any + :param allowable_errors: if `check` is ``True``, error messages + in this list will be ignored by error-checking + :param read_preference: The read preference for this + operation. See :mod:`~pymongo.read_preferences` for options. + If the provided `session` is in a transaction, defaults to the + read preference configured for the transaction. + Otherwise, defaults to + :attr:`~pymongo.read_preferences.ReadPreference.PRIMARY`. + :param codec_options: A :class:`~bson.codec_options.CodecOptions` + instance. + :param session: A + :class:`~pymongo.client_session.ClientSession`. + :param comment: A user-provided comment to attach to this + command. + :param kwargs: additional keyword arguments will + be added to the command document before it is sent + + + .. note:: :meth:`command` does **not** obey this Database's + :attr:`read_preference` or :attr:`codec_options`. You must use the + ``read_preference`` and ``codec_options`` parameters instead. + + .. note:: :meth:`command` does **not** apply any custom TypeDecoders + when decoding the command response. + + .. note:: If this client has been configured to use MongoDB Stable + API (see :ref:`versioned-api-ref`), then :meth:`command` will + automatically add API versioning options to the given command. + Explicitly adding API versioning options in the command and + declaring an API version on the client is not supported. + + .. versionchanged:: 3.6 + Added ``session`` parameter. + + .. versionchanged:: 3.0 + Removed the `as_class`, `fields`, `uuid_subtype`, `tag_sets`, + and `secondary_acceptable_latency_ms` option. + Removed `compile_re` option: PyMongo now always represents BSON + regular expressions as :class:`~bson.regex.Regex` objects. Use + :meth:`~bson.regex.Regex.try_compile` to attempt to convert from a + BSON regular expression to a Python regular expression object. + Added the ``codec_options`` parameter. + + .. seealso:: The MongoDB documentation on `commands `_. + """ + opts = codec_options or DEFAULT_CODEC_OPTIONS + if comment is not None: + kwargs["comment"] = comment + + if isinstance(command, str): + command_name = command + else: + command_name = next(iter(command)) + + if read_preference is None: + read_preference = (session and session._txn_read_preference()) or ReadPreference.PRIMARY + with self.__client._conn_for_reads(read_preference, session, operation=command_name) as ( + connection, + read_preference, + ): + return self._command( + connection, + command, + value, + check, + allowable_errors, + read_preference, + opts, + session=session, + **kwargs, + ) + + @_csot.apply + def cursor_command( + self, + command: Union[str, MutableMapping[str, Any]], + value: Any = 1, + read_preference: Optional[_ServerMode] = None, + codec_options: Optional[bson.codec_options.CodecOptions[_CodecDocumentType]] = None, + session: Optional[ClientSession] = None, + comment: Optional[Any] = None, + max_await_time_ms: Optional[int] = None, + **kwargs: Any, + ) -> CommandCursor[_DocumentType]: + """Issue a MongoDB command and parse the response as a cursor. + + If the response from the server does not include a cursor field, an error will be thrown. + + Otherwise, behaves identically to issuing a normal MongoDB command. + + :param command: document representing the command to be issued, + or the name of the command (for simple commands only). + + .. note:: the order of keys in the `command` document is + significant (the "verb" must come first), so commands + which require multiple keys (e.g. `findandmodify`) + should use an instance of :class:`~bson.son.SON` or + a string and kwargs instead of a Python `dict`. + + :param value: value to use for the command verb when + `command` is passed as a string + :param read_preference: The read preference for this + operation. See :mod:`~pymongo.read_preferences` for options. + If the provided `session` is in a transaction, defaults to the + read preference configured for the transaction. + Otherwise, defaults to + :attr:`~pymongo.read_preferences.ReadPreference.PRIMARY`. + :param codec_options`: A :class:`~bson.codec_options.CodecOptions` + instance. + :param session: A + :class:`~pymongo.client_session.ClientSession`. + :param comment: A user-provided comment to attach to future getMores for this + command. + :param max_await_time_ms: The number of ms to wait for more data on future getMores for this command. + :param kwargs: additional keyword arguments will + be added to the command document before it is sent + + .. note:: :meth:`command` does **not** obey this Database's + :attr:`read_preference` or :attr:`codec_options`. You must use the + ``read_preference`` and ``codec_options`` parameters instead. + + .. note:: :meth:`command` does **not** apply any custom TypeDecoders + when decoding the command response. + + .. note:: If this client has been configured to use MongoDB Stable + API (see :ref:`versioned-api-ref`), then :meth:`command` will + automatically add API versioning options to the given command. + Explicitly adding API versioning options in the command and + declaring an API version on the client is not supported. + + .. seealso:: The MongoDB documentation on `commands `_. + """ + if isinstance(command, str): + command_name = command + else: + command_name = next(iter(command)) + + with self.__client._tmp_session(session, close=False) as tmp_session: + opts = codec_options or DEFAULT_CODEC_OPTIONS + + if read_preference is None: + read_preference = ( + tmp_session and tmp_session._txn_read_preference() + ) or ReadPreference.PRIMARY + with self.__client._conn_for_reads(read_preference, tmp_session, command_name) as ( + conn, + read_preference, + ): + response = self._command( + conn, + command, + value, + True, + None, + read_preference, + opts, + session=tmp_session, + **kwargs, + ) + coll = self.get_collection("$cmd", read_preference=read_preference) + if response.get("cursor"): + cmd_cursor = CommandCursor( + coll, + response["cursor"], + conn.address, + max_await_time_ms=max_await_time_ms, + session=tmp_session, + explicit_session=session is not None, + comment=comment, + ) + cmd_cursor._maybe_pin_connection(conn) + return cmd_cursor + else: + raise InvalidOperation("Command does not return a cursor.") + + def _retryable_read_command( + self, + command: Union[str, MutableMapping[str, Any]], + operation: str, + session: Optional[ClientSession] = None, + ) -> dict[str, Any]: + """Same as command but used for retryable read commands.""" + read_preference = (session and session._txn_read_preference()) or ReadPreference.PRIMARY + + def _cmd( + session: Optional[ClientSession], + _server: Server, + conn: Connection, + read_preference: _ServerMode, + ) -> dict[str, Any]: + return self._command( + conn, + command, + read_preference=read_preference, + session=session, + ) + + return self.__client._retryable_read(_cmd, read_preference, session, operation) + + def _list_collections( + self, + conn: Connection, + session: Optional[ClientSession], + read_preference: _ServerMode, + **kwargs: Any, + ) -> CommandCursor[MutableMapping[str, Any]]: + """Internal listCollections helper.""" + coll = cast( + Collection[MutableMapping[str, Any]], + self.get_collection("$cmd", read_preference=read_preference), + ) + cmd = {"listCollections": 1, "cursor": {}} + cmd.update(kwargs) + with self.__client._tmp_session(session, close=False) as tmp_session: + cursor = self._command(conn, cmd, read_preference=read_preference, session=tmp_session)[ + "cursor" + ] + cmd_cursor = CommandCursor( + coll, + cursor, + conn.address, + session=tmp_session, + explicit_session=session is not None, + comment=cmd.get("comment"), + ) + cmd_cursor._maybe_pin_connection(conn) + return cmd_cursor + + def list_collections( + self, + session: Optional[ClientSession] = None, + filter: Optional[Mapping[str, Any]] = None, + comment: Optional[Any] = None, + **kwargs: Any, + ) -> CommandCursor[MutableMapping[str, Any]]: + """Get a cursor over the collections of this database. + + :param session: a + :class:`~pymongo.client_session.ClientSession`. + :param filter: A query document to filter the list of + collections returned from the listCollections command. + :param comment: A user-provided comment to attach to this + command. + :param kwargs: Optional parameters of the + `listCollections command + `_ + can be passed as keyword arguments to this method. The supported + options differ by server version. + + + :return: An instance of :class:`~pymongo.command_cursor.CommandCursor`. + + .. versionadded:: 3.6 + """ + if filter is not None: + kwargs["filter"] = filter + read_pref = (session and session._txn_read_preference()) or ReadPreference.PRIMARY + if comment is not None: + kwargs["comment"] = comment + + def _cmd( + session: Optional[ClientSession], + _server: Server, + conn: Connection, + read_preference: _ServerMode, + ) -> CommandCursor[MutableMapping[str, Any]]: + return self._list_collections(conn, session, read_preference=read_preference, **kwargs) + + return self.__client._retryable_read( + _cmd, read_pref, session, operation=_Op.LIST_COLLECTIONS + ) + + def list_collection_names( + self, + session: Optional[ClientSession] = None, + filter: Optional[Mapping[str, Any]] = None, + comment: Optional[Any] = None, + **kwargs: Any, + ) -> list[str]: + """Get a list of all the collection names in this database. + + For example, to list all non-system collections:: + + filter = {"name": {"$regex": r"^(?!system\\.)"}} + db.list_collection_names(filter=filter) + + :param session: a + :class:`~pymongo.client_session.ClientSession`. + :param filter: A query document to filter the list of + collections returned from the listCollections command. + :param comment: A user-provided comment to attach to this + command. + :param kwargs: Optional parameters of the + `listCollections command + `_ + can be passed as keyword arguments to this method. The supported + options differ by server version. + + + .. versionchanged:: 3.8 + Added the ``filter`` and ``**kwargs`` parameters. + + .. versionadded:: 3.6 + """ + if comment is not None: + kwargs["comment"] = comment + if filter is None: + kwargs["nameOnly"] = True + + else: + # The enumerate collections spec states that "drivers MUST NOT set + # nameOnly if a filter specifies any keys other than name." + common.validate_is_mapping("filter", filter) + kwargs["filter"] = filter + if not filter or (len(filter) == 1 and "name" in filter): + kwargs["nameOnly"] = True + + return [result["name"] for result in self.list_collections(session=session, **kwargs)] + + def _drop_helper( + self, name: str, session: Optional[ClientSession] = None, comment: Optional[Any] = None + ) -> dict[str, Any]: + command = {"drop": name} + if comment is not None: + command["comment"] = comment + + with self.__client._conn_for_writes(session, operation=_Op.DROP) as connection: + return self._command( + connection, + command, + allowable_errors=["ns not found", 26], + write_concern=self._write_concern_for(session), + parse_write_concern_error=True, + session=session, + ) + + @_csot.apply + def drop_collection( + self, + name_or_collection: Union[str, Collection[_DocumentTypeArg]], + session: Optional[ClientSession] = None, + comment: Optional[Any] = None, + encrypted_fields: Optional[Mapping[str, Any]] = None, + ) -> dict[str, Any]: + """Drop a collection. + + :param name_or_collection: the name of a collection to drop or the + collection object itself + :param session: a + :class:`~pymongo.client_session.ClientSession`. + :param comment: A user-provided comment to attach to this + command. + :param encrypted_fields: **(BETA)** Document that describes the encrypted fields for + Queryable Encryption. For example:: + + { + "escCollection": "enxcol_.encryptedCollection.esc", + "ecocCollection": "enxcol_.encryptedCollection.ecoc", + "fields": [ + { + "path": "firstName", + "keyId": Binary.from_uuid(UUID('00000000-0000-0000-0000-000000000000')), + "bsonType": "string", + "queries": {"queryType": "equality"} + }, + { + "path": "ssn", + "keyId": Binary.from_uuid(UUID('04104104-1041-0410-4104-104104104104')), + "bsonType": "string" + } + ] + + } + + + .. note:: The :attr:`~pymongo.database.Database.write_concern` of + this database is automatically applied to this operation. + + .. versionchanged:: 4.2 + Added ``encrypted_fields`` parameter. + + .. versionchanged:: 4.1 + Added ``comment`` parameter. + + .. versionchanged:: 3.6 + Added ``session`` parameter. + + .. versionchanged:: 3.4 + Apply this database's write concern automatically to this operation + when connected to MongoDB >= 3.4. + + """ + name = name_or_collection + if isinstance(name, Collection): + name = name.name + + if not isinstance(name, str): + raise TypeError("name_or_collection must be an instance of str") + encrypted_fields = self._get_encrypted_fields( + {"encryptedFields": encrypted_fields}, + name, + True, + ) + if encrypted_fields: + common.validate_is_mapping("encrypted_fields", encrypted_fields) + self._drop_helper( + _esc_coll_name(encrypted_fields, name), session=session, comment=comment + ) + self._drop_helper( + _ecoc_coll_name(encrypted_fields, name), session=session, comment=comment + ) + + return self._drop_helper(name, session, comment) + + def validate_collection( + self, + name_or_collection: Union[str, Collection[_DocumentTypeArg]], + scandata: bool = False, + full: bool = False, + session: Optional[ClientSession] = None, + background: Optional[bool] = None, + comment: Optional[Any] = None, + ) -> dict[str, Any]: + """Validate a collection. + + Returns a dict of validation info. Raises CollectionInvalid if + validation fails. + + See also the MongoDB documentation on the `validate command`_. + + :param name_or_collection: A Collection object or the name of a + collection to validate. + :param scandata: Do extra checks beyond checking the overall + structure of the collection. + :param full: Have the server do a more thorough scan of the + collection. Use with `scandata` for a thorough scan + of the structure of the collection and the individual + documents. + :param session: a + :class:`~pymongo.client_session.ClientSession`. + :param background: A boolean flag that determines whether + the command runs in the background. Requires MongoDB 4.4+. + :param comment: A user-provided comment to attach to this + command. + + .. versionchanged:: 4.1 + Added ``comment`` parameter. + + .. versionchanged:: 3.11 + Added ``background`` parameter. + + .. versionchanged:: 3.6 + Added ``session`` parameter. + + .. _validate command: https://mongodb.com/docs/manual/reference/command/validate/ + """ + name = name_or_collection + if isinstance(name, Collection): + name = name.name + + if not isinstance(name, str): + raise TypeError("name_or_collection must be an instance of str or Collection") + cmd = {"validate": name, "scandata": scandata, "full": full} + if comment is not None: + cmd["comment"] = comment + + if background is not None: + cmd["background"] = background + + result = self.command(cmd, session=session) + + valid = True + # Pre 1.9 results + if "result" in result: + info = result["result"] + if info.find("exception") != -1 or info.find("corrupt") != -1: + raise CollectionInvalid(f"{name} invalid: {info}") + # Sharded results + elif "raw" in result: + for _, res in result["raw"].items(): + if "result" in res: + info = res["result"] + if info.find("exception") != -1 or info.find("corrupt") != -1: + raise CollectionInvalid(f"{name} invalid: {info}") + elif not res.get("valid", False): + valid = False + break + # Post 1.9 non-sharded results. + elif not result.get("valid", False): + valid = False + + if not valid: + raise CollectionInvalid(f"{name} invalid: {result!r}") + + return result + + # See PYTHON-3084. + __iter__ = None + + def __next__(self) -> NoReturn: + raise TypeError("'Database' object is not iterable") + + next = __next__ + + def __bool__(self) -> NoReturn: + raise NotImplementedError( + "Database objects do not implement truth " + "value testing or bool(). Please compare " + "with None instead: database is not None" + ) + + def dereference( + self, + dbref: DBRef, + session: Optional[ClientSession] = None, + comment: Optional[Any] = None, + **kwargs: Any, + ) -> Optional[_DocumentType]: + """Dereference a :class:`~bson.dbref.DBRef`, getting the + document it points to. + + Raises :class:`TypeError` if `dbref` is not an instance of + :class:`~bson.dbref.DBRef`. Returns a document, or ``None`` if + the reference does not point to a valid document. Raises + :class:`ValueError` if `dbref` has a database specified that + is different from the current database. + + :param dbref: the reference + :param session: a + :class:`~pymongo.client_session.ClientSession`. + :param comment: A user-provided comment to attach to this + command. + :param kwargs: any additional keyword arguments + are the same as the arguments to + :meth:`~pymongo.collection.Collection.find`. + + + .. versionchanged:: 4.1 + Added ``comment`` parameter. + .. versionchanged:: 3.6 + Added ``session`` parameter. + """ + if not isinstance(dbref, DBRef): + raise TypeError("cannot dereference a %s" % type(dbref)) + if dbref.database is not None and dbref.database != self.__name: + raise ValueError( + "trying to dereference a DBRef that points to " + f"another database ({dbref.database!r} not {self.__name!r})" + ) + return self[dbref.collection].find_one( + {"_id": dbref.id}, session=session, comment=comment, **kwargs + ) diff --git a/venv/Lib/site-packages/pymongo/driver_info.py b/venv/Lib/site-packages/pymongo/driver_info.py new file mode 100644 index 00000000..9e7cfbda --- /dev/null +++ b/venv/Lib/site-packages/pymongo/driver_info.py @@ -0,0 +1,42 @@ +# Copyright 2018-present MongoDB, 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. + +"""Advanced options for MongoDB drivers implemented on top of PyMongo.""" +from __future__ import annotations + +from collections import namedtuple +from typing import Optional + + +class DriverInfo(namedtuple("DriverInfo", ["name", "version", "platform"])): + """Info about a driver wrapping PyMongo. + + The MongoDB server logs PyMongo's name, version, and platform whenever + PyMongo establishes a connection. A driver implemented on top of PyMongo + can add its own info to this log message. Initialize with three strings + like 'MyDriver', '1.2.3', 'some platform info'. Any of these strings may be + None to accept PyMongo's default. + """ + + def __new__( + cls, name: str, version: Optional[str] = None, platform: Optional[str] = None + ) -> DriverInfo: + self = super().__new__(cls, name, version, platform) + for key, value in self._asdict().items(): + if value is not None and not isinstance(value, str): + raise TypeError( + f"Wrong type for DriverInfo {key} option, value must be an instance of str" + ) + + return self diff --git a/venv/Lib/site-packages/pymongo/encryption.py b/venv/Lib/site-packages/pymongo/encryption.py new file mode 100644 index 00000000..c7f02766 --- /dev/null +++ b/venv/Lib/site-packages/pymongo/encryption.py @@ -0,0 +1,1112 @@ +# Copyright 2019-present MongoDB, 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. + +"""Support for explicit client-side field level encryption.""" +from __future__ import annotations + +import contextlib +import enum +import socket +import uuid +import weakref +from copy import deepcopy +from typing import ( + TYPE_CHECKING, + Any, + Dict, + Generic, + Iterator, + Mapping, + MutableMapping, + Optional, + Sequence, + Union, + cast, +) + +try: + from pymongocrypt.auto_encrypter import AutoEncrypter # type:ignore[import] + from pymongocrypt.errors import MongoCryptError # type:ignore[import] + from pymongocrypt.explicit_encrypter import ExplicitEncrypter # type:ignore[import] + from pymongocrypt.mongocrypt import MongoCryptOptions # type:ignore[import] + from pymongocrypt.state_machine import MongoCryptCallback # type:ignore[import] + + _HAVE_PYMONGOCRYPT = True +except ImportError: + _HAVE_PYMONGOCRYPT = False + MongoCryptCallback = object + +from bson import _dict_to_bson, decode, encode +from bson.binary import STANDARD, UUID_SUBTYPE, Binary +from bson.codec_options import CodecOptions +from bson.errors import BSONError +from bson.raw_bson import DEFAULT_RAW_BSON_OPTIONS, RawBSONDocument, _inflate_bson +from pymongo import _csot +from pymongo.collection import Collection +from pymongo.common import CONNECT_TIMEOUT +from pymongo.cursor import Cursor +from pymongo.daemon import _spawn_daemon +from pymongo.database import Database +from pymongo.encryption_options import AutoEncryptionOpts, RangeOpts +from pymongo.errors import ( + ConfigurationError, + EncryptedCollectionError, + EncryptionError, + InvalidOperation, + PyMongoError, + ServerSelectionTimeoutError, +) +from pymongo.mongo_client import MongoClient +from pymongo.network import BLOCKING_IO_ERRORS +from pymongo.operations import UpdateOne +from pymongo.pool import PoolOptions, _configured_socket, _raise_connection_failure +from pymongo.read_concern import ReadConcern +from pymongo.results import BulkWriteResult, DeleteResult +from pymongo.ssl_support import get_ssl_context +from pymongo.typings import _DocumentType, _DocumentTypeArg +from pymongo.uri_parser import parse_host +from pymongo.write_concern import WriteConcern + +if TYPE_CHECKING: + from pymongocrypt.mongocrypt import MongoCryptKmsContext + +_HTTPS_PORT = 443 +_KMS_CONNECT_TIMEOUT = CONNECT_TIMEOUT # CDRIVER-3262 redefined this value to CONNECT_TIMEOUT +_MONGOCRYPTD_TIMEOUT_MS = 10000 + +_DATA_KEY_OPTS: CodecOptions[dict[str, Any]] = CodecOptions( + document_class=Dict[str, Any], uuid_representation=STANDARD +) +# Use RawBSONDocument codec options to avoid needlessly decoding +# documents from the key vault. +_KEY_VAULT_OPTS = CodecOptions(document_class=RawBSONDocument) + + +@contextlib.contextmanager +def _wrap_encryption_errors() -> Iterator[None]: + """Context manager to wrap encryption related errors.""" + try: + yield + except BSONError: + # BSON encoding/decoding errors are unrelated to encryption so + # we should propagate them unchanged. + raise + except Exception as exc: + raise EncryptionError(exc) from exc + + +class _EncryptionIO(MongoCryptCallback): # type: ignore[misc] + def __init__( + self, + client: Optional[MongoClient[_DocumentTypeArg]], + key_vault_coll: Collection[_DocumentTypeArg], + mongocryptd_client: Optional[MongoClient[_DocumentTypeArg]], + opts: AutoEncryptionOpts, + ): + """Internal class to perform I/O on behalf of pymongocrypt.""" + self.client_ref: Any + # Use a weak ref to break reference cycle. + if client is not None: + self.client_ref = weakref.ref(client) + else: + self.client_ref = None + self.key_vault_coll: Optional[Collection[RawBSONDocument]] = cast( + Collection[RawBSONDocument], + key_vault_coll.with_options( + codec_options=_KEY_VAULT_OPTS, + read_concern=ReadConcern(level="majority"), + write_concern=WriteConcern(w="majority"), + ), + ) + self.mongocryptd_client = mongocryptd_client + self.opts = opts + self._spawned = False + + def kms_request(self, kms_context: MongoCryptKmsContext) -> None: + """Complete a KMS request. + + :param kms_context: A :class:`MongoCryptKmsContext`. + + :return: None + """ + endpoint = kms_context.endpoint + message = kms_context.message + provider = kms_context.kms_provider + ctx = self.opts._kms_ssl_contexts.get(provider) + if ctx is None: + # Enable strict certificate verification, OCSP, match hostname, and + # SNI using the system default CA certificates. + ctx = get_ssl_context( + None, # certfile + None, # passphrase + None, # ca_certs + None, # crlfile + False, # allow_invalid_certificates + False, # allow_invalid_hostnames + False, + ) # disable_ocsp_endpoint_check + # CSOT: set timeout for socket creation. + connect_timeout = max(_csot.clamp_remaining(_KMS_CONNECT_TIMEOUT), 0.001) + opts = PoolOptions( + connect_timeout=connect_timeout, + socket_timeout=connect_timeout, + ssl_context=ctx, + ) + host, port = parse_host(endpoint, _HTTPS_PORT) + try: + conn = _configured_socket((host, port), opts) + try: + conn.sendall(message) + while kms_context.bytes_needed > 0: + # CSOT: update timeout. + conn.settimeout(max(_csot.clamp_remaining(_KMS_CONNECT_TIMEOUT), 0)) + data = conn.recv(kms_context.bytes_needed) + if not data: + raise OSError("KMS connection closed") + kms_context.feed(data) + except BLOCKING_IO_ERRORS: + raise socket.timeout("timed out") from None + finally: + conn.close() + except (PyMongoError, MongoCryptError): + raise # Propagate pymongo errors directly. + except Exception as error: + # Wrap I/O errors in PyMongo exceptions. + _raise_connection_failure((host, port), error) + + def collection_info( + self, database: Database[Mapping[str, Any]], filter: bytes + ) -> Optional[bytes]: + """Get the collection info for a namespace. + + The returned collection info is passed to libmongocrypt which reads + the JSON schema. + + :param database: The database on which to run listCollections. + :param filter: The filter to pass to listCollections. + + :return: The first document from the listCollections command response as BSON. + """ + with self.client_ref()[database].list_collections(filter=RawBSONDocument(filter)) as cursor: + for doc in cursor: + return _dict_to_bson(doc, False, _DATA_KEY_OPTS) + return None + + def spawn(self) -> None: + """Spawn mongocryptd. + + Note this method is thread safe; at most one mongocryptd will start + successfully. + """ + self._spawned = True + args = [self.opts._mongocryptd_spawn_path or "mongocryptd"] + args.extend(self.opts._mongocryptd_spawn_args) + _spawn_daemon(args) + + def mark_command(self, database: str, cmd: bytes) -> bytes: + """Mark a command for encryption. + + :param database: The database on which to run this command. + :param cmd: The BSON command to run. + + :return: The marked command response from mongocryptd. + """ + if not self._spawned and not self.opts._mongocryptd_bypass_spawn: + self.spawn() + # Database.command only supports mutable mappings so we need to decode + # the raw BSON command first. + inflated_cmd = _inflate_bson(cmd, DEFAULT_RAW_BSON_OPTIONS) + assert self.mongocryptd_client is not None + try: + res = self.mongocryptd_client[database].command( + inflated_cmd, codec_options=DEFAULT_RAW_BSON_OPTIONS + ) + except ServerSelectionTimeoutError: + if self.opts._mongocryptd_bypass_spawn: + raise + self.spawn() + res = self.mongocryptd_client[database].command( + inflated_cmd, codec_options=DEFAULT_RAW_BSON_OPTIONS + ) + return res.raw + + def fetch_keys(self, filter: bytes) -> Iterator[bytes]: + """Yields one or more keys from the key vault. + + :param filter: The filter to pass to find. + + :return: A generator which yields the requested keys from the key vault. + """ + assert self.key_vault_coll is not None + with self.key_vault_coll.find(RawBSONDocument(filter)) as cursor: + for key in cursor: + yield key.raw + + def insert_data_key(self, data_key: bytes) -> Binary: + """Insert a data key into the key vault. + + :param data_key: The data key document to insert. + + :return: The _id of the inserted data key document. + """ + raw_doc = RawBSONDocument(data_key, _KEY_VAULT_OPTS) + data_key_id = raw_doc.get("_id") + if not isinstance(data_key_id, Binary) or data_key_id.subtype != UUID_SUBTYPE: + raise TypeError("data_key _id must be Binary with a UUID subtype") + + assert self.key_vault_coll is not None + self.key_vault_coll.insert_one(raw_doc) + return data_key_id + + def bson_encode(self, doc: MutableMapping[str, Any]) -> bytes: + """Encode a document to BSON. + + A document can be any mapping type (like :class:`dict`). + + :param doc: mapping type representing a document + + :return: The encoded BSON bytes. + """ + return encode(doc) + + def close(self) -> None: + """Release resources. + + Note it is not safe to call this method from __del__ or any GC hooks. + """ + self.client_ref = None + self.key_vault_coll = None + if self.mongocryptd_client: + self.mongocryptd_client.close() + self.mongocryptd_client = None + + +class RewrapManyDataKeyResult: + """Result object returned by a :meth:`~ClientEncryption.rewrap_many_data_key` operation. + + .. versionadded:: 4.2 + """ + + def __init__(self, bulk_write_result: Optional[BulkWriteResult] = None) -> None: + self._bulk_write_result = bulk_write_result + + @property + def bulk_write_result(self) -> Optional[BulkWriteResult]: + """The result of the bulk write operation used to update the key vault + collection with one or more rewrapped data keys. If + :meth:`~ClientEncryption.rewrap_many_data_key` does not find any matching keys to rewrap, + no bulk write operation will be executed and this field will be + ``None``. + """ + return self._bulk_write_result + + def __repr__(self) -> str: + return f"{self.__class__.__name__}({self._bulk_write_result!r})" + + +class _Encrypter: + """Encrypts and decrypts MongoDB commands. + + This class is used to support automatic encryption and decryption of + MongoDB commands. + """ + + def __init__(self, client: MongoClient[_DocumentTypeArg], opts: AutoEncryptionOpts): + """Create a _Encrypter for a client. + + :param client: The encrypted MongoClient. + :param opts: The encrypted client's :class:`AutoEncryptionOpts`. + """ + if opts._schema_map is None: + schema_map = None + else: + schema_map = _dict_to_bson(opts._schema_map, False, _DATA_KEY_OPTS) + + if opts._encrypted_fields_map is None: + encrypted_fields_map = None + else: + encrypted_fields_map = _dict_to_bson(opts._encrypted_fields_map, False, _DATA_KEY_OPTS) + self._bypass_auto_encryption = opts._bypass_auto_encryption + self._internal_client = None + + def _get_internal_client( + encrypter: _Encrypter, mongo_client: MongoClient[_DocumentTypeArg] + ) -> MongoClient[_DocumentTypeArg]: + if mongo_client.options.pool_options.max_pool_size is None: + # Unlimited pool size, use the same client. + return mongo_client + # Else - limited pool size, use an internal client. + if encrypter._internal_client is not None: + return encrypter._internal_client + internal_client = mongo_client._duplicate(minPoolSize=0, auto_encryption_opts=None) + encrypter._internal_client = internal_client + return internal_client + + if opts._key_vault_client is not None: + key_vault_client = opts._key_vault_client + else: + key_vault_client = _get_internal_client(self, client) + + if opts._bypass_auto_encryption: + metadata_client = None + else: + metadata_client = _get_internal_client(self, client) + + db, coll = opts._key_vault_namespace.split(".", 1) + key_vault_coll = key_vault_client[db][coll] + + mongocryptd_client: MongoClient[Mapping[str, Any]] = MongoClient( + opts._mongocryptd_uri, connect=False, serverSelectionTimeoutMS=_MONGOCRYPTD_TIMEOUT_MS + ) + + io_callbacks = _EncryptionIO( # type:ignore[misc] + metadata_client, key_vault_coll, mongocryptd_client, opts + ) + self._auto_encrypter = AutoEncrypter( + io_callbacks, + MongoCryptOptions( + opts._kms_providers, + schema_map, + crypt_shared_lib_path=opts._crypt_shared_lib_path, + crypt_shared_lib_required=opts._crypt_shared_lib_required, + bypass_encryption=opts._bypass_auto_encryption, + encrypted_fields_map=encrypted_fields_map, + bypass_query_analysis=opts._bypass_query_analysis, + ), + ) + self._closed = False + + def encrypt( + self, database: str, cmd: Mapping[str, Any], codec_options: CodecOptions[_DocumentTypeArg] + ) -> dict[str, Any]: + """Encrypt a MongoDB command. + + :param database: The database for this command. + :param cmd: A command document. + :param codec_options: The CodecOptions to use while encoding `cmd`. + + :return: The encrypted command to execute. + """ + self._check_closed() + encoded_cmd = _dict_to_bson(cmd, False, codec_options) + with _wrap_encryption_errors(): + encrypted_cmd = self._auto_encrypter.encrypt(database, encoded_cmd) + # TODO: PYTHON-1922 avoid decoding the encrypted_cmd. + return _inflate_bson(encrypted_cmd, DEFAULT_RAW_BSON_OPTIONS) + + def decrypt(self, response: bytes) -> Optional[bytes]: + """Decrypt a MongoDB command response. + + :param response: A MongoDB command response as BSON. + + :return: The decrypted command response. + """ + self._check_closed() + with _wrap_encryption_errors(): + return cast(bytes, self._auto_encrypter.decrypt(response)) + + def _check_closed(self) -> None: + if self._closed: + raise InvalidOperation("Cannot use MongoClient after close") + + def close(self) -> None: + """Cleanup resources.""" + self._closed = True + self._auto_encrypter.close() + if self._internal_client: + self._internal_client.close() + self._internal_client = None + + +class Algorithm(str, enum.Enum): + """An enum that defines the supported encryption algorithms.""" + + AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic = "AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic" + """AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic.""" + AEAD_AES_256_CBC_HMAC_SHA_512_Random = "AEAD_AES_256_CBC_HMAC_SHA_512-Random" + """AEAD_AES_256_CBC_HMAC_SHA_512_Random.""" + INDEXED = "Indexed" + """Indexed. + + .. versionadded:: 4.2 + """ + UNINDEXED = "Unindexed" + """Unindexed. + + .. versionadded:: 4.2 + """ + RANGEPREVIEW = "RangePreview" + """RangePreview. + + .. note:: Support for Range queries is in beta. + Backwards-breaking changes may be made before the final release. + + .. versionadded:: 4.4 + """ + + +class QueryType(str, enum.Enum): + """An enum that defines the supported values for explicit encryption query_type. + + .. versionadded:: 4.2 + """ + + EQUALITY = "equality" + """Used to encrypt a value for an equality query.""" + + RANGEPREVIEW = "rangePreview" + """Used to encrypt a value for a range query. + + .. note:: Support for Range queries is in beta. + Backwards-breaking changes may be made before the final release. +""" + + +class ClientEncryption(Generic[_DocumentType]): + """Explicit client-side field level encryption.""" + + def __init__( + self, + kms_providers: Mapping[str, Any], + key_vault_namespace: str, + key_vault_client: MongoClient[_DocumentTypeArg], + codec_options: CodecOptions[_DocumentTypeArg], + kms_tls_options: Optional[Mapping[str, Any]] = None, + ) -> None: + """Explicit client-side field level encryption. + + The ClientEncryption class encapsulates explicit operations on a key + vault collection that cannot be done directly on a MongoClient. Similar + to configuring auto encryption on a MongoClient, it is constructed with + a MongoClient (to a MongoDB cluster containing the key vault + collection), KMS provider configuration, and keyVaultNamespace. It + provides an API for explicitly encrypting and decrypting values, and + creating data keys. It does not provide an API to query keys from the + key vault collection, as this can be done directly on the MongoClient. + + See :ref:`explicit-client-side-encryption` for an example. + + :param kms_providers: Map of KMS provider options. The `kms_providers` + map values differ by provider: + + - `aws`: Map with "accessKeyId" and "secretAccessKey" as strings. + These are the AWS access key ID and AWS secret access key used + to generate KMS messages. An optional "sessionToken" may be + included to support temporary AWS credentials. + - `azure`: Map with "tenantId", "clientId", and "clientSecret" as + strings. Additionally, "identityPlatformEndpoint" may also be + specified as a string (defaults to 'login.microsoftonline.com'). + These are the Azure Active Directory credentials used to + generate Azure Key Vault messages. + - `gcp`: Map with "email" as a string and "privateKey" + as `bytes` or a base64 encoded string. + Additionally, "endpoint" may also be specified as a string + (defaults to 'oauth2.googleapis.com'). These are the + credentials used to generate Google Cloud KMS messages. + - `kmip`: Map with "endpoint" as a host with required port. + For example: ``{"endpoint": "example.com:443"}``. + - `local`: Map with "key" as `bytes` (96 bytes in length) or + a base64 encoded string which decodes + to 96 bytes. "key" is the master key used to encrypt/decrypt + data keys. This key should be generated and stored as securely + as possible. + + KMS providers may be specified with an optional name suffix + separated by a colon, for example "kmip:name" or "aws:name". + Named KMS providers do not support :ref:`CSFLE on-demand credentials`. + :param key_vault_namespace: The namespace for the key vault collection. + The key vault collection contains all data keys used for encryption + and decryption. Data keys are stored as documents in this MongoDB + collection. Data keys are protected with encryption by a KMS + provider. + :param key_vault_client: A MongoClient connected to a MongoDB cluster + containing the `key_vault_namespace` collection. + :param codec_options: An instance of + :class:`~bson.codec_options.CodecOptions` to use when encoding a + value for encryption and decoding the decrypted BSON value. This + should be the same CodecOptions instance configured on the + MongoClient, Database, or Collection used to access application + data. + :param kms_tls_options: A map of KMS provider names to TLS + options to use when creating secure connections to KMS providers. + Accepts the same TLS options as + :class:`pymongo.mongo_client.MongoClient`. For example, to + override the system default CA file:: + + kms_tls_options={'kmip': {'tlsCAFile': certifi.where()}} + + Or to supply a client certificate:: + + kms_tls_options={'kmip': {'tlsCertificateKeyFile': 'client.pem'}} + + .. versionchanged:: 4.0 + Added the `kms_tls_options` parameter and the "kmip" KMS provider. + + .. versionadded:: 3.9 + """ + if not _HAVE_PYMONGOCRYPT: + raise ConfigurationError( + "client-side field level encryption requires the pymongocrypt " + "library: install a compatible version with: " + "python -m pip install 'pymongo[encryption]'" + ) + + if not isinstance(codec_options, CodecOptions): + raise TypeError("codec_options must be an instance of bson.codec_options.CodecOptions") + + self._kms_providers = kms_providers + self._key_vault_namespace = key_vault_namespace + self._key_vault_client = key_vault_client + self._codec_options = codec_options + + db, coll = key_vault_namespace.split(".", 1) + key_vault_coll = key_vault_client[db][coll] + + opts = AutoEncryptionOpts( + kms_providers, key_vault_namespace, kms_tls_options=kms_tls_options + ) + self._io_callbacks: Optional[_EncryptionIO] = _EncryptionIO( + None, key_vault_coll, None, opts + ) + self._encryption = ExplicitEncrypter( + self._io_callbacks, MongoCryptOptions(kms_providers, None) + ) + # Use the same key vault collection as the callback. + assert self._io_callbacks.key_vault_coll is not None + self._key_vault_coll = self._io_callbacks.key_vault_coll + + def create_encrypted_collection( + self, + database: Database[_DocumentTypeArg], + name: str, + encrypted_fields: Mapping[str, Any], + kms_provider: Optional[str] = None, + master_key: Optional[Mapping[str, Any]] = None, + **kwargs: Any, + ) -> tuple[Collection[_DocumentTypeArg], Mapping[str, Any]]: + """Create a collection with encryptedFields. + + .. warning:: + This function does not update the encryptedFieldsMap in the client's + AutoEncryptionOpts, thus the user must create a new client after calling this function with + the encryptedFields returned. + + Normally collection creation is automatic. This method should + only be used to specify options on + creation. :class:`~pymongo.errors.EncryptionError` will be + raised if the collection already exists. + + :param name: the name of the collection to create + :param encrypted_fields: Document that describes the encrypted fields for + Queryable Encryption. The "keyId" may be set to ``None`` to auto-generate the data keys. For example: + + .. code-block: python + + { + "escCollection": "enxcol_.encryptedCollection.esc", + "ecocCollection": "enxcol_.encryptedCollection.ecoc", + "fields": [ + { + "path": "firstName", + "keyId": Binary.from_uuid(UUID('00000000-0000-0000-0000-000000000000')), + "bsonType": "string", + "queries": {"queryType": "equality"} + }, + { + "path": "ssn", + "keyId": Binary.from_uuid(UUID('04104104-1041-0410-4104-104104104104')), + "bsonType": "string" + } + ] + } + + :param kms_provider: the KMS provider to be used + :param master_key: Identifies a KMS-specific key used to encrypt the + new data key. If the kmsProvider is "local" the `master_key` is + not applicable and may be omitted. + :param kwargs: additional keyword arguments are the same as "create_collection". + + All optional `create collection command`_ parameters should be passed + as keyword arguments to this method. + See the documentation for :meth:`~pymongo.database.Database.create_collection` for all valid options. + + :raises: - :class:`~pymongo.errors.EncryptedCollectionError`: When either data-key creation or creating the collection fails. + + .. versionadded:: 4.4 + + .. _create collection command: + https://mongodb.com/docs/manual/reference/command/create + + """ + encrypted_fields = deepcopy(encrypted_fields) + for i, field in enumerate(encrypted_fields["fields"]): + if isinstance(field, dict) and field.get("keyId") is None: + try: + encrypted_fields["fields"][i]["keyId"] = self.create_data_key( + kms_provider=kms_provider, # type:ignore[arg-type] + master_key=master_key, + ) + except EncryptionError as exc: + raise EncryptedCollectionError(exc, encrypted_fields) from exc + kwargs["encryptedFields"] = encrypted_fields + kwargs["check_exists"] = False + try: + return ( + database.create_collection(name=name, **kwargs), + encrypted_fields, + ) + except Exception as exc: + raise EncryptedCollectionError(exc, encrypted_fields) from exc + + def create_data_key( + self, + kms_provider: str, + master_key: Optional[Mapping[str, Any]] = None, + key_alt_names: Optional[Sequence[str]] = None, + key_material: Optional[bytes] = None, + ) -> Binary: + """Create and insert a new data key into the key vault collection. + + :param kms_provider: The KMS provider to use. Supported values are + "aws", "azure", "gcp", "kmip", "local", or a named provider like + "kmip:name". + :param master_key: Identifies a KMS-specific key used to encrypt the + new data key. If the kmsProvider is "local" the `master_key` is + not applicable and may be omitted. + + If the `kms_provider` type is "aws" it is required and has the + following fields:: + + - `region` (string): Required. The AWS region, e.g. "us-east-1". + - `key` (string): Required. The Amazon Resource Name (ARN) to + the AWS customer. + - `endpoint` (string): Optional. An alternate host to send KMS + requests to. May include port number, e.g. + "kms.us-east-1.amazonaws.com:443". + + If the `kms_provider` type is "azure" it is required and has the + following fields:: + + - `keyVaultEndpoint` (string): Required. Host with optional + port, e.g. "example.vault.azure.net". + - `keyName` (string): Required. Key name in the key vault. + - `keyVersion` (string): Optional. Version of the key to use. + + If the `kms_provider` type is "gcp" it is required and has the + following fields:: + + - `projectId` (string): Required. The Google cloud project ID. + - `location` (string): Required. The GCP location, e.g. "us-east1". + - `keyRing` (string): Required. Name of the key ring that contains + the key to use. + - `keyName` (string): Required. Name of the key to use. + - `keyVersion` (string): Optional. Version of the key to use. + - `endpoint` (string): Optional. Host with optional port. + Defaults to "cloudkms.googleapis.com". + + If the `kms_provider` type is "kmip" it is optional and has the + following fields:: + + - `keyId` (string): Optional. `keyId` is the KMIP Unique + Identifier to a 96 byte KMIP Secret Data managed object. If + keyId is omitted, the driver creates a random 96 byte KMIP + Secret Data managed object. + - `endpoint` (string): Optional. Host with optional + port, e.g. "example.vault.azure.net:". + + :param key_alt_names: An optional list of string alternate + names used to reference a key. If a key is created with alternate + names, then encryption may refer to the key by the unique alternate + name instead of by ``key_id``. The following example shows creating + and referring to a data key by alternate name:: + + client_encryption.create_data_key("local", key_alt_names=["name1"]) + # reference the key with the alternate name + client_encryption.encrypt("457-55-5462", key_alt_name="name1", + algorithm=Algorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Random) + :param key_material: Sets the custom key material to be used + by the data key for encryption and decryption. + + :return: The ``_id`` of the created data key document as a + :class:`~bson.binary.Binary` with subtype + :data:`~bson.binary.UUID_SUBTYPE`. + + .. versionchanged:: 4.2 + Added the `key_material` parameter. + """ + self._check_closed() + with _wrap_encryption_errors(): + return cast( + Binary, + self._encryption.create_data_key( + kms_provider, + master_key=master_key, + key_alt_names=key_alt_names, + key_material=key_material, + ), + ) + + def _encrypt_helper( + self, + value: Any, + algorithm: str, + key_id: Optional[Union[Binary, uuid.UUID]] = None, + key_alt_name: Optional[str] = None, + query_type: Optional[str] = None, + contention_factor: Optional[int] = None, + range_opts: Optional[RangeOpts] = None, + is_expression: bool = False, + ) -> Any: + self._check_closed() + if isinstance(key_id, uuid.UUID): + key_id = Binary.from_uuid(key_id) + if key_id is not None and not ( + isinstance(key_id, Binary) and key_id.subtype == UUID_SUBTYPE + ): + raise TypeError("key_id must be a bson.binary.Binary with subtype 4") + + doc = encode( + {"v": value}, + codec_options=self._codec_options, + ) + range_opts_bytes = None + if range_opts: + range_opts_bytes = encode( + range_opts.document, + codec_options=self._codec_options, + ) + with _wrap_encryption_errors(): + encrypted_doc = self._encryption.encrypt( + value=doc, + algorithm=algorithm, + key_id=key_id, + key_alt_name=key_alt_name, + query_type=query_type, + contention_factor=contention_factor, + range_opts=range_opts_bytes, + is_expression=is_expression, + ) + return decode(encrypted_doc)["v"] + + def encrypt( + self, + value: Any, + algorithm: str, + key_id: Optional[Union[Binary, uuid.UUID]] = None, + key_alt_name: Optional[str] = None, + query_type: Optional[str] = None, + contention_factor: Optional[int] = None, + range_opts: Optional[RangeOpts] = None, + ) -> Binary: + """Encrypt a BSON value with a given key and algorithm. + + Note that exactly one of ``key_id`` or ``key_alt_name`` must be + provided. + + :param value: The BSON value to encrypt. + :param algorithm` (string): The encryption algorithm to use. See + :class:`Algorithm` for some valid options. + :param key_id: Identifies a data key by ``_id`` which must be a + :class:`~bson.binary.Binary` with subtype 4 ( + :attr:`~bson.binary.UUID_SUBTYPE`). + :param key_alt_name: Identifies a key vault document by 'keyAltName'. + :param query_type` (str): The query type to execute. See :class:`QueryType` for valid options. + :param contention_factor` (int): The contention factor to use + when the algorithm is :attr:`Algorithm.INDEXED`. An integer value + *must* be given when the :attr:`Algorithm.INDEXED` algorithm is + used. + :param range_opts: Experimental only, not intended for public use. + + :return: The encrypted value, a :class:`~bson.binary.Binary` with subtype 6. + + .. versionchanged:: 4.7 + ``key_id`` can now be passed in as a :class:`uuid.UUID`. + + .. versionchanged:: 4.2 + Added the `query_type` and `contention_factor` parameters. + """ + return cast( + Binary, + self._encrypt_helper( + value=value, + algorithm=algorithm, + key_id=key_id, + key_alt_name=key_alt_name, + query_type=query_type, + contention_factor=contention_factor, + range_opts=range_opts, + is_expression=False, + ), + ) + + def encrypt_expression( + self, + expression: Mapping[str, Any], + algorithm: str, + key_id: Optional[Union[Binary, uuid.UUID]] = None, + key_alt_name: Optional[str] = None, + query_type: Optional[str] = None, + contention_factor: Optional[int] = None, + range_opts: Optional[RangeOpts] = None, + ) -> RawBSONDocument: + """Encrypt a BSON expression with a given key and algorithm. + + Note that exactly one of ``key_id`` or ``key_alt_name`` must be + provided. + + :param expression: The BSON aggregate or match expression to encrypt. + :param algorithm` (string): The encryption algorithm to use. See + :class:`Algorithm` for some valid options. + :param key_id: Identifies a data key by ``_id`` which must be a + :class:`~bson.binary.Binary` with subtype 4 ( + :attr:`~bson.binary.UUID_SUBTYPE`). + :param key_alt_name: Identifies a key vault document by 'keyAltName'. + :param query_type` (str): The query type to execute. See + :class:`QueryType` for valid options. + :param contention_factor` (int): The contention factor to use + when the algorithm is :attr:`Algorithm.INDEXED`. An integer value + *must* be given when the :attr:`Algorithm.INDEXED` algorithm is + used. + :param range_opts: Experimental only, not intended for public use. + + :return: The encrypted expression, a :class:`~bson.RawBSONDocument`. + + .. versionchanged:: 4.7 + ``key_id`` can now be passed in as a :class:`uuid.UUID`. + + .. versionadded:: 4.4 + """ + return cast( + RawBSONDocument, + self._encrypt_helper( + value=expression, + algorithm=algorithm, + key_id=key_id, + key_alt_name=key_alt_name, + query_type=query_type, + contention_factor=contention_factor, + range_opts=range_opts, + is_expression=True, + ), + ) + + def decrypt(self, value: Binary) -> Any: + """Decrypt an encrypted value. + + :param value` (Binary): The encrypted value, a + :class:`~bson.binary.Binary` with subtype 6. + + :return: The decrypted BSON value. + """ + self._check_closed() + if not (isinstance(value, Binary) and value.subtype == 6): + raise TypeError("value to decrypt must be a bson.binary.Binary with subtype 6") + + with _wrap_encryption_errors(): + doc = encode({"v": value}) + decrypted_doc = self._encryption.decrypt(doc) + return decode(decrypted_doc, codec_options=self._codec_options)["v"] + + def get_key(self, id: Binary) -> Optional[RawBSONDocument]: + """Get a data key by id. + + :param id` (Binary): The UUID of a key a which must be a + :class:`~bson.binary.Binary` with subtype 4 ( + :attr:`~bson.binary.UUID_SUBTYPE`). + + :return: The key document. + + .. versionadded:: 4.2 + """ + self._check_closed() + assert self._key_vault_coll is not None + return self._key_vault_coll.find_one({"_id": id}) + + def get_keys(self) -> Cursor[RawBSONDocument]: + """Get all of the data keys. + + :return: An instance of :class:`~pymongo.cursor.Cursor` over the data key + documents. + + .. versionadded:: 4.2 + """ + self._check_closed() + assert self._key_vault_coll is not None + return self._key_vault_coll.find({}) + + def delete_key(self, id: Binary) -> DeleteResult: + """Delete a key document in the key vault collection that has the given ``key_id``. + + :param id` (Binary): The UUID of a key a which must be a + :class:`~bson.binary.Binary` with subtype 4 ( + :attr:`~bson.binary.UUID_SUBTYPE`). + + :return: The delete result. + + .. versionadded:: 4.2 + """ + self._check_closed() + assert self._key_vault_coll is not None + return self._key_vault_coll.delete_one({"_id": id}) + + def add_key_alt_name(self, id: Binary, key_alt_name: str) -> Any: + """Add ``key_alt_name`` to the set of alternate names in the key document with UUID ``key_id``. + + :param `id`: The UUID of a key a which must be a + :class:`~bson.binary.Binary` with subtype 4 ( + :attr:`~bson.binary.UUID_SUBTYPE`). + :param `key_alt_name`: The key alternate name to add. + + :return: The previous version of the key document. + + .. versionadded:: 4.2 + """ + self._check_closed() + update = {"$addToSet": {"keyAltNames": key_alt_name}} + assert self._key_vault_coll is not None + return self._key_vault_coll.find_one_and_update({"_id": id}, update) + + def get_key_by_alt_name(self, key_alt_name: str) -> Optional[RawBSONDocument]: + """Get a key document in the key vault collection that has the given ``key_alt_name``. + + :param key_alt_name: (str): The key alternate name of the key to get. + + :return: The key document. + + .. versionadded:: 4.2 + """ + self._check_closed() + assert self._key_vault_coll is not None + return self._key_vault_coll.find_one({"keyAltNames": key_alt_name}) + + def remove_key_alt_name(self, id: Binary, key_alt_name: str) -> Optional[RawBSONDocument]: + """Remove ``key_alt_name`` from the set of keyAltNames in the key document with UUID ``id``. + + Also removes the ``keyAltNames`` field from the key document if it would otherwise be empty. + + :param `id`: The UUID of a key a which must be a + :class:`~bson.binary.Binary` with subtype 4 ( + :attr:`~bson.binary.UUID_SUBTYPE`). + :param `key_alt_name`: The key alternate name to remove. + + :return: Returns the previous version of the key document. + + .. versionadded:: 4.2 + """ + self._check_closed() + pipeline = [ + { + "$set": { + "keyAltNames": { + "$cond": [ + {"$eq": ["$keyAltNames", [key_alt_name]]}, + "$$REMOVE", + { + "$filter": { + "input": "$keyAltNames", + "cond": {"$ne": ["$$this", key_alt_name]}, + } + }, + ] + } + } + } + ] + assert self._key_vault_coll is not None + return self._key_vault_coll.find_one_and_update({"_id": id}, pipeline) + + def rewrap_many_data_key( + self, + filter: Mapping[str, Any], + provider: Optional[str] = None, + master_key: Optional[Mapping[str, Any]] = None, + ) -> RewrapManyDataKeyResult: + """Decrypts and encrypts all matching data keys in the key vault with a possibly new `master_key` value. + + :param filter: A document used to filter the data keys. + :param provider: The new KMS provider to use to encrypt the data keys, + or ``None`` to use the current KMS provider(s). + :param `master_key`: The master key fields corresponding to the new KMS + provider when ``provider`` is not ``None``. + + :return: A :class:`RewrapManyDataKeyResult`. + + This method allows you to re-encrypt all of your data-keys with a new CMK, or master key. + Note that this does *not* require re-encrypting any of the data in your encrypted collections, + but rather refreshes the key that protects the keys that encrypt the data: + + .. code-block:: python + + client_encryption.rewrap_many_data_key( + filter={"keyAltNames": "optional filter for which keys you want to update"}, + master_key={ + "provider": "azure", # replace with your cloud provider + "master_key": { + # put the rest of your master_key options here + "key": "" + }, + }, + ) + + .. versionadded:: 4.2 + """ + if master_key is not None and provider is None: + raise ConfigurationError("A provider must be given if a master_key is given") + self._check_closed() + with _wrap_encryption_errors(): + raw_result = self._encryption.rewrap_many_data_key(filter, provider, master_key) + if raw_result is None: + return RewrapManyDataKeyResult() + + raw_doc = RawBSONDocument(raw_result, DEFAULT_RAW_BSON_OPTIONS) + replacements = [] + for key in raw_doc["v"]: + update_model = { + "$set": {"keyMaterial": key["keyMaterial"], "masterKey": key["masterKey"]}, + "$currentDate": {"updateDate": True}, + } + op = UpdateOne({"_id": key["_id"]}, update_model) + replacements.append(op) + if not replacements: + return RewrapManyDataKeyResult() + assert self._key_vault_coll is not None + result = self._key_vault_coll.bulk_write(replacements) + return RewrapManyDataKeyResult(result) + + def __enter__(self) -> ClientEncryption[_DocumentType]: + return self + + def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None: + self.close() + + def _check_closed(self) -> None: + if self._encryption is None: + raise InvalidOperation("Cannot use closed ClientEncryption") + + def close(self) -> None: + """Release resources. + + Note that using this class in a with-statement will automatically call + :meth:`close`:: + + with ClientEncryption(...) as client_encryption: + encrypted = client_encryption.encrypt(value, ...) + decrypted = client_encryption.decrypt(encrypted) + + """ + if self._io_callbacks: + self._io_callbacks.close() + self._encryption.close() + self._io_callbacks = None + self._encryption = None diff --git a/venv/Lib/site-packages/pymongo/encryption_options.py b/venv/Lib/site-packages/pymongo/encryption_options.py new file mode 100644 index 00000000..1d536997 --- /dev/null +++ b/venv/Lib/site-packages/pymongo/encryption_options.py @@ -0,0 +1,268 @@ +# Copyright 2019-present MongoDB, 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. + +"""Support for automatic client-side field level encryption.""" +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, Mapping, Optional + +try: + import pymongocrypt # type:ignore[import] # noqa: F401 + + _HAVE_PYMONGOCRYPT = True +except ImportError: + _HAVE_PYMONGOCRYPT = False +from bson import int64 +from pymongo.common import validate_is_mapping +from pymongo.errors import ConfigurationError +from pymongo.uri_parser import _parse_kms_tls_options + +if TYPE_CHECKING: + from pymongo.mongo_client import MongoClient + from pymongo.typings import _DocumentTypeArg + + +class AutoEncryptionOpts: + """Options to configure automatic client-side field level encryption.""" + + def __init__( + self, + kms_providers: Mapping[str, Any], + key_vault_namespace: str, + key_vault_client: Optional[MongoClient[_DocumentTypeArg]] = None, + schema_map: Optional[Mapping[str, Any]] = None, + bypass_auto_encryption: bool = False, + mongocryptd_uri: str = "mongodb://localhost:27020", + mongocryptd_bypass_spawn: bool = False, + mongocryptd_spawn_path: str = "mongocryptd", + mongocryptd_spawn_args: Optional[list[str]] = None, + kms_tls_options: Optional[Mapping[str, Any]] = None, + crypt_shared_lib_path: Optional[str] = None, + crypt_shared_lib_required: bool = False, + bypass_query_analysis: bool = False, + encrypted_fields_map: Optional[Mapping[str, Any]] = None, + ) -> None: + """Options to configure automatic client-side field level encryption. + + Automatic client-side field level encryption requires MongoDB >=4.2 + enterprise or a MongoDB >=4.2 Atlas cluster. Automatic encryption is not + supported for operations on a database or view and will result in + error. + + Although automatic encryption requires MongoDB >=4.2 enterprise or a + MongoDB >=4.2 Atlas cluster, automatic *decryption* is supported for all + users. To configure automatic *decryption* without automatic + *encryption* set ``bypass_auto_encryption=True``. Explicit + encryption and explicit decryption is also supported for all users + with the :class:`~pymongo.encryption.ClientEncryption` class. + + See :ref:`automatic-client-side-encryption` for an example. + + :param kms_providers: Map of KMS provider options. The `kms_providers` + map values differ by provider: + + - `aws`: Map with "accessKeyId" and "secretAccessKey" as strings. + These are the AWS access key ID and AWS secret access key used + to generate KMS messages. An optional "sessionToken" may be + included to support temporary AWS credentials. + - `azure`: Map with "tenantId", "clientId", and "clientSecret" as + strings. Additionally, "identityPlatformEndpoint" may also be + specified as a string (defaults to 'login.microsoftonline.com'). + These are the Azure Active Directory credentials used to + generate Azure Key Vault messages. + - `gcp`: Map with "email" as a string and "privateKey" + as `bytes` or a base64 encoded string. + Additionally, "endpoint" may also be specified as a string + (defaults to 'oauth2.googleapis.com'). These are the + credentials used to generate Google Cloud KMS messages. + - `kmip`: Map with "endpoint" as a host with required port. + For example: ``{"endpoint": "example.com:443"}``. + - `local`: Map with "key" as `bytes` (96 bytes in length) or + a base64 encoded string which decodes + to 96 bytes. "key" is the master key used to encrypt/decrypt + data keys. This key should be generated and stored as securely + as possible. + + KMS providers may be specified with an optional name suffix + separated by a colon, for example "kmip:name" or "aws:name". + Named KMS providers do not support :ref:`CSFLE on-demand credentials`. + Named KMS providers enables more than one of each KMS provider type to be configured. + For example, to configure multiple local KMS providers:: + + kms_providers = { + "local": {"key": local_kek1}, # Unnamed KMS provider. + "local:myname": {"key": local_kek2}, # Named KMS provider with name "myname". + } + + :param key_vault_namespace: The namespace for the key vault collection. + The key vault collection contains all data keys used for encryption + and decryption. Data keys are stored as documents in this MongoDB + collection. Data keys are protected with encryption by a KMS + provider. + :param key_vault_client: By default, the key vault collection + is assumed to reside in the same MongoDB cluster as the encrypted + MongoClient. Use this option to route data key queries to a + separate MongoDB cluster. + :param schema_map: Map of collection namespace ("db.coll") to + JSON Schema. By default, a collection's JSONSchema is periodically + polled with the listCollections command. But a JSONSchema may be + specified locally with the schemaMap option. + + **Supplying a `schema_map` provides more security than relying on + JSON Schemas obtained from the server. It protects against a + malicious server advertising a false JSON Schema, which could trick + the client into sending unencrypted data that should be + encrypted.** + + Schemas supplied in the schemaMap only apply to configuring + automatic encryption for client side encryption. Other validation + rules in the JSON schema will not be enforced by the driver and + will result in an error. + :param bypass_auto_encryption: If ``True``, automatic + encryption will be disabled but automatic decryption will still be + enabled. Defaults to ``False``. + :param mongocryptd_uri: The MongoDB URI used to connect + to the *local* mongocryptd process. Defaults to + ``'mongodb://localhost:27020'``. + :param mongocryptd_bypass_spawn: If ``True``, the encrypted + MongoClient will not attempt to spawn the mongocryptd process. + Defaults to ``False``. + :param mongocryptd_spawn_path: Used for spawning the + mongocryptd process. Defaults to ``'mongocryptd'`` and spawns + mongocryptd from the system path. + :param mongocryptd_spawn_args: A list of string arguments to + use when spawning the mongocryptd process. Defaults to + ``['--idleShutdownTimeoutSecs=60']``. If the list does not include + the ``idleShutdownTimeoutSecs`` option then + ``'--idleShutdownTimeoutSecs=60'`` will be added. + :param kms_tls_options: A map of KMS provider names to TLS + options to use when creating secure connections to KMS providers. + Accepts the same TLS options as + :class:`pymongo.mongo_client.MongoClient`. For example, to + override the system default CA file:: + + kms_tls_options={'kmip': {'tlsCAFile': certifi.where()}} + + Or to supply a client certificate:: + + kms_tls_options={'kmip': {'tlsCertificateKeyFile': 'client.pem'}} + :param crypt_shared_lib_path: Override the path to load the crypt_shared library. + :param crypt_shared_lib_required: If True, raise an error if libmongocrypt is + unable to load the crypt_shared library. + :param bypass_query_analysis: If ``True``, disable automatic analysis + of outgoing commands. Set `bypass_query_analysis` to use explicit + encryption on indexed fields without the MongoDB Enterprise Advanced + licensed crypt_shared library. + :param encrypted_fields_map: Map of collection namespace ("db.coll") to documents + that described the encrypted fields for Queryable Encryption. For example:: + + { + "db.encryptedCollection": { + "escCollection": "enxcol_.encryptedCollection.esc", + "ecocCollection": "enxcol_.encryptedCollection.ecoc", + "fields": [ + { + "path": "firstName", + "keyId": Binary.from_uuid(UUID('00000000-0000-0000-0000-000000000000')), + "bsonType": "string", + "queries": {"queryType": "equality"} + }, + { + "path": "ssn", + "keyId": Binary.from_uuid(UUID('04104104-1041-0410-4104-104104104104')), + "bsonType": "string" + } + ] + } + } + + .. versionchanged:: 4.2 + Added `encrypted_fields_map` `crypt_shared_lib_path`, `crypt_shared_lib_required`, + and `bypass_query_analysis` parameters. + + .. versionchanged:: 4.0 + Added the `kms_tls_options` parameter and the "kmip" KMS provider. + + .. versionadded:: 3.9 + """ + if not _HAVE_PYMONGOCRYPT: + raise ConfigurationError( + "client side encryption requires the pymongocrypt library: " + "install a compatible version with: " + "python -m pip install 'pymongo[encryption]'" + ) + if encrypted_fields_map: + validate_is_mapping("encrypted_fields_map", encrypted_fields_map) + self._encrypted_fields_map = encrypted_fields_map + self._bypass_query_analysis = bypass_query_analysis + self._crypt_shared_lib_path = crypt_shared_lib_path + self._crypt_shared_lib_required = crypt_shared_lib_required + self._kms_providers = kms_providers + self._key_vault_namespace = key_vault_namespace + self._key_vault_client = key_vault_client + self._schema_map = schema_map + self._bypass_auto_encryption = bypass_auto_encryption + self._mongocryptd_uri = mongocryptd_uri + self._mongocryptd_bypass_spawn = mongocryptd_bypass_spawn + self._mongocryptd_spawn_path = mongocryptd_spawn_path + if mongocryptd_spawn_args is None: + mongocryptd_spawn_args = ["--idleShutdownTimeoutSecs=60"] + self._mongocryptd_spawn_args = mongocryptd_spawn_args + if not isinstance(self._mongocryptd_spawn_args, list): + raise TypeError("mongocryptd_spawn_args must be a list") + if not any("idleShutdownTimeoutSecs" in s for s in self._mongocryptd_spawn_args): + self._mongocryptd_spawn_args.append("--idleShutdownTimeoutSecs=60") + # Maps KMS provider name to a SSLContext. + self._kms_ssl_contexts = _parse_kms_tls_options(kms_tls_options) + self._bypass_query_analysis = bypass_query_analysis + + +class RangeOpts: + """Options to configure encrypted queries using the rangePreview algorithm.""" + + def __init__( + self, + sparsity: int, + min: Optional[Any] = None, + max: Optional[Any] = None, + precision: Optional[int] = None, + ) -> None: + """Options to configure encrypted queries using the rangePreview algorithm. + + .. note:: This feature is experimental only, and not intended for public use. + + :param sparsity: An integer. + :param min: A BSON scalar value corresponding to the type being queried. + :param max: A BSON scalar value corresponding to the type being queried. + :param precision: An integer, may only be set for double or decimal128 types. + + .. versionadded:: 4.4 + """ + self.min = min + self.max = max + self.sparsity = sparsity + self.precision = precision + + @property + def document(self) -> dict[str, Any]: + doc = {} + for k, v in [ + ("sparsity", int64.Int64(self.sparsity)), + ("precision", self.precision), + ("min", self.min), + ("max", self.max), + ]: + if v is not None: + doc[k] = v + return doc diff --git a/venv/Lib/site-packages/pymongo/errors.py b/venv/Lib/site-packages/pymongo/errors.py new file mode 100644 index 00000000..a781e4a0 --- /dev/null +++ b/venv/Lib/site-packages/pymongo/errors.py @@ -0,0 +1,376 @@ +# Copyright 2009-present MongoDB, 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. + +"""Exceptions raised by PyMongo.""" +from __future__ import annotations + +from ssl import SSLCertVerificationError as _CertificateError # noqa: F401 +from typing import TYPE_CHECKING, Any, Iterable, Mapping, Optional, Sequence, Union + +from bson.errors import InvalidDocument + +if TYPE_CHECKING: + from pymongo.typings import _DocumentOut + + +class PyMongoError(Exception): + """Base class for all PyMongo exceptions.""" + + def __init__(self, message: str = "", error_labels: Optional[Iterable[str]] = None) -> None: + super().__init__(message) + self._message = message + self._error_labels = set(error_labels or []) + + def has_error_label(self, label: str) -> bool: + """Return True if this error contains the given label. + + .. versionadded:: 3.7 + """ + return label in self._error_labels + + def _add_error_label(self, label: str) -> None: + """Add the given label to this error.""" + self._error_labels.add(label) + + def _remove_error_label(self, label: str) -> None: + """Remove the given label from this error.""" + self._error_labels.discard(label) + + @property + def timeout(self) -> bool: + """True if this error was caused by a timeout. + + .. versionadded:: 4.2 + """ + return False + + +class ProtocolError(PyMongoError): + """Raised for failures related to the wire protocol.""" + + +class ConnectionFailure(PyMongoError): + """Raised when a connection to the database cannot be made or is lost.""" + + +class WaitQueueTimeoutError(ConnectionFailure): + """Raised when an operation times out waiting to checkout a connection from the pool. + + Subclass of :exc:`~pymongo.errors.ConnectionFailure`. + + .. versionadded:: 4.2 + """ + + @property + def timeout(self) -> bool: + return True + + +class AutoReconnect(ConnectionFailure): + """Raised when a connection to the database is lost and an attempt to + auto-reconnect will be made. + + In order to auto-reconnect you must handle this exception, recognizing that + the operation which caused it has not necessarily succeeded. Future + operations will attempt to open a new connection to the database (and + will continue to raise this exception until the first successful + connection is made). + + Subclass of :exc:`~pymongo.errors.ConnectionFailure`. + """ + + errors: Union[Mapping[str, Any], Sequence[Any]] + details: Union[Mapping[str, Any], Sequence[Any]] + + def __init__( + self, message: str = "", errors: Optional[Union[Mapping[str, Any], Sequence[Any]]] = None + ) -> None: + error_labels = None + if errors is not None: + if isinstance(errors, dict): + error_labels = errors.get("errorLabels") + super().__init__(message, error_labels) + self.errors = self.details = errors or [] + + +class NetworkTimeout(AutoReconnect): + """An operation on an open connection exceeded socketTimeoutMS. + + The remaining connections in the pool stay open. In the case of a write + operation, you cannot know whether it succeeded or failed. + + Subclass of :exc:`~pymongo.errors.AutoReconnect`. + """ + + @property + def timeout(self) -> bool: + return True + + +def _format_detailed_error( + message: str, details: Optional[Union[Mapping[str, Any], list[Any]]] +) -> str: + if details is not None: + message = f"{message}, full error: {details}" + return message + + +class NotPrimaryError(AutoReconnect): + """The server responded "not primary" or "node is recovering". + + These errors result from a query, write, or command. The operation failed + because the client thought it was using the primary but the primary has + stepped down, or the client thought it was using a healthy secondary but + the secondary is stale and trying to recover. + + The client launches a refresh operation on a background thread, to update + its view of the server as soon as possible after throwing this exception. + + Subclass of :exc:`~pymongo.errors.AutoReconnect`. + + .. versionadded:: 3.12 + """ + + def __init__( + self, message: str = "", errors: Optional[Union[Mapping[str, Any], list[Any]]] = None + ) -> None: + super().__init__(_format_detailed_error(message, errors), errors=errors) + + +class ServerSelectionTimeoutError(AutoReconnect): + """Thrown when no MongoDB server is available for an operation + + If there is no suitable server for an operation PyMongo tries for + ``serverSelectionTimeoutMS`` (default 30 seconds) to find one, then + throws this exception. For example, it is thrown after attempting an + operation when PyMongo cannot connect to any server, or if you attempt + an insert into a replica set that has no primary and does not elect one + within the timeout window, or if you attempt to query with a Read + Preference that the replica set cannot satisfy. + """ + + @property + def timeout(self) -> bool: + return True + + +class ConfigurationError(PyMongoError): + """Raised when something is incorrectly configured.""" + + +class OperationFailure(PyMongoError): + """Raised when a database operation fails. + + .. versionadded:: 2.7 + The :attr:`details` attribute. + """ + + def __init__( + self, + error: str, + code: Optional[int] = None, + details: Optional[Mapping[str, Any]] = None, + max_wire_version: Optional[int] = None, + ) -> None: + error_labels = None + if details is not None: + error_labels = details.get("errorLabels") + super().__init__(_format_detailed_error(error, details), error_labels=error_labels) + self.__code = code + self.__details = details + self.__max_wire_version = max_wire_version + + @property + def _max_wire_version(self) -> Optional[int]: + return self.__max_wire_version + + @property + def code(self) -> Optional[int]: + """The error code returned by the server, if any.""" + return self.__code + + @property + def details(self) -> Optional[Mapping[str, Any]]: + """The complete error document returned by the server. + + Depending on the error that occurred, the error document + may include useful information beyond just the error + message. When connected to a mongos the error document + may contain one or more subdocuments if errors occurred + on multiple shards. + """ + return self.__details + + @property + def timeout(self) -> bool: + return self.__code in (50,) + + +class CursorNotFound(OperationFailure): + """Raised while iterating query results if the cursor is + invalidated on the server. + + .. versionadded:: 2.7 + """ + + +class ExecutionTimeout(OperationFailure): + """Raised when a database operation times out, exceeding the $maxTimeMS + set in the query or command option. + + .. note:: Requires server version **>= 2.6.0** + + .. versionadded:: 2.7 + """ + + @property + def timeout(self) -> bool: + return True + + +class WriteConcernError(OperationFailure): + """Base exception type for errors raised due to write concern. + + .. versionadded:: 3.0 + """ + + +class WriteError(OperationFailure): + """Base exception type for errors raised during write operations. + + .. versionadded:: 3.0 + """ + + +class WTimeoutError(WriteConcernError): + """Raised when a database operation times out (i.e. wtimeout expires) + before replication completes. + + With newer versions of MongoDB the `details` attribute may include + write concern fields like 'n', 'updatedExisting', or 'writtenTo'. + + .. versionadded:: 2.7 + """ + + @property + def timeout(self) -> bool: + return True + + +class DuplicateKeyError(WriteError): + """Raised when an insert or update fails due to a duplicate key error.""" + + +def _wtimeout_error(error: Any) -> bool: + """Return True if this writeConcernError doc is a caused by a timeout.""" + return error.get("code") == 50 or ("errInfo" in error and error["errInfo"].get("wtimeout")) + + +class BulkWriteError(OperationFailure): + """Exception class for bulk write errors. + + .. versionadded:: 2.7 + """ + + details: _DocumentOut + + def __init__(self, results: _DocumentOut) -> None: + super().__init__("batch op errors occurred", 65, results) + + def __reduce__(self) -> tuple[Any, Any]: + return self.__class__, (self.details,) + + @property + def timeout(self) -> bool: + # Check the last writeConcernError and last writeError to determine if this + # BulkWriteError was caused by a timeout. + wces = self.details.get("writeConcernErrors", []) + if wces and _wtimeout_error(wces[-1]): + return True + + werrs = self.details.get("writeErrors", []) + if werrs and werrs[-1].get("code") == 50: + return True + return False + + +class InvalidOperation(PyMongoError): + """Raised when a client attempts to perform an invalid operation.""" + + +class InvalidName(PyMongoError): + """Raised when an invalid name is used.""" + + +class CollectionInvalid(PyMongoError): + """Raised when collection validation fails.""" + + +class InvalidURI(ConfigurationError): + """Raised when trying to parse an invalid mongodb URI.""" + + +class DocumentTooLarge(InvalidDocument): + """Raised when an encoded document is too large for the connected server.""" + + +class EncryptionError(PyMongoError): + """Raised when encryption or decryption fails. + + This error always wraps another exception which can be retrieved via the + :attr:`cause` property. + + .. versionadded:: 3.9 + """ + + def __init__(self, cause: Exception) -> None: + super().__init__(str(cause)) + self.__cause = cause + + @property + def cause(self) -> Exception: + """The exception that caused this encryption or decryption error.""" + return self.__cause + + @property + def timeout(self) -> bool: + if isinstance(self.__cause, PyMongoError): + return self.__cause.timeout + return False + + +class EncryptedCollectionError(EncryptionError): + """Raised when creating a collection with encrypted_fields fails. + + .. versionadded:: 4.4 + """ + + def __init__(self, cause: Exception, encrypted_fields: Mapping[str, Any]) -> None: + super().__init__(cause) + self.__encrypted_fields = encrypted_fields + + @property + def encrypted_fields(self) -> Mapping[str, Any]: + """The encrypted_fields document that allows inferring which data keys are *known* to be created. + + Note that the returned document is not guaranteed to contain information about *all* of the data keys that + were created, for example in the case of an indefinite error like a timeout. Use the `cause` property to + determine whether a definite or indefinite error caused this error, and only rely on the accuracy of the + encrypted_fields if the error is definite. + """ + return self.__encrypted_fields + + +class _OperationCancelled(AutoReconnect): + """Internal error raised when a socket operation is cancelled.""" diff --git a/venv/Lib/site-packages/pymongo/event_loggers.py b/venv/Lib/site-packages/pymongo/event_loggers.py new file mode 100644 index 00000000..287db3fc --- /dev/null +++ b/venv/Lib/site-packages/pymongo/event_loggers.py @@ -0,0 +1,223 @@ +# Copyright 2020-present MongoDB, 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. + + +"""Example event logger classes. + +.. versionadded:: 3.11 + +These loggers can be registered using :func:`register` or +:class:`~pymongo.mongo_client.MongoClient`. + +``monitoring.register(CommandLogger())`` + +or + +``MongoClient(event_listeners=[CommandLogger()])`` +""" +from __future__ import annotations + +import logging + +from pymongo import monitoring + + +class CommandLogger(monitoring.CommandListener): + """A simple listener that logs command events. + + Listens for :class:`~pymongo.monitoring.CommandStartedEvent`, + :class:`~pymongo.monitoring.CommandSucceededEvent` and + :class:`~pymongo.monitoring.CommandFailedEvent` events and + logs them at the `INFO` severity level using :mod:`logging`. + .. versionadded:: 3.11 + """ + + def started(self, event: monitoring.CommandStartedEvent) -> None: + logging.info( + f"Command {event.command_name} with request id " + f"{event.request_id} started on server " + f"{event.connection_id}" + ) + + def succeeded(self, event: monitoring.CommandSucceededEvent) -> None: + logging.info( + f"Command {event.command_name} with request id " + f"{event.request_id} on server {event.connection_id} " + f"succeeded in {event.duration_micros} " + "microseconds" + ) + + def failed(self, event: monitoring.CommandFailedEvent) -> None: + logging.info( + f"Command {event.command_name} with request id " + f"{event.request_id} on server {event.connection_id} " + f"failed in {event.duration_micros} " + "microseconds" + ) + + +class ServerLogger(monitoring.ServerListener): + """A simple listener that logs server discovery events. + + Listens for :class:`~pymongo.monitoring.ServerOpeningEvent`, + :class:`~pymongo.monitoring.ServerDescriptionChangedEvent`, + and :class:`~pymongo.monitoring.ServerClosedEvent` + events and logs them at the `INFO` severity level using :mod:`logging`. + + .. versionadded:: 3.11 + """ + + def opened(self, event: monitoring.ServerOpeningEvent) -> None: + logging.info(f"Server {event.server_address} added to topology {event.topology_id}") + + def description_changed(self, event: monitoring.ServerDescriptionChangedEvent) -> None: + previous_server_type = event.previous_description.server_type + new_server_type = event.new_description.server_type + if new_server_type != previous_server_type: + # server_type_name was added in PyMongo 3.4 + logging.info( + f"Server {event.server_address} changed type from " + f"{event.previous_description.server_type_name} to " + f"{event.new_description.server_type_name}" + ) + + def closed(self, event: monitoring.ServerClosedEvent) -> None: + logging.warning(f"Server {event.server_address} removed from topology {event.topology_id}") + + +class HeartbeatLogger(monitoring.ServerHeartbeatListener): + """A simple listener that logs server heartbeat events. + + Listens for :class:`~pymongo.monitoring.ServerHeartbeatStartedEvent`, + :class:`~pymongo.monitoring.ServerHeartbeatSucceededEvent`, + and :class:`~pymongo.monitoring.ServerHeartbeatFailedEvent` + events and logs them at the `INFO` severity level using :mod:`logging`. + + .. versionadded:: 3.11 + """ + + def started(self, event: monitoring.ServerHeartbeatStartedEvent) -> None: + logging.info(f"Heartbeat sent to server {event.connection_id}") + + def succeeded(self, event: monitoring.ServerHeartbeatSucceededEvent) -> None: + # The reply.document attribute was added in PyMongo 3.4. + logging.info( + f"Heartbeat to server {event.connection_id} " + "succeeded with reply " + f"{event.reply.document}" + ) + + def failed(self, event: monitoring.ServerHeartbeatFailedEvent) -> None: + logging.warning( + f"Heartbeat to server {event.connection_id} failed with error {event.reply}" + ) + + +class TopologyLogger(monitoring.TopologyListener): + """A simple listener that logs server topology events. + + Listens for :class:`~pymongo.monitoring.TopologyOpenedEvent`, + :class:`~pymongo.monitoring.TopologyDescriptionChangedEvent`, + and :class:`~pymongo.monitoring.TopologyClosedEvent` + events and logs them at the `INFO` severity level using :mod:`logging`. + + .. versionadded:: 3.11 + """ + + def opened(self, event: monitoring.TopologyOpenedEvent) -> None: + logging.info(f"Topology with id {event.topology_id} opened") + + def description_changed(self, event: monitoring.TopologyDescriptionChangedEvent) -> None: + logging.info(f"Topology description updated for topology id {event.topology_id}") + previous_topology_type = event.previous_description.topology_type + new_topology_type = event.new_description.topology_type + if new_topology_type != previous_topology_type: + # topology_type_name was added in PyMongo 3.4 + logging.info( + f"Topology {event.topology_id} changed type from " + f"{event.previous_description.topology_type_name} to " + f"{event.new_description.topology_type_name}" + ) + # The has_writable_server and has_readable_server methods + # were added in PyMongo 3.4. + if not event.new_description.has_writable_server(): + logging.warning("No writable servers available.") + if not event.new_description.has_readable_server(): + logging.warning("No readable servers available.") + + def closed(self, event: monitoring.TopologyClosedEvent) -> None: + logging.info(f"Topology with id {event.topology_id} closed") + + +class ConnectionPoolLogger(monitoring.ConnectionPoolListener): + """A simple listener that logs server connection pool events. + + Listens for :class:`~pymongo.monitoring.PoolCreatedEvent`, + :class:`~pymongo.monitoring.PoolClearedEvent`, + :class:`~pymongo.monitoring.PoolClosedEvent`, + :~pymongo.monitoring.class:`ConnectionCreatedEvent`, + :class:`~pymongo.monitoring.ConnectionReadyEvent`, + :class:`~pymongo.monitoring.ConnectionClosedEvent`, + :class:`~pymongo.monitoring.ConnectionCheckOutStartedEvent`, + :class:`~pymongo.monitoring.ConnectionCheckOutFailedEvent`, + :class:`~pymongo.monitoring.ConnectionCheckedOutEvent`, + and :class:`~pymongo.monitoring.ConnectionCheckedInEvent` + events and logs them at the `INFO` severity level using :mod:`logging`. + + .. versionadded:: 3.11 + """ + + def pool_created(self, event: monitoring.PoolCreatedEvent) -> None: + logging.info(f"[pool {event.address}] pool created") + + def pool_ready(self, event: monitoring.PoolReadyEvent) -> None: + logging.info(f"[pool {event.address}] pool ready") + + def pool_cleared(self, event: monitoring.PoolClearedEvent) -> None: + logging.info(f"[pool {event.address}] pool cleared") + + def pool_closed(self, event: monitoring.PoolClosedEvent) -> None: + logging.info(f"[pool {event.address}] pool closed") + + def connection_created(self, event: monitoring.ConnectionCreatedEvent) -> None: + logging.info(f"[pool {event.address}][conn #{event.connection_id}] connection created") + + def connection_ready(self, event: monitoring.ConnectionReadyEvent) -> None: + logging.info( + f"[pool {event.address}][conn #{event.connection_id}] connection setup succeeded" + ) + + def connection_closed(self, event: monitoring.ConnectionClosedEvent) -> None: + logging.info( + f"[pool {event.address}][conn #{event.connection_id}] " + f'connection closed, reason: "{event.reason}"' + ) + + def connection_check_out_started( + self, event: monitoring.ConnectionCheckOutStartedEvent + ) -> None: + logging.info(f"[pool {event.address}] connection check out started") + + def connection_check_out_failed(self, event: monitoring.ConnectionCheckOutFailedEvent) -> None: + logging.info(f"[pool {event.address}] connection check out failed, reason: {event.reason}") + + def connection_checked_out(self, event: monitoring.ConnectionCheckedOutEvent) -> None: + logging.info( + f"[pool {event.address}][conn #{event.connection_id}] connection checked out of pool" + ) + + def connection_checked_in(self, event: monitoring.ConnectionCheckedInEvent) -> None: + logging.info( + f"[pool {event.address}][conn #{event.connection_id}] connection checked into pool" + ) diff --git a/venv/Lib/site-packages/pymongo/hello.py b/venv/Lib/site-packages/pymongo/hello.py new file mode 100644 index 00000000..0f6d7a39 --- /dev/null +++ b/venv/Lib/site-packages/pymongo/hello.py @@ -0,0 +1,224 @@ +# Copyright 2021-present MongoDB, 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. + +"""Helpers for the 'hello' and legacy hello commands.""" +from __future__ import annotations + +import copy +import datetime +import itertools +from typing import Any, Generic, Mapping, Optional + +from bson.objectid import ObjectId +from pymongo import common +from pymongo.server_type import SERVER_TYPE +from pymongo.typings import ClusterTime, _DocumentType + + +class HelloCompat: + CMD = "hello" + LEGACY_CMD = "ismaster" + PRIMARY = "isWritablePrimary" + LEGACY_PRIMARY = "ismaster" + LEGACY_ERROR = "not master" + + +def _get_server_type(doc: Mapping[str, Any]) -> int: + """Determine the server type from a hello response.""" + if not doc.get("ok"): + return SERVER_TYPE.Unknown + + if doc.get("serviceId"): + return SERVER_TYPE.LoadBalancer + elif doc.get("isreplicaset"): + return SERVER_TYPE.RSGhost + elif doc.get("setName"): + if doc.get("hidden"): + return SERVER_TYPE.RSOther + elif doc.get(HelloCompat.PRIMARY): + return SERVER_TYPE.RSPrimary + elif doc.get(HelloCompat.LEGACY_PRIMARY): + return SERVER_TYPE.RSPrimary + elif doc.get("secondary"): + return SERVER_TYPE.RSSecondary + elif doc.get("arbiterOnly"): + return SERVER_TYPE.RSArbiter + else: + return SERVER_TYPE.RSOther + elif doc.get("msg") == "isdbgrid": + return SERVER_TYPE.Mongos + else: + return SERVER_TYPE.Standalone + + +class Hello(Generic[_DocumentType]): + """Parse a hello response from the server. + + .. versionadded:: 3.12 + """ + + __slots__ = ("_doc", "_server_type", "_is_writable", "_is_readable", "_awaitable") + + def __init__(self, doc: _DocumentType, awaitable: bool = False) -> None: + self._server_type = _get_server_type(doc) + self._doc: _DocumentType = doc + self._is_writable = self._server_type in ( + SERVER_TYPE.RSPrimary, + SERVER_TYPE.Standalone, + SERVER_TYPE.Mongos, + SERVER_TYPE.LoadBalancer, + ) + + self._is_readable = self.server_type == SERVER_TYPE.RSSecondary or self._is_writable + self._awaitable = awaitable + + @property + def document(self) -> _DocumentType: + """The complete hello command response document. + + .. versionadded:: 3.4 + """ + return copy.copy(self._doc) + + @property + def server_type(self) -> int: + return self._server_type + + @property + def all_hosts(self) -> set[tuple[str, int]]: + """List of hosts, passives, and arbiters known to this server.""" + return set( + map( + common.clean_node, + itertools.chain( + self._doc.get("hosts", []), + self._doc.get("passives", []), + self._doc.get("arbiters", []), + ), + ) + ) + + @property + def tags(self) -> Mapping[str, Any]: + """Replica set member tags or empty dict.""" + return self._doc.get("tags", {}) + + @property + def primary(self) -> Optional[tuple[str, int]]: + """This server's opinion about who the primary is, or None.""" + if self._doc.get("primary"): + return common.partition_node(self._doc["primary"]) + else: + return None + + @property + def replica_set_name(self) -> Optional[str]: + """Replica set name or None.""" + return self._doc.get("setName") + + @property + def max_bson_size(self) -> int: + return self._doc.get("maxBsonObjectSize", common.MAX_BSON_SIZE) + + @property + def max_message_size(self) -> int: + return self._doc.get("maxMessageSizeBytes", 2 * self.max_bson_size) + + @property + def max_write_batch_size(self) -> int: + return self._doc.get("maxWriteBatchSize", common.MAX_WRITE_BATCH_SIZE) + + @property + def min_wire_version(self) -> int: + return self._doc.get("minWireVersion", common.MIN_WIRE_VERSION) + + @property + def max_wire_version(self) -> int: + return self._doc.get("maxWireVersion", common.MAX_WIRE_VERSION) + + @property + def set_version(self) -> Optional[int]: + return self._doc.get("setVersion") + + @property + def election_id(self) -> Optional[ObjectId]: + return self._doc.get("electionId") + + @property + def cluster_time(self) -> Optional[ClusterTime]: + return self._doc.get("$clusterTime") + + @property + def logical_session_timeout_minutes(self) -> Optional[int]: + return self._doc.get("logicalSessionTimeoutMinutes") + + @property + def is_writable(self) -> bool: + return self._is_writable + + @property + def is_readable(self) -> bool: + return self._is_readable + + @property + def me(self) -> Optional[tuple[str, int]]: + me = self._doc.get("me") + if me: + return common.clean_node(me) + return None + + @property + def last_write_date(self) -> Optional[datetime.datetime]: + return self._doc.get("lastWrite", {}).get("lastWriteDate") + + @property + def compressors(self) -> Optional[list[str]]: + return self._doc.get("compression") + + @property + def sasl_supported_mechs(self) -> list[str]: + """Supported authentication mechanisms for the current user. + + For example:: + + >>> hello.sasl_supported_mechs + ["SCRAM-SHA-1", "SCRAM-SHA-256"] + + """ + return self._doc.get("saslSupportedMechs", []) + + @property + def speculative_authenticate(self) -> Optional[Mapping[str, Any]]: + """The speculativeAuthenticate field.""" + return self._doc.get("speculativeAuthenticate") + + @property + def topology_version(self) -> Optional[Mapping[str, Any]]: + return self._doc.get("topologyVersion") + + @property + def awaitable(self) -> bool: + return self._awaitable + + @property + def service_id(self) -> Optional[ObjectId]: + return self._doc.get("serviceId") + + @property + def hello_ok(self) -> bool: + return self._doc.get("helloOk", False) + + @property + def connection_id(self) -> Optional[int]: + return self._doc.get("connectionId") diff --git a/venv/Lib/site-packages/pymongo/helpers.py b/venv/Lib/site-packages/pymongo/helpers.py new file mode 100644 index 00000000..916d78a3 --- /dev/null +++ b/venv/Lib/site-packages/pymongo/helpers.py @@ -0,0 +1,350 @@ +# Copyright 2009-present MongoDB, 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. + +"""Bits and pieces used by the driver that don't really fit elsewhere.""" +from __future__ import annotations + +import sys +import traceback +from collections import abc +from typing import ( + TYPE_CHECKING, + Any, + Callable, + Container, + Iterable, + Mapping, + NoReturn, + Optional, + Sequence, + TypeVar, + Union, + cast, +) + +from pymongo import ASCENDING +from pymongo.errors import ( + CursorNotFound, + DuplicateKeyError, + ExecutionTimeout, + NotPrimaryError, + OperationFailure, + WriteConcernError, + WriteError, + WTimeoutError, + _wtimeout_error, +) +from pymongo.hello import HelloCompat + +if TYPE_CHECKING: + from pymongo.cursor import _Hint + from pymongo.operations import _IndexList + from pymongo.typings import _DocumentOut + +# From the SDAM spec, the "node is shutting down" codes. +_SHUTDOWN_CODES: frozenset = frozenset( + [ + 11600, # InterruptedAtShutdown + 91, # ShutdownInProgress + ] +) +# From the SDAM spec, the "not primary" error codes are combined with the +# "node is recovering" error codes (of which the "node is shutting down" +# errors are a subset). +_NOT_PRIMARY_CODES: frozenset = ( + frozenset( + [ + 10058, # LegacyNotPrimary <=3.2 "not primary" error code + 10107, # NotWritablePrimary + 13435, # NotPrimaryNoSecondaryOk + 11602, # InterruptedDueToReplStateChange + 13436, # NotPrimaryOrSecondary + 189, # PrimarySteppedDown + ] + ) + | _SHUTDOWN_CODES +) +# From the retryable writes spec. +_RETRYABLE_ERROR_CODES: frozenset = _NOT_PRIMARY_CODES | frozenset( + [ + 7, # HostNotFound + 6, # HostUnreachable + 89, # NetworkTimeout + 9001, # SocketException + 262, # ExceededTimeLimit + 134, # ReadConcernMajorityNotAvailableYet + ] +) + +# Server code raised when re-authentication is required +_REAUTHENTICATION_REQUIRED_CODE: int = 391 + +# Server code raised when authentication fails. +_AUTHENTICATION_FAILURE_CODE: int = 18 + + +def _gen_index_name(keys: _IndexList) -> str: + """Generate an index name from the set of fields it is over.""" + return "_".join(["{}_{}".format(*item) for item in keys]) + + +def _index_list( + key_or_list: _Hint, direction: Optional[Union[int, str]] = None +) -> Sequence[tuple[str, Union[int, str, Mapping[str, Any]]]]: + """Helper to generate a list of (key, direction) pairs. + + Takes such a list, or a single key, or a single key and direction. + """ + if direction is not None: + if not isinstance(key_or_list, str): + raise TypeError("Expected a string and a direction") + return [(key_or_list, direction)] + else: + if isinstance(key_or_list, str): + return [(key_or_list, ASCENDING)] + elif isinstance(key_or_list, abc.ItemsView): + return list(key_or_list) # type: ignore[arg-type] + elif isinstance(key_or_list, abc.Mapping): + return list(key_or_list.items()) + elif not isinstance(key_or_list, (list, tuple)): + raise TypeError("if no direction is specified, key_or_list must be an instance of list") + values: list[tuple[str, int]] = [] + for item in key_or_list: + if isinstance(item, str): + item = (item, ASCENDING) # noqa: PLW2901 + values.append(item) + return values + + +def _index_document(index_list: _IndexList) -> dict[str, Any]: + """Helper to generate an index specifying document. + + Takes a list of (key, direction) pairs. + """ + if not isinstance(index_list, (list, tuple, abc.Mapping)): + raise TypeError( + "must use a dictionary or a list of (key, direction) pairs, not: " + repr(index_list) + ) + if not len(index_list): + raise ValueError("key_or_list must not be empty") + + index: dict[str, Any] = {} + + if isinstance(index_list, abc.Mapping): + for key in index_list: + value = index_list[key] + _validate_index_key_pair(key, value) + index[key] = value + else: + for item in index_list: + if isinstance(item, str): + item = (item, ASCENDING) # noqa: PLW2901 + key, value = item + _validate_index_key_pair(key, value) + index[key] = value + return index + + +def _validate_index_key_pair(key: Any, value: Any) -> None: + if not isinstance(key, str): + raise TypeError("first item in each key pair must be an instance of str") + if not isinstance(value, (str, int, abc.Mapping)): + raise TypeError( + "second item in each key pair must be 1, -1, " + "'2d', or another valid MongoDB index specifier." + ) + + +def _check_command_response( + response: _DocumentOut, + max_wire_version: Optional[int], + allowable_errors: Optional[Container[Union[int, str]]] = None, + parse_write_concern_error: bool = False, +) -> None: + """Check the response to a command for errors.""" + if "ok" not in response: + # Server didn't recognize our message as a command. + raise OperationFailure( + response.get("$err"), # type: ignore[arg-type] + response.get("code"), + response, + max_wire_version, + ) + + if parse_write_concern_error and "writeConcernError" in response: + _error = response["writeConcernError"] + _labels = response.get("errorLabels") + if _labels: + _error.update({"errorLabels": _labels}) + _raise_write_concern_error(_error) + + if response["ok"]: + return + + details = response + # Mongos returns the error details in a 'raw' object + # for some errors. + if "raw" in response: + for shard in response["raw"].values(): + # Grab the first non-empty raw error from a shard. + if shard.get("errmsg") and not shard.get("ok"): + details = shard + break + + errmsg = details["errmsg"] + code = details.get("code") + + # For allowable errors, only check for error messages when the code is not + # included. + if allowable_errors: + if code is not None: + if code in allowable_errors: + return + elif errmsg in allowable_errors: + return + + # Server is "not primary" or "recovering" + if code is not None: + if code in _NOT_PRIMARY_CODES: + raise NotPrimaryError(errmsg, response) + elif HelloCompat.LEGACY_ERROR in errmsg or "node is recovering" in errmsg: + raise NotPrimaryError(errmsg, response) + + # Other errors + # findAndModify with upsert can raise duplicate key error + if code in (11000, 11001, 12582): + raise DuplicateKeyError(errmsg, code, response, max_wire_version) + elif code == 50: + raise ExecutionTimeout(errmsg, code, response, max_wire_version) + elif code == 43: + raise CursorNotFound(errmsg, code, response, max_wire_version) + + raise OperationFailure(errmsg, code, response, max_wire_version) + + +def _raise_last_write_error(write_errors: list[Any]) -> NoReturn: + # If the last batch had multiple errors only report + # the last error to emulate continue_on_error. + error = write_errors[-1] + if error.get("code") == 11000: + raise DuplicateKeyError(error.get("errmsg"), 11000, error) + raise WriteError(error.get("errmsg"), error.get("code"), error) + + +def _raise_write_concern_error(error: Any) -> NoReturn: + if _wtimeout_error(error): + # Make sure we raise WTimeoutError + raise WTimeoutError(error.get("errmsg"), error.get("code"), error) + raise WriteConcernError(error.get("errmsg"), error.get("code"), error) + + +def _get_wce_doc(result: Mapping[str, Any]) -> Optional[Mapping[str, Any]]: + """Return the writeConcernError or None.""" + wce = result.get("writeConcernError") + if wce: + # The server reports errorLabels at the top level but it's more + # convenient to attach it to the writeConcernError doc itself. + error_labels = result.get("errorLabels") + if error_labels: + # Copy to avoid changing the original document. + wce = wce.copy() + wce["errorLabels"] = error_labels + return wce + + +def _check_write_command_response(result: Mapping[str, Any]) -> None: + """Backward compatibility helper for write command error handling.""" + # Prefer write errors over write concern errors + write_errors = result.get("writeErrors") + if write_errors: + _raise_last_write_error(write_errors) + + wce = _get_wce_doc(result) + if wce: + _raise_write_concern_error(wce) + + +def _fields_list_to_dict( + fields: Union[Mapping[str, Any], Iterable[str]], option_name: str +) -> Mapping[str, Any]: + """Takes a sequence of field names and returns a matching dictionary. + + ["a", "b"] becomes {"a": 1, "b": 1} + + and + + ["a.b.c", "d", "a.c"] becomes {"a.b.c": 1, "d": 1, "a.c": 1} + """ + if isinstance(fields, abc.Mapping): + return fields + + if isinstance(fields, (abc.Sequence, abc.Set)): + if not all(isinstance(field, str) for field in fields): + raise TypeError(f"{option_name} must be a list of key names, each an instance of str") + return dict.fromkeys(fields, 1) + + raise TypeError(f"{option_name} must be a mapping or list of key names") + + +def _handle_exception() -> None: + """Print exceptions raised by subscribers to stderr.""" + # Heavily influenced by logging.Handler.handleError. + + # See note here: + # https://docs.python.org/3.4/library/sys.html#sys.__stderr__ + if sys.stderr: + einfo = sys.exc_info() + try: + traceback.print_exception(einfo[0], einfo[1], einfo[2], None, sys.stderr) + except OSError: + pass + finally: + del einfo + + +# See https://mypy.readthedocs.io/en/stable/generics.html?#decorator-factories +F = TypeVar("F", bound=Callable[..., Any]) + + +def _handle_reauth(func: F) -> F: + def inner(*args: Any, **kwargs: Any) -> Any: + no_reauth = kwargs.pop("no_reauth", False) + from pymongo.message import _BulkWriteContext + from pymongo.pool import Connection + + try: + return func(*args, **kwargs) + except OperationFailure as exc: + if no_reauth: + raise + if exc.code == _REAUTHENTICATION_REQUIRED_CODE: + # Look for an argument that either is a Connection + # or has a connection attribute, so we can trigger + # a reauth. + conn = None + for arg in args: + if isinstance(arg, Connection): + conn = arg + break + if isinstance(arg, _BulkWriteContext): + conn = arg.conn + break + if conn: + conn.authenticate(reauthenticate=True) + else: + raise + return func(*args, **kwargs) + raise + + return cast(F, inner) diff --git a/venv/Lib/site-packages/pymongo/lock.py b/venv/Lib/site-packages/pymongo/lock.py new file mode 100644 index 00000000..e3747850 --- /dev/null +++ b/venv/Lib/site-packages/pymongo/lock.py @@ -0,0 +1,40 @@ +# Copyright 2022-present MongoDB, 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. +from __future__ import annotations + +import os +import threading +import weakref + +_HAS_REGISTER_AT_FORK = hasattr(os, "register_at_fork") + +# References to instances of _create_lock +_forkable_locks: weakref.WeakSet[threading.Lock] = weakref.WeakSet() + + +def _create_lock() -> threading.Lock: + """Represents a lock that is tracked upon instantiation using a WeakSet and + reset by pymongo upon forking. + """ + lock = threading.Lock() + if _HAS_REGISTER_AT_FORK: + _forkable_locks.add(lock) + return lock + + +def _release_locks() -> None: + # Completed the fork, reset all the locks in the child. + for lock in _forkable_locks: + if lock.locked(): + lock.release() diff --git a/venv/Lib/site-packages/pymongo/logger.py b/venv/Lib/site-packages/pymongo/logger.py new file mode 100644 index 00000000..2caafa77 --- /dev/null +++ b/venv/Lib/site-packages/pymongo/logger.py @@ -0,0 +1,169 @@ +# Copyright 2023-present MongoDB, 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. +from __future__ import annotations + +import enum +import logging +import os +import warnings +from typing import Any + +from bson import UuidRepresentation, json_util +from bson.json_util import JSONOptions, _truncate_documents +from pymongo.monitoring import ConnectionCheckOutFailedReason, ConnectionClosedReason + + +class _CommandStatusMessage(str, enum.Enum): + STARTED = "Command started" + SUCCEEDED = "Command succeeded" + FAILED = "Command failed" + + +class _ServerSelectionStatusMessage(str, enum.Enum): + STARTED = "Server selection started" + SUCCEEDED = "Server selection succeeded" + FAILED = "Server selection failed" + WAITING = "Waiting for suitable server to become available" + + +class _ConnectionStatusMessage(str, enum.Enum): + POOL_CREATED = "Connection pool created" + POOL_READY = "Connection pool ready" + POOL_CLOSED = "Connection pool closed" + POOL_CLEARED = "Connection pool cleared" + + CONN_CREATED = "Connection created" + CONN_READY = "Connection ready" + CONN_CLOSED = "Connection closed" + + CHECKOUT_STARTED = "Connection checkout started" + CHECKOUT_SUCCEEDED = "Connection checked out" + CHECKOUT_FAILED = "Connection checkout failed" + CHECKEDIN = "Connection checked in" + + +_DEFAULT_DOCUMENT_LENGTH = 1000 +_SENSITIVE_COMMANDS = [ + "authenticate", + "saslStart", + "saslContinue", + "getnonce", + "createUser", + "updateUser", + "copydbgetnonce", + "copydbsaslstart", + "copydb", +] +_HELLO_COMMANDS = ["hello", "ismaster", "isMaster"] +_REDACTED_FAILURE_FIELDS = ["code", "codeName", "errorLabels"] +_DOCUMENT_NAMES = ["command", "reply", "failure"] +_JSON_OPTIONS = JSONOptions(uuid_representation=UuidRepresentation.STANDARD) +_COMMAND_LOGGER = logging.getLogger("pymongo.command") +_CONNECTION_LOGGER = logging.getLogger("pymongo.connection") +_SERVER_SELECTION_LOGGER = logging.getLogger("pymongo.serverSelection") +_CLIENT_LOGGER = logging.getLogger("pymongo.client") +_VERBOSE_CONNECTION_ERROR_REASONS = { + ConnectionClosedReason.POOL_CLOSED: "Connection pool was closed", + ConnectionCheckOutFailedReason.POOL_CLOSED: "Connection pool was closed", + ConnectionClosedReason.STALE: "Connection pool was stale", + ConnectionClosedReason.ERROR: "An error occurred while using the connection", + ConnectionCheckOutFailedReason.CONN_ERROR: "An error occurred while trying to establish a new connection", + ConnectionClosedReason.IDLE: "Connection was idle too long", + ConnectionCheckOutFailedReason.TIMEOUT: "Connection exceeded the specified timeout", +} + + +def _debug_log(logger: logging.Logger, **fields: Any) -> None: + logger.debug(LogMessage(**fields)) + + +def _verbose_connection_error_reason(reason: str) -> str: + return _VERBOSE_CONNECTION_ERROR_REASONS.get(reason, reason) + + +def _info_log(logger: logging.Logger, **fields: Any) -> None: + logger.info(LogMessage(**fields)) + + +def _log_or_warn(logger: logging.Logger, message: str) -> None: + if logger.isEnabledFor(logging.INFO): + logger.info(message) + else: + # stacklevel=4 ensures that the warning is for the user's code. + warnings.warn(message, UserWarning, stacklevel=4) + + +class LogMessage: + __slots__ = ("_kwargs", "_redacted") + + def __init__(self, **kwargs: Any): + self._kwargs = kwargs + self._redacted = False + + def __str__(self) -> str: + self._redact() + return "%s" % ( + json_util.dumps( + self._kwargs, json_options=_JSON_OPTIONS, default=lambda o: o.__repr__() + ) + ) + + def _is_sensitive(self, doc_name: str) -> bool: + is_speculative_authenticate = ( + self._kwargs.pop("speculative_authenticate", False) + or "speculativeAuthenticate" in self._kwargs[doc_name] + ) + is_sensitive_command = ( + "commandName" in self._kwargs and self._kwargs["commandName"] in _SENSITIVE_COMMANDS + ) + + is_sensitive_hello = ( + self._kwargs["commandName"] in _HELLO_COMMANDS and is_speculative_authenticate + ) + + return is_sensitive_command or is_sensitive_hello + + def _redact(self) -> None: + if self._redacted: + return + self._kwargs = {k: v for k, v in self._kwargs.items() if v is not None} + if "durationMS" in self._kwargs and hasattr(self._kwargs["durationMS"], "total_seconds"): + self._kwargs["durationMS"] = self._kwargs["durationMS"].total_seconds() * 1000 + if "serviceId" in self._kwargs: + self._kwargs["serviceId"] = str(self._kwargs["serviceId"]) + document_length = int(os.getenv("MONGOB_LOG_MAX_DOCUMENT_LENGTH", _DEFAULT_DOCUMENT_LENGTH)) + if document_length < 0: + document_length = _DEFAULT_DOCUMENT_LENGTH + is_server_side_error = self._kwargs.pop("isServerSideError", False) + + for doc_name in _DOCUMENT_NAMES: + doc = self._kwargs.get(doc_name) + if doc: + if doc_name == "failure" and is_server_side_error: + doc = {k: v for k, v in doc.items() if k in _REDACTED_FAILURE_FIELDS} + if doc_name != "failure" and self._is_sensitive(doc_name): + doc = json_util.dumps({}) + else: + truncated_doc = _truncate_documents(doc, document_length)[0] + doc = json_util.dumps( + truncated_doc, + json_options=_JSON_OPTIONS, + default=lambda o: o.__repr__(), + ) + if len(doc) > document_length: + doc = ( + doc.encode()[:document_length].decode("unicode-escape", "ignore") + ) + "..." + self._kwargs[doc_name] = doc + self._redacted = True diff --git a/venv/Lib/site-packages/pymongo/max_staleness_selectors.py b/venv/Lib/site-packages/pymongo/max_staleness_selectors.py new file mode 100644 index 00000000..72edf555 --- /dev/null +++ b/venv/Lib/site-packages/pymongo/max_staleness_selectors.py @@ -0,0 +1,122 @@ +# Copyright 2016 MongoDB, 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. + +"""Criteria to select ServerDescriptions based on maxStalenessSeconds. + +The Max Staleness Spec says: When there is a known primary P, +a secondary S's staleness is estimated with this formula: + + (S.lastUpdateTime - S.lastWriteDate) - (P.lastUpdateTime - P.lastWriteDate) + + heartbeatFrequencyMS + +When there is no known primary, a secondary S's staleness is estimated with: + + SMax.lastWriteDate - S.lastWriteDate + heartbeatFrequencyMS + +where "SMax" is the secondary with the greatest lastWriteDate. +""" +from __future__ import annotations + +from typing import TYPE_CHECKING + +from pymongo.errors import ConfigurationError +from pymongo.server_type import SERVER_TYPE + +if TYPE_CHECKING: + from pymongo.server_selectors import Selection +# Constant defined in Max Staleness Spec: An idle primary writes a no-op every +# 10 seconds to refresh secondaries' lastWriteDate values. +IDLE_WRITE_PERIOD = 10 +SMALLEST_MAX_STALENESS = 90 + + +def _validate_max_staleness(max_staleness: int, heartbeat_frequency: int) -> None: + # We checked for max staleness -1 before this, it must be positive here. + if max_staleness < heartbeat_frequency + IDLE_WRITE_PERIOD: + raise ConfigurationError( + "maxStalenessSeconds must be at least heartbeatFrequencyMS +" + " %d seconds. maxStalenessSeconds is set to %d," + " heartbeatFrequencyMS is set to %d." + % (IDLE_WRITE_PERIOD, max_staleness, heartbeat_frequency * 1000) + ) + + if max_staleness < SMALLEST_MAX_STALENESS: + raise ConfigurationError( + "maxStalenessSeconds must be at least %d. " + "maxStalenessSeconds is set to %d." % (SMALLEST_MAX_STALENESS, max_staleness) + ) + + +def _with_primary(max_staleness: int, selection: Selection) -> Selection: + """Apply max_staleness, in seconds, to a Selection with a known primary.""" + primary = selection.primary + assert primary + sds = [] + + for s in selection.server_descriptions: + if s.server_type == SERVER_TYPE.RSSecondary: + # See max-staleness.rst for explanation of this formula. + assert s.last_write_date and primary.last_write_date # noqa: PT018 + staleness = ( + (s.last_update_time - s.last_write_date) + - (primary.last_update_time - primary.last_write_date) + + selection.heartbeat_frequency + ) + + if staleness <= max_staleness: + sds.append(s) + else: + sds.append(s) + + return selection.with_server_descriptions(sds) + + +def _no_primary(max_staleness: int, selection: Selection) -> Selection: + """Apply max_staleness, in seconds, to a Selection with no known primary.""" + # Secondary that's replicated the most recent writes. + smax = selection.secondary_with_max_last_write_date() + if not smax: + # No secondaries and no primary, short-circuit out of here. + return selection.with_server_descriptions([]) + + sds = [] + + for s in selection.server_descriptions: + if s.server_type == SERVER_TYPE.RSSecondary: + # See max-staleness.rst for explanation of this formula. + assert smax.last_write_date and s.last_write_date # noqa: PT018 + staleness = smax.last_write_date - s.last_write_date + selection.heartbeat_frequency + + if staleness <= max_staleness: + sds.append(s) + else: + sds.append(s) + + return selection.with_server_descriptions(sds) + + +def select(max_staleness: int, selection: Selection) -> Selection: + """Apply max_staleness, in seconds, to a Selection.""" + if max_staleness == -1: + return selection + + # Server Selection Spec: If the TopologyType is ReplicaSetWithPrimary or + # ReplicaSetNoPrimary, a client MUST raise an error if maxStaleness < + # heartbeatFrequency + IDLE_WRITE_PERIOD, or if maxStaleness < 90. + _validate_max_staleness(max_staleness, selection.heartbeat_frequency) + + if selection.primary: + return _with_primary(max_staleness, selection) + else: + return _no_primary(max_staleness, selection) diff --git a/venv/Lib/site-packages/pymongo/message.py b/venv/Lib/site-packages/pymongo/message.py new file mode 100644 index 00000000..9412dc91 --- /dev/null +++ b/venv/Lib/site-packages/pymongo/message.py @@ -0,0 +1,1753 @@ +# Copyright 2009-present MongoDB, 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. + +"""Tools for creating `messages +`_ to be sent to +MongoDB. + +.. note:: This module is for internal use and is generally not needed by + application developers. +""" +from __future__ import annotations + +import datetime +import logging +import random +import struct +from io import BytesIO as _BytesIO +from typing import ( + TYPE_CHECKING, + Any, + Callable, + Iterable, + Mapping, + MutableMapping, + NoReturn, + Optional, + Union, +) + +import bson +from bson import CodecOptions, _decode_selective, _dict_to_bson, _make_c_string, encode +from bson.int64 import Int64 +from bson.raw_bson import ( + _RAW_ARRAY_BSON_OPTIONS, + DEFAULT_RAW_BSON_OPTIONS, + RawBSONDocument, + _inflate_bson, +) + +try: + from pymongo import _cmessage # type: ignore[attr-defined] + + _use_c = True +except ImportError: + _use_c = False +from pymongo.errors import ( + ConfigurationError, + CursorNotFound, + DocumentTooLarge, + ExecutionTimeout, + InvalidOperation, + NotPrimaryError, + OperationFailure, + ProtocolError, +) +from pymongo.hello import HelloCompat +from pymongo.helpers import _handle_reauth +from pymongo.logger import _COMMAND_LOGGER, _CommandStatusMessage, _debug_log +from pymongo.read_preferences import ReadPreference +from pymongo.write_concern import WriteConcern + +if TYPE_CHECKING: + from datetime import timedelta + + from pymongo.client_session import ClientSession + from pymongo.compression_support import SnappyContext, ZlibContext, ZstdContext + from pymongo.mongo_client import MongoClient + from pymongo.monitoring import _EventListeners + from pymongo.pool import Connection + from pymongo.read_concern import ReadConcern + from pymongo.read_preferences import _ServerMode + from pymongo.typings import _Address, _DocumentOut + +MAX_INT32 = 2147483647 +MIN_INT32 = -2147483648 + +# Overhead allowed for encoded command documents. +_COMMAND_OVERHEAD = 16382 + +_INSERT = 0 +_UPDATE = 1 +_DELETE = 2 + +_EMPTY = b"" +_BSONOBJ = b"\x03" +_ZERO_8 = b"\x00" +_ZERO_16 = b"\x00\x00" +_ZERO_32 = b"\x00\x00\x00\x00" +_ZERO_64 = b"\x00\x00\x00\x00\x00\x00\x00\x00" +_SKIPLIM = b"\x00\x00\x00\x00\xff\xff\xff\xff" +_OP_MAP = { + _INSERT: b"\x04documents\x00\x00\x00\x00\x00", + _UPDATE: b"\x04updates\x00\x00\x00\x00\x00", + _DELETE: b"\x04deletes\x00\x00\x00\x00\x00", +} +_FIELD_MAP = {"insert": "documents", "update": "updates", "delete": "deletes"} + +_UNICODE_REPLACE_CODEC_OPTIONS: CodecOptions[Mapping[str, Any]] = CodecOptions( + unicode_decode_error_handler="replace" +) + + +def _randint() -> int: + """Generate a pseudo random 32 bit integer.""" + return random.randint(MIN_INT32, MAX_INT32) # noqa: S311 + + +def _maybe_add_read_preference( + spec: MutableMapping[str, Any], read_preference: _ServerMode +) -> MutableMapping[str, Any]: + """Add $readPreference to spec when appropriate.""" + mode = read_preference.mode + document = read_preference.document + # Only add $readPreference if it's something other than primary to avoid + # problems with mongos versions that don't support read preferences. Also, + # for maximum backwards compatibility, don't add $readPreference for + # secondaryPreferred unless tags or maxStalenessSeconds are in use (setting + # the secondaryOkay bit has the same effect). + if mode and (mode != ReadPreference.SECONDARY_PREFERRED.mode or len(document) > 1): + if "$query" not in spec: + spec = {"$query": spec} + spec["$readPreference"] = document + return spec + + +def _convert_exception(exception: Exception) -> dict[str, Any]: + """Convert an Exception into a failure document for publishing.""" + return {"errmsg": str(exception), "errtype": exception.__class__.__name__} + + +def _convert_write_result( + operation: str, command: Mapping[str, Any], result: Mapping[str, Any] +) -> dict[str, Any]: + """Convert a legacy write result to write command format.""" + # Based on _merge_legacy from bulk.py + affected = result.get("n", 0) + res = {"ok": 1, "n": affected} + errmsg = result.get("errmsg", result.get("err", "")) + if errmsg: + # The write was successful on at least the primary so don't return. + if result.get("wtimeout"): + res["writeConcernError"] = {"errmsg": errmsg, "code": 64, "errInfo": {"wtimeout": True}} + else: + # The write failed. + error = {"index": 0, "code": result.get("code", 8), "errmsg": errmsg} + if "errInfo" in result: + error["errInfo"] = result["errInfo"] + res["writeErrors"] = [error] + return res + if operation == "insert": + # GLE result for insert is always 0 in most MongoDB versions. + res["n"] = len(command["documents"]) + elif operation == "update": + if "upserted" in result: + res["upserted"] = [{"index": 0, "_id": result["upserted"]}] + # Versions of MongoDB before 2.6 don't return the _id for an + # upsert if _id is not an ObjectId. + elif result.get("updatedExisting") is False and affected == 1: + # If _id is in both the update document *and* the query spec + # the update document _id takes precedence. + update = command["updates"][0] + _id = update["u"].get("_id", update["q"].get("_id")) + res["upserted"] = [{"index": 0, "_id": _id}] + return res + + +_OPTIONS = { + "tailable": 2, + "oplogReplay": 8, + "noCursorTimeout": 16, + "awaitData": 32, + "allowPartialResults": 128, +} + + +_MODIFIERS = { + "$query": "filter", + "$orderby": "sort", + "$hint": "hint", + "$comment": "comment", + "$maxScan": "maxScan", + "$maxTimeMS": "maxTimeMS", + "$max": "max", + "$min": "min", + "$returnKey": "returnKey", + "$showRecordId": "showRecordId", + "$showDiskLoc": "showRecordId", # <= MongoDb 3.0 + "$snapshot": "snapshot", +} + + +def _gen_find_command( + coll: str, + spec: Mapping[str, Any], + projection: Optional[Union[Mapping[str, Any], Iterable[str]]], + skip: int, + limit: int, + batch_size: Optional[int], + options: Optional[int], + read_concern: ReadConcern, + collation: Optional[Mapping[str, Any]] = None, + session: Optional[ClientSession] = None, + allow_disk_use: Optional[bool] = None, +) -> dict[str, Any]: + """Generate a find command document.""" + cmd: dict[str, Any] = {"find": coll} + if "$query" in spec: + cmd.update( + [ + (_MODIFIERS[key], val) if key in _MODIFIERS else (key, val) + for key, val in spec.items() + ] + ) + if "$explain" in cmd: + cmd.pop("$explain") + if "$readPreference" in cmd: + cmd.pop("$readPreference") + else: + cmd["filter"] = spec + + if projection: + cmd["projection"] = projection + if skip: + cmd["skip"] = skip + if limit: + cmd["limit"] = abs(limit) + if limit < 0: + cmd["singleBatch"] = True + if batch_size: + cmd["batchSize"] = batch_size + if read_concern.level and not (session and session.in_transaction): + cmd["readConcern"] = read_concern.document + if collation: + cmd["collation"] = collation + if allow_disk_use is not None: + cmd["allowDiskUse"] = allow_disk_use + if options: + cmd.update([(opt, True) for opt, val in _OPTIONS.items() if options & val]) + + return cmd + + +def _gen_get_more_command( + cursor_id: Optional[int], + coll: str, + batch_size: Optional[int], + max_await_time_ms: Optional[int], + comment: Optional[Any], + conn: Connection, +) -> dict[str, Any]: + """Generate a getMore command document.""" + cmd: dict[str, Any] = {"getMore": cursor_id, "collection": coll} + if batch_size: + cmd["batchSize"] = batch_size + if max_await_time_ms is not None: + cmd["maxTimeMS"] = max_await_time_ms + if comment is not None and conn.max_wire_version >= 9: + cmd["comment"] = comment + return cmd + + +class _Query: + """A query operation.""" + + __slots__ = ( + "flags", + "db", + "coll", + "ntoskip", + "spec", + "fields", + "codec_options", + "read_preference", + "limit", + "batch_size", + "name", + "read_concern", + "collation", + "session", + "client", + "allow_disk_use", + "_as_command", + "exhaust", + ) + + # For compatibility with the _GetMore class. + conn_mgr = None + cursor_id = None + + def __init__( + self, + flags: int, + db: str, + coll: str, + ntoskip: int, + spec: Mapping[str, Any], + fields: Optional[Mapping[str, Any]], + codec_options: CodecOptions, + read_preference: _ServerMode, + limit: int, + batch_size: int, + read_concern: ReadConcern, + collation: Optional[Mapping[str, Any]], + session: Optional[ClientSession], + client: MongoClient, + allow_disk_use: Optional[bool], + exhaust: bool, + ): + self.flags = flags + self.db = db + self.coll = coll + self.ntoskip = ntoskip + self.spec = spec + self.fields = fields + self.codec_options = codec_options + self.read_preference = read_preference + self.read_concern = read_concern + self.limit = limit + self.batch_size = batch_size + self.collation = collation + self.session = session + self.client = client + self.allow_disk_use = allow_disk_use + self.name = "find" + self._as_command: Optional[tuple[dict[str, Any], str]] = None + self.exhaust = exhaust + + def reset(self) -> None: + self._as_command = None + + def namespace(self) -> str: + return f"{self.db}.{self.coll}" + + def use_command(self, conn: Connection) -> bool: + use_find_cmd = False + if not self.exhaust: + use_find_cmd = True + elif conn.max_wire_version >= 8: + # OP_MSG supports exhaust on MongoDB 4.2+ + use_find_cmd = True + elif not self.read_concern.ok_for_legacy: + raise ConfigurationError( + "read concern level of %s is not valid " + "with a max wire version of %d." % (self.read_concern.level, conn.max_wire_version) + ) + + conn.validate_session(self.client, self.session) + return use_find_cmd + + def as_command( + self, conn: Connection, apply_timeout: bool = False + ) -> tuple[dict[str, Any], str]: + """Return a find command document for this query.""" + # We use the command twice: on the wire and for command monitoring. + # Generate it once, for speed and to avoid repeating side-effects. + if self._as_command is not None: + return self._as_command + + explain = "$explain" in self.spec + cmd: dict[str, Any] = _gen_find_command( + self.coll, + self.spec, + self.fields, + self.ntoskip, + self.limit, + self.batch_size, + self.flags, + self.read_concern, + self.collation, + self.session, + self.allow_disk_use, + ) + if explain: + self.name = "explain" + cmd = {"explain": cmd} + session = self.session + conn.add_server_api(cmd) + if session: + session._apply_to(cmd, False, self.read_preference, conn) + # Explain does not support readConcern. + if not explain and not session.in_transaction: + session._update_read_concern(cmd, conn) + conn.send_cluster_time(cmd, session, self.client) + # Support auto encryption + client = self.client + if client._encrypter and not client._encrypter._bypass_auto_encryption: + cmd = client._encrypter.encrypt(self.db, cmd, self.codec_options) + # Support CSOT + if apply_timeout: + conn.apply_timeout(client, cmd) + self._as_command = cmd, self.db + return self._as_command + + def get_message( + self, read_preference: _ServerMode, conn: Connection, use_cmd: bool = False + ) -> tuple[int, bytes, int]: + """Get a query message, possibly setting the secondaryOk bit.""" + # Use the read_preference decided by _socket_from_server. + self.read_preference = read_preference + if read_preference.mode: + # Set the secondaryOk bit. + flags = self.flags | 4 + else: + flags = self.flags + + ns = self.namespace() + spec = self.spec + + if use_cmd: + spec = self.as_command(conn, apply_timeout=True)[0] + request_id, msg, size, _ = _op_msg( + 0, + spec, + self.db, + read_preference, + self.codec_options, + ctx=conn.compression_context, + ) + return request_id, msg, size + + # OP_QUERY treats ntoreturn of -1 and 1 the same, return + # one document and close the cursor. We have to use 2 for + # batch size if 1 is specified. + ntoreturn = self.batch_size == 1 and 2 or self.batch_size + if self.limit: + if ntoreturn: + ntoreturn = min(self.limit, ntoreturn) + else: + ntoreturn = self.limit + + if conn.is_mongos: + assert isinstance(spec, MutableMapping) + spec = _maybe_add_read_preference(spec, read_preference) + + return _query( + flags, + ns, + self.ntoskip, + ntoreturn, + spec, + None if use_cmd else self.fields, + self.codec_options, + ctx=conn.compression_context, + ) + + +class _GetMore: + """A getmore operation.""" + + __slots__ = ( + "db", + "coll", + "ntoreturn", + "cursor_id", + "max_await_time_ms", + "codec_options", + "read_preference", + "session", + "client", + "conn_mgr", + "_as_command", + "exhaust", + "comment", + ) + + name = "getMore" + + def __init__( + self, + db: str, + coll: str, + ntoreturn: int, + cursor_id: int, + codec_options: CodecOptions, + read_preference: _ServerMode, + session: Optional[ClientSession], + client: MongoClient, + max_await_time_ms: Optional[int], + conn_mgr: Any, + exhaust: bool, + comment: Any, + ): + self.db = db + self.coll = coll + self.ntoreturn = ntoreturn + self.cursor_id = cursor_id + self.codec_options = codec_options + self.read_preference = read_preference + self.session = session + self.client = client + self.max_await_time_ms = max_await_time_ms + self.conn_mgr = conn_mgr + self._as_command: Optional[tuple[dict[str, Any], str]] = None + self.exhaust = exhaust + self.comment = comment + + def reset(self) -> None: + self._as_command = None + + def namespace(self) -> str: + return f"{self.db}.{self.coll}" + + def use_command(self, conn: Connection) -> bool: + use_cmd = False + if not self.exhaust: + use_cmd = True + elif conn.max_wire_version >= 8: + # OP_MSG supports exhaust on MongoDB 4.2+ + use_cmd = True + + conn.validate_session(self.client, self.session) + return use_cmd + + def as_command( + self, conn: Connection, apply_timeout: bool = False + ) -> tuple[dict[str, Any], str]: + """Return a getMore command document for this query.""" + # See _Query.as_command for an explanation of this caching. + if self._as_command is not None: + return self._as_command + + cmd: dict[str, Any] = _gen_get_more_command( + self.cursor_id, + self.coll, + self.ntoreturn, + self.max_await_time_ms, + self.comment, + conn, + ) + if self.session: + self.session._apply_to(cmd, False, self.read_preference, conn) + conn.add_server_api(cmd) + conn.send_cluster_time(cmd, self.session, self.client) + # Support auto encryption + client = self.client + if client._encrypter and not client._encrypter._bypass_auto_encryption: + cmd = client._encrypter.encrypt(self.db, cmd, self.codec_options) + # Support CSOT + if apply_timeout: + conn.apply_timeout(client, cmd=None) + self._as_command = cmd, self.db + return self._as_command + + def get_message( + self, dummy0: Any, conn: Connection, use_cmd: bool = False + ) -> Union[tuple[int, bytes, int], tuple[int, bytes]]: + """Get a getmore message.""" + ns = self.namespace() + ctx = conn.compression_context + + if use_cmd: + spec = self.as_command(conn, apply_timeout=True)[0] + if self.conn_mgr and self.exhaust: + flags = _OpMsg.EXHAUST_ALLOWED + else: + flags = 0 + request_id, msg, size, _ = _op_msg( + flags, spec, self.db, None, self.codec_options, ctx=conn.compression_context + ) + return request_id, msg, size + + return _get_more(ns, self.ntoreturn, self.cursor_id, ctx) + + +class _RawBatchQuery(_Query): + def use_command(self, conn: Connection) -> bool: + # Compatibility checks. + super().use_command(conn) + if conn.max_wire_version >= 8: + # MongoDB 4.2+ supports exhaust over OP_MSG + return True + elif not self.exhaust: + return True + return False + + +class _RawBatchGetMore(_GetMore): + def use_command(self, conn: Connection) -> bool: + # Compatibility checks. + super().use_command(conn) + if conn.max_wire_version >= 8: + # MongoDB 4.2+ supports exhaust over OP_MSG + return True + elif not self.exhaust: + return True + return False + + +class _CursorAddress(tuple): + """The server address (host, port) of a cursor, with namespace property.""" + + __namespace: Any + + def __new__(cls, address: _Address, namespace: str) -> _CursorAddress: + self = tuple.__new__(cls, address) + self.__namespace = namespace + return self + + @property + def namespace(self) -> str: + """The namespace this cursor.""" + return self.__namespace + + def __hash__(self) -> int: + # Two _CursorAddress instances with different namespaces + # must not hash the same. + return ((*self, self.__namespace)).__hash__() + + def __eq__(self, other: object) -> bool: + if isinstance(other, _CursorAddress): + return tuple(self) == tuple(other) and self.namespace == other.namespace + return NotImplemented + + def __ne__(self, other: object) -> bool: + return not self == other + + +_pack_compression_header = struct.Struct(" tuple[int, bytes]: + """Takes message data, compresses it, and adds an OP_COMPRESSED header.""" + compressed = ctx.compress(data) + request_id = _randint() + + header = _pack_compression_header( + _COMPRESSION_HEADER_SIZE + len(compressed), # Total message length + request_id, # Request id + 0, # responseTo + 2012, # operation id + operation, # original operation id + len(data), # uncompressed message length + ctx.compressor_id, + ) # compressor id + return request_id, header + compressed + + +_pack_header = struct.Struct(" tuple[int, bytes]: + """Takes message data and adds a message header based on the operation. + + Returns the resultant message string. + """ + rid = _randint() + message = _pack_header(16 + len(data), rid, 0, operation) + return rid, message + data + + +_pack_int = struct.Struct(" tuple[bytes, int, int]: + """Get a OP_MSG message. + + Note: this method handles multiple documents in a type one payload but + it does not perform batch splitting and the total message size is + only checked *after* generating the entire message. + """ + # Encode the command document in payload 0 without checking keys. + encoded = _dict_to_bson(command, False, opts) + flags_type = _pack_op_msg_flags_type(flags, 0) + total_size = len(encoded) + max_doc_size = 0 + if identifier and docs is not None: + type_one = _pack_byte(1) + cstring = _make_c_string(identifier) + encoded_docs = [_dict_to_bson(doc, False, opts) for doc in docs] + size = len(cstring) + sum(len(doc) for doc in encoded_docs) + 4 + encoded_size = _pack_int(size) + total_size += size + max_doc_size = max(len(doc) for doc in encoded_docs) + data = [flags_type, encoded, type_one, encoded_size, cstring, *encoded_docs] + else: + data = [flags_type, encoded] + return b"".join(data), total_size, max_doc_size + + +def _op_msg_compressed( + flags: int, + command: Mapping[str, Any], + identifier: str, + docs: Optional[list[Mapping[str, Any]]], + opts: CodecOptions, + ctx: Union[SnappyContext, ZlibContext, ZstdContext], +) -> tuple[int, bytes, int, int]: + """Internal OP_MSG message helper.""" + msg, total_size, max_bson_size = _op_msg_no_header(flags, command, identifier, docs, opts) + rid, msg = _compress(2013, msg, ctx) + return rid, msg, total_size, max_bson_size + + +def _op_msg_uncompressed( + flags: int, + command: Mapping[str, Any], + identifier: str, + docs: Optional[list[Mapping[str, Any]]], + opts: CodecOptions, +) -> tuple[int, bytes, int, int]: + """Internal compressed OP_MSG message helper.""" + data, total_size, max_bson_size = _op_msg_no_header(flags, command, identifier, docs, opts) + request_id, op_message = __pack_message(2013, data) + return request_id, op_message, total_size, max_bson_size + + +if _use_c: + _op_msg_uncompressed = _cmessage._op_msg + + +def _op_msg( + flags: int, + command: MutableMapping[str, Any], + dbname: str, + read_preference: Optional[_ServerMode], + opts: CodecOptions, + ctx: Union[SnappyContext, ZlibContext, ZstdContext, None] = None, +) -> tuple[int, bytes, int, int]: + """Get a OP_MSG message.""" + command["$db"] = dbname + # getMore commands do not send $readPreference. + if read_preference is not None and "$readPreference" not in command: + # Only send $readPreference if it's not primary (the default). + if read_preference.mode: + command["$readPreference"] = read_preference.document + name = next(iter(command)) + try: + identifier = _FIELD_MAP[name] + docs = command.pop(identifier) + except KeyError: + identifier = "" + docs = None + try: + if ctx: + return _op_msg_compressed(flags, command, identifier, docs, opts, ctx) + return _op_msg_uncompressed(flags, command, identifier, docs, opts) + finally: + # Add the field back to the command. + if identifier: + command[identifier] = docs + + +def _query_impl( + options: int, + collection_name: str, + num_to_skip: int, + num_to_return: int, + query: Mapping[str, Any], + field_selector: Optional[Mapping[str, Any]], + opts: CodecOptions, +) -> tuple[bytes, int]: + """Get an OP_QUERY message.""" + encoded = _dict_to_bson(query, False, opts) + if field_selector: + efs = _dict_to_bson(field_selector, False, opts) + else: + efs = b"" + max_bson_size = max(len(encoded), len(efs)) + return ( + b"".join( + [ + _pack_int(options), + _make_c_string(collection_name), + _pack_int(num_to_skip), + _pack_int(num_to_return), + encoded, + efs, + ] + ), + max_bson_size, + ) + + +def _query_compressed( + options: int, + collection_name: str, + num_to_skip: int, + num_to_return: int, + query: Mapping[str, Any], + field_selector: Optional[Mapping[str, Any]], + opts: CodecOptions, + ctx: Union[SnappyContext, ZlibContext, ZstdContext], +) -> tuple[int, bytes, int]: + """Internal compressed query message helper.""" + op_query, max_bson_size = _query_impl( + options, collection_name, num_to_skip, num_to_return, query, field_selector, opts + ) + rid, msg = _compress(2004, op_query, ctx) + return rid, msg, max_bson_size + + +def _query_uncompressed( + options: int, + collection_name: str, + num_to_skip: int, + num_to_return: int, + query: Mapping[str, Any], + field_selector: Optional[Mapping[str, Any]], + opts: CodecOptions, +) -> tuple[int, bytes, int]: + """Internal query message helper.""" + op_query, max_bson_size = _query_impl( + options, collection_name, num_to_skip, num_to_return, query, field_selector, opts + ) + rid, msg = __pack_message(2004, op_query) + return rid, msg, max_bson_size + + +if _use_c: + _query_uncompressed = _cmessage._query_message + + +def _query( + options: int, + collection_name: str, + num_to_skip: int, + num_to_return: int, + query: Mapping[str, Any], + field_selector: Optional[Mapping[str, Any]], + opts: CodecOptions, + ctx: Union[SnappyContext, ZlibContext, ZstdContext, None] = None, +) -> tuple[int, bytes, int]: + """Get a **query** message.""" + if ctx: + return _query_compressed( + options, collection_name, num_to_skip, num_to_return, query, field_selector, opts, ctx + ) + return _query_uncompressed( + options, collection_name, num_to_skip, num_to_return, query, field_selector, opts + ) + + +_pack_long_long = struct.Struct(" bytes: + """Get an OP_GET_MORE message.""" + return b"".join( + [ + _ZERO_32, + _make_c_string(collection_name), + _pack_int(num_to_return), + _pack_long_long(cursor_id), + ] + ) + + +def _get_more_compressed( + collection_name: str, + num_to_return: int, + cursor_id: int, + ctx: Union[SnappyContext, ZlibContext, ZstdContext], +) -> tuple[int, bytes]: + """Internal compressed getMore message helper.""" + return _compress(2005, _get_more_impl(collection_name, num_to_return, cursor_id), ctx) + + +def _get_more_uncompressed( + collection_name: str, num_to_return: int, cursor_id: int +) -> tuple[int, bytes]: + """Internal getMore message helper.""" + return __pack_message(2005, _get_more_impl(collection_name, num_to_return, cursor_id)) + + +if _use_c: + _get_more_uncompressed = _cmessage._get_more_message + + +def _get_more( + collection_name: str, + num_to_return: int, + cursor_id: int, + ctx: Union[SnappyContext, ZlibContext, ZstdContext, None] = None, +) -> tuple[int, bytes]: + """Get a **getMore** message.""" + if ctx: + return _get_more_compressed(collection_name, num_to_return, cursor_id, ctx) + return _get_more_uncompressed(collection_name, num_to_return, cursor_id) + + +class _BulkWriteContext: + """A wrapper around Connection for use with write splitting functions.""" + + __slots__ = ( + "db_name", + "conn", + "op_id", + "name", + "field", + "publish", + "start_time", + "listeners", + "session", + "compress", + "op_type", + "codec", + ) + + def __init__( + self, + database_name: str, + cmd_name: str, + conn: Connection, + operation_id: int, + listeners: _EventListeners, + session: ClientSession, + op_type: int, + codec: CodecOptions, + ): + self.db_name = database_name + self.conn = conn + self.op_id = operation_id + self.listeners = listeners + self.publish = listeners.enabled_for_commands + self.name = cmd_name + self.field = _FIELD_MAP[self.name] + self.start_time = datetime.datetime.now() + self.session = session + self.compress = bool(conn.compression_context) + self.op_type = op_type + self.codec = codec + + def __batch_command( + self, cmd: MutableMapping[str, Any], docs: list[Mapping[str, Any]] + ) -> tuple[int, bytes, list[Mapping[str, Any]]]: + namespace = self.db_name + ".$cmd" + request_id, msg, to_send = _do_batched_op_msg( + namespace, self.op_type, cmd, docs, self.codec, self + ) + if not to_send: + raise InvalidOperation("cannot do an empty bulk write") + return request_id, msg, to_send + + def execute( + self, cmd: MutableMapping[str, Any], docs: list[Mapping[str, Any]], client: MongoClient + ) -> tuple[Mapping[str, Any], list[Mapping[str, Any]]]: + request_id, msg, to_send = self.__batch_command(cmd, docs) + result = self.write_command(cmd, request_id, msg, to_send, client) + client._process_response(result, self.session) + return result, to_send + + def execute_unack( + self, cmd: MutableMapping[str, Any], docs: list[Mapping[str, Any]], client: MongoClient + ) -> list[Mapping[str, Any]]: + request_id, msg, to_send = self.__batch_command(cmd, docs) + # Though this isn't strictly a "legacy" write, the helper + # handles publishing commands and sending our message + # without receiving a result. Send 0 for max_doc_size + # to disable size checking. Size checking is handled while + # the documents are encoded to BSON. + self.unack_write(cmd, request_id, msg, 0, to_send, client) + return to_send + + @property + def max_bson_size(self) -> int: + """A proxy for SockInfo.max_bson_size.""" + return self.conn.max_bson_size + + @property + def max_message_size(self) -> int: + """A proxy for SockInfo.max_message_size.""" + if self.compress: + # Subtract 16 bytes for the message header. + return self.conn.max_message_size - 16 + return self.conn.max_message_size + + @property + def max_write_batch_size(self) -> int: + """A proxy for SockInfo.max_write_batch_size.""" + return self.conn.max_write_batch_size + + @property + def max_split_size(self) -> int: + """The maximum size of a BSON command before batch splitting.""" + return self.max_bson_size + + def unack_write( + self, + cmd: MutableMapping[str, Any], + request_id: int, + msg: bytes, + max_doc_size: int, + docs: list[Mapping[str, Any]], + client: MongoClient, + ) -> Optional[Mapping[str, Any]]: + """A proxy for Connection.unack_write that handles event publishing.""" + if _COMMAND_LOGGER.isEnabledFor(logging.DEBUG): + _debug_log( + _COMMAND_LOGGER, + clientId=client._topology_settings._topology_id, + message=_CommandStatusMessage.STARTED, + command=cmd, + commandName=next(iter(cmd)), + databaseName=self.db_name, + requestId=request_id, + operationId=request_id, + driverConnectionId=self.conn.id, + serverConnectionId=self.conn.server_connection_id, + serverHost=self.conn.address[0], + serverPort=self.conn.address[1], + serviceId=self.conn.service_id, + ) + if self.publish: + cmd = self._start(cmd, request_id, docs) + try: + result = self.conn.unack_write(msg, max_doc_size) # type: ignore[func-returns-value] + duration = datetime.datetime.now() - self.start_time + if result is not None: + reply = _convert_write_result(self.name, cmd, result) + else: + # Comply with APM spec. + reply = {"ok": 1} + if _COMMAND_LOGGER.isEnabledFor(logging.DEBUG): + _debug_log( + _COMMAND_LOGGER, + clientId=client._topology_settings._topology_id, + message=_CommandStatusMessage.SUCCEEDED, + durationMS=duration, + reply=reply, + commandName=next(iter(cmd)), + databaseName=self.db_name, + requestId=request_id, + operationId=request_id, + driverConnectionId=self.conn.id, + serverConnectionId=self.conn.server_connection_id, + serverHost=self.conn.address[0], + serverPort=self.conn.address[1], + serviceId=self.conn.service_id, + ) + if self.publish: + self._succeed(request_id, reply, duration) + except Exception as exc: + duration = datetime.datetime.now() - self.start_time + if isinstance(exc, OperationFailure): + failure: _DocumentOut = _convert_write_result(self.name, cmd, exc.details) # type: ignore[arg-type] + elif isinstance(exc, NotPrimaryError): + failure = exc.details # type: ignore[assignment] + else: + failure = _convert_exception(exc) + if _COMMAND_LOGGER.isEnabledFor(logging.DEBUG): + _debug_log( + _COMMAND_LOGGER, + clientId=client._topology_settings._topology_id, + message=_CommandStatusMessage.FAILED, + durationMS=duration, + failure=failure, + commandName=next(iter(cmd)), + databaseName=self.db_name, + requestId=request_id, + operationId=request_id, + driverConnectionId=self.conn.id, + serverConnectionId=self.conn.server_connection_id, + serverHost=self.conn.address[0], + serverPort=self.conn.address[1], + serviceId=self.conn.service_id, + isServerSideError=isinstance(exc, OperationFailure), + ) + if self.publish: + assert self.start_time is not None + self._fail(request_id, failure, duration) + raise + finally: + self.start_time = datetime.datetime.now() + return result + + @_handle_reauth + def write_command( + self, + cmd: MutableMapping[str, Any], + request_id: int, + msg: bytes, + docs: list[Mapping[str, Any]], + client: MongoClient, + ) -> dict[str, Any]: + """A proxy for SocketInfo.write_command that handles event publishing.""" + cmd[self.field] = docs + if _COMMAND_LOGGER.isEnabledFor(logging.DEBUG): + _debug_log( + _COMMAND_LOGGER, + clientId=client._topology_settings._topology_id, + message=_CommandStatusMessage.STARTED, + command=cmd, + commandName=next(iter(cmd)), + databaseName=self.db_name, + requestId=request_id, + operationId=request_id, + driverConnectionId=self.conn.id, + serverConnectionId=self.conn.server_connection_id, + serverHost=self.conn.address[0], + serverPort=self.conn.address[1], + serviceId=self.conn.service_id, + ) + if self.publish: + self._start(cmd, request_id, docs) + try: + reply = self.conn.write_command(request_id, msg, self.codec) + duration = datetime.datetime.now() - self.start_time + if _COMMAND_LOGGER.isEnabledFor(logging.DEBUG): + _debug_log( + _COMMAND_LOGGER, + clientId=client._topology_settings._topology_id, + message=_CommandStatusMessage.SUCCEEDED, + durationMS=duration, + reply=reply, + commandName=next(iter(cmd)), + databaseName=self.db_name, + requestId=request_id, + operationId=request_id, + driverConnectionId=self.conn.id, + serverConnectionId=self.conn.server_connection_id, + serverHost=self.conn.address[0], + serverPort=self.conn.address[1], + serviceId=self.conn.service_id, + ) + if self.publish: + self._succeed(request_id, reply, duration) + except Exception as exc: + duration = datetime.datetime.now() - self.start_time + if isinstance(exc, (NotPrimaryError, OperationFailure)): + failure: _DocumentOut = exc.details # type: ignore[assignment] + else: + failure = _convert_exception(exc) + if _COMMAND_LOGGER.isEnabledFor(logging.DEBUG): + _debug_log( + _COMMAND_LOGGER, + clientId=client._topology_settings._topology_id, + message=_CommandStatusMessage.FAILED, + durationMS=duration, + failure=failure, + commandName=next(iter(cmd)), + databaseName=self.db_name, + requestId=request_id, + operationId=request_id, + driverConnectionId=self.conn.id, + serverConnectionId=self.conn.server_connection_id, + serverHost=self.conn.address[0], + serverPort=self.conn.address[1], + serviceId=self.conn.service_id, + isServerSideError=isinstance(exc, OperationFailure), + ) + + if self.publish: + self._fail(request_id, failure, duration) + raise + finally: + self.start_time = datetime.datetime.now() + return reply + + def _start( + self, cmd: MutableMapping[str, Any], request_id: int, docs: list[Mapping[str, Any]] + ) -> MutableMapping[str, Any]: + """Publish a CommandStartedEvent.""" + cmd[self.field] = docs + self.listeners.publish_command_start( + cmd, + self.db_name, + request_id, + self.conn.address, + self.conn.server_connection_id, + self.op_id, + self.conn.service_id, + ) + return cmd + + def _succeed(self, request_id: int, reply: _DocumentOut, duration: timedelta) -> None: + """Publish a CommandSucceededEvent.""" + self.listeners.publish_command_success( + duration, + reply, + self.name, + request_id, + self.conn.address, + self.conn.server_connection_id, + self.op_id, + self.conn.service_id, + database_name=self.db_name, + ) + + def _fail(self, request_id: int, failure: _DocumentOut, duration: timedelta) -> None: + """Publish a CommandFailedEvent.""" + self.listeners.publish_command_failure( + duration, + failure, + self.name, + request_id, + self.conn.address, + self.conn.server_connection_id, + self.op_id, + self.conn.service_id, + database_name=self.db_name, + ) + + +# From the Client Side Encryption spec: +# Because automatic encryption increases the size of commands, the driver +# MUST split bulk writes at a reduced size limit before undergoing automatic +# encryption. The write payload MUST be split at 2MiB (2097152). +_MAX_SPLIT_SIZE_ENC = 2097152 + + +class _EncryptedBulkWriteContext(_BulkWriteContext): + __slots__ = () + + def __batch_command( + self, cmd: MutableMapping[str, Any], docs: list[Mapping[str, Any]] + ) -> tuple[dict[str, Any], list[Mapping[str, Any]]]: + namespace = self.db_name + ".$cmd" + msg, to_send = _encode_batched_write_command( + namespace, self.op_type, cmd, docs, self.codec, self + ) + if not to_send: + raise InvalidOperation("cannot do an empty bulk write") + + # Chop off the OP_QUERY header to get a properly batched write command. + cmd_start = msg.index(b"\x00", 4) + 9 + outgoing = _inflate_bson(memoryview(msg)[cmd_start:], DEFAULT_RAW_BSON_OPTIONS) + return outgoing, to_send + + def execute( + self, cmd: MutableMapping[str, Any], docs: list[Mapping[str, Any]], client: MongoClient + ) -> tuple[Mapping[str, Any], list[Mapping[str, Any]]]: + batched_cmd, to_send = self.__batch_command(cmd, docs) + result: Mapping[str, Any] = self.conn.command( + self.db_name, batched_cmd, codec_options=self.codec, session=self.session, client=client + ) + return result, to_send + + def execute_unack( + self, cmd: MutableMapping[str, Any], docs: list[Mapping[str, Any]], client: MongoClient + ) -> list[Mapping[str, Any]]: + batched_cmd, to_send = self.__batch_command(cmd, docs) + self.conn.command( + self.db_name, + batched_cmd, + write_concern=WriteConcern(w=0), + session=self.session, + client=client, + ) + return to_send + + @property + def max_split_size(self) -> int: + """Reduce the batch splitting size.""" + return _MAX_SPLIT_SIZE_ENC + + +def _raise_document_too_large(operation: str, doc_size: int, max_size: int) -> NoReturn: + """Internal helper for raising DocumentTooLarge.""" + if operation == "insert": + raise DocumentTooLarge( + "BSON document too large (%d bytes)" + " - the connected server supports" + " BSON document sizes up to %d" + " bytes." % (doc_size, max_size) + ) + else: + # There's nothing intelligent we can say + # about size for update and delete + raise DocumentTooLarge(f"{operation!r} command document too large") + + +# OP_MSG ------------------------------------------------------------- + + +_OP_MSG_MAP = { + _INSERT: b"documents\x00", + _UPDATE: b"updates\x00", + _DELETE: b"deletes\x00", +} + + +def _batched_op_msg_impl( + operation: int, + command: Mapping[str, Any], + docs: list[Mapping[str, Any]], + ack: bool, + opts: CodecOptions, + ctx: _BulkWriteContext, + buf: _BytesIO, +) -> tuple[list[Mapping[str, Any]], int]: + """Create a batched OP_MSG write.""" + max_bson_size = ctx.max_bson_size + max_write_batch_size = ctx.max_write_batch_size + max_message_size = ctx.max_message_size + + flags = b"\x00\x00\x00\x00" if ack else b"\x02\x00\x00\x00" + # Flags + buf.write(flags) + + # Type 0 Section + buf.write(b"\x00") + buf.write(_dict_to_bson(command, False, opts)) + + # Type 1 Section + buf.write(b"\x01") + size_location = buf.tell() + # Save space for size + buf.write(b"\x00\x00\x00\x00") + try: + buf.write(_OP_MSG_MAP[operation]) + except KeyError: + raise InvalidOperation("Unknown command") from None + + to_send = [] + idx = 0 + for doc in docs: + # Encode the current operation + value = _dict_to_bson(doc, False, opts) + doc_length = len(value) + new_message_size = buf.tell() + doc_length + # Does first document exceed max_message_size? + doc_too_large = idx == 0 and (new_message_size > max_message_size) + # When OP_MSG is used unacknowledged we have to check + # document size client side or applications won't be notified. + # Otherwise we let the server deal with documents that are too large + # since ordered=False causes those documents to be skipped instead of + # halting the bulk write operation. + unacked_doc_too_large = not ack and (doc_length > max_bson_size) + if doc_too_large or unacked_doc_too_large: + write_op = list(_FIELD_MAP.keys())[operation] + _raise_document_too_large(write_op, len(value), max_bson_size) + # We have enough data, return this batch. + if new_message_size > max_message_size: + break + buf.write(value) + to_send.append(doc) + idx += 1 + # We have enough documents, return this batch. + if idx == max_write_batch_size: + break + + # Write type 1 section size + length = buf.tell() + buf.seek(size_location) + buf.write(_pack_int(length - size_location)) + + return to_send, length + + +def _encode_batched_op_msg( + operation: int, + command: Mapping[str, Any], + docs: list[Mapping[str, Any]], + ack: bool, + opts: CodecOptions, + ctx: _BulkWriteContext, +) -> tuple[bytes, list[Mapping[str, Any]]]: + """Encode the next batched insert, update, or delete operation + as OP_MSG. + """ + buf = _BytesIO() + + to_send, _ = _batched_op_msg_impl(operation, command, docs, ack, opts, ctx, buf) + return buf.getvalue(), to_send + + +if _use_c: + _encode_batched_op_msg = _cmessage._encode_batched_op_msg + + +def _batched_op_msg_compressed( + operation: int, + command: Mapping[str, Any], + docs: list[Mapping[str, Any]], + ack: bool, + opts: CodecOptions, + ctx: _BulkWriteContext, +) -> tuple[int, bytes, list[Mapping[str, Any]]]: + """Create the next batched insert, update, or delete operation + with OP_MSG, compressed. + """ + data, to_send = _encode_batched_op_msg(operation, command, docs, ack, opts, ctx) + + assert ctx.conn.compression_context is not None + request_id, msg = _compress(2013, data, ctx.conn.compression_context) + return request_id, msg, to_send + + +def _batched_op_msg( + operation: int, + command: Mapping[str, Any], + docs: list[Mapping[str, Any]], + ack: bool, + opts: CodecOptions, + ctx: _BulkWriteContext, +) -> tuple[int, bytes, list[Mapping[str, Any]]]: + """OP_MSG implementation entry point.""" + buf = _BytesIO() + + # Save space for message length and request id + buf.write(_ZERO_64) + # responseTo, opCode + buf.write(b"\x00\x00\x00\x00\xdd\x07\x00\x00") + + to_send, length = _batched_op_msg_impl(operation, command, docs, ack, opts, ctx, buf) + + # Header - request id and message length + buf.seek(4) + request_id = _randint() + buf.write(_pack_int(request_id)) + buf.seek(0) + buf.write(_pack_int(length)) + + return request_id, buf.getvalue(), to_send + + +if _use_c: + _batched_op_msg = _cmessage._batched_op_msg + + +def _do_batched_op_msg( + namespace: str, + operation: int, + command: MutableMapping[str, Any], + docs: list[Mapping[str, Any]], + opts: CodecOptions, + ctx: _BulkWriteContext, +) -> tuple[int, bytes, list[Mapping[str, Any]]]: + """Create the next batched insert, update, or delete operation + using OP_MSG. + """ + command["$db"] = namespace.split(".", 1)[0] + if "writeConcern" in command: + ack = bool(command["writeConcern"].get("w", 1)) + else: + ack = True + if ctx.conn.compression_context: + return _batched_op_msg_compressed(operation, command, docs, ack, opts, ctx) + return _batched_op_msg(operation, command, docs, ack, opts, ctx) + + +# End OP_MSG ----------------------------------------------------- + + +def _encode_batched_write_command( + namespace: str, + operation: int, + command: MutableMapping[str, Any], + docs: list[Mapping[str, Any]], + opts: CodecOptions, + ctx: _BulkWriteContext, +) -> tuple[bytes, list[Mapping[str, Any]]]: + """Encode the next batched insert, update, or delete command.""" + buf = _BytesIO() + + to_send, _ = _batched_write_command_impl(namespace, operation, command, docs, opts, ctx, buf) + return buf.getvalue(), to_send + + +if _use_c: + _encode_batched_write_command = _cmessage._encode_batched_write_command + + +def _batched_write_command_impl( + namespace: str, + operation: int, + command: MutableMapping[str, Any], + docs: list[Mapping[str, Any]], + opts: CodecOptions, + ctx: _BulkWriteContext, + buf: _BytesIO, +) -> tuple[list[Mapping[str, Any]], int]: + """Create a batched OP_QUERY write command.""" + max_bson_size = ctx.max_bson_size + max_write_batch_size = ctx.max_write_batch_size + # Max BSON object size + 16k - 2 bytes for ending NUL bytes. + # Server guarantees there is enough room: SERVER-10643. + max_cmd_size = max_bson_size + _COMMAND_OVERHEAD + max_split_size = ctx.max_split_size + + # No options + buf.write(_ZERO_32) + # Namespace as C string + buf.write(namespace.encode("utf8")) + buf.write(_ZERO_8) + # Skip: 0, Limit: -1 + buf.write(_SKIPLIM) + + # Where to write command document length + command_start = buf.tell() + buf.write(encode(command)) + + # Start of payload + buf.seek(-1, 2) + # Work around some Jython weirdness. + buf.truncate() + try: + buf.write(_OP_MAP[operation]) + except KeyError: + raise InvalidOperation("Unknown command") from None + + # Where to write list document length + list_start = buf.tell() - 4 + to_send = [] + idx = 0 + for doc in docs: + # Encode the current operation + key = str(idx).encode("utf8") + value = _dict_to_bson(doc, False, opts) + # Is there enough room to add this document? max_cmd_size accounts for + # the two trailing null bytes. + doc_too_large = len(value) > max_cmd_size + if doc_too_large: + write_op = list(_FIELD_MAP.keys())[operation] + _raise_document_too_large(write_op, len(value), max_bson_size) + enough_data = idx >= 1 and (buf.tell() + len(key) + len(value)) >= max_split_size + enough_documents = idx >= max_write_batch_size + if enough_data or enough_documents: + break + buf.write(_BSONOBJ) + buf.write(key) + buf.write(_ZERO_8) + buf.write(value) + to_send.append(doc) + idx += 1 + + # Finalize the current OP_QUERY message. + # Close list and command documents + buf.write(_ZERO_16) + + # Write document lengths and request id + length = buf.tell() + buf.seek(list_start) + buf.write(_pack_int(length - list_start - 1)) + buf.seek(command_start) + buf.write(_pack_int(length - command_start)) + + return to_send, length + + +class _OpReply: + """A MongoDB OP_REPLY response message.""" + + __slots__ = ("flags", "cursor_id", "number_returned", "documents") + + UNPACK_FROM = struct.Struct(" list[bytes]: + """Check the response header from the database, without decoding BSON. + + Check the response for errors and unpack. + + Can raise CursorNotFound, NotPrimaryError, ExecutionTimeout, or + OperationFailure. + + :param cursor_id: cursor_id we sent to get this response - + used for raising an informative exception when we get cursor id not + valid at server response. + """ + if self.flags & 1: + # Shouldn't get this response if we aren't doing a getMore + if cursor_id is None: + raise ProtocolError("No cursor id for getMore operation") + + # Fake a getMore command response. OP_GET_MORE provides no + # document. + msg = "Cursor not found, cursor id: %d" % (cursor_id,) + errobj = {"ok": 0, "errmsg": msg, "code": 43} + raise CursorNotFound(msg, 43, errobj) + elif self.flags & 2: + error_object: dict = bson.BSON(self.documents).decode() + # Fake the ok field if it doesn't exist. + error_object.setdefault("ok", 0) + if error_object["$err"].startswith(HelloCompat.LEGACY_ERROR): + raise NotPrimaryError(error_object["$err"], error_object) + elif error_object.get("code") == 50: + default_msg = "operation exceeded time limit" + raise ExecutionTimeout( + error_object.get("$err", default_msg), error_object.get("code"), error_object + ) + raise OperationFailure( + "database error: %s" % error_object.get("$err"), + error_object.get("code"), + error_object, + ) + if self.documents: + return [self.documents] + return [] + + def unpack_response( + self, + cursor_id: Optional[int] = None, + codec_options: CodecOptions = _UNICODE_REPLACE_CODEC_OPTIONS, + user_fields: Optional[Mapping[str, Any]] = None, + legacy_response: bool = False, + ) -> list[dict[str, Any]]: + """Unpack a response from the database and decode the BSON document(s). + + Check the response for errors and unpack, returning a dictionary + containing the response data. + + Can raise CursorNotFound, NotPrimaryError, ExecutionTimeout, or + OperationFailure. + + :param cursor_id: cursor_id we sent to get this response - + used for raising an informative exception when we get cursor id not + valid at server response + :param codec_options: an instance of + :class:`~bson.codec_options.CodecOptions` + :param user_fields: Response fields that should be decoded + using the TypeDecoders from codec_options, passed to + bson._decode_all_selective. + """ + self.raw_response(cursor_id) + if legacy_response: + return bson.decode_all(self.documents, codec_options) + return bson._decode_all_selective(self.documents, codec_options, user_fields) + + def command_response(self, codec_options: CodecOptions) -> dict[str, Any]: + """Unpack a command response.""" + docs = self.unpack_response(codec_options=codec_options) + assert self.number_returned == 1 + return docs[0] + + def raw_command_response(self) -> NoReturn: + """Return the bytes of the command response.""" + # This should never be called on _OpReply. + raise NotImplementedError + + @property + def more_to_come(self) -> bool: + """Is the moreToCome bit set on this response?""" + return False + + @classmethod + def unpack(cls, msg: bytes) -> _OpReply: + """Construct an _OpReply from raw bytes.""" + # PYTHON-945: ignore starting_from field. + flags, cursor_id, _, number_returned = cls.UNPACK_FROM(msg) + + documents = msg[20:] + return cls(flags, cursor_id, number_returned, documents) + + +class _OpMsg: + """A MongoDB OP_MSG response message.""" + + __slots__ = ("flags", "cursor_id", "number_returned", "payload_document") + + UNPACK_FROM = struct.Struct(" list[Mapping[str, Any]]: + """ + cursor_id is ignored + user_fields is used to determine which fields must not be decoded + """ + inflated_response = _decode_selective( + RawBSONDocument(self.payload_document), user_fields, _RAW_ARRAY_BSON_OPTIONS + ) + return [inflated_response] + + def unpack_response( + self, + cursor_id: Optional[int] = None, + codec_options: CodecOptions = _UNICODE_REPLACE_CODEC_OPTIONS, + user_fields: Optional[Mapping[str, Any]] = None, + legacy_response: bool = False, + ) -> list[dict[str, Any]]: + """Unpack a OP_MSG command response. + + :param cursor_id: Ignored, for compatibility with _OpReply. + :param codec_options: an instance of + :class:`~bson.codec_options.CodecOptions` + :param user_fields: Response fields that should be decoded + using the TypeDecoders from codec_options, passed to + bson._decode_all_selective. + """ + # If _OpMsg is in-use, this cannot be a legacy response. + assert not legacy_response + return bson._decode_all_selective(self.payload_document, codec_options, user_fields) + + def command_response(self, codec_options: CodecOptions) -> dict[str, Any]: + """Unpack a command response.""" + return self.unpack_response(codec_options=codec_options)[0] + + def raw_command_response(self) -> bytes: + """Return the bytes of the command response.""" + return self.payload_document + + @property + def more_to_come(self) -> bool: + """Is the moreToCome bit set on this response?""" + return bool(self.flags & self.MORE_TO_COME) + + @classmethod + def unpack(cls, msg: bytes) -> _OpMsg: + """Construct an _OpMsg from raw bytes.""" + flags, first_payload_type, first_payload_size = cls.UNPACK_FROM(msg) + if flags != 0: + if flags & cls.CHECKSUM_PRESENT: + raise ProtocolError(f"Unsupported OP_MSG flag checksumPresent: 0x{flags:x}") + + if flags ^ cls.MORE_TO_COME: + raise ProtocolError(f"Unsupported OP_MSG flags: 0x{flags:x}") + if first_payload_type != 0: + raise ProtocolError(f"Unsupported OP_MSG payload type: 0x{first_payload_type:x}") + + if len(msg) != first_payload_size + 5: + raise ProtocolError("Unsupported OP_MSG reply: >1 section") + + payload_document = msg[5:] + return cls(flags, payload_document) + + +_UNPACK_REPLY: dict[int, Callable[[bytes], Union[_OpReply, _OpMsg]]] = { + _OpReply.OP_CODE: _OpReply.unpack, + _OpMsg.OP_CODE: _OpMsg.unpack, +} diff --git a/venv/Lib/site-packages/pymongo/mongo_client.py b/venv/Lib/site-packages/pymongo/mongo_client.py new file mode 100644 index 00000000..f2076b08 --- /dev/null +++ b/venv/Lib/site-packages/pymongo/mongo_client.py @@ -0,0 +1,2529 @@ +# Copyright 2009-present MongoDB, 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. + +"""Tools for connecting to MongoDB. + +.. seealso:: :doc:`/examples/high_availability` for examples of connecting + to replica sets or sets of mongos servers. + +To get a :class:`~pymongo.database.Database` instance from a +:class:`MongoClient` use either dictionary-style or attribute-style +access: + +.. doctest:: + + >>> from pymongo import MongoClient + >>> c = MongoClient() + >>> c.test_database + Database(MongoClient(host=['localhost:27017'], document_class=dict, tz_aware=False, connect=True), 'test_database') + >>> c["test-database"] + Database(MongoClient(host=['localhost:27017'], document_class=dict, tz_aware=False, connect=True), 'test-database') +""" +from __future__ import annotations + +import contextlib +import os +import weakref +from collections import defaultdict +from typing import ( + TYPE_CHECKING, + Any, + Callable, + ContextManager, + FrozenSet, + Generic, + Iterator, + Mapping, + MutableMapping, + NoReturn, + Optional, + Sequence, + Type, + TypeVar, + Union, + cast, +) + +from bson.codec_options import DEFAULT_CODEC_OPTIONS, CodecOptions, TypeRegistry +from bson.timestamp import Timestamp +from pymongo import ( + _csot, + client_session, + common, + database, + helpers, + message, + periodic_executor, + uri_parser, +) +from pymongo.change_stream import ChangeStream, ClusterChangeStream +from pymongo.client_options import ClientOptions +from pymongo.client_session import _EmptyServerSession +from pymongo.command_cursor import CommandCursor +from pymongo.errors import ( + AutoReconnect, + BulkWriteError, + ConfigurationError, + ConnectionFailure, + InvalidOperation, + NotPrimaryError, + OperationFailure, + PyMongoError, + ServerSelectionTimeoutError, + WaitQueueTimeoutError, + WriteConcernError, +) +from pymongo.lock import _HAS_REGISTER_AT_FORK, _create_lock, _release_locks +from pymongo.logger import _CLIENT_LOGGER, _log_or_warn +from pymongo.monitoring import ConnectionClosedReason +from pymongo.operations import _Op +from pymongo.read_preferences import ReadPreference, _ServerMode +from pymongo.server_selectors import writable_server_selector +from pymongo.server_type import SERVER_TYPE +from pymongo.settings import TopologySettings +from pymongo.topology import Topology, _ErrorContext +from pymongo.topology_description import TOPOLOGY_TYPE, TopologyDescription +from pymongo.typings import ( + ClusterTime, + _Address, + _CollationIn, + _DocumentType, + _DocumentTypeArg, + _Pipeline, +) +from pymongo.uri_parser import ( + _check_options, + _handle_option_deprecations, + _handle_security_options, + _normalize_options, +) +from pymongo.write_concern import DEFAULT_WRITE_CONCERN, WriteConcern + +if TYPE_CHECKING: + import sys + from types import TracebackType + + from bson.objectid import ObjectId + from pymongo.bulk import _Bulk + from pymongo.client_session import ClientSession, _ServerSession + from pymongo.cursor import _ConnectionManager + from pymongo.database import Database + from pymongo.message import _CursorAddress, _GetMore, _Query + from pymongo.pool import Connection + from pymongo.read_concern import ReadConcern + from pymongo.response import Response + from pymongo.server import Server + from pymongo.server_selectors import Selection + + if sys.version_info[:2] >= (3, 9): + from collections.abc import Generator + else: + # Deprecated since version 3.9: collections.abc.Generator now supports []. + from typing import Generator + +T = TypeVar("T") + +_WriteCall = Callable[[Optional["ClientSession"], "Connection", bool], T] +_ReadCall = Callable[[Optional["ClientSession"], "Server", "Connection", _ServerMode], T] + + +class MongoClient(common.BaseObject, Generic[_DocumentType]): + """ + A client-side representation of a MongoDB cluster. + + Instances can represent either a standalone MongoDB server, a replica + set, or a sharded cluster. Instances of this class are responsible for + maintaining up-to-date state of the cluster, and possibly cache + resources related to this, including background threads for monitoring, + and connection pools. + """ + + HOST = "localhost" + PORT = 27017 + # Define order to retrieve options from ClientOptions for __repr__. + # No host/port; these are retrieved from TopologySettings. + _constructor_args = ("document_class", "tz_aware", "connect") + _clients: weakref.WeakValueDictionary = weakref.WeakValueDictionary() + + def __init__( + self, + host: Optional[Union[str, Sequence[str]]] = None, + port: Optional[int] = None, + document_class: Optional[Type[_DocumentType]] = None, + tz_aware: Optional[bool] = None, + connect: Optional[bool] = None, + type_registry: Optional[TypeRegistry] = None, + **kwargs: Any, + ) -> None: + """Client for a MongoDB instance, a replica set, or a set of mongoses. + + .. warning:: Starting in PyMongo 4.0, ``directConnection`` now has a default value of + False instead of None. + For more details, see the relevant section of the PyMongo 4.x migration guide: + :ref:`pymongo4-migration-direct-connection`. + + The client object is thread-safe and has connection-pooling built in. + If an operation fails because of a network error, + :class:`~pymongo.errors.ConnectionFailure` is raised and the client + reconnects in the background. Application code should handle this + exception (recognizing that the operation failed) and then continue to + execute. + + The `host` parameter can be a full `mongodb URI + `_, in addition to + a simple hostname. It can also be a list of hostnames but no more + than one URI. Any port specified in the host string(s) will override + the `port` parameter. For username and + passwords reserved characters like ':', '/', '+' and '@' must be + percent encoded following RFC 2396:: + + from urllib.parse import quote_plus + + uri = "mongodb://%s:%s@%s" % ( + quote_plus(user), quote_plus(password), host) + client = MongoClient(uri) + + Unix domain sockets are also supported. The socket path must be percent + encoded in the URI:: + + uri = "mongodb://%s:%s@%s" % ( + quote_plus(user), quote_plus(password), quote_plus(socket_path)) + client = MongoClient(uri) + + But not when passed as a simple hostname:: + + client = MongoClient('/tmp/mongodb-27017.sock') + + Starting with version 3.6, PyMongo supports mongodb+srv:// URIs. The + URI must include one, and only one, hostname. The hostname will be + resolved to one or more DNS `SRV records + `_ which will be used + as the seed list for connecting to the MongoDB deployment. When using + SRV URIs, the `authSource` and `replicaSet` configuration options can + be specified using `TXT records + `_. See the + `Initial DNS Seedlist Discovery spec + `_ + for more details. Note that the use of SRV URIs implicitly enables + TLS support. Pass tls=false in the URI to override. + + .. note:: MongoClient creation will block waiting for answers from + DNS when mongodb+srv:// URIs are used. + + .. note:: Starting with version 3.0 the :class:`MongoClient` + constructor no longer blocks while connecting to the server or + servers, and it no longer raises + :class:`~pymongo.errors.ConnectionFailure` if they are + unavailable, nor :class:`~pymongo.errors.ConfigurationError` + if the user's credentials are wrong. Instead, the constructor + returns immediately and launches the connection process on + background threads. You can check if the server is available + like this:: + + from pymongo.errors import ConnectionFailure + client = MongoClient() + try: + # The ping command is cheap and does not require auth. + client.admin.command('ping') + except ConnectionFailure: + print("Server not available") + + .. warning:: When using PyMongo in a multiprocessing context, please + read :ref:`multiprocessing` first. + + .. note:: Many of the following options can be passed using a MongoDB + URI or keyword parameters. If the same option is passed in a URI and + as a keyword parameter the keyword parameter takes precedence. + + :param host: hostname or IP address or Unix domain socket + path of a single mongod or mongos instance to connect to, or a + mongodb URI, or a list of hostnames (but no more than one mongodb + URI). If `host` is an IPv6 literal it must be enclosed in '[' + and ']' characters + following the RFC2732 URL syntax (e.g. '[::1]' for localhost). + Multihomed and round robin DNS addresses are **not** supported. + :param port: port number on which to connect + :param document_class: default class to use for + documents returned from queries on this client + :param tz_aware: if ``True``, + :class:`~datetime.datetime` instances returned as values + in a document by this :class:`MongoClient` will be timezone + aware (otherwise they will be naive) + :param connect: if ``True`` (the default), immediately + begin connecting to MongoDB in the background. Otherwise connect + on the first operation. + :param type_registry: instance of + :class:`~bson.codec_options.TypeRegistry` to enable encoding + and decoding of custom types. + :param datetime_conversion: Specifies how UTC datetimes should be decoded + within BSON. Valid options include 'datetime_ms' to return as a + DatetimeMS, 'datetime' to return as a datetime.datetime and + raising a ValueError for out-of-range values, 'datetime_auto' to + return DatetimeMS objects when the underlying datetime is + out-of-range and 'datetime_clamp' to clamp to the minimum and + maximum possible datetimes. Defaults to 'datetime'. See + :ref:`handling-out-of-range-datetimes` for details. + + | **Other optional parameters can be passed as keyword arguments:** + + - `directConnection` (optional): if ``True``, forces this client to + connect directly to the specified MongoDB host as a standalone. + If ``false``, the client connects to the entire replica set of + which the given MongoDB host(s) is a part. If this is ``True`` + and a mongodb+srv:// URI or a URI containing multiple seeds is + provided, an exception will be raised. + - `maxPoolSize` (optional): The maximum allowable number of + concurrent connections to each connected server. Requests to a + server will block if there are `maxPoolSize` outstanding + connections to the requested server. Defaults to 100. Can be + either 0 or None, in which case there is no limit on the number + of concurrent connections. + - `minPoolSize` (optional): The minimum required number of concurrent + connections that the pool will maintain to each connected server. + Default is 0. + - `maxIdleTimeMS` (optional): The maximum number of milliseconds that + a connection can remain idle in the pool before being removed and + replaced. Defaults to `None` (no limit). + - `maxConnecting` (optional): The maximum number of connections that + each pool can establish concurrently. Defaults to `2`. + - `timeoutMS`: (integer or None) Controls how long (in + milliseconds) the driver will wait when executing an operation + (including retry attempts) before raising a timeout error. + ``0`` or ``None`` means no timeout. + - `socketTimeoutMS`: (integer or None) Controls how long (in + milliseconds) the driver will wait for a response after sending an + ordinary (non-monitoring) database operation before concluding that + a network error has occurred. ``0`` or ``None`` means no timeout. + Defaults to ``None`` (no timeout). + - `connectTimeoutMS`: (integer or None) Controls how long (in + milliseconds) the driver will wait during server monitoring when + connecting a new socket to a server before concluding the server + is unavailable. ``0`` or ``None`` means no timeout. + Defaults to ``20000`` (20 seconds). + - `server_selector`: (callable or None) Optional, user-provided + function that augments server selection rules. The function should + accept as an argument a list of + :class:`~pymongo.server_description.ServerDescription` objects and + return a list of server descriptions that should be considered + suitable for the desired operation. + - `serverSelectionTimeoutMS`: (integer) Controls how long (in + milliseconds) the driver will wait to find an available, + appropriate server to carry out a database operation; while it is + waiting, multiple server monitoring operations may be carried out, + each controlled by `connectTimeoutMS`. Defaults to ``30000`` (30 + seconds). + - `waitQueueTimeoutMS`: (integer or None) How long (in milliseconds) + a thread will wait for a socket from the pool if the pool has no + free sockets. Defaults to ``None`` (no timeout). + - `heartbeatFrequencyMS`: (optional) The number of milliseconds + between periodic server checks, or None to accept the default + frequency of 10 seconds. + - `serverMonitoringMode`: (optional) The server monitoring mode to use. + Valid values are the strings: "auto", "stream", "poll". Defaults to "auto". + - `appname`: (string or None) The name of the application that + created this MongoClient instance. The server will log this value + upon establishing each connection. It is also recorded in the slow + query log and profile collections. + - `driver`: (pair or None) A driver implemented on top of PyMongo can + pass a :class:`~pymongo.driver_info.DriverInfo` to add its name, + version, and platform to the message printed in the server log when + establishing a connection. + - `event_listeners`: a list or tuple of event listeners. See + :mod:`~pymongo.monitoring` for details. + - `retryWrites`: (boolean) Whether supported write operations + executed within this MongoClient will be retried once after a + network error. Defaults to ``True``. + The supported write operations are: + + - :meth:`~pymongo.collection.Collection.bulk_write`, as long as + :class:`~pymongo.operations.UpdateMany` or + :class:`~pymongo.operations.DeleteMany` are not included. + - :meth:`~pymongo.collection.Collection.delete_one` + - :meth:`~pymongo.collection.Collection.insert_one` + - :meth:`~pymongo.collection.Collection.insert_many` + - :meth:`~pymongo.collection.Collection.replace_one` + - :meth:`~pymongo.collection.Collection.update_one` + - :meth:`~pymongo.collection.Collection.find_one_and_delete` + - :meth:`~pymongo.collection.Collection.find_one_and_replace` + - :meth:`~pymongo.collection.Collection.find_one_and_update` + + Unsupported write operations include, but are not limited to, + :meth:`~pymongo.collection.Collection.aggregate` using the ``$out`` + pipeline operator and any operation with an unacknowledged write + concern (e.g. {w: 0})). See + https://github.com/mongodb/specifications/blob/master/source/retryable-writes/retryable-writes.rst + - `retryReads`: (boolean) Whether supported read operations + executed within this MongoClient will be retried once after a + network error. Defaults to ``True``. + The supported read operations are: + :meth:`~pymongo.collection.Collection.find`, + :meth:`~pymongo.collection.Collection.find_one`, + :meth:`~pymongo.collection.Collection.aggregate` without ``$out``, + :meth:`~pymongo.collection.Collection.distinct`, + :meth:`~pymongo.collection.Collection.count`, + :meth:`~pymongo.collection.Collection.estimated_document_count`, + :meth:`~pymongo.collection.Collection.count_documents`, + :meth:`pymongo.collection.Collection.watch`, + :meth:`~pymongo.collection.Collection.list_indexes`, + :meth:`pymongo.database.Database.watch`, + :meth:`~pymongo.database.Database.list_collections`, + :meth:`pymongo.mongo_client.MongoClient.watch`, + and :meth:`~pymongo.mongo_client.MongoClient.list_databases`. + + Unsupported read operations include, but are not limited to + :meth:`~pymongo.database.Database.command` and any getMore + operation on a cursor. + + Enabling retryable reads makes applications more resilient to + transient errors such as network failures, database upgrades, and + replica set failovers. For an exact definition of which errors + trigger a retry, see the `retryable reads specification + `_. + + - `compressors`: Comma separated list of compressors for wire + protocol compression. The list is used to negotiate a compressor + with the server. Currently supported options are "snappy", "zlib" + and "zstd". Support for snappy requires the + `python-snappy `_ package. + zlib support requires the Python standard library zlib module. zstd + requires the `zstandard `_ + package. By default no compression is used. Compression support + must also be enabled on the server. MongoDB 3.6+ supports snappy + and zlib compression. MongoDB 4.2+ adds support for zstd. + See :ref:`network-compression-example` for details. + - `zlibCompressionLevel`: (int) The zlib compression level to use + when zlib is used as the wire protocol compressor. Supported values + are -1 through 9. -1 tells the zlib library to use its default + compression level (usually 6). 0 means no compression. 1 is best + speed. 9 is best compression. Defaults to -1. + - `uuidRepresentation`: The BSON representation to use when encoding + from and decoding to instances of :class:`~uuid.UUID`. Valid + values are the strings: "standard", "pythonLegacy", "javaLegacy", + "csharpLegacy", and "unspecified" (the default). New applications + should consider setting this to "standard" for cross language + compatibility. See :ref:`handling-uuid-data-example` for details. + - `unicode_decode_error_handler`: The error handler to apply when + a Unicode-related error occurs during BSON decoding that would + otherwise raise :exc:`UnicodeDecodeError`. Valid options include + 'strict', 'replace', 'backslashreplace', 'surrogateescape', and + 'ignore'. Defaults to 'strict'. + - `srvServiceName`: (string) The SRV service name to use for + "mongodb+srv://" URIs. Defaults to "mongodb". Use it like so:: + + MongoClient("mongodb+srv://example.com/?srvServiceName=customname") + - `srvMaxHosts`: (int) limits the number of mongos-like hosts a client will + connect to. More specifically, when a "mongodb+srv://" connection string + resolves to more than srvMaxHosts number of hosts, the client will randomly + choose an srvMaxHosts sized subset of hosts. + + + | **Write Concern options:** + | (Only set if passed. No default values.) + + - `w`: (integer or string) If this is a replica set, write operations + will block until they have been replicated to the specified number + or tagged set of servers. `w=` always includes the replica set + primary (e.g. w=3 means write to the primary and wait until + replicated to **two** secondaries). Passing w=0 **disables write + acknowledgement** and all other write concern options. + - `wTimeoutMS`: **DEPRECATED** (integer) Used in conjunction with `w`. + Specify a value in milliseconds to control how long to wait for write propagation + to complete. If replication does not complete in the given + timeframe, a timeout exception is raised. Passing wTimeoutMS=0 + will cause **write operations to wait indefinitely**. + - `journal`: If ``True`` block until write operations have been + committed to the journal. Cannot be used in combination with + `fsync`. Write operations will fail with an exception if this + option is used when the server is running without journaling. + - `fsync`: If ``True`` and the server is running without journaling, + blocks until the server has synced all data files to disk. If the + server is running with journaling, this acts the same as the `j` + option, blocking until write operations have been committed to the + journal. Cannot be used in combination with `j`. + + | **Replica set keyword arguments for connecting with a replica set + - either directly or via a mongos:** + + - `replicaSet`: (string or None) The name of the replica set to + connect to. The driver will verify that all servers it connects to + match this name. Implies that the hosts specified are a seed list + and the driver should attempt to find all members of the set. + Defaults to ``None``. + + | **Read Preference:** + + - `readPreference`: The replica set read preference for this client. + One of ``primary``, ``primaryPreferred``, ``secondary``, + ``secondaryPreferred``, or ``nearest``. Defaults to ``primary``. + - `readPreferenceTags`: Specifies a tag set as a comma-separated list + of colon-separated key-value pairs. For example ``dc:ny,rack:1``. + Defaults to ``None``. + - `maxStalenessSeconds`: (integer) The maximum estimated + length of time a replica set secondary can fall behind the primary + in replication before it will no longer be selected for operations. + Defaults to ``-1``, meaning no maximum. If maxStalenessSeconds + is set, it must be a positive integer greater than or equal to + 90 seconds. + + .. seealso:: :doc:`/examples/server_selection` + + | **Authentication:** + + - `username`: A string. + - `password`: A string. + + Although username and password must be percent-escaped in a MongoDB + URI, they must not be percent-escaped when passed as parameters. In + this example, both the space and slash special characters are passed + as-is:: + + MongoClient(username="user name", password="pass/word") + + - `authSource`: The database to authenticate on. Defaults to the + database specified in the URI, if provided, or to "admin". + - `authMechanism`: See :data:`~pymongo.auth.MECHANISMS` for options. + If no mechanism is specified, PyMongo automatically SCRAM-SHA-1 + when connected to MongoDB 3.6 and negotiates the mechanism to use + (SCRAM-SHA-1 or SCRAM-SHA-256) when connected to MongoDB 4.0+. + - `authMechanismProperties`: Used to specify authentication mechanism + specific options. To specify the service name for GSSAPI + authentication pass authMechanismProperties='SERVICE_NAME:'. + To specify the session token for MONGODB-AWS authentication pass + ``authMechanismProperties='AWS_SESSION_TOKEN:'``. + + .. seealso:: :doc:`/examples/authentication` + + | **TLS/SSL configuration:** + + - `tls`: (boolean) If ``True``, create the connection to the server + using transport layer security. Defaults to ``False``. + - `tlsInsecure`: (boolean) Specify whether TLS constraints should be + relaxed as much as possible. Setting ``tlsInsecure=True`` implies + ``tlsAllowInvalidCertificates=True`` and + ``tlsAllowInvalidHostnames=True``. Defaults to ``False``. Think + very carefully before setting this to ``True`` as it dramatically + reduces the security of TLS. + - `tlsAllowInvalidCertificates`: (boolean) If ``True``, continues + the TLS handshake regardless of the outcome of the certificate + verification process. If this is ``False``, and a value is not + provided for ``tlsCAFile``, PyMongo will attempt to load system + provided CA certificates. If the python version in use does not + support loading system CA certificates then the ``tlsCAFile`` + parameter must point to a file of CA certificates. + ``tlsAllowInvalidCertificates=False`` implies ``tls=True``. + Defaults to ``False``. Think very carefully before setting this + to ``True`` as that could make your application vulnerable to + on-path attackers. + - `tlsAllowInvalidHostnames`: (boolean) If ``True``, disables TLS + hostname verification. ``tlsAllowInvalidHostnames=False`` implies + ``tls=True``. Defaults to ``False``. Think very carefully before + setting this to ``True`` as that could make your application + vulnerable to on-path attackers. + - `tlsCAFile`: A file containing a single or a bundle of + "certification authority" certificates, which are used to validate + certificates passed from the other end of the connection. + Implies ``tls=True``. Defaults to ``None``. + - `tlsCertificateKeyFile`: A file containing the client certificate + and private key. Implies ``tls=True``. Defaults to ``None``. + - `tlsCRLFile`: A file containing a PEM or DER formatted + certificate revocation list. Implies ``tls=True``. Defaults to + ``None``. + - `tlsCertificateKeyFilePassword`: The password or passphrase for + decrypting the private key in ``tlsCertificateKeyFile``. Only + necessary if the private key is encrypted. Defaults to ``None``. + - `tlsDisableOCSPEndpointCheck`: (boolean) If ``True``, disables + certificate revocation status checking via the OCSP responder + specified on the server certificate. + ``tlsDisableOCSPEndpointCheck=False`` implies ``tls=True``. + Defaults to ``False``. + - `ssl`: (boolean) Alias for ``tls``. + + | **Read Concern options:** + | (If not set explicitly, this will use the server default) + + - `readConcernLevel`: (string) The read concern level specifies the + level of isolation for read operations. For example, a read + operation using a read concern level of ``majority`` will only + return data that has been written to a majority of nodes. If the + level is left unspecified, the server default will be used. + + | **Client side encryption options:** + | (If not set explicitly, client side encryption will not be enabled.) + + - `auto_encryption_opts`: A + :class:`~pymongo.encryption_options.AutoEncryptionOpts` which + configures this client to automatically encrypt collection commands + and automatically decrypt results. See + :ref:`automatic-client-side-encryption` for an example. + If a :class:`MongoClient` is configured with + ``auto_encryption_opts`` and a non-None ``maxPoolSize``, a + separate internal ``MongoClient`` is created if any of the + following are true: + + - A ``key_vault_client`` is not passed to + :class:`~pymongo.encryption_options.AutoEncryptionOpts` + - ``bypass_auto_encrpytion=False`` is passed to + :class:`~pymongo.encryption_options.AutoEncryptionOpts` + + | **Stable API options:** + | (If not set explicitly, Stable API will not be enabled.) + + - `server_api`: A + :class:`~pymongo.server_api.ServerApi` which configures this + client to use Stable API. See :ref:`versioned-api-ref` for + details. + + .. seealso:: The MongoDB documentation on `connections `_. + + .. versionchanged:: 4.5 + Added the ``serverMonitoringMode`` keyword argument. + + .. versionchanged:: 4.2 + Added the ``timeoutMS`` keyword argument. + + .. versionchanged:: 4.0 + + - Removed the fsync, unlock, is_locked, database_names, and + close_cursor methods. + See the :ref:`pymongo4-migration-guide`. + - Removed the ``waitQueueMultiple`` and ``socketKeepAlive`` + keyword arguments. + - The default for `uuidRepresentation` was changed from + ``pythonLegacy`` to ``unspecified``. + - Added the ``srvServiceName``, ``maxConnecting``, and ``srvMaxHosts`` URI and + keyword arguments. + + .. versionchanged:: 3.12 + Added the ``server_api`` keyword argument. + The following keyword arguments were deprecated: + + - ``ssl_certfile`` and ``ssl_keyfile`` were deprecated in favor + of ``tlsCertificateKeyFile``. + + .. versionchanged:: 3.11 + Added the following keyword arguments and URI options: + + - ``tlsDisableOCSPEndpointCheck`` + - ``directConnection`` + + .. versionchanged:: 3.9 + Added the ``retryReads`` keyword argument and URI option. + Added the ``tlsInsecure`` keyword argument and URI option. + The following keyword arguments and URI options were deprecated: + + - ``wTimeout`` was deprecated in favor of ``wTimeoutMS``. + - ``j`` was deprecated in favor of ``journal``. + - ``ssl_cert_reqs`` was deprecated in favor of + ``tlsAllowInvalidCertificates``. + - ``ssl_match_hostname`` was deprecated in favor of + ``tlsAllowInvalidHostnames``. + - ``ssl_ca_certs`` was deprecated in favor of ``tlsCAFile``. + - ``ssl_certfile`` was deprecated in favor of + ``tlsCertificateKeyFile``. + - ``ssl_crlfile`` was deprecated in favor of ``tlsCRLFile``. + - ``ssl_pem_passphrase`` was deprecated in favor of + ``tlsCertificateKeyFilePassword``. + + .. versionchanged:: 3.9 + ``retryWrites`` now defaults to ``True``. + + .. versionchanged:: 3.8 + Added the ``server_selector`` keyword argument. + Added the ``type_registry`` keyword argument. + + .. versionchanged:: 3.7 + Added the ``driver`` keyword argument. + + .. versionchanged:: 3.6 + Added support for mongodb+srv:// URIs. + Added the ``retryWrites`` keyword argument and URI option. + + .. versionchanged:: 3.5 + Add ``username`` and ``password`` options. Document the + ``authSource``, ``authMechanism``, and ``authMechanismProperties`` + options. + Deprecated the ``socketKeepAlive`` keyword argument and URI option. + ``socketKeepAlive`` now defaults to ``True``. + + .. versionchanged:: 3.0 + :class:`~pymongo.mongo_client.MongoClient` is now the one and only + client class for a standalone server, mongos, or replica set. + It includes the functionality that had been split into + :class:`~pymongo.mongo_client.MongoReplicaSetClient`: it can connect + to a replica set, discover all its members, and monitor the set for + stepdowns, elections, and reconfigs. + + The :class:`~pymongo.mongo_client.MongoClient` constructor no + longer blocks while connecting to the server or servers, and it no + longer raises :class:`~pymongo.errors.ConnectionFailure` if they + are unavailable, nor :class:`~pymongo.errors.ConfigurationError` + if the user's credentials are wrong. Instead, the constructor + returns immediately and launches the connection process on + background threads. + + Therefore the ``alive`` method is removed since it no longer + provides meaningful information; even if the client is disconnected, + it may discover a server in time to fulfill the next operation. + + In PyMongo 2.x, :class:`~pymongo.MongoClient` accepted a list of + standalone MongoDB servers and used the first it could connect to:: + + MongoClient(['host1.com:27017', 'host2.com:27017']) + + A list of multiple standalones is no longer supported; if multiple + servers are listed they must be members of the same replica set, or + mongoses in the same sharded cluster. + + The behavior for a list of mongoses is changed from "high + availability" to "load balancing". Before, the client connected to + the lowest-latency mongos in the list, and used it until a network + error prompted it to re-evaluate all mongoses' latencies and + reconnect to one of them. In PyMongo 3, the client monitors its + network latency to all the mongoses continuously, and distributes + operations evenly among those with the lowest latency. See + :ref:`mongos-load-balancing` for more information. + + The ``connect`` option is added. + + The ``start_request``, ``in_request``, and ``end_request`` methods + are removed, as well as the ``auto_start_request`` option. + + The ``copy_database`` method is removed, see the + :doc:`copy_database examples ` for alternatives. + + The :meth:`MongoClient.disconnect` method is removed; it was a + synonym for :meth:`~pymongo.MongoClient.close`. + + :class:`~pymongo.mongo_client.MongoClient` no longer returns an + instance of :class:`~pymongo.database.Database` for attribute names + with leading underscores. You must use dict-style lookups instead:: + + client['__my_database__'] + + Not:: + + client.__my_database__ + + .. versionchanged:: 4.7 + Deprecated parameter ``wTimeoutMS``, use :meth:`~pymongo.timeout`. + """ + doc_class = document_class or dict + self.__init_kwargs: dict[str, Any] = { + "host": host, + "port": port, + "document_class": doc_class, + "tz_aware": tz_aware, + "connect": connect, + "type_registry": type_registry, + **kwargs, + } + + if host is None: + host = self.HOST + if isinstance(host, str): + host = [host] + if port is None: + port = self.PORT + if not isinstance(port, int): + raise TypeError("port must be an instance of int") + + # _pool_class, _monitor_class, and _condition_class are for deep + # customization of PyMongo, e.g. Motor. + pool_class = kwargs.pop("_pool_class", None) + monitor_class = kwargs.pop("_monitor_class", None) + condition_class = kwargs.pop("_condition_class", None) + + # Parse options passed as kwargs. + keyword_opts = common._CaseInsensitiveDictionary(kwargs) + keyword_opts["document_class"] = doc_class + + seeds = set() + username = None + password = None + dbase = None + opts = common._CaseInsensitiveDictionary() + fqdn = None + srv_service_name = keyword_opts.get("srvservicename") + srv_max_hosts = keyword_opts.get("srvmaxhosts") + if len([h for h in host if "/" in h]) > 1: + raise ConfigurationError("host must not contain multiple MongoDB URIs") + for entity in host: + # A hostname can only include a-z, 0-9, '-' and '.'. If we find a '/' + # it must be a URI, + # https://en.wikipedia.org/wiki/Hostname#Restrictions_on_valid_host_names + if "/" in entity: + # Determine connection timeout from kwargs. + timeout = keyword_opts.get("connecttimeoutms") + if timeout is not None: + timeout = common.validate_timeout_or_none_or_zero( + keyword_opts.cased_key("connecttimeoutms"), timeout + ) + res = uri_parser.parse_uri( + entity, + port, + validate=True, + warn=True, + normalize=False, + connect_timeout=timeout, + srv_service_name=srv_service_name, + srv_max_hosts=srv_max_hosts, + ) + seeds.update(res["nodelist"]) + username = res["username"] or username + password = res["password"] or password + dbase = res["database"] or dbase + opts = res["options"] + fqdn = res["fqdn"] + else: + seeds.update(uri_parser.split_hosts(entity, port)) + if not seeds: + raise ConfigurationError("need to specify at least one host") + + for hostname in [node[0] for node in seeds]: + if _detect_external_db(hostname): + break + + # Add options with named keyword arguments to the parsed kwarg options. + if type_registry is not None: + keyword_opts["type_registry"] = type_registry + if tz_aware is None: + tz_aware = opts.get("tz_aware", False) + if connect is None: + connect = opts.get("connect", True) + keyword_opts["tz_aware"] = tz_aware + keyword_opts["connect"] = connect + + # Handle deprecated options in kwarg options. + keyword_opts = _handle_option_deprecations(keyword_opts) + # Validate kwarg options. + keyword_opts = common._CaseInsensitiveDictionary( + dict(common.validate(keyword_opts.cased_key(k), v) for k, v in keyword_opts.items()) + ) + + # Override connection string options with kwarg options. + opts.update(keyword_opts) + + if srv_service_name is None: + srv_service_name = opts.get("srvServiceName", common.SRV_SERVICE_NAME) + + srv_max_hosts = srv_max_hosts or opts.get("srvmaxhosts") + # Handle security-option conflicts in combined options. + opts = _handle_security_options(opts) + # Normalize combined options. + opts = _normalize_options(opts) + _check_options(seeds, opts) + + # Username and password passed as kwargs override user info in URI. + username = opts.get("username", username) + password = opts.get("password", password) + self.__options = options = ClientOptions(username, password, dbase, opts) + + self.__default_database_name = dbase + self.__lock = _create_lock() + self.__kill_cursors_queue: list = [] + + self._event_listeners = options.pool_options._event_listeners + super().__init__( + options.codec_options, + options.read_preference, + options.write_concern, + options.read_concern, + ) + + self._topology_settings = TopologySettings( + seeds=seeds, + replica_set_name=options.replica_set_name, + pool_class=pool_class, + pool_options=options.pool_options, + monitor_class=monitor_class, + condition_class=condition_class, + local_threshold_ms=options.local_threshold_ms, + server_selection_timeout=options.server_selection_timeout, + server_selector=options.server_selector, + heartbeat_frequency=options.heartbeat_frequency, + fqdn=fqdn, + direct_connection=options.direct_connection, + load_balanced=options.load_balanced, + srv_service_name=srv_service_name, + srv_max_hosts=srv_max_hosts, + server_monitoring_mode=options.server_monitoring_mode, + ) + + self._init_background() + + if connect: + self._get_topology() + + self._encrypter = None + if self.__options.auto_encryption_opts: + from pymongo.encryption import _Encrypter + + self._encrypter = _Encrypter(self, self.__options.auto_encryption_opts) + self._timeout = self.__options.timeout + + if _HAS_REGISTER_AT_FORK: + # Add this client to the list of weakly referenced items. + # This will be used later if we fork. + MongoClient._clients[self._topology._topology_id] = self + + def _init_background(self, old_pid: Optional[int] = None) -> None: + self._topology = Topology(self._topology_settings) + # Seed the topology with the old one's pid so we can detect clients + # that are opened before a fork and used after. + self._topology._pid = old_pid + + def target() -> bool: + client = self_ref() + if client is None: + return False # Stop the executor. + MongoClient._process_periodic_tasks(client) + return True + + executor = periodic_executor.PeriodicExecutor( + interval=common.KILL_CURSOR_FREQUENCY, + min_interval=common.MIN_HEARTBEAT_INTERVAL, + target=target, + name="pymongo_kill_cursors_thread", + ) + + # We strongly reference the executor and it weakly references us via + # this closure. When the client is freed, stop the executor soon. + self_ref: Any = weakref.ref(self, executor.close) + self._kill_cursors_executor = executor + + def _after_fork(self) -> None: + """Resets topology in a child after successfully forking.""" + self._init_background(self._topology._pid) + + def _duplicate(self, **kwargs: Any) -> MongoClient: + args = self.__init_kwargs.copy() + args.update(kwargs) + return MongoClient(**args) + + def _server_property(self, attr_name: str) -> Any: + """An attribute of the current server's description. + + If the client is not connected, this will block until a connection is + established or raise ServerSelectionTimeoutError if no server is + available. + + Not threadsafe if used multiple times in a single method, since + the server may change. In such cases, store a local reference to a + ServerDescription first, then use its properties. + """ + server = self._get_topology().select_server(writable_server_selector, _Op.TEST) + + return getattr(server.description, attr_name) + + def watch( + self, + pipeline: Optional[_Pipeline] = None, + full_document: Optional[str] = None, + resume_after: Optional[Mapping[str, Any]] = None, + max_await_time_ms: Optional[int] = None, + batch_size: Optional[int] = None, + collation: Optional[_CollationIn] = None, + start_at_operation_time: Optional[Timestamp] = None, + session: Optional[client_session.ClientSession] = None, + start_after: Optional[Mapping[str, Any]] = None, + comment: Optional[Any] = None, + full_document_before_change: Optional[str] = None, + show_expanded_events: Optional[bool] = None, + ) -> ChangeStream[_DocumentType]: + """Watch changes on this cluster. + + Performs an aggregation with an implicit initial ``$changeStream`` + stage and returns a + :class:`~pymongo.change_stream.ClusterChangeStream` cursor which + iterates over changes on all databases on this cluster. + + Introduced in MongoDB 4.0. + + .. code-block:: python + + with client.watch() as stream: + for change in stream: + print(change) + + The :class:`~pymongo.change_stream.ClusterChangeStream` iterable + blocks until the next change document is returned or an error is + raised. If the + :meth:`~pymongo.change_stream.ClusterChangeStream.next` method + encounters a network error when retrieving a batch from the server, + it will automatically attempt to recreate the cursor such that no + change events are missed. Any error encountered during the resume + attempt indicates there may be an outage and will be raised. + + .. code-block:: python + + try: + with client.watch([{"$match": {"operationType": "insert"}}]) as stream: + for insert_change in stream: + print(insert_change) + except pymongo.errors.PyMongoError: + # The ChangeStream encountered an unrecoverable error or the + # resume attempt failed to recreate the cursor. + logging.error("...") + + For a precise description of the resume process see the + `change streams specification`_. + + :param pipeline: A list of aggregation pipeline stages to + append to an initial ``$changeStream`` stage. Not all + pipeline stages are valid after a ``$changeStream`` stage, see the + MongoDB documentation on change streams for the supported stages. + :param full_document: The fullDocument to pass as an option + to the ``$changeStream`` stage. Allowed values: 'updateLookup', + 'whenAvailable', 'required'. When set to 'updateLookup', the + change notification for partial updates will include both a delta + describing the changes to the document, as well as a copy of the + entire document that was changed from some time after the change + occurred. + :param full_document_before_change: Allowed values: 'whenAvailable' + and 'required'. Change events may now result in a + 'fullDocumentBeforeChange' response field. + :param resume_after: A resume token. If provided, the + change stream will start returning changes that occur directly + after the operation specified in the resume token. A resume token + is the _id value of a change document. + :param max_await_time_ms: The maximum time in milliseconds + for the server to wait for changes before responding to a getMore + operation. + :param batch_size: The maximum number of documents to return + per batch. + :param collation: The :class:`~pymongo.collation.Collation` + to use for the aggregation. + :param start_at_operation_time: If provided, the resulting + change stream will only return changes that occurred at or after + the specified :class:`~bson.timestamp.Timestamp`. Requires + MongoDB >= 4.0. + :param session: a + :class:`~pymongo.client_session.ClientSession`. + :param start_after: The same as `resume_after` except that + `start_after` can resume notifications after an invalidate event. + This option and `resume_after` are mutually exclusive. + :param comment: A user-provided comment to attach to this + command. + :param show_expanded_events: Include expanded events such as DDL events like `dropIndexes`. + + :return: A :class:`~pymongo.change_stream.ClusterChangeStream` cursor. + + .. versionchanged:: 4.3 + Added `show_expanded_events` parameter. + + .. versionchanged:: 4.2 + Added ``full_document_before_change`` parameter. + + .. versionchanged:: 4.1 + Added ``comment`` parameter. + + .. versionchanged:: 3.9 + Added the ``start_after`` parameter. + + .. versionadded:: 3.7 + + .. seealso:: The MongoDB documentation on `changeStreams `_. + + .. _change streams specification: + https://github.com/mongodb/specifications/blob/master/source/change-streams/change-streams.md + """ + return ClusterChangeStream( + self.admin, + pipeline, + full_document, + resume_after, + max_await_time_ms, + batch_size, + collation, + start_at_operation_time, + session, + start_after, + comment, + full_document_before_change, + show_expanded_events=show_expanded_events, + ) + + @property + def topology_description(self) -> TopologyDescription: + """The description of the connected MongoDB deployment. + + >>> client.topology_description + , , ]> + >>> client.topology_description.topology_type_name + 'ReplicaSetWithPrimary' + + Note that the description is periodically updated in the background + but the returned object itself is immutable. Access this property again + to get a more recent + :class:`~pymongo.topology_description.TopologyDescription`. + + :return: An instance of + :class:`~pymongo.topology_description.TopologyDescription`. + + .. versionadded:: 4.0 + """ + return self._topology.description + + @property + def address(self) -> Optional[tuple[str, int]]: + """(host, port) of the current standalone, primary, or mongos, or None. + + Accessing :attr:`address` raises :exc:`~.errors.InvalidOperation` if + the client is load-balancing among mongoses, since there is no single + address. Use :attr:`nodes` instead. + + If the client is not connected, this will block until a connection is + established or raise ServerSelectionTimeoutError if no server is + available. + + .. versionadded:: 3.0 + """ + topology_type = self._topology._description.topology_type + if ( + topology_type == TOPOLOGY_TYPE.Sharded + and len(self.topology_description.server_descriptions()) > 1 + ): + raise InvalidOperation( + 'Cannot use "address" property when load balancing among' + ' mongoses, use "nodes" instead.' + ) + if topology_type not in ( + TOPOLOGY_TYPE.ReplicaSetWithPrimary, + TOPOLOGY_TYPE.Single, + TOPOLOGY_TYPE.LoadBalanced, + TOPOLOGY_TYPE.Sharded, + ): + return None + return self._server_property("address") + + @property + def primary(self) -> Optional[tuple[str, int]]: + """The (host, port) of the current primary of the replica set. + + Returns ``None`` if this client is not connected to a replica set, + there is no primary, or this client was created without the + `replicaSet` option. + + .. versionadded:: 3.0 + MongoClient gained this property in version 3.0. + """ + return self._topology.get_primary() # type: ignore[return-value] + + @property + def secondaries(self) -> set[_Address]: + """The secondary members known to this client. + + A sequence of (host, port) pairs. Empty if this client is not + connected to a replica set, there are no visible secondaries, or this + client was created without the `replicaSet` option. + + .. versionadded:: 3.0 + MongoClient gained this property in version 3.0. + """ + return self._topology.get_secondaries() + + @property + def arbiters(self) -> set[_Address]: + """Arbiters in the replica set. + + A sequence of (host, port) pairs. Empty if this client is not + connected to a replica set, there are no arbiters, or this client was + created without the `replicaSet` option. + """ + return self._topology.get_arbiters() + + @property + def is_primary(self) -> bool: + """If this client is connected to a server that can accept writes. + + True if the current server is a standalone, mongos, or the primary of + a replica set. If the client is not connected, this will block until a + connection is established or raise ServerSelectionTimeoutError if no + server is available. + """ + return self._server_property("is_writable") + + @property + def is_mongos(self) -> bool: + """If this client is connected to mongos. If the client is not + connected, this will block until a connection is established or raise + ServerSelectionTimeoutError if no server is available. + """ + return self._server_property("server_type") == SERVER_TYPE.Mongos + + @property + def nodes(self) -> FrozenSet[_Address]: + """Set of all currently connected servers. + + .. warning:: When connected to a replica set the value of :attr:`nodes` + can change over time as :class:`MongoClient`'s view of the replica + set changes. :attr:`nodes` can also be an empty set when + :class:`MongoClient` is first instantiated and hasn't yet connected + to any servers, or a network partition causes it to lose connection + to all servers. + """ + description = self._topology.description + return frozenset(s.address for s in description.known_servers) + + @property + def options(self) -> ClientOptions: + """The configuration options for this client. + + :return: An instance of :class:`~pymongo.client_options.ClientOptions`. + + .. versionadded:: 4.0 + """ + return self.__options + + def _end_sessions(self, session_ids: list[_ServerSession]) -> None: + """Send endSessions command(s) with the given session ids.""" + try: + # Use Connection.command directly to avoid implicitly creating + # another session. + with self._conn_for_reads( + ReadPreference.PRIMARY_PREFERRED, None, operation=_Op.END_SESSIONS + ) as ( + conn, + read_pref, + ): + if not conn.supports_sessions: + return + + for i in range(0, len(session_ids), common._MAX_END_SESSIONS): + spec = {"endSessions": session_ids[i : i + common._MAX_END_SESSIONS]} + conn.command("admin", spec, read_preference=read_pref, client=self) + except PyMongoError: + # Drivers MUST ignore any errors returned by the endSessions + # command. + pass + + def close(self) -> None: + """Cleanup client resources and disconnect from MongoDB. + + End all server sessions created by this client by sending one or more + endSessions commands. + + Close all sockets in the connection pools and stop the monitor threads. + + .. versionchanged:: 4.0 + Once closed, the client cannot be used again and any attempt will + raise :exc:`~pymongo.errors.InvalidOperation`. + + .. versionchanged:: 3.6 + End all server sessions created by this client. + """ + session_ids = self._topology.pop_all_sessions() + if session_ids: + self._end_sessions(session_ids) + # Stop the periodic task thread and then send pending killCursor + # requests before closing the topology. + self._kill_cursors_executor.close() + self._process_kill_cursors() + self._topology.close() + if self._encrypter: + # TODO: PYTHON-1921 Encrypted MongoClients cannot be re-opened. + self._encrypter.close() + + def _get_topology(self) -> Topology: + """Get the internal :class:`~pymongo.topology.Topology` object. + + If this client was created with "connect=False", calling _get_topology + launches the connection process in the background. + """ + self._topology.open() + with self.__lock: + self._kill_cursors_executor.open() + return self._topology + + @contextlib.contextmanager + def _checkout(self, server: Server, session: Optional[ClientSession]) -> Iterator[Connection]: + in_txn = session and session.in_transaction + with _MongoClientErrorHandler(self, server, session) as err_handler: + # Reuse the pinned connection, if it exists. + if in_txn and session and session._pinned_connection: + err_handler.contribute_socket(session._pinned_connection) + yield session._pinned_connection + return + with server.checkout(handler=err_handler) as conn: + # Pin this session to the selected server or connection. + if ( + in_txn + and session + and server.description.server_type + in ( + SERVER_TYPE.Mongos, + SERVER_TYPE.LoadBalancer, + ) + ): + session._pin(server, conn) + err_handler.contribute_socket(conn) + if ( + self._encrypter + and not self._encrypter._bypass_auto_encryption + and conn.max_wire_version < 8 + ): + raise ConfigurationError( + "Auto-encryption requires a minimum MongoDB version of 4.2" + ) + yield conn + + def _select_server( + self, + server_selector: Callable[[Selection], Selection], + session: Optional[ClientSession], + operation: str, + address: Optional[_Address] = None, + deprioritized_servers: Optional[list[Server]] = None, + operation_id: Optional[int] = None, + ) -> Server: + """Select a server to run an operation on this client. + + :param server_selector: The server selector to use if the session is + not pinned and no address is given. + :param session: The ClientSession for the next operation, or None. May + be pinned to a mongos server address. + :param operation: The name of the operation that the server is being selected for. + :param address: Address when sending a message + to a specific server, used for getMore. + """ + try: + topology = self._get_topology() + if session and not session.in_transaction: + session._transaction.reset() + if not address and session: + address = session._pinned_address + if address: + # We're running a getMore or this session is pinned to a mongos. + server = topology.select_server_by_address( + address, operation, operation_id=operation_id + ) + if not server: + raise AutoReconnect("server %s:%s no longer available" % address) # noqa: UP031 + else: + server = topology.select_server( + server_selector, + operation, + deprioritized_servers=deprioritized_servers, + operation_id=operation_id, + ) + return server + except PyMongoError as exc: + # Server selection errors in a transaction are transient. + if session and session.in_transaction: + exc._add_error_label("TransientTransactionError") + session._unpin() + raise + + def _conn_for_writes( + self, session: Optional[ClientSession], operation: str + ) -> ContextManager[Connection]: + server = self._select_server(writable_server_selector, session, operation) + return self._checkout(server, session) + + @contextlib.contextmanager + def _conn_from_server( + self, read_preference: _ServerMode, server: Server, session: Optional[ClientSession] + ) -> Iterator[tuple[Connection, _ServerMode]]: + assert read_preference is not None, "read_preference must not be None" + # Get a connection for a server matching the read preference, and yield + # conn with the effective read preference. The Server Selection + # Spec says not to send any $readPreference to standalones and to + # always send primaryPreferred when directly connected to a repl set + # member. + # Thread safe: if the type is single it cannot change. + topology = self._get_topology() + single = topology.description.topology_type == TOPOLOGY_TYPE.Single + + with self._checkout(server, session) as conn: + if single: + if conn.is_repl and not (session and session.in_transaction): + # Use primary preferred to ensure any repl set member + # can handle the request. + read_preference = ReadPreference.PRIMARY_PREFERRED + elif conn.is_standalone: + # Don't send read preference to standalones. + read_preference = ReadPreference.PRIMARY + yield conn, read_preference + + def _conn_for_reads( + self, + read_preference: _ServerMode, + session: Optional[ClientSession], + operation: str, + ) -> ContextManager[tuple[Connection, _ServerMode]]: + assert read_preference is not None, "read_preference must not be None" + _ = self._get_topology() + server = self._select_server(read_preference, session, operation) + return self._conn_from_server(read_preference, server, session) + + def _should_pin_cursor(self, session: Optional[ClientSession]) -> Optional[bool]: + return self.__options.load_balanced and not (session and session.in_transaction) + + @_csot.apply + def _run_operation( + self, + operation: Union[_Query, _GetMore], + unpack_res: Callable, + address: Optional[_Address] = None, + ) -> Response: + """Run a _Query/_GetMore operation and return a Response. + + :param operation: a _Query or _GetMore object. + :param unpack_res: A callable that decodes the wire protocol response. + :param address: Optional address when sending a message + to a specific server, used for getMore. + """ + if operation.conn_mgr: + server = self._select_server( + operation.read_preference, + operation.session, + operation.name, + address=address, + ) + + with operation.conn_mgr.lock: + with _MongoClientErrorHandler(self, server, operation.session) as err_handler: + err_handler.contribute_socket(operation.conn_mgr.conn) + return server.run_operation( + operation.conn_mgr.conn, + operation, + operation.read_preference, + self._event_listeners, + unpack_res, + self, + ) + + def _cmd( + _session: Optional[ClientSession], + server: Server, + conn: Connection, + read_preference: _ServerMode, + ) -> Response: + operation.reset() # Reset op in case of retry. + return server.run_operation( + conn, + operation, + read_preference, + self._event_listeners, + unpack_res, + self, + ) + + return self._retryable_read( + _cmd, + operation.read_preference, + operation.session, + address=address, + retryable=isinstance(operation, message._Query), + operation=operation.name, + ) + + def _retry_with_session( + self, + retryable: bool, + func: _WriteCall[T], + session: Optional[ClientSession], + bulk: Optional[_Bulk], + operation: str, + operation_id: Optional[int] = None, + ) -> T: + """Execute an operation with at most one consecutive retries + + Returns func()'s return value on success. On error retries the same + command. + + Re-raises any exception thrown by func(). + """ + # Ensure that the options supports retry_writes and there is a valid session not in + # transaction, otherwise, we will not support retry behavior for this txn. + retryable = bool( + retryable and self.options.retry_writes and session and not session.in_transaction + ) + return self._retry_internal( + func=func, + session=session, + bulk=bulk, + operation=operation, + retryable=retryable, + operation_id=operation_id, + ) + + @_csot.apply + def _retry_internal( + self, + func: _WriteCall[T] | _ReadCall[T], + session: Optional[ClientSession], + bulk: Optional[_Bulk], + operation: str, + is_read: bool = False, + address: Optional[_Address] = None, + read_pref: Optional[_ServerMode] = None, + retryable: bool = False, + operation_id: Optional[int] = None, + ) -> T: + """Internal retryable helper for all client transactions. + + :param func: Callback function we want to retry + :param session: Client Session on which the transaction should occur + :param bulk: Abstraction to handle bulk write operations + :param operation: The name of the operation that the server is being selected for + :param is_read: If this is an exclusive read transaction, defaults to False + :param address: Server Address, defaults to None + :param read_pref: Topology of read operation, defaults to None + :param retryable: If the operation should be retried once, defaults to None + + :return: Output of the calling func() + """ + return _ClientConnectionRetryable( + mongo_client=self, + func=func, + bulk=bulk, + operation=operation, + is_read=is_read, + session=session, + read_pref=read_pref, + address=address, + retryable=retryable, + operation_id=operation_id, + ).run() + + def _retryable_read( + self, + func: _ReadCall[T], + read_pref: _ServerMode, + session: Optional[ClientSession], + operation: str, + address: Optional[_Address] = None, + retryable: bool = True, + operation_id: Optional[int] = None, + ) -> T: + """Execute an operation with consecutive retries if possible + + Returns func()'s return value on success. On error retries the same + command. + + Re-raises any exception thrown by func(). + + :param func: Read call we want to execute + :param read_pref: Desired topology of read operation + :param session: Client session we should use to execute operation + :param operation: The name of the operation that the server is being selected for + :param address: Optional address when sending a message, defaults to None + :param retryable: if we should attempt retries + (may not always be supported even if supplied), defaults to False + """ + + # Ensure that the client supports retrying on reads and there is no session in + # transaction, otherwise, we will not support retry behavior for this call. + retryable = bool( + retryable and self.options.retry_reads and not (session and session.in_transaction) + ) + return self._retry_internal( + func, + session, + None, + operation, + is_read=True, + address=address, + read_pref=read_pref, + retryable=retryable, + operation_id=operation_id, + ) + + def _retryable_write( + self, + retryable: bool, + func: _WriteCall[T], + session: Optional[ClientSession], + operation: str, + bulk: Optional[_Bulk] = None, + operation_id: Optional[int] = None, + ) -> T: + """Execute an operation with consecutive retries if possible + + Returns func()'s return value on success. On error retries the same + command. + + Re-raises any exception thrown by func(). + + :param retryable: if we should attempt retries (may not always be supported) + :param func: write call we want to execute during a session + :param session: Client session we will use to execute write operation + :param operation: The name of the operation that the server is being selected for + :param bulk: bulk abstraction to execute operations in bulk, defaults to None + """ + with self._tmp_session(session) as s: + return self._retry_with_session(retryable, func, s, bulk, operation, operation_id) + + def __eq__(self, other: Any) -> bool: + if isinstance(other, self.__class__): + return self._topology == other._topology + return NotImplemented + + def __ne__(self, other: Any) -> bool: + return not self == other + + def __hash__(self) -> int: + return hash(self._topology) + + def _repr_helper(self) -> str: + def option_repr(option: str, value: Any) -> str: + """Fix options whose __repr__ isn't usable in a constructor.""" + if option == "document_class": + if value is dict: + return "document_class=dict" + else: + return f"document_class={value.__module__}.{value.__name__}" + if option in common.TIMEOUT_OPTIONS and value is not None: + return f"{option}={int(value * 1000)}" + + return f"{option}={value!r}" + + # Host first... + options = [ + "host=%r" + % [ + "%s:%d" % (host, port) if port is not None else host + for host, port in self._topology_settings.seeds + ] + ] + # ... then everything in self._constructor_args... + options.extend( + option_repr(key, self.__options._options[key]) for key in self._constructor_args + ) + # ... then everything else. + options.extend( + option_repr(key, self.__options._options[key]) + for key in self.__options._options + if key not in set(self._constructor_args) and key != "username" and key != "password" + ) + return ", ".join(options) + + def __repr__(self) -> str: + return f"MongoClient({self._repr_helper()})" + + def __getattr__(self, name: str) -> database.Database[_DocumentType]: + """Get a database by name. + + Raises :class:`~pymongo.errors.InvalidName` if an invalid + database name is used. + + :param name: the name of the database to get + """ + if name.startswith("_"): + raise AttributeError( + f"MongoClient has no attribute {name!r}. To access the {name}" + f" database, use client[{name!r}]." + ) + return self.__getitem__(name) + + def __getitem__(self, name: str) -> database.Database[_DocumentType]: + """Get a database by name. + + Raises :class:`~pymongo.errors.InvalidName` if an invalid + database name is used. + + :param name: the name of the database to get + """ + return database.Database(self, name) + + def _cleanup_cursor( + self, + locks_allowed: bool, + cursor_id: int, + address: Optional[_CursorAddress], + conn_mgr: _ConnectionManager, + session: Optional[ClientSession], + explicit_session: bool, + ) -> None: + """Cleanup a cursor from cursor.close() or __del__. + + This method handles cleanup for Cursors/CommandCursors including any + pinned connection or implicit session attached at the time the cursor + was closed or garbage collected. + + :param locks_allowed: True if we are allowed to acquire locks. + :param cursor_id: The cursor id which may be 0. + :param address: The _CursorAddress. + :param conn_mgr: The _ConnectionManager for the pinned connection or None. + :param session: The cursor's session. + :param explicit_session: True if the session was passed explicitly. + """ + if locks_allowed: + if cursor_id: + if conn_mgr and conn_mgr.more_to_come: + # If this is an exhaust cursor and we haven't completely + # exhausted the result set we *must* close the socket + # to stop the server from sending more data. + assert conn_mgr.conn is not None + conn_mgr.conn.close_conn(ConnectionClosedReason.ERROR) + else: + self._close_cursor_now(cursor_id, address, session=session, conn_mgr=conn_mgr) + if conn_mgr: + conn_mgr.close() + else: + # The cursor will be closed later in a different session. + if cursor_id or conn_mgr: + self._close_cursor_soon(cursor_id, address, conn_mgr) + if session and not explicit_session: + session._end_session(lock=locks_allowed) + + def _close_cursor_soon( + self, + cursor_id: int, + address: Optional[_CursorAddress], + conn_mgr: Optional[_ConnectionManager] = None, + ) -> None: + """Request that a cursor and/or connection be cleaned up soon.""" + self.__kill_cursors_queue.append((address, cursor_id, conn_mgr)) + + def _close_cursor_now( + self, + cursor_id: int, + address: Optional[_CursorAddress], + session: Optional[ClientSession] = None, + conn_mgr: Optional[_ConnectionManager] = None, + ) -> None: + """Send a kill cursors message with the given id. + + The cursor is closed synchronously on the current thread. + """ + if not isinstance(cursor_id, int): + raise TypeError("cursor_id must be an instance of int") + + try: + if conn_mgr: + with conn_mgr.lock: + # Cursor is pinned to LB outside of a transaction. + assert address is not None + assert conn_mgr.conn is not None + self._kill_cursor_impl([cursor_id], address, session, conn_mgr.conn) + else: + self._kill_cursors([cursor_id], address, self._get_topology(), session) + except PyMongoError: + # Make another attempt to kill the cursor later. + self._close_cursor_soon(cursor_id, address) + + def _kill_cursors( + self, + cursor_ids: Sequence[int], + address: Optional[_CursorAddress], + topology: Topology, + session: Optional[ClientSession], + ) -> None: + """Send a kill cursors message with the given ids.""" + if address: + # address could be a tuple or _CursorAddress, but + # select_server_by_address needs (host, port). + server = topology.select_server_by_address(tuple(address), _Op.KILL_CURSORS) # type: ignore[arg-type] + else: + # Application called close_cursor() with no address. + server = topology.select_server(writable_server_selector, _Op.KILL_CURSORS) + + with self._checkout(server, session) as conn: + assert address is not None + self._kill_cursor_impl(cursor_ids, address, session, conn) + + def _kill_cursor_impl( + self, + cursor_ids: Sequence[int], + address: _CursorAddress, + session: Optional[ClientSession], + conn: Connection, + ) -> None: + namespace = address.namespace + db, coll = namespace.split(".", 1) + spec = {"killCursors": coll, "cursors": cursor_ids} + conn.command(db, spec, session=session, client=self) + + def _process_kill_cursors(self) -> None: + """Process any pending kill cursors requests.""" + address_to_cursor_ids = defaultdict(list) + pinned_cursors = [] + + # Other threads or the GC may append to the queue concurrently. + while True: + try: + address, cursor_id, conn_mgr = self.__kill_cursors_queue.pop() + except IndexError: + break + + if conn_mgr: + pinned_cursors.append((address, cursor_id, conn_mgr)) + else: + address_to_cursor_ids[address].append(cursor_id) + + for address, cursor_id, conn_mgr in pinned_cursors: + try: + self._cleanup_cursor(True, cursor_id, address, conn_mgr, None, False) + except Exception as exc: + if isinstance(exc, InvalidOperation) and self._topology._closed: + # Raise the exception when client is closed so that it + # can be caught in _process_periodic_tasks + raise + else: + helpers._handle_exception() + + # Don't re-open topology if it's closed and there's no pending cursors. + if address_to_cursor_ids: + topology = self._get_topology() + for address, cursor_ids in address_to_cursor_ids.items(): + try: + self._kill_cursors(cursor_ids, address, topology, session=None) + except Exception as exc: + if isinstance(exc, InvalidOperation) and self._topology._closed: + raise + else: + helpers._handle_exception() + + # This method is run periodically by a background thread. + def _process_periodic_tasks(self) -> None: + """Process any pending kill cursors requests and + maintain connection pool parameters. + """ + try: + self._process_kill_cursors() + self._topology.update_pool() + except Exception as exc: + if isinstance(exc, InvalidOperation) and self._topology._closed: + return + else: + helpers._handle_exception() + + def __start_session(self, implicit: bool, **kwargs: Any) -> ClientSession: + server_session = _EmptyServerSession() + opts = client_session.SessionOptions(**kwargs) + return client_session.ClientSession(self, server_session, opts, implicit) + + def start_session( + self, + causal_consistency: Optional[bool] = None, + default_transaction_options: Optional[client_session.TransactionOptions] = None, + snapshot: Optional[bool] = False, + ) -> client_session.ClientSession: + """Start a logical session. + + This method takes the same parameters as + :class:`~pymongo.client_session.SessionOptions`. See the + :mod:`~pymongo.client_session` module for details and examples. + + A :class:`~pymongo.client_session.ClientSession` may only be used with + the MongoClient that started it. :class:`ClientSession` instances are + **not thread-safe or fork-safe**. They can only be used by one thread + or process at a time. A single :class:`ClientSession` cannot be used + to run multiple operations concurrently. + + :return: An instance of :class:`~pymongo.client_session.ClientSession`. + + .. versionadded:: 3.6 + """ + return self.__start_session( + False, + causal_consistency=causal_consistency, + default_transaction_options=default_transaction_options, + snapshot=snapshot, + ) + + def _return_server_session( + self, server_session: Union[_ServerSession, _EmptyServerSession], lock: bool + ) -> None: + """Internal: return a _ServerSession to the pool.""" + if isinstance(server_session, _EmptyServerSession): + return None + return self._topology.return_server_session(server_session, lock) + + def _ensure_session(self, session: Optional[ClientSession] = None) -> Optional[ClientSession]: + """If provided session is None, lend a temporary session.""" + if session: + return session + + try: + # Don't make implicit sessions causally consistent. Applications + # should always opt-in. + return self.__start_session(True, causal_consistency=False) + except (ConfigurationError, InvalidOperation): + # Sessions not supported. + return None + + @contextlib.contextmanager + def _tmp_session( + self, session: Optional[client_session.ClientSession], close: bool = True + ) -> Generator[Optional[client_session.ClientSession], None, None]: + """If provided session is None, lend a temporary session.""" + if session is not None: + if not isinstance(session, client_session.ClientSession): + raise ValueError("'session' argument must be a ClientSession or None.") + # Don't call end_session. + yield session + return + + s = self._ensure_session(session) + if s: + try: + yield s + except Exception as exc: + if isinstance(exc, ConnectionFailure): + s._server_session.mark_dirty() + + # Always call end_session on error. + s.end_session() + raise + finally: + # Call end_session when we exit this scope. + if close: + s.end_session() + else: + yield None + + def _send_cluster_time( + self, command: MutableMapping[str, Any], session: Optional[ClientSession] + ) -> None: + topology_time = self._topology.max_cluster_time() + session_time = session.cluster_time if session else None + if topology_time and session_time: + if topology_time["clusterTime"] > session_time["clusterTime"]: + cluster_time: Optional[ClusterTime] = topology_time + else: + cluster_time = session_time + else: + cluster_time = topology_time or session_time + if cluster_time: + command["$clusterTime"] = cluster_time + + def _process_response(self, reply: Mapping[str, Any], session: Optional[ClientSession]) -> None: + self._topology.receive_cluster_time(reply.get("$clusterTime")) + if session is not None: + session._process_response(reply) + + def server_info(self, session: Optional[client_session.ClientSession] = None) -> dict[str, Any]: + """Get information about the MongoDB server we're connected to. + + :param session: a + :class:`~pymongo.client_session.ClientSession`. + + .. versionchanged:: 3.6 + Added ``session`` parameter. + """ + return cast( + dict, + self.admin.command( + "buildinfo", read_preference=ReadPreference.PRIMARY, session=session + ), + ) + + def list_databases( + self, + session: Optional[client_session.ClientSession] = None, + comment: Optional[Any] = None, + **kwargs: Any, + ) -> CommandCursor[dict[str, Any]]: + """Get a cursor over the databases of the connected server. + + :param session: a + :class:`~pymongo.client_session.ClientSession`. + :param comment: A user-provided comment to attach to this + command. + :param kwargs: Optional parameters of the + `listDatabases command + `_ + can be passed as keyword arguments to this method. The supported + options differ by server version. + + + :return: An instance of :class:`~pymongo.command_cursor.CommandCursor`. + + .. versionadded:: 3.6 + """ + cmd = {"listDatabases": 1} + cmd.update(kwargs) + if comment is not None: + cmd["comment"] = comment + admin = self._database_default_options("admin") + res = admin._retryable_read_command(cmd, session=session, operation=_Op.LIST_DATABASES) + # listDatabases doesn't return a cursor (yet). Fake one. + cursor = { + "id": 0, + "firstBatch": res["databases"], + "ns": "admin.$cmd", + } + return CommandCursor(admin["$cmd"], cursor, None, comment=comment) + + def list_database_names( + self, + session: Optional[client_session.ClientSession] = None, + comment: Optional[Any] = None, + ) -> list[str]: + """Get a list of the names of all databases on the connected server. + + :param session: a + :class:`~pymongo.client_session.ClientSession`. + :param comment: A user-provided comment to attach to this + command. + + .. versionchanged:: 4.1 + Added ``comment`` parameter. + + .. versionadded:: 3.6 + """ + return [doc["name"] for doc in self.list_databases(session, nameOnly=True, comment=comment)] + + @_csot.apply + def drop_database( + self, + name_or_database: Union[str, database.Database[_DocumentTypeArg]], + session: Optional[client_session.ClientSession] = None, + comment: Optional[Any] = None, + ) -> None: + """Drop a database. + + Raises :class:`TypeError` if `name_or_database` is not an instance of + :class:`str` or :class:`~pymongo.database.Database`. + + :param name_or_database: the name of a database to drop, or a + :class:`~pymongo.database.Database` instance representing the + database to drop + :param session: a + :class:`~pymongo.client_session.ClientSession`. + :param comment: A user-provided comment to attach to this + command. + + .. versionchanged:: 4.1 + Added ``comment`` parameter. + + .. versionchanged:: 3.6 + Added ``session`` parameter. + + .. note:: The :attr:`~pymongo.mongo_client.MongoClient.write_concern` of + this client is automatically applied to this operation. + + .. versionchanged:: 3.4 + Apply this client's write concern automatically to this operation + when connected to MongoDB >= 3.4. + + """ + name = name_or_database + if isinstance(name, database.Database): + name = name.name + + if not isinstance(name, str): + raise TypeError("name_or_database must be an instance of str or a Database") + + with self._conn_for_writes(session, operation=_Op.DROP_DATABASE) as conn: + self[name]._command( + conn, + {"dropDatabase": 1, "comment": comment}, + read_preference=ReadPreference.PRIMARY, + write_concern=self._write_concern_for(session), + parse_write_concern_error=True, + session=session, + ) + + def get_default_database( + self, + default: Optional[str] = None, + codec_options: Optional[CodecOptions[_DocumentTypeArg]] = None, + read_preference: Optional[_ServerMode] = None, + write_concern: Optional[WriteConcern] = None, + read_concern: Optional[ReadConcern] = None, + ) -> database.Database[_DocumentType]: + """Get the database named in the MongoDB connection URI. + + >>> uri = 'mongodb://host/my_database' + >>> client = MongoClient(uri) + >>> db = client.get_default_database() + >>> assert db.name == 'my_database' + >>> db = client.get_database() + >>> assert db.name == 'my_database' + + Useful in scripts where you want to choose which database to use + based only on the URI in a configuration file. + + :param default: the database name to use if no database name + was provided in the URI. + :param codec_options: An instance of + :class:`~bson.codec_options.CodecOptions`. If ``None`` (the + default) the :attr:`codec_options` of this :class:`MongoClient` is + used. + :param read_preference: The read preference to use. If + ``None`` (the default) the :attr:`read_preference` of this + :class:`MongoClient` is used. See :mod:`~pymongo.read_preferences` + for options. + :param write_concern: An instance of + :class:`~pymongo.write_concern.WriteConcern`. If ``None`` (the + default) the :attr:`write_concern` of this :class:`MongoClient` is + used. + :param read_concern: An instance of + :class:`~pymongo.read_concern.ReadConcern`. If ``None`` (the + default) the :attr:`read_concern` of this :class:`MongoClient` is + used. + :param comment: A user-provided comment to attach to this + command. + + .. versionchanged:: 4.1 + Added ``comment`` parameter. + + .. versionchanged:: 3.8 + Undeprecated. Added the ``default``, ``codec_options``, + ``read_preference``, ``write_concern`` and ``read_concern`` + parameters. + + .. versionchanged:: 3.5 + Deprecated, use :meth:`get_database` instead. + """ + if self.__default_database_name is None and default is None: + raise ConfigurationError("No default database name defined or provided.") + + name = cast(str, self.__default_database_name or default) + return database.Database( + self, name, codec_options, read_preference, write_concern, read_concern + ) + + def get_database( + self, + name: Optional[str] = None, + codec_options: Optional[CodecOptions[_DocumentTypeArg]] = None, + read_preference: Optional[_ServerMode] = None, + write_concern: Optional[WriteConcern] = None, + read_concern: Optional[ReadConcern] = None, + ) -> database.Database[_DocumentType]: + """Get a :class:`~pymongo.database.Database` with the given name and + options. + + Useful for creating a :class:`~pymongo.database.Database` with + different codec options, read preference, and/or write concern from + this :class:`MongoClient`. + + >>> client.read_preference + Primary() + >>> db1 = client.test + >>> db1.read_preference + Primary() + >>> from pymongo import ReadPreference + >>> db2 = client.get_database( + ... 'test', read_preference=ReadPreference.SECONDARY) + >>> db2.read_preference + Secondary(tag_sets=None) + + :param name: The name of the database - a string. If ``None`` + (the default) the database named in the MongoDB connection URI is + returned. + :param codec_options: An instance of + :class:`~bson.codec_options.CodecOptions`. If ``None`` (the + default) the :attr:`codec_options` of this :class:`MongoClient` is + used. + :param read_preference: The read preference to use. If + ``None`` (the default) the :attr:`read_preference` of this + :class:`MongoClient` is used. See :mod:`~pymongo.read_preferences` + for options. + :param write_concern: An instance of + :class:`~pymongo.write_concern.WriteConcern`. If ``None`` (the + default) the :attr:`write_concern` of this :class:`MongoClient` is + used. + :param read_concern: An instance of + :class:`~pymongo.read_concern.ReadConcern`. If ``None`` (the + default) the :attr:`read_concern` of this :class:`MongoClient` is + used. + + .. versionchanged:: 3.5 + The `name` parameter is now optional, defaulting to the database + named in the MongoDB connection URI. + """ + if name is None: + if self.__default_database_name is None: + raise ConfigurationError("No default database defined") + name = self.__default_database_name + + return database.Database( + self, name, codec_options, read_preference, write_concern, read_concern + ) + + def _database_default_options(self, name: str) -> Database: + """Get a Database instance with the default settings.""" + return self.get_database( + name, + codec_options=DEFAULT_CODEC_OPTIONS, + read_preference=ReadPreference.PRIMARY, + write_concern=DEFAULT_WRITE_CONCERN, + ) + + def __enter__(self) -> MongoClient[_DocumentType]: + return self + + def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None: + self.close() + + # See PYTHON-3084. + __iter__ = None + + def __next__(self) -> NoReturn: + raise TypeError("'MongoClient' object is not iterable") + + next = __next__ + + +def _retryable_error_doc(exc: PyMongoError) -> Optional[Mapping[str, Any]]: + """Return the server response from PyMongo exception or None.""" + if isinstance(exc, BulkWriteError): + # Check the last writeConcernError to determine if this + # BulkWriteError is retryable. + wces = exc.details["writeConcernErrors"] + return wces[-1] if wces else None + if isinstance(exc, (NotPrimaryError, OperationFailure)): + return cast(Mapping[str, Any], exc.details) + return None + + +def _add_retryable_write_error(exc: PyMongoError, max_wire_version: int, is_mongos: bool) -> None: + doc = _retryable_error_doc(exc) + if doc: + code = doc.get("code", 0) + # retryWrites on MMAPv1 should raise an actionable error. + if code == 20 and str(exc).startswith("Transaction numbers"): + errmsg = ( + "This MongoDB deployment does not support " + "retryable writes. Please add retryWrites=false " + "to your connection string." + ) + raise OperationFailure(errmsg, code, exc.details) # type: ignore[attr-defined] + if max_wire_version >= 9: + # In MongoDB 4.4+, the server reports the error labels. + for label in doc.get("errorLabels", []): + exc._add_error_label(label) + else: + # Do not consult writeConcernError for pre-4.4 mongos. + if isinstance(exc, WriteConcernError) and is_mongos: + pass + elif code in helpers._RETRYABLE_ERROR_CODES: + exc._add_error_label("RetryableWriteError") + + # Connection errors are always retryable except NotPrimaryError and WaitQueueTimeoutError which is + # handled above. + if isinstance(exc, ConnectionFailure) and not isinstance( + exc, (NotPrimaryError, WaitQueueTimeoutError) + ): + exc._add_error_label("RetryableWriteError") + + +class _MongoClientErrorHandler: + """Handle errors raised when executing an operation.""" + + __slots__ = ( + "client", + "server_address", + "session", + "max_wire_version", + "sock_generation", + "completed_handshake", + "service_id", + "handled", + ) + + def __init__(self, client: MongoClient, server: Server, session: Optional[ClientSession]): + self.client = client + self.server_address = server.description.address + self.session = session + self.max_wire_version = common.MIN_WIRE_VERSION + # XXX: When get_socket fails, this generation could be out of date: + # "Note that when a network error occurs before the handshake + # completes then the error's generation number is the generation + # of the pool at the time the connection attempt was started." + self.sock_generation = server.pool.gen.get_overall() + self.completed_handshake = False + self.service_id: Optional[ObjectId] = None + self.handled = False + + def contribute_socket(self, conn: Connection, completed_handshake: bool = True) -> None: + """Provide socket information to the error handler.""" + self.max_wire_version = conn.max_wire_version + self.sock_generation = conn.generation + self.service_id = conn.service_id + self.completed_handshake = completed_handshake + + def handle( + self, exc_type: Optional[Type[BaseException]], exc_val: Optional[BaseException] + ) -> None: + if self.handled or exc_val is None: + return + self.handled = True + if self.session: + if isinstance(exc_val, ConnectionFailure): + if self.session.in_transaction: + exc_val._add_error_label("TransientTransactionError") + self.session._server_session.mark_dirty() + + if isinstance(exc_val, PyMongoError): + if exc_val.has_error_label("TransientTransactionError") or exc_val.has_error_label( + "RetryableWriteError" + ): + self.session._unpin() + err_ctx = _ErrorContext( + exc_val, + self.max_wire_version, + self.sock_generation, + self.completed_handshake, + self.service_id, + ) + self.client._topology.handle_error(self.server_address, err_ctx) + + def __enter__(self) -> _MongoClientErrorHandler: + return self + + def __exit__( + self, + exc_type: Optional[Type[Exception]], + exc_val: Optional[Exception], + exc_tb: Optional[TracebackType], + ) -> None: + return self.handle(exc_type, exc_val) + + +class _ClientConnectionRetryable(Generic[T]): + """Responsible for executing retryable connections on read or write operations""" + + def __init__( + self, + mongo_client: MongoClient, + func: _WriteCall[T] | _ReadCall[T], + bulk: Optional[_Bulk], + operation: str, + is_read: bool = False, + session: Optional[ClientSession] = None, + read_pref: Optional[_ServerMode] = None, + address: Optional[_Address] = None, + retryable: bool = False, + operation_id: Optional[int] = None, + ): + self._last_error: Optional[Exception] = None + self._retrying = False + self._multiple_retries = _csot.get_timeout() is not None + self._client = mongo_client + + self._func = func + self._bulk = bulk + self._session = session + self._is_read = is_read + self._retryable = retryable + self._read_pref = read_pref + self._server_selector: Callable[[Selection], Selection] = ( + read_pref if is_read else writable_server_selector # type: ignore + ) + self._address = address + self._server: Server = None # type: ignore + self._deprioritized_servers: list[Server] = [] + self._operation = operation + self._operation_id = operation_id + + def run(self) -> T: + """Runs the supplied func() and attempts a retry + + :raises: self._last_error: Last exception raised + + :return: Result of the func() call + """ + # Increment the transaction id up front to ensure any retry attempt + # will use the proper txnNumber, even if server or socket selection + # fails before the command can be sent. + if self._is_session_state_retryable() and self._retryable and not self._is_read: + self._session._start_retryable_write() # type: ignore + if self._bulk: + self._bulk.started_retryable_write = True + + while True: + self._check_last_error(check_csot=True) + try: + return self._read() if self._is_read else self._write() + except ServerSelectionTimeoutError: + # The application may think the write was never attempted + # if we raise ServerSelectionTimeoutError on the retry + # attempt. Raise the original exception instead. + self._check_last_error() + # A ServerSelectionTimeoutError error indicates that there may + # be a persistent outage. Attempting to retry in this case will + # most likely be a waste of time. + raise + except PyMongoError as exc: + # Execute specialized catch on read + if self._is_read: + if isinstance(exc, (ConnectionFailure, OperationFailure)): + # ConnectionFailures do not supply a code property + exc_code = getattr(exc, "code", None) + if self._is_not_eligible_for_retry() or ( + isinstance(exc, OperationFailure) + and exc_code not in helpers._RETRYABLE_ERROR_CODES + ): + raise + self._retrying = True + self._last_error = exc + else: + raise + + # Specialized catch on write operation + if not self._is_read: + if not self._retryable: + raise + retryable_write_error_exc = exc.has_error_label("RetryableWriteError") + if retryable_write_error_exc: + assert self._session + self._session._unpin() + if not retryable_write_error_exc or self._is_not_eligible_for_retry(): + if exc.has_error_label("NoWritesPerformed") and self._last_error: + raise self._last_error from exc + else: + raise + if self._bulk: + self._bulk.retrying = True + else: + self._retrying = True + if not exc.has_error_label("NoWritesPerformed"): + self._last_error = exc + if self._last_error is None: + self._last_error = exc + + if self._client.topology_description.topology_type == TOPOLOGY_TYPE.Sharded: + self._deprioritized_servers.append(self._server) + + def _is_not_eligible_for_retry(self) -> bool: + """Checks if the exchange is not eligible for retry""" + return not self._retryable or (self._is_retrying() and not self._multiple_retries) + + def _is_retrying(self) -> bool: + """Checks if the exchange is currently undergoing a retry""" + return self._bulk.retrying if self._bulk else self._retrying + + def _is_session_state_retryable(self) -> bool: + """Checks if provided session is eligible for retry + + reads: Make sure there is no ongoing transaction (if provided a session) + writes: Make sure there is a session without an active transaction + """ + if self._is_read: + return not (self._session and self._session.in_transaction) + return bool(self._session and not self._session.in_transaction) + + def _check_last_error(self, check_csot: bool = False) -> None: + """Checks if the ongoing client exchange experienced a exception previously. + If so, raise last error + + :param check_csot: Checks CSOT to ensure we are retrying with time remaining defaults to False + """ + if self._is_retrying(): + remaining = _csot.remaining() + if not check_csot or (remaining is not None and remaining <= 0): + assert self._last_error is not None + raise self._last_error + + def _get_server(self) -> Server: + """Retrieves a server object based on provided object context + + :return: Abstraction to connect to server + """ + return self._client._select_server( + self._server_selector, + self._session, + self._operation, + address=self._address, + deprioritized_servers=self._deprioritized_servers, + operation_id=self._operation_id, + ) + + def _write(self) -> T: + """Wrapper method for write-type retryable client executions + + :return: Output for func()'s call + """ + try: + max_wire_version = 0 + is_mongos = False + self._server = self._get_server() + with self._client._checkout(self._server, self._session) as conn: + max_wire_version = conn.max_wire_version + sessions_supported = ( + self._session + and self._server.description.retryable_writes_supported + and conn.supports_sessions + ) + is_mongos = conn.is_mongos + if not sessions_supported: + # A retry is not possible because this server does + # not support sessions raise the last error. + self._check_last_error() + self._retryable = False + return self._func(self._session, conn, self._retryable) # type: ignore + except PyMongoError as exc: + if not self._retryable: + raise + # Add the RetryableWriteError label, if applicable. + _add_retryable_write_error(exc, max_wire_version, is_mongos) + raise + + def _read(self) -> T: + """Wrapper method for read-type retryable client executions + + :return: Output for func()'s call + """ + self._server = self._get_server() + assert self._read_pref is not None, "Read Preference required on read calls" + with self._client._conn_from_server(self._read_pref, self._server, self._session) as ( + conn, + read_pref, + ): + if self._retrying and not self._retryable: + self._check_last_error() + return self._func(self._session, self._server, conn, read_pref) # type: ignore + + +def _after_fork_child() -> None: + """Releases the locks in child process and resets the + topologies in all MongoClients. + """ + # Reinitialize locks + _release_locks() + + # Perform cleanup in clients (i.e. get rid of topology) + for _, client in MongoClient._clients.items(): + client._after_fork() + + +def _detect_external_db(entity: str) -> bool: + """Detects external database hosts and logs an informational message at the INFO level.""" + entity = entity.lower() + cosmos_db_hosts = [".cosmos.azure.com"] + document_db_hosts = [".docdb.amazonaws.com", ".docdb-elastic.amazonaws.com"] + + for host in cosmos_db_hosts: + if entity.endswith(host): + _log_or_warn( + _CLIENT_LOGGER, + "You appear to be connected to a CosmosDB cluster. For more information regarding feature " + "compatibility and support please visit https://www.mongodb.com/supportability/cosmosdb", + ) + return True + for host in document_db_hosts: + if entity.endswith(host): + _log_or_warn( + _CLIENT_LOGGER, + "You appear to be connected to a DocumentDB cluster. For more information regarding feature " + "compatibility and support please visit https://www.mongodb.com/supportability/documentdb", + ) + return True + return False + + +if _HAS_REGISTER_AT_FORK: + # This will run in the same thread as the fork was called. + # If we fork in a critical region on the same thread, it should break. + # This is fine since we would never call fork directly from a critical region. + os.register_at_fork(after_in_child=_after_fork_child) diff --git a/venv/Lib/site-packages/pymongo/monitor.py b/venv/Lib/site-packages/pymongo/monitor.py new file mode 100644 index 00000000..64945dd1 --- /dev/null +++ b/venv/Lib/site-packages/pymongo/monitor.py @@ -0,0 +1,485 @@ +# Copyright 2014-present MongoDB, 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. + +"""Class to monitor a MongoDB server on a background thread.""" + +from __future__ import annotations + +import atexit +import time +import weakref +from typing import TYPE_CHECKING, Any, Mapping, Optional, cast + +from pymongo import common, periodic_executor +from pymongo._csot import MovingMinimum +from pymongo.errors import NetworkTimeout, NotPrimaryError, OperationFailure, _OperationCancelled +from pymongo.hello import Hello +from pymongo.lock import _create_lock +from pymongo.periodic_executor import _shutdown_executors +from pymongo.pool import _is_faas +from pymongo.read_preferences import MovingAverage +from pymongo.server_description import ServerDescription +from pymongo.srv_resolver import _SrvResolver + +if TYPE_CHECKING: + from pymongo.pool import Connection, Pool, _CancellationContext + from pymongo.settings import TopologySettings + from pymongo.topology import Topology + + +def _sanitize(error: Exception) -> None: + """PYTHON-2433 Clear error traceback info.""" + error.__traceback__ = None + error.__context__ = None + error.__cause__ = None + + +class MonitorBase: + def __init__(self, topology: Topology, name: str, interval: int, min_interval: float): + """Base class to do periodic work on a background thread. + + The background thread is signaled to stop when the Topology or + this instance is freed. + """ + + # We strongly reference the executor and it weakly references us via + # this closure. When the monitor is freed, stop the executor soon. + def target() -> bool: + monitor = self_ref() + if monitor is None: + return False # Stop the executor. + monitor._run() # type:ignore[attr-defined] + return True + + executor = periodic_executor.PeriodicExecutor( + interval=interval, min_interval=min_interval, target=target, name=name + ) + + self._executor = executor + + def _on_topology_gc(dummy: Optional[Topology] = None) -> None: + # This prevents GC from waiting 10 seconds for hello to complete + # See test_cleanup_executors_on_client_del. + monitor = self_ref() + if monitor: + monitor.gc_safe_close() + + # Avoid cycles. When self or topology is freed, stop executor soon. + self_ref = weakref.ref(self, executor.close) + self._topology = weakref.proxy(topology, _on_topology_gc) + _register(self) + + def open(self) -> None: + """Start monitoring, or restart after a fork. + + Multiple calls have no effect. + """ + self._executor.open() + + def gc_safe_close(self) -> None: + """GC safe close.""" + self._executor.close() + + def close(self) -> None: + """Close and stop monitoring. + + open() restarts the monitor after closing. + """ + self.gc_safe_close() + + def join(self, timeout: Optional[int] = None) -> None: + """Wait for the monitor to stop.""" + self._executor.join(timeout) + + def request_check(self) -> None: + """If the monitor is sleeping, wake it soon.""" + self._executor.wake() + + +class Monitor(MonitorBase): + def __init__( + self, + server_description: ServerDescription, + topology: Topology, + pool: Pool, + topology_settings: TopologySettings, + ): + """Class to monitor a MongoDB server on a background thread. + + Pass an initial ServerDescription, a Topology, a Pool, and + TopologySettings. + + The Topology is weakly referenced. The Pool must be exclusive to this + Monitor. + """ + super().__init__( + topology, + "pymongo_server_monitor_thread", + topology_settings.heartbeat_frequency, + common.MIN_HEARTBEAT_INTERVAL, + ) + self._server_description = server_description + self._pool = pool + self._settings = topology_settings + self._listeners = self._settings._pool_options._event_listeners + self._publish = self._listeners is not None and self._listeners.enabled_for_server_heartbeat + self._cancel_context: Optional[_CancellationContext] = None + self._rtt_monitor = _RttMonitor( + topology, + topology_settings, + topology._create_pool_for_monitor(server_description.address), + ) + if topology_settings.server_monitoring_mode == "stream": + self._stream = True + elif topology_settings.server_monitoring_mode == "poll": + self._stream = False + else: + self._stream = not _is_faas() + + def cancel_check(self) -> None: + """Cancel any concurrent hello check. + + Note: this is called from a weakref.proxy callback and MUST NOT take + any locks. + """ + context = self._cancel_context + if context: + # Note: we cannot close the socket because doing so may cause + # concurrent reads/writes to hang until a timeout occurs + # (depending on the platform). + context.cancel() + + def _start_rtt_monitor(self) -> None: + """Start an _RttMonitor that periodically runs ping.""" + # If this monitor is closed directly before (or during) this open() + # call, the _RttMonitor will not be closed. Checking if this monitor + # was closed directly after resolves the race. + self._rtt_monitor.open() + if self._executor._stopped: + self._rtt_monitor.close() + + def gc_safe_close(self) -> None: + self._executor.close() + self._rtt_monitor.gc_safe_close() + self.cancel_check() + + def close(self) -> None: + self.gc_safe_close() + self._rtt_monitor.close() + # Increment the generation and maybe close the socket. If the executor + # thread has the socket checked out, it will be closed when checked in. + self._reset_connection() + + def _reset_connection(self) -> None: + # Clear our pooled connection. + self._pool.reset() + + def _run(self) -> None: + try: + prev_sd = self._server_description + try: + self._server_description = self._check_server() + except _OperationCancelled as exc: + _sanitize(exc) + # Already closed the connection, wait for the next check. + self._server_description = ServerDescription( + self._server_description.address, error=exc + ) + if prev_sd.is_server_type_known: + # Immediately retry since we've already waited 500ms to + # discover that we've been cancelled. + self._executor.skip_sleep() + return + + # Update the Topology and clear the server pool on error. + self._topology.on_change( + self._server_description, + reset_pool=self._server_description.error, + interrupt_connections=isinstance(self._server_description.error, NetworkTimeout), + ) + + if self._stream and ( + self._server_description.is_server_type_known + and self._server_description.topology_version + ): + self._start_rtt_monitor() + # Immediately check for the next streaming response. + self._executor.skip_sleep() + + if self._server_description.error and prev_sd.is_server_type_known: + # Immediately retry on network errors. + self._executor.skip_sleep() + except ReferenceError: + # Topology was garbage-collected. + self.close() + + def _check_server(self) -> ServerDescription: + """Call hello or read the next streaming response. + + Returns a ServerDescription. + """ + start = time.monotonic() + try: + try: + return self._check_once() + except (OperationFailure, NotPrimaryError) as exc: + # Update max cluster time even when hello fails. + details = cast(Mapping[str, Any], exc.details) + self._topology.receive_cluster_time(details.get("$clusterTime")) + raise + except ReferenceError: + raise + except Exception as error: + _sanitize(error) + sd = self._server_description + address = sd.address + duration = time.monotonic() - start + if self._publish: + awaited = bool(self._stream and sd.is_server_type_known and sd.topology_version) + assert self._listeners is not None + self._listeners.publish_server_heartbeat_failed(address, duration, error, awaited) + self._reset_connection() + if isinstance(error, _OperationCancelled): + raise + self._rtt_monitor.reset() + # Server type defaults to Unknown. + return ServerDescription(address, error=error) + + def _check_once(self) -> ServerDescription: + """A single attempt to call hello. + + Returns a ServerDescription, or raises an exception. + """ + address = self._server_description.address + if self._publish: + assert self._listeners is not None + sd = self._server_description + # XXX: "awaited" could be incorrectly set to True in the rare case + # the pool checkout closes and recreates a connection. + awaited = bool( + self._pool.conns + and self._stream + and sd.is_server_type_known + and sd.topology_version + ) + self._listeners.publish_server_heartbeat_started(address, awaited) + + if self._cancel_context and self._cancel_context.cancelled: + self._reset_connection() + with self._pool.checkout() as conn: + self._cancel_context = conn.cancel_context + response, round_trip_time = self._check_with_socket(conn) + if not response.awaitable: + self._rtt_monitor.add_sample(round_trip_time) + + avg_rtt, min_rtt = self._rtt_monitor.get() + sd = ServerDescription(address, response, avg_rtt, min_round_trip_time=min_rtt) + if self._publish: + assert self._listeners is not None + self._listeners.publish_server_heartbeat_succeeded( + address, round_trip_time, response, response.awaitable + ) + return sd + + def _check_with_socket(self, conn: Connection) -> tuple[Hello, float]: + """Return (Hello, round_trip_time). + + Can raise ConnectionFailure or OperationFailure. + """ + cluster_time = self._topology.max_cluster_time() + start = time.monotonic() + if conn.more_to_come: + # Read the next streaming hello (MongoDB 4.4+). + response = Hello(conn._next_reply(), awaitable=True) + elif ( + self._stream and conn.performed_handshake and self._server_description.topology_version + ): + # Initiate streaming hello (MongoDB 4.4+). + response = conn._hello( + cluster_time, + self._server_description.topology_version, + self._settings.heartbeat_frequency, + ) + else: + # New connection handshake or polling hello (MongoDB <4.4). + response = conn._hello(cluster_time, None, None) + return response, time.monotonic() - start + + +class SrvMonitor(MonitorBase): + def __init__(self, topology: Topology, topology_settings: TopologySettings): + """Class to poll SRV records on a background thread. + + Pass a Topology and a TopologySettings. + + The Topology is weakly referenced. + """ + super().__init__( + topology, + "pymongo_srv_polling_thread", + common.MIN_SRV_RESCAN_INTERVAL, + topology_settings.heartbeat_frequency, + ) + self._settings = topology_settings + self._seedlist = self._settings._seeds + assert isinstance(self._settings.fqdn, str) + self._fqdn: str = self._settings.fqdn + self._startup_time = time.monotonic() + + def _run(self) -> None: + # Don't poll right after creation, wait 60 seconds first + if time.monotonic() < self._startup_time + common.MIN_SRV_RESCAN_INTERVAL: + return + seedlist = self._get_seedlist() + if seedlist: + self._seedlist = seedlist + try: + self._topology.on_srv_update(self._seedlist) + except ReferenceError: + # Topology was garbage-collected. + self.close() + + def _get_seedlist(self) -> Optional[list[tuple[str, Any]]]: + """Poll SRV records for a seedlist. + + Returns a list of ServerDescriptions. + """ + try: + resolver = _SrvResolver( + self._fqdn, + self._settings.pool_options.connect_timeout, + self._settings.srv_service_name, + ) + seedlist, ttl = resolver.get_hosts_and_min_ttl() + if len(seedlist) == 0: + # As per the spec: this should be treated as a failure. + raise Exception + except Exception: + # As per the spec, upon encountering an error: + # - An error must not be raised + # - SRV records must be rescanned every heartbeatFrequencyMS + # - Topology must be left unchanged + self.request_check() + return None + else: + self._executor.update_interval(max(ttl, common.MIN_SRV_RESCAN_INTERVAL)) + return seedlist + + +class _RttMonitor(MonitorBase): + def __init__(self, topology: Topology, topology_settings: TopologySettings, pool: Pool): + """Maintain round trip times for a server. + + The Topology is weakly referenced. + """ + super().__init__( + topology, + "pymongo_server_rtt_thread", + topology_settings.heartbeat_frequency, + common.MIN_HEARTBEAT_INTERVAL, + ) + + self._pool = pool + self._moving_average = MovingAverage() + self._moving_min = MovingMinimum() + self._lock = _create_lock() + + def close(self) -> None: + self.gc_safe_close() + # Increment the generation and maybe close the socket. If the executor + # thread has the socket checked out, it will be closed when checked in. + self._pool.reset() + + def add_sample(self, sample: float) -> None: + """Add a RTT sample.""" + with self._lock: + self._moving_average.add_sample(sample) + self._moving_min.add_sample(sample) + + def get(self) -> tuple[Optional[float], float]: + """Get the calculated average, or None if no samples yet and the min.""" + with self._lock: + return self._moving_average.get(), self._moving_min.get() + + def reset(self) -> None: + """Reset the average RTT.""" + with self._lock: + self._moving_average.reset() + self._moving_min.reset() + + def _run(self) -> None: + try: + # NOTE: This thread is only run when using the streaming + # heartbeat protocol (MongoDB 4.4+). + # XXX: Skip check if the server is unknown? + rtt = self._ping() + self.add_sample(rtt) + except ReferenceError: + # Topology was garbage-collected. + self.close() + except Exception: + self._pool.reset() + + def _ping(self) -> float: + """Run a "hello" command and return the RTT.""" + with self._pool.checkout() as conn: + if self._executor._stopped: + raise Exception("_RttMonitor closed") + start = time.monotonic() + conn.hello() + return time.monotonic() - start + + +# Close monitors to cancel any in progress streaming checks before joining +# executor threads. For an explanation of how this works see the comment +# about _EXECUTORS in periodic_executor.py. +_MONITORS = set() + + +def _register(monitor: MonitorBase) -> None: + ref = weakref.ref(monitor, _unregister) + _MONITORS.add(ref) + + +def _unregister(monitor_ref: weakref.ReferenceType[MonitorBase]) -> None: + _MONITORS.remove(monitor_ref) + + +def _shutdown_monitors() -> None: + if _MONITORS is None: + return + + # Copy the set. Closing monitors removes them. + monitors = list(_MONITORS) + + # Close all monitors. + for ref in monitors: + monitor = ref() + if monitor: + monitor.gc_safe_close() + + monitor = None + + +def _shutdown_resources() -> None: + # _shutdown_monitors/_shutdown_executors may already be GC'd at shutdown. + shutdown = _shutdown_monitors + if shutdown: # type:ignore[truthy-function] + shutdown() + shutdown = _shutdown_executors + if shutdown: # type:ignore[truthy-function] + shutdown() + + +atexit.register(_shutdown_resources) diff --git a/venv/Lib/site-packages/pymongo/monitoring.py b/venv/Lib/site-packages/pymongo/monitoring.py new file mode 100644 index 00000000..aff11a9f --- /dev/null +++ b/venv/Lib/site-packages/pymongo/monitoring.py @@ -0,0 +1,1916 @@ +# Copyright 2015-present MongoDB, 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. + +"""Tools to monitor driver events. + +.. versionadded:: 3.1 + +.. attention:: Starting in PyMongo 3.11, the monitoring classes outlined below + are included in the PyMongo distribution under the + :mod:`~pymongo.event_loggers` submodule. + +Use :func:`register` to register global listeners for specific events. +Listeners must inherit from one of the abstract classes below and implement +the correct functions for that class. + +For example, a simple command logger might be implemented like this:: + + import logging + + from pymongo import monitoring + + class CommandLogger(monitoring.CommandListener): + + def started(self, event): + logging.info("Command {0.command_name} with request id " + "{0.request_id} started on server " + "{0.connection_id}".format(event)) + + def succeeded(self, event): + logging.info("Command {0.command_name} with request id " + "{0.request_id} on server {0.connection_id} " + "succeeded in {0.duration_micros} " + "microseconds".format(event)) + + def failed(self, event): + logging.info("Command {0.command_name} with request id " + "{0.request_id} on server {0.connection_id} " + "failed in {0.duration_micros} " + "microseconds".format(event)) + + monitoring.register(CommandLogger()) + +Server discovery and monitoring events are also available. For example:: + + class ServerLogger(monitoring.ServerListener): + + def opened(self, event): + logging.info("Server {0.server_address} added to topology " + "{0.topology_id}".format(event)) + + def description_changed(self, event): + previous_server_type = event.previous_description.server_type + new_server_type = event.new_description.server_type + if new_server_type != previous_server_type: + # server_type_name was added in PyMongo 3.4 + logging.info( + "Server {0.server_address} changed type from " + "{0.previous_description.server_type_name} to " + "{0.new_description.server_type_name}".format(event)) + + def closed(self, event): + logging.warning("Server {0.server_address} removed from topology " + "{0.topology_id}".format(event)) + + + class HeartbeatLogger(monitoring.ServerHeartbeatListener): + + def started(self, event): + logging.info("Heartbeat sent to server " + "{0.connection_id}".format(event)) + + def succeeded(self, event): + # The reply.document attribute was added in PyMongo 3.4. + logging.info("Heartbeat to server {0.connection_id} " + "succeeded with reply " + "{0.reply.document}".format(event)) + + def failed(self, event): + logging.warning("Heartbeat to server {0.connection_id} " + "failed with error {0.reply}".format(event)) + + class TopologyLogger(monitoring.TopologyListener): + + def opened(self, event): + logging.info("Topology with id {0.topology_id} " + "opened".format(event)) + + def description_changed(self, event): + logging.info("Topology description updated for " + "topology id {0.topology_id}".format(event)) + previous_topology_type = event.previous_description.topology_type + new_topology_type = event.new_description.topology_type + if new_topology_type != previous_topology_type: + # topology_type_name was added in PyMongo 3.4 + logging.info( + "Topology {0.topology_id} changed type from " + "{0.previous_description.topology_type_name} to " + "{0.new_description.topology_type_name}".format(event)) + # The has_writable_server and has_readable_server methods + # were added in PyMongo 3.4. + if not event.new_description.has_writable_server(): + logging.warning("No writable servers available.") + if not event.new_description.has_readable_server(): + logging.warning("No readable servers available.") + + def closed(self, event): + logging.info("Topology with id {0.topology_id} " + "closed".format(event)) + +Connection monitoring and pooling events are also available. For example:: + + class ConnectionPoolLogger(ConnectionPoolListener): + + def pool_created(self, event): + logging.info("[pool {0.address}] pool created".format(event)) + + def pool_ready(self, event): + logging.info("[pool {0.address}] pool is ready".format(event)) + + def pool_cleared(self, event): + logging.info("[pool {0.address}] pool cleared".format(event)) + + def pool_closed(self, event): + logging.info("[pool {0.address}] pool closed".format(event)) + + def connection_created(self, event): + logging.info("[pool {0.address}][connection #{0.connection_id}] " + "connection created".format(event)) + + def connection_ready(self, event): + logging.info("[pool {0.address}][connection #{0.connection_id}] " + "connection setup succeeded".format(event)) + + def connection_closed(self, event): + logging.info("[pool {0.address}][connection #{0.connection_id}] " + "connection closed, reason: " + "{0.reason}".format(event)) + + def connection_check_out_started(self, event): + logging.info("[pool {0.address}] connection check out " + "started".format(event)) + + def connection_check_out_failed(self, event): + logging.info("[pool {0.address}] connection check out " + "failed, reason: {0.reason}".format(event)) + + def connection_checked_out(self, event): + logging.info("[pool {0.address}][connection #{0.connection_id}] " + "connection checked out of pool".format(event)) + + def connection_checked_in(self, event): + logging.info("[pool {0.address}][connection #{0.connection_id}] " + "connection checked into pool".format(event)) + + +Event listeners can also be registered per instance of +:class:`~pymongo.mongo_client.MongoClient`:: + + client = MongoClient(event_listeners=[CommandLogger()]) + +Note that previously registered global listeners are automatically included +when configuring per client event listeners. Registering a new global listener +will not add that listener to existing client instances. + +.. note:: Events are delivered **synchronously**. Application threads block + waiting for event handlers (e.g. :meth:`~CommandListener.started`) to + return. Care must be taken to ensure that your event handlers are efficient + enough to not adversely affect overall application performance. + +.. warning:: The command documents published through this API are *not* copies. + If you intend to modify them in any way you must copy them in your event + handler first. +""" + +from __future__ import annotations + +import datetime +from collections import abc, namedtuple +from typing import TYPE_CHECKING, Any, Mapping, Optional, Sequence + +from bson.objectid import ObjectId +from pymongo.hello import Hello, HelloCompat +from pymongo.helpers import _handle_exception +from pymongo.typings import _Address, _DocumentOut + +if TYPE_CHECKING: + from datetime import timedelta + + from pymongo.server_description import ServerDescription + from pymongo.topology_description import TopologyDescription + + +_Listeners = namedtuple( + "_Listeners", + ( + "command_listeners", + "server_listeners", + "server_heartbeat_listeners", + "topology_listeners", + "cmap_listeners", + ), +) + +_LISTENERS = _Listeners([], [], [], [], []) + + +class _EventListener: + """Abstract base class for all event listeners.""" + + +class CommandListener(_EventListener): + """Abstract base class for command listeners. + + Handles `CommandStartedEvent`, `CommandSucceededEvent`, + and `CommandFailedEvent`. + """ + + def started(self, event: CommandStartedEvent) -> None: + """Abstract method to handle a `CommandStartedEvent`. + + :param event: An instance of :class:`CommandStartedEvent`. + """ + raise NotImplementedError + + def succeeded(self, event: CommandSucceededEvent) -> None: + """Abstract method to handle a `CommandSucceededEvent`. + + :param event: An instance of :class:`CommandSucceededEvent`. + """ + raise NotImplementedError + + def failed(self, event: CommandFailedEvent) -> None: + """Abstract method to handle a `CommandFailedEvent`. + + :param event: An instance of :class:`CommandFailedEvent`. + """ + raise NotImplementedError + + +class ConnectionPoolListener(_EventListener): + """Abstract base class for connection pool listeners. + + Handles all of the connection pool events defined in the Connection + Monitoring and Pooling Specification: + :class:`PoolCreatedEvent`, :class:`PoolClearedEvent`, + :class:`PoolClosedEvent`, :class:`ConnectionCreatedEvent`, + :class:`ConnectionReadyEvent`, :class:`ConnectionClosedEvent`, + :class:`ConnectionCheckOutStartedEvent`, + :class:`ConnectionCheckOutFailedEvent`, + :class:`ConnectionCheckedOutEvent`, + and :class:`ConnectionCheckedInEvent`. + + .. versionadded:: 3.9 + """ + + def pool_created(self, event: PoolCreatedEvent) -> None: + """Abstract method to handle a :class:`PoolCreatedEvent`. + + Emitted when a connection Pool is created. + + :param event: An instance of :class:`PoolCreatedEvent`. + """ + raise NotImplementedError + + def pool_ready(self, event: PoolReadyEvent) -> None: + """Abstract method to handle a :class:`PoolReadyEvent`. + + Emitted when a connection Pool is marked ready. + + :param event: An instance of :class:`PoolReadyEvent`. + + .. versionadded:: 4.0 + """ + raise NotImplementedError + + def pool_cleared(self, event: PoolClearedEvent) -> None: + """Abstract method to handle a `PoolClearedEvent`. + + Emitted when a connection Pool is cleared. + + :param event: An instance of :class:`PoolClearedEvent`. + """ + raise NotImplementedError + + def pool_closed(self, event: PoolClosedEvent) -> None: + """Abstract method to handle a `PoolClosedEvent`. + + Emitted when a connection Pool is closed. + + :param event: An instance of :class:`PoolClosedEvent`. + """ + raise NotImplementedError + + def connection_created(self, event: ConnectionCreatedEvent) -> None: + """Abstract method to handle a :class:`ConnectionCreatedEvent`. + + Emitted when a connection Pool creates a Connection object. + + :param event: An instance of :class:`ConnectionCreatedEvent`. + """ + raise NotImplementedError + + def connection_ready(self, event: ConnectionReadyEvent) -> None: + """Abstract method to handle a :class:`ConnectionReadyEvent`. + + Emitted when a connection has finished its setup, and is now ready to + use. + + :param event: An instance of :class:`ConnectionReadyEvent`. + """ + raise NotImplementedError + + def connection_closed(self, event: ConnectionClosedEvent) -> None: + """Abstract method to handle a :class:`ConnectionClosedEvent`. + + Emitted when a connection Pool closes a connection. + + :param event: An instance of :class:`ConnectionClosedEvent`. + """ + raise NotImplementedError + + def connection_check_out_started(self, event: ConnectionCheckOutStartedEvent) -> None: + """Abstract method to handle a :class:`ConnectionCheckOutStartedEvent`. + + Emitted when the driver starts attempting to check out a connection. + + :param event: An instance of :class:`ConnectionCheckOutStartedEvent`. + """ + raise NotImplementedError + + def connection_check_out_failed(self, event: ConnectionCheckOutFailedEvent) -> None: + """Abstract method to handle a :class:`ConnectionCheckOutFailedEvent`. + + Emitted when the driver's attempt to check out a connection fails. + + :param event: An instance of :class:`ConnectionCheckOutFailedEvent`. + """ + raise NotImplementedError + + def connection_checked_out(self, event: ConnectionCheckedOutEvent) -> None: + """Abstract method to handle a :class:`ConnectionCheckedOutEvent`. + + Emitted when the driver successfully checks out a connection. + + :param event: An instance of :class:`ConnectionCheckedOutEvent`. + """ + raise NotImplementedError + + def connection_checked_in(self, event: ConnectionCheckedInEvent) -> None: + """Abstract method to handle a :class:`ConnectionCheckedInEvent`. + + Emitted when the driver checks in a connection back to the connection + Pool. + + :param event: An instance of :class:`ConnectionCheckedInEvent`. + """ + raise NotImplementedError + + +class ServerHeartbeatListener(_EventListener): + """Abstract base class for server heartbeat listeners. + + Handles `ServerHeartbeatStartedEvent`, `ServerHeartbeatSucceededEvent`, + and `ServerHeartbeatFailedEvent`. + + .. versionadded:: 3.3 + """ + + def started(self, event: ServerHeartbeatStartedEvent) -> None: + """Abstract method to handle a `ServerHeartbeatStartedEvent`. + + :param event: An instance of :class:`ServerHeartbeatStartedEvent`. + """ + raise NotImplementedError + + def succeeded(self, event: ServerHeartbeatSucceededEvent) -> None: + """Abstract method to handle a `ServerHeartbeatSucceededEvent`. + + :param event: An instance of :class:`ServerHeartbeatSucceededEvent`. + """ + raise NotImplementedError + + def failed(self, event: ServerHeartbeatFailedEvent) -> None: + """Abstract method to handle a `ServerHeartbeatFailedEvent`. + + :param event: An instance of :class:`ServerHeartbeatFailedEvent`. + """ + raise NotImplementedError + + +class TopologyListener(_EventListener): + """Abstract base class for topology monitoring listeners. + Handles `TopologyOpenedEvent`, `TopologyDescriptionChangedEvent`, and + `TopologyClosedEvent`. + + .. versionadded:: 3.3 + """ + + def opened(self, event: TopologyOpenedEvent) -> None: + """Abstract method to handle a `TopologyOpenedEvent`. + + :param event: An instance of :class:`TopologyOpenedEvent`. + """ + raise NotImplementedError + + def description_changed(self, event: TopologyDescriptionChangedEvent) -> None: + """Abstract method to handle a `TopologyDescriptionChangedEvent`. + + :param event: An instance of :class:`TopologyDescriptionChangedEvent`. + """ + raise NotImplementedError + + def closed(self, event: TopologyClosedEvent) -> None: + """Abstract method to handle a `TopologyClosedEvent`. + + :param event: An instance of :class:`TopologyClosedEvent`. + """ + raise NotImplementedError + + +class ServerListener(_EventListener): + """Abstract base class for server listeners. + Handles `ServerOpeningEvent`, `ServerDescriptionChangedEvent`, and + `ServerClosedEvent`. + + .. versionadded:: 3.3 + """ + + def opened(self, event: ServerOpeningEvent) -> None: + """Abstract method to handle a `ServerOpeningEvent`. + + :param event: An instance of :class:`ServerOpeningEvent`. + """ + raise NotImplementedError + + def description_changed(self, event: ServerDescriptionChangedEvent) -> None: + """Abstract method to handle a `ServerDescriptionChangedEvent`. + + :param event: An instance of :class:`ServerDescriptionChangedEvent`. + """ + raise NotImplementedError + + def closed(self, event: ServerClosedEvent) -> None: + """Abstract method to handle a `ServerClosedEvent`. + + :param event: An instance of :class:`ServerClosedEvent`. + """ + raise NotImplementedError + + +def _to_micros(dur: timedelta) -> int: + """Convert duration 'dur' to microseconds.""" + return int(dur.total_seconds() * 10e5) + + +def _validate_event_listeners( + option: str, listeners: Sequence[_EventListeners] +) -> Sequence[_EventListeners]: + """Validate event listeners""" + if not isinstance(listeners, abc.Sequence): + raise TypeError(f"{option} must be a list or tuple") + for listener in listeners: + if not isinstance(listener, _EventListener): + raise TypeError( + f"Listeners for {option} must be either a " + "CommandListener, ServerHeartbeatListener, " + "ServerListener, TopologyListener, or " + "ConnectionPoolListener." + ) + return listeners + + +def register(listener: _EventListener) -> None: + """Register a global event listener. + + :param listener: A subclasses of :class:`CommandListener`, + :class:`ServerHeartbeatListener`, :class:`ServerListener`, + :class:`TopologyListener`, or :class:`ConnectionPoolListener`. + """ + if not isinstance(listener, _EventListener): + raise TypeError( + f"Listeners for {listener} must be either a " + "CommandListener, ServerHeartbeatListener, " + "ServerListener, TopologyListener, or " + "ConnectionPoolListener." + ) + if isinstance(listener, CommandListener): + _LISTENERS.command_listeners.append(listener) + if isinstance(listener, ServerHeartbeatListener): + _LISTENERS.server_heartbeat_listeners.append(listener) + if isinstance(listener, ServerListener): + _LISTENERS.server_listeners.append(listener) + if isinstance(listener, TopologyListener): + _LISTENERS.topology_listeners.append(listener) + if isinstance(listener, ConnectionPoolListener): + _LISTENERS.cmap_listeners.append(listener) + + +# Note - to avoid bugs from forgetting which if these is all lowercase and +# which are camelCase, and at the same time avoid having to add a test for +# every command, use all lowercase here and test against command_name.lower(). +_SENSITIVE_COMMANDS: set = { + "authenticate", + "saslstart", + "saslcontinue", + "getnonce", + "createuser", + "updateuser", + "copydbgetnonce", + "copydbsaslstart", + "copydb", +} + + +# The "hello" command is also deemed sensitive when attempting speculative +# authentication. +def _is_speculative_authenticate(command_name: str, doc: Mapping[str, Any]) -> bool: + if ( + command_name.lower() in ("hello", HelloCompat.LEGACY_CMD) + and "speculativeAuthenticate" in doc + ): + return True + return False + + +class _CommandEvent: + """Base class for command events.""" + + __slots__ = ( + "__cmd_name", + "__rqst_id", + "__conn_id", + "__op_id", + "__service_id", + "__db", + "__server_conn_id", + ) + + def __init__( + self, + command_name: str, + request_id: int, + connection_id: _Address, + operation_id: Optional[int], + service_id: Optional[ObjectId] = None, + database_name: str = "", + server_connection_id: Optional[int] = None, + ) -> None: + self.__cmd_name = command_name + self.__rqst_id = request_id + self.__conn_id = connection_id + self.__op_id = operation_id + self.__service_id = service_id + self.__db = database_name + self.__server_conn_id = server_connection_id + + @property + def command_name(self) -> str: + """The command name.""" + return self.__cmd_name + + @property + def request_id(self) -> int: + """The request id for this operation.""" + return self.__rqst_id + + @property + def connection_id(self) -> _Address: + """The address (host, port) of the server this command was sent to.""" + return self.__conn_id + + @property + def service_id(self) -> Optional[ObjectId]: + """The service_id this command was sent to, or ``None``. + + .. versionadded:: 3.12 + """ + return self.__service_id + + @property + def operation_id(self) -> Optional[int]: + """An id for this series of events or None.""" + return self.__op_id + + @property + def database_name(self) -> str: + """The database_name this command was sent to, or ``""``. + + .. versionadded:: 4.6 + """ + return self.__db + + @property + def server_connection_id(self) -> Optional[int]: + """The server-side connection id for the connection this command was sent on, or ``None``. + + .. versionadded:: 4.7 + """ + return self.__server_conn_id + + +class CommandStartedEvent(_CommandEvent): + """Event published when a command starts. + + :param command: The command document. + :param database_name: The name of the database this command was run against. + :param request_id: The request id for this operation. + :param connection_id: The address (host, port) of the server this command + was sent to. + :param operation_id: An optional identifier for a series of related events. + :param service_id: The service_id this command was sent to, or ``None``. + """ + + __slots__ = ("__cmd",) + + def __init__( + self, + command: _DocumentOut, + database_name: str, + request_id: int, + connection_id: _Address, + operation_id: Optional[int], + service_id: Optional[ObjectId] = None, + server_connection_id: Optional[int] = None, + ) -> None: + if not command: + raise ValueError(f"{command!r} is not a valid command") + # Command name must be first key. + command_name = next(iter(command)) + super().__init__( + command_name, + request_id, + connection_id, + operation_id, + service_id=service_id, + database_name=database_name, + server_connection_id=server_connection_id, + ) + cmd_name = command_name.lower() + if cmd_name in _SENSITIVE_COMMANDS or _is_speculative_authenticate(cmd_name, command): + self.__cmd: _DocumentOut = {} + else: + self.__cmd = command + + @property + def command(self) -> _DocumentOut: + """The command document.""" + return self.__cmd + + @property + def database_name(self) -> str: + """The name of the database this command was run against.""" + return super().database_name + + def __repr__(self) -> str: + return ( + "<{} {} db: {!r}, command: {!r}, operation_id: {}, service_id: {}, server_connection_id: {}>" + ).format( + self.__class__.__name__, + self.connection_id, + self.database_name, + self.command_name, + self.operation_id, + self.service_id, + self.server_connection_id, + ) + + +class CommandSucceededEvent(_CommandEvent): + """Event published when a command succeeds. + + :param duration: The command duration as a datetime.timedelta. + :param reply: The server reply document. + :param command_name: The command name. + :param request_id: The request id for this operation. + :param connection_id: The address (host, port) of the server this command + was sent to. + :param operation_id: An optional identifier for a series of related events. + :param service_id: The service_id this command was sent to, or ``None``. + :param database_name: The database this command was sent to, or ``""``. + """ + + __slots__ = ("__duration_micros", "__reply") + + def __init__( + self, + duration: datetime.timedelta, + reply: _DocumentOut, + command_name: str, + request_id: int, + connection_id: _Address, + operation_id: Optional[int], + service_id: Optional[ObjectId] = None, + database_name: str = "", + server_connection_id: Optional[int] = None, + ) -> None: + super().__init__( + command_name, + request_id, + connection_id, + operation_id, + service_id=service_id, + database_name=database_name, + server_connection_id=server_connection_id, + ) + self.__duration_micros = _to_micros(duration) + cmd_name = command_name.lower() + if cmd_name in _SENSITIVE_COMMANDS or _is_speculative_authenticate(cmd_name, reply): + self.__reply: _DocumentOut = {} + else: + self.__reply = reply + + @property + def duration_micros(self) -> int: + """The duration of this operation in microseconds.""" + return self.__duration_micros + + @property + def reply(self) -> _DocumentOut: + """The server failure document for this operation.""" + return self.__reply + + def __repr__(self) -> str: + return ( + "<{} {} db: {!r}, command: {!r}, operation_id: {}, duration_micros: {}, service_id: {}, server_connection_id: {}>" + ).format( + self.__class__.__name__, + self.connection_id, + self.database_name, + self.command_name, + self.operation_id, + self.duration_micros, + self.service_id, + self.server_connection_id, + ) + + +class CommandFailedEvent(_CommandEvent): + """Event published when a command fails. + + :param duration: The command duration as a datetime.timedelta. + :param failure: The server reply document. + :param command_name: The command name. + :param request_id: The request id for this operation. + :param connection_id: The address (host, port) of the server this command + was sent to. + :param operation_id: An optional identifier for a series of related events. + :param service_id: The service_id this command was sent to, or ``None``. + :param database_name: The database this command was sent to, or ``""``. + """ + + __slots__ = ("__duration_micros", "__failure") + + def __init__( + self, + duration: datetime.timedelta, + failure: _DocumentOut, + command_name: str, + request_id: int, + connection_id: _Address, + operation_id: Optional[int], + service_id: Optional[ObjectId] = None, + database_name: str = "", + server_connection_id: Optional[int] = None, + ) -> None: + super().__init__( + command_name, + request_id, + connection_id, + operation_id, + service_id=service_id, + database_name=database_name, + server_connection_id=server_connection_id, + ) + self.__duration_micros = _to_micros(duration) + self.__failure = failure + + @property + def duration_micros(self) -> int: + """The duration of this operation in microseconds.""" + return self.__duration_micros + + @property + def failure(self) -> _DocumentOut: + """The server failure document for this operation.""" + return self.__failure + + def __repr__(self) -> str: + return ( + "<{} {} db: {!r}, command: {!r}, operation_id: {}, duration_micros: {}, " + "failure: {!r}, service_id: {}, server_connection_id: {}>" + ).format( + self.__class__.__name__, + self.connection_id, + self.database_name, + self.command_name, + self.operation_id, + self.duration_micros, + self.failure, + self.service_id, + self.server_connection_id, + ) + + +class _PoolEvent: + """Base class for pool events.""" + + __slots__ = ("__address",) + + def __init__(self, address: _Address) -> None: + self.__address = address + + @property + def address(self) -> _Address: + """The address (host, port) pair of the server the pool is attempting + to connect to. + """ + return self.__address + + def __repr__(self) -> str: + return f"{self.__class__.__name__}({self.__address!r})" + + +class PoolCreatedEvent(_PoolEvent): + """Published when a Connection Pool is created. + + :param address: The address (host, port) pair of the server this Pool is + attempting to connect to. + + .. versionadded:: 3.9 + """ + + __slots__ = ("__options",) + + def __init__(self, address: _Address, options: dict[str, Any]) -> None: + super().__init__(address) + self.__options = options + + @property + def options(self) -> dict[str, Any]: + """Any non-default pool options that were set on this Connection Pool.""" + return self.__options + + def __repr__(self) -> str: + return f"{self.__class__.__name__}({self.address!r}, {self.__options!r})" + + +class PoolReadyEvent(_PoolEvent): + """Published when a Connection Pool is marked ready. + + :param address: The address (host, port) pair of the server this Pool is + attempting to connect to. + + .. versionadded:: 4.0 + """ + + __slots__ = () + + +class PoolClearedEvent(_PoolEvent): + """Published when a Connection Pool is cleared. + + :param address: The address (host, port) pair of the server this Pool is + attempting to connect to. + :param service_id: The service_id this command was sent to, or ``None``. + :param interrupt_connections: True if all active connections were interrupted by the Pool during clearing. + + .. versionadded:: 3.9 + """ + + __slots__ = ("__service_id", "__interrupt_connections") + + def __init__( + self, + address: _Address, + service_id: Optional[ObjectId] = None, + interrupt_connections: bool = False, + ) -> None: + super().__init__(address) + self.__service_id = service_id + self.__interrupt_connections = interrupt_connections + + @property + def service_id(self) -> Optional[ObjectId]: + """Connections with this service_id are cleared. + + When service_id is ``None``, all connections in the pool are cleared. + + .. versionadded:: 3.12 + """ + return self.__service_id + + @property + def interrupt_connections(self) -> bool: + """If True, active connections are interrupted during clearing. + + .. versionadded:: 4.7 + """ + return self.__interrupt_connections + + def __repr__(self) -> str: + return f"{self.__class__.__name__}({self.address!r}, {self.__service_id!r}, {self.__interrupt_connections!r})" + + +class PoolClosedEvent(_PoolEvent): + """Published when a Connection Pool is closed. + + :param address: The address (host, port) pair of the server this Pool is + attempting to connect to. + + .. versionadded:: 3.9 + """ + + __slots__ = () + + +class ConnectionClosedReason: + """An enum that defines values for `reason` on a + :class:`ConnectionClosedEvent`. + + .. versionadded:: 3.9 + """ + + STALE = "stale" + """The pool was cleared, making the connection no longer valid.""" + + IDLE = "idle" + """The connection became stale by being idle for too long (maxIdleTimeMS). + """ + + ERROR = "error" + """The connection experienced an error, making it no longer valid.""" + + POOL_CLOSED = "poolClosed" + """The pool was closed, making the connection no longer valid.""" + + +class ConnectionCheckOutFailedReason: + """An enum that defines values for `reason` on a + :class:`ConnectionCheckOutFailedEvent`. + + .. versionadded:: 3.9 + """ + + TIMEOUT = "timeout" + """The connection check out attempt exceeded the specified timeout.""" + + POOL_CLOSED = "poolClosed" + """The pool was previously closed, and cannot provide new connections.""" + + CONN_ERROR = "connectionError" + """The connection check out attempt experienced an error while setting up + a new connection. + """ + + +class _ConnectionEvent: + """Private base class for connection events.""" + + __slots__ = ("__address",) + + def __init__(self, address: _Address) -> None: + self.__address = address + + @property + def address(self) -> _Address: + """The address (host, port) pair of the server this connection is + attempting to connect to. + """ + return self.__address + + def __repr__(self) -> str: + return f"{self.__class__.__name__}({self.__address!r})" + + +class _ConnectionIdEvent(_ConnectionEvent): + """Private base class for connection events with an id.""" + + __slots__ = ("__connection_id",) + + def __init__(self, address: _Address, connection_id: int) -> None: + super().__init__(address) + self.__connection_id = connection_id + + @property + def connection_id(self) -> int: + """The ID of the connection.""" + return self.__connection_id + + def __repr__(self) -> str: + return f"{self.__class__.__name__}({self.address!r}, {self.__connection_id!r})" + + +class _ConnectionDurationEvent(_ConnectionIdEvent): + """Private base class for connection events with a duration.""" + + __slots__ = ("__duration",) + + def __init__(self, address: _Address, connection_id: int, duration: Optional[float]) -> None: + super().__init__(address, connection_id) + self.__duration = duration + + @property + def duration(self) -> Optional[float]: + """The duration of the connection event. + + .. versionadded:: 4.7 + """ + return self.__duration + + def __repr__(self) -> str: + return f"{self.__class__.__name__}({self.address!r}, {self.connection_id!r}, {self.__duration!r})" + + +class ConnectionCreatedEvent(_ConnectionIdEvent): + """Published when a Connection Pool creates a Connection object. + + NOTE: This connection is not ready for use until the + :class:`ConnectionReadyEvent` is published. + + :param address: The address (host, port) pair of the server this + Connection is attempting to connect to. + :param connection_id: The integer ID of the Connection in this Pool. + + .. versionadded:: 3.9 + """ + + __slots__ = () + + +class ConnectionReadyEvent(_ConnectionDurationEvent): + """Published when a Connection has finished its setup, and is ready to use. + + :param address: The address (host, port) pair of the server this + Connection is attempting to connect to. + :param connection_id: The integer ID of the Connection in this Pool. + + .. versionadded:: 3.9 + """ + + __slots__ = () + + +class ConnectionClosedEvent(_ConnectionIdEvent): + """Published when a Connection is closed. + + :param address: The address (host, port) pair of the server this + Connection is attempting to connect to. + :param connection_id: The integer ID of the Connection in this Pool. + :param reason: A reason explaining why this connection was closed. + + .. versionadded:: 3.9 + """ + + __slots__ = ("__reason",) + + def __init__(self, address: _Address, connection_id: int, reason: str): + super().__init__(address, connection_id) + self.__reason = reason + + @property + def reason(self) -> str: + """A reason explaining why this connection was closed. + + The reason must be one of the strings from the + :class:`ConnectionClosedReason` enum. + """ + return self.__reason + + def __repr__(self) -> str: + return "{}({!r}, {!r}, {!r})".format( + self.__class__.__name__, + self.address, + self.connection_id, + self.__reason, + ) + + +class ConnectionCheckOutStartedEvent(_ConnectionEvent): + """Published when the driver starts attempting to check out a connection. + + :param address: The address (host, port) pair of the server this + Connection is attempting to connect to. + + .. versionadded:: 3.9 + """ + + __slots__ = () + + +class ConnectionCheckOutFailedEvent(_ConnectionDurationEvent): + """Published when the driver's attempt to check out a connection fails. + + :param address: The address (host, port) pair of the server this + Connection is attempting to connect to. + :param reason: A reason explaining why connection check out failed. + + .. versionadded:: 3.9 + """ + + __slots__ = ("__reason",) + + def __init__(self, address: _Address, reason: str, duration: Optional[float]) -> None: + super().__init__(address=address, connection_id=0, duration=duration) + self.__reason = reason + + @property + def reason(self) -> str: + """A reason explaining why connection check out failed. + + The reason must be one of the strings from the + :class:`ConnectionCheckOutFailedReason` enum. + """ + return self.__reason + + def __repr__(self) -> str: + return f"{self.__class__.__name__}({self.address!r}, {self.__reason!r}, {self.duration!r})" + + +class ConnectionCheckedOutEvent(_ConnectionDurationEvent): + """Published when the driver successfully checks out a connection. + + :param address: The address (host, port) pair of the server this + Connection is attempting to connect to. + :param connection_id: The integer ID of the Connection in this Pool. + + .. versionadded:: 3.9 + """ + + __slots__ = () + + +class ConnectionCheckedInEvent(_ConnectionIdEvent): + """Published when the driver checks in a Connection into the Pool. + + :param address: The address (host, port) pair of the server this + Connection is attempting to connect to. + :param connection_id: The integer ID of the Connection in this Pool. + + .. versionadded:: 3.9 + """ + + __slots__ = () + + +class _ServerEvent: + """Base class for server events.""" + + __slots__ = ("__server_address", "__topology_id") + + def __init__(self, server_address: _Address, topology_id: ObjectId) -> None: + self.__server_address = server_address + self.__topology_id = topology_id + + @property + def server_address(self) -> _Address: + """The address (host, port) pair of the server""" + return self.__server_address + + @property + def topology_id(self) -> ObjectId: + """A unique identifier for the topology this server is a part of.""" + return self.__topology_id + + def __repr__(self) -> str: + return f"<{self.__class__.__name__} {self.server_address} topology_id: {self.topology_id}>" + + +class ServerDescriptionChangedEvent(_ServerEvent): + """Published when server description changes. + + .. versionadded:: 3.3 + """ + + __slots__ = ("__previous_description", "__new_description") + + def __init__( + self, + previous_description: ServerDescription, + new_description: ServerDescription, + *args: Any, + ) -> None: + super().__init__(*args) + self.__previous_description = previous_description + self.__new_description = new_description + + @property + def previous_description(self) -> ServerDescription: + """The previous + :class:`~pymongo.server_description.ServerDescription`. + """ + return self.__previous_description + + @property + def new_description(self) -> ServerDescription: + """The new + :class:`~pymongo.server_description.ServerDescription`. + """ + return self.__new_description + + def __repr__(self) -> str: + return "<{} {} changed from: {}, to: {}>".format( + self.__class__.__name__, + self.server_address, + self.previous_description, + self.new_description, + ) + + +class ServerOpeningEvent(_ServerEvent): + """Published when server is initialized. + + .. versionadded:: 3.3 + """ + + __slots__ = () + + +class ServerClosedEvent(_ServerEvent): + """Published when server is closed. + + .. versionadded:: 3.3 + """ + + __slots__ = () + + +class TopologyEvent: + """Base class for topology description events.""" + + __slots__ = ("__topology_id",) + + def __init__(self, topology_id: ObjectId) -> None: + self.__topology_id = topology_id + + @property + def topology_id(self) -> ObjectId: + """A unique identifier for the topology this server is a part of.""" + return self.__topology_id + + def __repr__(self) -> str: + return f"<{self.__class__.__name__} topology_id: {self.topology_id}>" + + +class TopologyDescriptionChangedEvent(TopologyEvent): + """Published when the topology description changes. + + .. versionadded:: 3.3 + """ + + __slots__ = ("__previous_description", "__new_description") + + def __init__( + self, + previous_description: TopologyDescription, + new_description: TopologyDescription, + *args: Any, + ) -> None: + super().__init__(*args) + self.__previous_description = previous_description + self.__new_description = new_description + + @property + def previous_description(self) -> TopologyDescription: + """The previous + :class:`~pymongo.topology_description.TopologyDescription`. + """ + return self.__previous_description + + @property + def new_description(self) -> TopologyDescription: + """The new + :class:`~pymongo.topology_description.TopologyDescription`. + """ + return self.__new_description + + def __repr__(self) -> str: + return "<{} topology_id: {} changed from: {}, to: {}>".format( + self.__class__.__name__, + self.topology_id, + self.previous_description, + self.new_description, + ) + + +class TopologyOpenedEvent(TopologyEvent): + """Published when the topology is initialized. + + .. versionadded:: 3.3 + """ + + __slots__ = () + + +class TopologyClosedEvent(TopologyEvent): + """Published when the topology is closed. + + .. versionadded:: 3.3 + """ + + __slots__ = () + + +class _ServerHeartbeatEvent: + """Base class for server heartbeat events.""" + + __slots__ = ("__connection_id", "__awaited") + + def __init__(self, connection_id: _Address, awaited: bool = False) -> None: + self.__connection_id = connection_id + self.__awaited = awaited + + @property + def connection_id(self) -> _Address: + """The address (host, port) of the server this heartbeat was sent + to. + """ + return self.__connection_id + + @property + def awaited(self) -> bool: + """Whether the heartbeat was issued as an awaitable hello command. + + .. versionadded:: 4.6 + """ + return self.__awaited + + def __repr__(self) -> str: + return f"<{self.__class__.__name__} {self.connection_id} awaited: {self.awaited}>" + + +class ServerHeartbeatStartedEvent(_ServerHeartbeatEvent): + """Published when a heartbeat is started. + + .. versionadded:: 3.3 + """ + + __slots__ = () + + +class ServerHeartbeatSucceededEvent(_ServerHeartbeatEvent): + """Fired when the server heartbeat succeeds. + + .. versionadded:: 3.3 + """ + + __slots__ = ("__duration", "__reply") + + def __init__( + self, duration: float, reply: Hello, connection_id: _Address, awaited: bool = False + ) -> None: + super().__init__(connection_id, awaited) + self.__duration = duration + self.__reply = reply + + @property + def duration(self) -> float: + """The duration of this heartbeat in microseconds.""" + return self.__duration + + @property + def reply(self) -> Hello: + """An instance of :class:`~pymongo.hello.Hello`.""" + return self.__reply + + @property + def awaited(self) -> bool: + """Whether the heartbeat was awaited. + + If true, then :meth:`duration` reflects the sum of the round trip time + to the server and the time that the server waited before sending a + response. + + .. versionadded:: 3.11 + """ + return super().awaited + + def __repr__(self) -> str: + return "<{} {} duration: {}, awaited: {}, reply: {}>".format( + self.__class__.__name__, + self.connection_id, + self.duration, + self.awaited, + self.reply, + ) + + +class ServerHeartbeatFailedEvent(_ServerHeartbeatEvent): + """Fired when the server heartbeat fails, either with an "ok: 0" + or a socket exception. + + .. versionadded:: 3.3 + """ + + __slots__ = ("__duration", "__reply") + + def __init__( + self, duration: float, reply: Exception, connection_id: _Address, awaited: bool = False + ) -> None: + super().__init__(connection_id, awaited) + self.__duration = duration + self.__reply = reply + + @property + def duration(self) -> float: + """The duration of this heartbeat in microseconds.""" + return self.__duration + + @property + def reply(self) -> Exception: + """A subclass of :exc:`Exception`.""" + return self.__reply + + @property + def awaited(self) -> bool: + """Whether the heartbeat was awaited. + + If true, then :meth:`duration` reflects the sum of the round trip time + to the server and the time that the server waited before sending a + response. + + .. versionadded:: 3.11 + """ + return super().awaited + + def __repr__(self) -> str: + return "<{} {} duration: {}, awaited: {}, reply: {!r}>".format( + self.__class__.__name__, + self.connection_id, + self.duration, + self.awaited, + self.reply, + ) + + +class _EventListeners: + """Configure event listeners for a client instance. + + Any event listeners registered globally are included by default. + + :param listeners: A list of event listeners. + """ + + def __init__(self, listeners: Optional[Sequence[_EventListener]]): + self.__command_listeners = _LISTENERS.command_listeners[:] + self.__server_listeners = _LISTENERS.server_listeners[:] + lst = _LISTENERS.server_heartbeat_listeners + self.__server_heartbeat_listeners = lst[:] + self.__topology_listeners = _LISTENERS.topology_listeners[:] + self.__cmap_listeners = _LISTENERS.cmap_listeners[:] + if listeners is not None: + for lst in listeners: + if isinstance(lst, CommandListener): + self.__command_listeners.append(lst) + if isinstance(lst, ServerListener): + self.__server_listeners.append(lst) + if isinstance(lst, ServerHeartbeatListener): + self.__server_heartbeat_listeners.append(lst) + if isinstance(lst, TopologyListener): + self.__topology_listeners.append(lst) + if isinstance(lst, ConnectionPoolListener): + self.__cmap_listeners.append(lst) + self.__enabled_for_commands = bool(self.__command_listeners) + self.__enabled_for_server = bool(self.__server_listeners) + self.__enabled_for_server_heartbeat = bool(self.__server_heartbeat_listeners) + self.__enabled_for_topology = bool(self.__topology_listeners) + self.__enabled_for_cmap = bool(self.__cmap_listeners) + + @property + def enabled_for_commands(self) -> bool: + """Are any CommandListener instances registered?""" + return self.__enabled_for_commands + + @property + def enabled_for_server(self) -> bool: + """Are any ServerListener instances registered?""" + return self.__enabled_for_server + + @property + def enabled_for_server_heartbeat(self) -> bool: + """Are any ServerHeartbeatListener instances registered?""" + return self.__enabled_for_server_heartbeat + + @property + def enabled_for_topology(self) -> bool: + """Are any TopologyListener instances registered?""" + return self.__enabled_for_topology + + @property + def enabled_for_cmap(self) -> bool: + """Are any ConnectionPoolListener instances registered?""" + return self.__enabled_for_cmap + + def event_listeners(self) -> list[_EventListeners]: + """List of registered event listeners.""" + return ( + self.__command_listeners + + self.__server_heartbeat_listeners + + self.__server_listeners + + self.__topology_listeners + + self.__cmap_listeners + ) + + def publish_command_start( + self, + command: _DocumentOut, + database_name: str, + request_id: int, + connection_id: _Address, + server_connection_id: Optional[int], + op_id: Optional[int] = None, + service_id: Optional[ObjectId] = None, + ) -> None: + """Publish a CommandStartedEvent to all command listeners. + + :param command: The command document. + :param database_name: The name of the database this command was run + against. + :param request_id: The request id for this operation. + :param connection_id: The address (host, port) of the server this + command was sent to. + :param op_id: The (optional) operation id for this operation. + :param service_id: The service_id this command was sent to, or ``None``. + """ + if op_id is None: + op_id = request_id + event = CommandStartedEvent( + command, + database_name, + request_id, + connection_id, + op_id, + service_id=service_id, + server_connection_id=server_connection_id, + ) + for subscriber in self.__command_listeners: + try: + subscriber.started(event) + except Exception: + _handle_exception() + + def publish_command_success( + self, + duration: timedelta, + reply: _DocumentOut, + command_name: str, + request_id: int, + connection_id: _Address, + server_connection_id: Optional[int], + op_id: Optional[int] = None, + service_id: Optional[ObjectId] = None, + speculative_hello: bool = False, + database_name: str = "", + ) -> None: + """Publish a CommandSucceededEvent to all command listeners. + + :param duration: The command duration as a datetime.timedelta. + :param reply: The server reply document. + :param command_name: The command name. + :param request_id: The request id for this operation. + :param connection_id: The address (host, port) of the server this + command was sent to. + :param op_id: The (optional) operation id for this operation. + :param service_id: The service_id this command was sent to, or ``None``. + :param speculative_hello: Was the command sent with speculative auth? + :param database_name: The database this command was sent to, or ``""``. + """ + if op_id is None: + op_id = request_id + if speculative_hello: + # Redact entire response when the command started contained + # speculativeAuthenticate. + reply = {} + event = CommandSucceededEvent( + duration, + reply, + command_name, + request_id, + connection_id, + op_id, + service_id, + database_name=database_name, + server_connection_id=server_connection_id, + ) + for subscriber in self.__command_listeners: + try: + subscriber.succeeded(event) + except Exception: + _handle_exception() + + def publish_command_failure( + self, + duration: timedelta, + failure: _DocumentOut, + command_name: str, + request_id: int, + connection_id: _Address, + server_connection_id: Optional[int], + op_id: Optional[int] = None, + service_id: Optional[ObjectId] = None, + database_name: str = "", + ) -> None: + """Publish a CommandFailedEvent to all command listeners. + + :param duration: The command duration as a datetime.timedelta. + :param failure: The server reply document or failure description + document. + :param command_name: The command name. + :param request_id: The request id for this operation. + :param connection_id: The address (host, port) of the server this + command was sent to. + :param op_id: The (optional) operation id for this operation. + :param service_id: The service_id this command was sent to, or ``None``. + :param database_name: The database this command was sent to, or ``""``. + """ + if op_id is None: + op_id = request_id + event = CommandFailedEvent( + duration, + failure, + command_name, + request_id, + connection_id, + op_id, + service_id=service_id, + database_name=database_name, + server_connection_id=server_connection_id, + ) + for subscriber in self.__command_listeners: + try: + subscriber.failed(event) + except Exception: + _handle_exception() + + def publish_server_heartbeat_started(self, connection_id: _Address, awaited: bool) -> None: + """Publish a ServerHeartbeatStartedEvent to all server heartbeat + listeners. + + :param connection_id: The address (host, port) pair of the connection. + :param awaited: True if this heartbeat is part of an awaitable hello command. + """ + event = ServerHeartbeatStartedEvent(connection_id, awaited) + for subscriber in self.__server_heartbeat_listeners: + try: + subscriber.started(event) + except Exception: + _handle_exception() + + def publish_server_heartbeat_succeeded( + self, connection_id: _Address, duration: float, reply: Hello, awaited: bool + ) -> None: + """Publish a ServerHeartbeatSucceededEvent to all server heartbeat + listeners. + + :param connection_id: The address (host, port) pair of the connection. + :param duration: The execution time of the event in the highest possible + resolution for the platform. + :param reply: The command reply. + :param awaited: True if the response was awaited. + """ + event = ServerHeartbeatSucceededEvent(duration, reply, connection_id, awaited) + for subscriber in self.__server_heartbeat_listeners: + try: + subscriber.succeeded(event) + except Exception: + _handle_exception() + + def publish_server_heartbeat_failed( + self, connection_id: _Address, duration: float, reply: Exception, awaited: bool + ) -> None: + """Publish a ServerHeartbeatFailedEvent to all server heartbeat + listeners. + + :param connection_id: The address (host, port) pair of the connection. + :param duration: The execution time of the event in the highest possible + resolution for the platform. + :param reply: The command reply. + :param awaited: True if the response was awaited. + """ + event = ServerHeartbeatFailedEvent(duration, reply, connection_id, awaited) + for subscriber in self.__server_heartbeat_listeners: + try: + subscriber.failed(event) + except Exception: + _handle_exception() + + def publish_server_opened(self, server_address: _Address, topology_id: ObjectId) -> None: + """Publish a ServerOpeningEvent to all server listeners. + + :param server_address: The address (host, port) pair of the server. + :param topology_id: A unique identifier for the topology this server + is a part of. + """ + event = ServerOpeningEvent(server_address, topology_id) + for subscriber in self.__server_listeners: + try: + subscriber.opened(event) + except Exception: + _handle_exception() + + def publish_server_closed(self, server_address: _Address, topology_id: ObjectId) -> None: + """Publish a ServerClosedEvent to all server listeners. + + :param server_address: The address (host, port) pair of the server. + :param topology_id: A unique identifier for the topology this server + is a part of. + """ + event = ServerClosedEvent(server_address, topology_id) + for subscriber in self.__server_listeners: + try: + subscriber.closed(event) + except Exception: + _handle_exception() + + def publish_server_description_changed( + self, + previous_description: ServerDescription, + new_description: ServerDescription, + server_address: _Address, + topology_id: ObjectId, + ) -> None: + """Publish a ServerDescriptionChangedEvent to all server listeners. + + :param previous_description: The previous server description. + :param server_address: The address (host, port) pair of the server. + :param new_description: The new server description. + :param topology_id: A unique identifier for the topology this server + is a part of. + """ + event = ServerDescriptionChangedEvent( + previous_description, new_description, server_address, topology_id + ) + for subscriber in self.__server_listeners: + try: + subscriber.description_changed(event) + except Exception: + _handle_exception() + + def publish_topology_opened(self, topology_id: ObjectId) -> None: + """Publish a TopologyOpenedEvent to all topology listeners. + + :param topology_id: A unique identifier for the topology this server + is a part of. + """ + event = TopologyOpenedEvent(topology_id) + for subscriber in self.__topology_listeners: + try: + subscriber.opened(event) + except Exception: + _handle_exception() + + def publish_topology_closed(self, topology_id: ObjectId) -> None: + """Publish a TopologyClosedEvent to all topology listeners. + + :param topology_id: A unique identifier for the topology this server + is a part of. + """ + event = TopologyClosedEvent(topology_id) + for subscriber in self.__topology_listeners: + try: + subscriber.closed(event) + except Exception: + _handle_exception() + + def publish_topology_description_changed( + self, + previous_description: TopologyDescription, + new_description: TopologyDescription, + topology_id: ObjectId, + ) -> None: + """Publish a TopologyDescriptionChangedEvent to all topology listeners. + + :param previous_description: The previous topology description. + :param new_description: The new topology description. + :param topology_id: A unique identifier for the topology this server + is a part of. + """ + event = TopologyDescriptionChangedEvent(previous_description, new_description, topology_id) + for subscriber in self.__topology_listeners: + try: + subscriber.description_changed(event) + except Exception: + _handle_exception() + + def publish_pool_created(self, address: _Address, options: dict[str, Any]) -> None: + """Publish a :class:`PoolCreatedEvent` to all pool listeners.""" + event = PoolCreatedEvent(address, options) + for subscriber in self.__cmap_listeners: + try: + subscriber.pool_created(event) + except Exception: + _handle_exception() + + def publish_pool_ready(self, address: _Address) -> None: + """Publish a :class:`PoolReadyEvent` to all pool listeners.""" + event = PoolReadyEvent(address) + for subscriber in self.__cmap_listeners: + try: + subscriber.pool_ready(event) + except Exception: + _handle_exception() + + def publish_pool_cleared( + self, + address: _Address, + service_id: Optional[ObjectId], + interrupt_connections: bool = False, + ) -> None: + """Publish a :class:`PoolClearedEvent` to all pool listeners.""" + event = PoolClearedEvent(address, service_id, interrupt_connections) + for subscriber in self.__cmap_listeners: + try: + subscriber.pool_cleared(event) + except Exception: + _handle_exception() + + def publish_pool_closed(self, address: _Address) -> None: + """Publish a :class:`PoolClosedEvent` to all pool listeners.""" + event = PoolClosedEvent(address) + for subscriber in self.__cmap_listeners: + try: + subscriber.pool_closed(event) + except Exception: + _handle_exception() + + def publish_connection_created(self, address: _Address, connection_id: int) -> None: + """Publish a :class:`ConnectionCreatedEvent` to all connection + listeners. + """ + event = ConnectionCreatedEvent(address, connection_id) + for subscriber in self.__cmap_listeners: + try: + subscriber.connection_created(event) + except Exception: + _handle_exception() + + def publish_connection_ready( + self, address: _Address, connection_id: int, duration: float + ) -> None: + """Publish a :class:`ConnectionReadyEvent` to all connection listeners.""" + event = ConnectionReadyEvent(address, connection_id, duration) + for subscriber in self.__cmap_listeners: + try: + subscriber.connection_ready(event) + except Exception: + _handle_exception() + + def publish_connection_closed(self, address: _Address, connection_id: int, reason: str) -> None: + """Publish a :class:`ConnectionClosedEvent` to all connection + listeners. + """ + event = ConnectionClosedEvent(address, connection_id, reason) + for subscriber in self.__cmap_listeners: + try: + subscriber.connection_closed(event) + except Exception: + _handle_exception() + + def publish_connection_check_out_started(self, address: _Address) -> None: + """Publish a :class:`ConnectionCheckOutStartedEvent` to all connection + listeners. + """ + event = ConnectionCheckOutStartedEvent(address) + for subscriber in self.__cmap_listeners: + try: + subscriber.connection_check_out_started(event) + except Exception: + _handle_exception() + + def publish_connection_check_out_failed( + self, address: _Address, reason: str, duration: float + ) -> None: + """Publish a :class:`ConnectionCheckOutFailedEvent` to all connection + listeners. + """ + event = ConnectionCheckOutFailedEvent(address, reason, duration) + for subscriber in self.__cmap_listeners: + try: + subscriber.connection_check_out_failed(event) + except Exception: + _handle_exception() + + def publish_connection_checked_out( + self, address: _Address, connection_id: int, duration: float + ) -> None: + """Publish a :class:`ConnectionCheckedOutEvent` to all connection + listeners. + """ + event = ConnectionCheckedOutEvent(address, connection_id, duration) + for subscriber in self.__cmap_listeners: + try: + subscriber.connection_checked_out(event) + except Exception: + _handle_exception() + + def publish_connection_checked_in(self, address: _Address, connection_id: int) -> None: + """Publish a :class:`ConnectionCheckedInEvent` to all connection + listeners. + """ + event = ConnectionCheckedInEvent(address, connection_id) + for subscriber in self.__cmap_listeners: + try: + subscriber.connection_checked_in(event) + except Exception: + _handle_exception() diff --git a/venv/Lib/site-packages/pymongo/network.py b/venv/Lib/site-packages/pymongo/network.py new file mode 100644 index 00000000..76afbe13 --- /dev/null +++ b/venv/Lib/site-packages/pymongo/network.py @@ -0,0 +1,412 @@ +# Copyright 2015-present MongoDB, 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. + +"""Internal network layer helper methods.""" +from __future__ import annotations + +import datetime +import errno +import logging +import socket +import struct +import time +from typing import ( + TYPE_CHECKING, + Any, + Mapping, + MutableMapping, + Optional, + Sequence, + Union, + cast, +) + +from bson import _decode_all_selective +from pymongo import _csot, helpers, message, ssl_support +from pymongo.common import MAX_MESSAGE_SIZE +from pymongo.compression_support import _NO_COMPRESSION, decompress +from pymongo.errors import ( + NotPrimaryError, + OperationFailure, + ProtocolError, + _OperationCancelled, +) +from pymongo.logger import _COMMAND_LOGGER, _CommandStatusMessage, _debug_log +from pymongo.message import _UNPACK_REPLY, _OpMsg, _OpReply +from pymongo.monitoring import _is_speculative_authenticate +from pymongo.socket_checker import _errno_from_exception + +if TYPE_CHECKING: + from bson import CodecOptions + from pymongo.client_session import ClientSession + from pymongo.compression_support import SnappyContext, ZlibContext, ZstdContext + from pymongo.mongo_client import MongoClient + from pymongo.monitoring import _EventListeners + from pymongo.pool import Connection + from pymongo.read_concern import ReadConcern + from pymongo.read_preferences import _ServerMode + from pymongo.typings import _Address, _CollationIn, _DocumentOut, _DocumentType + from pymongo.write_concern import WriteConcern + +_UNPACK_HEADER = struct.Struct(" _DocumentType: + """Execute a command over the socket, or raise socket.error. + + :param conn: a Connection instance + :param dbname: name of the database on which to run the command + :param spec: a command document as an ordered dict type, eg SON. + :param is_mongos: are we connected to a mongos? + :param read_preference: a read preference + :param codec_options: a CodecOptions instance + :param session: optional ClientSession instance. + :param client: optional MongoClient instance for updating $clusterTime. + :param check: raise OperationFailure if there are errors + :param allowable_errors: errors to ignore if `check` is True + :param address: the (host, port) of `conn` + :param listeners: An instance of :class:`~pymongo.monitoring.EventListeners` + :param max_bson_size: The maximum encoded bson size for this server + :param read_concern: The read concern for this command. + :param parse_write_concern_error: Whether to parse the ``writeConcernError`` + field in the command response. + :param collation: The collation for this command. + :param compression_ctx: optional compression Context. + :param use_op_msg: True if we should use OP_MSG. + :param unacknowledged: True if this is an unacknowledged command. + :param user_fields: Response fields that should be decoded + using the TypeDecoders from codec_options, passed to + bson._decode_all_selective. + :param exhaust_allowed: True if we should enable OP_MSG exhaustAllowed. + """ + name = next(iter(spec)) + ns = dbname + ".$cmd" + speculative_hello = False + + # Publish the original command document, perhaps with lsid and $clusterTime. + orig = spec + if is_mongos and not use_op_msg: + assert read_preference is not None + spec = message._maybe_add_read_preference(spec, read_preference) + if read_concern and not (session and session.in_transaction): + if read_concern.level: + spec["readConcern"] = read_concern.document + if session: + session._update_read_concern(spec, conn) + if collation is not None: + spec["collation"] = collation + + publish = listeners is not None and listeners.enabled_for_commands + start = datetime.datetime.now() + if publish: + speculative_hello = _is_speculative_authenticate(name, spec) + + if compression_ctx and name.lower() in _NO_COMPRESSION: + compression_ctx = None + + if client and client._encrypter and not client._encrypter._bypass_auto_encryption: + spec = orig = client._encrypter.encrypt(dbname, spec, codec_options) + + # Support CSOT + if client: + conn.apply_timeout(client, spec) + _csot.apply_write_concern(spec, write_concern) + + if use_op_msg: + flags = _OpMsg.MORE_TO_COME if unacknowledged else 0 + flags |= _OpMsg.EXHAUST_ALLOWED if exhaust_allowed else 0 + request_id, msg, size, max_doc_size = message._op_msg( + flags, spec, dbname, read_preference, codec_options, ctx=compression_ctx + ) + # If this is an unacknowledged write then make sure the encoded doc(s) + # are small enough, otherwise rely on the server to return an error. + if unacknowledged and max_bson_size is not None and max_doc_size > max_bson_size: + message._raise_document_too_large(name, size, max_bson_size) + else: + request_id, msg, size = message._query( + 0, ns, 0, -1, spec, None, codec_options, compression_ctx + ) + + if max_bson_size is not None and size > max_bson_size + message._COMMAND_OVERHEAD: + message._raise_document_too_large(name, size, max_bson_size + message._COMMAND_OVERHEAD) + if client is not None: + if _COMMAND_LOGGER.isEnabledFor(logging.DEBUG): + _debug_log( + _COMMAND_LOGGER, + clientId=client._topology_settings._topology_id, + message=_CommandStatusMessage.STARTED, + command=spec, + commandName=next(iter(spec)), + databaseName=dbname, + requestId=request_id, + operationId=request_id, + driverConnectionId=conn.id, + serverConnectionId=conn.server_connection_id, + serverHost=conn.address[0], + serverPort=conn.address[1], + serviceId=conn.service_id, + ) + if publish: + assert listeners is not None + assert address is not None + listeners.publish_command_start( + orig, + dbname, + request_id, + address, + conn.server_connection_id, + service_id=conn.service_id, + ) + + try: + conn.conn.sendall(msg) + if use_op_msg and unacknowledged: + # Unacknowledged, fake a successful command response. + reply = None + response_doc: _DocumentOut = {"ok": 1} + else: + reply = receive_message(conn, request_id) + conn.more_to_come = reply.more_to_come + unpacked_docs = reply.unpack_response( + codec_options=codec_options, user_fields=user_fields + ) + + response_doc = unpacked_docs[0] + if client: + client._process_response(response_doc, session) + if check: + helpers._check_command_response( + response_doc, + conn.max_wire_version, + allowable_errors, + parse_write_concern_error=parse_write_concern_error, + ) + except Exception as exc: + duration = datetime.datetime.now() - start + if isinstance(exc, (NotPrimaryError, OperationFailure)): + failure: _DocumentOut = exc.details # type: ignore[assignment] + else: + failure = message._convert_exception(exc) + if client is not None: + if _COMMAND_LOGGER.isEnabledFor(logging.DEBUG): + _debug_log( + _COMMAND_LOGGER, + clientId=client._topology_settings._topology_id, + message=_CommandStatusMessage.FAILED, + durationMS=duration, + failure=failure, + commandName=next(iter(spec)), + databaseName=dbname, + requestId=request_id, + operationId=request_id, + driverConnectionId=conn.id, + serverConnectionId=conn.server_connection_id, + serverHost=conn.address[0], + serverPort=conn.address[1], + serviceId=conn.service_id, + isServerSideError=isinstance(exc, OperationFailure), + ) + if publish: + assert listeners is not None + assert address is not None + listeners.publish_command_failure( + duration, + failure, + name, + request_id, + address, + conn.server_connection_id, + service_id=conn.service_id, + database_name=dbname, + ) + raise + duration = datetime.datetime.now() - start + if client is not None: + if _COMMAND_LOGGER.isEnabledFor(logging.DEBUG): + _debug_log( + _COMMAND_LOGGER, + clientId=client._topology_settings._topology_id, + message=_CommandStatusMessage.SUCCEEDED, + durationMS=duration, + reply=response_doc, + commandName=next(iter(spec)), + databaseName=dbname, + requestId=request_id, + operationId=request_id, + driverConnectionId=conn.id, + serverConnectionId=conn.server_connection_id, + serverHost=conn.address[0], + serverPort=conn.address[1], + serviceId=conn.service_id, + speculative_authenticate="speculativeAuthenticate" in orig, + ) + if publish: + assert listeners is not None + assert address is not None + listeners.publish_command_success( + duration, + response_doc, + name, + request_id, + address, + conn.server_connection_id, + service_id=conn.service_id, + speculative_hello=speculative_hello, + database_name=dbname, + ) + + if client and client._encrypter and reply: + decrypted = client._encrypter.decrypt(reply.raw_command_response()) + response_doc = cast( + "_DocumentOut", _decode_all_selective(decrypted, codec_options, user_fields)[0] + ) + + return response_doc # type: ignore[return-value] + + +_UNPACK_COMPRESSION_HEADER = struct.Struct(" Union[_OpReply, _OpMsg]: + """Receive a raw BSON message or raise socket.error.""" + if _csot.get_timeout(): + deadline = _csot.get_deadline() + else: + timeout = conn.conn.gettimeout() + if timeout: + deadline = time.monotonic() + timeout + else: + deadline = None + # Ignore the response's request id. + length, _, response_to, op_code = _UNPACK_HEADER(_receive_data_on_socket(conn, 16, deadline)) + # No request_id for exhaust cursor "getMore". + if request_id is not None: + if request_id != response_to: + raise ProtocolError(f"Got response id {response_to!r} but expected {request_id!r}") + if length <= 16: + raise ProtocolError( + f"Message length ({length!r}) not longer than standard message header size (16)" + ) + if length > max_message_size: + raise ProtocolError( + f"Message length ({length!r}) is larger than server max " + f"message size ({max_message_size!r})" + ) + if op_code == 2012: + op_code, _, compressor_id = _UNPACK_COMPRESSION_HEADER( + _receive_data_on_socket(conn, 9, deadline) + ) + data = decompress(_receive_data_on_socket(conn, length - 25, deadline), compressor_id) + else: + data = _receive_data_on_socket(conn, length - 16, deadline) + + try: + unpack_reply = _UNPACK_REPLY[op_code] + except KeyError: + raise ProtocolError( + f"Got opcode {op_code!r} but expected {_UNPACK_REPLY.keys()!r}" + ) from None + return unpack_reply(data) + + +_POLL_TIMEOUT = 0.5 + + +def wait_for_read(conn: Connection, deadline: Optional[float]) -> None: + """Block until at least one byte is read, or a timeout, or a cancel.""" + sock = conn.conn + timed_out = False + # Check if the connection's socket has been manually closed + if sock.fileno() == -1: + return + while True: + # SSLSocket can have buffered data which won't be caught by select. + if hasattr(sock, "pending") and sock.pending() > 0: + readable = True + else: + # Wait up to 500ms for the socket to become readable and then + # check for cancellation. + if deadline: + remaining = deadline - time.monotonic() + # When the timeout has expired perform one final check to + # see if the socket is readable. This helps avoid spurious + # timeouts on AWS Lambda and other FaaS environments. + if remaining <= 0: + timed_out = True + timeout = max(min(remaining, _POLL_TIMEOUT), 0) + else: + timeout = _POLL_TIMEOUT + readable = conn.socket_checker.select(sock, read=True, timeout=timeout) + if conn.cancel_context.cancelled: + raise _OperationCancelled("operation cancelled") + if readable: + return + if timed_out: + raise socket.timeout("timed out") + + +# Errors raised by sockets (and TLS sockets) when in non-blocking mode. +BLOCKING_IO_ERRORS = (BlockingIOError, *ssl_support.BLOCKING_IO_ERRORS) + + +def _receive_data_on_socket(conn: Connection, length: int, deadline: Optional[float]) -> memoryview: + buf = bytearray(length) + mv = memoryview(buf) + bytes_read = 0 + while bytes_read < length: + try: + wait_for_read(conn, deadline) + # CSOT: Update timeout. When the timeout has expired perform one + # final non-blocking recv. This helps avoid spurious timeouts when + # the response is actually already buffered on the client. + if _csot.get_timeout() and deadline is not None: + conn.set_conn_timeout(max(deadline - time.monotonic(), 0)) + chunk_length = conn.conn.recv_into(mv[bytes_read:]) + except BLOCKING_IO_ERRORS: + raise socket.timeout("timed out") from None + except OSError as exc: + if _errno_from_exception(exc) == errno.EINTR: + continue + raise + if chunk_length == 0: + raise OSError("connection closed") + + bytes_read += chunk_length + + return mv diff --git a/venv/Lib/site-packages/pymongo/ocsp_cache.py b/venv/Lib/site-packages/pymongo/ocsp_cache.py new file mode 100644 index 00000000..74257931 --- /dev/null +++ b/venv/Lib/site-packages/pymongo/ocsp_cache.py @@ -0,0 +1,108 @@ +# Copyright 2020-present MongoDB, 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. + +"""Utilities for caching OCSP responses.""" + +from __future__ import annotations + +from collections import namedtuple +from datetime import datetime as _datetime +from datetime import timezone +from typing import TYPE_CHECKING, Any + +from pymongo.lock import _create_lock + +if TYPE_CHECKING: + from cryptography.x509.ocsp import OCSPRequest, OCSPResponse + + +class _OCSPCache: + """A cache for OCSP responses.""" + + CACHE_KEY_TYPE = namedtuple( # type: ignore + "OcspResponseCacheKey", + ["hash_algorithm", "issuer_name_hash", "issuer_key_hash", "serial_number"], + ) + + def __init__(self) -> None: + self._data: dict[Any, OCSPResponse] = {} + # Hold this lock when accessing _data. + self._lock = _create_lock() + + def _get_cache_key(self, ocsp_request: OCSPRequest) -> CACHE_KEY_TYPE: + return self.CACHE_KEY_TYPE( + hash_algorithm=ocsp_request.hash_algorithm.name.lower(), + issuer_name_hash=ocsp_request.issuer_name_hash, + issuer_key_hash=ocsp_request.issuer_key_hash, + serial_number=ocsp_request.serial_number, + ) + + def __setitem__(self, key: OCSPRequest, value: OCSPResponse) -> None: + """Add/update a cache entry. + + 'key' is of type cryptography.x509.ocsp.OCSPRequest + 'value' is of type cryptography.x509.ocsp.OCSPResponse + + Validity of the OCSP response must be checked by caller. + """ + with self._lock: + cache_key = self._get_cache_key(key) + + # As per the OCSP protocol, if the response's nextUpdate field is + # not set, the responder is indicating that newer revocation + # information is available all the time. + if value.next_update is None: + self._data.pop(cache_key, None) + return + + # Do nothing if the response is invalid. + if not ( + value.this_update + <= _datetime.now(tz=timezone.utc).replace(tzinfo=None) + < value.next_update + ): + return + + # Cache new response OR update cached response if new response + # has longer validity. + cached_value = self._data.get(cache_key, None) + if cached_value is None or ( + cached_value.next_update is not None + and cached_value.next_update < value.next_update + ): + self._data[cache_key] = value + + def __getitem__(self, item: OCSPRequest) -> OCSPResponse: + """Get a cache entry if it exists. + + 'item' is of type cryptography.x509.ocsp.OCSPRequest + + Raises KeyError if the item is not in the cache. + """ + with self._lock: + cache_key = self._get_cache_key(item) + value = self._data[cache_key] + + # Return cached response if it is still valid. + assert value.this_update is not None + assert value.next_update is not None + if ( + value.this_update + <= _datetime.now(tz=timezone.utc).replace(tzinfo=None) + < value.next_update + ): + return value + + self._data.pop(cache_key, None) + raise KeyError(cache_key) diff --git a/venv/Lib/site-packages/pymongo/ocsp_support.py b/venv/Lib/site-packages/pymongo/ocsp_support.py new file mode 100644 index 00000000..1bda3b4d --- /dev/null +++ b/venv/Lib/site-packages/pymongo/ocsp_support.py @@ -0,0 +1,432 @@ +# Copyright 2020-present MongoDB, 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. + +"""Support for requesting and verifying OCSP responses.""" +from __future__ import annotations + +import logging as _logging +import re as _re +from datetime import datetime as _datetime +from datetime import timezone +from typing import TYPE_CHECKING, Iterable, Optional, Type, Union + +from cryptography.exceptions import InvalidSignature as _InvalidSignature +from cryptography.hazmat.backends import default_backend as _default_backend +from cryptography.hazmat.primitives.asymmetric.dsa import DSAPublicKey as _DSAPublicKey +from cryptography.hazmat.primitives.asymmetric.ec import ECDSA as _ECDSA +from cryptography.hazmat.primitives.asymmetric.ec import ( + EllipticCurvePublicKey as _EllipticCurvePublicKey, +) +from cryptography.hazmat.primitives.asymmetric.padding import PKCS1v15 as _PKCS1v15 +from cryptography.hazmat.primitives.asymmetric.rsa import RSAPublicKey as _RSAPublicKey +from cryptography.hazmat.primitives.asymmetric.x448 import ( + X448PublicKey as _X448PublicKey, +) +from cryptography.hazmat.primitives.asymmetric.x25519 import ( + X25519PublicKey as _X25519PublicKey, +) +from cryptography.hazmat.primitives.hashes import SHA1 as _SHA1 +from cryptography.hazmat.primitives.hashes import Hash as _Hash +from cryptography.hazmat.primitives.serialization import Encoding as _Encoding +from cryptography.hazmat.primitives.serialization import PublicFormat as _PublicFormat +from cryptography.x509 import AuthorityInformationAccess as _AuthorityInformationAccess +from cryptography.x509 import ExtendedKeyUsage as _ExtendedKeyUsage +from cryptography.x509 import ExtensionNotFound as _ExtensionNotFound +from cryptography.x509 import TLSFeature as _TLSFeature +from cryptography.x509 import TLSFeatureType as _TLSFeatureType +from cryptography.x509 import load_pem_x509_certificate as _load_pem_x509_certificate +from cryptography.x509.ocsp import OCSPCertStatus as _OCSPCertStatus +from cryptography.x509.ocsp import OCSPRequestBuilder as _OCSPRequestBuilder +from cryptography.x509.ocsp import OCSPResponseStatus as _OCSPResponseStatus +from cryptography.x509.ocsp import load_der_ocsp_response as _load_der_ocsp_response +from cryptography.x509.oid import ( + AuthorityInformationAccessOID as _AuthorityInformationAccessOID, +) +from cryptography.x509.oid import ExtendedKeyUsageOID as _ExtendedKeyUsageOID +from requests import post as _post +from requests.exceptions import RequestException as _RequestException + +from pymongo import _csot + +if TYPE_CHECKING: + from cryptography.hazmat.primitives.asymmetric import ( + dsa, + ec, + ed448, + ed25519, + rsa, + x448, + x25519, + ) + from cryptography.hazmat.primitives.asymmetric.utils import Prehashed + from cryptography.hazmat.primitives.hashes import HashAlgorithm + from cryptography.x509 import Certificate, Name + from cryptography.x509.extensions import Extension, ExtensionTypeVar + from cryptography.x509.ocsp import OCSPRequest, OCSPResponse + from OpenSSL.SSL import Connection + + from pymongo.ocsp_cache import _OCSPCache + from pymongo.pyopenssl_context import _CallbackData + + CertificateIssuerPublicKeyTypes = Union[ + dsa.DSAPublicKey, + rsa.RSAPublicKey, + ec.EllipticCurvePublicKey, + ed25519.Ed25519PublicKey, + ed448.Ed448PublicKey, + x25519.X25519PublicKey, + x448.X448PublicKey, + ] + +# Note: the functions in this module generally return 1 or 0. The reason +# is simple. The entry point, ocsp_callback, is registered as a callback +# with OpenSSL through PyOpenSSL. The callback must return 1 (success) or +# 0 (failure). + +_LOGGER = _logging.getLogger(__name__) + +_CERT_REGEX = _re.compile( + b"-----BEGIN CERTIFICATE[^\r\n]+.+?-----END CERTIFICATE[^\r\n]+", _re.DOTALL +) + + +def _load_trusted_ca_certs(cafile: str) -> list[Certificate]: + """Parse the tlsCAFile into a list of certificates.""" + with open(cafile, "rb") as f: + data = f.read() + + # Load all the certs in the file. + trusted_ca_certs = [] + backend = _default_backend() + for cert_data in _re.findall(_CERT_REGEX, data): + trusted_ca_certs.append(_load_pem_x509_certificate(cert_data, backend)) + return trusted_ca_certs + + +def _get_issuer_cert( + cert: Certificate, chain: Iterable[Certificate], trusted_ca_certs: Optional[list[Certificate]] +) -> Optional[Certificate]: + issuer_name = cert.issuer + for candidate in chain: + if candidate.subject == issuer_name: + return candidate + + # Depending on the server's TLS library, the peer's cert chain may not + # include the self signed root CA. In this case we check the user + # provided tlsCAFile for the issuer. + # Remove once we use the verified peer cert chain in PYTHON-2147. + if trusted_ca_certs: + for candidate in trusted_ca_certs: + if candidate.subject == issuer_name: + return candidate + return None + + +def _verify_signature( + key: CertificateIssuerPublicKeyTypes, + signature: bytes, + algorithm: Union[Prehashed, HashAlgorithm, None], + data: bytes, +) -> int: + # See cryptography.x509.Certificate.public_key + # for the public key types. + try: + if isinstance(key, _RSAPublicKey): + key.verify(signature, data, _PKCS1v15(), algorithm) # type: ignore[arg-type] + elif isinstance(key, _DSAPublicKey): + key.verify(signature, data, algorithm) # type: ignore[arg-type] + elif isinstance(key, _EllipticCurvePublicKey): + key.verify(signature, data, _ECDSA(algorithm)) # type: ignore[arg-type] + elif isinstance( + key, (_X25519PublicKey, _X448PublicKey) + ): # Curve25519 and Curve448 keys do not require verification + return 1 + else: + key.verify(signature, data) + except _InvalidSignature: + return 0 + return 1 + + +def _get_extension( + cert: Certificate, klass: Type[ExtensionTypeVar] +) -> Optional[Extension[ExtensionTypeVar]]: + try: + return cert.extensions.get_extension_for_class(klass) + except _ExtensionNotFound: + return None + + +def _public_key_hash(cert: Certificate) -> bytes: + public_key = cert.public_key() + # https://tools.ietf.org/html/rfc2560#section-4.2.1 + # "KeyHash ::= OCTET STRING -- SHA-1 hash of responder's public key + # (excluding the tag and length fields)" + # https://stackoverflow.com/a/46309453/600498 + if isinstance(public_key, _RSAPublicKey): + pbytes = public_key.public_bytes(_Encoding.DER, _PublicFormat.PKCS1) + elif isinstance(public_key, _EllipticCurvePublicKey): + pbytes = public_key.public_bytes(_Encoding.X962, _PublicFormat.UncompressedPoint) + else: + pbytes = public_key.public_bytes(_Encoding.DER, _PublicFormat.SubjectPublicKeyInfo) + digest = _Hash(_SHA1(), backend=_default_backend()) # noqa: S303 + digest.update(pbytes) + return digest.finalize() + + +def _get_certs_by_key_hash( + certificates: Iterable[Certificate], issuer: Certificate, responder_key_hash: Optional[bytes] +) -> list[Certificate]: + return [ + cert + for cert in certificates + if _public_key_hash(cert) == responder_key_hash and cert.issuer == issuer.subject + ] + + +def _get_certs_by_name( + certificates: Iterable[Certificate], issuer: Certificate, responder_name: Optional[Name] +) -> list[Certificate]: + return [ + cert + for cert in certificates + if cert.subject == responder_name and cert.issuer == issuer.subject + ] + + +def _verify_response_signature(issuer: Certificate, response: OCSPResponse) -> int: + # Response object will have a responder_name or responder_key_hash + # not both. + name = response.responder_name + rkey_hash = response.responder_key_hash + ikey_hash = response.issuer_key_hash + if name is not None and name == issuer.subject or rkey_hash == ikey_hash: + _LOGGER.debug("Responder is issuer") + # Responder is the issuer + responder_cert = issuer + else: + _LOGGER.debug("Responder is a delegate") + # Responder is a delegate + # https://tools.ietf.org/html/rfc6960#section-2.6 + # RFC6960, Section 3.2, Number 3 + certs = response.certificates + if response.responder_name is not None: + responder_certs = _get_certs_by_name(certs, issuer, name) + _LOGGER.debug("Using responder name") + else: + responder_certs = _get_certs_by_key_hash(certs, issuer, rkey_hash) + _LOGGER.debug("Using key hash") + if not responder_certs: + _LOGGER.debug("No matching or valid responder certs.") + return 0 + # XXX: Can there be more than one? If so, should we try each one + # until we find one that passes signature verification? + responder_cert = responder_certs[0] + + # RFC6960, Section 3.2, Number 4 + ext = _get_extension(responder_cert, _ExtendedKeyUsage) + if not ext or _ExtendedKeyUsageOID.OCSP_SIGNING not in ext.value: + _LOGGER.debug("Delegate not authorized for OCSP signing") + return 0 + if not _verify_signature( + issuer.public_key(), + responder_cert.signature, + responder_cert.signature_hash_algorithm, + responder_cert.tbs_certificate_bytes, + ): + _LOGGER.debug("Delegate signature verification failed") + return 0 + # RFC6960, Section 3.2, Number 2 + ret = _verify_signature( + responder_cert.public_key(), + response.signature, + response.signature_hash_algorithm, + response.tbs_response_bytes, + ) + if not ret: + _LOGGER.debug("Response signature verification failed") + return ret + + +def _build_ocsp_request(cert: Certificate, issuer: Certificate) -> OCSPRequest: + # https://cryptography.io/en/latest/x509/ocsp/#creating-requests + builder = _OCSPRequestBuilder() + builder = builder.add_certificate(cert, issuer, _SHA1()) # noqa: S303 + return builder.build() + + +def _verify_response(issuer: Certificate, response: OCSPResponse) -> int: + _LOGGER.debug("Verifying response") + # RFC6960, Section 3.2, Number 2, 3 and 4 happen here. + res = _verify_response_signature(issuer, response) + if not res: + return 0 + + # Note that we are not using a "tolerance period" as discussed in + # https://tools.ietf.org/rfc/rfc5019.txt? + now = _datetime.now(tz=timezone.utc).replace(tzinfo=None) + # RFC6960, Section 3.2, Number 5 + if response.this_update > now: + _LOGGER.debug("thisUpdate is in the future") + return 0 + # RFC6960, Section 3.2, Number 6 + if response.next_update and response.next_update < now: + _LOGGER.debug("nextUpdate is in the past") + return 0 + return 1 + + +def _get_ocsp_response( + cert: Certificate, issuer: Certificate, uri: Union[str, bytes], ocsp_response_cache: _OCSPCache +) -> Optional[OCSPResponse]: + ocsp_request = _build_ocsp_request(cert, issuer) + try: + ocsp_response = ocsp_response_cache[ocsp_request] + _LOGGER.debug("Using cached OCSP response.") + except KeyError: + # CSOT: use the configured timeout or 5 seconds, whichever is smaller. + # Note that request's timeout works differently and does not imply an absolute + # deadline: https://requests.readthedocs.io/en/stable/user/quickstart/#timeouts + timeout = max(_csot.clamp_remaining(5), 0.001) + try: + response = _post( + uri, + data=ocsp_request.public_bytes(_Encoding.DER), + headers={"Content-Type": "application/ocsp-request"}, + timeout=timeout, + ) + except _RequestException as exc: + _LOGGER.debug("HTTP request failed: %s", exc) + return None + if response.status_code != 200: + _LOGGER.debug("HTTP request returned %d", response.status_code) + return None + ocsp_response = _load_der_ocsp_response(response.content) + _LOGGER.debug("OCSP response status: %r", ocsp_response.response_status) + if ocsp_response.response_status != _OCSPResponseStatus.SUCCESSFUL: + return None + # RFC6960, Section 3.2, Number 1. Only relevant if we need to + # talk to the responder directly. + # Accessing response.serial_number raises if response status is not + # SUCCESSFUL. + if ocsp_response.serial_number != ocsp_request.serial_number: + _LOGGER.debug("Response serial number does not match request") + return None + if not _verify_response(issuer, ocsp_response): + # The response failed verification. + return None + _LOGGER.debug("Caching OCSP response.") + ocsp_response_cache[ocsp_request] = ocsp_response + + return ocsp_response + + +def _ocsp_callback(conn: Connection, ocsp_bytes: bytes, user_data: Optional[_CallbackData]) -> bool: + """Callback for use with OpenSSL.SSL.Context.set_ocsp_client_callback.""" + # always pass in user_data but OpenSSL requires it be optional + assert user_data + pycert = conn.get_peer_certificate() + if pycert is None: + _LOGGER.debug("No peer cert?") + return False + cert = pycert.to_cryptography() + # Use the verified chain when available (pyopenssl>=20.0). + if hasattr(conn, "get_verified_chain"): + pychain = conn.get_verified_chain() + trusted_ca_certs = None + else: + pychain = conn.get_peer_cert_chain() + trusted_ca_certs = user_data.trusted_ca_certs + if not pychain: + _LOGGER.debug("No peer cert chain?") + return False + chain = [cer.to_cryptography() for cer in pychain] + issuer = _get_issuer_cert(cert, chain, trusted_ca_certs) + must_staple = False + # https://tools.ietf.org/html/rfc7633#section-4.2.3.1 + ext_tls = _get_extension(cert, _TLSFeature) + if ext_tls is not None: + for feature in ext_tls.value: + if feature == _TLSFeatureType.status_request: + _LOGGER.debug("Peer presented a must-staple cert") + must_staple = True + break + ocsp_response_cache = user_data.ocsp_response_cache + + # No stapled OCSP response + if ocsp_bytes == b"": + _LOGGER.debug("Peer did not staple an OCSP response") + if must_staple: + _LOGGER.debug("Must-staple cert with no stapled response, hard fail.") + return False + if not user_data.check_ocsp_endpoint: + _LOGGER.debug("OCSP endpoint checking is disabled, soft fail.") + # No stapled OCSP response, checking responder URI disabled, soft fail. + return True + # https://tools.ietf.org/html/rfc6960#section-3.1 + ext_aia = _get_extension(cert, _AuthorityInformationAccess) + if ext_aia is None: + _LOGGER.debug("No authority access information, soft fail") + # No stapled OCSP response, no responder URI, soft fail. + return True + uris = [ + desc.access_location.value + for desc in ext_aia.value + if desc.access_method == _AuthorityInformationAccessOID.OCSP + ] + if not uris: + _LOGGER.debug("No OCSP URI, soft fail") + # No responder URI, soft fail. + return True + if issuer is None: + _LOGGER.debug("No issuer cert?") + return False + _LOGGER.debug("Requesting OCSP data") + # When requesting data from an OCSP endpoint we only fail on + # successful, valid responses with a certificate status of REVOKED. + for uri in uris: + _LOGGER.debug("Trying %s", uri) + response = _get_ocsp_response(cert, issuer, uri, ocsp_response_cache) + if response is None: + # The endpoint didn't respond in time, or the response was + # unsuccessful or didn't match the request, or the response + # failed verification. + continue + _LOGGER.debug("OCSP cert status: %r", response.certificate_status) + if response.certificate_status == _OCSPCertStatus.GOOD: + return True + if response.certificate_status == _OCSPCertStatus.REVOKED: + return False + # Soft fail if we couldn't get a definitive status. + _LOGGER.debug("No definitive OCSP cert status, soft fail") + return True + + _LOGGER.debug("Peer stapled an OCSP response") + if issuer is None: + _LOGGER.debug("No issuer cert?") + return False + response = _load_der_ocsp_response(ocsp_bytes) + _LOGGER.debug("OCSP response status: %r", response.response_status) + # This happens in _request_ocsp when there is no stapled response so + # we know if we can compare serial numbers for the request and response. + if response.response_status != _OCSPResponseStatus.SUCCESSFUL: + return False + if not _verify_response(issuer, response): + return False + # Cache the verified, stapled response. + ocsp_response_cache[_build_ocsp_request(cert, issuer)] = response + _LOGGER.debug("OCSP cert status: %r", response.certificate_status) + if response.certificate_status == _OCSPCertStatus.REVOKED: + return False + return True diff --git a/venv/Lib/site-packages/pymongo/operations.py b/venv/Lib/site-packages/pymongo/operations.py new file mode 100644 index 00000000..4872afa9 --- /dev/null +++ b/venv/Lib/site-packages/pymongo/operations.py @@ -0,0 +1,623 @@ +# Copyright 2015-present MongoDB, 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. + +"""Operation class definitions.""" +from __future__ import annotations + +import enum +from typing import ( + TYPE_CHECKING, + Any, + Generic, + Mapping, + Optional, + Sequence, + Tuple, + Union, +) + +from bson.raw_bson import RawBSONDocument +from pymongo import helpers +from pymongo.collation import validate_collation_or_none +from pymongo.common import validate_is_mapping, validate_list +from pymongo.helpers import _gen_index_name, _index_document, _index_list +from pymongo.typings import _CollationIn, _DocumentType, _Pipeline +from pymongo.write_concern import validate_boolean + +if TYPE_CHECKING: + from pymongo.bulk import _Bulk + +# Hint supports index name, "myIndex", a list of either strings or index pairs: [('x', 1), ('y', -1), 'z''], or a dictionary +_IndexList = Union[ + Sequence[Union[str, Tuple[str, Union[int, str, Mapping[str, Any]]]]], Mapping[str, Any] +] +_IndexKeyHint = Union[str, _IndexList] + + +class _Op(str, enum.Enum): + ABORT = "abortTransaction" + AGGREGATE = "aggregate" + COMMIT = "commitTransaction" + COUNT = "count" + CREATE = "create" + CREATE_INDEXES = "createIndexes" + CREATE_SEARCH_INDEXES = "createSearchIndexes" + DELETE = "delete" + DISTINCT = "distinct" + DROP = "drop" + DROP_DATABASE = "dropDatabase" + DROP_INDEXES = "dropIndexes" + DROP_SEARCH_INDEXES = "dropSearchIndexes" + END_SESSIONS = "endSessions" + FIND_AND_MODIFY = "findAndModify" + FIND = "find" + INSERT = "insert" + LIST_COLLECTIONS = "listCollections" + LIST_INDEXES = "listIndexes" + LIST_SEARCH_INDEX = "listSearchIndexes" + LIST_DATABASES = "listDatabases" + UPDATE = "update" + UPDATE_INDEX = "updateIndex" + UPDATE_SEARCH_INDEX = "updateSearchIndex" + RENAME = "rename" + GETMORE = "getMore" + KILL_CURSORS = "killCursors" + TEST = "testOperation" + + +class InsertOne(Generic[_DocumentType]): + """Represents an insert_one operation.""" + + __slots__ = ("_doc",) + + def __init__(self, document: _DocumentType) -> None: + """Create an InsertOne instance. + + For use with :meth:`~pymongo.collection.Collection.bulk_write`. + + :param document: The document to insert. If the document is missing an + _id field one will be added. + """ + self._doc = document + + def _add_to_bulk(self, bulkobj: _Bulk) -> None: + """Add this operation to the _Bulk instance `bulkobj`.""" + bulkobj.add_insert(self._doc) # type: ignore[arg-type] + + def __repr__(self) -> str: + return f"InsertOne({self._doc!r})" + + def __eq__(self, other: Any) -> bool: + if type(other) == type(self): + return other._doc == self._doc + return NotImplemented + + def __ne__(self, other: Any) -> bool: + return not self == other + + +class DeleteOne: + """Represents a delete_one operation.""" + + __slots__ = ("_filter", "_collation", "_hint") + + def __init__( + self, + filter: Mapping[str, Any], + collation: Optional[_CollationIn] = None, + hint: Optional[_IndexKeyHint] = None, + ) -> None: + """Create a DeleteOne instance. + + For use with :meth:`~pymongo.collection.Collection.bulk_write`. + + :param filter: A query that matches the document to delete. + :param collation: An instance of + :class:`~pymongo.collation.Collation`. + :param hint: An index to use to support the query + predicate specified either by its string name, or in the same + format as passed to + :meth:`~pymongo.collection.Collection.create_index` (e.g. + ``[('field', ASCENDING)]``). This option is only supported on + MongoDB 4.4 and above. + + .. versionchanged:: 3.11 + Added the ``hint`` option. + .. versionchanged:: 3.5 + Added the `collation` option. + """ + if filter is not None: + validate_is_mapping("filter", filter) + if hint is not None and not isinstance(hint, str): + self._hint: Union[str, dict[str, Any], None] = helpers._index_document(hint) + else: + self._hint = hint + self._filter = filter + self._collation = collation + + def _add_to_bulk(self, bulkobj: _Bulk) -> None: + """Add this operation to the _Bulk instance `bulkobj`.""" + bulkobj.add_delete( + self._filter, + 1, + collation=validate_collation_or_none(self._collation), + hint=self._hint, + ) + + def __repr__(self) -> str: + return f"DeleteOne({self._filter!r}, {self._collation!r}, {self._hint!r})" + + def __eq__(self, other: Any) -> bool: + if type(other) == type(self): + return (other._filter, other._collation, other._hint) == ( + self._filter, + self._collation, + self._hint, + ) + return NotImplemented + + def __ne__(self, other: Any) -> bool: + return not self == other + + +class DeleteMany: + """Represents a delete_many operation.""" + + __slots__ = ("_filter", "_collation", "_hint") + + def __init__( + self, + filter: Mapping[str, Any], + collation: Optional[_CollationIn] = None, + hint: Optional[_IndexKeyHint] = None, + ) -> None: + """Create a DeleteMany instance. + + For use with :meth:`~pymongo.collection.Collection.bulk_write`. + + :param filter: A query that matches the documents to delete. + :param collation: An instance of + :class:`~pymongo.collation.Collation`. + :param hint: An index to use to support the query + predicate specified either by its string name, or in the same + format as passed to + :meth:`~pymongo.collection.Collection.create_index` (e.g. + ``[('field', ASCENDING)]``). This option is only supported on + MongoDB 4.4 and above. + + .. versionchanged:: 3.11 + Added the ``hint`` option. + .. versionchanged:: 3.5 + Added the `collation` option. + """ + if filter is not None: + validate_is_mapping("filter", filter) + if hint is not None and not isinstance(hint, str): + self._hint: Union[str, dict[str, Any], None] = helpers._index_document(hint) + else: + self._hint = hint + self._filter = filter + self._collation = collation + + def _add_to_bulk(self, bulkobj: _Bulk) -> None: + """Add this operation to the _Bulk instance `bulkobj`.""" + bulkobj.add_delete( + self._filter, + 0, + collation=validate_collation_or_none(self._collation), + hint=self._hint, + ) + + def __repr__(self) -> str: + return f"DeleteMany({self._filter!r}, {self._collation!r}, {self._hint!r})" + + def __eq__(self, other: Any) -> bool: + if type(other) == type(self): + return (other._filter, other._collation, other._hint) == ( + self._filter, + self._collation, + self._hint, + ) + return NotImplemented + + def __ne__(self, other: Any) -> bool: + return not self == other + + +class ReplaceOne(Generic[_DocumentType]): + """Represents a replace_one operation.""" + + __slots__ = ("_filter", "_doc", "_upsert", "_collation", "_hint") + + def __init__( + self, + filter: Mapping[str, Any], + replacement: Union[_DocumentType, RawBSONDocument], + upsert: bool = False, + collation: Optional[_CollationIn] = None, + hint: Optional[_IndexKeyHint] = None, + ) -> None: + """Create a ReplaceOne instance. + + For use with :meth:`~pymongo.collection.Collection.bulk_write`. + + :param filter: A query that matches the document to replace. + :param replacement: The new document. + :param upsert: If ``True``, perform an insert if no documents + match the filter. + :param collation: An instance of + :class:`~pymongo.collation.Collation`. + :param hint: An index to use to support the query + predicate specified either by its string name, or in the same + format as passed to + :meth:`~pymongo.collection.Collection.create_index` (e.g. + ``[('field', ASCENDING)]``). This option is only supported on + MongoDB 4.2 and above. + + .. versionchanged:: 3.11 + Added the ``hint`` option. + .. versionchanged:: 3.5 + Added the ``collation`` option. + """ + if filter is not None: + validate_is_mapping("filter", filter) + if upsert is not None: + validate_boolean("upsert", upsert) + if hint is not None and not isinstance(hint, str): + self._hint: Union[str, dict[str, Any], None] = helpers._index_document(hint) + else: + self._hint = hint + self._filter = filter + self._doc = replacement + self._upsert = upsert + self._collation = collation + + def _add_to_bulk(self, bulkobj: _Bulk) -> None: + """Add this operation to the _Bulk instance `bulkobj`.""" + bulkobj.add_replace( + self._filter, + self._doc, + self._upsert, + collation=validate_collation_or_none(self._collation), + hint=self._hint, + ) + + def __eq__(self, other: Any) -> bool: + if type(other) == type(self): + return ( + other._filter, + other._doc, + other._upsert, + other._collation, + other._hint, + ) == ( + self._filter, + self._doc, + self._upsert, + self._collation, + other._hint, + ) + return NotImplemented + + def __ne__(self, other: Any) -> bool: + return not self == other + + def __repr__(self) -> str: + return "{}({!r}, {!r}, {!r}, {!r}, {!r})".format( + self.__class__.__name__, + self._filter, + self._doc, + self._upsert, + self._collation, + self._hint, + ) + + +class _UpdateOp: + """Private base class for update operations.""" + + __slots__ = ("_filter", "_doc", "_upsert", "_collation", "_array_filters", "_hint") + + def __init__( + self, + filter: Mapping[str, Any], + doc: Union[Mapping[str, Any], _Pipeline], + upsert: bool, + collation: Optional[_CollationIn], + array_filters: Optional[list[Mapping[str, Any]]], + hint: Optional[_IndexKeyHint], + ): + if filter is not None: + validate_is_mapping("filter", filter) + if upsert is not None: + validate_boolean("upsert", upsert) + if array_filters is not None: + validate_list("array_filters", array_filters) + if hint is not None and not isinstance(hint, str): + self._hint: Union[str, dict[str, Any], None] = helpers._index_document(hint) + else: + self._hint = hint + + self._filter = filter + self._doc = doc + self._upsert = upsert + self._collation = collation + self._array_filters = array_filters + + def __eq__(self, other: object) -> bool: + if isinstance(other, type(self)): + return ( + other._filter, + other._doc, + other._upsert, + other._collation, + other._array_filters, + other._hint, + ) == ( + self._filter, + self._doc, + self._upsert, + self._collation, + self._array_filters, + self._hint, + ) + return NotImplemented + + def __repr__(self) -> str: + return "{}({!r}, {!r}, {!r}, {!r}, {!r}, {!r})".format( + self.__class__.__name__, + self._filter, + self._doc, + self._upsert, + self._collation, + self._array_filters, + self._hint, + ) + + +class UpdateOne(_UpdateOp): + """Represents an update_one operation.""" + + __slots__ = () + + def __init__( + self, + filter: Mapping[str, Any], + update: Union[Mapping[str, Any], _Pipeline], + upsert: bool = False, + collation: Optional[_CollationIn] = None, + array_filters: Optional[list[Mapping[str, Any]]] = None, + hint: Optional[_IndexKeyHint] = None, + ) -> None: + """Represents an update_one operation. + + For use with :meth:`~pymongo.collection.Collection.bulk_write`. + + :param filter: A query that matches the document to update. + :param update: The modifications to apply. + :param upsert: If ``True``, perform an insert if no documents + match the filter. + :param collation: An instance of + :class:`~pymongo.collation.Collation`. + :param array_filters: A list of filters specifying which + array elements an update should apply. + :param hint: An index to use to support the query + predicate specified either by its string name, or in the same + format as passed to + :meth:`~pymongo.collection.Collection.create_index` (e.g. + ``[('field', ASCENDING)]``). This option is only supported on + MongoDB 4.2 and above. + + .. versionchanged:: 3.11 + Added the `hint` option. + .. versionchanged:: 3.9 + Added the ability to accept a pipeline as the `update`. + .. versionchanged:: 3.6 + Added the `array_filters` option. + .. versionchanged:: 3.5 + Added the `collation` option. + """ + super().__init__(filter, update, upsert, collation, array_filters, hint) + + def _add_to_bulk(self, bulkobj: _Bulk) -> None: + """Add this operation to the _Bulk instance `bulkobj`.""" + bulkobj.add_update( + self._filter, + self._doc, + False, + self._upsert, + collation=validate_collation_or_none(self._collation), + array_filters=self._array_filters, + hint=self._hint, + ) + + +class UpdateMany(_UpdateOp): + """Represents an update_many operation.""" + + __slots__ = () + + def __init__( + self, + filter: Mapping[str, Any], + update: Union[Mapping[str, Any], _Pipeline], + upsert: bool = False, + collation: Optional[_CollationIn] = None, + array_filters: Optional[list[Mapping[str, Any]]] = None, + hint: Optional[_IndexKeyHint] = None, + ) -> None: + """Create an UpdateMany instance. + + For use with :meth:`~pymongo.collection.Collection.bulk_write`. + + :param filter: A query that matches the documents to update. + :param update: The modifications to apply. + :param upsert: If ``True``, perform an insert if no documents + match the filter. + :param collation: An instance of + :class:`~pymongo.collation.Collation`. + :param array_filters: A list of filters specifying which + array elements an update should apply. + :param hint: An index to use to support the query + predicate specified either by its string name, or in the same + format as passed to + :meth:`~pymongo.collection.Collection.create_index` (e.g. + ``[('field', ASCENDING)]``). This option is only supported on + MongoDB 4.2 and above. + + .. versionchanged:: 3.11 + Added the `hint` option. + .. versionchanged:: 3.9 + Added the ability to accept a pipeline as the `update`. + .. versionchanged:: 3.6 + Added the `array_filters` option. + .. versionchanged:: 3.5 + Added the `collation` option. + """ + super().__init__(filter, update, upsert, collation, array_filters, hint) + + def _add_to_bulk(self, bulkobj: _Bulk) -> None: + """Add this operation to the _Bulk instance `bulkobj`.""" + bulkobj.add_update( + self._filter, + self._doc, + True, + self._upsert, + collation=validate_collation_or_none(self._collation), + array_filters=self._array_filters, + hint=self._hint, + ) + + +class IndexModel: + """Represents an index to create.""" + + __slots__ = ("__document",) + + def __init__(self, keys: _IndexKeyHint, **kwargs: Any) -> None: + """Create an Index instance. + + For use with :meth:`~pymongo.collection.Collection.create_indexes`. + + Takes either a single key or a list containing (key, direction) pairs + or keys. If no direction is given, :data:`~pymongo.ASCENDING` will + be assumed. + The key(s) must be an instance of :class:`str`, and the direction(s) must + be one of (:data:`~pymongo.ASCENDING`, :data:`~pymongo.DESCENDING`, + :data:`~pymongo.GEO2D`, :data:`~pymongo.GEOSPHERE`, + :data:`~pymongo.HASHED`, :data:`~pymongo.TEXT`). + + Valid options include, but are not limited to: + + - `name`: custom name to use for this index - if none is + given, a name will be generated. + - `unique`: if ``True``, creates a uniqueness constraint on the index. + - `background`: if ``True``, this index should be created in the + background. + - `sparse`: if ``True``, omit from the index any documents that lack + the indexed field. + - `bucketSize`: for use with geoHaystack indexes. + Number of documents to group together within a certain proximity + to a given longitude and latitude. + - `min`: minimum value for keys in a :data:`~pymongo.GEO2D` + index. + - `max`: maximum value for keys in a :data:`~pymongo.GEO2D` + index. + - `expireAfterSeconds`: Used to create an expiring (TTL) + collection. MongoDB will automatically delete documents from + this collection after seconds. The indexed field must + be a UTC datetime or the data will not expire. + - `partialFilterExpression`: A document that specifies a filter for + a partial index. + - `collation`: An instance of :class:`~pymongo.collation.Collation` + that specifies the collation to use. + - `wildcardProjection`: Allows users to include or exclude specific + field paths from a `wildcard index`_ using the { "$**" : 1} key + pattern. Requires MongoDB >= 4.2. + - `hidden`: if ``True``, this index will be hidden from the query + planner and will not be evaluated as part of query plan + selection. Requires MongoDB >= 4.4. + + See the MongoDB documentation for a full list of supported options by + server version. + + :param keys: a single key or a list containing (key, direction) pairs + or keys specifying the index to create. + :param kwargs: any additional index creation + options (see the above list) should be passed as keyword + arguments. + + .. versionchanged:: 3.11 + Added the ``hidden`` option. + .. versionchanged:: 3.2 + Added the ``partialFilterExpression`` option to support partial + indexes. + + .. _wildcard index: https://mongodb.com/docs/master/core/index-wildcard/ + """ + keys = _index_list(keys) + if kwargs.get("name") is None: + kwargs["name"] = _gen_index_name(keys) + kwargs["key"] = _index_document(keys) + collation = validate_collation_or_none(kwargs.pop("collation", None)) + self.__document = kwargs + if collation is not None: + self.__document["collation"] = collation + + @property + def document(self) -> dict[str, Any]: + """An index document suitable for passing to the createIndexes + command. + """ + return self.__document + + +class SearchIndexModel: + """Represents a search index to create.""" + + __slots__ = ("__document",) + + def __init__( + self, + definition: Mapping[str, Any], + name: Optional[str] = None, + type: Optional[str] = None, + **kwargs: Any, + ) -> None: + """Create a Search Index instance. + + For use with :meth:`~pymongo.collection.Collection.create_search_index` and :meth:`~pymongo.collection.Collection.create_search_indexes`. + + :param definition: The definition for this index. + :param name: The name for this index, if present. + :param type: The type for this index which defaults to "search". Alternative values include "vectorSearch". + :param kwargs: Keyword arguments supplying any additional options. + + .. note:: Search indexes require a MongoDB server version 7.0+ Atlas cluster. + .. versionadded:: 4.5 + .. versionchanged:: 4.7 + Added the type and kwargs arguments. + """ + self.__document: dict[str, Any] = {} + if name is not None: + self.__document["name"] = name + self.__document["definition"] = definition + if type is not None: + self.__document["type"] = type + self.__document.update(kwargs) + + @property + def document(self) -> Mapping[str, Any]: + """The document for this index.""" + return self.__document diff --git a/venv/Lib/site-packages/pymongo/periodic_executor.py b/venv/Lib/site-packages/pymongo/periodic_executor.py new file mode 100644 index 00000000..9e9ead61 --- /dev/null +++ b/venv/Lib/site-packages/pymongo/periodic_executor.py @@ -0,0 +1,200 @@ +# Copyright 2014-present MongoDB, 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. + +"""Run a target function on a background thread.""" + +from __future__ import annotations + +import sys +import threading +import time +import weakref +from typing import Any, Callable, Optional + +from pymongo.lock import _create_lock + + +class PeriodicExecutor: + def __init__( + self, + interval: float, + min_interval: float, + target: Callable[[], bool], + name: Optional[str] = None, + ): + """ "Run a target function periodically on a background thread. + + If the target's return value is false, the executor stops. + + :param interval: Seconds between calls to `target`. + :param min_interval: Minimum seconds between calls if `wake` is + called very often. + :param target: A function. + :param name: A name to give the underlying thread. + """ + # threading.Event and its internal condition variable are expensive + # in Python 2, see PYTHON-983. Use a boolean to know when to wake. + # The executor's design is constrained by several Python issues, see + # "periodic_executor.rst" in this repository. + self._event = False + self._interval = interval + self._min_interval = min_interval + self._target = target + self._stopped = False + self._thread: Optional[threading.Thread] = None + self._name = name + self._skip_sleep = False + self._thread_will_exit = False + self._lock = _create_lock() + + def __repr__(self) -> str: + return f"<{self.__class__.__name__}(name={self._name}) object at 0x{id(self):x}>" + + def open(self) -> None: + """Start. Multiple calls have no effect. + + Not safe to call from multiple threads at once. + """ + with self._lock: + if self._thread_will_exit: + # If the background thread has read self._stopped as True + # there is a chance that it has not yet exited. The call to + # join should not block indefinitely because there is no + # other work done outside the while loop in self._run. + try: + assert self._thread is not None + self._thread.join() + except ReferenceError: + # Thread terminated. + pass + self._thread_will_exit = False + self._stopped = False + started: Any = False + try: + started = self._thread and self._thread.is_alive() + except ReferenceError: + # Thread terminated. + pass + + if not started: + thread = threading.Thread(target=self._run, name=self._name) + thread.daemon = True + self._thread = weakref.proxy(thread) + _register_executor(self) + # Mitigation to RuntimeError firing when thread starts on shutdown + # https://github.com/python/cpython/issues/114570 + try: + thread.start() + except RuntimeError as e: + if "interpreter shutdown" in str(e) or sys.is_finalizing(): + self._thread = None + return + raise + + def close(self, dummy: Any = None) -> None: + """Stop. To restart, call open(). + + The dummy parameter allows an executor's close method to be a weakref + callback; see monitor.py. + """ + self._stopped = True + + def join(self, timeout: Optional[int] = None) -> None: + if self._thread is not None: + try: + self._thread.join(timeout) + except (ReferenceError, RuntimeError): + # Thread already terminated, or not yet started. + pass + + def wake(self) -> None: + """Execute the target function soon.""" + self._event = True + + def update_interval(self, new_interval: int) -> None: + self._interval = new_interval + + def skip_sleep(self) -> None: + self._skip_sleep = True + + def __should_stop(self) -> bool: + with self._lock: + if self._stopped: + self._thread_will_exit = True + return True + return False + + def _run(self) -> None: + while not self.__should_stop(): + try: + if not self._target(): + self._stopped = True + break + except BaseException: + with self._lock: + self._stopped = True + self._thread_will_exit = True + + raise + + if self._skip_sleep: + self._skip_sleep = False + else: + deadline = time.monotonic() + self._interval + while not self._stopped and time.monotonic() < deadline: + time.sleep(self._min_interval) + if self._event: + break # Early wake. + + self._event = False + + +# _EXECUTORS has a weakref to each running PeriodicExecutor. Once started, +# an executor is kept alive by a strong reference from its thread and perhaps +# from other objects. When the thread dies and all other referrers are freed, +# the executor is freed and removed from _EXECUTORS. If any threads are +# running when the interpreter begins to shut down, we try to halt and join +# them to avoid spurious errors. +_EXECUTORS = set() + + +def _register_executor(executor: PeriodicExecutor) -> None: + ref = weakref.ref(executor, _on_executor_deleted) + _EXECUTORS.add(ref) + + +def _on_executor_deleted(ref: weakref.ReferenceType[PeriodicExecutor]) -> None: + _EXECUTORS.remove(ref) + + +def _shutdown_executors() -> None: + if _EXECUTORS is None: + return + + # Copy the set. Stopping threads has the side effect of removing executors. + executors = list(_EXECUTORS) + + # First signal all executors to close... + for ref in executors: + executor = ref() + if executor: + executor.close() + + # ...then try to join them. + for ref in executors: + executor = ref() + if executor: + executor.join(1) + + executor = None diff --git a/venv/Lib/site-packages/pymongo/pool.py b/venv/Lib/site-packages/pymongo/pool.py new file mode 100644 index 00000000..6a8cb54b --- /dev/null +++ b/venv/Lib/site-packages/pymongo/pool.py @@ -0,0 +1,2105 @@ +# Copyright 2011-present MongoDB, 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. + +from __future__ import annotations + +import collections +import contextlib +import copy +import logging +import os +import platform +import socket +import ssl +import sys +import threading +import time +import weakref +from pathlib import Path +from typing import ( + TYPE_CHECKING, + Any, + Iterator, + Mapping, + MutableMapping, + NoReturn, + Optional, + Sequence, + Union, +) + +import bson +from bson import DEFAULT_CODEC_OPTIONS +from pymongo import __version__, _csot, auth, helpers +from pymongo.client_session import _validate_session_write_concern +from pymongo.common import ( + MAX_BSON_SIZE, + MAX_CONNECTING, + MAX_IDLE_TIME_SEC, + MAX_MESSAGE_SIZE, + MAX_POOL_SIZE, + MAX_WIRE_VERSION, + MAX_WRITE_BATCH_SIZE, + MIN_POOL_SIZE, + ORDERED_TYPES, + WAIT_QUEUE_TIMEOUT, +) +from pymongo.errors import ( # type:ignore[attr-defined] + AutoReconnect, + ConfigurationError, + ConnectionFailure, + DocumentTooLarge, + ExecutionTimeout, + InvalidOperation, + NetworkTimeout, + NotPrimaryError, + OperationFailure, + PyMongoError, + WaitQueueTimeoutError, + _CertificateError, +) +from pymongo.hello import Hello, HelloCompat +from pymongo.helpers import _handle_reauth +from pymongo.lock import _create_lock +from pymongo.logger import ( + _CONNECTION_LOGGER, + _ConnectionStatusMessage, + _debug_log, + _verbose_connection_error_reason, +) +from pymongo.monitoring import ( + ConnectionCheckOutFailedReason, + ConnectionClosedReason, + _EventListeners, +) +from pymongo.network import command, receive_message +from pymongo.read_preferences import ReadPreference +from pymongo.server_api import _add_to_command +from pymongo.server_type import SERVER_TYPE +from pymongo.socket_checker import SocketChecker +from pymongo.ssl_support import HAS_SNI, SSLError + +if TYPE_CHECKING: + from bson import CodecOptions + from bson.objectid import ObjectId + from pymongo.auth import MongoCredential, _AuthContext + from pymongo.client_session import ClientSession + from pymongo.compression_support import ( + CompressionSettings, + SnappyContext, + ZlibContext, + ZstdContext, + ) + from pymongo.driver_info import DriverInfo + from pymongo.message import _OpMsg, _OpReply + from pymongo.mongo_client import MongoClient, _MongoClientErrorHandler + from pymongo.pyopenssl_context import SSLContext, _sslConn + from pymongo.read_concern import ReadConcern + from pymongo.read_preferences import _ServerMode + from pymongo.server_api import ServerApi + from pymongo.typings import ClusterTime, _Address, _CollationIn + from pymongo.write_concern import WriteConcern + +try: + from fcntl import F_GETFD, F_SETFD, FD_CLOEXEC, fcntl + + def _set_non_inheritable_non_atomic(fd: int) -> None: + """Set the close-on-exec flag on the given file descriptor.""" + flags = fcntl(fd, F_GETFD) + fcntl(fd, F_SETFD, flags | FD_CLOEXEC) + +except ImportError: + # Windows, various platforms we don't claim to support + # (Jython, IronPython, ..), systems that don't provide + # everything we need from fcntl, etc. + def _set_non_inheritable_non_atomic(fd: int) -> None: # noqa: ARG001 + """Dummy function for platforms that don't provide fcntl.""" + + +_MAX_TCP_KEEPIDLE = 120 +_MAX_TCP_KEEPINTVL = 10 +_MAX_TCP_KEEPCNT = 9 + +if sys.platform == "win32": + try: + import _winreg as winreg + except ImportError: + import winreg + + def _query(key, name, default): + try: + value, _ = winreg.QueryValueEx(key, name) + # Ensure the value is a number or raise ValueError. + return int(value) + except (OSError, ValueError): + # QueryValueEx raises OSError when the key does not exist (i.e. + # the system is using the Windows default value). + return default + + try: + with winreg.OpenKey( + winreg.HKEY_LOCAL_MACHINE, r"SYSTEM\CurrentControlSet\Services\Tcpip\Parameters" + ) as key: + _WINDOWS_TCP_IDLE_MS = _query(key, "KeepAliveTime", 7200000) + _WINDOWS_TCP_INTERVAL_MS = _query(key, "KeepAliveInterval", 1000) + except OSError: + # We could not check the default values because winreg.OpenKey failed. + # Assume the system is using the default values. + _WINDOWS_TCP_IDLE_MS = 7200000 + _WINDOWS_TCP_INTERVAL_MS = 1000 + + def _set_keepalive_times(sock): + idle_ms = min(_WINDOWS_TCP_IDLE_MS, _MAX_TCP_KEEPIDLE * 1000) + interval_ms = min(_WINDOWS_TCP_INTERVAL_MS, _MAX_TCP_KEEPINTVL * 1000) + if idle_ms < _WINDOWS_TCP_IDLE_MS or interval_ms < _WINDOWS_TCP_INTERVAL_MS: + sock.ioctl(socket.SIO_KEEPALIVE_VALS, (1, idle_ms, interval_ms)) + +else: + + def _set_tcp_option(sock: socket.socket, tcp_option: str, max_value: int) -> None: + if hasattr(socket, tcp_option): + sockopt = getattr(socket, tcp_option) + try: + # PYTHON-1350 - NetBSD doesn't implement getsockopt for + # TCP_KEEPIDLE and friends. Don't attempt to set the + # values there. + default = sock.getsockopt(socket.IPPROTO_TCP, sockopt) + if default > max_value: + sock.setsockopt(socket.IPPROTO_TCP, sockopt, max_value) + except OSError: + pass + + def _set_keepalive_times(sock: socket.socket) -> None: + _set_tcp_option(sock, "TCP_KEEPIDLE", _MAX_TCP_KEEPIDLE) + _set_tcp_option(sock, "TCP_KEEPINTVL", _MAX_TCP_KEEPINTVL) + _set_tcp_option(sock, "TCP_KEEPCNT", _MAX_TCP_KEEPCNT) + + +_METADATA: dict[str, Any] = {"driver": {"name": "PyMongo", "version": __version__}} + +if sys.platform.startswith("linux"): + # platform.linux_distribution was deprecated in Python 3.5 + # and removed in Python 3.8. Starting in Python 3.5 it + # raises DeprecationWarning + # DeprecationWarning: dist() and linux_distribution() functions are deprecated in Python 3.5 + _name = platform.system() + _METADATA["os"] = { + "type": _name, + "name": _name, + "architecture": platform.machine(), + # Kernel version (e.g. 4.4.0-17-generic). + "version": platform.release(), + } +elif sys.platform == "darwin": + _METADATA["os"] = { + "type": platform.system(), + "name": platform.system(), + "architecture": platform.machine(), + # (mac|i|tv)OS(X) version (e.g. 10.11.6) instead of darwin + # kernel version. + "version": platform.mac_ver()[0], + } +elif sys.platform == "win32": + _METADATA["os"] = { + "type": platform.system(), + # "Windows XP", "Windows 7", "Windows 10", etc. + "name": " ".join((platform.system(), platform.release())), + "architecture": platform.machine(), + # Windows patch level (e.g. 5.1.2600-SP3) + "version": "-".join(platform.win32_ver()[1:3]), + } +elif sys.platform.startswith("java"): + _name, _ver, _arch = platform.java_ver()[-1] + _METADATA["os"] = { + # Linux, Windows 7, Mac OS X, etc. + "type": _name, + "name": _name, + # x86, x86_64, AMD64, etc. + "architecture": _arch, + # Linux kernel version, OSX version, etc. + "version": _ver, + } +else: + # Get potential alias (e.g. SunOS 5.11 becomes Solaris 2.11) + _aliased = platform.system_alias(platform.system(), platform.release(), platform.version()) + _METADATA["os"] = { + "type": platform.system(), + "name": " ".join([part for part in _aliased[:2] if part]), + "architecture": platform.machine(), + "version": _aliased[2], + } + +if platform.python_implementation().startswith("PyPy"): + _METADATA["platform"] = " ".join( + ( + platform.python_implementation(), + ".".join(map(str, sys.pypy_version_info)), # type: ignore + "(Python %s)" % ".".join(map(str, sys.version_info)), + ) + ) +elif sys.platform.startswith("java"): + _METADATA["platform"] = " ".join( + ( + platform.python_implementation(), + ".".join(map(str, sys.version_info)), + "(%s)" % " ".join((platform.system(), platform.release())), + ) + ) +else: + _METADATA["platform"] = " ".join( + (platform.python_implementation(), ".".join(map(str, sys.version_info))) + ) + +DOCKER_ENV_PATH = "/.dockerenv" +ENV_VAR_K8S = "KUBERNETES_SERVICE_HOST" + +RUNTIME_NAME_DOCKER = "docker" +ORCHESTRATOR_NAME_K8S = "kubernetes" + + +def get_container_env_info() -> dict[str, str]: + """Returns the runtime and orchestrator of a container. + If neither value is present, the metadata client.env.container field will be omitted.""" + container = {} + + if Path(DOCKER_ENV_PATH).exists(): + container["runtime"] = RUNTIME_NAME_DOCKER + if os.getenv(ENV_VAR_K8S): + container["orchestrator"] = ORCHESTRATOR_NAME_K8S + + return container + + +def _is_lambda() -> bool: + if os.getenv("AWS_LAMBDA_RUNTIME_API"): + return True + env = os.getenv("AWS_EXECUTION_ENV") + if env: + return env.startswith("AWS_Lambda_") + return False + + +def _is_azure_func() -> bool: + return bool(os.getenv("FUNCTIONS_WORKER_RUNTIME")) + + +def _is_gcp_func() -> bool: + return bool(os.getenv("K_SERVICE") or os.getenv("FUNCTION_NAME")) + + +def _is_vercel() -> bool: + return bool(os.getenv("VERCEL")) + + +def _is_faas() -> bool: + return _is_lambda() or _is_azure_func() or _is_gcp_func() or _is_vercel() + + +def _getenv_int(key: str) -> Optional[int]: + """Like os.getenv but returns an int, or None if the value is missing/malformed.""" + val = os.getenv(key) + if not val: + return None + try: + return int(val) + except ValueError: + return None + + +def _metadata_env() -> dict[str, Any]: + env: dict[str, Any] = {} + container = get_container_env_info() + if container: + env["container"] = container + # Skip if multiple (or no) envs are matched. + if (_is_lambda(), _is_azure_func(), _is_gcp_func(), _is_vercel()).count(True) != 1: + return env + if _is_lambda(): + env["name"] = "aws.lambda" + region = os.getenv("AWS_REGION") + if region: + env["region"] = region + memory_mb = _getenv_int("AWS_LAMBDA_FUNCTION_MEMORY_SIZE") + if memory_mb is not None: + env["memory_mb"] = memory_mb + elif _is_azure_func(): + env["name"] = "azure.func" + elif _is_gcp_func(): + env["name"] = "gcp.func" + region = os.getenv("FUNCTION_REGION") + if region: + env["region"] = region + memory_mb = _getenv_int("FUNCTION_MEMORY_MB") + if memory_mb is not None: + env["memory_mb"] = memory_mb + timeout_sec = _getenv_int("FUNCTION_TIMEOUT_SEC") + if timeout_sec is not None: + env["timeout_sec"] = timeout_sec + elif _is_vercel(): + env["name"] = "vercel" + region = os.getenv("VERCEL_REGION") + if region: + env["region"] = region + return env + + +_MAX_METADATA_SIZE = 512 + + +# See: https://github.com/mongodb/specifications/blob/5112bcc/source/mongodb-handshake/handshake.rst#limitations +def _truncate_metadata(metadata: MutableMapping[str, Any]) -> None: + """Perform metadata truncation.""" + if len(bson.encode(metadata)) <= _MAX_METADATA_SIZE: + return + # 1. Omit fields from env except env.name. + env_name = metadata.get("env", {}).get("name") + if env_name: + metadata["env"] = {"name": env_name} + if len(bson.encode(metadata)) <= _MAX_METADATA_SIZE: + return + # 2. Omit fields from os except os.type. + os_type = metadata.get("os", {}).get("type") + if os_type: + metadata["os"] = {"type": os_type} + if len(bson.encode(metadata)) <= _MAX_METADATA_SIZE: + return + # 3. Omit the env document entirely. + metadata.pop("env", None) + encoded_size = len(bson.encode(metadata)) + if encoded_size <= _MAX_METADATA_SIZE: + return + # 4. Truncate platform. + overflow = encoded_size - _MAX_METADATA_SIZE + plat = metadata.get("platform", "") + if plat: + plat = plat[:-overflow] + if plat: + metadata["platform"] = plat + else: + metadata.pop("platform", None) + + +# If the first getaddrinfo call of this interpreter's life is on a thread, +# while the main thread holds the import lock, getaddrinfo deadlocks trying +# to import the IDNA codec. Import it here, where presumably we're on the +# main thread, to avoid the deadlock. See PYTHON-607. +"foo".encode("idna") + + +def _raise_connection_failure( + address: Any, + error: Exception, + msg_prefix: Optional[str] = None, + timeout_details: Optional[dict[str, float]] = None, +) -> NoReturn: + """Convert a socket.error to ConnectionFailure and raise it.""" + host, port = address + # If connecting to a Unix socket, port will be None. + if port is not None: + msg = "%s:%d: %s" % (host, port, error) + else: + msg = f"{host}: {error}" + if msg_prefix: + msg = msg_prefix + msg + if "configured timeouts" not in msg: + msg += format_timeout_details(timeout_details) + if isinstance(error, socket.timeout): + raise NetworkTimeout(msg) from error + elif isinstance(error, SSLError) and "timed out" in str(error): + # Eventlet does not distinguish TLS network timeouts from other + # SSLErrors (https://github.com/eventlet/eventlet/issues/692). + # Luckily, we can work around this limitation because the phrase + # 'timed out' appears in all the timeout related SSLErrors raised. + raise NetworkTimeout(msg) from error + else: + raise AutoReconnect(msg) from error + + +def _cond_wait(condition: threading.Condition, deadline: Optional[float]) -> bool: + timeout = deadline - time.monotonic() if deadline else None + return condition.wait(timeout) + + +def _get_timeout_details(options: PoolOptions) -> dict[str, float]: + details = {} + timeout = _csot.get_timeout() + socket_timeout = options.socket_timeout + connect_timeout = options.connect_timeout + if timeout: + details["timeoutMS"] = timeout * 1000 + if socket_timeout and not timeout: + details["socketTimeoutMS"] = socket_timeout * 1000 + if connect_timeout: + details["connectTimeoutMS"] = connect_timeout * 1000 + return details + + +def format_timeout_details(details: Optional[dict[str, float]]) -> str: + result = "" + if details: + result += " (configured timeouts:" + for timeout in ["socketTimeoutMS", "timeoutMS", "connectTimeoutMS"]: + if timeout in details: + result += f" {timeout}: {details[timeout]}ms," + result = result[:-1] + result += ")" + return result + + +class PoolOptions: + """Read only connection pool options for a MongoClient. + + Should not be instantiated directly by application developers. Access + a client's pool options via + :attr:`~pymongo.client_options.ClientOptions.pool_options` instead:: + + pool_opts = client.options.pool_options + pool_opts.max_pool_size + pool_opts.min_pool_size + + """ + + __slots__ = ( + "__max_pool_size", + "__min_pool_size", + "__max_idle_time_seconds", + "__connect_timeout", + "__socket_timeout", + "__wait_queue_timeout", + "__ssl_context", + "__tls_allow_invalid_hostnames", + "__event_listeners", + "__appname", + "__driver", + "__metadata", + "__compression_settings", + "__max_connecting", + "__pause_enabled", + "__server_api", + "__load_balanced", + "__credentials", + ) + + def __init__( + self, + max_pool_size: int = MAX_POOL_SIZE, + min_pool_size: int = MIN_POOL_SIZE, + max_idle_time_seconds: Optional[int] = MAX_IDLE_TIME_SEC, + connect_timeout: Optional[float] = None, + socket_timeout: Optional[float] = None, + wait_queue_timeout: Optional[int] = WAIT_QUEUE_TIMEOUT, + ssl_context: Optional[SSLContext] = None, + tls_allow_invalid_hostnames: bool = False, + event_listeners: Optional[_EventListeners] = None, + appname: Optional[str] = None, + driver: Optional[DriverInfo] = None, + compression_settings: Optional[CompressionSettings] = None, + max_connecting: int = MAX_CONNECTING, + pause_enabled: bool = True, + server_api: Optional[ServerApi] = None, + load_balanced: Optional[bool] = None, + credentials: Optional[MongoCredential] = None, + ): + self.__max_pool_size = max_pool_size + self.__min_pool_size = min_pool_size + self.__max_idle_time_seconds = max_idle_time_seconds + self.__connect_timeout = connect_timeout + self.__socket_timeout = socket_timeout + self.__wait_queue_timeout = wait_queue_timeout + self.__ssl_context = ssl_context + self.__tls_allow_invalid_hostnames = tls_allow_invalid_hostnames + self.__event_listeners = event_listeners + self.__appname = appname + self.__driver = driver + self.__compression_settings = compression_settings + self.__max_connecting = max_connecting + self.__pause_enabled = pause_enabled + self.__server_api = server_api + self.__load_balanced = load_balanced + self.__credentials = credentials + self.__metadata = copy.deepcopy(_METADATA) + if appname: + self.__metadata["application"] = {"name": appname} + + # Combine the "driver" MongoClient option with PyMongo's info, like: + # { + # 'driver': { + # 'name': 'PyMongo|MyDriver', + # 'version': '4.2.0|1.2.3', + # }, + # 'platform': 'CPython 3.7.0|MyPlatform' + # } + if driver: + if driver.name: + self.__metadata["driver"]["name"] = "{}|{}".format( + _METADATA["driver"]["name"], + driver.name, + ) + if driver.version: + self.__metadata["driver"]["version"] = "{}|{}".format( + _METADATA["driver"]["version"], + driver.version, + ) + if driver.platform: + self.__metadata["platform"] = "{}|{}".format(_METADATA["platform"], driver.platform) + + env = _metadata_env() + if env: + self.__metadata["env"] = env + + _truncate_metadata(self.__metadata) + + @property + def _credentials(self) -> Optional[MongoCredential]: + """A :class:`~pymongo.auth.MongoCredentials` instance or None.""" + return self.__credentials + + @property + def non_default_options(self) -> dict[str, Any]: + """The non-default options this pool was created with. + + Added for CMAP's :class:`PoolCreatedEvent`. + """ + opts = {} + if self.__max_pool_size != MAX_POOL_SIZE: + opts["maxPoolSize"] = self.__max_pool_size + if self.__min_pool_size != MIN_POOL_SIZE: + opts["minPoolSize"] = self.__min_pool_size + if self.__max_idle_time_seconds != MAX_IDLE_TIME_SEC: + assert self.__max_idle_time_seconds is not None + opts["maxIdleTimeMS"] = self.__max_idle_time_seconds * 1000 + if self.__wait_queue_timeout != WAIT_QUEUE_TIMEOUT: + assert self.__wait_queue_timeout is not None + opts["waitQueueTimeoutMS"] = self.__wait_queue_timeout * 1000 + if self.__max_connecting != MAX_CONNECTING: + opts["maxConnecting"] = self.__max_connecting + return opts + + @property + def max_pool_size(self) -> float: + """The maximum allowable number of concurrent connections to each + connected server. Requests to a server will block if there are + `maxPoolSize` outstanding connections to the requested server. + Defaults to 100. Cannot be 0. + + When a server's pool has reached `max_pool_size`, operations for that + server block waiting for a socket to be returned to the pool. If + ``waitQueueTimeoutMS`` is set, a blocked operation will raise + :exc:`~pymongo.errors.ConnectionFailure` after a timeout. + By default ``waitQueueTimeoutMS`` is not set. + """ + return self.__max_pool_size + + @property + def min_pool_size(self) -> int: + """The minimum required number of concurrent connections that the pool + will maintain to each connected server. Default is 0. + """ + return self.__min_pool_size + + @property + def max_connecting(self) -> int: + """The maximum number of concurrent connection creation attempts per + pool. Defaults to 2. + """ + return self.__max_connecting + + @property + def pause_enabled(self) -> bool: + return self.__pause_enabled + + @property + def max_idle_time_seconds(self) -> Optional[int]: + """The maximum number of seconds that a connection can remain + idle in the pool before being removed and replaced. Defaults to + `None` (no limit). + """ + return self.__max_idle_time_seconds + + @property + def connect_timeout(self) -> Optional[float]: + """How long a connection can take to be opened before timing out.""" + return self.__connect_timeout + + @property + def socket_timeout(self) -> Optional[float]: + """How long a send or receive on a socket can take before timing out.""" + return self.__socket_timeout + + @property + def wait_queue_timeout(self) -> Optional[int]: + """How long a thread will wait for a socket from the pool if the pool + has no free sockets. + """ + return self.__wait_queue_timeout + + @property + def _ssl_context(self) -> Optional[SSLContext]: + """An SSLContext instance or None.""" + return self.__ssl_context + + @property + def tls_allow_invalid_hostnames(self) -> bool: + """If True skip ssl.match_hostname.""" + return self.__tls_allow_invalid_hostnames + + @property + def _event_listeners(self) -> Optional[_EventListeners]: + """An instance of pymongo.monitoring._EventListeners.""" + return self.__event_listeners + + @property + def appname(self) -> Optional[str]: + """The application name, for sending with hello in server handshake.""" + return self.__appname + + @property + def driver(self) -> Optional[DriverInfo]: + """Driver name and version, for sending with hello in handshake.""" + return self.__driver + + @property + def _compression_settings(self) -> Optional[CompressionSettings]: + return self.__compression_settings + + @property + def metadata(self) -> dict[str, Any]: + """A dict of metadata about the application, driver, os, and platform.""" + return self.__metadata.copy() + + @property + def server_api(self) -> Optional[ServerApi]: + """A pymongo.server_api.ServerApi or None.""" + return self.__server_api + + @property + def load_balanced(self) -> Optional[bool]: + """True if this Pool is configured in load balanced mode.""" + return self.__load_balanced + + +class _CancellationContext: + def __init__(self) -> None: + self._cancelled = False + + def cancel(self) -> None: + """Cancel this context.""" + self._cancelled = True + + @property + def cancelled(self) -> bool: + """Was cancel called?""" + return self._cancelled + + +class Connection: + """Store a connection with some metadata. + + :param conn: a raw connection object + :param pool: a Pool instance + :param address: the server's (host, port) + :param id: the id of this socket in it's pool + """ + + def __init__( + self, conn: Union[socket.socket, _sslConn], pool: Pool, address: tuple[str, int], id: int + ): + self.pool_ref = weakref.ref(pool) + self.conn = conn + self.address = address + self.id = id + self.closed = False + self.last_checkin_time = time.monotonic() + self.performed_handshake = False + self.is_writable: bool = False + self.max_wire_version = MAX_WIRE_VERSION + self.max_bson_size = MAX_BSON_SIZE + self.max_message_size = MAX_MESSAGE_SIZE + self.max_write_batch_size = MAX_WRITE_BATCH_SIZE + self.supports_sessions = False + self.hello_ok: bool = False + self.is_mongos = False + self.op_msg_enabled = False + self.listeners = pool.opts._event_listeners + self.enabled_for_cmap = pool.enabled_for_cmap + self.compression_settings = pool.opts._compression_settings + self.compression_context: Union[SnappyContext, ZlibContext, ZstdContext, None] = None + self.socket_checker: SocketChecker = SocketChecker() + self.oidc_token_gen_id: Optional[int] = None + # Support for mechanism negotiation on the initial handshake. + self.negotiated_mechs: Optional[list[str]] = None + self.auth_ctx: Optional[_AuthContext] = None + + # The pool's generation changes with each reset() so we can close + # sockets created before the last reset. + self.pool_gen = pool.gen + self.generation = self.pool_gen.get_overall() + self.ready = False + self.cancel_context: _CancellationContext = _CancellationContext() + self.opts = pool.opts + self.more_to_come: bool = False + # For load balancer support. + self.service_id: Optional[ObjectId] = None + self.server_connection_id: Optional[int] = None + # When executing a transaction in load balancing mode, this flag is + # set to true to indicate that the session now owns the connection. + self.pinned_txn = False + self.pinned_cursor = False + self.active = False + self.last_timeout = self.opts.socket_timeout + self.connect_rtt = 0.0 + self._client_id = pool._client_id + self.creation_time = time.monotonic() + + def set_conn_timeout(self, timeout: Optional[float]) -> None: + """Cache last timeout to avoid duplicate calls to conn.settimeout.""" + if timeout == self.last_timeout: + return + self.last_timeout = timeout + self.conn.settimeout(timeout) + + def apply_timeout( + self, client: MongoClient, cmd: Optional[MutableMapping[str, Any]] + ) -> Optional[float]: + # CSOT: use remaining timeout when set. + timeout = _csot.remaining() + if timeout is None: + # Reset the socket timeout unless we're performing a streaming monitor check. + if not self.more_to_come: + self.set_conn_timeout(self.opts.socket_timeout) + return None + # RTT validation. + rtt = _csot.get_rtt() + if rtt is None: + rtt = self.connect_rtt + max_time_ms = timeout - rtt + if max_time_ms < 0: + timeout_details = _get_timeout_details(self.opts) + formatted = format_timeout_details(timeout_details) + # CSOT: raise an error without running the command since we know it will time out. + errmsg = f"operation would exceed time limit, remaining timeout:{timeout:.5f} <= network round trip time:{rtt:.5f} {formatted}" + raise ExecutionTimeout( + errmsg, + 50, + {"ok": 0, "errmsg": errmsg, "code": 50}, + self.max_wire_version, + ) + if cmd is not None: + cmd["maxTimeMS"] = int(max_time_ms * 1000) + self.set_conn_timeout(timeout) + return timeout + + def pin_txn(self) -> None: + self.pinned_txn = True + assert not self.pinned_cursor + + def pin_cursor(self) -> None: + self.pinned_cursor = True + assert not self.pinned_txn + + def unpin(self) -> None: + pool = self.pool_ref() + if pool: + pool.checkin(self) + else: + self.close_conn(ConnectionClosedReason.STALE) + + def hello_cmd(self) -> dict[str, Any]: + # Handshake spec requires us to use OP_MSG+hello command for the + # initial handshake in load balanced or stable API mode. + if self.opts.server_api or self.hello_ok or self.opts.load_balanced: + self.op_msg_enabled = True + return {HelloCompat.CMD: 1} + else: + return {HelloCompat.LEGACY_CMD: 1, "helloOk": True} + + def hello(self) -> Hello[dict[str, Any]]: + return self._hello(None, None, None) + + def _hello( + self, + cluster_time: Optional[ClusterTime], + topology_version: Optional[Any], + heartbeat_frequency: Optional[int], + ) -> Hello[dict[str, Any]]: + cmd = self.hello_cmd() + performing_handshake = not self.performed_handshake + awaitable = False + if performing_handshake: + self.performed_handshake = True + cmd["client"] = self.opts.metadata + if self.compression_settings: + cmd["compression"] = self.compression_settings.compressors + if self.opts.load_balanced: + cmd["loadBalanced"] = True + elif topology_version is not None: + cmd["topologyVersion"] = topology_version + assert heartbeat_frequency is not None + cmd["maxAwaitTimeMS"] = int(heartbeat_frequency * 1000) + awaitable = True + # If connect_timeout is None there is no timeout. + if self.opts.connect_timeout: + self.set_conn_timeout(self.opts.connect_timeout + heartbeat_frequency) + + if not performing_handshake and cluster_time is not None: + cmd["$clusterTime"] = cluster_time + + creds = self.opts._credentials + if creds: + if creds.mechanism == "DEFAULT" and creds.username: + cmd["saslSupportedMechs"] = creds.source + "." + creds.username + auth_ctx = auth._AuthContext.from_credentials(creds, self.address) + if auth_ctx: + speculative_authenticate = auth_ctx.speculate_command() + if speculative_authenticate is not None: + cmd["speculativeAuthenticate"] = speculative_authenticate + else: + auth_ctx = None + + if performing_handshake: + start = time.monotonic() + doc = self.command("admin", cmd, publish_events=False, exhaust_allowed=awaitable) + if performing_handshake: + self.connect_rtt = time.monotonic() - start + hello = Hello(doc, awaitable=awaitable) + self.is_writable = hello.is_writable + self.max_wire_version = hello.max_wire_version + self.max_bson_size = hello.max_bson_size + self.max_message_size = hello.max_message_size + self.max_write_batch_size = hello.max_write_batch_size + self.supports_sessions = ( + hello.logical_session_timeout_minutes is not None and hello.is_readable + ) + self.logical_session_timeout_minutes: Optional[int] = hello.logical_session_timeout_minutes + self.hello_ok = hello.hello_ok + self.is_repl = hello.server_type in ( + SERVER_TYPE.RSPrimary, + SERVER_TYPE.RSSecondary, + SERVER_TYPE.RSArbiter, + SERVER_TYPE.RSOther, + SERVER_TYPE.RSGhost, + ) + self.is_standalone = hello.server_type == SERVER_TYPE.Standalone + self.is_mongos = hello.server_type == SERVER_TYPE.Mongos + if performing_handshake and self.compression_settings: + ctx = self.compression_settings.get_compression_context(hello.compressors) + self.compression_context = ctx + + self.op_msg_enabled = True + self.server_connection_id = hello.connection_id + if creds: + self.negotiated_mechs = hello.sasl_supported_mechs + if auth_ctx: + auth_ctx.parse_response(hello) # type:ignore[arg-type] + if auth_ctx.speculate_succeeded(): + self.auth_ctx = auth_ctx + if self.opts.load_balanced: + if not hello.service_id: + raise ConfigurationError( + "Driver attempted to initialize in load balancing mode," + " but the server does not support this mode" + ) + self.service_id = hello.service_id + self.generation = self.pool_gen.get(self.service_id) + return hello + + def _next_reply(self) -> dict[str, Any]: + reply = self.receive_message(None) + self.more_to_come = reply.more_to_come + unpacked_docs = reply.unpack_response() + response_doc = unpacked_docs[0] + helpers._check_command_response(response_doc, self.max_wire_version) + return response_doc + + @_handle_reauth + def command( + self, + dbname: str, + spec: MutableMapping[str, Any], + read_preference: _ServerMode = ReadPreference.PRIMARY, + codec_options: CodecOptions = DEFAULT_CODEC_OPTIONS, + check: bool = True, + allowable_errors: Optional[Sequence[Union[str, int]]] = None, + read_concern: Optional[ReadConcern] = None, + write_concern: Optional[WriteConcern] = None, + parse_write_concern_error: bool = False, + collation: Optional[_CollationIn] = None, + session: Optional[ClientSession] = None, + client: Optional[MongoClient] = None, + retryable_write: bool = False, + publish_events: bool = True, + user_fields: Optional[Mapping[str, Any]] = None, + exhaust_allowed: bool = False, + ) -> dict[str, Any]: + """Execute a command or raise an error. + + :param dbname: name of the database on which to run the command + :param spec: a command document as a dict, SON, or mapping object + :param read_preference: a read preference + :param codec_options: a CodecOptions instance + :param check: raise OperationFailure if there are errors + :param allowable_errors: errors to ignore if `check` is True + :param read_concern: The read concern for this command. + :param write_concern: The write concern for this command. + :param parse_write_concern_error: Whether to parse the + ``writeConcernError`` field in the command response. + :param collation: The collation for this command. + :param session: optional ClientSession instance. + :param client: optional MongoClient for gossipping $clusterTime. + :param retryable_write: True if this command is a retryable write. + :param publish_events: Should we publish events for this command? + :param user_fields: Response fields that should be decoded + using the TypeDecoders from codec_options, passed to + bson._decode_all_selective. + """ + self.validate_session(client, session) + session = _validate_session_write_concern(session, write_concern) + + # Ensure command name remains in first place. + if not isinstance(spec, ORDERED_TYPES): # type:ignore[arg-type] + spec = dict(spec) + + if not (write_concern is None or write_concern.acknowledged or collation is None): + raise ConfigurationError("Collation is unsupported for unacknowledged writes.") + + self.add_server_api(spec) + if session: + session._apply_to(spec, retryable_write, read_preference, self) + self.send_cluster_time(spec, session, client) + listeners = self.listeners if publish_events else None + unacknowledged = bool(write_concern and not write_concern.acknowledged) + if self.op_msg_enabled: + self._raise_if_not_writable(unacknowledged) + try: + return command( + self, + dbname, + spec, + self.is_mongos, + read_preference, + codec_options, + session, + client, + check, + allowable_errors, + self.address, + listeners, + self.max_bson_size, + read_concern, + parse_write_concern_error=parse_write_concern_error, + collation=collation, + compression_ctx=self.compression_context, + use_op_msg=self.op_msg_enabled, + unacknowledged=unacknowledged, + user_fields=user_fields, + exhaust_allowed=exhaust_allowed, + write_concern=write_concern, + ) + except (OperationFailure, NotPrimaryError): + raise + # Catch socket.error, KeyboardInterrupt, etc. and close ourselves. + except BaseException as error: + self._raise_connection_failure(error) + + def send_message(self, message: bytes, max_doc_size: int) -> None: + """Send a raw BSON message or raise ConnectionFailure. + + If a network exception is raised, the socket is closed. + """ + if self.max_bson_size is not None and max_doc_size > self.max_bson_size: + raise DocumentTooLarge( + "BSON document too large (%d bytes) - the connected server " + "supports BSON document sizes up to %d bytes." % (max_doc_size, self.max_bson_size) + ) + + try: + self.conn.sendall(message) + except BaseException as error: + self._raise_connection_failure(error) + + def receive_message(self, request_id: Optional[int]) -> Union[_OpReply, _OpMsg]: + """Receive a raw BSON message or raise ConnectionFailure. + + If any exception is raised, the socket is closed. + """ + try: + return receive_message(self, request_id, self.max_message_size) + except BaseException as error: + self._raise_connection_failure(error) + + def _raise_if_not_writable(self, unacknowledged: bool) -> None: + """Raise NotPrimaryError on unacknowledged write if this socket is not + writable. + """ + if unacknowledged and not self.is_writable: + # Write won't succeed, bail as if we'd received a not primary error. + raise NotPrimaryError("not primary", {"ok": 0, "errmsg": "not primary", "code": 10107}) + + def unack_write(self, msg: bytes, max_doc_size: int) -> None: + """Send unack OP_MSG. + + Can raise ConnectionFailure or InvalidDocument. + + :param msg: bytes, an OP_MSG message. + :param max_doc_size: size in bytes of the largest document in `msg`. + """ + self._raise_if_not_writable(True) + self.send_message(msg, max_doc_size) + + def write_command( + self, request_id: int, msg: bytes, codec_options: CodecOptions + ) -> dict[str, Any]: + """Send "insert" etc. command, returning response as a dict. + + Can raise ConnectionFailure or OperationFailure. + + :param request_id: an int. + :param msg: bytes, the command message. + """ + self.send_message(msg, 0) + reply = self.receive_message(request_id) + result = reply.command_response(codec_options) + + # Raises NotPrimaryError or OperationFailure. + helpers._check_command_response(result, self.max_wire_version) + return result + + def authenticate(self, reauthenticate: bool = False) -> None: + """Authenticate to the server if needed. + + Can raise ConnectionFailure or OperationFailure. + """ + # CMAP spec says to publish the ready event only after authenticating + # the connection. + if reauthenticate: + if self.performed_handshake: + # Existing auth_ctx is stale, remove it. + self.auth_ctx = None + self.ready = False + if not self.ready: + creds = self.opts._credentials + if creds: + auth.authenticate(creds, self, reauthenticate=reauthenticate) + self.ready = True + if self.enabled_for_cmap: + assert self.listeners is not None + duration = time.monotonic() - self.creation_time + self.listeners.publish_connection_ready(self.address, self.id, duration) + if _CONNECTION_LOGGER.isEnabledFor(logging.DEBUG): + _debug_log( + _CONNECTION_LOGGER, + clientId=self._client_id, + message=_ConnectionStatusMessage.CONN_READY, + serverHost=self.address[0], + serverPort=self.address[1], + driverConnectionId=self.id, + durationMS=duration, + ) + + def validate_session( + self, client: Optional[MongoClient], session: Optional[ClientSession] + ) -> None: + """Validate this session before use with client. + + Raises error if the client is not the one that created the session. + """ + if session: + if session._client is not client: + raise InvalidOperation("Can only use session with the MongoClient that started it") + + def close_conn(self, reason: Optional[str]) -> None: + """Close this connection with a reason.""" + if self.closed: + return + self._close_conn() + if reason and self.enabled_for_cmap: + assert self.listeners is not None + self.listeners.publish_connection_closed(self.address, self.id, reason) + if _CONNECTION_LOGGER.isEnabledFor(logging.DEBUG): + _debug_log( + _CONNECTION_LOGGER, + clientId=self._client_id, + message=_ConnectionStatusMessage.CONN_CLOSED, + serverHost=self.address[0], + serverPort=self.address[1], + driverConnectionId=self.id, + reason=_verbose_connection_error_reason(reason), + error=reason, + ) + + def _close_conn(self) -> None: + """Close this connection.""" + if self.closed: + return + self.closed = True + self.cancel_context.cancel() + # Note: We catch exceptions to avoid spurious errors on interpreter + # shutdown. + try: + self.conn.close() + except Exception: # noqa: S110 + pass + + def conn_closed(self) -> bool: + """Return True if we know socket has been closed, False otherwise.""" + return self.socket_checker.socket_closed(self.conn) + + def send_cluster_time( + self, + command: MutableMapping[str, Any], + session: Optional[ClientSession], + client: Optional[MongoClient], + ) -> None: + """Add $clusterTime.""" + if client: + client._send_cluster_time(command, session) + + def add_server_api(self, command: MutableMapping[str, Any]) -> None: + """Add server_api parameters.""" + if self.opts.server_api: + _add_to_command(command, self.opts.server_api) + + def update_last_checkin_time(self) -> None: + self.last_checkin_time = time.monotonic() + + def update_is_writable(self, is_writable: bool) -> None: + self.is_writable = is_writable + + def idle_time_seconds(self) -> float: + """Seconds since this socket was last checked into its pool.""" + return time.monotonic() - self.last_checkin_time + + def _raise_connection_failure(self, error: BaseException) -> NoReturn: + # Catch *all* exceptions from socket methods and close the socket. In + # regular Python, socket operations only raise socket.error, even if + # the underlying cause was a Ctrl-C: a signal raised during socket.recv + # is expressed as an EINTR error from poll. See internal_select_ex() in + # socketmodule.c. All error codes from poll become socket.error at + # first. Eventually in PyEval_EvalFrameEx the interpreter checks for + # signals and throws KeyboardInterrupt into the current frame on the + # main thread. + # + # But in Gevent and Eventlet, the polling mechanism (epoll, kqueue, + # ..) is called in Python code, which experiences the signal as a + # KeyboardInterrupt from the start, rather than as an initial + # socket.error, so we catch that, close the socket, and reraise it. + # + # The connection closed event will be emitted later in checkin. + if self.ready: + reason = None + else: + reason = ConnectionClosedReason.ERROR + self.close_conn(reason) + # SSLError from PyOpenSSL inherits directly from Exception. + if isinstance(error, (IOError, OSError, SSLError)): + details = _get_timeout_details(self.opts) + _raise_connection_failure(self.address, error, timeout_details=details) + else: + raise + + def __eq__(self, other: Any) -> bool: + return self.conn == other.conn + + def __ne__(self, other: Any) -> bool: + return not self == other + + def __hash__(self) -> int: + return hash(self.conn) + + def __repr__(self) -> str: + return "Connection({}){} at {}".format( + repr(self.conn), + self.closed and " CLOSED" or "", + id(self), + ) + + +def _create_connection(address: _Address, options: PoolOptions) -> socket.socket: + """Given (host, port) and PoolOptions, connect and return a socket object. + + Can raise socket.error. + + This is a modified version of create_connection from CPython >= 2.7. + """ + host, port = address + + # Check if dealing with a unix domain socket + if host.endswith(".sock"): + if not hasattr(socket, "AF_UNIX"): + raise ConnectionFailure("UNIX-sockets are not supported on this system") + sock = socket.socket(socket.AF_UNIX) + # SOCK_CLOEXEC not supported for Unix sockets. + _set_non_inheritable_non_atomic(sock.fileno()) + try: + sock.connect(host) + return sock + except OSError: + sock.close() + raise + + # Don't try IPv6 if we don't support it. Also skip it if host + # is 'localhost' (::1 is fine). Avoids slow connect issues + # like PYTHON-356. + family = socket.AF_INET + if socket.has_ipv6 and host != "localhost": + family = socket.AF_UNSPEC + + err = None + for res in socket.getaddrinfo(host, port, family, socket.SOCK_STREAM): + af, socktype, proto, dummy, sa = res + # SOCK_CLOEXEC was new in CPython 3.2, and only available on a limited + # number of platforms (newer Linux and *BSD). Starting with CPython 3.4 + # all file descriptors are created non-inheritable. See PEP 446. + try: + sock = socket.socket(af, socktype | getattr(socket, "SOCK_CLOEXEC", 0), proto) + except OSError: + # Can SOCK_CLOEXEC be defined even if the kernel doesn't support + # it? + sock = socket.socket(af, socktype, proto) + # Fallback when SOCK_CLOEXEC isn't available. + _set_non_inheritable_non_atomic(sock.fileno()) + try: + sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) + # CSOT: apply timeout to socket connect. + timeout = _csot.remaining() + if timeout is None: + timeout = options.connect_timeout + elif timeout <= 0: + raise socket.timeout("timed out") + sock.settimeout(timeout) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, True) + _set_keepalive_times(sock) + sock.connect(sa) + return sock + except OSError as e: + err = e + sock.close() + + if err is not None: + raise err + else: + # This likely means we tried to connect to an IPv6 only + # host with an OS/kernel or Python interpreter that doesn't + # support IPv6. The test case is Jython2.5.1 which doesn't + # support IPv6 at all. + raise OSError("getaddrinfo failed") + + +def _configured_socket(address: _Address, options: PoolOptions) -> Union[socket.socket, _sslConn]: + """Given (host, port) and PoolOptions, return a configured socket. + + Can raise socket.error, ConnectionFailure, or _CertificateError. + + Sets socket's SSL and timeout options. + """ + sock = _create_connection(address, options) + ssl_context = options._ssl_context + + if ssl_context is None: + sock.settimeout(options.socket_timeout) + return sock + + host = address[0] + try: + # We have to pass hostname / ip address to wrap_socket + # to use SSLContext.check_hostname. + if HAS_SNI: + ssl_sock = ssl_context.wrap_socket(sock, server_hostname=host) + else: + ssl_sock = ssl_context.wrap_socket(sock) + except _CertificateError: + sock.close() + # Raise _CertificateError directly like we do after match_hostname + # below. + raise + except (OSError, SSLError) as exc: + sock.close() + # We raise AutoReconnect for transient and permanent SSL handshake + # failures alike. Permanent handshake failures, like protocol + # mismatch, will be turned into ServerSelectionTimeoutErrors later. + details = _get_timeout_details(options) + _raise_connection_failure(address, exc, "SSL handshake failed: ", timeout_details=details) + if ( + ssl_context.verify_mode + and not ssl_context.check_hostname + and not options.tls_allow_invalid_hostnames + ): + try: + ssl.match_hostname(ssl_sock.getpeercert(), hostname=host) + except _CertificateError: + ssl_sock.close() + raise + + ssl_sock.settimeout(options.socket_timeout) + return ssl_sock + + +class _PoolClosedError(PyMongoError): + """Internal error raised when a thread tries to get a connection from a + closed pool. + """ + + +class _PoolGeneration: + def __init__(self) -> None: + # Maps service_id to generation. + self._generations: dict[ObjectId, int] = collections.defaultdict(int) + # Overall pool generation. + self._generation = 0 + + def get(self, service_id: Optional[ObjectId]) -> int: + """Get the generation for the given service_id.""" + if service_id is None: + return self._generation + return self._generations[service_id] + + def get_overall(self) -> int: + """Get the Pool's overall generation.""" + return self._generation + + def inc(self, service_id: Optional[ObjectId]) -> None: + """Increment the generation for the given service_id.""" + self._generation += 1 + if service_id is None: + for service_id in self._generations: + self._generations[service_id] += 1 + else: + self._generations[service_id] += 1 + + def stale(self, gen: int, service_id: Optional[ObjectId]) -> bool: + """Return if the given generation for a given service_id is stale.""" + return gen != self.get(service_id) + + +class PoolState: + PAUSED = 1 + READY = 2 + CLOSED = 3 + + +# Do *not* explicitly inherit from object or Jython won't call __del__ +# http://bugs.jython.org/issue1057 +class Pool: + def __init__( + self, + address: _Address, + options: PoolOptions, + handshake: bool = True, + client_id: Optional[ObjectId] = None, + ): + """ + :param address: a (hostname, port) tuple + :param options: a PoolOptions instance + :param handshake: whether to call hello for each new Connection + """ + if options.pause_enabled: + self.state = PoolState.PAUSED + else: + self.state = PoolState.READY + # Check a socket's health with socket_closed() every once in a while. + # Can override for testing: 0 to always check, None to never check. + self._check_interval_seconds = 1 + # LIFO pool. Sockets are ordered on idle time. Sockets claimed + # and returned to pool from the left side. Stale sockets removed + # from the right side. + self.conns: collections.deque = collections.deque() + self.active_contexts: set[_CancellationContext] = set() + self.lock = _create_lock() + self.active_sockets = 0 + # Monotonically increasing connection ID required for CMAP Events. + self.next_connection_id = 1 + # Track whether the sockets in this pool are writeable or not. + self.is_writable: Optional[bool] = None + + # Keep track of resets, so we notice sockets created before the most + # recent reset and close them. + # self.generation = 0 + self.gen = _PoolGeneration() + self.pid = os.getpid() + self.address = address + self.opts = options + self.handshake = handshake + # Don't publish events in Monitor pools. + self.enabled_for_cmap = ( + self.handshake + and self.opts._event_listeners is not None + and self.opts._event_listeners.enabled_for_cmap + ) + + # The first portion of the wait queue. + # Enforces: maxPoolSize + # Also used for: clearing the wait queue + self.size_cond = threading.Condition(self.lock) + self.requests = 0 + self.max_pool_size = self.opts.max_pool_size + if not self.max_pool_size: + self.max_pool_size = float("inf") + # The second portion of the wait queue. + # Enforces: maxConnecting + # Also used for: clearing the wait queue + self._max_connecting_cond = threading.Condition(self.lock) + self._max_connecting = self.opts.max_connecting + self._pending = 0 + self._client_id = client_id + if self.enabled_for_cmap: + assert self.opts._event_listeners is not None + self.opts._event_listeners.publish_pool_created( + self.address, self.opts.non_default_options + ) + if _CONNECTION_LOGGER.isEnabledFor(logging.DEBUG): + _debug_log( + _CONNECTION_LOGGER, + clientId=self._client_id, + message=_ConnectionStatusMessage.POOL_CREATED, + serverHost=self.address[0], + serverPort=self.address[1], + **self.opts.non_default_options, + ) + # Similar to active_sockets but includes threads in the wait queue. + self.operation_count: int = 0 + # Retain references to pinned connections to prevent the CPython GC + # from thinking that a cursor's pinned connection can be GC'd when the + # cursor is GC'd (see PYTHON-2751). + self.__pinned_sockets: set[Connection] = set() + self.ncursors = 0 + self.ntxns = 0 + + def ready(self) -> None: + # Take the lock to avoid the race condition described in PYTHON-2699. + with self.lock: + if self.state != PoolState.READY: + self.state = PoolState.READY + if self.enabled_for_cmap: + assert self.opts._event_listeners is not None + self.opts._event_listeners.publish_pool_ready(self.address) + if _CONNECTION_LOGGER.isEnabledFor(logging.DEBUG): + _debug_log( + _CONNECTION_LOGGER, + clientId=self._client_id, + message=_ConnectionStatusMessage.POOL_READY, + serverHost=self.address[0], + serverPort=self.address[1], + ) + + @property + def closed(self) -> bool: + return self.state == PoolState.CLOSED + + def _reset( + self, + close: bool, + pause: bool = True, + service_id: Optional[ObjectId] = None, + interrupt_connections: bool = False, + ) -> None: + old_state = self.state + with self.size_cond: + if self.closed: + return + if self.opts.pause_enabled and pause and not self.opts.load_balanced: + old_state, self.state = self.state, PoolState.PAUSED + self.gen.inc(service_id) + newpid = os.getpid() + if self.pid != newpid: + self.pid = newpid + self.active_sockets = 0 + self.operation_count = 0 + if service_id is None: + sockets, self.conns = self.conns, collections.deque() + else: + discard: collections.deque = collections.deque() + keep: collections.deque = collections.deque() + for conn in self.conns: + if conn.service_id == service_id: + discard.append(conn) + else: + keep.append(conn) + sockets = discard + self.conns = keep + + if close: + self.state = PoolState.CLOSED + # Clear the wait queue + self._max_connecting_cond.notify_all() + self.size_cond.notify_all() + + if interrupt_connections: + for context in self.active_contexts: + context.cancel() + + listeners = self.opts._event_listeners + # CMAP spec says that close() MUST close sockets before publishing the + # PoolClosedEvent but that reset() SHOULD close sockets *after* + # publishing the PoolClearedEvent. + if close: + for conn in sockets: + conn.close_conn(ConnectionClosedReason.POOL_CLOSED) + if self.enabled_for_cmap: + assert listeners is not None + listeners.publish_pool_closed(self.address) + if _CONNECTION_LOGGER.isEnabledFor(logging.DEBUG): + _debug_log( + _CONNECTION_LOGGER, + clientId=self._client_id, + message=_ConnectionStatusMessage.POOL_CLOSED, + serverHost=self.address[0], + serverPort=self.address[1], + ) + else: + if old_state != PoolState.PAUSED and self.enabled_for_cmap: + assert listeners is not None + listeners.publish_pool_cleared( + self.address, + service_id=service_id, + interrupt_connections=interrupt_connections, + ) + if _CONNECTION_LOGGER.isEnabledFor(logging.DEBUG): + _debug_log( + _CONNECTION_LOGGER, + clientId=self._client_id, + message=_ConnectionStatusMessage.POOL_CLEARED, + serverHost=self.address[0], + serverPort=self.address[1], + serviceId=service_id, + ) + for conn in sockets: + conn.close_conn(ConnectionClosedReason.STALE) + + def update_is_writable(self, is_writable: Optional[bool]) -> None: + """Updates the is_writable attribute on all sockets currently in the + Pool. + """ + self.is_writable = is_writable + with self.lock: + for _socket in self.conns: + _socket.update_is_writable(self.is_writable) + + def reset( + self, service_id: Optional[ObjectId] = None, interrupt_connections: bool = False + ) -> None: + self._reset(close=False, service_id=service_id, interrupt_connections=interrupt_connections) + + def reset_without_pause(self) -> None: + self._reset(close=False, pause=False) + + def close(self) -> None: + self._reset(close=True) + + def stale_generation(self, gen: int, service_id: Optional[ObjectId]) -> bool: + return self.gen.stale(gen, service_id) + + def remove_stale_sockets(self, reference_generation: int) -> None: + """Removes stale sockets then adds new ones if pool is too small and + has not been reset. The `reference_generation` argument specifies the + `generation` at the point in time this operation was requested on the + pool. + """ + # Take the lock to avoid the race condition described in PYTHON-2699. + with self.lock: + if self.state != PoolState.READY: + return + + if self.opts.max_idle_time_seconds is not None: + with self.lock: + while ( + self.conns + and self.conns[-1].idle_time_seconds() > self.opts.max_idle_time_seconds + ): + conn = self.conns.pop() + conn.close_conn(ConnectionClosedReason.IDLE) + + while True: + with self.size_cond: + # There are enough sockets in the pool. + if len(self.conns) + self.active_sockets >= self.opts.min_pool_size: + return + if self.requests >= self.opts.min_pool_size: + return + self.requests += 1 + incremented = False + try: + with self._max_connecting_cond: + # If maxConnecting connections are already being created + # by this pool then try again later instead of waiting. + if self._pending >= self._max_connecting: + return + self._pending += 1 + incremented = True + conn = self.connect() + with self.lock: + # Close connection and return if the pool was reset during + # socket creation or while acquiring the pool lock. + if self.gen.get_overall() != reference_generation: + conn.close_conn(ConnectionClosedReason.STALE) + return + self.conns.appendleft(conn) + self.active_contexts.discard(conn.cancel_context) + finally: + if incremented: + # Notify after adding the socket to the pool. + with self._max_connecting_cond: + self._pending -= 1 + self._max_connecting_cond.notify() + + with self.size_cond: + self.requests -= 1 + self.size_cond.notify() + + def connect(self, handler: Optional[_MongoClientErrorHandler] = None) -> Connection: + """Connect to Mongo and return a new Connection. + + Can raise ConnectionFailure. + + Note that the pool does not keep a reference to the socket -- you + must call checkin() when you're done with it. + """ + with self.lock: + conn_id = self.next_connection_id + self.next_connection_id += 1 + + listeners = self.opts._event_listeners + if self.enabled_for_cmap: + assert listeners is not None + listeners.publish_connection_created(self.address, conn_id) + if _CONNECTION_LOGGER.isEnabledFor(logging.DEBUG): + _debug_log( + _CONNECTION_LOGGER, + clientId=self._client_id, + message=_ConnectionStatusMessage.CONN_CREATED, + serverHost=self.address[0], + serverPort=self.address[1], + driverConnectionId=conn_id, + ) + + try: + sock = _configured_socket(self.address, self.opts) + except BaseException as error: + if self.enabled_for_cmap: + assert listeners is not None + listeners.publish_connection_closed( + self.address, conn_id, ConnectionClosedReason.ERROR + ) + if _CONNECTION_LOGGER.isEnabledFor(logging.DEBUG): + _debug_log( + _CONNECTION_LOGGER, + clientId=self._client_id, + message=_ConnectionStatusMessage.CONN_CLOSED, + serverHost=self.address[0], + serverPort=self.address[1], + driverConnectionId=conn_id, + reason=_verbose_connection_error_reason(ConnectionClosedReason.ERROR), + error=ConnectionClosedReason.ERROR, + ) + if isinstance(error, (IOError, OSError, SSLError)): + details = _get_timeout_details(self.opts) + _raise_connection_failure(self.address, error, timeout_details=details) + + raise + + conn = Connection(sock, self, self.address, conn_id) # type: ignore[arg-type] + with self.lock: + self.active_contexts.add(conn.cancel_context) + try: + if self.handshake: + conn.hello() + self.is_writable = conn.is_writable + if handler: + handler.contribute_socket(conn, completed_handshake=False) + + conn.authenticate() + except BaseException: + conn.close_conn(ConnectionClosedReason.ERROR) + raise + + return conn + + @contextlib.contextmanager + def checkout(self, handler: Optional[_MongoClientErrorHandler] = None) -> Iterator[Connection]: + """Get a connection from the pool. Use with a "with" statement. + + Returns a :class:`Connection` object wrapping a connected + :class:`socket.socket`. + + This method should always be used in a with-statement:: + + with pool.get_conn() as connection: + connection.send_message(msg) + data = connection.receive_message(op_code, request_id) + + Can raise ConnectionFailure or OperationFailure. + + :param handler: A _MongoClientErrorHandler. + """ + listeners = self.opts._event_listeners + checkout_started_time = time.monotonic() + if self.enabled_for_cmap: + assert listeners is not None + listeners.publish_connection_check_out_started(self.address) + if _CONNECTION_LOGGER.isEnabledFor(logging.DEBUG): + _debug_log( + _CONNECTION_LOGGER, + clientId=self._client_id, + message=_ConnectionStatusMessage.CHECKOUT_STARTED, + serverHost=self.address[0], + serverPort=self.address[1], + ) + + conn = self._get_conn(checkout_started_time, handler=handler) + + if self.enabled_for_cmap: + assert listeners is not None + duration = time.monotonic() - checkout_started_time + listeners.publish_connection_checked_out(self.address, conn.id, duration) + if _CONNECTION_LOGGER.isEnabledFor(logging.DEBUG): + _debug_log( + _CONNECTION_LOGGER, + clientId=self._client_id, + message=_ConnectionStatusMessage.CHECKOUT_SUCCEEDED, + serverHost=self.address[0], + serverPort=self.address[1], + driverConnectionId=conn.id, + durationMS=duration, + ) + try: + with self.lock: + self.active_contexts.add(conn.cancel_context) + yield conn + except BaseException: + # Exception in caller. Ensure the connection gets returned. + # Note that when pinned is True, the session owns the + # connection and it is responsible for checking the connection + # back into the pool. + pinned = conn.pinned_txn or conn.pinned_cursor + if handler: + # Perform SDAM error handling rules while the connection is + # still checked out. + exc_type, exc_val, _ = sys.exc_info() + handler.handle(exc_type, exc_val) + if not pinned and conn.active: + self.checkin(conn) + raise + if conn.pinned_txn: + with self.lock: + self.__pinned_sockets.add(conn) + self.ntxns += 1 + elif conn.pinned_cursor: + with self.lock: + self.__pinned_sockets.add(conn) + self.ncursors += 1 + elif conn.active: + self.checkin(conn) + + def _raise_if_not_ready(self, checkout_started_time: float, emit_event: bool) -> None: + if self.state != PoolState.READY: + if self.enabled_for_cmap and emit_event: + assert self.opts._event_listeners is not None + duration = time.monotonic() - checkout_started_time + self.opts._event_listeners.publish_connection_check_out_failed( + self.address, ConnectionCheckOutFailedReason.CONN_ERROR, duration + ) + if _CONNECTION_LOGGER.isEnabledFor(logging.DEBUG): + _debug_log( + _CONNECTION_LOGGER, + clientId=self._client_id, + message=_ConnectionStatusMessage.CHECKOUT_FAILED, + serverHost=self.address[0], + serverPort=self.address[1], + reason="An error occurred while trying to establish a new connection", + error=ConnectionCheckOutFailedReason.CONN_ERROR, + durationMS=duration, + ) + + details = _get_timeout_details(self.opts) + _raise_connection_failure( + self.address, AutoReconnect("connection pool paused"), timeout_details=details + ) + + def _get_conn( + self, checkout_started_time: float, handler: Optional[_MongoClientErrorHandler] = None + ) -> Connection: + """Get or create a Connection. Can raise ConnectionFailure.""" + # We use the pid here to avoid issues with fork / multiprocessing. + # See test.test_client:TestClient.test_fork for an example of + # what could go wrong otherwise + if self.pid != os.getpid(): + self.reset_without_pause() + + if self.closed: + if self.enabled_for_cmap: + assert self.opts._event_listeners is not None + duration = time.monotonic() - checkout_started_time + self.opts._event_listeners.publish_connection_check_out_failed( + self.address, ConnectionCheckOutFailedReason.POOL_CLOSED, duration + ) + if _CONNECTION_LOGGER.isEnabledFor(logging.DEBUG): + _debug_log( + _CONNECTION_LOGGER, + clientId=self._client_id, + message=_ConnectionStatusMessage.CHECKOUT_FAILED, + serverHost=self.address[0], + serverPort=self.address[1], + reason="Connection pool was closed", + error=ConnectionCheckOutFailedReason.POOL_CLOSED, + durationMS=duration, + ) + raise _PoolClosedError( + "Attempted to check out a connection from closed connection pool" + ) + + with self.lock: + self.operation_count += 1 + + # Get a free socket or create one. + if _csot.get_timeout(): + deadline = _csot.get_deadline() + elif self.opts.wait_queue_timeout: + deadline = time.monotonic() + self.opts.wait_queue_timeout + else: + deadline = None + + with self.size_cond: + self._raise_if_not_ready(checkout_started_time, emit_event=True) + while not (self.requests < self.max_pool_size): + if not _cond_wait(self.size_cond, deadline): + # Timed out, notify the next thread to ensure a + # timeout doesn't consume the condition. + if self.requests < self.max_pool_size: + self.size_cond.notify() + self._raise_wait_queue_timeout(checkout_started_time) + self._raise_if_not_ready(checkout_started_time, emit_event=True) + self.requests += 1 + + # We've now acquired the semaphore and must release it on error. + conn = None + incremented = False + emitted_event = False + try: + with self.lock: + self.active_sockets += 1 + incremented = True + while conn is None: + # CMAP: we MUST wait for either maxConnecting OR for a socket + # to be checked back into the pool. + with self._max_connecting_cond: + self._raise_if_not_ready(checkout_started_time, emit_event=False) + while not (self.conns or self._pending < self._max_connecting): + if not _cond_wait(self._max_connecting_cond, deadline): + # Timed out, notify the next thread to ensure a + # timeout doesn't consume the condition. + if self.conns or self._pending < self._max_connecting: + self._max_connecting_cond.notify() + emitted_event = True + self._raise_wait_queue_timeout(checkout_started_time) + self._raise_if_not_ready(checkout_started_time, emit_event=False) + + try: + conn = self.conns.popleft() + except IndexError: + self._pending += 1 + if conn: # We got a socket from the pool + if self._perished(conn): + conn = None + continue + else: # We need to create a new connection + try: + conn = self.connect(handler=handler) + finally: + with self._max_connecting_cond: + self._pending -= 1 + self._max_connecting_cond.notify() + except BaseException: + if conn: + # We checked out a socket but authentication failed. + conn.close_conn(ConnectionClosedReason.ERROR) + with self.size_cond: + self.requests -= 1 + if incremented: + self.active_sockets -= 1 + self.size_cond.notify() + + if self.enabled_for_cmap and not emitted_event: + assert self.opts._event_listeners is not None + duration = time.monotonic() - checkout_started_time + self.opts._event_listeners.publish_connection_check_out_failed( + self.address, ConnectionCheckOutFailedReason.CONN_ERROR, duration + ) + if _CONNECTION_LOGGER.isEnabledFor(logging.DEBUG): + _debug_log( + _CONNECTION_LOGGER, + clientId=self._client_id, + message=_ConnectionStatusMessage.CHECKOUT_FAILED, + serverHost=self.address[0], + serverPort=self.address[1], + reason="An error occurred while trying to establish a new connection", + error=ConnectionCheckOutFailedReason.CONN_ERROR, + durationMS=duration, + ) + raise + + conn.active = True + return conn + + def checkin(self, conn: Connection) -> None: + """Return the connection to the pool, or if it's closed discard it. + + :param conn: The connection to check into the pool. + """ + txn = conn.pinned_txn + cursor = conn.pinned_cursor + conn.active = False + conn.pinned_txn = False + conn.pinned_cursor = False + self.__pinned_sockets.discard(conn) + listeners = self.opts._event_listeners + with self.lock: + self.active_contexts.discard(conn.cancel_context) + if self.enabled_for_cmap: + assert listeners is not None + listeners.publish_connection_checked_in(self.address, conn.id) + if _CONNECTION_LOGGER.isEnabledFor(logging.DEBUG): + _debug_log( + _CONNECTION_LOGGER, + clientId=self._client_id, + message=_ConnectionStatusMessage.CHECKEDIN, + serverHost=self.address[0], + serverPort=self.address[1], + driverConnectionId=conn.id, + ) + if self.pid != os.getpid(): + self.reset_without_pause() + else: + if self.closed: + conn.close_conn(ConnectionClosedReason.POOL_CLOSED) + elif conn.closed: + # CMAP requires the closed event be emitted after the check in. + if self.enabled_for_cmap: + assert listeners is not None + listeners.publish_connection_closed( + self.address, conn.id, ConnectionClosedReason.ERROR + ) + if _CONNECTION_LOGGER.isEnabledFor(logging.DEBUG): + _debug_log( + _CONNECTION_LOGGER, + clientId=self._client_id, + message=_ConnectionStatusMessage.CONN_CLOSED, + serverHost=self.address[0], + serverPort=self.address[1], + driverConnectionId=conn.id, + reason=_verbose_connection_error_reason(ConnectionClosedReason.ERROR), + error=ConnectionClosedReason.ERROR, + ) + else: + with self.lock: + # Hold the lock to ensure this section does not race with + # Pool.reset(). + if self.stale_generation(conn.generation, conn.service_id): + conn.close_conn(ConnectionClosedReason.STALE) + else: + conn.update_last_checkin_time() + conn.update_is_writable(bool(self.is_writable)) + self.conns.appendleft(conn) + # Notify any threads waiting to create a connection. + self._max_connecting_cond.notify() + + with self.size_cond: + if txn: + self.ntxns -= 1 + elif cursor: + self.ncursors -= 1 + self.requests -= 1 + self.active_sockets -= 1 + self.operation_count -= 1 + self.size_cond.notify() + + def _perished(self, conn: Connection) -> bool: + """Return True and close the connection if it is "perished". + + This side-effecty function checks if this socket has been idle for + for longer than the max idle time, or if the socket has been closed by + some external network error, or if the socket's generation is outdated. + + Checking sockets lets us avoid seeing *some* + :class:`~pymongo.errors.AutoReconnect` exceptions on server + hiccups, etc. We only check if the socket was closed by an external + error if it has been > 1 second since the socket was checked into the + pool, to keep performance reasonable - we can't avoid AutoReconnects + completely anyway. + """ + idle_time_seconds = conn.idle_time_seconds() + # If socket is idle, open a new one. + if ( + self.opts.max_idle_time_seconds is not None + and idle_time_seconds > self.opts.max_idle_time_seconds + ): + conn.close_conn(ConnectionClosedReason.IDLE) + return True + + if self._check_interval_seconds is not None and ( + self._check_interval_seconds == 0 or idle_time_seconds > self._check_interval_seconds + ): + if conn.conn_closed(): + conn.close_conn(ConnectionClosedReason.ERROR) + return True + + if self.stale_generation(conn.generation, conn.service_id): + conn.close_conn(ConnectionClosedReason.STALE) + return True + + return False + + def _raise_wait_queue_timeout(self, checkout_started_time: float) -> NoReturn: + listeners = self.opts._event_listeners + if self.enabled_for_cmap: + assert listeners is not None + duration = time.monotonic() - checkout_started_time + listeners.publish_connection_check_out_failed( + self.address, ConnectionCheckOutFailedReason.TIMEOUT, duration + ) + if _CONNECTION_LOGGER.isEnabledFor(logging.DEBUG): + _debug_log( + _CONNECTION_LOGGER, + clientId=self._client_id, + message=_ConnectionStatusMessage.CHECKOUT_FAILED, + serverHost=self.address[0], + serverPort=self.address[1], + reason="Wait queue timeout elapsed without a connection becoming available", + error=ConnectionCheckOutFailedReason.TIMEOUT, + durationMS=duration, + ) + timeout = _csot.get_timeout() or self.opts.wait_queue_timeout + if self.opts.load_balanced: + other_ops = self.active_sockets - self.ncursors - self.ntxns + raise WaitQueueTimeoutError( + "Timeout waiting for connection from the connection pool. " + "maxPoolSize: {}, connections in use by cursors: {}, " + "connections in use by transactions: {}, connections in use " + "by other operations: {}, timeout: {}".format( + self.opts.max_pool_size, + self.ncursors, + self.ntxns, + other_ops, + timeout, + ) + ) + raise WaitQueueTimeoutError( + "Timed out while checking out a connection from connection pool. " + f"maxPoolSize: {self.opts.max_pool_size}, timeout: {timeout}" + ) + + def __del__(self) -> None: + # Avoid ResourceWarnings in Python 3 + # Close all sockets without calling reset() or close() because it is + # not safe to acquire a lock in __del__. + for conn in self.conns: + conn.close_conn(None) diff --git a/venv/Lib/site-packages/pymongo/py.typed b/venv/Lib/site-packages/pymongo/py.typed new file mode 100644 index 00000000..0f405706 --- /dev/null +++ b/venv/Lib/site-packages/pymongo/py.typed @@ -0,0 +1,2 @@ +# PEP-561 Support File. +# "Package maintainers who wish to support type checking of their code MUST add a marker file named py.typed to their package supporting typing". diff --git a/venv/Lib/site-packages/pymongo/pyopenssl_context.py b/venv/Lib/site-packages/pymongo/pyopenssl_context.py new file mode 100644 index 00000000..fb007135 --- /dev/null +++ b/venv/Lib/site-packages/pymongo/pyopenssl_context.py @@ -0,0 +1,417 @@ +# Copyright 2019-present MongoDB, 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. + +"""A CPython compatible SSLContext implementation wrapping PyOpenSSL's +context. +""" +from __future__ import annotations + +import socket as _socket +import ssl as _stdlibssl +import sys as _sys +import time as _time +from errno import EINTR as _EINTR +from ipaddress import ip_address as _ip_address +from typing import TYPE_CHECKING, Any, Callable, Optional, TypeVar, Union + +from OpenSSL import SSL as _SSL +from OpenSSL import crypto as _crypto + +from pymongo._lazy_import import lazy_import +from pymongo.errors import ConfigurationError as _ConfigurationError +from pymongo.errors import _CertificateError # type:ignore[attr-defined] +from pymongo.ocsp_cache import _OCSPCache +from pymongo.ocsp_support import _load_trusted_ca_certs, _ocsp_callback +from pymongo.socket_checker import SocketChecker as _SocketChecker +from pymongo.socket_checker import _errno_from_exception +from pymongo.write_concern import validate_boolean + +_x509 = lazy_import("cryptography.x509") +_service_identity = lazy_import("service_identity") +_service_identity_pyopenssl = lazy_import("service_identity.pyopenssl") + +if TYPE_CHECKING: + from ssl import VerifyMode + + from cryptography.x509 import Certificate + +_T = TypeVar("_T") + +try: + import certifi + + _HAVE_CERTIFI = True +except ImportError: + _HAVE_CERTIFI = False + +PROTOCOL_SSLv23 = _SSL.SSLv23_METHOD +# Always available +OP_NO_SSLv2 = _SSL.OP_NO_SSLv2 +OP_NO_SSLv3 = _SSL.OP_NO_SSLv3 +OP_NO_COMPRESSION = _SSL.OP_NO_COMPRESSION +# This isn't currently documented for PyOpenSSL +OP_NO_RENEGOTIATION = getattr(_SSL, "OP_NO_RENEGOTIATION", 0) + +# Always available +HAS_SNI = True +IS_PYOPENSSL = True + +# Base Exception class +SSLError = _SSL.Error + +# https://github.com/python/cpython/blob/v3.8.0/Modules/_ssl.c#L2995-L3002 +_VERIFY_MAP = { + _stdlibssl.CERT_NONE: _SSL.VERIFY_NONE, + _stdlibssl.CERT_OPTIONAL: _SSL.VERIFY_PEER, + _stdlibssl.CERT_REQUIRED: _SSL.VERIFY_PEER | _SSL.VERIFY_FAIL_IF_NO_PEER_CERT, +} + +_REVERSE_VERIFY_MAP = {value: key for key, value in _VERIFY_MAP.items()} + + +# For SNI support. According to RFC6066, section 3, IPv4 and IPv6 literals are +# not permitted for SNI hostname. +def _is_ip_address(address: Any) -> bool: + try: + _ip_address(address) + return True + except (ValueError, UnicodeError): + return False + + +# According to the docs for socket.send it can raise +# WantX509LookupError and should be retried. +BLOCKING_IO_ERRORS = (_SSL.WantReadError, _SSL.WantWriteError, _SSL.WantX509LookupError) + + +def _ragged_eof(exc: BaseException) -> bool: + """Return True if the OpenSSL.SSL.SysCallError is a ragged EOF.""" + return exc.args == (-1, "Unexpected EOF") + + +# https://github.com/pyca/pyopenssl/issues/168 +# https://github.com/pyca/pyopenssl/issues/176 +# https://docs.python.org/3/library/ssl.html#notes-on-non-blocking-sockets +class _sslConn(_SSL.Connection): + def __init__( + self, ctx: _SSL.Context, sock: Optional[_socket.socket], suppress_ragged_eofs: bool + ): + self.socket_checker = _SocketChecker() + self.suppress_ragged_eofs = suppress_ragged_eofs + super().__init__(ctx, sock) + + def _call(self, call: Callable[..., _T], *args: Any, **kwargs: Any) -> _T: + timeout = self.gettimeout() + if timeout: + start = _time.monotonic() + while True: + try: + return call(*args, **kwargs) + except BLOCKING_IO_ERRORS as exc: + # Check for closed socket. + if self.fileno() == -1: + if timeout and _time.monotonic() - start > timeout: + raise _socket.timeout("timed out") from None + raise SSLError("Underlying socket has been closed") from None + if isinstance(exc, _SSL.WantReadError): + want_read = True + want_write = False + elif isinstance(exc, _SSL.WantWriteError): + want_read = False + want_write = True + else: + want_read = True + want_write = True + self.socket_checker.select(self, want_read, want_write, timeout) + if timeout and _time.monotonic() - start > timeout: + raise _socket.timeout("timed out") from None + continue + + def do_handshake(self, *args: Any, **kwargs: Any) -> None: + return self._call(super().do_handshake, *args, **kwargs) + + def recv(self, *args: Any, **kwargs: Any) -> bytes: + try: + return self._call(super().recv, *args, **kwargs) + except _SSL.SysCallError as exc: + # Suppress ragged EOFs to match the stdlib. + if self.suppress_ragged_eofs and _ragged_eof(exc): + return b"" + raise + + def recv_into(self, *args: Any, **kwargs: Any) -> int: + try: + return self._call(super().recv_into, *args, **kwargs) + except _SSL.SysCallError as exc: + # Suppress ragged EOFs to match the stdlib. + if self.suppress_ragged_eofs and _ragged_eof(exc): + return 0 + raise + + def sendall(self, buf: bytes, flags: int = 0) -> None: # type: ignore[override] + view = memoryview(buf) + total_length = len(buf) + total_sent = 0 + while total_sent < total_length: + try: + sent = self._call(super().send, view[total_sent:], flags) + # XXX: It's not clear if this can actually happen. PyOpenSSL + # doesn't appear to have any interrupt handling, nor any interrupt + # errors for OpenSSL connections. + except OSError as exc: + if _errno_from_exception(exc) == _EINTR: + continue + raise + # https://github.com/pyca/pyopenssl/blob/19.1.0/src/OpenSSL/SSL.py#L1756 + # https://www.openssl.org/docs/man1.0.2/man3/SSL_write.html + if sent <= 0: + raise OSError("connection closed") + total_sent += sent + + +class _CallbackData: + """Data class which is passed to the OCSP callback.""" + + def __init__(self) -> None: + self.trusted_ca_certs: Optional[list[Certificate]] = None + self.check_ocsp_endpoint: Optional[bool] = None + self.ocsp_response_cache = _OCSPCache() + + +class SSLContext: + """A CPython compatible SSLContext implementation wrapping PyOpenSSL's + context. + """ + + __slots__ = ("_protocol", "_ctx", "_callback_data", "_check_hostname") + + def __init__(self, protocol: int): + self._protocol = protocol + self._ctx = _SSL.Context(self._protocol) + self._callback_data = _CallbackData() + self._check_hostname = True + # OCSP + # XXX: Find a better place to do this someday, since this is client + # side configuration and wrap_socket tries to support both client and + # server side sockets. + self._callback_data.check_ocsp_endpoint = True + self._ctx.set_ocsp_client_callback(callback=_ocsp_callback, data=self._callback_data) + + @property + def protocol(self) -> int: + """The protocol version chosen when constructing the context. + This attribute is read-only. + """ + return self._protocol + + def __get_verify_mode(self) -> VerifyMode: + """Whether to try to verify other peers' certificates and how to + behave if verification fails. This attribute must be one of + ssl.CERT_NONE, ssl.CERT_OPTIONAL or ssl.CERT_REQUIRED. + """ + return _REVERSE_VERIFY_MAP[self._ctx.get_verify_mode()] + + def __set_verify_mode(self, value: VerifyMode) -> None: + """Setter for verify_mode.""" + + def _cb( + _connobj: _SSL.Connection, + _x509obj: _crypto.X509, + _errnum: int, + _errdepth: int, + retcode: int, + ) -> bool: + # It seems we don't need to do anything here. Twisted doesn't, + # and OpenSSL's SSL_CTX_set_verify let's you pass NULL + # for the callback option. It's weird that PyOpenSSL requires + # this. + # This is optional in pyopenssl >= 20 and can be removed once minimum + # supported version is bumped + # See: pyopenssl.org/en/latest/changelog.html#id47 + return bool(retcode) + + self._ctx.set_verify(_VERIFY_MAP[value], _cb) + + verify_mode = property(__get_verify_mode, __set_verify_mode) + + def __get_check_hostname(self) -> bool: + return self._check_hostname + + def __set_check_hostname(self, value: Any) -> None: + validate_boolean("check_hostname", value) + self._check_hostname = value + + check_hostname = property(__get_check_hostname, __set_check_hostname) + + def __get_check_ocsp_endpoint(self) -> Optional[bool]: + return self._callback_data.check_ocsp_endpoint + + def __set_check_ocsp_endpoint(self, value: bool) -> None: + validate_boolean("check_ocsp", value) + self._callback_data.check_ocsp_endpoint = value + + check_ocsp_endpoint = property(__get_check_ocsp_endpoint, __set_check_ocsp_endpoint) + + def __get_options(self) -> None: + # Calling set_options adds the option to the existing bitmask and + # returns the new bitmask. + # https://www.pyopenssl.org/en/stable/api/ssl.html#OpenSSL.SSL.Context.set_options + return self._ctx.set_options(0) + + def __set_options(self, value: int) -> None: + # Explicitly convert to int, since newer CPython versions + # use enum.IntFlag for options. The values are the same + # regardless of implementation. + self._ctx.set_options(int(value)) + + options = property(__get_options, __set_options) + + def load_cert_chain( + self, + certfile: Union[str, bytes], + keyfile: Union[str, bytes, None] = None, + password: Optional[str] = None, + ) -> None: + """Load a private key and the corresponding certificate. The certfile + string must be the path to a single file in PEM format containing the + certificate as well as any number of CA certificates needed to + establish the certificate's authenticity. The keyfile string, if + present, must point to a file containing the private key. Otherwise + the private key will be taken from certfile as well. + """ + # Match CPython behavior + # https://github.com/python/cpython/blob/v3.8.0/Modules/_ssl.c#L3930-L3971 + # Password callback MUST be set first or it will be ignored. + if password: + + def _pwcb(_max_length: int, _prompt_twice: bool, _user_data: bytes) -> bytes: + # XXX:We could check the password length against what OpenSSL + # tells us is the max, but we can't raise an exception, so... + # warn? + assert password is not None + return password.encode("utf-8") + + self._ctx.set_passwd_cb(_pwcb) + self._ctx.use_certificate_chain_file(certfile) + self._ctx.use_privatekey_file(keyfile or certfile) + self._ctx.check_privatekey() + + def load_verify_locations( + self, cafile: Optional[str] = None, capath: Optional[str] = None + ) -> None: + """Load a set of "certification authority"(CA) certificates used to + validate other peers' certificates when `~verify_mode` is other than + ssl.CERT_NONE. + """ + self._ctx.load_verify_locations(cafile, capath) + # Manually load the CA certs when get_verified_chain is not available (pyopenssl<20). + if not hasattr(_SSL.Connection, "get_verified_chain"): + assert cafile is not None + self._callback_data.trusted_ca_certs = _load_trusted_ca_certs(cafile) + + def _load_certifi(self) -> None: + """Attempt to load CA certs from certifi.""" + if _HAVE_CERTIFI: + self.load_verify_locations(certifi.where()) + else: + raise _ConfigurationError( + "tlsAllowInvalidCertificates is False but no system " + "CA certificates could be loaded. Please install the " + "certifi package, or provide a path to a CA file using " + "the tlsCAFile option" + ) + + def _load_wincerts(self, store: str) -> None: + """Attempt to load CA certs from Windows trust store.""" + cert_store = self._ctx.get_cert_store() + oid = _stdlibssl.Purpose.SERVER_AUTH.oid + for cert, encoding, trust in _stdlibssl.enum_certificates(store): # type: ignore + if encoding == "x509_asn": + if trust is True or oid in trust: + cert_store.add_cert( + _crypto.X509.from_cryptography(_x509.load_der_x509_certificate(cert)) + ) + + def load_default_certs(self) -> None: + """A PyOpenSSL version of load_default_certs from CPython.""" + # PyOpenSSL is incapable of loading CA certs from Windows, and mostly + # incapable on macOS. + # https://www.pyopenssl.org/en/stable/api/ssl.html#OpenSSL.SSL.Context.set_default_verify_paths + if _sys.platform == "win32": + try: + for storename in ("CA", "ROOT"): + self._load_wincerts(storename) + except PermissionError: + # Fall back to certifi + self._load_certifi() + elif _sys.platform == "darwin": + self._load_certifi() + self._ctx.set_default_verify_paths() + + def set_default_verify_paths(self) -> None: + """Specify that the platform provided CA certificates are to be used + for verification purposes. + """ + # Note: See PyOpenSSL's docs for limitations, which are similar + # but not that same as CPython's. + self._ctx.set_default_verify_paths() + + def wrap_socket( + self, + sock: _socket.socket, + server_side: bool = False, + do_handshake_on_connect: bool = True, + suppress_ragged_eofs: bool = True, + server_hostname: Optional[str] = None, + session: Optional[_SSL.Session] = None, + ) -> _sslConn: + """Wrap an existing Python socket connection and return a TLS socket + object. + """ + ssl_conn = _sslConn(self._ctx, sock, suppress_ragged_eofs) + if session: + ssl_conn.set_session(session) + if server_side is True: + ssl_conn.set_accept_state() + else: + # SNI + if server_hostname and not _is_ip_address(server_hostname): + # XXX: Do this in a callback registered with + # SSLContext.set_info_callback? See Twisted for an example. + ssl_conn.set_tlsext_host_name(server_hostname.encode("idna")) + if self.verify_mode != _stdlibssl.CERT_NONE: + # Request a stapled OCSP response. + ssl_conn.request_ocsp() + ssl_conn.set_connect_state() + # If this wasn't true the caller of wrap_socket would call + # do_handshake() + if do_handshake_on_connect: + # XXX: If we do hostname checking in a callback we can get rid + # of this call to do_handshake() since the handshake + # will happen automatically later. + ssl_conn.do_handshake() + # XXX: Do this in a callback registered with + # SSLContext.set_info_callback? See Twisted for an example. + if self.check_hostname and server_hostname is not None: + try: + if _is_ip_address(server_hostname): + _service_identity_pyopenssl.verify_ip_address(ssl_conn, server_hostname) + else: + _service_identity_pyopenssl.verify_hostname(ssl_conn, server_hostname) + except ( + _service_identity.SICertificateError, + _service_identity.SIVerificationError, + ) as exc: + raise _CertificateError(str(exc)) from None + return ssl_conn diff --git a/venv/Lib/site-packages/pymongo/read_concern.py b/venv/Lib/site-packages/pymongo/read_concern.py new file mode 100644 index 00000000..eda715f7 --- /dev/null +++ b/venv/Lib/site-packages/pymongo/read_concern.py @@ -0,0 +1,76 @@ +# Copyright 2015 MongoDB, 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. + +"""Tools for working with read concerns.""" +from __future__ import annotations + +from typing import Any, Optional + + +class ReadConcern: + """ReadConcern + + :param level: (string) The read concern level specifies the level of + isolation for read operations. For example, a read operation using a + read concern level of ``majority`` will only return data that has been + written to a majority of nodes. If the level is left unspecified, the + server default will be used. + + .. versionadded:: 3.2 + + """ + + def __init__(self, level: Optional[str] = None) -> None: + if level is None or isinstance(level, str): + self.__level = level + else: + raise TypeError("level must be a string or None.") + + @property + def level(self) -> Optional[str]: + """The read concern level.""" + return self.__level + + @property + def ok_for_legacy(self) -> bool: + """Return ``True`` if this read concern is compatible with + old wire protocol versions. + """ + return self.level is None or self.level == "local" + + @property + def document(self) -> dict[str, Any]: + """The document representation of this read concern. + + .. note:: + :class:`ReadConcern` is immutable. Mutating the value of + :attr:`document` does not mutate this :class:`ReadConcern`. + """ + doc = {} + if self.__level: + doc["level"] = self.level + return doc + + def __eq__(self, other: Any) -> bool: + if isinstance(other, ReadConcern): + return self.document == other.document + return NotImplemented + + def __repr__(self) -> str: + if self.level: + return "ReadConcern(%s)" % self.level + return "ReadConcern()" + + +DEFAULT_READ_CONCERN = ReadConcern() diff --git a/venv/Lib/site-packages/pymongo/read_preferences.py b/venv/Lib/site-packages/pymongo/read_preferences.py new file mode 100644 index 00000000..7752750c --- /dev/null +++ b/venv/Lib/site-packages/pymongo/read_preferences.py @@ -0,0 +1,622 @@ +# Copyright 2012-present MongoDB, 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. + +"""Utilities for choosing which member of a replica set to read from.""" + +from __future__ import annotations + +from collections import abc +from typing import TYPE_CHECKING, Any, Mapping, Optional, Sequence + +from pymongo import max_staleness_selectors +from pymongo.errors import ConfigurationError +from pymongo.server_selectors import ( + member_with_tags_server_selector, + secondary_with_tags_server_selector, +) + +if TYPE_CHECKING: + from pymongo.server_selectors import Selection + from pymongo.topology_description import TopologyDescription + +_PRIMARY = 0 +_PRIMARY_PREFERRED = 1 +_SECONDARY = 2 +_SECONDARY_PREFERRED = 3 +_NEAREST = 4 + + +_MONGOS_MODES = ( + "primary", + "primaryPreferred", + "secondary", + "secondaryPreferred", + "nearest", +) + +_Hedge = Mapping[str, Any] +_TagSets = Sequence[Mapping[str, Any]] + + +def _validate_tag_sets(tag_sets: Optional[_TagSets]) -> Optional[_TagSets]: + """Validate tag sets for a MongoClient.""" + if tag_sets is None: + return tag_sets + + if not isinstance(tag_sets, (list, tuple)): + raise TypeError(f"Tag sets {tag_sets!r} invalid, must be a sequence") + if len(tag_sets) == 0: + raise ValueError( + f"Tag sets {tag_sets!r} invalid, must be None or contain at least one set of tags" + ) + + for tags in tag_sets: + if not isinstance(tags, abc.Mapping): + raise TypeError( + f"Tag set {tags!r} invalid, must be an instance of dict, " + "bson.son.SON or other type that inherits from " + "collection.Mapping" + ) + + return list(tag_sets) + + +def _invalid_max_staleness_msg(max_staleness: Any) -> str: + return "maxStalenessSeconds must be a positive integer, not %s" % max_staleness + + +# Some duplication with common.py to avoid import cycle. +def _validate_max_staleness(max_staleness: Any) -> int: + """Validate max_staleness.""" + if max_staleness == -1: + return -1 + + if not isinstance(max_staleness, int): + raise TypeError(_invalid_max_staleness_msg(max_staleness)) + + if max_staleness <= 0: + raise ValueError(_invalid_max_staleness_msg(max_staleness)) + + return max_staleness + + +def _validate_hedge(hedge: Optional[_Hedge]) -> Optional[_Hedge]: + """Validate hedge.""" + if hedge is None: + return None + + if not isinstance(hedge, dict): + raise TypeError(f"hedge must be a dictionary, not {hedge!r}") + + return hedge + + +class _ServerMode: + """Base class for all read preferences.""" + + __slots__ = ("__mongos_mode", "__mode", "__tag_sets", "__max_staleness", "__hedge") + + def __init__( + self, + mode: int, + tag_sets: Optional[_TagSets] = None, + max_staleness: int = -1, + hedge: Optional[_Hedge] = None, + ) -> None: + self.__mongos_mode = _MONGOS_MODES[mode] + self.__mode = mode + self.__tag_sets = _validate_tag_sets(tag_sets) + self.__max_staleness = _validate_max_staleness(max_staleness) + self.__hedge = _validate_hedge(hedge) + + @property + def name(self) -> str: + """The name of this read preference.""" + return self.__class__.__name__ + + @property + def mongos_mode(self) -> str: + """The mongos mode of this read preference.""" + return self.__mongos_mode + + @property + def document(self) -> dict[str, Any]: + """Read preference as a document.""" + doc: dict[str, Any] = {"mode": self.__mongos_mode} + if self.__tag_sets not in (None, [{}]): + doc["tags"] = self.__tag_sets + if self.__max_staleness != -1: + doc["maxStalenessSeconds"] = self.__max_staleness + if self.__hedge not in (None, {}): + doc["hedge"] = self.__hedge + return doc + + @property + def mode(self) -> int: + """The mode of this read preference instance.""" + return self.__mode + + @property + def tag_sets(self) -> _TagSets: + """Set ``tag_sets`` to a list of dictionaries like [{'dc': 'ny'}] to + read only from members whose ``dc`` tag has the value ``"ny"``. + To specify a priority-order for tag sets, provide a list of + tag sets: ``[{'dc': 'ny'}, {'dc': 'la'}, {}]``. A final, empty tag + set, ``{}``, means "read from any member that matches the mode, + ignoring tags." MongoClient tries each set of tags in turn + until it finds a set of tags with at least one matching member. + For example, to only send a query to an analytic node:: + + Nearest(tag_sets=[{"node":"analytics"}]) + + Or using :class:`SecondaryPreferred`:: + + SecondaryPreferred(tag_sets=[{"node":"analytics"}]) + + .. seealso:: `Data-Center Awareness + `_ + """ + return list(self.__tag_sets) if self.__tag_sets else [{}] + + @property + def max_staleness(self) -> int: + """The maximum estimated length of time (in seconds) a replica set + secondary can fall behind the primary in replication before it will + no longer be selected for operations, or -1 for no maximum. + """ + return self.__max_staleness + + @property + def hedge(self) -> Optional[_Hedge]: + """The read preference ``hedge`` parameter. + + A dictionary that configures how the server will perform hedged reads. + It consists of the following keys: + + - ``enabled``: Enables or disables hedged reads in sharded clusters. + + Hedged reads are automatically enabled in MongoDB 4.4+ when using a + ``nearest`` read preference. To explicitly enable hedged reads, set + the ``enabled`` key to ``true``:: + + >>> Nearest(hedge={'enabled': True}) + + To explicitly disable hedged reads, set the ``enabled`` key to + ``False``:: + + >>> Nearest(hedge={'enabled': False}) + + .. versionadded:: 3.11 + """ + return self.__hedge + + @property + def min_wire_version(self) -> int: + """The wire protocol version the server must support. + + Some read preferences impose version requirements on all servers (e.g. + maxStalenessSeconds requires MongoDB 3.4 / maxWireVersion 5). + + All servers' maxWireVersion must be at least this read preference's + `min_wire_version`, or the driver raises + :exc:`~pymongo.errors.ConfigurationError`. + """ + return 0 if self.__max_staleness == -1 else 5 + + def __repr__(self) -> str: + return "{}(tag_sets={!r}, max_staleness={!r}, hedge={!r})".format( + self.name, + self.__tag_sets, + self.__max_staleness, + self.__hedge, + ) + + def __eq__(self, other: Any) -> bool: + if isinstance(other, _ServerMode): + return ( + self.mode == other.mode + and self.tag_sets == other.tag_sets + and self.max_staleness == other.max_staleness + and self.hedge == other.hedge + ) + return NotImplemented + + def __ne__(self, other: Any) -> bool: + return not self == other + + def __getstate__(self) -> dict[str, Any]: + """Return value of object for pickling. + + Needed explicitly because __slots__() defined. + """ + return { + "mode": self.__mode, + "tag_sets": self.__tag_sets, + "max_staleness": self.__max_staleness, + "hedge": self.__hedge, + } + + def __setstate__(self, value: Mapping[str, Any]) -> None: + """Restore from pickling.""" + self.__mode = value["mode"] + self.__mongos_mode = _MONGOS_MODES[self.__mode] + self.__tag_sets = _validate_tag_sets(value["tag_sets"]) + self.__max_staleness = _validate_max_staleness(value["max_staleness"]) + self.__hedge = _validate_hedge(value["hedge"]) + + def __call__(self, selection: Selection) -> Selection: + return selection + + +class Primary(_ServerMode): + """Primary read preference. + + * When directly connected to one mongod queries are allowed if the server + is standalone or a replica set primary. + * When connected to a mongos queries are sent to the primary of a shard. + * When connected to a replica set queries are sent to the primary of + the replica set. + """ + + __slots__ = () + + def __init__(self) -> None: + super().__init__(_PRIMARY) + + def __call__(self, selection: Selection) -> Selection: + """Apply this read preference to a Selection.""" + return selection.primary_selection + + def __repr__(self) -> str: + return "Primary()" + + def __eq__(self, other: Any) -> bool: + if isinstance(other, _ServerMode): + return other.mode == _PRIMARY + return NotImplemented + + +class PrimaryPreferred(_ServerMode): + """PrimaryPreferred read preference. + + * When directly connected to one mongod queries are allowed to standalone + servers, to a replica set primary, or to replica set secondaries. + * When connected to a mongos queries are sent to the primary of a shard if + available, otherwise a shard secondary. + * When connected to a replica set queries are sent to the primary if + available, otherwise a secondary. + + .. note:: When a :class:`~pymongo.mongo_client.MongoClient` is first + created reads will be routed to an available secondary until the + primary of the replica set is discovered. + + :param tag_sets: The :attr:`~tag_sets` to use if the primary is not + available. + :param max_staleness: (integer, in seconds) The maximum estimated + length of time a replica set secondary can fall behind the primary in + replication before it will no longer be selected for operations. + Default -1, meaning no maximum. If it is set, it must be at least + 90 seconds. + :param hedge: The :attr:`~hedge` to use if the primary is not available. + + .. versionchanged:: 3.11 + Added ``hedge`` parameter. + """ + + __slots__ = () + + def __init__( + self, + tag_sets: Optional[_TagSets] = None, + max_staleness: int = -1, + hedge: Optional[_Hedge] = None, + ) -> None: + super().__init__(_PRIMARY_PREFERRED, tag_sets, max_staleness, hedge) + + def __call__(self, selection: Selection) -> Selection: + """Apply this read preference to Selection.""" + if selection.primary: + return selection.primary_selection + else: + return secondary_with_tags_server_selector( + self.tag_sets, max_staleness_selectors.select(self.max_staleness, selection) + ) + + +class Secondary(_ServerMode): + """Secondary read preference. + + * When directly connected to one mongod queries are allowed to standalone + servers, to a replica set primary, or to replica set secondaries. + * When connected to a mongos queries are distributed among shard + secondaries. An error is raised if no secondaries are available. + * When connected to a replica set queries are distributed among + secondaries. An error is raised if no secondaries are available. + + :param tag_sets: The :attr:`~tag_sets` for this read preference. + :param max_staleness: (integer, in seconds) The maximum estimated + length of time a replica set secondary can fall behind the primary in + replication before it will no longer be selected for operations. + Default -1, meaning no maximum. If it is set, it must be at least + 90 seconds. + :param hedge: The :attr:`~hedge` for this read preference. + + .. versionchanged:: 3.11 + Added ``hedge`` parameter. + """ + + __slots__ = () + + def __init__( + self, + tag_sets: Optional[_TagSets] = None, + max_staleness: int = -1, + hedge: Optional[_Hedge] = None, + ) -> None: + super().__init__(_SECONDARY, tag_sets, max_staleness, hedge) + + def __call__(self, selection: Selection) -> Selection: + """Apply this read preference to Selection.""" + return secondary_with_tags_server_selector( + self.tag_sets, max_staleness_selectors.select(self.max_staleness, selection) + ) + + +class SecondaryPreferred(_ServerMode): + """SecondaryPreferred read preference. + + * When directly connected to one mongod queries are allowed to standalone + servers, to a replica set primary, or to replica set secondaries. + * When connected to a mongos queries are distributed among shard + secondaries, or the shard primary if no secondary is available. + * When connected to a replica set queries are distributed among + secondaries, or the primary if no secondary is available. + + .. note:: When a :class:`~pymongo.mongo_client.MongoClient` is first + created reads will be routed to the primary of the replica set until + an available secondary is discovered. + + :param tag_sets: The :attr:`~tag_sets` for this read preference. + :param max_staleness: (integer, in seconds) The maximum estimated + length of time a replica set secondary can fall behind the primary in + replication before it will no longer be selected for operations. + Default -1, meaning no maximum. If it is set, it must be at least + 90 seconds. + :param hedge: The :attr:`~hedge` for this read preference. + + .. versionchanged:: 3.11 + Added ``hedge`` parameter. + """ + + __slots__ = () + + def __init__( + self, + tag_sets: Optional[_TagSets] = None, + max_staleness: int = -1, + hedge: Optional[_Hedge] = None, + ) -> None: + super().__init__(_SECONDARY_PREFERRED, tag_sets, max_staleness, hedge) + + def __call__(self, selection: Selection) -> Selection: + """Apply this read preference to Selection.""" + secondaries = secondary_with_tags_server_selector( + self.tag_sets, max_staleness_selectors.select(self.max_staleness, selection) + ) + + if secondaries: + return secondaries + else: + return selection.primary_selection + + +class Nearest(_ServerMode): + """Nearest read preference. + + * When directly connected to one mongod queries are allowed to standalone + servers, to a replica set primary, or to replica set secondaries. + * When connected to a mongos queries are distributed among all members of + a shard. + * When connected to a replica set queries are distributed among all + members. + + :param tag_sets: The :attr:`~tag_sets` for this read preference. + :param max_staleness: (integer, in seconds) The maximum estimated + length of time a replica set secondary can fall behind the primary in + replication before it will no longer be selected for operations. + Default -1, meaning no maximum. If it is set, it must be at least + 90 seconds. + :param hedge: The :attr:`~hedge` for this read preference. + + .. versionchanged:: 3.11 + Added ``hedge`` parameter. + """ + + __slots__ = () + + def __init__( + self, + tag_sets: Optional[_TagSets] = None, + max_staleness: int = -1, + hedge: Optional[_Hedge] = None, + ) -> None: + super().__init__(_NEAREST, tag_sets, max_staleness, hedge) + + def __call__(self, selection: Selection) -> Selection: + """Apply this read preference to Selection.""" + return member_with_tags_server_selector( + self.tag_sets, max_staleness_selectors.select(self.max_staleness, selection) + ) + + +class _AggWritePref: + """Agg $out/$merge write preference. + + * If there are readable servers and there is any pre-5.0 server, use + primary read preference. + * Otherwise use `pref` read preference. + + :param pref: The read preference to use on MongoDB 5.0+. + """ + + __slots__ = ("pref", "effective_pref") + + def __init__(self, pref: _ServerMode): + self.pref = pref + self.effective_pref: _ServerMode = ReadPreference.PRIMARY + + def selection_hook(self, topology_description: TopologyDescription) -> None: + common_wv = topology_description.common_wire_version + if ( + topology_description.has_readable_server(ReadPreference.PRIMARY_PREFERRED) + and common_wv + and common_wv < 13 + ): + self.effective_pref = ReadPreference.PRIMARY + else: + self.effective_pref = self.pref + + def __call__(self, selection: Selection) -> Selection: + """Apply this read preference to a Selection.""" + return self.effective_pref(selection) + + def __repr__(self) -> str: + return f"_AggWritePref(pref={self.pref!r})" + + # Proxy other calls to the effective_pref so that _AggWritePref can be + # used in place of an actual read preference. + def __getattr__(self, name: str) -> Any: + return getattr(self.effective_pref, name) + + +_ALL_READ_PREFERENCES = (Primary, PrimaryPreferred, Secondary, SecondaryPreferred, Nearest) + + +def make_read_preference( + mode: int, tag_sets: Optional[_TagSets], max_staleness: int = -1 +) -> _ServerMode: + if mode == _PRIMARY: + if tag_sets not in (None, [{}]): + raise ConfigurationError("Read preference primary cannot be combined with tags") + if max_staleness != -1: + raise ConfigurationError( + "Read preference primary cannot be combined with maxStalenessSeconds" + ) + return Primary() + return _ALL_READ_PREFERENCES[mode](tag_sets, max_staleness) # type: ignore + + +_MODES = ( + "PRIMARY", + "PRIMARY_PREFERRED", + "SECONDARY", + "SECONDARY_PREFERRED", + "NEAREST", +) + + +class ReadPreference: + """An enum that defines some commonly used read preference modes. + + Apps can also create a custom read preference, for example:: + + Nearest(tag_sets=[{"node":"analytics"}]) + + See :doc:`/examples/high_availability` for code examples. + + A read preference is used in three cases: + + :class:`~pymongo.mongo_client.MongoClient` connected to a single mongod: + + - ``PRIMARY``: Queries are allowed if the server is standalone or a replica + set primary. + - All other modes allow queries to standalone servers, to a replica set + primary, or to replica set secondaries. + + :class:`~pymongo.mongo_client.MongoClient` initialized with the + ``replicaSet`` option: + + - ``PRIMARY``: Read from the primary. This is the default, and provides the + strongest consistency. If no primary is available, raise + :class:`~pymongo.errors.AutoReconnect`. + + - ``PRIMARY_PREFERRED``: Read from the primary if available, or if there is + none, read from a secondary. + + - ``SECONDARY``: Read from a secondary. If no secondary is available, + raise :class:`~pymongo.errors.AutoReconnect`. + + - ``SECONDARY_PREFERRED``: Read from a secondary if available, otherwise + from the primary. + + - ``NEAREST``: Read from any member. + + :class:`~pymongo.mongo_client.MongoClient` connected to a mongos, with a + sharded cluster of replica sets: + + - ``PRIMARY``: Read from the primary of the shard, or raise + :class:`~pymongo.errors.OperationFailure` if there is none. + This is the default. + + - ``PRIMARY_PREFERRED``: Read from the primary of the shard, or if there is + none, read from a secondary of the shard. + + - ``SECONDARY``: Read from a secondary of the shard, or raise + :class:`~pymongo.errors.OperationFailure` if there is none. + + - ``SECONDARY_PREFERRED``: Read from a secondary of the shard if available, + otherwise from the shard primary. + + - ``NEAREST``: Read from any shard member. + """ + + PRIMARY = Primary() + PRIMARY_PREFERRED = PrimaryPreferred() + SECONDARY = Secondary() + SECONDARY_PREFERRED = SecondaryPreferred() + NEAREST = Nearest() + + +def read_pref_mode_from_name(name: str) -> int: + """Get the read preference mode from mongos/uri name.""" + return _MONGOS_MODES.index(name) + + +class MovingAverage: + """Tracks an exponentially-weighted moving average.""" + + average: Optional[float] + + def __init__(self) -> None: + self.average = None + + def add_sample(self, sample: float) -> None: + if sample < 0: + # Likely system time change while waiting for hello response + # and not using time.monotonic. Ignore it, the next one will + # probably be valid. + return + if self.average is None: + self.average = sample + else: + # The Server Selection Spec requires an exponentially weighted + # average with alpha = 0.2. + self.average = 0.8 * self.average + 0.2 * sample + + def get(self) -> Optional[float]: + """Get the calculated average, or None if no samples yet.""" + return self.average + + def reset(self) -> None: + self.average = None diff --git a/venv/Lib/site-packages/pymongo/response.py b/venv/Lib/site-packages/pymongo/response.py new file mode 100644 index 00000000..5cdd3e7e --- /dev/null +++ b/venv/Lib/site-packages/pymongo/response.py @@ -0,0 +1,131 @@ +# Copyright 2014-present MongoDB, 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. + +"""Represent a response from the server.""" +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, Mapping, Optional, Sequence, Union + +if TYPE_CHECKING: + from datetime import timedelta + + from pymongo.message import _OpMsg, _OpReply + from pymongo.pool import Connection + from pymongo.typings import _Address, _DocumentOut + + +class Response: + __slots__ = ("_data", "_address", "_request_id", "_duration", "_from_command", "_docs") + + def __init__( + self, + data: Union[_OpMsg, _OpReply], + address: _Address, + request_id: int, + duration: Optional[timedelta], + from_command: bool, + docs: Sequence[Mapping[str, Any]], + ): + """Represent a response from the server. + + :param data: A network response message. + :param address: (host, port) of the source server. + :param request_id: The request id of this operation. + :param duration: The duration of the operation. + :param from_command: if the response is the result of a db command. + """ + self._data = data + self._address = address + self._request_id = request_id + self._duration = duration + self._from_command = from_command + self._docs = docs + + @property + def data(self) -> Union[_OpMsg, _OpReply]: + """Server response's raw BSON bytes.""" + return self._data + + @property + def address(self) -> _Address: + """(host, port) of the source server.""" + return self._address + + @property + def request_id(self) -> int: + """The request id of this operation.""" + return self._request_id + + @property + def duration(self) -> Optional[timedelta]: + """The duration of the operation.""" + return self._duration + + @property + def from_command(self) -> bool: + """If the response is a result from a db command.""" + return self._from_command + + @property + def docs(self) -> Sequence[Mapping[str, Any]]: + """The decoded document(s).""" + return self._docs + + +class PinnedResponse(Response): + __slots__ = ("_conn", "_more_to_come") + + def __init__( + self, + data: Union[_OpMsg, _OpReply], + address: _Address, + conn: Connection, + request_id: int, + duration: Optional[timedelta], + from_command: bool, + docs: list[_DocumentOut], + more_to_come: bool, + ): + """Represent a response to an exhaust cursor's initial query. + + :param data: A network response message. + :param address: (host, port) of the source server. + :param conn: The Connection used for the initial query. + :param request_id: The request id of this operation. + :param duration: The duration of the operation. + :param from_command: If the response is the result of a db command. + :param docs: List of documents. + :param more_to_come: Bool indicating whether cursor is ready to be + exhausted. + """ + super().__init__(data, address, request_id, duration, from_command, docs) + self._conn = conn + self._more_to_come = more_to_come + + @property + def conn(self) -> Connection: + """The Connection used for the initial query. + + The server will send batches on this socket, without waiting for + getMores from the client, until the result set is exhausted or there + is an error. + """ + return self._conn + + @property + def more_to_come(self) -> bool: + """If true, server is ready to send batches on the socket until the + result set is exhausted or there is an error. + """ + return self._more_to_come diff --git a/venv/Lib/site-packages/pymongo/results.py b/venv/Lib/site-packages/pymongo/results.py new file mode 100644 index 00000000..f5728656 --- /dev/null +++ b/venv/Lib/site-packages/pymongo/results.py @@ -0,0 +1,242 @@ +# Copyright 2015-present MongoDB, 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. + +"""Result class definitions.""" +from __future__ import annotations + +from typing import Any, Mapping, Optional, cast + +from pymongo.errors import InvalidOperation + + +class _WriteResult: + """Base class for write result classes.""" + + __slots__ = ("__acknowledged",) + + def __init__(self, acknowledged: bool) -> None: + self.__acknowledged = acknowledged + + def __repr__(self) -> str: + return f"{self.__class__.__name__}({self.__acknowledged})" + + def _raise_if_unacknowledged(self, property_name: str) -> None: + """Raise an exception on property access if unacknowledged.""" + if not self.__acknowledged: + raise InvalidOperation( + f"A value for {property_name} is not available when " + "the write is unacknowledged. Check the " + "acknowledged attribute to avoid this " + "error." + ) + + @property + def acknowledged(self) -> bool: + """Is this the result of an acknowledged write operation? + + The :attr:`acknowledged` attribute will be ``False`` when using + ``WriteConcern(w=0)``, otherwise ``True``. + + .. note:: + If the :attr:`acknowledged` attribute is ``False`` all other + attributes of this class will raise + :class:`~pymongo.errors.InvalidOperation` when accessed. Values for + other attributes cannot be determined if the write operation was + unacknowledged. + + .. seealso:: + :class:`~pymongo.write_concern.WriteConcern` + """ + return self.__acknowledged + + +class InsertOneResult(_WriteResult): + """The return type for :meth:`~pymongo.collection.Collection.insert_one`.""" + + __slots__ = ("__inserted_id",) + + def __init__(self, inserted_id: Any, acknowledged: bool) -> None: + self.__inserted_id = inserted_id + super().__init__(acknowledged) + + def __repr__(self) -> str: + return ( + f"{self.__class__.__name__}({self.__inserted_id!r}, acknowledged={self.acknowledged})" + ) + + @property + def inserted_id(self) -> Any: + """The inserted document's _id.""" + return self.__inserted_id + + +class InsertManyResult(_WriteResult): + """The return type for :meth:`~pymongo.collection.Collection.insert_many`.""" + + __slots__ = ("__inserted_ids",) + + def __init__(self, inserted_ids: list[Any], acknowledged: bool) -> None: + self.__inserted_ids = inserted_ids + super().__init__(acknowledged) + + def __repr__(self) -> str: + return ( + f"{self.__class__.__name__}({self.__inserted_ids!r}, acknowledged={self.acknowledged})" + ) + + @property + def inserted_ids(self) -> list[Any]: + """A list of _ids of the inserted documents, in the order provided. + + .. note:: If ``False`` is passed for the `ordered` parameter to + :meth:`~pymongo.collection.Collection.insert_many` the server + may have inserted the documents in a different order than what + is presented here. + """ + return self.__inserted_ids + + +class UpdateResult(_WriteResult): + """The return type for :meth:`~pymongo.collection.Collection.update_one`, + :meth:`~pymongo.collection.Collection.update_many`, and + :meth:`~pymongo.collection.Collection.replace_one`. + """ + + __slots__ = ("__raw_result",) + + def __init__(self, raw_result: Optional[Mapping[str, Any]], acknowledged: bool): + self.__raw_result = raw_result + super().__init__(acknowledged) + + def __repr__(self) -> str: + return f"{self.__class__.__name__}({self.__raw_result!r}, acknowledged={self.acknowledged})" + + @property + def raw_result(self) -> Optional[Mapping[str, Any]]: + """The raw result document returned by the server.""" + return self.__raw_result + + @property + def matched_count(self) -> int: + """The number of documents matched for this update.""" + self._raise_if_unacknowledged("matched_count") + if self.upserted_id is not None: + return 0 + assert self.__raw_result is not None + return self.__raw_result.get("n", 0) + + @property + def modified_count(self) -> int: + """The number of documents modified.""" + self._raise_if_unacknowledged("modified_count") + assert self.__raw_result is not None + return cast(int, self.__raw_result.get("nModified")) + + @property + def upserted_id(self) -> Any: + """The _id of the inserted document if an upsert took place. Otherwise + ``None``. + """ + self._raise_if_unacknowledged("upserted_id") + assert self.__raw_result is not None + return self.__raw_result.get("upserted") + + +class DeleteResult(_WriteResult): + """The return type for :meth:`~pymongo.collection.Collection.delete_one` + and :meth:`~pymongo.collection.Collection.delete_many` + """ + + __slots__ = ("__raw_result",) + + def __init__(self, raw_result: Mapping[str, Any], acknowledged: bool) -> None: + self.__raw_result = raw_result + super().__init__(acknowledged) + + def __repr__(self) -> str: + return f"{self.__class__.__name__}({self.__raw_result!r}, acknowledged={self.acknowledged})" + + @property + def raw_result(self) -> Mapping[str, Any]: + """The raw result document returned by the server.""" + return self.__raw_result + + @property + def deleted_count(self) -> int: + """The number of documents deleted.""" + self._raise_if_unacknowledged("deleted_count") + return self.__raw_result.get("n", 0) + + +class BulkWriteResult(_WriteResult): + """An object wrapper for bulk API write results.""" + + __slots__ = ("__bulk_api_result",) + + def __init__(self, bulk_api_result: dict[str, Any], acknowledged: bool) -> None: + """Create a BulkWriteResult instance. + + :param bulk_api_result: A result dict from the bulk API + :param acknowledged: Was this write result acknowledged? If ``False`` + then all properties of this object will raise + :exc:`~pymongo.errors.InvalidOperation`. + """ + self.__bulk_api_result = bulk_api_result + super().__init__(acknowledged) + + def __repr__(self) -> str: + return f"{self.__class__.__name__}({self.__bulk_api_result!r}, acknowledged={self.acknowledged})" + + @property + def bulk_api_result(self) -> dict[str, Any]: + """The raw bulk API result.""" + return self.__bulk_api_result + + @property + def inserted_count(self) -> int: + """The number of documents inserted.""" + self._raise_if_unacknowledged("inserted_count") + return cast(int, self.__bulk_api_result.get("nInserted")) + + @property + def matched_count(self) -> int: + """The number of documents matched for an update.""" + self._raise_if_unacknowledged("matched_count") + return cast(int, self.__bulk_api_result.get("nMatched")) + + @property + def modified_count(self) -> int: + """The number of documents modified.""" + self._raise_if_unacknowledged("modified_count") + return cast(int, self.__bulk_api_result.get("nModified")) + + @property + def deleted_count(self) -> int: + """The number of documents deleted.""" + self._raise_if_unacknowledged("deleted_count") + return cast(int, self.__bulk_api_result.get("nRemoved")) + + @property + def upserted_count(self) -> int: + """The number of documents upserted.""" + self._raise_if_unacknowledged("upserted_count") + return cast(int, self.__bulk_api_result.get("nUpserted")) + + @property + def upserted_ids(self) -> Optional[dict[int, Any]]: + """A map of operation index to the _id of the upserted document.""" + self._raise_if_unacknowledged("upserted_ids") + if self.__bulk_api_result: + return {upsert["index"]: upsert["_id"] for upsert in self.bulk_api_result["upserted"]} + return None diff --git a/venv/Lib/site-packages/pymongo/saslprep.py b/venv/Lib/site-packages/pymongo/saslprep.py new file mode 100644 index 00000000..7fb546f6 --- /dev/null +++ b/venv/Lib/site-packages/pymongo/saslprep.py @@ -0,0 +1,116 @@ +# Copyright 2016-present MongoDB, 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. + +"""An implementation of RFC4013 SASLprep.""" +from __future__ import annotations + +from typing import Any, Optional + +try: + import stringprep +except ImportError: + HAVE_STRINGPREP = False + + def saslprep( + data: Any, + prohibit_unassigned_code_points: Optional[bool] = True, # noqa: ARG001 + ) -> Any: + """SASLprep dummy""" + if isinstance(data, str): + raise TypeError( + "The stringprep module is not available. Usernames and " + "passwords must be instances of bytes." + ) + return data + +else: + HAVE_STRINGPREP = True + import unicodedata + + # RFC4013 section 2.3 prohibited output. + _PROHIBITED = ( + # A strict reading of RFC 4013 requires table c12 here, but + # characters from it are mapped to SPACE in the Map step. Can + # normalization reintroduce them somehow? + stringprep.in_table_c12, + stringprep.in_table_c21_c22, + stringprep.in_table_c3, + stringprep.in_table_c4, + stringprep.in_table_c5, + stringprep.in_table_c6, + stringprep.in_table_c7, + stringprep.in_table_c8, + stringprep.in_table_c9, + ) + + def saslprep(data: Any, prohibit_unassigned_code_points: Optional[bool] = True) -> Any: + """An implementation of RFC4013 SASLprep. + + :param data: The string to SASLprep. Unicode strings + (:class:`str`) are supported. Byte strings + (:class:`bytes`) are ignored. + :param prohibit_unassigned_code_points: True / False. RFC 3454 + and RFCs for various SASL mechanisms distinguish between + `queries` (unassigned code points allowed) and + `stored strings` (unassigned code points prohibited). Defaults + to ``True`` (unassigned code points are prohibited). + + :return: The SASLprep'ed version of `data`. + """ + prohibited: Any + + if not isinstance(data, str): + return data + + if prohibit_unassigned_code_points: + prohibited = (*_PROHIBITED, stringprep.in_table_a1) + else: + prohibited = _PROHIBITED + + # RFC3454 section 2, step 1 - Map + # RFC4013 section 2.1 mappings + # Map Non-ASCII space characters to SPACE (U+0020). Map + # commonly mapped to nothing characters to, well, nothing. + in_table_c12 = stringprep.in_table_c12 + in_table_b1 = stringprep.in_table_b1 + data = "".join( + ["\u0020" if in_table_c12(elt) else elt for elt in data if not in_table_b1(elt)] + ) + + # RFC3454 section 2, step 2 - Normalize + # RFC4013 section 2.2 normalization + data = unicodedata.ucd_3_2_0.normalize("NFKC", data) + + in_table_d1 = stringprep.in_table_d1 + if in_table_d1(data[0]): + if not in_table_d1(data[-1]): + # RFC3454, Section 6, #3. If a string contains any + # RandALCat character, the first and last characters + # MUST be RandALCat characters. + raise ValueError("SASLprep: failed bidirectional check") + # RFC3454, Section 6, #2. If a string contains any RandALCat + # character, it MUST NOT contain any LCat character. + prohibited = (*prohibited, stringprep.in_table_d2) + else: + # RFC3454, Section 6, #3. Following the logic of #3, if + # the first character is not a RandALCat, no other character + # can be either. + prohibited = (*prohibited, in_table_d1) + + # RFC3454 section 2, step 3 and 4 - Prohibit and check bidi + for char in data: + if any(in_table(char) for in_table in prohibited): + raise ValueError("SASLprep: failed prohibited character check") + + return data diff --git a/venv/Lib/site-packages/pymongo/server.py b/venv/Lib/site-packages/pymongo/server.py new file mode 100644 index 00000000..1c437a7e --- /dev/null +++ b/venv/Lib/site-packages/pymongo/server.py @@ -0,0 +1,346 @@ +# Copyright 2014-present MongoDB, 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. + +"""Communicate with one MongoDB server in a topology.""" +from __future__ import annotations + +import logging +from datetime import datetime +from typing import TYPE_CHECKING, Any, Callable, ContextManager, Optional, Union + +from bson import _decode_all_selective +from pymongo.errors import NotPrimaryError, OperationFailure +from pymongo.helpers import _check_command_response, _handle_reauth +from pymongo.logger import _COMMAND_LOGGER, _CommandStatusMessage, _debug_log +from pymongo.message import _convert_exception, _GetMore, _OpMsg, _Query +from pymongo.response import PinnedResponse, Response + +if TYPE_CHECKING: + from queue import Queue + from weakref import ReferenceType + + from bson.objectid import ObjectId + from pymongo.mongo_client import MongoClient, _MongoClientErrorHandler + from pymongo.monitor import Monitor + from pymongo.monitoring import _EventListeners + from pymongo.pool import Connection, Pool + from pymongo.read_preferences import _ServerMode + from pymongo.server_description import ServerDescription + from pymongo.typings import _DocumentOut + +_CURSOR_DOC_FIELDS = {"cursor": {"firstBatch": 1, "nextBatch": 1}} + + +class Server: + def __init__( + self, + server_description: ServerDescription, + pool: Pool, + monitor: Monitor, + topology_id: Optional[ObjectId] = None, + listeners: Optional[_EventListeners] = None, + events: Optional[ReferenceType[Queue]] = None, + ) -> None: + """Represent one MongoDB server.""" + self._description = server_description + self._pool = pool + self._monitor = monitor + self._topology_id = topology_id + self._publish = listeners is not None and listeners.enabled_for_server + self._listener = listeners + self._events = None + if self._publish: + self._events = events() # type: ignore[misc] + + def open(self) -> None: + """Start monitoring, or restart after a fork. + + Multiple calls have no effect. + """ + if not self._pool.opts.load_balanced: + self._monitor.open() + + def reset(self, service_id: Optional[ObjectId] = None) -> None: + """Clear the connection pool.""" + self.pool.reset(service_id) + + def close(self) -> None: + """Clear the connection pool and stop the monitor. + + Reconnect with open(). + """ + if self._publish: + assert self._listener is not None + assert self._events is not None + self._events.put( + ( + self._listener.publish_server_closed, + (self._description.address, self._topology_id), + ) + ) + self._monitor.close() + self._pool.close() + + def request_check(self) -> None: + """Check the server's state soon.""" + self._monitor.request_check() + + @_handle_reauth + def run_operation( + self, + conn: Connection, + operation: Union[_Query, _GetMore], + read_preference: _ServerMode, + listeners: Optional[_EventListeners], + unpack_res: Callable[..., list[_DocumentOut]], + client: MongoClient, + ) -> Response: + """Run a _Query or _GetMore operation and return a Response object. + + This method is used only to run _Query/_GetMore operations from + cursors. + Can raise ConnectionFailure, OperationFailure, etc. + + :param conn: A Connection instance. + :param operation: A _Query or _GetMore object. + :param read_preference: The read preference to use. + :param listeners: Instance of _EventListeners or None. + :param unpack_res: A callable that decodes the wire protocol response. + """ + duration = None + assert listeners is not None + publish = listeners.enabled_for_commands + start = datetime.now() + + use_cmd = operation.use_command(conn) + more_to_come = operation.conn_mgr and operation.conn_mgr.more_to_come + if more_to_come: + request_id = 0 + else: + message = operation.get_message(read_preference, conn, use_cmd) + request_id, data, max_doc_size = self._split_message(message) + + cmd, dbn = operation.as_command(conn) + if _COMMAND_LOGGER.isEnabledFor(logging.DEBUG): + _debug_log( + _COMMAND_LOGGER, + clientId=client._topology_settings._topology_id, + message=_CommandStatusMessage.STARTED, + command=cmd, + commandName=next(iter(cmd)), + databaseName=dbn, + requestId=request_id, + operationId=request_id, + driverConnectionId=conn.id, + serverConnectionId=conn.server_connection_id, + serverHost=conn.address[0], + serverPort=conn.address[1], + serviceId=conn.service_id, + ) + + if publish: + cmd, dbn = operation.as_command(conn) + if "$db" not in cmd: + cmd["$db"] = dbn + assert listeners is not None + listeners.publish_command_start( + cmd, + dbn, + request_id, + conn.address, + conn.server_connection_id, + service_id=conn.service_id, + ) + + try: + if more_to_come: + reply = conn.receive_message(None) + else: + conn.send_message(data, max_doc_size) + reply = conn.receive_message(request_id) + + # Unpack and check for command errors. + if use_cmd: + user_fields = _CURSOR_DOC_FIELDS + legacy_response = False + else: + user_fields = None + legacy_response = True + docs = unpack_res( + reply, + operation.cursor_id, + operation.codec_options, + legacy_response=legacy_response, + user_fields=user_fields, + ) + if use_cmd: + first = docs[0] + operation.client._process_response(first, operation.session) + _check_command_response(first, conn.max_wire_version) + except Exception as exc: + duration = datetime.now() - start + if isinstance(exc, (NotPrimaryError, OperationFailure)): + failure: _DocumentOut = exc.details # type: ignore[assignment] + else: + failure = _convert_exception(exc) + if _COMMAND_LOGGER.isEnabledFor(logging.DEBUG): + _debug_log( + _COMMAND_LOGGER, + clientId=client._topology_settings._topology_id, + message=_CommandStatusMessage.FAILED, + durationMS=duration, + failure=failure, + commandName=next(iter(cmd)), + databaseName=dbn, + requestId=request_id, + operationId=request_id, + driverConnectionId=conn.id, + serverConnectionId=conn.server_connection_id, + serverHost=conn.address[0], + serverPort=conn.address[1], + serviceId=conn.service_id, + isServerSideError=isinstance(exc, OperationFailure), + ) + if publish: + assert listeners is not None + listeners.publish_command_failure( + duration, + failure, + operation.name, + request_id, + conn.address, + conn.server_connection_id, + service_id=conn.service_id, + database_name=dbn, + ) + raise + duration = datetime.now() - start + # Must publish in find / getMore / explain command response + # format. + if use_cmd: + res = docs[0] + elif operation.name == "explain": + res = docs[0] if docs else {} + else: + res = {"cursor": {"id": reply.cursor_id, "ns": operation.namespace()}, "ok": 1} # type: ignore[union-attr] + if operation.name == "find": + res["cursor"]["firstBatch"] = docs + else: + res["cursor"]["nextBatch"] = docs + if _COMMAND_LOGGER.isEnabledFor(logging.DEBUG): + _debug_log( + _COMMAND_LOGGER, + clientId=client._topology_settings._topology_id, + message=_CommandStatusMessage.SUCCEEDED, + durationMS=duration, + reply=res, + commandName=next(iter(cmd)), + databaseName=dbn, + requestId=request_id, + operationId=request_id, + driverConnectionId=conn.id, + serverConnectionId=conn.server_connection_id, + serverHost=conn.address[0], + serverPort=conn.address[1], + serviceId=conn.service_id, + ) + if publish: + assert listeners is not None + listeners.publish_command_success( + duration, + res, + operation.name, + request_id, + conn.address, + conn.server_connection_id, + service_id=conn.service_id, + database_name=dbn, + ) + + # Decrypt response. + client = operation.client + if client and client._encrypter: + if use_cmd: + decrypted = client._encrypter.decrypt(reply.raw_command_response()) + docs = _decode_all_selective(decrypted, operation.codec_options, user_fields) + + response: Response + + if client._should_pin_cursor(operation.session) or operation.exhaust: + conn.pin_cursor() + if isinstance(reply, _OpMsg): + # In OP_MSG, the server keeps sending only if the + # more_to_come flag is set. + more_to_come = reply.more_to_come + else: + # In OP_REPLY, the server keeps sending until cursor_id is 0. + more_to_come = bool(operation.exhaust and reply.cursor_id) + if operation.conn_mgr: + operation.conn_mgr.update_exhaust(more_to_come) + response = PinnedResponse( + data=reply, + address=self._description.address, + conn=conn, + duration=duration, + request_id=request_id, + from_command=use_cmd, + docs=docs, + more_to_come=more_to_come, + ) + else: + response = Response( + data=reply, + address=self._description.address, + duration=duration, + request_id=request_id, + from_command=use_cmd, + docs=docs, + ) + + return response + + def checkout( + self, handler: Optional[_MongoClientErrorHandler] = None + ) -> ContextManager[Connection]: + return self.pool.checkout(handler) + + @property + def description(self) -> ServerDescription: + return self._description + + @description.setter + def description(self, server_description: ServerDescription) -> None: + assert server_description.address == self._description.address + self._description = server_description + + @property + def pool(self) -> Pool: + return self._pool + + def _split_message( + self, message: Union[tuple[int, Any], tuple[int, Any, int]] + ) -> tuple[int, Any, int]: + """Return request_id, data, max_doc_size. + + :param message: (request_id, data, max_doc_size) or (request_id, data) + """ + if len(message) == 3: + return message # type: ignore[return-value] + else: + # get_more and kill_cursors messages don't include BSON documents. + request_id, data = message # type: ignore[misc] + return request_id, data, 0 + + def __repr__(self) -> str: + return f"<{self.__class__.__name__} {self._description!r}>" diff --git a/venv/Lib/site-packages/pymongo/server_api.py b/venv/Lib/site-packages/pymongo/server_api.py new file mode 100644 index 00000000..4a746008 --- /dev/null +++ b/venv/Lib/site-packages/pymongo/server_api.py @@ -0,0 +1,173 @@ +# Copyright 2020-present MongoDB, 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. + +"""Support for MongoDB Stable API. + +.. _versioned-api-ref: + +MongoDB Stable API +===================== + +Starting in MongoDB 5.0, applications can specify the server API version +to use when creating a :class:`~pymongo.mongo_client.MongoClient`. Doing so +ensures that the driver behaves in a manner compatible with that server API +version, regardless of the server's actual release version. + +Declaring an API Version +```````````````````````` + +.. attention:: Stable API requires MongoDB >=5.0. + +To configure MongoDB Stable API, pass the ``server_api`` keyword option to +:class:`~pymongo.mongo_client.MongoClient`:: + + >>> from pymongo.mongo_client import MongoClient + >>> from pymongo.server_api import ServerApi + >>> + >>> # Declare API version "1" for MongoClient "client" + >>> server_api = ServerApi('1') + >>> client = MongoClient(server_api=server_api) + +The declared API version is applied to all commands run through ``client``, +including those sent through the generic +:meth:`~pymongo.database.Database.command` helper. + +.. note:: Declaring an API version on the + :class:`~pymongo.mongo_client.MongoClient` **and** specifying stable + API options in :meth:`~pymongo.database.Database.command` command document + is not supported and will lead to undefined behaviour. + +To run any command without declaring a server API version or using a different +API version, create a separate :class:`~pymongo.mongo_client.MongoClient` +instance. + +Strict Mode +``````````` + +Configuring ``strict`` mode will cause the MongoDB server to reject all +commands that are not part of the declared :attr:`ServerApi.version`. This +includes command options and aggregation pipeline stages. + +For example:: + + >>> server_api = ServerApi('1', strict=True) + >>> client = MongoClient(server_api=server_api) + >>> client.test.command('count', 'test') + Traceback (most recent call last): + ... + pymongo.errors.OperationFailure: Provided apiStrict:true, but the command count is not in API Version 1, full error: {'ok': 0.0, 'errmsg': 'Provided apiStrict:true, but the command count is not in API Version 1', 'code': 323, 'codeName': 'APIStrictError' + +Detecting API Deprecations +`````````````````````````` + +The ``deprecationErrors`` option can be used to enable command failures +when using functionality that is deprecated from the configured +:attr:`ServerApi.version`. For example:: + + >>> server_api = ServerApi('1', deprecation_errors=True) + >>> client = MongoClient(server_api=server_api) + +Note that at the time of this writing, no deprecated APIs exist. + +Classes +======= +""" +from __future__ import annotations + +from typing import Any, MutableMapping, Optional + + +class ServerApiVersion: + """An enum that defines values for :attr:`ServerApi.version`. + + .. versionadded:: 3.12 + """ + + V1 = "1" + """Server API version "1".""" + + +class ServerApi: + """MongoDB Stable API.""" + + def __init__( + self, version: str, strict: Optional[bool] = None, deprecation_errors: Optional[bool] = None + ): + """Options to configure MongoDB Stable API. + + :param version: The API version string. Must be one of the values in + :class:`ServerApiVersion`. + :param strict: Set to ``True`` to enable API strict mode. + Defaults to ``None`` which means "use the server's default". + :param deprecation_errors: Set to ``True`` to enable + deprecation errors. Defaults to ``None`` which means "use the + server's default". + + .. versionadded:: 3.12 + """ + if version != ServerApiVersion.V1: + raise ValueError(f"Unknown ServerApi version: {version}") + if strict is not None and not isinstance(strict, bool): + raise TypeError( + "Wrong type for ServerApi strict, value must be an instance " + f"of bool, not {type(strict)}" + ) + if deprecation_errors is not None and not isinstance(deprecation_errors, bool): + raise TypeError( + "Wrong type for ServerApi deprecation_errors, value must be " + f"an instance of bool, not {type(deprecation_errors)}" + ) + self._version = version + self._strict = strict + self._deprecation_errors = deprecation_errors + + @property + def version(self) -> str: + """The API version setting. + + This value is sent to the server in the "apiVersion" field. + """ + return self._version + + @property + def strict(self) -> Optional[bool]: + """The API strict mode setting. + + When set, this value is sent to the server in the "apiStrict" field. + """ + return self._strict + + @property + def deprecation_errors(self) -> Optional[bool]: + """The API deprecation errors setting. + + When set, this value is sent to the server in the + "apiDeprecationErrors" field. + """ + return self._deprecation_errors + + +def _add_to_command(cmd: MutableMapping[str, Any], server_api: Optional[ServerApi]) -> None: + """Internal helper which adds API versioning options to a command. + + :param cmd: The command. + :param server_api: A :class:`ServerApi` or ``None``. + """ + if not server_api: + return + cmd["apiVersion"] = server_api.version + if server_api.strict is not None: + cmd["apiStrict"] = server_api.strict + if server_api.deprecation_errors is not None: + cmd["apiDeprecationErrors"] = server_api.deprecation_errors diff --git a/venv/Lib/site-packages/pymongo/server_description.py b/venv/Lib/site-packages/pymongo/server_description.py new file mode 100644 index 00000000..6393fce0 --- /dev/null +++ b/venv/Lib/site-packages/pymongo/server_description.py @@ -0,0 +1,299 @@ +# Copyright 2014-present MongoDB, 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. + +"""Represent one server the driver is connected to.""" +from __future__ import annotations + +import time +import warnings +from typing import Any, Mapping, Optional + +from bson import EPOCH_NAIVE +from bson.objectid import ObjectId +from pymongo.hello import Hello +from pymongo.server_type import SERVER_TYPE +from pymongo.typings import ClusterTime, _Address + + +class ServerDescription: + """Immutable representation of one server. + + :param address: A (host, port) pair + :param hello: Optional Hello instance + :param round_trip_time: Optional float + :param error: Optional, the last error attempting to connect to the server + :param round_trip_time: Optional float, the min latency from the most recent samples + """ + + __slots__ = ( + "_address", + "_server_type", + "_all_hosts", + "_tags", + "_replica_set_name", + "_primary", + "_max_bson_size", + "_max_message_size", + "_max_write_batch_size", + "_min_wire_version", + "_max_wire_version", + "_round_trip_time", + "_min_round_trip_time", + "_me", + "_is_writable", + "_is_readable", + "_ls_timeout_minutes", + "_error", + "_set_version", + "_election_id", + "_cluster_time", + "_last_write_date", + "_last_update_time", + "_topology_version", + ) + + def __init__( + self, + address: _Address, + hello: Optional[Hello] = None, + round_trip_time: Optional[float] = None, + error: Optional[Exception] = None, + min_round_trip_time: float = 0.0, + ) -> None: + self._address = address + if not hello: + hello = Hello({}) + + self._server_type = hello.server_type + self._all_hosts = hello.all_hosts + self._tags = hello.tags + self._replica_set_name = hello.replica_set_name + self._primary = hello.primary + self._max_bson_size = hello.max_bson_size + self._max_message_size = hello.max_message_size + self._max_write_batch_size = hello.max_write_batch_size + self._min_wire_version = hello.min_wire_version + self._max_wire_version = hello.max_wire_version + self._set_version = hello.set_version + self._election_id = hello.election_id + self._cluster_time = hello.cluster_time + self._is_writable = hello.is_writable + self._is_readable = hello.is_readable + self._ls_timeout_minutes = hello.logical_session_timeout_minutes + self._round_trip_time = round_trip_time + self._min_round_trip_time = min_round_trip_time + self._me = hello.me + self._last_update_time = time.monotonic() + self._error = error + self._topology_version = hello.topology_version + if error: + details = getattr(error, "details", None) + if isinstance(details, dict): + self._topology_version = details.get("topologyVersion") + + self._last_write_date: Optional[float] + if hello.last_write_date: + # Convert from datetime to seconds. + delta = hello.last_write_date - EPOCH_NAIVE + self._last_write_date = delta.total_seconds() + else: + self._last_write_date = None + + @property + def address(self) -> _Address: + """The address (host, port) of this server.""" + return self._address + + @property + def server_type(self) -> int: + """The type of this server.""" + return self._server_type + + @property + def server_type_name(self) -> str: + """The server type as a human readable string. + + .. versionadded:: 3.4 + """ + return SERVER_TYPE._fields[self._server_type] + + @property + def all_hosts(self) -> set[tuple[str, int]]: + """List of hosts, passives, and arbiters known to this server.""" + return self._all_hosts + + @property + def tags(self) -> Mapping[str, Any]: + return self._tags + + @property + def replica_set_name(self) -> Optional[str]: + """Replica set name or None.""" + return self._replica_set_name + + @property + def primary(self) -> Optional[tuple[str, int]]: + """This server's opinion about who the primary is, or None.""" + return self._primary + + @property + def max_bson_size(self) -> int: + return self._max_bson_size + + @property + def max_message_size(self) -> int: + return self._max_message_size + + @property + def max_write_batch_size(self) -> int: + return self._max_write_batch_size + + @property + def min_wire_version(self) -> int: + return self._min_wire_version + + @property + def max_wire_version(self) -> int: + return self._max_wire_version + + @property + def set_version(self) -> Optional[int]: + return self._set_version + + @property + def election_id(self) -> Optional[ObjectId]: + return self._election_id + + @property + def cluster_time(self) -> Optional[ClusterTime]: + return self._cluster_time + + @property + def election_tuple(self) -> tuple[Optional[int], Optional[ObjectId]]: + warnings.warn( + "'election_tuple' is deprecated, use 'set_version' and 'election_id' instead", + DeprecationWarning, + stacklevel=2, + ) + return self._set_version, self._election_id + + @property + def me(self) -> Optional[tuple[str, int]]: + return self._me + + @property + def logical_session_timeout_minutes(self) -> Optional[int]: + return self._ls_timeout_minutes + + @property + def last_write_date(self) -> Optional[float]: + return self._last_write_date + + @property + def last_update_time(self) -> float: + return self._last_update_time + + @property + def round_trip_time(self) -> Optional[float]: + """The current average latency or None.""" + # This override is for unittesting only! + if self._address in self._host_to_round_trip_time: + return self._host_to_round_trip_time[self._address] + + return self._round_trip_time + + @property + def min_round_trip_time(self) -> float: + """The min latency from the most recent samples.""" + return self._min_round_trip_time + + @property + def error(self) -> Optional[Exception]: + """The last error attempting to connect to the server, or None.""" + return self._error + + @property + def is_writable(self) -> bool: + return self._is_writable + + @property + def is_readable(self) -> bool: + return self._is_readable + + @property + def mongos(self) -> bool: + return self._server_type == SERVER_TYPE.Mongos + + @property + def is_server_type_known(self) -> bool: + return self.server_type != SERVER_TYPE.Unknown + + @property + def retryable_writes_supported(self) -> bool: + """Checks if this server supports retryable writes.""" + return ( + self._server_type in (SERVER_TYPE.Mongos, SERVER_TYPE.RSPrimary) + ) or self._server_type == SERVER_TYPE.LoadBalancer + + @property + def retryable_reads_supported(self) -> bool: + """Checks if this server supports retryable writes.""" + return self._max_wire_version >= 6 + + @property + def topology_version(self) -> Optional[Mapping[str, Any]]: + return self._topology_version + + def to_unknown(self, error: Optional[Exception] = None) -> ServerDescription: + unknown = ServerDescription(self.address, error=error) + unknown._topology_version = self.topology_version + return unknown + + def __eq__(self, other: Any) -> bool: + if isinstance(other, ServerDescription): + return ( + (self._address == other.address) + and (self._server_type == other.server_type) + and (self._min_wire_version == other.min_wire_version) + and (self._max_wire_version == other.max_wire_version) + and (self._me == other.me) + and (self._all_hosts == other.all_hosts) + and (self._tags == other.tags) + and (self._replica_set_name == other.replica_set_name) + and (self._set_version == other.set_version) + and (self._election_id == other.election_id) + and (self._primary == other.primary) + and (self._ls_timeout_minutes == other.logical_session_timeout_minutes) + and (self._error == other.error) + ) + + return NotImplemented + + def __ne__(self, other: Any) -> bool: + return not self == other + + def __repr__(self) -> str: + errmsg = "" + if self.error: + errmsg = f", error={self.error!r}" + return "<{} {} server_type: {}, rtt: {}{}>".format( + self.__class__.__name__, + self.address, + self.server_type_name, + self.round_trip_time, + errmsg, + ) + + # For unittesting only. Use under no circumstances! + _host_to_round_trip_time: dict = {} diff --git a/venv/Lib/site-packages/pymongo/server_selectors.py b/venv/Lib/site-packages/pymongo/server_selectors.py new file mode 100644 index 00000000..c22ad599 --- /dev/null +++ b/venv/Lib/site-packages/pymongo/server_selectors.py @@ -0,0 +1,174 @@ +# Copyright 2014-2016 MongoDB, 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. + +"""Criteria to select some ServerDescriptions from a TopologyDescription.""" +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, Mapping, Optional, Sequence, TypeVar, cast + +from pymongo.server_type import SERVER_TYPE + +if TYPE_CHECKING: + from pymongo.server_description import ServerDescription + from pymongo.topology_description import TopologyDescription + + +T = TypeVar("T") +TagSet = Mapping[str, Any] +TagSets = Sequence[TagSet] + + +class Selection: + """Input or output of a server selector function.""" + + @classmethod + def from_topology_description(cls, topology_description: TopologyDescription) -> Selection: + known_servers = topology_description.known_servers + primary = None + for sd in known_servers: + if sd.server_type == SERVER_TYPE.RSPrimary: + primary = sd + break + + return Selection( + topology_description, + topology_description.known_servers, + topology_description.common_wire_version, + primary, + ) + + def __init__( + self, + topology_description: TopologyDescription, + server_descriptions: list[ServerDescription], + common_wire_version: Optional[int], + primary: Optional[ServerDescription], + ): + self.topology_description = topology_description + self.server_descriptions = server_descriptions + self.primary = primary + self.common_wire_version = common_wire_version + + def with_server_descriptions(self, server_descriptions: list[ServerDescription]) -> Selection: + return Selection( + self.topology_description, server_descriptions, self.common_wire_version, self.primary + ) + + def secondary_with_max_last_write_date(self) -> Optional[ServerDescription]: + secondaries = secondary_server_selector(self) + if secondaries.server_descriptions: + return max( + secondaries.server_descriptions, key=lambda sd: cast(float, sd.last_write_date) + ) + return None + + @property + def primary_selection(self) -> Selection: + primaries = [self.primary] if self.primary else [] + return self.with_server_descriptions(primaries) + + @property + def heartbeat_frequency(self) -> int: + return self.topology_description.heartbeat_frequency + + @property + def topology_type(self) -> int: + return self.topology_description.topology_type + + def __bool__(self) -> bool: + return bool(self.server_descriptions) + + def __getitem__(self, item: int) -> ServerDescription: + return self.server_descriptions[item] + + +def any_server_selector(selection: T) -> T: + return selection + + +def readable_server_selector(selection: Selection) -> Selection: + return selection.with_server_descriptions( + [s for s in selection.server_descriptions if s.is_readable] + ) + + +def writable_server_selector(selection: Selection) -> Selection: + return selection.with_server_descriptions( + [s for s in selection.server_descriptions if s.is_writable] + ) + + +def secondary_server_selector(selection: Selection) -> Selection: + return selection.with_server_descriptions( + [s for s in selection.server_descriptions if s.server_type == SERVER_TYPE.RSSecondary] + ) + + +def arbiter_server_selector(selection: Selection) -> Selection: + return selection.with_server_descriptions( + [s for s in selection.server_descriptions if s.server_type == SERVER_TYPE.RSArbiter] + ) + + +def writable_preferred_server_selector(selection: Selection) -> Selection: + """Like PrimaryPreferred but doesn't use tags or latency.""" + return writable_server_selector(selection) or secondary_server_selector(selection) + + +def apply_single_tag_set(tag_set: TagSet, selection: Selection) -> Selection: + """All servers matching one tag set. + + A tag set is a dict. A server matches if its tags are a superset: + A server tagged {'a': '1', 'b': '2'} matches the tag set {'a': '1'}. + + The empty tag set {} matches any server. + """ + + def tags_match(server_tags: Mapping[str, Any]) -> bool: + for key, value in tag_set.items(): + if key not in server_tags or server_tags[key] != value: + return False + + return True + + return selection.with_server_descriptions( + [s for s in selection.server_descriptions if tags_match(s.tags)] + ) + + +def apply_tag_sets(tag_sets: TagSets, selection: Selection) -> Selection: + """All servers match a list of tag sets. + + tag_sets is a list of dicts. The empty tag set {} matches any server, + and may be provided at the end of the list as a fallback. So + [{'a': 'value'}, {}] expresses a preference for servers tagged + {'a': 'value'}, but accepts any server if none matches the first + preference. + """ + for tag_set in tag_sets: + with_tag_set = apply_single_tag_set(tag_set, selection) + if with_tag_set: + return with_tag_set + + return selection.with_server_descriptions([]) + + +def secondary_with_tags_server_selector(tag_sets: TagSets, selection: Selection) -> Selection: + """All near-enough secondaries matching the tag sets.""" + return apply_tag_sets(tag_sets, secondary_server_selector(selection)) + + +def member_with_tags_server_selector(tag_sets: TagSets, selection: Selection) -> Selection: + """All near-enough members matching the tag sets.""" + return apply_tag_sets(tag_sets, readable_server_selector(selection)) diff --git a/venv/Lib/site-packages/pymongo/server_type.py b/venv/Lib/site-packages/pymongo/server_type.py new file mode 100644 index 00000000..937855cc --- /dev/null +++ b/venv/Lib/site-packages/pymongo/server_type.py @@ -0,0 +1,33 @@ +# Copyright 2014-2015 MongoDB, 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. + +"""Type codes for MongoDB servers.""" +from __future__ import annotations + +from typing import NamedTuple + + +class _ServerType(NamedTuple): + Unknown: int + Mongos: int + RSPrimary: int + RSSecondary: int + RSArbiter: int + RSOther: int + RSGhost: int + Standalone: int + LoadBalancer: int + + +SERVER_TYPE = _ServerType(*range(9)) diff --git a/venv/Lib/site-packages/pymongo/settings.py b/venv/Lib/site-packages/pymongo/settings.py new file mode 100644 index 00000000..4a3e7be4 --- /dev/null +++ b/venv/Lib/site-packages/pymongo/settings.py @@ -0,0 +1,168 @@ +# Copyright 2014-present MongoDB, 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. + +"""Represent MongoClient's configuration.""" +from __future__ import annotations + +import threading +import traceback +from typing import Any, Collection, Optional, Type, Union + +from bson.objectid import ObjectId +from pymongo import common, monitor, pool +from pymongo.common import LOCAL_THRESHOLD_MS, SERVER_SELECTION_TIMEOUT +from pymongo.errors import ConfigurationError +from pymongo.pool import Pool, PoolOptions +from pymongo.server_description import ServerDescription +from pymongo.topology_description import TOPOLOGY_TYPE, _ServerSelector + + +class TopologySettings: + def __init__( + self, + seeds: Optional[Collection[tuple[str, int]]] = None, + replica_set_name: Optional[str] = None, + pool_class: Optional[Type[Pool]] = None, + pool_options: Optional[PoolOptions] = None, + monitor_class: Optional[Type[monitor.Monitor]] = None, + condition_class: Optional[Type[threading.Condition]] = None, + local_threshold_ms: int = LOCAL_THRESHOLD_MS, + server_selection_timeout: int = SERVER_SELECTION_TIMEOUT, + heartbeat_frequency: int = common.HEARTBEAT_FREQUENCY, + server_selector: Optional[_ServerSelector] = None, + fqdn: Optional[str] = None, + direct_connection: Optional[bool] = False, + load_balanced: Optional[bool] = None, + srv_service_name: str = common.SRV_SERVICE_NAME, + srv_max_hosts: int = 0, + server_monitoring_mode: str = common.SERVER_MONITORING_MODE, + ): + """Represent MongoClient's configuration. + + Take a list of (host, port) pairs and optional replica set name. + """ + if heartbeat_frequency < common.MIN_HEARTBEAT_INTERVAL: + raise ConfigurationError( + "heartbeatFrequencyMS cannot be less than %d" + % (common.MIN_HEARTBEAT_INTERVAL * 1000,) + ) + + self._seeds: Collection[tuple[str, int]] = seeds or [("localhost", 27017)] + self._replica_set_name = replica_set_name + self._pool_class: Type[Pool] = pool_class or pool.Pool + self._pool_options: PoolOptions = pool_options or PoolOptions() + self._monitor_class: Type[monitor.Monitor] = monitor_class or monitor.Monitor + self._condition_class: Type[threading.Condition] = condition_class or threading.Condition + self._local_threshold_ms = local_threshold_ms + self._server_selection_timeout = server_selection_timeout + self._server_selector = server_selector + self._fqdn = fqdn + self._heartbeat_frequency = heartbeat_frequency + self._direct = direct_connection + self._load_balanced = load_balanced + self._srv_service_name = srv_service_name + self._srv_max_hosts = srv_max_hosts or 0 + self._server_monitoring_mode = server_monitoring_mode + + self._topology_id = ObjectId() + # Store the allocation traceback to catch unclosed clients in the + # test suite. + self._stack = "".join(traceback.format_stack()) + + @property + def seeds(self) -> Collection[tuple[str, int]]: + """List of server addresses.""" + return self._seeds + + @property + def replica_set_name(self) -> Optional[str]: + return self._replica_set_name + + @property + def pool_class(self) -> Type[Pool]: + return self._pool_class + + @property + def pool_options(self) -> PoolOptions: + return self._pool_options + + @property + def monitor_class(self) -> Type[monitor.Monitor]: + return self._monitor_class + + @property + def condition_class(self) -> Type[threading.Condition]: + return self._condition_class + + @property + def local_threshold_ms(self) -> int: + return self._local_threshold_ms + + @property + def server_selection_timeout(self) -> int: + return self._server_selection_timeout + + @property + def server_selector(self) -> Optional[_ServerSelector]: + return self._server_selector + + @property + def heartbeat_frequency(self) -> int: + return self._heartbeat_frequency + + @property + def fqdn(self) -> Optional[str]: + return self._fqdn + + @property + def direct(self) -> Optional[bool]: + """Connect directly to a single server, or use a set of servers? + + True if there is one seed and no replica_set_name. + """ + return self._direct + + @property + def load_balanced(self) -> Optional[bool]: + """True if the client was configured to connect to a load balancer.""" + return self._load_balanced + + @property + def srv_service_name(self) -> str: + """The srvServiceName.""" + return self._srv_service_name + + @property + def srv_max_hosts(self) -> int: + """The srvMaxHosts.""" + return self._srv_max_hosts + + @property + def server_monitoring_mode(self) -> str: + """The serverMonitoringMode.""" + return self._server_monitoring_mode + + def get_topology_type(self) -> int: + if self.load_balanced: + return TOPOLOGY_TYPE.LoadBalanced + elif self.direct: + return TOPOLOGY_TYPE.Single + elif self.replica_set_name is not None: + return TOPOLOGY_TYPE.ReplicaSetNoPrimary + else: + return TOPOLOGY_TYPE.Unknown + + def get_server_descriptions(self) -> dict[Union[tuple[str, int], Any], ServerDescription]: + """Initial dict of (address, ServerDescription) for all seeds.""" + return {address: ServerDescription(address) for address in self.seeds} diff --git a/venv/Lib/site-packages/pymongo/socket_checker.py b/venv/Lib/site-packages/pymongo/socket_checker.py new file mode 100644 index 00000000..78861854 --- /dev/null +++ b/venv/Lib/site-packages/pymongo/socket_checker.py @@ -0,0 +1,105 @@ +# Copyright 2020-present MongoDB, 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. + +"""Select / poll helper""" +from __future__ import annotations + +import errno +import select +import sys +from typing import Any, Optional, cast + +# PYTHON-2320: Jython does not fully support poll on SSL sockets, +# https://bugs.jython.org/issue2900 +_HAVE_POLL = hasattr(select, "poll") and not sys.platform.startswith("java") +_SelectError = getattr(select, "error", OSError) + + +def _errno_from_exception(exc: BaseException) -> Optional[int]: + if hasattr(exc, "errno"): + return cast(int, exc.errno) + if exc.args: + return cast(int, exc.args[0]) + return None + + +class SocketChecker: + def __init__(self) -> None: + self._poller: Optional[select.poll] + if _HAVE_POLL: + self._poller = select.poll() + else: + self._poller = None + + def select( + self, sock: Any, read: bool = False, write: bool = False, timeout: Optional[float] = 0 + ) -> bool: + """Select for reads or writes with a timeout in seconds (or None). + + Returns True if the socket is readable/writable, False on timeout. + """ + res: Any + while True: + try: + if self._poller: + mask = select.POLLERR | select.POLLHUP + if read: + mask = mask | select.POLLIN | select.POLLPRI + if write: + mask = mask | select.POLLOUT + self._poller.register(sock, mask) + try: + # poll() timeout is in milliseconds. select() + # timeout is in seconds. + timeout_ = None if timeout is None else timeout * 1000 + res = self._poller.poll(timeout_) + # poll returns a possibly-empty list containing + # (fd, event) 2-tuples for the descriptors that have + # events or errors to report. Return True if the list + # is not empty. + return bool(res) + finally: + self._poller.unregister(sock) + else: + rlist = [sock] if read else [] + wlist = [sock] if write else [] + res = select.select(rlist, wlist, [sock], timeout) + # select returns a 3-tuple of lists of objects that are + # ready: subsets of the first three arguments. Return + # True if any of the lists are not empty. + return any(res) + except (_SelectError, OSError) as exc: # type: ignore + if _errno_from_exception(exc) in (errno.EINTR, errno.EAGAIN): + continue + raise + + def socket_closed(self, sock: Any) -> bool: + """Return True if we know socket has been closed, False otherwise.""" + try: + return self.select(sock, read=True) + except (RuntimeError, KeyError): + # RuntimeError is raised during a concurrent poll. KeyError + # is raised by unregister if the socket is not in the poller. + # These errors should not be possible since we protect the + # poller with a mutex. + raise + except ValueError: + # ValueError is raised by register/unregister/select if the + # socket file descriptor is negative or outside the range for + # select (> 1023). + return True + except Exception: + # Any other exceptions should be attributed to a closed + # or invalid socket. + return True diff --git a/venv/Lib/site-packages/pymongo/srv_resolver.py b/venv/Lib/site-packages/pymongo/srv_resolver.py new file mode 100644 index 00000000..4ee1b1f5 --- /dev/null +++ b/venv/Lib/site-packages/pymongo/srv_resolver.py @@ -0,0 +1,138 @@ +# Copyright 2019-present MongoDB, 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. + +"""Support for resolving hosts and options from mongodb+srv:// URIs.""" +from __future__ import annotations + +import ipaddress +import random +from typing import Any, Optional, Union + +from pymongo.common import CONNECT_TIMEOUT +from pymongo.errors import ConfigurationError + +try: + from dns import resolver + + _HAVE_DNSPYTHON = True +except ImportError: + _HAVE_DNSPYTHON = False + + +# dnspython can return bytes or str from various parts +# of its API depending on version. We always want str. +def maybe_decode(text: Union[str, bytes]) -> str: + if isinstance(text, bytes): + return text.decode() + return text + + +# PYTHON-2667 Lazily call dns.resolver methods for compatibility with eventlet. +def _resolve(*args: Any, **kwargs: Any) -> resolver.Answer: + if hasattr(resolver, "resolve"): + # dnspython >= 2 + return resolver.resolve(*args, **kwargs) + # dnspython 1.X + return resolver.query(*args, **kwargs) + + +_INVALID_HOST_MSG = ( + "Invalid URI host: %s is not a valid hostname for 'mongodb+srv://'. " + "Did you mean to use 'mongodb://'?" +) + + +class _SrvResolver: + def __init__( + self, + fqdn: str, + connect_timeout: Optional[float], + srv_service_name: str, + srv_max_hosts: int = 0, + ): + self.__fqdn = fqdn + self.__srv = srv_service_name + self.__connect_timeout = connect_timeout or CONNECT_TIMEOUT + self.__srv_max_hosts = srv_max_hosts or 0 + # Validate the fully qualified domain name. + try: + ipaddress.ip_address(fqdn) + raise ConfigurationError(_INVALID_HOST_MSG % ("an IP address",)) + except ValueError: + pass + + try: + self.__plist = self.__fqdn.split(".")[1:] + except Exception: + raise ConfigurationError(_INVALID_HOST_MSG % (fqdn,)) from None + self.__slen = len(self.__plist) + if self.__slen < 2: + raise ConfigurationError(_INVALID_HOST_MSG % (fqdn,)) + + def get_options(self) -> Optional[str]: + try: + results = _resolve(self.__fqdn, "TXT", lifetime=self.__connect_timeout) + except (resolver.NoAnswer, resolver.NXDOMAIN): + # No TXT records + return None + except Exception as exc: + raise ConfigurationError(str(exc)) from None + if len(results) > 1: + raise ConfigurationError("Only one TXT record is supported") + return (b"&".join([b"".join(res.strings) for res in results])).decode("utf-8") + + def _resolve_uri(self, encapsulate_errors: bool) -> resolver.Answer: + try: + results = _resolve( + "_" + self.__srv + "._tcp." + self.__fqdn, "SRV", lifetime=self.__connect_timeout + ) + except Exception as exc: + if not encapsulate_errors: + # Raise the original error. + raise + # Else, raise all errors as ConfigurationError. + raise ConfigurationError(str(exc)) from None + return results + + def _get_srv_response_and_hosts( + self, encapsulate_errors: bool + ) -> tuple[resolver.Answer, list[tuple[str, Any]]]: + results = self._resolve_uri(encapsulate_errors) + + # Construct address tuples + nodes = [ + (maybe_decode(res.target.to_text(omit_final_dot=True)), res.port) for res in results + ] + + # Validate hosts + for node in nodes: + try: + nlist = node[0].lower().split(".")[1:][-self.__slen :] + except Exception: + raise ConfigurationError(f"Invalid SRV host: {node[0]}") from None + if self.__plist != nlist: + raise ConfigurationError(f"Invalid SRV host: {node[0]}") + if self.__srv_max_hosts: + nodes = random.sample(nodes, min(self.__srv_max_hosts, len(nodes))) + return results, nodes + + def get_hosts(self) -> list[tuple[str, Any]]: + _, nodes = self._get_srv_response_and_hosts(True) + return nodes + + def get_hosts_and_min_ttl(self) -> tuple[list[tuple[str, Any]], int]: + results, nodes = self._get_srv_response_and_hosts(False) + rrset = results.rrset + ttl = rrset.ttl if rrset else 0 + return nodes, ttl diff --git a/venv/Lib/site-packages/pymongo/ssl_context.py b/venv/Lib/site-packages/pymongo/ssl_context.py new file mode 100644 index 00000000..1a042420 --- /dev/null +++ b/venv/Lib/site-packages/pymongo/ssl_context.py @@ -0,0 +1,40 @@ +# Copyright 2014-present MongoDB, 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. + +"""A fake SSLContext implementation.""" +from __future__ import annotations + +import ssl as _ssl + +# PROTOCOL_TLS_CLIENT is Python 3.6+ +PROTOCOL_SSLv23 = getattr(_ssl, "PROTOCOL_TLS_CLIENT", _ssl.PROTOCOL_SSLv23) +OP_NO_SSLv2 = getattr(_ssl, "OP_NO_SSLv2", 0) +OP_NO_SSLv3 = getattr(_ssl, "OP_NO_SSLv3", 0) +OP_NO_COMPRESSION = getattr(_ssl, "OP_NO_COMPRESSION", 0) +# Python 3.7+, OpenSSL 1.1.0h+ +OP_NO_RENEGOTIATION = getattr(_ssl, "OP_NO_RENEGOTIATION", 0) + +HAS_SNI = getattr(_ssl, "HAS_SNI", False) +IS_PYOPENSSL = False + +# Errors raised by SSL sockets when in non-blocking mode. +BLOCKING_IO_ERRORS = (_ssl.SSLWantReadError, _ssl.SSLWantWriteError) + +# Base Exception class +SSLError = _ssl.SSLError + +from ssl import SSLContext # noqa: F401,E402 + +if hasattr(_ssl, "VERIFY_CRL_CHECK_LEAF"): + from ssl import VERIFY_CRL_CHECK_LEAF # noqa: F401 diff --git a/venv/Lib/site-packages/pymongo/ssl_support.py b/venv/Lib/site-packages/pymongo/ssl_support.py new file mode 100644 index 00000000..849fbf70 --- /dev/null +++ b/venv/Lib/site-packages/pymongo/ssl_support.py @@ -0,0 +1,104 @@ +# Copyright 2014-present MongoDB, 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. + +"""Support for SSL in PyMongo.""" +from __future__ import annotations + +from typing import Optional + +from pymongo.errors import ConfigurationError + +HAVE_SSL = True + +try: + import pymongo.pyopenssl_context as _ssl +except ImportError: + try: + import pymongo.ssl_context as _ssl # type: ignore[no-redef] + except ImportError: + HAVE_SSL = False + + +if HAVE_SSL: + # Note: The validate* functions below deal with users passing + # CPython ssl module constants to configure certificate verification + # at a high level. This is legacy behavior, but requires us to + # import the ssl module even if we're only using it for this purpose. + import ssl as _stdlibssl # noqa: F401 + from ssl import CERT_NONE, CERT_REQUIRED + + HAS_SNI = _ssl.HAS_SNI + IPADDR_SAFE = True + SSLError = _ssl.SSLError + BLOCKING_IO_ERRORS = _ssl.BLOCKING_IO_ERRORS + + def get_ssl_context( + certfile: Optional[str], + passphrase: Optional[str], + ca_certs: Optional[str], + crlfile: Optional[str], + allow_invalid_certificates: bool, + allow_invalid_hostnames: bool, + disable_ocsp_endpoint_check: bool, + ) -> _ssl.SSLContext: + """Create and return an SSLContext object.""" + verify_mode = CERT_NONE if allow_invalid_certificates else CERT_REQUIRED + ctx = _ssl.SSLContext(_ssl.PROTOCOL_SSLv23) + if verify_mode != CERT_NONE: + ctx.check_hostname = not allow_invalid_hostnames + else: + ctx.check_hostname = False + if hasattr(ctx, "check_ocsp_endpoint"): + ctx.check_ocsp_endpoint = not disable_ocsp_endpoint_check + if hasattr(ctx, "options"): + # Explicitly disable SSLv2, SSLv3 and TLS compression. Note that + # up to date versions of MongoDB 2.4 and above already disable + # SSLv2 and SSLv3, python disables SSLv2 by default in >= 2.7.7 + # and >= 3.3.4 and SSLv3 in >= 3.4.3. + ctx.options |= _ssl.OP_NO_SSLv2 + ctx.options |= _ssl.OP_NO_SSLv3 + ctx.options |= _ssl.OP_NO_COMPRESSION + ctx.options |= _ssl.OP_NO_RENEGOTIATION + if certfile is not None: + try: + ctx.load_cert_chain(certfile, None, passphrase) + except _ssl.SSLError as exc: + raise ConfigurationError(f"Private key doesn't match certificate: {exc}") from None + if crlfile is not None: + if _ssl.IS_PYOPENSSL: + raise ConfigurationError("tlsCRLFile cannot be used with PyOpenSSL") + # Match the server's behavior. + ctx.verify_flags = getattr( # type:ignore[attr-defined] + _ssl, "VERIFY_CRL_CHECK_LEAF", 0 + ) + ctx.load_verify_locations(crlfile) + if ca_certs is not None: + ctx.load_verify_locations(ca_certs) + elif verify_mode != CERT_NONE: + ctx.load_default_certs() + ctx.verify_mode = verify_mode + return ctx + +else: + + class SSLError(Exception): # type: ignore + pass + + HAS_SNI = False + IPADDR_SAFE = False + BLOCKING_IO_ERRORS = () # type:ignore[assignment] + + def get_ssl_context(*dummy): # type: ignore + """No ssl module, raise ConfigurationError.""" + raise ConfigurationError("The ssl module is not available.") diff --git a/venv/Lib/site-packages/pymongo/topology.py b/venv/Lib/site-packages/pymongo/topology.py new file mode 100644 index 00000000..99adcae6 --- /dev/null +++ b/venv/Lib/site-packages/pymongo/topology.py @@ -0,0 +1,1027 @@ +# Copyright 2014-present MongoDB, 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. + +"""Internal class to monitor a topology of one or more servers.""" + +from __future__ import annotations + +import logging +import os +import queue +import random +import sys +import time +import warnings +import weakref +from pathlib import Path +from typing import TYPE_CHECKING, Any, Callable, Mapping, Optional, cast + +from pymongo import _csot, common, helpers, periodic_executor +from pymongo.client_session import _ServerSession, _ServerSessionPool +from pymongo.errors import ( + ConnectionFailure, + InvalidOperation, + NetworkTimeout, + NotPrimaryError, + OperationFailure, + PyMongoError, + ServerSelectionTimeoutError, + WriteError, +) +from pymongo.hello import Hello +from pymongo.lock import _create_lock +from pymongo.logger import ( + _SERVER_SELECTION_LOGGER, + _debug_log, + _info_log, + _ServerSelectionStatusMessage, +) +from pymongo.monitor import SrvMonitor +from pymongo.pool import Pool, PoolOptions +from pymongo.server import Server +from pymongo.server_description import ServerDescription +from pymongo.server_selectors import ( + Selection, + any_server_selector, + arbiter_server_selector, + secondary_server_selector, + writable_server_selector, +) +from pymongo.topology_description import ( + SRV_POLLING_TOPOLOGIES, + TOPOLOGY_TYPE, + TopologyDescription, + _updated_topology_description_srv_polling, + updated_topology_description, +) + +if TYPE_CHECKING: + from bson import ObjectId + from pymongo.settings import TopologySettings + from pymongo.typings import ClusterTime, _Address + + +_pymongo_dir = str(Path(__file__).parent) + + +def process_events_queue(queue_ref: weakref.ReferenceType[queue.Queue]) -> bool: + q = queue_ref() + if not q: + return False # Cancel PeriodicExecutor. + + while True: + try: + event = q.get_nowait() + except queue.Empty: + break + else: + fn, args = event + fn(*args) + + return True # Continue PeriodicExecutor. + + +class Topology: + """Monitor a topology of one or more servers.""" + + def __init__(self, topology_settings: TopologySettings): + self._topology_id = topology_settings._topology_id + self._listeners = topology_settings._pool_options._event_listeners + self._publish_server = self._listeners is not None and self._listeners.enabled_for_server + self._publish_tp = self._listeners is not None and self._listeners.enabled_for_topology + + # Create events queue if there are publishers. + self._events = None + self.__events_executor: Any = None + + if self._publish_server or self._publish_tp: + self._events = queue.Queue(maxsize=100) + + if self._publish_tp: + assert self._events is not None + self._events.put((self._listeners.publish_topology_opened, (self._topology_id,))) + self._settings = topology_settings + topology_description = TopologyDescription( + topology_settings.get_topology_type(), + topology_settings.get_server_descriptions(), + topology_settings.replica_set_name, + None, + None, + topology_settings, + ) + + self._description = topology_description + if self._publish_tp: + assert self._events is not None + initial_td = TopologyDescription( + TOPOLOGY_TYPE.Unknown, {}, None, None, None, self._settings + ) + self._events.put( + ( + self._listeners.publish_topology_description_changed, + (initial_td, self._description, self._topology_id), + ) + ) + + for seed in topology_settings.seeds: + if self._publish_server: + assert self._events is not None + self._events.put((self._listeners.publish_server_opened, (seed, self._topology_id))) + + # Store the seed list to help diagnose errors in _error_message(). + self._seed_addresses = list(topology_description.server_descriptions()) + self._opened = False + self._closed = False + self._lock = _create_lock() + self._condition = self._settings.condition_class(self._lock) + self._servers: dict[_Address, Server] = {} + self._pid: Optional[int] = None + self._max_cluster_time: Optional[ClusterTime] = None + self._session_pool = _ServerSessionPool() + + if self._publish_server or self._publish_tp: + assert self._events is not None + weak: weakref.ReferenceType[queue.Queue] + + def target() -> bool: + return process_events_queue(weak) + + executor = periodic_executor.PeriodicExecutor( + interval=common.EVENTS_QUEUE_FREQUENCY, + min_interval=common.MIN_HEARTBEAT_INTERVAL, + target=target, + name="pymongo_events_thread", + ) + + # We strongly reference the executor and it weakly references + # the queue via this closure. When the topology is freed, stop + # the executor soon. + weak = weakref.ref(self._events, executor.close) + self.__events_executor = executor + executor.open() + + self._srv_monitor = None + if self._settings.fqdn is not None and not self._settings.load_balanced: + self._srv_monitor = SrvMonitor(self, self._settings) + + def open(self) -> None: + """Start monitoring, or restart after a fork. + + No effect if called multiple times. + + .. warning:: Topology is shared among multiple threads and is protected + by mutual exclusion. Using Topology from a process other than the one + that initialized it will emit a warning and may result in deadlock. To + prevent this from happening, MongoClient must be created after any + forking. + + """ + pid = os.getpid() + if self._pid is None: + self._pid = pid + elif pid != self._pid: + self._pid = pid + if sys.version_info[:2] >= (3, 12): + kwargs = {"skip_file_prefixes": (_pymongo_dir,)} + else: + kwargs = {"stacklevel": 6} + # Ignore B028 warning for missing stacklevel. + warnings.warn( # type: ignore[call-overload] # noqa: B028 + "MongoClient opened before fork. May not be entirely fork-safe, " + "proceed with caution. See PyMongo's documentation for details: " + "https://pymongo.readthedocs.io/en/stable/faq.html#" + "is-pymongo-fork-safe", + **kwargs, + ) + with self._lock: + # Close servers and clear the pools. + for server in self._servers.values(): + server.close() + # Reset the session pool to avoid duplicate sessions in + # the child process. + self._session_pool.reset() + + with self._lock: + self._ensure_opened() + + def get_server_selection_timeout(self) -> float: + # CSOT: use remaining timeout when set. + timeout = _csot.remaining() + if timeout is None: + return self._settings.server_selection_timeout + return timeout + + def select_servers( + self, + selector: Callable[[Selection], Selection], + operation: str, + server_selection_timeout: Optional[float] = None, + address: Optional[_Address] = None, + operation_id: Optional[int] = None, + ) -> list[Server]: + """Return a list of Servers matching selector, or time out. + + :param selector: function that takes a list of Servers and returns + a subset of them. + :param operation: The name of the operation that the server is being selected for. + :param server_selection_timeout: maximum seconds to wait. + If not provided, the default value common.SERVER_SELECTION_TIMEOUT + is used. + :param address: optional server address to select. + + Calls self.open() if needed. + + Raises exc:`ServerSelectionTimeoutError` after + `server_selection_timeout` if no matching servers are found. + """ + if server_selection_timeout is None: + server_timeout = self.get_server_selection_timeout() + else: + server_timeout = server_selection_timeout + + with self._lock: + server_descriptions = self._select_servers_loop( + selector, server_timeout, operation, operation_id, address + ) + + return [ + cast(Server, self.get_server_by_address(sd.address)) for sd in server_descriptions + ] + + def _select_servers_loop( + self, + selector: Callable[[Selection], Selection], + timeout: float, + operation: str, + operation_id: Optional[int], + address: Optional[_Address], + ) -> list[ServerDescription]: + """select_servers() guts. Hold the lock when calling this.""" + now = time.monotonic() + end_time = now + timeout + logged_waiting = False + + if _SERVER_SELECTION_LOGGER.isEnabledFor(logging.DEBUG): + _debug_log( + _SERVER_SELECTION_LOGGER, + message=_ServerSelectionStatusMessage.STARTED, + selector=selector, + operation=operation, + operationId=operation_id, + topologyDescription=self.description, + clientId=self.description._topology_settings._topology_id, + ) + + server_descriptions = self._description.apply_selector( + selector, address, custom_selector=self._settings.server_selector + ) + + while not server_descriptions: + # No suitable servers. + if timeout == 0 or now > end_time: + if _SERVER_SELECTION_LOGGER.isEnabledFor(logging.DEBUG): + _debug_log( + _SERVER_SELECTION_LOGGER, + message=_ServerSelectionStatusMessage.FAILED, + selector=selector, + operation=operation, + operationId=operation_id, + topologyDescription=self.description, + clientId=self.description._topology_settings._topology_id, + failure=self._error_message(selector), + ) + raise ServerSelectionTimeoutError( + f"{self._error_message(selector)}, Timeout: {timeout}s, Topology Description: {self.description!r}" + ) + + if not logged_waiting: + _info_log( + _SERVER_SELECTION_LOGGER, + message=_ServerSelectionStatusMessage.WAITING, + selector=selector, + operation=operation, + operationId=operation_id, + topologyDescription=self.description, + clientId=self.description._topology_settings._topology_id, + remainingTimeMS=int(end_time - time.monotonic()), + ) + logged_waiting = True + + self._ensure_opened() + self._request_check_all() + + # Release the lock and wait for the topology description to + # change, or for a timeout. We won't miss any changes that + # came after our most recent apply_selector call, since we've + # held the lock until now. + self._condition.wait(common.MIN_HEARTBEAT_INTERVAL) + self._description.check_compatible() + now = time.monotonic() + server_descriptions = self._description.apply_selector( + selector, address, custom_selector=self._settings.server_selector + ) + + self._description.check_compatible() + return server_descriptions + + def _select_server( + self, + selector: Callable[[Selection], Selection], + operation: str, + server_selection_timeout: Optional[float] = None, + address: Optional[_Address] = None, + deprioritized_servers: Optional[list[Server]] = None, + operation_id: Optional[int] = None, + ) -> Server: + servers = self.select_servers( + selector, operation, server_selection_timeout, address, operation_id + ) + servers = _filter_servers(servers, deprioritized_servers) + if len(servers) == 1: + return servers[0] + server1, server2 = random.sample(servers, 2) + if server1.pool.operation_count <= server2.pool.operation_count: + return server1 + else: + return server2 + + def select_server( + self, + selector: Callable[[Selection], Selection], + operation: str, + server_selection_timeout: Optional[float] = None, + address: Optional[_Address] = None, + deprioritized_servers: Optional[list[Server]] = None, + operation_id: Optional[int] = None, + ) -> Server: + """Like select_servers, but choose a random server if several match.""" + server = self._select_server( + selector, + operation, + server_selection_timeout, + address, + deprioritized_servers, + operation_id=operation_id, + ) + if _csot.get_timeout(): + _csot.set_rtt(server.description.min_round_trip_time) + if _SERVER_SELECTION_LOGGER.isEnabledFor(logging.DEBUG): + _debug_log( + _SERVER_SELECTION_LOGGER, + message=_ServerSelectionStatusMessage.SUCCEEDED, + selector=selector, + operation=operation, + operationId=operation_id, + topologyDescription=self.description, + clientId=self.description._topology_settings._topology_id, + serverHost=server.description.address[0], + serverPort=server.description.address[1], + ) + return server + + def select_server_by_address( + self, + address: _Address, + operation: str, + server_selection_timeout: Optional[int] = None, + operation_id: Optional[int] = None, + ) -> Server: + """Return a Server for "address", reconnecting if necessary. + + If the server's type is not known, request an immediate check of all + servers. Time out after "server_selection_timeout" if the server + cannot be reached. + + :param address: A (host, port) pair. + :param operation: The name of the operation that the server is being selected for. + :param server_selection_timeout: maximum seconds to wait. + If not provided, the default value + common.SERVER_SELECTION_TIMEOUT is used. + :param operation_id: The unique id of the current operation being performed. Defaults to None if not provided. + + Calls self.open() if needed. + + Raises exc:`ServerSelectionTimeoutError` after + `server_selection_timeout` if no matching servers are found. + """ + return self.select_server( + any_server_selector, + operation, + server_selection_timeout, + address, + operation_id=operation_id, + ) + + def _process_change( + self, + server_description: ServerDescription, + reset_pool: bool = False, + interrupt_connections: bool = False, + ) -> None: + """Process a new ServerDescription on an opened topology. + + Hold the lock when calling this. + """ + td_old = self._description + sd_old = td_old._server_descriptions[server_description.address] + if _is_stale_server_description(sd_old, server_description): + # This is a stale hello response. Ignore it. + return + + new_td = updated_topology_description(self._description, server_description) + # CMAP: Ensure the pool is "ready" when the server is selectable. + if server_description.is_readable or ( + server_description.is_server_type_known and new_td.topology_type == TOPOLOGY_TYPE.Single + ): + server = self._servers.get(server_description.address) + if server: + server.pool.ready() + + suppress_event = (self._publish_server or self._publish_tp) and sd_old == server_description + if self._publish_server and not suppress_event: + assert self._events is not None + self._events.put( + ( + self._listeners.publish_server_description_changed, + (sd_old, server_description, server_description.address, self._topology_id), + ) + ) + + self._description = new_td + self._update_servers() + self._receive_cluster_time_no_lock(server_description.cluster_time) + + if self._publish_tp and not suppress_event: + assert self._events is not None + self._events.put( + ( + self._listeners.publish_topology_description_changed, + (td_old, self._description, self._topology_id), + ) + ) + + # Shutdown SRV polling for unsupported cluster types. + # This is only applicable if the old topology was Unknown, and the + # new one is something other than Unknown or Sharded. + if self._srv_monitor and ( + td_old.topology_type == TOPOLOGY_TYPE.Unknown + and self._description.topology_type not in SRV_POLLING_TOPOLOGIES + ): + self._srv_monitor.close() + + # Clear the pool from a failed heartbeat. + if reset_pool: + server = self._servers.get(server_description.address) + if server: + server.pool.reset(interrupt_connections=interrupt_connections) + + # Wake waiters in select_servers(). + self._condition.notify_all() + + def on_change( + self, + server_description: ServerDescription, + reset_pool: bool = False, + interrupt_connections: bool = False, + ) -> None: + """Process a new ServerDescription after an hello call completes.""" + # We do no I/O holding the lock. + with self._lock: + # Monitors may continue working on hello calls for some time + # after a call to Topology.close, so this method may be called at + # any time. Ensure the topology is open before processing the + # change. + # Any monitored server was definitely in the topology description + # once. Check if it's still in the description or if some state- + # change removed it. E.g., we got a host list from the primary + # that didn't include this server. + if self._opened and self._description.has_server(server_description.address): + self._process_change(server_description, reset_pool, interrupt_connections) + + def _process_srv_update(self, seedlist: list[tuple[str, Any]]) -> None: + """Process a new seedlist on an opened topology. + Hold the lock when calling this. + """ + td_old = self._description + if td_old.topology_type not in SRV_POLLING_TOPOLOGIES: + return + self._description = _updated_topology_description_srv_polling(self._description, seedlist) + + self._update_servers() + + if self._publish_tp: + assert self._events is not None + self._events.put( + ( + self._listeners.publish_topology_description_changed, + (td_old, self._description, self._topology_id), + ) + ) + + def on_srv_update(self, seedlist: list[tuple[str, Any]]) -> None: + """Process a new list of nodes obtained from scanning SRV records.""" + # We do no I/O holding the lock. + with self._lock: + if self._opened: + self._process_srv_update(seedlist) + + def get_server_by_address(self, address: _Address) -> Optional[Server]: + """Get a Server or None. + + Returns the current version of the server immediately, even if it's + Unknown or absent from the topology. Only use this in unittests. + In driver code, use select_server_by_address, since then you're + assured a recent view of the server's type and wire protocol version. + """ + return self._servers.get(address) + + def has_server(self, address: _Address) -> bool: + return address in self._servers + + def get_primary(self) -> Optional[_Address]: + """Return primary's address or None.""" + # Implemented here in Topology instead of MongoClient, so it can lock. + with self._lock: + topology_type = self._description.topology_type + if topology_type != TOPOLOGY_TYPE.ReplicaSetWithPrimary: + return None + + return writable_server_selector(self._new_selection())[0].address + + def _get_replica_set_members(self, selector: Callable[[Selection], Selection]) -> set[_Address]: + """Return set of replica set member addresses.""" + # Implemented here in Topology instead of MongoClient, so it can lock. + with self._lock: + topology_type = self._description.topology_type + if topology_type not in ( + TOPOLOGY_TYPE.ReplicaSetWithPrimary, + TOPOLOGY_TYPE.ReplicaSetNoPrimary, + ): + return set() + + return {sd.address for sd in iter(selector(self._new_selection()))} + + def get_secondaries(self) -> set[_Address]: + """Return set of secondary addresses.""" + return self._get_replica_set_members(secondary_server_selector) + + def get_arbiters(self) -> set[_Address]: + """Return set of arbiter addresses.""" + return self._get_replica_set_members(arbiter_server_selector) + + def max_cluster_time(self) -> Optional[ClusterTime]: + """Return a document, the highest seen $clusterTime.""" + return self._max_cluster_time + + def _receive_cluster_time_no_lock(self, cluster_time: Optional[Mapping[str, Any]]) -> None: + # Driver Sessions Spec: "Whenever a driver receives a cluster time from + # a server it MUST compare it to the current highest seen cluster time + # for the deployment. If the new cluster time is higher than the + # highest seen cluster time it MUST become the new highest seen cluster + # time. Two cluster times are compared using only the BsonTimestamp + # value of the clusterTime embedded field." + if cluster_time: + # ">" uses bson.timestamp.Timestamp's comparison operator. + if ( + not self._max_cluster_time + or cluster_time["clusterTime"] > self._max_cluster_time["clusterTime"] + ): + self._max_cluster_time = cluster_time + + def receive_cluster_time(self, cluster_time: Optional[Mapping[str, Any]]) -> None: + with self._lock: + self._receive_cluster_time_no_lock(cluster_time) + + def request_check_all(self, wait_time: int = 5) -> None: + """Wake all monitors, wait for at least one to check its server.""" + with self._lock: + self._request_check_all() + self._condition.wait(wait_time) + + def data_bearing_servers(self) -> list[ServerDescription]: + """Return a list of all data-bearing servers. + + This includes any server that might be selected for an operation. + """ + if self._description.topology_type == TOPOLOGY_TYPE.Single: + return self._description.known_servers + return self._description.readable_servers + + def update_pool(self) -> None: + # Remove any stale sockets and add new sockets if pool is too small. + servers = [] + with self._lock: + # Only update pools for data-bearing servers. + for sd in self.data_bearing_servers(): + server = self._servers[sd.address] + servers.append((server, server.pool.gen.get_overall())) + + for server, generation in servers: + try: + server.pool.remove_stale_sockets(generation) + except PyMongoError as exc: + ctx = _ErrorContext(exc, 0, generation, False, None) + self.handle_error(server.description.address, ctx) + raise + + def close(self) -> None: + """Clear pools and terminate monitors. Topology does not reopen on + demand. Any further operations will raise + :exc:`~.errors.InvalidOperation`. + """ + with self._lock: + for server in self._servers.values(): + server.close() + + # Mark all servers Unknown. + self._description = self._description.reset() + for address, sd in self._description.server_descriptions().items(): + if address in self._servers: + self._servers[address].description = sd + + # Stop SRV polling thread. + if self._srv_monitor: + self._srv_monitor.close() + + self._opened = False + self._closed = True + + # Publish only after releasing the lock. + if self._publish_tp: + assert self._events is not None + self._events.put((self._listeners.publish_topology_closed, (self._topology_id,))) + if self._publish_server or self._publish_tp: + self.__events_executor.close() + + @property + def description(self) -> TopologyDescription: + return self._description + + def pop_all_sessions(self) -> list[_ServerSession]: + """Pop all session ids from the pool.""" + with self._lock: + return self._session_pool.pop_all() + + def get_server_session(self, session_timeout_minutes: Optional[int]) -> _ServerSession: + """Start or resume a server session, or raise ConfigurationError.""" + with self._lock: + return self._session_pool.get_server_session(session_timeout_minutes) + + def return_server_session(self, server_session: _ServerSession, lock: bool) -> None: + if lock: + with self._lock: + self._session_pool.return_server_session( + server_session, self._description.logical_session_timeout_minutes + ) + else: + # Called from a __del__ method, can't use a lock. + self._session_pool.return_server_session_no_lock(server_session) + + def _new_selection(self) -> Selection: + """A Selection object, initially including all known servers. + + Hold the lock when calling this. + """ + return Selection.from_topology_description(self._description) + + def _ensure_opened(self) -> None: + """Start monitors, or restart after a fork. + + Hold the lock when calling this. + """ + if self._closed: + raise InvalidOperation("Cannot use MongoClient after close") + + if not self._opened: + self._opened = True + self._update_servers() + + # Start or restart the events publishing thread. + if self._publish_tp or self._publish_server: + self.__events_executor.open() + + # Start the SRV polling thread. + if self._srv_monitor and (self.description.topology_type in SRV_POLLING_TOPOLOGIES): + self._srv_monitor.open() + + if self._settings.load_balanced: + # Emit initial SDAM events for load balancer mode. + self._process_change( + ServerDescription( + self._seed_addresses[0], + Hello({"ok": 1, "serviceId": self._topology_id, "maxWireVersion": 13}), + ) + ) + + # Ensure that the monitors are open. + for server in self._servers.values(): + server.open() + + def _is_stale_error(self, address: _Address, err_ctx: _ErrorContext) -> bool: + server = self._servers.get(address) + if server is None: + # Another thread removed this server from the topology. + return True + + if server._pool.stale_generation(err_ctx.sock_generation, err_ctx.service_id): + # This is an outdated error from a previous pool version. + return True + + # topologyVersion check, ignore error when cur_tv >= error_tv: + cur_tv = server.description.topology_version + error = err_ctx.error + error_tv = None + if error and hasattr(error, "details"): + if isinstance(error.details, dict): + error_tv = error.details.get("topologyVersion") + + return _is_stale_error_topology_version(cur_tv, error_tv) + + def _handle_error(self, address: _Address, err_ctx: _ErrorContext) -> None: + if self._is_stale_error(address, err_ctx): + return + + server = self._servers[address] + error = err_ctx.error + service_id = err_ctx.service_id + + # Ignore a handshake error if the server is behind a load balancer but + # the service ID is unknown. This indicates that the error happened + # when dialing the connection or during the MongoDB handshake, so we + # don't know the service ID to use for clearing the pool. + if self._settings.load_balanced and not service_id and not err_ctx.completed_handshake: + return + + if isinstance(error, NetworkTimeout) and err_ctx.completed_handshake: + # The socket has been closed. Don't reset the server. + # Server Discovery And Monitoring Spec: "When an application + # operation fails because of any network error besides a socket + # timeout...." + return + elif isinstance(error, WriteError): + # Ignore writeErrors. + return + elif isinstance(error, (NotPrimaryError, OperationFailure)): + # As per the SDAM spec if: + # - the server sees a "not primary" error, and + # - the server is not shutting down, and + # - the server version is >= 4.2, then + # we keep the existing connection pool, but mark the server type + # as Unknown and request an immediate check of the server. + # Otherwise, we clear the connection pool, mark the server as + # Unknown and request an immediate check of the server. + if hasattr(error, "code"): + err_code = error.code + else: + # Default error code if one does not exist. + default = 10107 if isinstance(error, NotPrimaryError) else None + err_code = error.details.get("code", default) # type: ignore[union-attr] + if err_code in helpers._NOT_PRIMARY_CODES: + is_shutting_down = err_code in helpers._SHUTDOWN_CODES + # Mark server Unknown, clear the pool, and request check. + if not self._settings.load_balanced: + self._process_change(ServerDescription(address, error=error)) + if is_shutting_down or (err_ctx.max_wire_version <= 7): + # Clear the pool. + server.reset(service_id) + server.request_check() + elif not err_ctx.completed_handshake: + # Unknown command error during the connection handshake. + if not self._settings.load_balanced: + self._process_change(ServerDescription(address, error=error)) + # Clear the pool. + server.reset(service_id) + elif isinstance(error, ConnectionFailure): + # "Client MUST replace the server's description with type Unknown + # ... MUST NOT request an immediate check of the server." + if not self._settings.load_balanced: + self._process_change(ServerDescription(address, error=error)) + # Clear the pool. + server.reset(service_id) + # "When a client marks a server Unknown from `Network error when + # reading or writing`_, clients MUST cancel the hello check on + # that server and close the current monitoring connection." + server._monitor.cancel_check() + + def handle_error(self, address: _Address, err_ctx: _ErrorContext) -> None: + """Handle an application error. + + May reset the server to Unknown, clear the pool, and request an + immediate check depending on the error and the context. + """ + with self._lock: + self._handle_error(address, err_ctx) + + def _request_check_all(self) -> None: + """Wake all monitors. Hold the lock when calling this.""" + for server in self._servers.values(): + server.request_check() + + def _update_servers(self) -> None: + """Sync our Servers from TopologyDescription.server_descriptions. + + Hold the lock while calling this. + """ + for address, sd in self._description.server_descriptions().items(): + if address not in self._servers: + monitor = self._settings.monitor_class( + server_description=sd, + topology=self, + pool=self._create_pool_for_monitor(address), + topology_settings=self._settings, + ) + + weak = None + if self._publish_server and self._events is not None: + weak = weakref.ref(self._events) + server = Server( + server_description=sd, + pool=self._create_pool_for_server(address), + monitor=monitor, + topology_id=self._topology_id, + listeners=self._listeners, + events=weak, + ) + + self._servers[address] = server + server.open() + else: + # Cache old is_writable value. + was_writable = self._servers[address].description.is_writable + # Update server description. + self._servers[address].description = sd + # Update is_writable value of the pool, if it changed. + if was_writable != sd.is_writable: + self._servers[address].pool.update_is_writable(sd.is_writable) + + for address, server in list(self._servers.items()): + if not self._description.has_server(address): + server.close() + self._servers.pop(address) + + def _create_pool_for_server(self, address: _Address) -> Pool: + return self._settings.pool_class( + address, self._settings.pool_options, client_id=self._topology_id + ) + + def _create_pool_for_monitor(self, address: _Address) -> Pool: + options = self._settings.pool_options + + # According to the Server Discovery And Monitoring Spec, monitors use + # connect_timeout for both connect_timeout and socket_timeout. The + # pool only has one socket so maxPoolSize and so on aren't needed. + monitor_pool_options = PoolOptions( + connect_timeout=options.connect_timeout, + socket_timeout=options.connect_timeout, + ssl_context=options._ssl_context, + tls_allow_invalid_hostnames=options.tls_allow_invalid_hostnames, + event_listeners=options._event_listeners, + appname=options.appname, + driver=options.driver, + pause_enabled=False, + server_api=options.server_api, + ) + + return self._settings.pool_class( + address, monitor_pool_options, handshake=False, client_id=self._topology_id + ) + + def _error_message(self, selector: Callable[[Selection], Selection]) -> str: + """Format an error message if server selection fails. + + Hold the lock when calling this. + """ + is_replica_set = self._description.topology_type in ( + TOPOLOGY_TYPE.ReplicaSetWithPrimary, + TOPOLOGY_TYPE.ReplicaSetNoPrimary, + ) + + if is_replica_set: + server_plural = "replica set members" + elif self._description.topology_type == TOPOLOGY_TYPE.Sharded: + server_plural = "mongoses" + else: + server_plural = "servers" + + if self._description.known_servers: + # We've connected, but no servers match the selector. + if selector is writable_server_selector: + if is_replica_set: + return "No primary available for writes" + else: + return "No %s available for writes" % server_plural + else: + return f'No {server_plural} match selector "{selector}"' + else: + addresses = list(self._description.server_descriptions()) + servers = list(self._description.server_descriptions().values()) + if not servers: + if is_replica_set: + # We removed all servers because of the wrong setName? + return 'No {} available for replica set name "{}"'.format( + server_plural, + self._settings.replica_set_name, + ) + else: + return "No %s available" % server_plural + + # 1 or more servers, all Unknown. Are they unknown for one reason? + error = servers[0].error + same = all(server.error == error for server in servers[1:]) + if same: + if error is None: + # We're still discovering. + return "No %s found yet" % server_plural + + if is_replica_set and not set(addresses).intersection(self._seed_addresses): + # We replaced our seeds with new hosts but can't reach any. + return ( + "Could not reach any servers in %s. Replica set is" + " configured with internal hostnames or IPs?" % addresses + ) + + return str(error) + else: + return ",".join(str(server.error) for server in servers if server.error) + + def __repr__(self) -> str: + msg = "" + if not self._opened: + msg = "CLOSED " + return f"<{self.__class__.__name__} {msg}{self._description!r}>" + + def eq_props(self) -> tuple[tuple[_Address, ...], Optional[str], Optional[str], str]: + """The properties to use for MongoClient/Topology equality checks.""" + ts = self._settings + return (tuple(sorted(ts.seeds)), ts.replica_set_name, ts.fqdn, ts.srv_service_name) + + def __eq__(self, other: object) -> bool: + if isinstance(other, self.__class__): + return self.eq_props() == other.eq_props() + return NotImplemented + + def __hash__(self) -> int: + return hash(self.eq_props()) + + +class _ErrorContext: + """An error with context for SDAM error handling.""" + + def __init__( + self, + error: BaseException, + max_wire_version: int, + sock_generation: int, + completed_handshake: bool, + service_id: Optional[ObjectId], + ): + self.error = error + self.max_wire_version = max_wire_version + self.sock_generation = sock_generation + self.completed_handshake = completed_handshake + self.service_id = service_id + + +def _is_stale_error_topology_version( + current_tv: Optional[Mapping[str, Any]], error_tv: Optional[Mapping[str, Any]] +) -> bool: + """Return True if the error's topologyVersion is <= current.""" + if current_tv is None or error_tv is None: + return False + if current_tv["processId"] != error_tv["processId"]: + return False + return current_tv["counter"] >= error_tv["counter"] + + +def _is_stale_server_description(current_sd: ServerDescription, new_sd: ServerDescription) -> bool: + """Return True if the new topologyVersion is < current.""" + current_tv, new_tv = current_sd.topology_version, new_sd.topology_version + if current_tv is None or new_tv is None: + return False + if current_tv["processId"] != new_tv["processId"]: + return False + return current_tv["counter"] > new_tv["counter"] + + +def _filter_servers( + candidates: list[Server], deprioritized_servers: Optional[list[Server]] = None +) -> list[Server]: + """Filter out deprioritized servers from a list of server candidates.""" + if not deprioritized_servers: + return candidates + + filtered = [server for server in candidates if server not in deprioritized_servers] + + # If not possible to pick a prioritized server, return the original list + return filtered or candidates diff --git a/venv/Lib/site-packages/pymongo/topology_description.py b/venv/Lib/site-packages/pymongo/topology_description.py new file mode 100644 index 00000000..cc2330cb --- /dev/null +++ b/venv/Lib/site-packages/pymongo/topology_description.py @@ -0,0 +1,676 @@ +# Copyright 2014-present MongoDB, 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. + +"""Represent a deployment of MongoDB servers.""" +from __future__ import annotations + +from random import sample +from typing import ( + Any, + Callable, + List, + Mapping, + MutableMapping, + NamedTuple, + Optional, + cast, +) + +from bson.min_key import MinKey +from bson.objectid import ObjectId +from pymongo import common +from pymongo.errors import ConfigurationError +from pymongo.read_preferences import ReadPreference, _AggWritePref, _ServerMode +from pymongo.server_description import ServerDescription +from pymongo.server_selectors import Selection +from pymongo.server_type import SERVER_TYPE +from pymongo.typings import _Address + + +# Enumeration for various kinds of MongoDB cluster topologies. +class _TopologyType(NamedTuple): + Single: int + ReplicaSetNoPrimary: int + ReplicaSetWithPrimary: int + Sharded: int + Unknown: int + LoadBalanced: int + + +TOPOLOGY_TYPE = _TopologyType(*range(6)) + +# Topologies compatible with SRV record polling. +SRV_POLLING_TOPOLOGIES: tuple[int, int] = (TOPOLOGY_TYPE.Unknown, TOPOLOGY_TYPE.Sharded) + + +_ServerSelector = Callable[[List[ServerDescription]], List[ServerDescription]] + + +class TopologyDescription: + def __init__( + self, + topology_type: int, + server_descriptions: dict[_Address, ServerDescription], + replica_set_name: Optional[str], + max_set_version: Optional[int], + max_election_id: Optional[ObjectId], + topology_settings: Any, + ) -> None: + """Representation of a deployment of MongoDB servers. + + :param topology_type: initial type + :param server_descriptions: dict of (address, ServerDescription) for + all seeds + :param replica_set_name: replica set name or None + :param max_set_version: greatest setVersion seen from a primary, or None + :param max_election_id: greatest electionId seen from a primary, or None + :param topology_settings: a TopologySettings + """ + self._topology_type = topology_type + self._replica_set_name = replica_set_name + self._server_descriptions = server_descriptions + self._max_set_version = max_set_version + self._max_election_id = max_election_id + + # The heartbeat_frequency is used in staleness estimates. + self._topology_settings = topology_settings + + # Is PyMongo compatible with all servers' wire protocols? + self._incompatible_err = None + if self._topology_type != TOPOLOGY_TYPE.LoadBalanced: + self._init_incompatible_err() + + # Server Discovery And Monitoring Spec: Whenever a client updates the + # TopologyDescription from an hello response, it MUST set + # TopologyDescription.logicalSessionTimeoutMinutes to the smallest + # logicalSessionTimeoutMinutes value among ServerDescriptions of all + # data-bearing server types. If any have a null + # logicalSessionTimeoutMinutes, then + # TopologyDescription.logicalSessionTimeoutMinutes MUST be set to null. + readable_servers = self.readable_servers + if not readable_servers: + self._ls_timeout_minutes = None + elif any(s.logical_session_timeout_minutes is None for s in readable_servers): + self._ls_timeout_minutes = None + else: + self._ls_timeout_minutes = min( # type: ignore[type-var] + s.logical_session_timeout_minutes for s in readable_servers + ) + + def _init_incompatible_err(self) -> None: + """Internal compatibility check for non-load balanced topologies.""" + for s in self._server_descriptions.values(): + if not s.is_server_type_known: + continue + + # s.min/max_wire_version is the server's wire protocol. + # MIN/MAX_SUPPORTED_WIRE_VERSION is what PyMongo supports. + server_too_new = ( + # Server too new. + s.min_wire_version is not None + and s.min_wire_version > common.MAX_SUPPORTED_WIRE_VERSION + ) + + server_too_old = ( + # Server too old. + s.max_wire_version is not None + and s.max_wire_version < common.MIN_SUPPORTED_WIRE_VERSION + ) + + if server_too_new: + self._incompatible_err = ( + "Server at %s:%d requires wire version %d, but this " # type: ignore + "version of PyMongo only supports up to %d." + % ( + s.address[0], + s.address[1] or 0, + s.min_wire_version, + common.MAX_SUPPORTED_WIRE_VERSION, + ) + ) + + elif server_too_old: + self._incompatible_err = ( + "Server at %s:%d reports wire version %d, but this " # type: ignore + "version of PyMongo requires at least %d (MongoDB %s)." + % ( + s.address[0], + s.address[1] or 0, + s.max_wire_version, + common.MIN_SUPPORTED_WIRE_VERSION, + common.MIN_SUPPORTED_SERVER_VERSION, + ) + ) + + break + + def check_compatible(self) -> None: + """Raise ConfigurationError if any server is incompatible. + + A server is incompatible if its wire protocol version range does not + overlap with PyMongo's. + """ + if self._incompatible_err: + raise ConfigurationError(self._incompatible_err) + + def has_server(self, address: _Address) -> bool: + return address in self._server_descriptions + + def reset_server(self, address: _Address) -> TopologyDescription: + """A copy of this description, with one server marked Unknown.""" + unknown_sd = self._server_descriptions[address].to_unknown() + return updated_topology_description(self, unknown_sd) + + def reset(self) -> TopologyDescription: + """A copy of this description, with all servers marked Unknown.""" + if self._topology_type == TOPOLOGY_TYPE.ReplicaSetWithPrimary: + topology_type = TOPOLOGY_TYPE.ReplicaSetNoPrimary + else: + topology_type = self._topology_type + + # The default ServerDescription's type is Unknown. + sds = {address: ServerDescription(address) for address in self._server_descriptions} + + return TopologyDescription( + topology_type, + sds, + self._replica_set_name, + self._max_set_version, + self._max_election_id, + self._topology_settings, + ) + + def server_descriptions(self) -> dict[_Address, ServerDescription]: + """dict of (address, + :class:`~pymongo.server_description.ServerDescription`). + """ + return self._server_descriptions.copy() + + @property + def topology_type(self) -> int: + """The type of this topology.""" + return self._topology_type + + @property + def topology_type_name(self) -> str: + """The topology type as a human readable string. + + .. versionadded:: 3.4 + """ + return TOPOLOGY_TYPE._fields[self._topology_type] + + @property + def replica_set_name(self) -> Optional[str]: + """The replica set name.""" + return self._replica_set_name + + @property + def max_set_version(self) -> Optional[int]: + """Greatest setVersion seen from a primary, or None.""" + return self._max_set_version + + @property + def max_election_id(self) -> Optional[ObjectId]: + """Greatest electionId seen from a primary, or None.""" + return self._max_election_id + + @property + def logical_session_timeout_minutes(self) -> Optional[int]: + """Minimum logical session timeout, or None.""" + return self._ls_timeout_minutes + + @property + def known_servers(self) -> list[ServerDescription]: + """List of Servers of types besides Unknown.""" + return [s for s in self._server_descriptions.values() if s.is_server_type_known] + + @property + def has_known_servers(self) -> bool: + """Whether there are any Servers of types besides Unknown.""" + return any(s for s in self._server_descriptions.values() if s.is_server_type_known) + + @property + def readable_servers(self) -> list[ServerDescription]: + """List of readable Servers.""" + return [s for s in self._server_descriptions.values() if s.is_readable] + + @property + def common_wire_version(self) -> Optional[int]: + """Minimum of all servers' max wire versions, or None.""" + servers = self.known_servers + if servers: + return min(s.max_wire_version for s in self.known_servers) + + return None + + @property + def heartbeat_frequency(self) -> int: + return self._topology_settings.heartbeat_frequency + + @property + def srv_max_hosts(self) -> int: + return self._topology_settings._srv_max_hosts + + def _apply_local_threshold(self, selection: Optional[Selection]) -> list[ServerDescription]: + if not selection: + return [] + round_trip_times: list[float] = [] + for server in selection.server_descriptions: + if server.round_trip_time is None: + config_err_msg = f"round_trip_time for server {server.address} is unexpectedly None: {self}, servers: {selection.server_descriptions}" + raise ConfigurationError(config_err_msg) + round_trip_times.append(server.round_trip_time) + # Round trip time in seconds. + fastest = min(round_trip_times) + threshold = self._topology_settings.local_threshold_ms / 1000.0 + return [ + s + for s in selection.server_descriptions + if (cast(float, s.round_trip_time) - fastest) <= threshold + ] + + def apply_selector( + self, + selector: Any, + address: Optional[_Address] = None, + custom_selector: Optional[_ServerSelector] = None, + ) -> list[ServerDescription]: + """List of servers matching the provided selector(s). + + :param selector: a callable that takes a Selection as input and returns + a Selection as output. For example, an instance of a read + preference from :mod:`~pymongo.read_preferences`. + :param address: A server address to select. + :param custom_selector: A callable that augments server + selection rules. Accepts a list of + :class:`~pymongo.server_description.ServerDescription` objects and + return a list of server descriptions that should be considered + suitable for the desired operation. + + .. versionadded:: 3.4 + """ + if getattr(selector, "min_wire_version", 0): + common_wv = self.common_wire_version + if common_wv and common_wv < selector.min_wire_version: + raise ConfigurationError( + "%s requires min wire version %d, but topology's min" + " wire version is %d" % (selector, selector.min_wire_version, common_wv) + ) + + if isinstance(selector, _AggWritePref): + selector.selection_hook(self) + + if self.topology_type == TOPOLOGY_TYPE.Unknown: + return [] + elif self.topology_type in (TOPOLOGY_TYPE.Single, TOPOLOGY_TYPE.LoadBalanced): + # Ignore selectors for standalone and load balancer mode. + return self.known_servers + if address: + # Ignore selectors when explicit address is requested. + description = self.server_descriptions().get(address) + return [description] if description else [] + + selection = Selection.from_topology_description(self) + # Ignore read preference for sharded clusters. + if self.topology_type != TOPOLOGY_TYPE.Sharded: + selection = selector(selection) + + # Apply custom selector followed by localThresholdMS. + if custom_selector is not None and selection: + selection = selection.with_server_descriptions( + custom_selector(selection.server_descriptions) + ) + return self._apply_local_threshold(selection) + + def has_readable_server(self, read_preference: _ServerMode = ReadPreference.PRIMARY) -> bool: + """Does this topology have any readable servers available matching the + given read preference? + + :param read_preference: an instance of a read preference from + :mod:`~pymongo.read_preferences`. Defaults to + :attr:`~pymongo.read_preferences.ReadPreference.PRIMARY`. + + .. note:: When connected directly to a single server this method + always returns ``True``. + + .. versionadded:: 3.4 + """ + common.validate_read_preference("read_preference", read_preference) + return any(self.apply_selector(read_preference)) + + def has_writable_server(self) -> bool: + """Does this topology have a writable server available? + + .. note:: When connected directly to a single server this method + always returns ``True``. + + .. versionadded:: 3.4 + """ + return self.has_readable_server(ReadPreference.PRIMARY) + + def __repr__(self) -> str: + # Sort the servers by address. + servers = sorted(self._server_descriptions.values(), key=lambda sd: sd.address) + return "<{} id: {}, topology_type: {}, servers: {!r}>".format( + self.__class__.__name__, + self._topology_settings._topology_id, + self.topology_type_name, + servers, + ) + + +# If topology type is Unknown and we receive a hello response, what should +# the new topology type be? +_SERVER_TYPE_TO_TOPOLOGY_TYPE = { + SERVER_TYPE.Mongos: TOPOLOGY_TYPE.Sharded, + SERVER_TYPE.RSPrimary: TOPOLOGY_TYPE.ReplicaSetWithPrimary, + SERVER_TYPE.RSSecondary: TOPOLOGY_TYPE.ReplicaSetNoPrimary, + SERVER_TYPE.RSArbiter: TOPOLOGY_TYPE.ReplicaSetNoPrimary, + SERVER_TYPE.RSOther: TOPOLOGY_TYPE.ReplicaSetNoPrimary, + # Note: SERVER_TYPE.LoadBalancer and Unknown are intentionally left out. +} + + +def updated_topology_description( + topology_description: TopologyDescription, server_description: ServerDescription +) -> TopologyDescription: + """Return an updated copy of a TopologyDescription. + + :param topology_description: the current TopologyDescription + :param server_description: a new ServerDescription that resulted from + a hello call + + Called after attempting (successfully or not) to call hello on the + server at server_description.address. Does not modify topology_description. + """ + address = server_description.address + + # These values will be updated, if necessary, to form the new + # TopologyDescription. + topology_type = topology_description.topology_type + set_name = topology_description.replica_set_name + max_set_version = topology_description.max_set_version + max_election_id = topology_description.max_election_id + server_type = server_description.server_type + + # Don't mutate the original dict of server descriptions; copy it. + sds = topology_description.server_descriptions() + + # Replace this server's description with the new one. + sds[address] = server_description + + if topology_type == TOPOLOGY_TYPE.Single: + # Set server type to Unknown if replica set name does not match. + if set_name is not None and set_name != server_description.replica_set_name: + error = ConfigurationError( + "client is configured to connect to a replica set named " + "'{}' but this node belongs to a set named '{}'".format( + set_name, server_description.replica_set_name + ) + ) + sds[address] = server_description.to_unknown(error=error) + # Single type never changes. + return TopologyDescription( + TOPOLOGY_TYPE.Single, + sds, + set_name, + max_set_version, + max_election_id, + topology_description._topology_settings, + ) + + if topology_type == TOPOLOGY_TYPE.Unknown: + if server_type in (SERVER_TYPE.Standalone, SERVER_TYPE.LoadBalancer): + if len(topology_description._topology_settings.seeds) == 1: + topology_type = TOPOLOGY_TYPE.Single + else: + # Remove standalone from Topology when given multiple seeds. + sds.pop(address) + elif server_type not in (SERVER_TYPE.Unknown, SERVER_TYPE.RSGhost): + topology_type = _SERVER_TYPE_TO_TOPOLOGY_TYPE[server_type] + + if topology_type == TOPOLOGY_TYPE.Sharded: + if server_type not in (SERVER_TYPE.Mongos, SERVER_TYPE.Unknown): + sds.pop(address) + + elif topology_type == TOPOLOGY_TYPE.ReplicaSetNoPrimary: + if server_type in (SERVER_TYPE.Standalone, SERVER_TYPE.Mongos): + sds.pop(address) + + elif server_type == SERVER_TYPE.RSPrimary: + (topology_type, set_name, max_set_version, max_election_id) = _update_rs_from_primary( + sds, set_name, server_description, max_set_version, max_election_id + ) + + elif server_type in (SERVER_TYPE.RSSecondary, SERVER_TYPE.RSArbiter, SERVER_TYPE.RSOther): + topology_type, set_name = _update_rs_no_primary_from_member( + sds, set_name, server_description + ) + + elif topology_type == TOPOLOGY_TYPE.ReplicaSetWithPrimary: + if server_type in (SERVER_TYPE.Standalone, SERVER_TYPE.Mongos): + sds.pop(address) + topology_type = _check_has_primary(sds) + + elif server_type == SERVER_TYPE.RSPrimary: + (topology_type, set_name, max_set_version, max_election_id) = _update_rs_from_primary( + sds, set_name, server_description, max_set_version, max_election_id + ) + + elif server_type in (SERVER_TYPE.RSSecondary, SERVER_TYPE.RSArbiter, SERVER_TYPE.RSOther): + topology_type = _update_rs_with_primary_from_member(sds, set_name, server_description) + + else: + # Server type is Unknown or RSGhost: did we just lose the primary? + topology_type = _check_has_primary(sds) + + # Return updated copy. + return TopologyDescription( + topology_type, + sds, + set_name, + max_set_version, + max_election_id, + topology_description._topology_settings, + ) + + +def _updated_topology_description_srv_polling( + topology_description: TopologyDescription, seedlist: list[tuple[str, Any]] +) -> TopologyDescription: + """Return an updated copy of a TopologyDescription. + + :param topology_description: the current TopologyDescription + :param seedlist: a list of new seeds new ServerDescription that resulted from + a hello call + """ + assert topology_description.topology_type in SRV_POLLING_TOPOLOGIES + # Create a copy of the server descriptions. + sds = topology_description.server_descriptions() + + # If seeds haven't changed, don't do anything. + if set(sds.keys()) == set(seedlist): + return topology_description + + # Remove SDs corresponding to servers no longer part of the SRV record. + for address in list(sds.keys()): + if address not in seedlist: + sds.pop(address) + + if topology_description.srv_max_hosts != 0: + new_hosts = set(seedlist) - set(sds.keys()) + n_to_add = topology_description.srv_max_hosts - len(sds) + if n_to_add > 0: + seedlist = sample(sorted(new_hosts), min(n_to_add, len(new_hosts))) + else: + seedlist = [] + # Add SDs corresponding to servers recently added to the SRV record. + for address in seedlist: + if address not in sds: + sds[address] = ServerDescription(address) + return TopologyDescription( + topology_description.topology_type, + sds, + topology_description.replica_set_name, + topology_description.max_set_version, + topology_description.max_election_id, + topology_description._topology_settings, + ) + + +def _update_rs_from_primary( + sds: MutableMapping[_Address, ServerDescription], + replica_set_name: Optional[str], + server_description: ServerDescription, + max_set_version: Optional[int], + max_election_id: Optional[ObjectId], +) -> tuple[int, Optional[str], Optional[int], Optional[ObjectId]]: + """Update topology description from a primary's hello response. + + Pass in a dict of ServerDescriptions, current replica set name, the + ServerDescription we are processing, and the TopologyDescription's + max_set_version and max_election_id if any. + + Returns (new topology type, new replica_set_name, new max_set_version, + new max_election_id). + """ + if replica_set_name is None: + replica_set_name = server_description.replica_set_name + + elif replica_set_name != server_description.replica_set_name: + # We found a primary but it doesn't have the replica_set_name + # provided by the user. + sds.pop(server_description.address) + return _check_has_primary(sds), replica_set_name, max_set_version, max_election_id + + if server_description.max_wire_version is None or server_description.max_wire_version < 17: + new_election_tuple: tuple = (server_description.set_version, server_description.election_id) + max_election_tuple: tuple = (max_set_version, max_election_id) + if None not in new_election_tuple: + if None not in max_election_tuple and new_election_tuple < max_election_tuple: + # Stale primary, set to type Unknown. + sds[server_description.address] = server_description.to_unknown() + return _check_has_primary(sds), replica_set_name, max_set_version, max_election_id + max_election_id = server_description.election_id + + if server_description.set_version is not None and ( + max_set_version is None or server_description.set_version > max_set_version + ): + max_set_version = server_description.set_version + else: + new_election_tuple = server_description.election_id, server_description.set_version + max_election_tuple = max_election_id, max_set_version + new_election_safe = tuple(MinKey() if i is None else i for i in new_election_tuple) + max_election_safe = tuple(MinKey() if i is None else i for i in max_election_tuple) + if new_election_safe < max_election_safe: + # Stale primary, set to type Unknown. + sds[server_description.address] = server_description.to_unknown() + return _check_has_primary(sds), replica_set_name, max_set_version, max_election_id + else: + max_election_id = server_description.election_id + max_set_version = server_description.set_version + + # We've heard from the primary. Is it the same primary as before? + for server in sds.values(): + if ( + server.server_type is SERVER_TYPE.RSPrimary + and server.address != server_description.address + ): + # Reset old primary's type to Unknown. + sds[server.address] = server.to_unknown() + + # There can be only one prior primary. + break + + # Discover new hosts from this primary's response. + for new_address in server_description.all_hosts: + if new_address not in sds: + sds[new_address] = ServerDescription(new_address) + + # Remove hosts not in the response. + for addr in set(sds) - server_description.all_hosts: + sds.pop(addr) + + # If the host list differs from the seed list, we may not have a primary + # after all. + return (_check_has_primary(sds), replica_set_name, max_set_version, max_election_id) + + +def _update_rs_with_primary_from_member( + sds: MutableMapping[_Address, ServerDescription], + replica_set_name: Optional[str], + server_description: ServerDescription, +) -> int: + """RS with known primary. Process a response from a non-primary. + + Pass in a dict of ServerDescriptions, current replica set name, and the + ServerDescription we are processing. + + Returns new topology type. + """ + assert replica_set_name is not None + + if replica_set_name != server_description.replica_set_name: + sds.pop(server_description.address) + elif server_description.me and server_description.address != server_description.me: + sds.pop(server_description.address) + + # Had this member been the primary? + return _check_has_primary(sds) + + +def _update_rs_no_primary_from_member( + sds: MutableMapping[_Address, ServerDescription], + replica_set_name: Optional[str], + server_description: ServerDescription, +) -> tuple[int, Optional[str]]: + """RS without known primary. Update from a non-primary's response. + + Pass in a dict of ServerDescriptions, current replica set name, and the + ServerDescription we are processing. + + Returns (new topology type, new replica_set_name). + """ + topology_type = TOPOLOGY_TYPE.ReplicaSetNoPrimary + if replica_set_name is None: + replica_set_name = server_description.replica_set_name + + elif replica_set_name != server_description.replica_set_name: + sds.pop(server_description.address) + return topology_type, replica_set_name + + # This isn't the primary's response, so don't remove any servers + # it doesn't report. Only add new servers. + for address in server_description.all_hosts: + if address not in sds: + sds[address] = ServerDescription(address) + + if server_description.me and server_description.address != server_description.me: + sds.pop(server_description.address) + + return topology_type, replica_set_name + + +def _check_has_primary(sds: Mapping[_Address, ServerDescription]) -> int: + """Current topology type is ReplicaSetWithPrimary. Is primary still known? + + Pass in a dict of ServerDescriptions. + + Returns new topology type. + """ + for s in sds.values(): + if s.server_type == SERVER_TYPE.RSPrimary: + return TOPOLOGY_TYPE.ReplicaSetWithPrimary + else: # noqa: PLW0120 + return TOPOLOGY_TYPE.ReplicaSetNoPrimary diff --git a/venv/Lib/site-packages/pymongo/typings.py b/venv/Lib/site-packages/pymongo/typings.py new file mode 100644 index 00000000..174a0e36 --- /dev/null +++ b/venv/Lib/site-packages/pymongo/typings.py @@ -0,0 +1,60 @@ +# Copyright 2022-Present MongoDB, 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. + +"""Type aliases used by PyMongo""" +from __future__ import annotations + +from typing import ( + TYPE_CHECKING, + Any, + Mapping, + Optional, + Sequence, + Tuple, + TypeVar, + Union, +) + +from bson.typings import _DocumentOut, _DocumentType, _DocumentTypeArg + +if TYPE_CHECKING: + from pymongo.collation import Collation + + +# Common Shared Types. +_Address = Tuple[str, Optional[int]] +_CollationIn = Union[Mapping[str, Any], "Collation"] +_Pipeline = Sequence[Mapping[str, Any]] +ClusterTime = Mapping[str, Any] + +_T = TypeVar("_T") + + +def strip_optional(elem: Optional[_T]) -> _T: + """This function is to allow us to cast all of the elements of an iterator from Optional[_T] to _T + while inside a list comprehension. + """ + assert elem is not None + return elem + + +__all__ = [ + "_DocumentOut", + "_DocumentType", + "_DocumentTypeArg", + "_Address", + "_CollationIn", + "_Pipeline", + "strip_optional", +] diff --git a/venv/Lib/site-packages/pymongo/uri_parser.py b/venv/Lib/site-packages/pymongo/uri_parser.py new file mode 100644 index 00000000..7f4ef57f --- /dev/null +++ b/venv/Lib/site-packages/pymongo/uri_parser.py @@ -0,0 +1,628 @@ +# Copyright 2011-present MongoDB, 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. + + +"""Tools to parse and validate a MongoDB URI.""" +from __future__ import annotations + +import re +import sys +import warnings +from typing import ( + TYPE_CHECKING, + Any, + Mapping, + MutableMapping, + Optional, + Sized, + Union, + cast, +) +from urllib.parse import unquote_plus + +from pymongo.client_options import _parse_ssl_options +from pymongo.common import ( + INTERNAL_URI_OPTION_NAME_MAP, + SRV_SERVICE_NAME, + URI_OPTIONS_DEPRECATION_MAP, + _CaseInsensitiveDictionary, + get_validated_options, +) +from pymongo.errors import ConfigurationError, InvalidURI +from pymongo.srv_resolver import _HAVE_DNSPYTHON, _SrvResolver +from pymongo.typings import _Address + +if TYPE_CHECKING: + from pymongo.pyopenssl_context import SSLContext + +SCHEME = "mongodb://" +SCHEME_LEN = len(SCHEME) +SRV_SCHEME = "mongodb+srv://" +SRV_SCHEME_LEN = len(SRV_SCHEME) +DEFAULT_PORT = 27017 + + +def _unquoted_percent(s: str) -> bool: + """Check for unescaped percent signs. + + :param s: A string. `s` can have things like '%25', '%2525', + and '%E2%85%A8' but cannot have unquoted percent like '%foo'. + """ + for i in range(len(s)): + if s[i] == "%": + sub = s[i : i + 3] + # If unquoting yields the same string this means there was an + # unquoted %. + if unquote_plus(sub) == sub: + return True + return False + + +def parse_userinfo(userinfo: str) -> tuple[str, str]: + """Validates the format of user information in a MongoDB URI. + Reserved characters that are gen-delimiters (":", "/", "?", "#", "[", + "]", "@") as per RFC 3986 must be escaped. + + Returns a 2-tuple containing the unescaped username followed + by the unescaped password. + + :param userinfo: A string of the form : + """ + if "@" in userinfo or userinfo.count(":") > 1 or _unquoted_percent(userinfo): + raise InvalidURI( + "Username and password must be escaped according to " + "RFC 3986, use urllib.parse.quote_plus" + ) + + user, _, passwd = userinfo.partition(":") + # No password is expected with GSSAPI authentication. + if not user: + raise InvalidURI("The empty string is not valid username.") + + return unquote_plus(user), unquote_plus(passwd) + + +def parse_ipv6_literal_host( + entity: str, default_port: Optional[int] +) -> tuple[str, Optional[Union[str, int]]]: + """Validates an IPv6 literal host:port string. + + Returns a 2-tuple of IPv6 literal followed by port where + port is default_port if it wasn't specified in entity. + + :param entity: A string that represents an IPv6 literal enclosed + in braces (e.g. '[::1]' or '[::1]:27017'). + :param default_port: The port number to use when one wasn't + specified in entity. + """ + if entity.find("]") == -1: + raise ValueError( + "an IPv6 address literal must be enclosed in '[' and ']' according to RFC 2732." + ) + i = entity.find("]:") + if i == -1: + return entity[1:-1], default_port + return entity[1:i], entity[i + 2 :] + + +def parse_host(entity: str, default_port: Optional[int] = DEFAULT_PORT) -> _Address: + """Validates a host string + + Returns a 2-tuple of host followed by port where port is default_port + if it wasn't specified in the string. + + :param entity: A host or host:port string where host could be a + hostname or IP address. + :param default_port: The port number to use when one wasn't + specified in entity. + """ + host = entity + port: Optional[Union[str, int]] = default_port + if entity[0] == "[": + host, port = parse_ipv6_literal_host(entity, default_port) + elif entity.endswith(".sock"): + return entity, default_port + elif entity.find(":") != -1: + if entity.count(":") > 1: + raise ValueError( + "Reserved characters such as ':' must be " + "escaped according RFC 2396. An IPv6 " + "address literal must be enclosed in '[' " + "and ']' according to RFC 2732." + ) + host, port = host.split(":", 1) + if isinstance(port, str): + if not port.isdigit() or int(port) > 65535 or int(port) <= 0: + raise ValueError(f"Port must be an integer between 0 and 65535: {port!r}") + port = int(port) + + # Normalize hostname to lowercase, since DNS is case-insensitive: + # http://tools.ietf.org/html/rfc4343 + # This prevents useless rediscovery if "foo.com" is in the seed list but + # "FOO.com" is in the hello response. + return host.lower(), port + + +# Options whose values are implicitly determined by tlsInsecure. +_IMPLICIT_TLSINSECURE_OPTS = { + "tlsallowinvalidcertificates", + "tlsallowinvalidhostnames", + "tlsdisableocspendpointcheck", +} + + +def _parse_options(opts: str, delim: Optional[str]) -> _CaseInsensitiveDictionary: + """Helper method for split_options which creates the options dict. + Also handles the creation of a list for the URI tag_sets/ + readpreferencetags portion, and the use of a unicode options string. + """ + options = _CaseInsensitiveDictionary() + for uriopt in opts.split(delim): + key, value = uriopt.split("=") + if key.lower() == "readpreferencetags": + options.setdefault(key, []).append(value) + else: + if key in options: + warnings.warn(f"Duplicate URI option '{key}'.", stacklevel=2) + if key.lower() == "authmechanismproperties": + val = value + else: + val = unquote_plus(value) + options[key] = val + + return options + + +def _handle_security_options(options: _CaseInsensitiveDictionary) -> _CaseInsensitiveDictionary: + """Raise appropriate errors when conflicting TLS options are present in + the options dictionary. + + :param options: Instance of _CaseInsensitiveDictionary containing + MongoDB URI options. + """ + # Implicitly defined options must not be explicitly specified. + tlsinsecure = options.get("tlsinsecure") + if tlsinsecure is not None: + for opt in _IMPLICIT_TLSINSECURE_OPTS: + if opt in options: + err_msg = "URI options %s and %s cannot be specified simultaneously." + raise InvalidURI( + err_msg % (options.cased_key("tlsinsecure"), options.cased_key(opt)) + ) + + # Handle co-occurence of OCSP & tlsAllowInvalidCertificates options. + tlsallowinvalidcerts = options.get("tlsallowinvalidcertificates") + if tlsallowinvalidcerts is not None: + if "tlsdisableocspendpointcheck" in options: + err_msg = "URI options %s and %s cannot be specified simultaneously." + raise InvalidURI( + err_msg + % ("tlsallowinvalidcertificates", options.cased_key("tlsdisableocspendpointcheck")) + ) + if tlsallowinvalidcerts is True: + options["tlsdisableocspendpointcheck"] = True + + # Handle co-occurence of CRL and OCSP-related options. + tlscrlfile = options.get("tlscrlfile") + if tlscrlfile is not None: + for opt in ("tlsinsecure", "tlsallowinvalidcertificates", "tlsdisableocspendpointcheck"): + if options.get(opt) is True: + err_msg = "URI option %s=True cannot be specified when CRL checking is enabled." + raise InvalidURI(err_msg % (opt,)) + + if "ssl" in options and "tls" in options: + + def truth_value(val: Any) -> Any: + if val in ("true", "false"): + return val == "true" + if isinstance(val, bool): + return val + return val + + if truth_value(options.get("ssl")) != truth_value(options.get("tls")): + err_msg = "Can not specify conflicting values for URI options %s and %s." + raise InvalidURI(err_msg % (options.cased_key("ssl"), options.cased_key("tls"))) + + return options + + +def _handle_option_deprecations(options: _CaseInsensitiveDictionary) -> _CaseInsensitiveDictionary: + """Issue appropriate warnings when deprecated options are present in the + options dictionary. Removes deprecated option key, value pairs if the + options dictionary is found to also have the renamed option. + + :param options: Instance of _CaseInsensitiveDictionary containing + MongoDB URI options. + """ + for optname in list(options): + if optname in URI_OPTIONS_DEPRECATION_MAP: + mode, message = URI_OPTIONS_DEPRECATION_MAP[optname] + if mode == "renamed": + newoptname = message + if newoptname in options: + warn_msg = "Deprecated option '%s' ignored in favor of '%s'." + warnings.warn( + warn_msg % (options.cased_key(optname), options.cased_key(newoptname)), + DeprecationWarning, + stacklevel=2, + ) + options.pop(optname) + continue + warn_msg = "Option '%s' is deprecated, use '%s' instead." + warnings.warn( + warn_msg % (options.cased_key(optname), newoptname), + DeprecationWarning, + stacklevel=2, + ) + elif mode == "removed": + warn_msg = "Option '%s' is deprecated. %s." + warnings.warn( + warn_msg % (options.cased_key(optname), message), + DeprecationWarning, + stacklevel=2, + ) + + return options + + +def _normalize_options(options: _CaseInsensitiveDictionary) -> _CaseInsensitiveDictionary: + """Normalizes option names in the options dictionary by converting them to + their internally-used names. + + :param options: Instance of _CaseInsensitiveDictionary containing + MongoDB URI options. + """ + # Expand the tlsInsecure option. + tlsinsecure = options.get("tlsinsecure") + if tlsinsecure is not None: + for opt in _IMPLICIT_TLSINSECURE_OPTS: + # Implicit options are logically the same as tlsInsecure. + options[opt] = tlsinsecure + + for optname in list(options): + intname = INTERNAL_URI_OPTION_NAME_MAP.get(optname, None) + if intname is not None: + options[intname] = options.pop(optname) + + return options + + +def validate_options(opts: Mapping[str, Any], warn: bool = False) -> MutableMapping[str, Any]: + """Validates and normalizes options passed in a MongoDB URI. + + Returns a new dictionary of validated and normalized options. If warn is + False then errors will be thrown for invalid options, otherwise they will + be ignored and a warning will be issued. + + :param opts: A dict of MongoDB URI options. + :param warn: If ``True`` then warnings will be logged and + invalid options will be ignored. Otherwise invalid options will + cause errors. + """ + return get_validated_options(opts, warn) + + +def split_options( + opts: str, validate: bool = True, warn: bool = False, normalize: bool = True +) -> MutableMapping[str, Any]: + """Takes the options portion of a MongoDB URI, validates each option + and returns the options in a dictionary. + + :param opt: A string representing MongoDB URI options. + :param validate: If ``True`` (the default), validate and normalize all + options. + :param warn: If ``False`` (the default), suppress all warnings raised + during validation of options. + :param normalize: If ``True`` (the default), renames all options to their + internally-used names. + """ + and_idx = opts.find("&") + semi_idx = opts.find(";") + try: + if and_idx >= 0 and semi_idx >= 0: + raise InvalidURI("Can not mix '&' and ';' for option separators.") + elif and_idx >= 0: + options = _parse_options(opts, "&") + elif semi_idx >= 0: + options = _parse_options(opts, ";") + elif opts.find("=") != -1: + options = _parse_options(opts, None) + else: + raise ValueError + except ValueError: + raise InvalidURI("MongoDB URI options are key=value pairs.") from None + + options = _handle_security_options(options) + + options = _handle_option_deprecations(options) + + if normalize: + options = _normalize_options(options) + + if validate: + options = cast(_CaseInsensitiveDictionary, validate_options(options, warn)) + if options.get("authsource") == "": + raise InvalidURI("the authSource database cannot be an empty string") + + return options + + +def split_hosts(hosts: str, default_port: Optional[int] = DEFAULT_PORT) -> list[_Address]: + """Takes a string of the form host1[:port],host2[:port]... and + splits it into (host, port) tuples. If [:port] isn't present the + default_port is used. + + Returns a set of 2-tuples containing the host name (or IP) followed by + port number. + + :param hosts: A string of the form host1[:port],host2[:port],... + :param default_port: The port number to use when one wasn't specified + for a host. + """ + nodes = [] + for entity in hosts.split(","): + if not entity: + raise ConfigurationError("Empty host (or extra comma in host list).") + port = default_port + # Unix socket entities don't have ports + if entity.endswith(".sock"): + port = None + nodes.append(parse_host(entity, port)) + return nodes + + +# Prohibited characters in database name. DB names also can't have ".", but for +# backward-compat we allow "db.collection" in URI. +_BAD_DB_CHARS = re.compile("[" + re.escape(r'/ "$') + "]") + +_ALLOWED_TXT_OPTS = frozenset( + ["authsource", "authSource", "replicaset", "replicaSet", "loadbalanced", "loadBalanced"] +) + + +def _check_options(nodes: Sized, options: Mapping[str, Any]) -> None: + # Ensure directConnection was not True if there are multiple seeds. + if len(nodes) > 1 and options.get("directconnection"): + raise ConfigurationError("Cannot specify multiple hosts with directConnection=true") + + if options.get("loadbalanced"): + if len(nodes) > 1: + raise ConfigurationError("Cannot specify multiple hosts with loadBalanced=true") + if options.get("directconnection"): + raise ConfigurationError("Cannot specify directConnection=true with loadBalanced=true") + if options.get("replicaset"): + raise ConfigurationError("Cannot specify replicaSet with loadBalanced=true") + + +def parse_uri( + uri: str, + default_port: Optional[int] = DEFAULT_PORT, + validate: bool = True, + warn: bool = False, + normalize: bool = True, + connect_timeout: Optional[float] = None, + srv_service_name: Optional[str] = None, + srv_max_hosts: Optional[int] = None, +) -> dict[str, Any]: + """Parse and validate a MongoDB URI. + + Returns a dict of the form:: + + { + 'nodelist': , + 'username': or None, + 'password': or None, + 'database': or None, + 'collection': or None, + 'options': , + 'fqdn': or None + } + + If the URI scheme is "mongodb+srv://" DNS SRV and TXT lookups will be done + to build nodelist and options. + + :param uri: The MongoDB URI to parse. + :param default_port: The port number to use when one wasn't specified + for a host in the URI. + :param validate: If ``True`` (the default), validate and + normalize all options. Default: ``True``. + :param warn: When validating, if ``True`` then will warn + the user then ignore any invalid options or values. If ``False``, + validation will error when options are unsupported or values are + invalid. Default: ``False``. + :param normalize: If ``True``, convert names of URI options + to their internally-used names. Default: ``True``. + :param connect_timeout: The maximum time in milliseconds to + wait for a response from the DNS server. + :param srv_service_name: A custom SRV service name + + .. versionchanged:: 4.6 + The delimiting slash (``/``) between hosts and connection options is now optional. + For example, "mongodb://example.com?tls=true" is now a valid URI. + + .. versionchanged:: 4.0 + To better follow RFC 3986, unquoted percent signs ("%") are no longer + supported. + + .. versionchanged:: 3.9 + Added the ``normalize`` parameter. + + .. versionchanged:: 3.6 + Added support for mongodb+srv:// URIs. + + .. versionchanged:: 3.5 + Return the original value of the ``readPreference`` MongoDB URI option + instead of the validated read preference mode. + + .. versionchanged:: 3.1 + ``warn`` added so invalid options can be ignored. + """ + if uri.startswith(SCHEME): + is_srv = False + scheme_free = uri[SCHEME_LEN:] + elif uri.startswith(SRV_SCHEME): + if not _HAVE_DNSPYTHON: + python_path = sys.executable or "python" + raise ConfigurationError( + 'The "dnspython" module must be ' + "installed to use mongodb+srv:// URIs. " + "To fix this error install pymongo again:\n " + "%s -m pip install pymongo>=4.3" % (python_path) + ) + is_srv = True + scheme_free = uri[SRV_SCHEME_LEN:] + else: + raise InvalidURI(f"Invalid URI scheme: URI must begin with '{SCHEME}' or '{SRV_SCHEME}'") + + if not scheme_free: + raise InvalidURI("Must provide at least one hostname or IP.") + + user = None + passwd = None + dbase = None + collection = None + options = _CaseInsensitiveDictionary() + + host_part, _, path_part = scheme_free.partition("/") + if not host_part: + host_part = path_part + path_part = "" + + if path_part: + dbase, _, opts = path_part.partition("?") + else: + # There was no slash in scheme_free, check for a sole "?". + host_part, _, opts = host_part.partition("?") + + if dbase: + dbase = unquote_plus(dbase) + if "." in dbase: + dbase, collection = dbase.split(".", 1) + if _BAD_DB_CHARS.search(dbase): + raise InvalidURI('Bad database name "%s"' % dbase) + else: + dbase = None + + if opts: + options.update(split_options(opts, validate, warn, normalize)) + if srv_service_name is None: + srv_service_name = options.get("srvServiceName", SRV_SERVICE_NAME) + if "@" in host_part: + userinfo, _, hosts = host_part.rpartition("@") + user, passwd = parse_userinfo(userinfo) + else: + hosts = host_part + + if "/" in hosts: + raise InvalidURI("Any '/' in a unix domain socket must be percent-encoded: %s" % host_part) + + hosts = unquote_plus(hosts) + fqdn = None + srv_max_hosts = srv_max_hosts or options.get("srvMaxHosts") + if is_srv: + if options.get("directConnection"): + raise ConfigurationError(f"Cannot specify directConnection=true with {SRV_SCHEME} URIs") + nodes = split_hosts(hosts, default_port=None) + if len(nodes) != 1: + raise InvalidURI(f"{SRV_SCHEME} URIs must include one, and only one, hostname") + fqdn, port = nodes[0] + if port is not None: + raise InvalidURI(f"{SRV_SCHEME} URIs must not include a port number") + + # Use the connection timeout. connectTimeoutMS passed as a keyword + # argument overrides the same option passed in the connection string. + connect_timeout = connect_timeout or options.get("connectTimeoutMS") + dns_resolver = _SrvResolver(fqdn, connect_timeout, srv_service_name, srv_max_hosts) + nodes = dns_resolver.get_hosts() + dns_options = dns_resolver.get_options() + if dns_options: + parsed_dns_options = split_options(dns_options, validate, warn, normalize) + if set(parsed_dns_options) - _ALLOWED_TXT_OPTS: + raise ConfigurationError( + "Only authSource, replicaSet, and loadBalanced are supported from DNS" + ) + for opt, val in parsed_dns_options.items(): + if opt not in options: + options[opt] = val + if options.get("loadBalanced") and srv_max_hosts: + raise InvalidURI("You cannot specify loadBalanced with srvMaxHosts") + if options.get("replicaSet") and srv_max_hosts: + raise InvalidURI("You cannot specify replicaSet with srvMaxHosts") + if "tls" not in options and "ssl" not in options: + options["tls"] = True if validate else "true" + elif not is_srv and options.get("srvServiceName") is not None: + raise ConfigurationError( + "The srvServiceName option is only allowed with 'mongodb+srv://' URIs" + ) + elif not is_srv and srv_max_hosts: + raise ConfigurationError( + "The srvMaxHosts option is only allowed with 'mongodb+srv://' URIs" + ) + else: + nodes = split_hosts(hosts, default_port=default_port) + + _check_options(nodes, options) + + return { + "nodelist": nodes, + "username": user, + "password": passwd, + "database": dbase, + "collection": collection, + "options": options, + "fqdn": fqdn, + } + + +def _parse_kms_tls_options(kms_tls_options: Optional[Mapping[str, Any]]) -> dict[str, SSLContext]: + """Parse KMS TLS connection options.""" + if not kms_tls_options: + return {} + if not isinstance(kms_tls_options, dict): + raise TypeError("kms_tls_options must be a dict") + contexts = {} + for provider, options in kms_tls_options.items(): + if not isinstance(options, dict): + raise TypeError(f'kms_tls_options["{provider}"] must be a dict') + options.setdefault("tls", True) + opts = _CaseInsensitiveDictionary(options) + opts = _handle_security_options(opts) + opts = _normalize_options(opts) + opts = cast(_CaseInsensitiveDictionary, validate_options(opts)) + ssl_context, allow_invalid_hostnames = _parse_ssl_options(opts) + if ssl_context is None: + raise ConfigurationError("TLS is required for KMS providers") + if allow_invalid_hostnames: + raise ConfigurationError("Insecure TLS options prohibited") + + for n in [ + "tlsInsecure", + "tlsAllowInvalidCertificates", + "tlsAllowInvalidHostnames", + "tlsDisableCertificateRevocationCheck", + ]: + if n in opts: + raise ConfigurationError(f"Insecure TLS options prohibited: {n}") + contexts[provider] = ssl_context + return contexts + + +if __name__ == "__main__": + import pprint + + try: + pprint.pprint(parse_uri(sys.argv[1])) # noqa: T203 + except InvalidURI as exc: + print(exc) # noqa: T201 + sys.exit(0) diff --git a/venv/Lib/site-packages/pymongo/write_concern.py b/venv/Lib/site-packages/pymongo/write_concern.py new file mode 100644 index 00000000..591a126f --- /dev/null +++ b/venv/Lib/site-packages/pymongo/write_concern.py @@ -0,0 +1,141 @@ +# Copyright 2014-present MongoDB, 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. + +"""Tools for working with write concerns.""" +from __future__ import annotations + +from typing import Any, Optional, Union + +from pymongo.errors import ConfigurationError + + +# Duplicated here to avoid a circular import. +def validate_boolean(option: str, value: Any) -> bool: + """Validates that 'value' is True or False.""" + if isinstance(value, bool): + return value + raise TypeError(f"{option} must be True or False, was: {option}={value}") + + +class WriteConcern: + """WriteConcern + + :param w: (integer or string) Used with replication, write operations + will block until they have been replicated to the specified number + or tagged set of servers. `w=` always includes the replica + set primary (e.g. w=3 means write to the primary and wait until + replicated to **two** secondaries). **w=0 disables acknowledgement + of write operations and can not be used with other write concern + options.** + :param wtimeout: (integer) **DEPRECATED** Used in conjunction with `w`. + Specify a value in milliseconds to control how long to wait for write + propagation to complete. If replication does not complete in the given + timeframe, a timeout exception is raised. + :param j: If ``True`` block until write operations have been committed + to the journal. Cannot be used in combination with `fsync`. Write + operations will fail with an exception if this option is used when + the server is running without journaling. + :param fsync: If ``True`` and the server is running without journaling, + blocks until the server has synced all data files to disk. If the + server is running with journaling, this acts the same as the `j` + option, blocking until write operations have been committed to the + journal. Cannot be used in combination with `j`. + + + .. versionchanged:: 4.7 + Deprecated parameter ``wtimeout``, use :meth:`~pymongo.timeout`. + """ + + __slots__ = ("__document", "__acknowledged", "__server_default") + + def __init__( + self, + w: Optional[Union[int, str]] = None, + wtimeout: Optional[int] = None, + j: Optional[bool] = None, + fsync: Optional[bool] = None, + ) -> None: + self.__document: dict[str, Any] = {} + self.__acknowledged = True + + if wtimeout is not None: + if not isinstance(wtimeout, int): + raise TypeError("wtimeout must be an integer") + if wtimeout < 0: + raise ValueError("wtimeout cannot be less than 0") + self.__document["wtimeout"] = wtimeout + + if j is not None: + validate_boolean("j", j) + self.__document["j"] = j + + if fsync is not None: + validate_boolean("fsync", fsync) + if j and fsync: + raise ConfigurationError("Can't set both j and fsync at the same time") + self.__document["fsync"] = fsync + + if w == 0 and j is True: + raise ConfigurationError("Cannot set w to 0 and j to True") + + if w is not None: + if isinstance(w, int): + if w < 0: + raise ValueError("w cannot be less than 0") + self.__acknowledged = w > 0 + elif not isinstance(w, str): + raise TypeError("w must be an integer or string") + self.__document["w"] = w + + self.__server_default = not self.__document + + @property + def is_server_default(self) -> bool: + """Does this WriteConcern match the server default.""" + return self.__server_default + + @property + def document(self) -> dict[str, Any]: + """The document representation of this write concern. + + .. note:: + :class:`WriteConcern` is immutable. Mutating the value of + :attr:`document` does not mutate this :class:`WriteConcern`. + """ + return self.__document.copy() + + @property + def acknowledged(self) -> bool: + """If ``True`` write operations will wait for acknowledgement before + returning. + """ + return self.__acknowledged + + def __repr__(self) -> str: + return "WriteConcern({})".format( + ", ".join("{}={}".format(*kvt) for kvt in self.__document.items()) + ) + + def __eq__(self, other: Any) -> bool: + if isinstance(other, WriteConcern): + return self.__document == other.document + return NotImplemented + + def __ne__(self, other: Any) -> bool: + if isinstance(other, WriteConcern): + return self.__document != other.document + return NotImplemented + + +DEFAULT_WRITE_CONCERN = WriteConcern()

3zCs6&&;W*ulVjLF!DxsQ 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 literal 0 HcmV?d00001 diff --git a/venv/Lib/site-packages/pymongo/__pycache__/database.cpython-312.pyc b/venv/Lib/site-packages/pymongo/__pycache__/database.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e1bb1f3b8ce7a58c09da8bdd983b6fd0bea9903f GIT binary patch literal 57452 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 literal 0 HcmV?d00001 diff --git a/venv/Lib/site-packages/pymongo/__pycache__/driver_info.cpython-312.pyc b/venv/Lib/site-packages/pymongo/__pycache__/driver_info.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..deedf50460ecdd1b9a61e09fe8b7357a2a343bd7 GIT binary patch literal 1820 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 literal 0 HcmV?d00001 diff --git a/venv/Lib/site-packages/pymongo/__pycache__/event_loggers.cpython-312.pyc b/venv/Lib/site-packages/pymongo/__pycache__/event_loggers.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1d1c3e52e078404561868861f4ef17d08acee060 GIT binary patch literal 13541 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 literal 0 HcmV?d00001 diff --git a/venv/Lib/site-packages/pymongo/__pycache__/helpers.cpython-312.pyc b/venv/Lib/site-packages/pymongo/__pycache__/helpers.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..beb73b3ad5ef126f7306dc8284b40774f04f5d4c GIT binary patch literal 11498 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 literal 0 HcmV?d00001 diff --git a/venv/Lib/site-packages/pymongo/__pycache__/logger.cpython-312.pyc b/venv/Lib/site-packages/pymongo/__pycache__/logger.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..19898027bf6842071d88a4c1c728efa46dbb7a0e GIT binary patch literal 8400 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 UrrAKh`QNT%bmun|ROB-K59q1ox&QzG literal 0 HcmV?d00001 diff --git a/venv/Lib/site-packages/pymongo/__pycache__/max_staleness_selectors.cpython-312.pyc b/venv/Lib/site-packages/pymongo/__pycache__/max_staleness_selectors.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0b5383fad43b85d641922bf737fffabb6a613cbc GIT binary patch literal 4030 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 literal 0 HcmV?d00001 diff --git a/venv/Lib/site-packages/pymongo/__pycache__/mongo_client.cpython-312.pyc b/venv/Lib/site-packages/pymongo/__pycache__/mongo_client.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..641ce1b97d881dcdb621c11c69d8eb76203cc0b2 GIT binary patch literal 114606 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 literal 0 HcmV?d00001 diff --git a/venv/Lib/site-packages/pymongo/__pycache__/monitoring.cpython-312.pyc b/venv/Lib/site-packages/pymongo/__pycache__/monitoring.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4820992483a1b81ca601575c39877ff439f77aff GIT binary patch literal 80130 zcmeIb3w&J1bsu~ezygcKYw;jKf*`p7!3RJPd`lEXQl$8>D3Kr~l6pXr!R79fgoKyx zE<|w++O(}Sl0Q3E+NM?$>kE@Q4H@M@=k=4DHny53Rhvh5ODcdjN=+wr(l|{E2}tGY z>z920|ID5H+Pe>cv@P|^B{6q)?mW+&Ip@roGc&(mR~OXqe75q~+21MmX@AZi>Zy_* z-|T3GrhQR6rNy)XE#`}r4f{sQ2FiT&mk*Z@l;d47TruJw@Q+juRElu_aMeg)ATUxr zP(2bH2#(YY)Qr>))QWGF!*wI|1NFjRHQX=~8VHRv4m672f#If+=7DD6uO4n0Sv0U{ zWbwdapB4}3O(-WgTt3n|&?>&y3@;g3If2~Z`exMy8 z?dLT0?=`;>zPwEPh!$&jL5qdVxGO|hBf^^ED_$!%Vy+aS%?NF=g@#4wB7`osg?5P0 zR)j9Gg?5V2r3h`ag|3R7jV*hjVxX&9v;M`-#o7_JI(9y`9Pc&fwb+V(;Tu>RTNz&$ z3-3aeVneYGeCZZ>b|TMJX8n6aSQo-p$9v5BuNR?f5W3bD+ABiWA+*~Txf2p0I`9FG5cu^pu(Fei8O4!XAt7H*-B8LLW!y6SmNU zB6I+uPufBciO{DI`m~wrM?~015jGhAh?(mHBJ?yuBeu}PBJ>PGqqfioMQ9A6aWmJ4 zE|zyc6Zz?OO*=U@Hk=42$HF6HqeICtJsi`Ao{Q_@_;c~mWTGz^?CT5TcVcL4G!l!& zV>@?-xAtuo!I5MVaqt0QCz268IW&4MJTw|Ue(~to=(#b*+uR#Yo{y`%`8_%uNhIQl z@YqChcxW^p3!jM(k6j3c@kI1El1GOpPz>{7N|je^D3R2M&P=el@Wf~gHNW zZ0G4uj9)|x&W-hniVqHtojZqGpAIJ`&fxpRa2!oJm57ITo}Czt?mVr>&mqUSews~J zKf>pR$Ie8C!^8YEisa$507zmy9vwP66g5F|#QZ!mkw~JZ=i~ZNGJIAa8wrn%#=~Q0 zMb#r`P~}K8X*OB3G#nX4<3`4Z<0AlGkZp{P>3SUTSTnXy7MnaDF_NO@hft6B^ARTN z4M)NWkpU8pj4(Ud*zm~Ex${ZD)5;!j93FZ$j_M30cJ2%ca3SfKo)l@&A2Li--2fm{ zGlGE+W*pIR;r+76BLckZtT=t@g8`dvqcrwwEPgheAb!MS>k{$dv%M0Mh{CVxVIxpj z-_YpUv2~qBcHxh2>N8q7I2sv=UkYCsN}flDeQY95(8t1^j!aZ$;@yeFM$EyX*d1zn`kvn0M1_h zf{jh6+H>3?X>3A|h<1+*MfI`7rGklNoH(ErOB4pu*~rlFhZI7x>faym*oKiJMeEdf zk^_y{c0%GTC}DIA$a_&x5-X`eo&*(*3@66Ik>>#I$Qe++u#J2uG9ZVu%rkd18{v5d z^Vm43W9f))R2*oP5KR~aBhljtV3pt~q`=9s@i7p9iv{rv8K`*0$73v>i0VV*0*RyN zBctaER(M>GKQ}Zskr*^;n7lY15AT*B?K3~x@>ES>LLjbHH0gyE&6A37U$XVI}c zY*xWmKPUMl9a?43wL#WO(97_JNWy@!gE4P&)R}1=4M+3lN0XD%*yeTUw zve}e8&@kvckRpE`1%N4&7bZi|;ju(PbWp&!5Yb1$lhzezx*i|FXpBkjuh273)d@vD z1cQp|jme?|yS59#q-oxV&_SNaOWbY`6Ull&Xb9#?4$q)!4K85kD-En zAj>LD6l~;ZusFkTqLo=WTPER&@mK^Rub4x4lQKyRR4XXWx*C;JW0AseOhT$M5M;kF z*K$O4j1!Vn$5%5WH?eiZa^zv~E=Pp0oqIXHc=0bsggei{PAW0O1?TW>GqBu>Un1f3 z?spQ)f>6!F#O%E9u|tT34A*dcnkQL_+&w*si=)x=`q(IReZv=fdiuiq#>a=z9O?;C z6nRJ@d}eqo`Ya^B3y~pF5LbbO(mxDEVr-ZTL+j#w=la4sDO01j6nG60sm~xYr@K*K zf!L=vtiU6_<&_IB;jIWZ0PJoB#w_zWgZ#52w+|m zkwp8E)cR^Q8d(_-4xb&;6Un~dU(rCI+n240jG|1@O^N>QvTS+eOf(zhl28onb~xVc z&(@uM;`qVA{SO@6|Ip$7k7UdDjb6-F9R(PGE7`y?p$d)+X9Fj=cmzlw*TAte&j4nJ zW606}Kzw+3EL$U9=+N;<5-}PFWrYUg&qw1T3q%bJ?vonqZ0+CyLos!1g3+s!L#R=F zI2mDDsW3fYX`&I>tY!ta;B0WvTvJ3t8jb$225>fH=zY~VNMzfLkn>7CXp7xs&PJ{H z_0f^YxD_WV4KJkYGhVYHc)qB`wE-XOP|9Myf%1!G-R0T(K{1+C9aFvg)Y9ge2$oc( zCWwQCcqa#nS#V!}cLe|@X0(HYXz8F_j)lK=aPVUjkzsZsepL+)0{;dFuW7Ta&ikL- zzw;?9*y@R=#wUQ^PbJ4Lj3&+xjSnV9BEy4cCK6aZP9)6W4Vc0w&x|Fl0LB~MpdxPI zHxT9Lo;osg=BdO`GQMFv5`8vuE{+1^((y)XF{N+(qP`5Z5Kb1J1Z`Sg(6SYG=^ZNE z0Yk$U#y7hTpPev_RmRF-O;Qo7ik0K-j|E~CcvoKZcUNZ{T(l$g*?hoNqtpVeT!DN* z(4<88w2{*ZxzsAU>~yactRzz71I8Mb+%a5Lx|ESR@v`Ev4B;|O`*2D8f zEy*zPeA?ITOMTL;wNx{Z2*{BG8m~%l)C`1(HQ=*zJfcTNBy86 z^Jw^`QX1~WxCNnuGm9cku9OmWYO%Y$GMseBYAooM-h+^Avj^s7D7!K80tzFXm_cuht;Tji}t*Q#`w*37Nkx3Zan|Pha7Z>rWH;7k*LA*kec*PoHeh{&$SW~PL z??9|MR)u$UtR)t}I~ZFOtH!$~wm25VyEfJutHHZ2wj@@Icm2h>?xk6Dgqjl_r{2ks zpCT}k(@X?%@fh1Qin6y&vUHL|@*=|$$DKsd0?tf@1rh`ln&DV=5ts;hn)XmaN(Gf< z{pdeoSOW=BM5?eAg<_QbkRYK5lT@_VBwpf^R3I6jL>Z2hV*c+=XjRIYvm?0`=@BZk z3glrgBTp9lIZJ~y;~>_(MIzfU`EZX%Wkr&2Y^=}H?>J~%Q2!?##iOnM^rX+2YgAtoD|D_fe9IGjhJMF?8$I7(E? z*VHNte#}aZrapV58nAK2)9M$-({7duKpQgJKv5igBN3fy0?x%Q+7gxwL+^5Pk!M@q zCV@Nzavmlb6tELDNp%zguZYqD;3f=#p=n{j{iQ;{J{6Y!vqWarGM_Bz*z{P|MgOBEYJV!&6Y^6zm?o z7@{$RrG|K+pvn{il_Gpr`3`<#gF+F&N!ka+i;bU#$HX+-mPtv7m$xoOAlei)foRgk zZYwhmB9k`eM%}zn_@xVFAr$Plp4Y>V{4BC7^+?OZH^rg}^70_;y>4;)g` zwW-(&u_bmf&r(7uO;tUkSb{)q1eu3zN|GC;K@?+gyxbTzHBjN|v{~1>u(b-35 zKb?bg9-#9eog;Mm=^UpMrqe-3s1SG1N9X^+Ut$j&su1g-LR?V=6=G90oW;UtIRS*V zSU;6-t_dGx*32-9Kl6cRkI%f2?tnN-51D3l9iVRL~jucq*{D{DIr zZ~?{r*`eJv)LmjB#TC(p8mA3{k*kE*+P76umU(EfzF@Z4LxYu5Tl;Fze94xpK@PxJ zYcJk>TWG9PD_vQrZc}S4lO#szv%Iz}?1;yv* z0}2nMWc5qpVy-4Qvnm1vu3Ds4xNE3$qUs_&+lPcw{80`)c*T`EB_LmrDRjCe;VVMtjJDCnhr%R~Sd2~>6f zc2nVDW06G|ZiK_;>aoi8nAqHBsOZC|Rj|!mVLujjv7Jsu0qGMI%#3Q|xp$QA6<%Xj z6}8uYz60GmkHVOpt(3bX$zG;U^tfJr6qnMYCJI=)5W|U&k!8?fLU70*B_9 zW$2=I(4@LlHBF#+#QK?qLZ~#1@W4klr6iK4s_)FB2qY?_QcaVK5%R-TM|({=XPO4t zP}m|tN+e&Ys+6ara#NW3+8SZS%Cp{lj>K(<&a-sFbUNq=VKGi0oqvp`%&PTfKS&=} znceHnjCe;#U%jN&3YSvX%n{-udlQK8Aj(iYdOwe9V#e_AoIbIThgM96KGhpncpV0XUff3rsp-o z7xSh3n)aFz`-IkAmf~Uu%x;rmb#6v@EqrUKM6w@_(Z^AwU4Ef&A1zp6Et{=N!l-o^ zJNKeIJOk#TR(JuHH0$_;TsK|S59Uc zx^L9k2PoGP?QA0S$hTJmUQc!>LF z-S(uZC8h;+M9k>Jusand!0*?qktq+*dH;DcW*mpcz#^S%lH$Z16|pxgF*Zl5pgvd; z71y;wP_Cu2p^i&^>Vpk$a0vFP31Qc-6RtYV0S_Md6LS=0A+ zed{ZCy|OOdxih`w-W&D1(t%ymEz954s{HjY2HtL1`pUX5_rKM!_3fr*(@o(ki?0M` zwDRsAIE_8C<^JaS8O>i;KjTOAnUGeyC|%JaF^p)c9*hWyVQs)C2Zm`DhABKqG$a{c zJTafZf-*Z6*sw@T5_iR;QtK-%%zB1kbv@72xdQNW(j2co$2d>Ym{mNUxrfwO>pidB zn^Zc_+*{OF>pidBDRzm+GxsIxtM#5&?rkcanS0mZ{t%|+pQf)14$c1$pE!B2|KKAh^hFG)#I6PG zq!7vly_N9BWoBmOZEGesE16-ad?`if(^@)_-1X zTWRGfbrZv|N_{TA&(^M!hrnd~IAITGdXO$=gGUa2WZ(WL2KOI503oC{DjA~Ko&n)w zEb6ckZyy}Okw{j~gLaVVuOhz?$bEwP+soRmK+9C1EfZ+F5oo{C`itwQdiG{|_NK%8 zGJ$>5wX3f-XKH(IH8fAxge3bwBJ&}j@c3rG4%cOXTsBZ4&+d%XT4#8A`EVv_#S1tg z6{nNd(>PfHcAyF&4K!0WLjnj1(O}sOsYXa6O_$A(AVQjG#B7GtAf%q=%w|Zfhy%lB zGo%hgt~pybXh<4ja+z9jf5fp*#(=t%Z4!OmRooX79f`?qs2&{DKbA;hvvd|-;3SWq zgn*d7c1ThEYFqJ*xQ0Gk;x0Xgtz;F z9PUdsp0w=`e)_%Q;_%~>#ryT&u=qM;bKcjW^!8c6}DuVvn39k#c8#g z{##7i0mm^1E*%p-%IaI#p0}DpFgksj`(jQjy%>?*Yw2_I79k<=GaC0xUXA-THqO;e zRxNNku9l#b4wR||O3CSPIuO2{magLtGOjBu@36&EYdyO`tGv#&M$kl^w-%^7wS1`agrUx zNtV$;U%2?v#dKiF?KJh13K8S_hQv_JZlhy$nR&yeJwa+asF78jGgLDvH{L8AduKR~mkrJ~~SY zw*Lvw3)(j-*3DD|8Y?d!o(XF`ebdWVPWNq@X$<+d-&(wM#*a7n%c3PSRrCe4CE=NB z`hr?(*Gvt4wc4V!Gj;UUYfF~RG|(5)R`!ZKW}38M{cJPCy0psrcgxEBD`(5$o1qW+ z$*>h>*op#Set(A%4?dJ8)3%#w+l{n-|57vFQX`(}tspZQcspKo>& zo*&I4I>)NK#APci7I;xRi;D(cs1w3-HFx-#0xgB-p!n{8q1Fry#DZeqU-gUH^S*%~ zWQ7`iGpZ2z7C4F!dTnP2m^-E9an)j4rmRSfX}&Q!z0V_%^JpWcgS^Q2luHy7RiAUo zpFROQzjG0)k%sLL5>K%7@A7 z*&*mP#nyZBW`j-XaZCiUgG#b1M`JD4DeG(f&IOnucetpeWY$@~5au1T3{J&`RCx=Y zpYTDh^g(Y-2}3-SzMsO80(QM|(wtdX`oKxwYeow7z7()@fG-6nwa+(VCK8w|$K1p} zS&7+8)i0L4W@x8<`0`ARD$#l?yuDOCS)Hs^;m>J_+9c_lcqXe+Hi!%#JurmX`k*<7 zx`?(oe|MMnr&{I43mn1`7P?tjiwX?CRxW6HviYZONKED)d zG*D@RiXAGS%aY`hW~bWs%eL6dHfJPM5+pv2Xy3c6z0;zFTHkJJzf$$e$J0Ig(#!9^ z(X>BZyZ_s*%f58rO6yM@z0un9V(r_3x|galfhE(m_`i6%b}2or9n)=Vr#rUKG-%aJ z=d?g|5VW@?Jk`89)4cjd^V$~!(>2XETYEA!JvaLvd~-+o)KfS6o}OOY{o;YoKX_|t zn6KNBBoX11;RQySy50i zXZs1Zi2^oydJc_R^wI^Ju;Q3Xn8QDf=wHMVk(aUyDlpc<^?0ef5YtpASkVz`4oXL; z$3g(&nnT?Y*HrpDLfJ~!qsR(+qpUymGv3U|~`C$auGB3R@V zag5)F%E6Qe`DG6%M5Q>QC#kcmq*x`!GCNAjO|DVx!cQKuf`f*2=n?XaUHSDV*`ueB zDKvJqL*ERBURpC<8~Q^3OZ`)|t1`8#@MEfWMW%Lz)3ZENyIh1Ao-Z7G>6rMvI#auP zwjodxobd;%g96#FmGx7C<$%!%RY{&GVCI!&rBEv5K?<3L1XYpazeglcK`ukMPfGZG zhLMKpe?UjnLI9CxBS15UO;osoBnq`D4x8}_a3{!e_DbqUDMpAgaRTa#`1L&+LRb3( zGmEvxWso_WD05QaT;}h%)wq1dk2eHP2$EIw1t@w}(-)-ZSwmkfMbA3=>M43Q&?f~> z`Y3SzZ8Lp44FR#u6cF2lfH()yQwoC=LRY^RsPRisi<(AJN%Ju!!cQY8pF~)#6_W{- zbXZ(Abf{Vbu^N|LSgUVCMIxN@7LyC5rKU?VR7>hoUNNb)aY03x=i7%tvf?~>sn0xq z$szQJ^S3Q|2NIJAu}HpM;xbAQ)X!O_P=DE@{)6NwbkOsW9Pw+0*+YHU-Wtb%Y^{fj|kbDY;GI6v806xOgVZ zpJD#4#}qk(lfk4}7a?+*3)GXr$ugJ7X_{F;U858?& zr1;^ewJBx3P6GTal2i6rqE*+IB0VSSze6zvfGAq`0PFCFi4dO*9X}b65jG(EP6ilbew@s5}hApVVq^ zIJu(T1h~WrZiae&s$q4eVRbqnRR<1HZJuqS6vJsIjP!X@p*ki|%sX?n?SJLbCjGAw zXbI2G{uW5}4b)!$8k2UwanzQBNA$p7!1I5GUJ+k4c?UMS8UpHn;lTbVB2onX#DYc8soHR+ zHVh%uDT_8%Qx>f;WKsR!qMYs){kwR*-;^sT2+WjHDyPX(O?sU^4~?r^N)_}@$h86% zPQ|MJCUdz(r*vsmK$n9>08;-JeSZeWE}>GM5<=Bg!ts~!OGv0SkWkyS#?6pWTP&^U zqBaPrObZJ;kiakY`YCJMOC^tDh^ z%6uUZLvU@SZ-*hc!hQrk3aM)h&1RY3rtN$$P^$!2wjzo@^ZDQjuG>+!NI!sFL~tcx zyz^44L2my+ZZ%GB{h?|+@2uRae;b&^WfG&0y=B$Yh~$!0ss6k@S(P%SG14^cqNe{P z3hDN{1~E)yxA%vuoTfzNR2h9mL)aXg+`@;v(?O$ z82?`q#QTx-b|gmV6)QeppDTuxC^YGRLx}qH_waIyb~?#B4|x(|ue}8>&b>v_$X=}8go zlXpl&Gr$h{OsEx0*BaK8EU~f6i-~RCLrcnRE79qR^T-6Viwa-eM|^*|P|cwwwjFut zNRIT@9E9{%Rp%kS>9cHtXopY%eqh8kQ680adMu8+eoe&IG@KY)El!RNT&(O4!tRkfCj@^8+>K$QfE6DqDbg25SO2?lo+KajfFD#*;oSRBb#8TrZulCabRQ-hqcr81e! zcC>t!WZ<$keVe=j26kttH1J$C9*+#^YE|2qfhQ4HKm~dQ>&z^nk|eXlb|b^?3tp)K zWw2fbo{__f{0K}nSx`||G3{XU>P7Q_Y_QVHXZc1c0&#t_{qW>7BJ^7A;r1a=S5pc> za^t}QaoMXCCo~wryzg80zOTEx+%_96_iBciZ#uh460G~!j@8H*-PE-Bi+w-dH`Uag zY3fcl_FV0_df-?3ul1+b?|%K^bi;k=ze2^MK$vju5|g^d0m=;IvjT6HNa z4k8Od55i2?L8p_>-zjGk>XzunFWR!Kz7<+=`S5Io596{@jLQnY8iVl7)%yH^E&-#YxDyr`#F>vVqBR(o#BF1D{C;kgY;y3%%WFEn;|C6Uw82y9K3?W)g|00U!d2 z$SM(*J?j#zVf8EoYg54sO9G0@q%Puts7y)%guIEDv}zh1){m*KXhT6oZXoQBBJF2=KGD+TfK>O4t;9%JWi zLOyv&(2;9L((CVieaq{yuZ@0nG`;)Sn;%OzJe&?ZEJpjaN*ggEJm0U64Mmer!X&U4 zmn2+#+OpwOHhD`CODda!r4TA+T0qZ4QXuHTezv0zzl5F%2b@68Xk2;u@ZVOHfo6Fa z#Jv~r`5!T87ITT>&wNlQAKxs+>s-Nso5b`-@G156QmBx}VbMQyQlt)+*28E<|22R9 zzF-7N+69APvqjsV1jGds=F+nP@a9h)mQfkY0a1gK$}!``1-O{&UhusG3%e&!k;nz~ zXDJ=!Jbn5H*GH6qjMD7$DvDj!?_!IN%9`Dl88181^@;JM&AK-MzNb&b!?>$dT)`CO zb}S{6ku6AqY9@t^-WhQZlH`6+0`Le_0V#NljgSRcU77x6bOCnRU{M-?S-XpSW~42)V-<>I3+a8ch1UTg(*K;rB+=p6 zdM>fchHbpk%Q+s{h{t$~nk^QDBDIeKhmYVo3q!aUh-6$^Ay~F|c3ybn+;9;l{dX_m zaA_yCOmJ>H46S;7f>7iP9GZa8o!WB|!xds2I%sI_pkez=B*2*Q9w!80Zkt_}`yF0Q z?nGc}O-_@|BJCsmMO*9Tj+6D7hV|*ddO@2V7!yz!^J#c~0HO@!RWIW>A>;&3XnHG} ziQ(lEYy=W`g2g7?*fBwwnY~O%+%NFJbAn|_H&c6#W%e9P?|vlx$jS7nC#Ft)EOY8( z>66KHaw6UETsrWafE`Ti4qXe_WFBIoF08@WLD`Bk5Kk?cNB@ubj2S$Id3`Orwe(ds zkux(bApMZJ>2%Jqo#XHb=_df`r$uYr1+`6(t88o5&r}EeyKgmO;{o27!C}FyioSpr z?h@Y+5zAyX3~LedyDD>20DREYa8rQa-)n9F=zXu==f7acPpm=|f98W~^C>7lokWDx zzbcI_+ip4d!ExmxlQ=z$2K4L8*hTc(WF$6Cr}IzIFM2y;bf_S2IIY3wTsadGQTc!$ zE^cCj>mR6sLaNGsC6PRG1_#aJ9^BN&uqiA)Ix!;WMe^#P1kJuDur-VZ_@{B4JtCgw zR4F2wmwP&Z?GPKaH7=K8ZS9j2S(t|7Y?wIg#Bt>$E->SLW?69|iOi-Sc0lcH`Eoid z=&Xd3^`AJo@5sSy#o+^R{Rba;)q0%6!26N+ay>t)>;1ADXT>D51~v-p~&t!@|(VJ$RK_m<*`R z6yow9ToWYMM-2F1yd&`UJbzqtR)Kz~qE+G8L{i^~MrLsofU4+0b_upQdv-(B$-_qv z9y@hX-^x_M{m1(I2SsPN;lCZ1o}HED9`q^xdqgTMiTY zS#ErF07YqO(`;gZ={nFNM-wUZizeQAy{sNYi4L9Y9LgKiX7~jhP$cWe6tjNTr;@^=`lNUYwjxFuFjOZP$@+wh-yG! z`|OLGffdFYP%y{kEz~-(1Zi@VnJara>uS$MW^`ML<8>*=>)``x#l!+1b(9B*KkgCs z737@?d(_uT3($X?l{Th3+|@JHuqxBADjisrTiG9q?ox{NIr>V^7K?%D-9u7i`B!Z0 zR=}mG#&T-UBbhyqq<5c6pL#Ui@K`$Vn1F{vX-NP}GddxpGhb;GCnWWfRFonMR@-}R z^?vp%oj+m4dk_yo`Zp@}+zNF`_2dRqJ=tYirgc$0`M4ooGjSB@%m*3qxr2nkPQ(uw z*6MP_^PYK<2h+NkGy+G)D60{NqQ|8F+t3Ubc`y|{mfblPIvhv7DCLfQp@X~H)u48? zz^Z~nZzFn*7MJ<8GYaz`LKvqW-nl;lFe zFPCUYamIGaXs&$d(kzJEecU95Fi7QZsw)3~vZ=)Bo=YR(Fvk< z!nl{c^M1oI!@lszv%_N%9HU^;Dz&O|oJ6K{k|ppyKgoKKxzYI>HnNBKO_H_$R>)XU zS!Iwczj^EceAEa$Ymh7^jv}4;;5VOw8i7-Y5cv_rEUFZc$E9&wl{M&{H~94*J9&^R zyl$Bni-}2Wt-`gPVn&Qp*1{9mqdtr|b6iZaa%aSt-Kh&}xu!g>)}5V)04{h{S(FM1 z#nuv(l7DR(QPpCBLkN)T*;ELg!$t^8%CP5atdLTTSX6T_u$l@)f#;WJBg6g=`^8}%!RrtbW)S8 zlNiC#N#zK@vtZex%4Mz*&>ur~k%x+ODaWZek^aLzs%*m(cnED9PX(2}aQyl4VI1zk z1+)w2xv1u(PCQ`40fbzJt`u*u7884^S-|&OvRYx2tZdprGC~%gBUSO@d49+*SlJb6 z)}>pfRMktWl1HjqvI0_7-R($KKfB=WVAoP4GkSAmB9RQA!Kp5z=E*LwdqC$x*{Y9G z3oAK_zzDISd*^A37)wY#Vkl&(wzaI`FdKXv<0WS{_yhvEb6&~0w%+KImD;4nIu^d_ zDWzR{=~CKj(rV5Fe+;AZgJ2{58fwl*S}nJH{L(sUXl%LN-IZ=rVj(dJVaXF-LX^vk zY;rnz50UL26pZO)!|JJq-b_O;Eq?8X4SYdb0h?zIeBtOzM`x@3A$bU`6yNNYFqZhn z9+UgEgl%3)^GT5=zi>u!Dro9T)||R<`=Srb^l0*;&VZ`)FE{3t@VytRRHm}b#6k8^ zPd)|Zv&Ru3^(QXj3|PMKn^J$!Y+11J{8Y7o$iuelbnXY#ZA&@~ascwq^Uo(xM5F^9 zlo#^zz8Cce4R=8T$0K@V#2}venW81r+Ai7U<^%Jd^8)>M3Q%&eLmFS8M|`t8;K`?G zfT?de&Sz3Rmt$3lbCmQlq=#17A~>8vIV>&L*x6+6B914!6hmE1#-@>7!pl@-jg8#r z=4$fH0TP21$i$|JxYGK%@9PiXD8rk9qtmy~IA7bRIG;<7Dc)u5|2xMy9bm}IK_eu+ zuM@1KZDDENEI4RaVUS8r`caPaCz(eFk~>x}J{%ej(SCtjZXgJ@SQM`$dpt+Hx(^U2 zs%`z+$X7?wyZhfflYaQr)WaXmJp9r0@klyyCfyKC2cnWm+I1WjYZR3f8VpmfA#@q~ z(**yYsV>}WbRmB0q>25knrZTBOl%7CKc7ElC@7eLgKiT^@DyZ#1Beh=j+l8VG0y)l z`Fi9ZA`!MmELIPGfQkbGP^a@ON7o5N}HZ1Dw+;inTPhKr2lwr59 ztwQE`^{TX`&abwTAqdbSG+Qt0tTusVYiw~6gd+H$q+-8`AE`z53{6)4S<7D|%eB>E zuf~kBF-shcQJDdm{s~&*JgTN6)6kI)bmaCymFk0ML?4KqWAM43Jp0UO5{a_h4|bqo z&C7E~2<*XLtwNvru>)<_`@Zn|^(GVqIe*``$FVtty{5CZIsGXl0XG&(#{UX?atCS! zENpCgdD|EF{P>=!=AKM*Pr7OS)y-E=PHotm*|0a=yYK5O(#KCu9Y38pemZ^3#qs;G z*eZ$_%Nn8%+?{PLU0!myO0R*n^@*i`i-okkx?wzS*7di0KvJBcGSJj*IV;GJV2uy9sNol`^xldKbj!nNzU>N*@J9(!XdNK_?=|E2| zl_h2YXG$=OJI#S!mV~8(Qo85IPVE)A8-|I*f>~1QW@Mu0iCxz zy(>A!_nusJtsvM$6`hdFK_WRXEeuYz{Qj>2M?uu7w%t6{up`s3!>zWRs$H3>U5U`C zTAlzaJUouet-1~9qD~aKWW|K<1y+%)-C|a1wOxECpSo7k@vc^j1)usB^ZYY736!9Y zbR|s}g#Alw*m?XCTHSIi*9Eo4y*}9TR|Nc`u!8~)9(ac)~r8~)AUSfOUx@NYJ4 z_%|Ci{1yK71{Z@5meRE0ukfqmui$&H)>H_y{3!m+hjqhKa6bGTBBTy_Ga`s!yfSDA z7eb~As_rBYDxi90DLS3s<=CMK3RjyA$e63mj4kBT~_q_x zDHQLSYB-o_IG8IGw`XeGOBRZofyGiN7QO;PvG`8Dp#NX&NVkAXLaEdFuO#;c1l-N0 zfV<&N3b-uV!7WfpJ_QBbVMIthREWCYIWd=I>2w-77%WkdLG!NoC@X%bNX2hoyk#*u zoi=Bk8RV$5!ODEI58{jG3PLs35ah+ULFWNbP>I#J)(~o?N(^fVby=mod>{^W@0oZc zDMjD4F3~q37Zc8_SfHE*LJ1siafC* zvKq#cg4oG~)8AyE#~OlnHYUehN0QKpRzUdU4yK!d_B(Ja(+P4E&$R^HKuPJx0JpGk zv|z4o5Hl4s_9Nh+TKX~xhnob_m@2i(6Ii&qRhhy;Uhid79lE5vyP!DsMYk4RH93b9 zFVG-f*%}W!TQZbLOyHyk-tK|z)a;;EMJQ_H=hIk&NI3loNOY;q132u7F} z*CqSAmw@1oBF~AAShBb;v*EsU@7}Km-`w_}cKwH4Z}z1heyUQ?l;VP|Gb6NXNhz#}3+vokbml5lB2%%F2uWkPi^}4zi5`6z0Qri=lcy zJTAL6+;FZA*j5*_%zuGpotvb%uUAp~zOt)}#l+wnzKBsH^gQ21edT=x7Rpn1k)RwO z=5yJ$0B$Uqn2gd{36qH(2WZSClS=bYq~%wu)J8kpHs0C}%kbI-PYkin?0q?k)vX?v zqg-e2xj7bLkSfwN1>VOv0KQ6~b--~bnUHmfOlkuTp5UFKk$E;RZ{()uSY~*KgBwK&9lzHSy|^L@VM|6N-}wEAVB@S{A|_WYBtV^ zbC;6h+$9XQ(eTa@Fyx)56#!4eDDmttZmLU2!2`#i7&3@HHZdAQ%AxTvPsgwc9d;2V z!Z_gVi~b^jMyS>JtF`$naml`0EbJdk6etPp71r z1WRG*Lzzna8|Gk~EG1Hz0?D;nud`nC-)GjpN#_sfSaezdNJu5$;s^JlJ-&4Msa=(B1^pfTBjU%VT?_CGv``8KbzExfKw|>OK-V~|Y^}4m% zb{C5<$C$&#hAJL>?=7xUyBJuv5454_2(m5fLC zUq$7;r3N>TvL!Y}hReWK{nwDig88cc8%Paa`EQwtCyOss55=B-l=Zk>{=RY_?26Bb zEo5YFJAnc_PajlxAUOK(c-2yH^stMa*r6pL{W(_gHrToTI?Mhl9dCx#S)c|6MgMWu z;a@R}4vuMf%;!VNzWPdMU${i3Tb~NQEt=N`yG< z{6q*W{mfL=A_NmqGb9*W6$_w*8evD+l{@XVhs0LLf>5Z|s<7A^xtE&mJMF1V(Vk|_!jc~)*EZUx5n6pSP1VXyc_Xu_Ke#XYr?md z*v42h-iz>V!F#bgZfsL*5yD$zn`4XdUJ~09YsGshYP;1LH?}Rd1hsENjHQUNEVez? zhIhL&7Si1nTZZuEvAbjKc&|YG<#?~miMJ!R0^#AD@VjCw5#E9P!+3XItnR)CXK4uz zd1NS&jE~0kMCzBYTjA``IbLNMr~P|)SbY-5Rz$E*e2Bqn)@R59JT2cHUyq+-T52rL z4Ue6{A-xyF^0eCM@I(xuINsSfUEj#X*a~45$<2EFLEhuaUG7!^M%PHV*W31Tk zjrX#Kh-opcO|#pECe5MdW&3Q#uTar!@q;SNdS5D=EHl$k1tA_=8tP45j~So)yu@RR zPtz~gW5zejpY(m|o0Gl++S6H0yHu8JRS71`lAITdN2zY~W#0DR&n#0htv60Y0s-ZN z8-MM&w#!gWbL48)UwzfIdF8r7rE}!!F?E|)u3^tyske7M^UAeTC02D!_&)b_K(pckifv0OoStQ#_6|9B>W;{jP)nnj-Fc)-n;X0c})vvl