From 0c3c5a02a75bc61b6bf6e303de20e11741d2afac Mon Sep 17 00:00:00 2001 From: morpheus65535 Date: Sun, 23 Jan 2022 23:07:52 -0500 Subject: [PATCH] Upgraded vendored Python dependencies to the latest versions and removed the unused dependencies. --- bazarr/get_subtitle/refiners/ffprobe.py | 5 +- bazarr/init.py | 3 - bazarr/logger.py | 3 + libs/_markerlib/__init__.py | 16 - libs/_markerlib/markers.py | 119 - libs/appdirs.py | 8 +- libs/arghelper.py | 116 - libs/asio/__init__.py | 61 - libs/asio/file.py | 92 - libs/asio/file_opener.py | 21 - libs/asio/interfaces/base.py | 41 - libs/asio/interfaces/posix.py | 123 - libs/asio/interfaces/windows/__init__.py | 201 - libs/asio/interfaces/windows/interop.py | 230 - libs/asio/open_parameters.py | 47 - libs/auditok/__init__.py | 10 +- libs/auditok/cmdline.py | 1155 +- libs/auditok/cmdline_util.py | 126 + libs/auditok/core.py | 1654 +- libs/auditok/dataset.py | 24 +- libs/auditok/exceptions.py | 42 +- libs/auditok/io.py | 1270 +- libs/auditok/plotting.py | 150 + libs/auditok/signal.py | 179 + libs/auditok/signal_numpy.py | 30 + libs/auditok/util.py | 1734 +- libs/auditok/workers.py | 427 + libs/backports/__init__.py | 12 +- libs/backports/functools_lru_cache.py | 58 +- libs/backports/zoneinfo/__init__.py | 49 + libs/backports/zoneinfo/__init__.pyi | 45 + libs/backports/zoneinfo/_common.py | 171 + libs/backports/zoneinfo/_tzpath.py | 207 + libs/backports/zoneinfo/_version.py | 1 + libs/backports/zoneinfo/_zoneinfo.py | 754 + libs/{twine => backports/zoneinfo}/py.typed | 0 libs/beaker/__init__.py | 1 - libs/beaker/_compat.py | 169 - libs/beaker/cache.py | 615 - libs/beaker/container.py | 760 - libs/beaker/converters.py | 29 - libs/beaker/cookie.py | 72 - libs/beaker/crypto/__init__.py | 83 - libs/beaker/crypto/jcecrypto.py | 41 - libs/beaker/crypto/noencryption.py | 12 - libs/beaker/crypto/nsscrypto.py | 47 - libs/beaker/crypto/pbkdf2.py | 94 - libs/beaker/crypto/pyca_cryptography.py | 52 - libs/beaker/crypto/pycrypto.py | 34 - libs/beaker/crypto/util.py | 16 - libs/beaker/exceptions.py | 29 - libs/beaker/ext/database.py | 180 - libs/beaker/ext/google.py | 122 - libs/beaker/ext/memcached.py | 218 - libs/beaker/ext/mongodb.py | 184 - libs/beaker/ext/redisnm.py | 144 - libs/beaker/ext/sqla.py | 137 - libs/beaker/middleware.py | 169 - libs/beaker/session.py | 845 - libs/beaker/synchronization.py | 392 - libs/beaker/util.py | 507 - libs/bidict/__init__.py | 74 +- libs/bidict/_abc.py | 40 +- libs/bidict/_base.py | 374 +- libs/bidict/_bidict.py | 15 +- libs/bidict/_delegating.py | 39 + libs/bidict/_delegating_mixins.py | 92 - libs/bidict/_dup.py | 50 +- libs/bidict/_exc.py | 6 +- libs/bidict/_frozenbidict.py | 33 +- libs/bidict/_frozenordered.py | 75 +- libs/bidict/{_util.py => _iter.py} | 38 +- libs/bidict/_marker.py | 19 - libs/bidict/_miss.py | 14 - libs/bidict/_mut.py | 107 +- libs/bidict/_named.py | 90 +- libs/bidict/_noop.py | 14 - libs/bidict/_orderedbase.py | 162 +- libs/bidict/_orderedbidict.py | 38 +- libs/bidict/_typing.py | 33 + libs/bidict/compat.py | 78 - libs/bidict/metadata.py | 44 +- .../__init__.py => bidict/py.typed} | 0 libs/bs4/__init__.py | 384 +- libs/bs4/builder/__init__.py | 211 +- libs/bs4/builder/_html5lib.py | 75 +- libs/bs4/builder/_htmlparser.py | 192 +- libs/bs4/builder/_lxml.py | 74 +- libs/bs4/dammit.py | 2606 +- libs/bs4/diagnose.py | 68 +- libs/bs4/element.py | 978 +- libs/bs4/formatter.py | 84 +- libs/bs4/testing.py | 162 +- libs/bs4/tests/test_html5lib.py | 56 + libs/bs4/tests/test_htmlparser.py | 89 +- libs/bs4/tests/test_lxml.py | 17 +- libs/bs4/tests/test_soup.py | 408 +- libs/bs4/tests/test_tree.py | 231 +- libs/certifi/__init__.py | 4 +- libs/certifi/__main__.py | 14 +- libs/certifi/cacert.pem | 1546 +- libs/certifi/core.py | 53 +- libs/chardet/__init__.py | 48 +- libs/chardet/charsetgroupprober.py | 1 + libs/chardet/cli/chardetect.py | 7 +- libs/chardet/compat.py | 6 +- libs/chardet/langbulgarianmodel.py | 4862 ++- libs/chardet/langcyrillicmodel.py | 333 - libs/chardet/langgreekmodel.py | 4607 ++- libs/chardet/langhebrewmodel.py | 4571 ++- libs/chardet/langhungarianmodel.py | 4859 ++- libs/chardet/langrussianmodel.py | 5718 ++++ libs/chardet/langthaimodel.py | 4570 ++- libs/chardet/langturkishmodel.py | 4562 ++- .../ext => chardet/metadata}/__init__.py | 0 libs/chardet/metadata/languages.py | 310 + libs/chardet/sbcharsetprober.py | 45 +- libs/chardet/sbcsgroupprober.py | 76 +- libs/chardet/universaldetector.py | 8 +- libs/chardet/version.py | 2 +- libs/click/__init__.py | 158 +- libs/click/_bashcomplete.py | 293 - libs/click/_compat.py | 946 +- libs/click/_termui_impl.py | 531 +- libs/click/_textwrap.py | 23 +- libs/click/_unicodefun.py | 173 +- libs/click/_winconsole.py | 294 +- libs/click/core.py | 2401 +- libs/click/decorators.py | 455 +- libs/click/exceptions.py | 210 +- libs/click/formatting.py | 197 +- libs/click/globals.py | 41 +- libs/click/parser.py | 348 +- .../fixtures/ls_tree_empty => click/py.typed} | 0 libs/click/shell_completion.py | 581 + libs/click/termui.py | 581 +- libs/click/testing.py | 437 +- libs/click/types.py | 944 +- libs/click/utils.py | 511 +- libs/contextlib2.py | 436 - libs/dateutil/__init__.py | 8 +- libs/dateutil/_common.py | 12 +- libs/dateutil/_version.py | 3 +- libs/dateutil/easter.py | 20 +- libs/dateutil/parser.py | 1360 - libs/dateutil/parser/__init__.py | 3 +- libs/dateutil/parser/_parser.py | 163 +- libs/dateutil/parser/isoparser.py | 56 +- libs/dateutil/relativedelta.py | 186 +- libs/dateutil/rrule.py | 270 +- libs/dateutil/test/_common.py | 83 +- libs/dateutil/test/conftest.py | 41 + .../test/property/test_isoparse_prop.py | 27 + .../test/property/test_parser_prop.py | 22 + libs/dateutil/test/property/test_tz_prop.py | 35 + libs/dateutil/test/test_easter.py | 38 +- libs/dateutil/test/test_import_star.py | 33 + libs/dateutil/test/test_imports.py | 287 +- libs/dateutil/test/test_internals.py | 91 + libs/dateutil/test/test_isoparser.py | 509 + libs/dateutil/test/test_parser.py | 1329 +- libs/dateutil/test/test_relativedelta.py | 163 +- libs/dateutil/test/test_rrule.py | 253 +- libs/dateutil/test/test_tz.py | 1233 +- libs/dateutil/test/test_utils.py | 52 + libs/dateutil/tz/__init__.py | 10 +- libs/dateutil/tz/_common.py | 123 +- libs/dateutil/tz/_factories.py | 35 +- libs/dateutil/tz/tz.py | 761 +- libs/dateutil/tz/win.py | 58 +- libs/dateutil/tzwin.py | 2 +- libs/dateutil/utils.py | 4 +- libs/dateutil/zoneinfo/__init__.py | 50 +- .../zoneinfo/dateutil-zoneinfo.tar.gz | Bin 139671 -> 174394 bytes libs/dateutil/zoneinfo/rebuild.py | 44 +- libs/decorator.py | 240 +- libs/deep_translator/__init__.py | 9 +- libs/deep_translator/constants.py | 54 +- libs/deep_translator/exceptions.py | 3 + libs/deep_translator/google_trans.py | 47 +- libs/deep_translator/libre.py | 137 + libs/deep_translator/main.py | 10 +- libs/deep_translator/microsoft.py | 2 +- libs/deep_translator/mymemory.py | 2 +- libs/deep_translator/papago.py | 2 +- libs/deep_translator/parent.py | 2 + libs/deep_translator/qcri.py | 1 + libs/deep_translator/yandex.py | 2 +- libs/dns/__init__.py | 16 +- libs/dns/_asyncbackend.py | 69 + libs/dns/_asyncio_backend.py | 149 + libs/dns/_compat.py | 47 - libs/dns/_curio_backend.py | 108 + libs/dns/_immutable_attr.py | 84 + libs/dns/_immutable_ctx.py | 75 + libs/dns/_trio_backend.py | 121 + libs/dns/asyncbackend.py | 101 + libs/dns/asyncbackend.pyi | 13 + libs/dns/asyncquery.py | 523 + libs/dns/asyncquery.pyi | 43 + libs/dns/asyncresolver.py | 232 + libs/dns/asyncresolver.pyi | 26 + libs/dns/dnssec.py | 715 +- libs/dns/dnssec.pyi | 21 + libs/dns/e164.py | 69 +- libs/dns/e164.pyi | 10 + libs/dns/edns.py | 422 +- libs/dns/entropy.py | 82 +- libs/dns/entropy.pyi | 10 + libs/dns/enum.py | 90 + libs/dns/exception.py | 66 +- libs/dns/exception.pyi | 10 + libs/dns/flags.py | 121 +- libs/dns/grange.py | 40 +- libs/dns/immutable.py | 70 + libs/dns/inet.py | 131 +- libs/dns/inet.pyi | 4 + libs/dns/ipv4.py | 39 +- libs/dns/ipv6.py | 99 +- libs/dns/message.py | 1750 +- libs/dns/message.pyi | 47 + libs/dns/name.py | 702 +- libs/dns/name.pyi | 40 + libs/dns/namedict.py | 40 +- libs/dns/node.py | 262 +- libs/dns/node.pyi | 17 + libs/dns/opcode.py | 102 +- .../performance/__init__.py => dns/py.typed} | 0 libs/dns/query.py | 1405 +- libs/dns/query.pyi | 64 + libs/dns/rcode.py | 175 +- libs/dns/rdata.py | 758 +- libs/dns/rdata.pyi | 19 + libs/dns/rdataclass.py | 143 +- libs/dns/rdataset.py | 304 +- libs/dns/rdataset.pyi | 58 + libs/dns/rdatatype.py | 472 +- libs/dns/rdtypes/ANY/AFSDB.py | 29 +- libs/dns/rdtypes/ANY/AMTRELAY.py | 86 + libs/dns/rdtypes/ANY/AVC.py | 8 +- libs/dns/rdtypes/ANY/CAA.py | 42 +- libs/dns/rdtypes/ANY/CDNSKEY.py | 11 +- libs/dns/rdtypes/ANY/CDS.py | 9 + libs/dns/rdtypes/ANY/CERT.py | 66 +- libs/dns/rdtypes/ANY/CNAME.py | 4 + libs/dns/rdtypes/ANY/CSYNC.py | 114 +- libs/dns/rdtypes/ANY/DLV.py | 4 + libs/dns/rdtypes/ANY/DNAME.py | 8 +- libs/dns/rdtypes/ANY/DNSKEY.py | 11 +- libs/dns/rdtypes/ANY/DS.py | 4 + libs/dns/rdtypes/ANY/EUI48.py | 10 +- libs/dns/rdtypes/ANY/EUI64.py | 10 +- libs/dns/rdtypes/ANY/GPOS.py | 120 +- libs/dns/rdtypes/ANY/HINFO.py | 57 +- libs/dns/rdtypes/ANY/HIP.py | 80 +- libs/dns/rdtypes/ANY/ISDN.py | 65 +- libs/dns/rdtypes/ANY/L32.py | 40 + libs/dns/rdtypes/ANY/L64.py | 48 + libs/dns/rdtypes/ANY/LOC.py | 155 +- libs/dns/rdtypes/ANY/LP.py | 41 + libs/dns/rdtypes/ANY/MX.py | 4 + libs/dns/rdtypes/ANY/NID.py | 47 + libs/dns/{hash.py => rdtypes/ANY/NINFO.py} | 22 +- libs/dns/rdtypes/ANY/NS.py | 4 + libs/dns/rdtypes/ANY/NSEC.py | 121 +- libs/dns/rdtypes/ANY/NSEC3.py | 164 +- libs/dns/rdtypes/ANY/NSEC3PARAM.py | 49 +- libs/dns/rdtypes/ANY/OPENPGPKEY.py | 52 + libs/dns/rdtypes/ANY/OPT.py | 76 + libs/dns/rdtypes/ANY/PTR.py | 4 + libs/dns/rdtypes/ANY/RP.py | 62 +- libs/dns/rdtypes/ANY/RRSIG.py | 90 +- libs/dns/rdtypes/ANY/RT.py | 4 + libs/dns/rdtypes/ANY/SMIMEA.py | 9 + libs/dns/rdtypes/ANY/SOA.py | 86 +- libs/dns/rdtypes/ANY/SPF.py | 8 +- libs/dns/rdtypes/ANY/SSHFP.py | 50 +- libs/dns/rdtypes/ANY/TKEY.py | 118 + libs/dns/rdtypes/ANY/TLSA.py | 84 +- libs/dns/rdtypes/ANY/TSIG.py | 120 + libs/dns/rdtypes/ANY/TXT.py | 4 + libs/dns/rdtypes/ANY/URI.py | 60 +- libs/dns/rdtypes/ANY/X25.py | 33 +- libs/dns/rdtypes/ANY/ZONEMD.py | 65 + libs/dns/rdtypes/ANY/__init__.py | 16 +- libs/dns/rdtypes/CH/A.py | 58 + libs/dns/rdtypes/CH/__init__.py | 22 + libs/dns/rdtypes/IN/A.py | 25 +- libs/dns/rdtypes/IN/AAAA.py | 30 +- libs/dns/rdtypes/IN/APL.py | 104 +- libs/dns/rdtypes/IN/DHCID.py | 36 +- libs/dns/rdtypes/IN/HTTPS.py | 8 + libs/dns/rdtypes/IN/IPSECKEY.py | 133 +- libs/dns/rdtypes/IN/KX.py | 6 +- libs/dns/rdtypes/IN/NAPTR.py | 86 +- libs/dns/rdtypes/IN/NSAP.py | 24 +- libs/dns/rdtypes/IN/NSAP_PTR.py | 4 + libs/dns/rdtypes/IN/PX.py | 70 +- libs/dns/rdtypes/IN/SRV.py | 63 +- libs/dns/rdtypes/IN/SVCB.py | 8 + libs/dns/rdtypes/IN/WKS.py | 70 +- libs/dns/rdtypes/IN/__init__.py | 5 + libs/dns/rdtypes/__init__.py | 9 + libs/dns/rdtypes/dnskeybase.py | 118 +- libs/dns/rdtypes/dnskeybase.pyi | 38 + libs/dns/rdtypes/dsbase.py | 71 +- libs/dns/rdtypes/euibase.py | 30 +- libs/dns/rdtypes/mxbase.py | 70 +- libs/dns/rdtypes/nsbase.py | 51 +- libs/dns/rdtypes/svcbbase.py | 555 + libs/dns/rdtypes/tlsabase.py | 72 + libs/dns/rdtypes/txtbase.py | 65 +- libs/dns/rdtypes/txtbase.pyi | 6 + libs/dns/rdtypes/util.py | 244 + libs/dns/renderer.py | 271 +- libs/dns/resolver.py | 1762 +- libs/dns/resolver.pyi | 61 + libs/dns/reversename.py | 97 +- libs/dns/reversename.pyi | 6 + libs/dns/rrset.py | 137 +- libs/dns/rrset.pyi | 10 + libs/dns/serial.py | 117 + libs/dns/set.py | 147 +- libs/dns/tokenizer.py | 433 +- libs/dns/transaction.py | 587 + libs/dns/tsig.py | 350 +- libs/dns/tsigkeyring.py | 44 +- libs/dns/tsigkeyring.pyi | 7 + libs/dns/ttl.py | 60 +- libs/dns/update.py | 218 +- libs/dns/update.pyi | 21 + libs/dns/version.py | 26 +- libs/dns/versioned.py | 274 + libs/dns/win32util.py | 235 + libs/dns/wire.py | 85 + libs/dns/wiredata.py | 103 - libs/dns/xfr.py | 313 + libs/dns/zone.py | 1482 +- libs/dns/zone.pyi | 55 + libs/dns/zonefile.py | 624 + libs/dogpile/__init__.py | 2 +- libs/dogpile/cache/__init__.py | 6 +- libs/dogpile/cache/api.py | 446 +- libs/dogpile/cache/backends/__init__.py | 51 +- libs/dogpile/cache/backends/file.py | 96 +- libs/dogpile/cache/backends/memcached.py | 359 +- libs/dogpile/cache/backends/memory.py | 31 +- libs/dogpile/cache/backends/null.py | 8 +- libs/dogpile/cache/backends/redis.py | 274 +- libs/dogpile/cache/plugins/mako_cache.py | 14 +- libs/dogpile/cache/proxy.py | 53 +- libs/dogpile/cache/region.py | 822 +- libs/dogpile/cache/util.py | 88 +- libs/dogpile/core.py | 10 +- libs/dogpile/lock.py | 21 +- libs/dogpile/util/__init__.py | 7 +- libs/dogpile/util/compat.py | 137 +- libs/dogpile/util/langhelpers.py | 87 +- libs/dogpile/util/nameregistry.py | 26 +- libs/dogpile/util/readwrite_lock.py | 26 +- libs/dumprar.py | 556 - libs/engineio/__init__.py | 4 +- libs/engineio/async_drivers/sanic.py | 6 +- libs/ffsubsync/__init__.py | 4 +- libs/ffsubsync/_version.py | 533 +- libs/ffsubsync/aligners.py | 123 +- libs/ffsubsync/constants.py | 57 +- libs/ffsubsync/ffmpeg_utils.py | 30 +- libs/ffsubsync/ffsubsync.py | 780 +- libs/ffsubsync/ffsubsync_gui.py | 96 +- libs/ffsubsync/file_utils.py | 11 +- libs/ffsubsync/generic_subtitles.py | 151 +- libs/ffsubsync/golden_section_search.py | 18 +- libs/ffsubsync/sklearn_shim.py | 108 +- libs/ffsubsync/speech_transformers.py | 401 +- libs/ffsubsync/suboffset.py | 27 - libs/ffsubsync/subtitle_parser.py | 130 +- libs/ffsubsync/subtitle_transformers.py | 34 +- libs/ffsubsync/version.py | 20 +- libs/flask/__init__.py | 102 +- libs/flask/__main__.py | 16 +- libs/flask/_compat.py | 145 - libs/flask/app.py | 1247 +- libs/flask/blueprints.py | 534 +- libs/flask/cli.py | 394 +- libs/flask/config.py | 158 +- libs/flask/ctx.py | 161 +- libs/flask/debughelpers.py | 103 +- libs/flask/globals.py | 27 +- libs/flask/helpers.py | 987 +- libs/flask/json/__init__.py | 469 +- libs/flask/json/tag.py | 127 +- libs/flask/logging.py | 55 +- .../utils/__init__.py => flask/py.typed} | 0 libs/flask/scaffold.py | 875 + libs/flask/sessions.py | 128 +- libs/flask/signals.py | 25 +- libs/flask/templating.py | 52 +- libs/flask/testing.py | 155 +- libs/flask/typing.py | 56 + libs/flask/views.py | 51 +- libs/flask/wrappers.py | 114 +- libs/flask_cors/__init__.py | 27 - libs/flask_cors/core.py | 383 - libs/flask_cors/decorator.py | 135 - libs/flask_cors/extension.py | 186 - libs/flask_cors/version.py | 1 - libs/flask_socketio/__init__.py | 50 +- libs/flask_socketio/test_client.py | 11 +- libs/ftfy/__init__.py | 852 +- libs/ftfy/bad_codecs/__init__.py | 9 +- libs/ftfy/bad_codecs/sloppy.py | 23 +- libs/ftfy/bad_codecs/utf8_variants.py | 54 +- libs/ftfy/badness.py | 504 +- libs/ftfy/build_data.py | 132 - libs/ftfy/char_classes.dat | Bin 3989 -> 0 bytes libs/ftfy/chardata.py | 355 +- libs/ftfy/cli.py | 120 +- libs/ftfy/compatibility.py | 55 - libs/ftfy/fixes.py | 562 +- libs/ftfy/formatting.py | 27 +- libs/ftfy/streamtester/__init__.py | 47 - libs/ftfy/streamtester/oauth.py | 72 - libs/ftfy/streamtester/twitter_tester.py | 88 - libs/funcsigs/__init__.py | 829 - libs/funcsigs/version.py | 1 - libs/git/__init__.py | 86 - libs/git/cmd.py | 1109 - libs/git/compat.py | 313 - libs/git/config.py | 593 - libs/git/db.py | 60 - libs/git/diff.py | 517 - libs/git/exc.py | 132 - libs/git/index/__init__.py | 6 - libs/git/index/base.py | 1239 - libs/git/index/fun.py | 384 - libs/git/index/typ.py | 176 - libs/git/index/util.py | 99 - libs/git/objects/__init__.py | 26 - libs/git/objects/base.py | 182 - libs/git/objects/blob.py | 33 - libs/git/objects/commit.py | 533 - libs/git/objects/fun.py | 207 - libs/git/objects/submodule/__init__.py | 2 - libs/git/objects/submodule/base.py | 1203 - libs/git/objects/submodule/root.py | 350 - libs/git/objects/submodule/util.py | 94 - libs/git/objects/tag.py | 74 - libs/git/objects/tree.py | 341 - libs/git/objects/util.py | 363 - libs/git/refs/__init__.py | 10 - libs/git/refs/head.py | 247 - libs/git/refs/log.py | 317 - libs/git/refs/reference.py | 126 - libs/git/refs/remote.py | 52 - libs/git/refs/symbolic.py | 679 - libs/git/refs/tag.py | 93 - libs/git/remote.py | 875 - libs/git/repo/__init__.py | 4 - libs/git/repo/base.py | 1031 - libs/git/repo/fun.py | 344 - libs/git/test/__init__.py | 5 - libs/git/test/fixtures/blame | 131 - libs/git/test/fixtures/blame_binary | Bin 14807 -> 0 bytes libs/git/test/fixtures/blame_complex_revision | 177 - libs/git/test/fixtures/blame_incremental | 30 - .../fixtures/blame_incremental_2.11.1_plus | 33 - libs/git/test/fixtures/cat_file.py | 6 - libs/git/test/fixtures/cat_file_blob | 1 - libs/git/test/fixtures/cat_file_blob_nl | 1 - libs/git/test/fixtures/cat_file_blob_size | 1 - libs/git/test/fixtures/commit_invalid_data | 6 - libs/git/test/fixtures/commit_with_gpgsig | 30 - libs/git/test/fixtures/diff_2 | 54 - libs/git/test/fixtures/diff_2f | 19 - .../diff_abbrev-40_full-index_M_raw_no-color | 1 - libs/git/test/fixtures/diff_change_in_type | 10 - .../git/test/fixtures/diff_change_in_type_raw | 1 - libs/git/test/fixtures/diff_f | 15 - libs/git/test/fixtures/diff_file_with_spaces | 7 - libs/git/test/fixtures/diff_i | 201 - libs/git/test/fixtures/diff_index_patch | 100 - libs/git/test/fixtures/diff_index_raw | 1 - libs/git/test/fixtures/diff_initial | 8 - libs/git/test/fixtures/diff_mode_only | 1152 - libs/git/test/fixtures/diff_new_mode | 14 - libs/git/test/fixtures/diff_numstat | 2 - libs/git/test/fixtures/diff_p | 610 - libs/git/test/fixtures/diff_patch_binary | 3 - .../git/test/fixtures/diff_patch_unsafe_paths | 89 - libs/git/test/fixtures/diff_raw_binary | 1 - libs/git/test/fixtures/diff_rename | 12 - libs/git/test/fixtures/diff_rename_raw | 1 - libs/git/test/fixtures/diff_tree_numstat_root | 3 - .../fixtures/for_each_ref_with_path_component | Bin 84 -> 0 bytes libs/git/test/fixtures/git_config | 46 - libs/git/test/fixtures/git_config-inc.cfg | 5 - libs/git/test/fixtures/git_config_global | 25 - .../test/fixtures/git_config_with_comments | 183 - .../test/fixtures/git_config_with_empty_value | 4 - libs/git/test/fixtures/git_file | 1 - libs/git/test/fixtures/index | Bin 163616 -> 0 bytes libs/git/test/fixtures/index_merge | Bin 9192 -> 0 bytes libs/git/test/fixtures/issue-301_stderr | 5002 --- libs/git/test/fixtures/ls_tree_a | 7 - libs/git/test/fixtures/ls_tree_b | 2 - libs/git/test/fixtures/ls_tree_commit | 3 - libs/git/test/fixtures/reflog_HEAD | 460 - libs/git/test/fixtures/reflog_invalid_date | 2 - libs/git/test/fixtures/reflog_invalid_email | 2 - libs/git/test/fixtures/reflog_invalid_newsha | 2 - libs/git/test/fixtures/reflog_invalid_oldsha | 2 - libs/git/test/fixtures/reflog_invalid_sep | 2 - libs/git/test/fixtures/reflog_master | 124 - libs/git/test/fixtures/rev_list | 3 - libs/git/test/fixtures/rev_list_bisect_all | 51 - libs/git/test/fixtures/rev_list_commit_diffs | 8 - .../test/fixtures/rev_list_commit_idabbrev | 8 - libs/git/test/fixtures/rev_list_commit_stats | 7 - libs/git/test/fixtures/rev_list_count | 655 - libs/git/test/fixtures/rev_list_delta_a | 8 - libs/git/test/fixtures/rev_list_delta_b | 11 - libs/git/test/fixtures/rev_list_single | 7 - libs/git/test/fixtures/rev_parse | 1 - libs/git/test/fixtures/show_empty_commit | 6 - .../uncommon_branch_prefix_FETCH_HEAD | 6 - .../fixtures/uncommon_branch_prefix_stderr | 6 - libs/git/test/lib/__init__.py | 13 - libs/git/test/lib/asserts.py | 71 - libs/git/test/lib/helper.py | 381 - libs/git/test/performance/lib.py | 94 - libs/git/test/performance/test_commit.py | 109 - libs/git/test/performance/test_odb.py | 74 - libs/git/test/performance/test_streams.py | 149 - libs/git/test/test_actor.py | 37 - libs/git/test/test_base.py | 150 - libs/git/test/test_blob.py | 25 - libs/git/test/test_commit.py | 402 - libs/git/test/test_config.py | 267 - libs/git/test/test_db.py | 27 - libs/git/test/test_diff.py | 297 - libs/git/test/test_docs.py | 493 - libs/git/test/test_exc.py | 169 - libs/git/test/test_fun.py | 293 - libs/git/test/test_git.py | 287 - libs/git/test/test_index.py | 929 - libs/git/test/test_reflog.py | 107 - libs/git/test/test_refs.py | 568 - libs/git/test/test_remote.py | 642 - libs/git/test/test_repo.py | 1004 - libs/git/test/test_stats.py | 31 - libs/git/test/test_submodule.py | 920 - libs/git/test/test_tree.py | 108 - libs/git/test/test_util.py | 276 - libs/git/util.py | 953 - libs/gitdb/__init__.py | 39 - libs/gitdb/base.py | 315 - libs/gitdb/const.py | 4 - libs/gitdb/db/__init__.py | 11 - libs/gitdb/db/base.py | 273 - libs/gitdb/db/git.py | 85 - libs/gitdb/db/loose.py | 262 - libs/gitdb/db/mem.py | 112 - libs/gitdb/db/pack.py | 207 - libs/gitdb/db/ref.py | 82 - libs/gitdb/exc.py | 46 - libs/gitdb/fun.py | 781 - libs/gitdb/pack.py | 1033 - libs/gitdb/stream.py | 732 - libs/gitdb/test/__init__.py | 4 - libs/gitdb/test/lib.py | 208 - libs/gitdb/test/test_base.py | 105 - libs/gitdb/test/test_example.py | 43 - libs/gitdb/test/test_pack.py | 255 - libs/gitdb/test/test_stream.py | 164 - libs/gitdb/test/test_util.py | 100 - libs/gitdb/typ.py | 10 - libs/gitdb/util.py | 401 - libs/gitdb/utils/compat.py | 43 - libs/gitdb/utils/encoding.py | 31 - libs/html5lib/tests/__init__.py | 1 - libs/html5lib/tests/conftest.py | 108 - .../tests/sanitizer-testdata/tests1.dat | 433 - libs/html5lib/tests/sanitizer.py | 51 - .../tests/serializer-testdata/core.test | 395 - .../tests/serializer-testdata/injectmeta.test | 350 - .../serializer-testdata/optionaltags.test | 3254 -- .../tests/serializer-testdata/options.test | 334 - .../tests/serializer-testdata/whitespace.test | 198 - libs/html5lib/tests/support.py | 199 - .../tests/test_alphabeticalattributes.py | 78 - libs/html5lib/tests/test_encoding.py | 117 - libs/html5lib/tests/test_meta.py | 41 - .../tests/test_optionaltags_filter.py | 7 - libs/html5lib/tests/test_parser2.py | 94 - libs/html5lib/tests/test_sanitizer.py | 133 - libs/html5lib/tests/test_serializer.py | 226 - libs/html5lib/tests/test_stream.py | 325 - libs/html5lib/tests/test_tokenizer2.py | 66 - libs/html5lib/tests/test_treeadapters.py | 40 - libs/html5lib/tests/test_treewalkers.py | 205 - libs/html5lib/tests/test_whitespace_filter.py | 125 - libs/html5lib/tests/tokenizer.py | 253 - libs/html5lib/tests/tokenizertotree.py | 69 - libs/html5lib/tests/tree_construction.py | 205 - libs/html5lib/tests/us-ascii.html | 3 - libs/html5lib/tests/utf-8-bom.html | 3 - libs/idna/__init__.py | 44 +- libs/idna/codec.py | 64 +- libs/idna/compat.py | 9 +- libs/idna/core.py | 183 +- libs/idna/idnadata.py | 278 +- libs/idna/intranges.py | 9 +- libs/idna/package_data.py | 2 +- .../tests/__init__.py => idna/py.typed} | 0 libs/idna/uts46data.py | 12493 +++---- libs/importlib_metadata/__init__.py | 1051 + libs/importlib_metadata/_adapters.py | 68 + libs/importlib_metadata/_collections.py | 30 + libs/importlib_metadata/_compat.py | 71 + libs/importlib_metadata/_functools.py | 104 + libs/importlib_metadata/_itertools.py | 73 + libs/importlib_metadata/_meta.py | 48 + libs/importlib_metadata/_text.py | 99 + .../py.typed} | 0 libs/importlib_resources/tests/_compat.py | 19 - .../tests/data01/binary.file | Bin 4 -> 0 bytes .../tests/data01/subdirectory/binary.file | Bin 4 -> 0 bytes .../tests/data01/utf-16.file | Bin 44 -> 0 bytes .../tests/data01/utf-8.file | 1 - .../tests/data02/one/resource1.txt | 1 - .../tests/data02/two/resource2.txt | 1 - .../tests/namespacedata01/binary.file | Bin 4 -> 0 bytes .../tests/namespacedata01/utf-16.file | Bin 44 -> 0 bytes .../tests/namespacedata01/utf-8.file | 1 - .../tests/test_compatibilty_files.py | 102 - .../tests/test_contents.py | 43 - libs/importlib_resources/tests/test_files.py | 46 - libs/importlib_resources/tests/test_open.py | 81 - libs/importlib_resources/tests/test_path.py | 64 - libs/importlib_resources/tests/test_read.py | 76 - libs/importlib_resources/tests/test_reader.py | 128 - .../tests/test_resource.py | 252 - libs/importlib_resources/tests/update-zips.py | 53 - libs/importlib_resources/tests/util.py | 178 - .../tests/zipdata01/ziptestdata.zip | Bin 876 -> 0 bytes .../tests/zipdata02/ziptestdata.zip | Bin 698 -> 0 bytes libs/inflect.py | 1368 +- libs/ipaddress.py | 2419 -- libs/itsdangerous/__init__.py | 36 +- libs/itsdangerous/_compat.py | 46 - libs/itsdangerous/_json.py | 34 +- libs/itsdangerous/encoding.py | 23 +- libs/itsdangerous/exc.py | 63 +- libs/itsdangerous/jws.py | 71 +- .../__init__.py => itsdangerous/py.typed} | 0 libs/itsdangerous/serializer.py | 244 +- libs/itsdangerous/signer.py | 212 +- libs/itsdangerous/timed.py | 148 +- libs/itsdangerous/url_safe.py | 25 +- libs/jinja2/__init__.py | 124 +- libs/jinja2/_compat.py | 105 - libs/jinja2/_identifier.py | 6 +- libs/jinja2/async_utils.py | 75 + libs/jinja2/asyncfilters.py | 146 - libs/jinja2/asyncsupport.py | 256 - libs/jinja2/bccache.py | 245 +- libs/jinja2/compiler.py | 1850 +- libs/jinja2/constants.py | 16 +- libs/jinja2/debug.py | 529 +- libs/jinja2/defaults.py | 78 +- libs/jinja2/environment.py | 1347 +- libs/jinja2/exceptions.py | 140 +- libs/jinja2/ext.py | 720 +- libs/jinja2/filters.py | 1534 +- libs/jinja2/idtracking.py | 210 +- libs/jinja2/lexer.py | 990 +- libs/jinja2/loaders.py | 481 +- libs/jinja2/meta.py | 77 +- libs/jinja2/nativetypes.py | 282 +- libs/jinja2/nodes.py | 805 +- libs/jinja2/optimizer.py | 68 +- libs/jinja2/parser.py | 841 +- .../data02/__init__.py => jinja2/py.typed} | 0 libs/jinja2/runtime.py | 1172 +- libs/jinja2/sandbox.py | 396 +- libs/jinja2/tests.py | 227 +- libs/jinja2/utils.py | 834 +- libs/jinja2/visitor.py | 39 +- libs/js2py/base.py | 19 +- libs/js2py/constructors/jsdate.py | 215 +- libs/js2py/constructors/time_helpers.py | 4 +- libs/js2py/es6/babel.js | 6 - libs/js2py/es6/buildBabel | 12 - libs/js2py/evaljs.py | 45 +- libs/js2py/internals/__init__.py | 1 + libs/js2py/internals/base.py | 7 +- libs/js2py/internals/constructors/jsdate.py | 168 +- .../internals/constructors/jsfunction.py | 2 +- libs/js2py/internals/constructors/jsmath.py | 15 +- libs/js2py/internals/constructors/jsstring.py | 2 +- .../internals/constructors/time_helpers.py | 4 +- libs/js2py/legecy_translators/__init__.py | 1 - libs/js2py/legecy_translators/constants.py | 309 - libs/js2py/legecy_translators/exps.py | 85 - libs/js2py/legecy_translators/flow.py | 482 - libs/js2py/legecy_translators/functions.py | 100 - libs/js2py/legecy_translators/jsparser.py | 327 - libs/js2py/legecy_translators/nodevisitor.py | 564 - libs/js2py/legecy_translators/nparser.py | 3209 -- libs/js2py/legecy_translators/objects.py | 302 - libs/js2py/legecy_translators/tokenize.py | 4 - libs/js2py/legecy_translators/translator.py | 153 - libs/js2py/legecy_translators/utils.py | 91 - libs/js2py/node_import.py | 84 +- libs/js2py/py_node_modules/crypto_js.py | 15477 +++++++++ libs/js2py/py_node_modules/escodegen.py | 16171 +++++++++ libs/js2py/py_node_modules/esprima.py | 14120 ++++++++ libs/js2py/translators/translating_nodes.py | 50 +- libs/js2py/translators/translator.py | 13 +- libs/json_tricks/__init__.py | 13 +- libs/json_tricks/_version.py | 3 + libs/json_tricks/comment.py | 2 +- libs/json_tricks/decoders.py | 308 +- libs/json_tricks/encoders.py | 217 +- libs/json_tricks/nonp.py | 173 +- libs/json_tricks/np.py | 6 +- libs/json_tricks/np_utils.py | 2 +- libs/json_tricks/utils.py | 151 +- libs/jstyleson.py | 124 - libs/knowit/__init__.py | 14 +- libs/knowit/__main__.py | 234 +- libs/knowit/api.py | 72 +- libs/knowit/config.py | 30 +- libs/knowit/core.py | 216 +- libs/knowit/defaults.yml | 15 + libs/knowit/properties/__init__.py | 23 +- libs/knowit/properties/audio.py | 55 + libs/knowit/properties/audio/__init__.py | 8 - libs/knowit/properties/audio/bitratemode.py | 10 - libs/knowit/properties/audio/channels.py | 26 - libs/knowit/properties/audio/codec.py | 24 - libs/knowit/properties/audio/compression.py | 10 - libs/knowit/properties/audio/profile.py | 10 - libs/knowit/properties/basic.py | 27 - libs/knowit/properties/duration.py | 38 - libs/knowit/properties/general.py | 144 + libs/knowit/properties/language.py | 28 - libs/knowit/properties/quantity.py | 27 - libs/knowit/properties/subtitle.py | 14 + libs/knowit/properties/subtitle/__init__.py | 4 - libs/knowit/properties/subtitle/format.py | 18 - libs/knowit/properties/video.py | 120 + libs/knowit/properties/video/__init__.py | 10 - libs/knowit/properties/video/codec.py | 16 - libs/knowit/properties/video/encoder.py | 10 - libs/knowit/properties/video/profile.py | 41 - libs/knowit/properties/video/ratio.py | 35 - libs/knowit/properties/video/scantype.py | 10 - libs/knowit/properties/yesno.py | 25 - libs/knowit/property.py | 137 - libs/knowit/provider.py | 29 +- libs/knowit/providers/__init__.py | 9 +- libs/knowit/providers/enzyme.py | 128 +- libs/knowit/providers/ffmpeg.py | 185 +- libs/knowit/providers/mediainfo.py | 236 +- libs/knowit/providers/mkvmerge.py | 248 + libs/knowit/rule.py | 17 - libs/knowit/rules/__init__.py | 17 +- libs/knowit/rules/audio.py | 104 + libs/knowit/rules/audio/__init__.py | 7 - libs/knowit/rules/audio/atmos.py | 33 - libs/knowit/rules/audio/channels.py | 57 - libs/knowit/rules/audio/codec.py | 13 - libs/knowit/rules/audio/dtshd.py | 32 - libs/knowit/rules/{language.py => general.py} | 4 +- .../closedcaption.py => subtitle.py} | 17 +- libs/knowit/rules/subtitle/__init__.py | 5 - libs/knowit/rules/subtitle/hearingimpaired.py | 18 - .../rules/{video/resolution.py => video.py} | 12 +- libs/knowit/rules/video/__init__.py | 4 - libs/knowit/serializer.py | 128 +- libs/knowit/units.py | 43 +- libs/knowit/utils.py | 121 +- libs/markdown/__init__.py | 550 +- libs/markdown/__main__.py | 51 +- libs/markdown/__meta__.py | 49 + libs/markdown/__version__.py | 30 - libs/markdown/blockparser.py | 43 +- libs/markdown/blockprocessors.py | 158 +- libs/markdown/core.py | 407 + libs/markdown/extensions/__init__.py | 75 +- libs/markdown/extensions/abbr.py | 71 +- libs/markdown/extensions/admonition.py | 116 +- libs/markdown/extensions/attr_list.py | 44 +- libs/markdown/extensions/codehilite.py | 210 +- libs/markdown/extensions/def_list.py | 38 +- libs/markdown/extensions/extra.py | 100 +- libs/markdown/extensions/fenced_code.py | 162 +- libs/markdown/extensions/footnotes.py | 267 +- libs/markdown/extensions/headerid.py | 97 - libs/markdown/extensions/legacy_attrs.py | 67 + libs/markdown/extensions/legacy_em.py | 49 + libs/markdown/extensions/md_in_html.py | 364 + libs/markdown/extensions/meta.py | 21 +- libs/markdown/extensions/nl2br.py | 18 +- libs/markdown/extensions/sane_lists.py | 19 +- libs/markdown/extensions/smart_strong.py | 41 - libs/markdown/extensions/smarty.py | 77 +- libs/markdown/extensions/tables.py | 28 +- libs/markdown/extensions/toc.py | 178 +- libs/markdown/extensions/wikilinks.py | 36 +- libs/markdown/htmlparser.py | 323 + libs/markdown/inlinepatterns.py | 832 +- libs/markdown/odict.py | 191 - libs/markdown/pep562.py | 245 + libs/markdown/postprocessors.py | 97 +- libs/markdown/preprocessors.py | 335 +- libs/markdown/serializers.py | 217 +- libs/markdown/test_tools.py | 220 + libs/markdown/treeprocessors.py | 122 +- libs/markdown/util.py | 416 +- libs/markupsafe/__init__.py | 327 +- libs/markupsafe/_compat.py | 33 - libs/markupsafe/_constants.py | 264 - libs/markupsafe/_native.py | 42 +- libs/markupsafe/_speedups.c | 316 +- libs/markupsafe/_speedups.pyi | 9 + .../one/__init__.py => markupsafe/py.typed} | 0 libs/msgpack/_cmsgpack.cpp | 16620 ++++++++++ libs/msgpack/_packer.pyx | 2 + libs/msgpack/_version.py | 2 +- libs/msgpack/fallback.py | 6 +- libs/msgpack/unpack.h | 2 +- libs/oauthlib/__init__.py | 33 +- libs/oauthlib/common.py | 99 +- libs/oauthlib/oauth1/__init__.py | 19 +- libs/oauthlib/oauth1/rfc5849/__init__.py | 107 +- .../oauth1/rfc5849/endpoints/__init__.py | 9 +- .../oauth1/rfc5849/endpoints/access_token.py | 8 +- .../oauth1/rfc5849/endpoints/authorization.py | 12 +- .../oauthlib/oauth1/rfc5849/endpoints/base.py | 72 +- .../rfc5849/endpoints/pre_configured.py | 8 +- .../oauth1/rfc5849/endpoints/request_token.py | 10 +- .../oauth1/rfc5849/endpoints/resource.py | 2 - .../rfc5849/endpoints/signature_only.py | 2 - libs/oauthlib/oauth1/rfc5849/errors.py | 7 +- libs/oauthlib/oauth1/rfc5849/parameters.py | 10 +- .../oauth1/rfc5849/request_validator.py | 72 +- libs/oauthlib/oauth1/rfc5849/signature.py | 862 +- libs/oauthlib/oauth1/rfc5849/utils.py | 17 +- libs/oauthlib/oauth2/__init__.py | 49 +- libs/oauthlib/oauth2/rfc6749/__init__.py | 58 +- .../oauth2/rfc6749/clients/__init__.py | 10 +- .../rfc6749/clients/backend_application.py | 24 +- libs/oauthlib/oauth2/rfc6749/clients/base.py | 83 +- .../rfc6749/clients/legacy_application.py | 26 +- .../rfc6749/clients/mobile_application.py | 8 +- .../rfc6749/clients/service_application.py | 62 +- .../oauth2/rfc6749/clients/web_application.py | 53 +- .../oauth2/rfc6749/endpoints/__init__.py | 16 +- .../oauth2/rfc6749/endpoints/authorization.py | 3 - .../oauthlib/oauth2/rfc6749/endpoints/base.py | 62 +- .../oauth2/rfc6749/endpoints/introspect.py | 122 + .../oauth2/rfc6749/endpoints/metadata.py | 237 + .../rfc6749/endpoints/pre_configured.py | 145 +- .../oauth2/rfc6749/endpoints/resource.py | 5 +- .../oauth2/rfc6749/endpoints/revocation.py | 38 +- .../oauth2/rfc6749/endpoints/token.py | 14 +- libs/oauthlib/oauth2/rfc6749/errors.py | 240 +- .../oauth2/rfc6749/grant_types/__init__.py | 15 +- .../rfc6749/grant_types/authorization_code.py | 194 +- .../oauth2/rfc6749/grant_types/base.py | 133 +- .../rfc6749/grant_types/client_credentials.py | 25 +- .../oauth2/rfc6749/grant_types/implicit.py | 88 +- .../rfc6749/grant_types/refresh_token.py | 28 +- .../resource_owner_password_credentials.py | 22 +- libs/oauthlib/oauth2/rfc6749/parameters.py | 130 +- .../oauth2/rfc6749/request_validator.py | 384 +- libs/oauthlib/oauth2/rfc6749/tokens.py | 112 +- libs/oauthlib/oauth2/rfc6749/utils.py | 25 +- libs/oauthlib/openid/__init__.py | 7 + .../openid/connect}/__init__.py | 0 .../openid/connect/core}/__init__.py | 0 .../openid/connect/core/endpoints/__init__.py | 9 + .../connect/core/endpoints/pre_configured.py | 97 + .../openid/connect/core/endpoints/userinfo.py | 99 + .../openid/connect/core/exceptions.py | 149 + .../connect/core/grant_types/__init__.py | 12 + .../core/grant_types/authorization_code.py | 43 + .../connect/core/grant_types/base.py} | 255 +- .../connect/core/grant_types/dispatchers.py | 101 + .../openid/connect/core/grant_types/hybrid.py | 63 + .../connect/core/grant_types/implicit.py | 51 + .../openid/connect/core/request_validator.py | 308 + libs/oauthlib/openid/connect/core/tokens.py | 46 + libs/oauthlib/signals.py | 7 +- libs/oauthlib/uri_validate.py | 30 +- libs/peewee.py | 118 +- libs/playhouse/_sqlite_ext.c | 27591 ++++++++++++++++ libs/playhouse/_sqlite_ext.pyx | 86 +- libs/playhouse/_sqlite_udf.c | 7721 +++++ libs/playhouse/apsw_ext.py | 1 + libs/playhouse/cockroachdb.py | 27 +- libs/playhouse/dataset.py | 27 +- libs/playhouse/fields.py | 4 - libs/playhouse/mysql_ext.py | 41 + libs/playhouse/psycopg3_ext.py | 35 + libs/playhouse/shortcuts.py | 28 + libs/playhouse/sqlcipher_ext.py | 3 + libs/playhouse/sqlite_ext.py | 25 +- libs/pycountry/__init__.py | 123 +- libs/pycountry/databases/iso3166-1.json | 261 +- libs/pycountry/databases/iso3166-2.json | 8571 +++-- libs/pycountry/databases/iso639-5.json | 464 + libs/pycountry/db.py | 44 +- .../locales/ab/LC_MESSAGES/iso3166-1.mo | Bin 569 -> 528 bytes .../locales/ace/LC_MESSAGES/iso3166-1.mo | Bin 565 -> 524 bytes .../locales/ach/LC_MESSAGES/iso3166-1.mo | Bin 9240 -> 9124 bytes .../locales/af/LC_MESSAGES/iso3166-1.mo | Bin 22090 -> 22573 bytes .../locales/af/LC_MESSAGES/iso3166-3.mo | Bin 1042 -> 1001 bytes .../locales/af/LC_MESSAGES/iso639-3.mo | Bin 5413 -> 5414 bytes .../locales/ak/LC_MESSAGES/iso3166-1.mo | Bin 563 -> 522 bytes .../locales/am/LC_MESSAGES/iso3166-1.mo | Bin 6487 -> 6446 bytes .../locales/am/LC_MESSAGES/iso3166-3.mo | Bin 517 -> 476 bytes .../locales/am/LC_MESSAGES/iso639-3.mo | Bin 5838 -> 5797 bytes .../locales/an/LC_MESSAGES/iso3166-1.mo | Bin 569 -> 3810 bytes .../locales/ar/LC_MESSAGES/iso15924.mo | Bin 3928 -> 4139 bytes .../locales/ar/LC_MESSAGES/iso3166-1.mo | Bin 27240 -> 28440 bytes .../locales/ar/LC_MESSAGES/iso3166-3.mo | Bin 3396 -> 3365 bytes .../locales/ar/LC_MESSAGES/iso4217.mo | Bin 0 -> 8945 bytes .../locales/ar/LC_MESSAGES/iso639-3.mo | Bin 3368 -> 8284 bytes .../locales/as/LC_MESSAGES/iso3166-1.mo | Bin 34158 -> 33684 bytes .../locales/as/LC_MESSAGES/iso3166-3.mo | Bin 4329 -> 4288 bytes .../locales/ast/LC_MESSAGES/iso15924.mo | Bin 0 -> 372 bytes .../locales/ast/LC_MESSAGES/iso3166-1.mo | Bin 23199 -> 22866 bytes .../locales/ast/LC_MESSAGES/iso3166-2.mo | Bin 0 -> 373 bytes .../locales/ast/LC_MESSAGES/iso3166-3.mo | Bin 2822 -> 2781 bytes .../locales/ast/LC_MESSAGES/iso4217.mo | Bin 0 -> 371 bytes .../locales/ast/LC_MESSAGES/iso639-3.mo | Bin 317312 -> 317271 bytes .../locales/ast/LC_MESSAGES/iso639-5.mo | Bin 0 -> 372 bytes .../locales/ay/LC_MESSAGES/iso3166-1.mo | Bin 562 -> 521 bytes .../locales/az/LC_MESSAGES/iso3166-1.mo | Bin 9944 -> 9867 bytes .../locales/az/LC_MESSAGES/iso3166-2.mo | Bin 3475 -> 3341 bytes .../locales/az/LC_MESSAGES/iso3166-3.mo | Bin 538 -> 497 bytes .../locales/az/LC_MESSAGES/iso639-3.mo | Bin 1472 -> 1431 bytes .../locales/ba/LC_MESSAGES/iso3166-1.mo | Bin 567 -> 526 bytes .../locales/bar/LC_MESSAGES/iso3166-1.mo | Bin 6935 -> 6894 bytes .../locales/be/LC_MESSAGES/iso15924.mo | Bin 12741 -> 12701 bytes .../locales/be/LC_MESSAGES/iso3166-1.mo | Bin 29889 -> 29801 bytes .../locales/be/LC_MESSAGES/iso3166-2.mo | Bin 249143 -> 197150 bytes .../locales/be/LC_MESSAGES/iso3166-3.mo | Bin 3571 -> 3530 bytes .../locales/be/LC_MESSAGES/iso4217.mo | Bin 11447 -> 11406 bytes .../locales/be/LC_MESSAGES/iso639-3.mo | Bin 0 -> 18867 bytes .../locales/be/LC_MESSAGES/iso639-5.mo | Bin 0 -> 9437 bytes .../locales/bg/LC_MESSAGES/iso15924.mo | Bin 1802 -> 2003 bytes .../locales/bg/LC_MESSAGES/iso3166-1.mo | Bin 28540 -> 28175 bytes .../locales/bg/LC_MESSAGES/iso3166-2.mo | Bin 19459 -> 15126 bytes .../locales/bg/LC_MESSAGES/iso3166-3.mo | Bin 3420 -> 3379 bytes .../locales/bg/LC_MESSAGES/iso639-3.mo | Bin 24608 -> 24566 bytes .../locales/bi/LC_MESSAGES/iso3166-1.mo | Bin 567 -> 526 bytes .../locales/bn/LC_MESSAGES/iso3166-1.mo | Bin 35358 -> 35747 bytes .../locales/bn/LC_MESSAGES/iso3166-3.mo | Bin 4274 -> 4233 bytes .../locales/bn/LC_MESSAGES/iso4217.mo | Bin 0 -> 3100 bytes .../locales/bn/LC_MESSAGES/iso639-3.mo | Bin 89884 -> 98168 bytes .../locales/bn/LC_MESSAGES/iso639-5.mo | Bin 0 -> 1205 bytes .../locales/bn_BD/LC_MESSAGES/iso15924.mo | Bin 0 -> 14811 bytes .../locales/bn_BD/LC_MESSAGES/iso3166-2.mo | Bin 0 -> 1161 bytes .../locales/bn_BD/LC_MESSAGES/iso639-5.mo | Bin 0 -> 2772 bytes .../locales/bn_IN/LC_MESSAGES/iso3166-1.mo | Bin 34259 -> 35843 bytes .../locales/bn_IN/LC_MESSAGES/iso3166-3.mo | Bin 4350 -> 4309 bytes .../locales/br/LC_MESSAGES/iso15924.mo | Bin 6586 -> 6545 bytes .../locales/br/LC_MESSAGES/iso3166-1.mo | Bin 10678 -> 10585 bytes .../locales/br/LC_MESSAGES/iso3166-3.mo | Bin 2246 -> 2205 bytes .../locales/br/LC_MESSAGES/iso4217.mo | Bin 4990 -> 4949 bytes .../locales/br/LC_MESSAGES/iso639-3.mo | Bin 33536 -> 33495 bytes .../locales/bs/LC_MESSAGES/iso3166-1.mo | Bin 22684 -> 22365 bytes .../locales/bs/LC_MESSAGES/iso3166-2.mo | Bin 3509 -> 3326 bytes .../locales/bs/LC_MESSAGES/iso3166-3.mo | Bin 2750 -> 2708 bytes .../locales/bs/LC_MESSAGES/iso639-3.mo | Bin 2435 -> 2394 bytes .../locales/byn/LC_MESSAGES/iso3166-1.mo | Bin 5809 -> 5768 bytes .../locales/byn/LC_MESSAGES/iso3166-3.mo | Bin 515 -> 474 bytes .../locales/byn/LC_MESSAGES/iso639-3.mo | Bin 5734 -> 5693 bytes .../locales/ca/LC_MESSAGES/iso15924.mo | Bin 2986 -> 2945 bytes .../locales/ca/LC_MESSAGES/iso3166-1.mo | Bin 23523 -> 23143 bytes .../locales/ca/LC_MESSAGES/iso3166-2.mo | Bin 3474 -> 4601 bytes .../locales/ca/LC_MESSAGES/iso3166-3.mo | Bin 2878 -> 2837 bytes .../locales/ca/LC_MESSAGES/iso4217.mo | Bin 8520 -> 8851 bytes .../locales/ca/LC_MESSAGES/iso639-3.mo | Bin 10493 -> 26053 bytes .../locales/ce/LC_MESSAGES/iso3166-1.mo | Bin 10947 -> 10853 bytes .../locales/ch/LC_MESSAGES/iso3166-1.mo | Bin 1511 -> 1470 bytes .../locales/chr/LC_MESSAGES/iso3166-1.mo | Bin 5217 -> 5176 bytes .../locales/ckb/LC_MESSAGES/iso3166-1.mo | Bin 10093 -> 10489 bytes .../locales/crh/LC_MESSAGES/iso3166-1.mo | Bin 22045 -> 21677 bytes .../locales/crh/LC_MESSAGES/iso3166-2.mo | Bin 3985 -> 3684 bytes .../locales/crh/LC_MESSAGES/iso3166-3.mo | Bin 2922 -> 2881 bytes .../locales/crh/LC_MESSAGES/iso639-3.mo | Bin 261783 -> 261742 bytes .../locales/cs/LC_MESSAGES/iso15924.mo | Bin 8802 -> 8919 bytes .../locales/cs/LC_MESSAGES/iso3166-1.mo | Bin 23924 -> 23895 bytes .../locales/cs/LC_MESSAGES/iso3166-2.mo | Bin 10095 -> 9150 bytes .../locales/cs/LC_MESSAGES/iso3166-3.mo | Bin 2875 -> 2834 bytes .../locales/cs/LC_MESSAGES/iso4217.mo | Bin 7444 -> 9342 bytes .../locales/cs/LC_MESSAGES/iso639-3.mo | Bin 14507 -> 14466 bytes .../locales/csb/LC_MESSAGES/iso3166-1.mo | Bin 4522 -> 4481 bytes .../locales/cv/LC_MESSAGES/iso15924.mo | Bin 0 -> 10986 bytes .../locales/cv/LC_MESSAGES/iso3166-1.mo | Bin 10947 -> 10948 bytes .../locales/cy/LC_MESSAGES/iso15924.mo | Bin 0 -> 381 bytes .../locales/cy/LC_MESSAGES/iso3166-1.mo | Bin 23235 -> 23595 bytes .../locales/cy/LC_MESSAGES/iso3166-3.mo | Bin 2742 -> 2701 bytes .../locales/cy/LC_MESSAGES/iso639-3.mo | Bin 4754 -> 5078 bytes .../locales/da/LC_MESSAGES/iso15924.mo | Bin 10223 -> 10182 bytes .../locales/da/LC_MESSAGES/iso3166-1.mo | Bin 23220 -> 23414 bytes .../locales/da/LC_MESSAGES/iso3166-2.mo | Bin 174278 -> 134961 bytes .../locales/da/LC_MESSAGES/iso3166-3.mo | Bin 2756 -> 2787 bytes .../locales/da/LC_MESSAGES/iso4217.mo | Bin 7244 -> 9085 bytes .../locales/da/LC_MESSAGES/iso639-3.mo | Bin 19578 -> 19846 bytes .../locales/da/LC_MESSAGES/iso639-5.mo | Bin 0 -> 910 bytes .../locales/de/LC_MESSAGES/iso15924.mo | Bin 10374 -> 10383 bytes .../locales/de/LC_MESSAGES/iso3166-1.mo | Bin 23416 -> 23382 bytes .../locales/de/LC_MESSAGES/iso3166-2.mo | Bin 206722 -> 219381 bytes .../locales/de/LC_MESSAGES/iso3166-3.mo | Bin 2823 -> 2782 bytes .../locales/de/LC_MESSAGES/iso4217.mo | Bin 9281 -> 9295 bytes .../locales/de/LC_MESSAGES/iso639-3.mo | Bin 398398 -> 401340 bytes .../locales/de/LC_MESSAGES/iso639-5.mo | Bin 0 -> 7860 bytes .../locales/dv/LC_MESSAGES/iso3166-1.mo | Bin 11421 -> 11329 bytes .../locales/dz/LC_MESSAGES/iso3166-1.mo | Bin 40966 -> 40371 bytes .../locales/dz/LC_MESSAGES/iso3166-3.mo | Bin 5016 -> 4975 bytes .../locales/ee/LC_MESSAGES/iso3166-1.mo | Bin 563 -> 522 bytes .../locales/el/LC_MESSAGES/iso15924.mo | Bin 1832 -> 1864 bytes .../locales/el/LC_MESSAGES/iso3166-1.mo | Bin 31075 -> 30890 bytes .../locales/el/LC_MESSAGES/iso3166-2.mo | Bin 13348 -> 9264 bytes .../locales/el/LC_MESSAGES/iso3166-3.mo | Bin 3635 -> 3594 bytes .../locales/el/LC_MESSAGES/iso4217.mo | Bin 9656 -> 9686 bytes .../locales/el/LC_MESSAGES/iso639-3.mo | Bin 58367 -> 58461 bytes .../locales/el/LC_MESSAGES/iso639-5.mo | Bin 0 -> 1951 bytes .../locales/en/LC_MESSAGES/iso3166-2.mo | Bin 107786 -> 90270 bytes .../locales/eo/LC_MESSAGES/iso15924.mo | Bin 8565 -> 8524 bytes .../locales/eo/LC_MESSAGES/iso3166-1.mo | Bin 22788 -> 22497 bytes .../locales/eo/LC_MESSAGES/iso3166-2.mo | Bin 3468 -> 3296 bytes .../locales/eo/LC_MESSAGES/iso3166-3.mo | Bin 2750 -> 2709 bytes .../locales/eo/LC_MESSAGES/iso639-3.mo | Bin 13376 -> 13335 bytes .../locales/es/LC_MESSAGES/iso15924.mo | Bin 6912 -> 10493 bytes .../locales/es/LC_MESSAGES/iso3166-1.mo | Bin 23486 -> 23831 bytes .../locales/es/LC_MESSAGES/iso3166-2.mo | Bin 5343 -> 15692 bytes .../locales/es/LC_MESSAGES/iso3166-3.mo | Bin 2948 -> 2907 bytes .../locales/es/LC_MESSAGES/iso4217.mo | Bin 7370 -> 9159 bytes .../locales/es/LC_MESSAGES/iso639-3.mo | Bin 13133 -> 33788 bytes .../locales/et/LC_MESSAGES/iso15924.mo | Bin 9465 -> 9689 bytes .../locales/et/LC_MESSAGES/iso3166-1.mo | Bin 22979 -> 22972 bytes .../locales/et/LC_MESSAGES/iso3166-2.mo | Bin 1912 -> 1751 bytes .../locales/et/LC_MESSAGES/iso3166-3.mo | Bin 2787 -> 2746 bytes .../locales/et/LC_MESSAGES/iso4217.mo | Bin 9003 -> 8962 bytes .../locales/et/LC_MESSAGES/iso639-3.mo | Bin 24288 -> 29517 bytes .../locales/et/LC_MESSAGES/iso639-5.mo | Bin 0 -> 7437 bytes .../locales/eu/LC_MESSAGES/iso15924.mo | Bin 0 -> 10499 bytes .../locales/eu/LC_MESSAGES/iso3166-1.mo | Bin 23454 -> 23796 bytes .../locales/eu/LC_MESSAGES/iso3166-2.mo | Bin 3557 -> 23772 bytes .../locales/eu/LC_MESSAGES/iso3166-3.mo | Bin 2891 -> 2856 bytes .../locales/eu/LC_MESSAGES/iso4217.mo | Bin 0 -> 856 bytes .../locales/eu/LC_MESSAGES/iso639-3.mo | Bin 17732 -> 20650 bytes .../locales/fa/LC_MESSAGES/iso15924.mo | Bin 2098 -> 2314 bytes .../locales/fa/LC_MESSAGES/iso3166-1.mo | Bin 25991 -> 26497 bytes .../locales/fa/LC_MESSAGES/iso3166-2.mo | Bin 0 -> 372 bytes .../locales/fa/LC_MESSAGES/iso3166-3.mo | Bin 3307 -> 3266 bytes .../locales/fa/LC_MESSAGES/iso639-3.mo | Bin 13141 -> 13407 bytes .../locales/ff/LC_MESSAGES/iso3166-1.mo | Bin 3793 -> 3752 bytes .../locales/fi/LC_MESSAGES/iso15924.mo | Bin 9047 -> 9006 bytes .../locales/fi/LC_MESSAGES/iso3166-1.mo | Bin 22819 -> 22500 bytes .../locales/fi/LC_MESSAGES/iso3166-2.mo | Bin 10647 -> 4296 bytes .../locales/fi/LC_MESSAGES/iso3166-3.mo | Bin 2796 -> 2755 bytes .../locales/fi/LC_MESSAGES/iso4217.mo | Bin 7407 -> 7366 bytes .../locales/fi/LC_MESSAGES/iso639-3.mo | Bin 13317 -> 13276 bytes .../locales/fil/LC_MESSAGES/iso15924.mo | Bin 0 -> 11351 bytes .../locales/fil/LC_MESSAGES/iso3166-1.mo | Bin 0 -> 23858 bytes .../locales/fo/LC_MESSAGES/iso3166-1.mo | Bin 5191 -> 5870 bytes .../locales/fo/LC_MESSAGES/iso3166-3.mo | Bin 434 -> 393 bytes .../locales/fr/LC_MESSAGES/iso15924.mo | Bin 10268 -> 10249 bytes .../locales/fr/LC_MESSAGES/iso3166-1.mo | Bin 24239 -> 24254 bytes .../locales/fr/LC_MESSAGES/iso3166-2.mo | Bin 205955 -> 165098 bytes .../locales/fr/LC_MESSAGES/iso3166-3.mo | Bin 2882 -> 2841 bytes .../locales/fr/LC_MESSAGES/iso4217.mo | Bin 9452 -> 9403 bytes .../locales/fr/LC_MESSAGES/iso639-3.mo | Bin 414048 -> 413964 bytes .../locales/fr/LC_MESSAGES/iso639-5.mo | Bin 0 -> 7886 bytes .../locales/frp/LC_MESSAGES/iso3166-1.mo | Bin 9283 -> 9204 bytes .../locales/fur/LC_MESSAGES/iso3166-1.mo | Bin 2610 -> 23765 bytes .../locales/fur/LC_MESSAGES/iso3166-3.mo | Bin 0 -> 1048 bytes .../locales/fur/LC_MESSAGES/iso639-5.mo | Bin 0 -> 7043 bytes .../locales/fy/LC_MESSAGES/iso3166-1.mo | Bin 9464 -> 9387 bytes .../locales/ga/LC_MESSAGES/iso3166-1.mo | Bin 23956 -> 23614 bytes .../locales/ga/LC_MESSAGES/iso3166-2.mo | Bin 2594 -> 2380 bytes .../locales/ga/LC_MESSAGES/iso3166-3.mo | Bin 2846 -> 2805 bytes .../locales/ga/LC_MESSAGES/iso4217.mo | Bin 4340 -> 4299 bytes .../locales/ga/LC_MESSAGES/iso639-3.mo | Bin 8909 -> 8868 bytes .../locales/gez/LC_MESSAGES/iso3166-1.mo | Bin 5859 -> 5818 bytes .../locales/gez/LC_MESSAGES/iso3166-3.mo | Bin 517 -> 476 bytes .../locales/gez/LC_MESSAGES/iso639-3.mo | Bin 5737 -> 5696 bytes .../locales/gl/LC_MESSAGES/iso15924.mo | Bin 8095 -> 8054 bytes .../locales/gl/LC_MESSAGES/iso3166-1.mo | Bin 23575 -> 23342 bytes .../locales/gl/LC_MESSAGES/iso3166-3.mo | Bin 2847 -> 2806 bytes .../locales/gl/LC_MESSAGES/iso4217.mo | Bin 2950 -> 2909 bytes .../locales/gl/LC_MESSAGES/iso639-3.mo | Bin 312845 -> 312801 bytes .../locales/gn/LC_MESSAGES/iso3166-1.mo | Bin 9059 -> 8982 bytes .../locales/gu/LC_MESSAGES/iso3166-1.mo | Bin 34580 -> 34068 bytes .../locales/gu/LC_MESSAGES/iso3166-3.mo | Bin 4128 -> 4087 bytes .../locales/gu/LC_MESSAGES/iso639-3.mo | Bin 45070 -> 45029 bytes .../locales/gv/LC_MESSAGES/iso3166-1.mo | Bin 9100 -> 9015 bytes .../locales/ha/LC_MESSAGES/iso3166-1.mo | Bin 5208 -> 5167 bytes .../locales/haw/LC_MESSAGES/iso3166-1.mo | Bin 1346 -> 1305 bytes .../locales/haw/LC_MESSAGES/iso3166-3.mo | Bin 436 -> 395 bytes .../locales/he/LC_MESSAGES/iso15924.mo | Bin 1572 -> 3240 bytes .../locales/he/LC_MESSAGES/iso3166-1.mo | Bin 27234 -> 27707 bytes .../locales/he/LC_MESSAGES/iso3166-2.mo | Bin 0 -> 34323 bytes .../locales/he/LC_MESSAGES/iso3166-3.mo | Bin 3242 -> 3201 bytes .../locales/he/LC_MESSAGES/iso639-3.mo | Bin 2522 -> 5910 bytes .../locales/hi/LC_MESSAGES/iso3166-1.mo | Bin 34778 -> 35304 bytes .../locales/hi/LC_MESSAGES/iso3166-3.mo | Bin 4142 -> 4156 bytes .../locales/hi/LC_MESSAGES/iso639-3.mo | Bin 6531 -> 6662 bytes .../locales/hr/LC_MESSAGES/iso15924.mo | Bin 0 -> 10432 bytes .../locales/hr/LC_MESSAGES/iso3166-1.mo | Bin 23248 -> 23481 bytes .../locales/hr/LC_MESSAGES/iso3166-2.mo | Bin 0 -> 24617 bytes .../locales/hr/LC_MESSAGES/iso3166-3.mo | Bin 2892 -> 2872 bytes .../locales/hr/LC_MESSAGES/iso4217.mo | Bin 9471 -> 9793 bytes .../locales/hr/LC_MESSAGES/iso639-3.mo | Bin 30781 -> 53229 bytes .../locales/hr/LC_MESSAGES/iso639-5.mo | Bin 0 -> 7671 bytes .../locales/ht/LC_MESSAGES/iso3166-1.mo | Bin 7873 -> 7772 bytes .../locales/hu/LC_MESSAGES/iso15924.mo | Bin 10342 -> 10301 bytes .../locales/hu/LC_MESSAGES/iso3166-1.mo | Bin 24495 -> 24406 bytes .../locales/hu/LC_MESSAGES/iso3166-2.mo | Bin 87367 -> 69162 bytes .../locales/hu/LC_MESSAGES/iso3166-3.mo | Bin 2921 -> 2880 bytes .../locales/hu/LC_MESSAGES/iso4217.mo | Bin 9252 -> 9211 bytes .../locales/hu/LC_MESSAGES/iso639-3.mo | Bin 37513 -> 37653 bytes .../locales/hu/LC_MESSAGES/iso639-5.mo | Bin 0 -> 7550 bytes .../locales/hy/LC_MESSAGES/iso3166-1.mo | Bin 31145 -> 30690 bytes .../locales/hy/LC_MESSAGES/iso3166-3.mo | Bin 3680 -> 3639 bytes .../locales/ia/LC_MESSAGES/iso15924.mo | Bin 7856 -> 8085 bytes .../locales/ia/LC_MESSAGES/iso3166-1.mo | Bin 23331 -> 23425 bytes .../locales/ia/LC_MESSAGES/iso3166-3.mo | Bin 2829 -> 2788 bytes .../locales/id/LC_MESSAGES/iso15924.mo | Bin 9285 -> 10195 bytes .../locales/id/LC_MESSAGES/iso3166-1.mo | Bin 22691 -> 23010 bytes .../locales/id/LC_MESSAGES/iso3166-2.mo | Bin 203606 -> 165267 bytes .../locales/id/LC_MESSAGES/iso3166-3.mo | Bin 2717 -> 2696 bytes .../locales/id/LC_MESSAGES/iso4217.mo | Bin 7237 -> 8931 bytes .../locales/id/LC_MESSAGES/iso639-3.mo | Bin 9818 -> 297913 bytes .../locales/id/LC_MESSAGES/iso639-5.mo | Bin 0 -> 5730 bytes .../locales/io/LC_MESSAGES/iso3166-1.mo | Bin 9326 -> 9240 bytes .../locales/is/LC_MESSAGES/iso15924.mo | Bin 10679 -> 10637 bytes .../locales/is/LC_MESSAGES/iso3166-1.mo | Bin 24178 -> 24205 bytes .../locales/is/LC_MESSAGES/iso3166-2.mo | Bin 5751 -> 4405 bytes .../locales/is/LC_MESSAGES/iso3166-3.mo | Bin 2973 -> 2932 bytes .../locales/is/LC_MESSAGES/iso4217.mo | Bin 9424 -> 9404 bytes .../locales/is/LC_MESSAGES/iso639-3.mo | Bin 41147 -> 152931 bytes .../locales/is/LC_MESSAGES/iso639-5.mo | Bin 0 -> 8015 bytes .../locales/it/LC_MESSAGES/iso15924.mo | Bin 9392 -> 9447 bytes .../locales/it/LC_MESSAGES/iso3166-1.mo | Bin 23662 -> 23655 bytes .../locales/it/LC_MESSAGES/iso3166-2.mo | Bin 207226 -> 161104 bytes .../locales/it/LC_MESSAGES/iso3166-3.mo | Bin 2838 -> 2830 bytes .../locales/it/LC_MESSAGES/iso4217.mo | Bin 9203 -> 9162 bytes .../locales/it/LC_MESSAGES/iso639-3.mo | Bin 375798 -> 375816 bytes .../locales/it/LC_MESSAGES/iso639-5.mo | Bin 0 -> 6606 bytes .../locales/iu/LC_MESSAGES/iso3166-1.mo | Bin 1708 -> 1667 bytes .../locales/ja/LC_MESSAGES/iso15924.mo | Bin 3688 -> 4023 bytes .../locales/ja/LC_MESSAGES/iso3166-1.mo | Bin 25126 -> 25029 bytes .../locales/ja/LC_MESSAGES/iso3166-2.mo | Bin 125567 -> 100171 bytes .../locales/ja/LC_MESSAGES/iso3166-3.mo | Bin 2880 -> 2839 bytes .../locales/ja/LC_MESSAGES/iso4217.mo | Bin 10298 -> 10318 bytes .../locales/ja/LC_MESSAGES/iso639-3.mo | Bin 16370 -> 16539 bytes .../locales/jam/LC_MESSAGES/iso3166-1.mo | Bin 8426 -> 8350 bytes .../locales/ka/LC_MESSAGES/iso3166-1.mo | Bin 33690 -> 33159 bytes .../locales/ka/LC_MESSAGES/iso3166-3.mo | Bin 4316 -> 4275 bytes .../locales/kab/LC_MESSAGES/iso15924.mo | Bin 0 -> 571 bytes .../locales/kab/LC_MESSAGES/iso3166-1.mo | Bin 6175 -> 6400 bytes .../locales/kab/LC_MESSAGES/iso3166-2.mo | Bin 0 -> 572 bytes .../locales/kab/LC_MESSAGES/iso4217.mo | Bin 0 -> 830 bytes .../locales/kab/LC_MESSAGES/iso639-3.mo | Bin 0 -> 979 bytes .../locales/kab/LC_MESSAGES/iso639-5.mo | Bin 0 -> 372 bytes .../locales/ki/LC_MESSAGES/iso3166-1.mo | Bin 5190 -> 5149 bytes .../locales/kk/LC_MESSAGES/iso3166-1.mo | Bin 29211 -> 28833 bytes .../locales/kk/LC_MESSAGES/iso3166-3.mo | Bin 3363 -> 3322 bytes .../locales/kl/LC_MESSAGES/iso3166-1.mo | Bin 571 -> 530 bytes .../locales/km/LC_MESSAGES/iso3166-1.mo | Bin 36052 -> 35929 bytes .../locales/km/LC_MESSAGES/iso3166-3.mo | Bin 4306 -> 4265 bytes .../locales/kmr/LC_MESSAGES/iso3166-1.mo | Bin 0 -> 21933 bytes .../locales/kmr/LC_MESSAGES/iso3166-3.mo | Bin 0 -> 2801 bytes .../locales/kn/LC_MESSAGES/iso3166-1.mo | Bin 30116 -> 29718 bytes .../locales/kn/LC_MESSAGES/iso3166-3.mo | Bin 460 -> 419 bytes .../locales/kn/LC_MESSAGES/iso639-3.mo | Bin 397699 -> 397658 bytes .../locales/ko/LC_MESSAGES/iso15924.mo | Bin 2709 -> 2668 bytes .../locales/ko/LC_MESSAGES/iso3166-1.mo | Bin 24227 -> 24062 bytes .../locales/ko/LC_MESSAGES/iso3166-2.mo | Bin 3217 -> 3047 bytes .../locales/ko/LC_MESSAGES/iso3166-3.mo | Bin 2733 -> 2692 bytes .../locales/ko/LC_MESSAGES/iso4217.mo | Bin 10282 -> 10241 bytes .../locales/ko/LC_MESSAGES/iso639-3.mo | Bin 16987 -> 16938 bytes .../locales/kok/LC_MESSAGES/iso639-3.mo | Bin 6625 -> 6584 bytes .../locales/ku/LC_MESSAGES/iso3166-1.mo | Bin 22322 -> 21933 bytes .../locales/ku/LC_MESSAGES/iso3166-3.mo | Bin 2842 -> 2801 bytes .../locales/kv/LC_MESSAGES/iso3166-1.mo | Bin 5880 -> 5839 bytes .../locales/kw/LC_MESSAGES/iso3166-1.mo | Bin 9464 -> 9347 bytes .../locales/ky/LC_MESSAGES/iso3166-1.mo | Bin 29686 -> 29199 bytes .../locales/ky/LC_MESSAGES/iso3166-2.mo | Bin 1602 -> 1419 bytes .../locales/lo/LC_MESSAGES/iso3166-1.mo | Bin 5851 -> 5810 bytes .../locales/lt/LC_MESSAGES/iso15924.mo | Bin 6742 -> 7253 bytes .../locales/lt/LC_MESSAGES/iso3166-1.mo | Bin 23612 -> 23272 bytes .../locales/lt/LC_MESSAGES/iso3166-2.mo | Bin 95623 -> 75622 bytes .../locales/lt/LC_MESSAGES/iso3166-3.mo | Bin 2924 -> 2883 bytes .../locales/lt/LC_MESSAGES/iso4217.mo | Bin 7259 -> 7218 bytes .../locales/lt/LC_MESSAGES/iso639-3.mo | Bin 31811 -> 32056 bytes .../locales/lv/LC_MESSAGES/iso15924.mo | Bin 9308 -> 9267 bytes .../locales/lv/LC_MESSAGES/iso3166-1.mo | Bin 23282 -> 23013 bytes .../locales/lv/LC_MESSAGES/iso3166-2.mo | Bin 2652 -> 2565 bytes .../locales/lv/LC_MESSAGES/iso3166-3.mo | Bin 2834 -> 2793 bytes .../locales/lv/LC_MESSAGES/iso4217.mo | Bin 7550 -> 7509 bytes .../locales/lv/LC_MESSAGES/iso639-3.mo | Bin 10703 -> 10662 bytes .../locales/mai/LC_MESSAGES/iso3166-1.mo | Bin 4631 -> 4590 bytes .../locales/mhr/LC_MESSAGES/iso3166-1.mo | Bin 5758 -> 5672 bytes .../locales/mi/LC_MESSAGES/iso3166-1.mo | Bin 10588 -> 10547 bytes .../locales/mi/LC_MESSAGES/iso3166-3.mo | Bin 487 -> 446 bytes .../locales/mi/LC_MESSAGES/iso639-3.mo | Bin 1660 -> 1619 bytes .../locales/mk/LC_MESSAGES/iso3166-1.mo | Bin 27873 -> 27532 bytes .../locales/mk/LC_MESSAGES/iso3166-3.mo | Bin 3439 -> 3398 bytes .../locales/mk/LC_MESSAGES/iso639-3.mo | Bin 2219 -> 2178 bytes .../locales/ml/LC_MESSAGES/iso15924.mo | Bin 1336 -> 1295 bytes .../locales/ml/LC_MESSAGES/iso3166-1.mo | Bin 35492 -> 34908 bytes .../locales/ml/LC_MESSAGES/iso3166-3.mo | Bin 4643 -> 4602 bytes .../locales/mn/LC_MESSAGES/iso3166-1.mo | Bin 10215 -> 10133 bytes .../locales/mn/LC_MESSAGES/iso3166-3.mo | Bin 468 -> 427 bytes .../locales/mn/LC_MESSAGES/iso4217.mo | Bin 5438 -> 5397 bytes .../locales/mn/LC_MESSAGES/iso639-3.mo | Bin 6442 -> 6401 bytes .../locales/mo/LC_MESSAGES/iso3166-1.mo | Bin 1854 -> 1813 bytes .../locales/mr/LC_MESSAGES/iso3166-1.mo | Bin 34338 -> 35272 bytes .../locales/mr/LC_MESSAGES/iso3166-3.mo | Bin 4143 -> 4102 bytes .../locales/mr/LC_MESSAGES/iso639-3.mo | Bin 429556 -> 433262 bytes .../locales/ms/LC_MESSAGES/iso3166-1.mo | Bin 12430 -> 12722 bytes .../locales/ms/LC_MESSAGES/iso3166-3.mo | Bin 514 -> 473 bytes .../locales/ms/LC_MESSAGES/iso639-3.mo | Bin 2198 -> 2386 bytes .../locales/mt/LC_MESSAGES/iso3166-1.mo | Bin 9713 -> 9672 bytes .../locales/mt/LC_MESSAGES/iso3166-3.mo | Bin 443 -> 402 bytes .../locales/mt/LC_MESSAGES/iso639-3.mo | Bin 9735 -> 9694 bytes .../locales/my/LC_MESSAGES/iso3166-1.mo | Bin 15761 -> 15648 bytes .../locales/na/LC_MESSAGES/iso3166-1.mo | Bin 6644 -> 6603 bytes .../locales/nah/LC_MESSAGES/iso3166-1.mo | Bin 8228 -> 8147 bytes .../locales/nb/LC_MESSAGES/iso15924.mo | Bin 4013 -> 4069 bytes .../locales/nb/LC_MESSAGES/iso3166-1.mo | Bin 23439 -> 23457 bytes .../locales/nb/LC_MESSAGES/iso3166-3.mo | Bin 2856 -> 2815 bytes .../locales/nb/LC_MESSAGES/iso4217.mo | Bin 8352 -> 8833 bytes .../locales/nb/LC_MESSAGES/iso639-3.mo | Bin 5106 -> 5570 bytes .../locales/nb_NO/LC_MESSAGES/iso3166-2.mo | Bin 0 -> 375 bytes .../locales/nb_NO/LC_MESSAGES/iso639-5.mo | Bin 0 -> 1237 bytes .../locales/ne/LC_MESSAGES/iso3166-1.mo | Bin 32969 -> 32501 bytes .../locales/ne/LC_MESSAGES/iso3166-3.mo | Bin 4004 -> 3963 bytes .../locales/nl/LC_MESSAGES/iso15924.mo | Bin 10355 -> 10308 bytes .../locales/nl/LC_MESSAGES/iso3166-1.mo | Bin 23324 -> 23335 bytes .../locales/nl/LC_MESSAGES/iso3166-2.mo | Bin 206441 -> 167430 bytes .../locales/nl/LC_MESSAGES/iso3166-3.mo | Bin 2875 -> 2834 bytes .../locales/nl/LC_MESSAGES/iso4217.mo | Bin 9748 -> 9706 bytes .../locales/nl/LC_MESSAGES/iso639-3.mo | Bin 66473 -> 70897 bytes .../locales/nl/LC_MESSAGES/iso639-5.mo | Bin 0 -> 7539 bytes .../locales/nn/LC_MESSAGES/iso15924.mo | Bin 3505 -> 3464 bytes .../locales/nn/LC_MESSAGES/iso3166-1.mo | Bin 22339 -> 22050 bytes .../locales/nn/LC_MESSAGES/iso3166-3.mo | Bin 2759 -> 2718 bytes .../locales/nn/LC_MESSAGES/iso4217.mo | Bin 7797 -> 7756 bytes .../locales/nn/LC_MESSAGES/iso639-3.mo | Bin 7503 -> 7462 bytes .../locales/nso/LC_MESSAGES/iso3166-1.mo | Bin 7937 -> 7860 bytes .../locales/nso/LC_MESSAGES/iso3166-2.mo | Bin 1011 -> 932 bytes .../locales/nso/LC_MESSAGES/iso3166-3.mo | Bin 562 -> 521 bytes .../locales/nso/LC_MESSAGES/iso639-3.mo | Bin 2913 -> 2872 bytes .../locales/nv/LC_MESSAGES/iso3166-1.mo | Bin 5997 -> 5904 bytes .../locales/oc/LC_MESSAGES/iso15924.mo | Bin 1754 -> 1713 bytes .../locales/oc/LC_MESSAGES/iso3166-1.mo | Bin 20598 -> 20290 bytes .../locales/oc/LC_MESSAGES/iso3166-2.mo | Bin 5578 -> 5315 bytes .../locales/oc/LC_MESSAGES/iso3166-3.mo | Bin 1233 -> 1192 bytes .../locales/oc/LC_MESSAGES/iso4217.mo | Bin 568 -> 527 bytes .../locales/oc/LC_MESSAGES/iso639-3.mo | Bin 3487 -> 3446 bytes .../locales/oc/LC_MESSAGES/iso639-5.mo | Bin 0 -> 807 bytes .../locales/or/LC_MESSAGES/iso3166-1.mo | Bin 32768 -> 34231 bytes .../locales/or/LC_MESSAGES/iso3166-3.mo | Bin 4189 -> 4148 bytes .../locales/or/LC_MESSAGES/iso639-3.mo | Bin 218718 -> 221063 bytes .../locales/pa/LC_MESSAGES/iso3166-1.mo | Bin 30109 -> 30607 bytes .../locales/pa/LC_MESSAGES/iso3166-3.mo | Bin 3543 -> 3502 bytes .../locales/pa/LC_MESSAGES/iso639-3.mo | Bin 418278 -> 418297 bytes .../locales/pap/LC_MESSAGES/iso3166-1.mo | Bin 572 -> 531 bytes .../locales/pi/LC_MESSAGES/iso3166-1.mo | Bin 9411 -> 9312 bytes .../locales/pl/LC_MESSAGES/iso15924.mo | Bin 10048 -> 10158 bytes .../locales/pl/LC_MESSAGES/iso3166-1.mo | Bin 23452 -> 23530 bytes .../locales/pl/LC_MESSAGES/iso3166-2.mo | Bin 203412 -> 166377 bytes .../locales/pl/LC_MESSAGES/iso3166-3.mo | Bin 2949 -> 3049 bytes .../locales/pl/LC_MESSAGES/iso4217.mo | Bin 9108 -> 9067 bytes .../locales/pl/LC_MESSAGES/iso639-3.mo | Bin 122770 -> 123770 bytes .../locales/pl/LC_MESSAGES/iso639-5.mo | Bin 0 -> 7985 bytes .../locales/ps/LC_MESSAGES/iso3166-1.mo | Bin 7347 -> 7253 bytes .../locales/ps/LC_MESSAGES/iso3166-3.mo | Bin 433 -> 392 bytes .../locales/ps/LC_MESSAGES/iso639-3.mo | Bin 1750 -> 1709 bytes .../locales/pt/LC_MESSAGES/iso15924.mo | Bin 3399 -> 9583 bytes .../locales/pt/LC_MESSAGES/iso3166-1.mo | Bin 23722 -> 23893 bytes .../locales/pt/LC_MESSAGES/iso3166-3.mo | Bin 2869 -> 2828 bytes .../locales/pt/LC_MESSAGES/iso4217.mo | Bin 5380 -> 9339 bytes .../locales/pt/LC_MESSAGES/iso639-3.mo | Bin 9467 -> 15062 bytes .../locales/pt_BR/LC_MESSAGES/iso15924.mo | Bin 3568 -> 10363 bytes .../locales/pt_BR/LC_MESSAGES/iso3166-1.mo | Bin 24033 -> 24071 bytes .../locales/pt_BR/LC_MESSAGES/iso3166-2.mo | Bin 0 -> 858 bytes .../locales/pt_BR/LC_MESSAGES/iso3166-3.mo | Bin 2884 -> 2843 bytes .../locales/pt_BR/LC_MESSAGES/iso4217.mo | Bin 602 -> 9263 bytes .../locales/pt_BR/LC_MESSAGES/iso639-3.mo | Bin 7098 -> 17873 bytes .../locales/pt_BR/LC_MESSAGES/iso639-5.mo | Bin 0 -> 7751 bytes .../locales/ro/LC_MESSAGES/iso15924.mo | Bin 8325 -> 8284 bytes .../locales/ro/LC_MESSAGES/iso3166-1.mo | Bin 22313 -> 22948 bytes .../locales/ro/LC_MESSAGES/iso3166-2.mo | Bin 31597 -> 27706 bytes .../locales/ro/LC_MESSAGES/iso3166-3.mo | Bin 2969 -> 2928 bytes .../locales/ro/LC_MESSAGES/iso4217.mo | Bin 7256 -> 7215 bytes .../locales/ro/LC_MESSAGES/iso639-3.mo | Bin 10780 -> 10902 bytes .../locales/ru/LC_MESSAGES/iso15924.mo | Bin 12700 -> 12713 bytes .../locales/ru/LC_MESSAGES/iso3166-1.mo | Bin 29602 -> 29812 bytes .../locales/ru/LC_MESSAGES/iso3166-2.mo | Bin 0 -> 16170 bytes .../locales/ru/LC_MESSAGES/iso3166-3.mo | Bin 3685 -> 3644 bytes .../locales/ru/LC_MESSAGES/iso4217.mo | Bin 11772 -> 11791 bytes .../locales/ru/LC_MESSAGES/iso639-3.mo | Bin 17713 -> 17756 bytes .../locales/rw/LC_MESSAGES/iso3166-1.mo | Bin 22371 -> 22064 bytes .../locales/rw/LC_MESSAGES/iso3166-3.mo | Bin 602 -> 560 bytes .../locales/rw/LC_MESSAGES/iso4217.mo | Bin 5253 -> 5212 bytes .../locales/rw/LC_MESSAGES/iso639-3.mo | Bin 14278 -> 14237 bytes .../locales/sc/LC_MESSAGES/iso15924.mo | Bin 0 -> 10473 bytes .../locales/sc/LC_MESSAGES/iso3166-1.mo | Bin 6180 -> 24343 bytes .../locales/sc/LC_MESSAGES/iso3166-2.mo | Bin 0 -> 104523 bytes .../locales/sc/LC_MESSAGES/iso3166-3.mo | Bin 0 -> 2941 bytes .../locales/sc/LC_MESSAGES/iso4217.mo | Bin 0 -> 9386 bytes .../locales/sc/LC_MESSAGES/iso639-3.mo | Bin 0 -> 20067 bytes .../locales/sc/LC_MESSAGES/iso639-5.mo | Bin 0 -> 7679 bytes .../locales/sd/LC_MESSAGES/iso3166-1.mo | Bin 3880 -> 3785 bytes .../locales/si/LC_MESSAGES/iso15924.mo | Bin 0 -> 600 bytes .../locales/si/LC_MESSAGES/iso3166-1.mo | Bin 32496 -> 32046 bytes .../locales/si/LC_MESSAGES/iso3166-3.mo | Bin 4020 -> 3979 bytes .../locales/sk/LC_MESSAGES/iso15924.mo | Bin 1823 -> 1782 bytes .../locales/sk/LC_MESSAGES/iso3166-1.mo | Bin 24021 -> 23616 bytes .../locales/sk/LC_MESSAGES/iso3166-2.mo | Bin 18605 -> 13211 bytes .../locales/sk/LC_MESSAGES/iso3166-3.mo | Bin 2847 -> 2806 bytes .../locales/sk/LC_MESSAGES/iso4217.mo | Bin 5158 -> 5117 bytes .../locales/sk/LC_MESSAGES/iso639-3.mo | Bin 11539 -> 11498 bytes .../locales/sl/LC_MESSAGES/iso15924.mo | Bin 6391 -> 6350 bytes .../locales/sl/LC_MESSAGES/iso3166-1.mo | Bin 23016 -> 22737 bytes .../locales/sl/LC_MESSAGES/iso3166-2.mo | Bin 94997 -> 79740 bytes .../locales/sl/LC_MESSAGES/iso3166-3.mo | Bin 2752 -> 2711 bytes .../locales/sl/LC_MESSAGES/iso4217.mo | Bin 7350 -> 7309 bytes .../locales/sl/LC_MESSAGES/iso639-3.mo | Bin 14387 -> 14346 bytes .../locales/so/LC_MESSAGES/iso15924.mo | Bin 0 -> 371 bytes .../locales/so/LC_MESSAGES/iso3166-1.mo | Bin 5888 -> 6095 bytes .../locales/so/LC_MESSAGES/iso3166-2.mo | Bin 0 -> 372 bytes .../locales/so/LC_MESSAGES/iso3166-3.mo | Bin 449 -> 996 bytes .../locales/so/LC_MESSAGES/iso4217.mo | Bin 0 -> 370 bytes .../locales/so/LC_MESSAGES/iso639-3.mo | Bin 0 -> 371 bytes .../locales/son/LC_MESSAGES/iso3166-1.mo | Bin 9432 -> 9312 bytes .../locales/sq/LC_MESSAGES/iso15924.mo | Bin 0 -> 371 bytes .../locales/sq/LC_MESSAGES/iso3166-1.mo | Bin 22341 -> 23872 bytes .../locales/sq/LC_MESSAGES/iso3166-2.mo | Bin 0 -> 372 bytes .../locales/sq/LC_MESSAGES/iso3166-3.mo | Bin 2830 -> 2768 bytes .../locales/sq/LC_MESSAGES/iso4217.mo | Bin 0 -> 370 bytes .../locales/sq/LC_MESSAGES/iso639-3.mo | Bin 0 -> 371 bytes .../locales/sq/LC_MESSAGES/iso639-5.mo | Bin 0 -> 371 bytes .../locales/sr/LC_MESSAGES/iso15924.mo | Bin 11540 -> 12875 bytes .../locales/sr/LC_MESSAGES/iso3166-1.mo | Bin 28738 -> 29018 bytes .../locales/sr/LC_MESSAGES/iso3166-2.mo | Bin 181678 -> 145314 bytes .../locales/sr/LC_MESSAGES/iso3166-3.mo | Bin 3554 -> 3513 bytes .../locales/sr/LC_MESSAGES/iso4217.mo | Bin 8953 -> 9651 bytes .../locales/sr/LC_MESSAGES/iso639-3.mo | Bin 15763 -> 17095 bytes .../locales/sr/LC_MESSAGES/iso639-5.mo | Bin 0 -> 4031 bytes .../locales/sr@latin/LC_MESSAGES/iso15924.mo | Bin 9434 -> 10451 bytes .../locales/sr@latin/LC_MESSAGES/iso3166-1.mo | Bin 23092 -> 23328 bytes .../locales/sr@latin/LC_MESSAGES/iso3166-2.mo | Bin 152567 -> 122440 bytes .../locales/sr@latin/LC_MESSAGES/iso3166-3.mo | Bin 2867 -> 2826 bytes .../locales/sr@latin/LC_MESSAGES/iso4217.mo | Bin 7363 -> 7908 bytes .../locales/sr@latin/LC_MESSAGES/iso639-3.mo | Bin 13380 -> 14420 bytes .../locales/sr@latin/LC_MESSAGES/iso639-5.mo | Bin 0 -> 3277 bytes .../locales/sv/LC_MESSAGES/iso15924.mo | Bin 10331 -> 10290 bytes .../locales/sv/LC_MESSAGES/iso3166-1.mo | Bin 23444 -> 23439 bytes .../locales/sv/LC_MESSAGES/iso3166-2.mo | Bin 201706 -> 163817 bytes .../locales/sv/LC_MESSAGES/iso3166-3.mo | Bin 2674 -> 2720 bytes .../locales/sv/LC_MESSAGES/iso4217.mo | Bin 9214 -> 9246 bytes .../locales/sv/LC_MESSAGES/iso639-3.mo | Bin 400832 -> 405289 bytes .../locales/sv/LC_MESSAGES/iso639-5.mo | Bin 0 -> 7469 bytes .../locales/sw/LC_MESSAGES/iso3166-1.mo | Bin 8026 -> 7984 bytes .../locales/sw/LC_MESSAGES/iso3166-3.mo | Bin 497 -> 456 bytes .../locales/ta/LC_MESSAGES/iso3166-1.mo | Bin 32666 -> 32125 bytes .../locales/ta/LC_MESSAGES/iso3166-3.mo | Bin 4249 -> 4208 bytes .../locales/ta/LC_MESSAGES/iso639-3.mo | Bin 446703 -> 446660 bytes .../locales/te/LC_MESSAGES/iso3166-1.mo | Bin 35346 -> 35466 bytes .../locales/te/LC_MESSAGES/iso3166-3.mo | Bin 4399 -> 4358 bytes .../locales/tg/LC_MESSAGES/iso3166-1.mo | Bin 28699 -> 28628 bytes .../locales/th/LC_MESSAGES/iso15924.mo | Bin 11625 -> 11584 bytes .../locales/th/LC_MESSAGES/iso3166-1.mo | Bin 34230 -> 33780 bytes .../locales/th/LC_MESSAGES/iso3166-2.mo | Bin 111940 -> 83632 bytes .../locales/th/LC_MESSAGES/iso3166-3.mo | Bin 4037 -> 3996 bytes .../locales/th/LC_MESSAGES/iso4217.mo | Bin 10508 -> 10467 bytes .../locales/th/LC_MESSAGES/iso639-3.mo | Bin 54066 -> 54025 bytes .../locales/ti/LC_MESSAGES/iso3166-1.mo | Bin 6818 -> 11366 bytes .../locales/ti/LC_MESSAGES/iso3166-3.mo | Bin 518 -> 477 bytes .../locales/ti/LC_MESSAGES/iso639-3.mo | Bin 5899 -> 5858 bytes .../locales/tig/LC_MESSAGES/iso3166-1.mo | Bin 5810 -> 5769 bytes .../locales/tig/LC_MESSAGES/iso3166-3.mo | Bin 516 -> 475 bytes .../locales/tig/LC_MESSAGES/iso639-3.mo | Bin 5738 -> 5697 bytes .../locales/tk/LC_MESSAGES/iso3166-1.mo | Bin 18283 -> 18144 bytes .../locales/tk/LC_MESSAGES/iso3166-3.mo | Bin 457 -> 416 bytes .../locales/tl/LC_MESSAGES/iso3166-1.mo | Bin 21804 -> 21496 bytes .../locales/tl/LC_MESSAGES/iso3166-3.mo | Bin 518 -> 477 bytes .../locales/tr/LC_MESSAGES/iso15924.mo | Bin 3483 -> 10236 bytes .../locales/tr/LC_MESSAGES/iso3166-1.mo | Bin 23181 -> 23601 bytes .../locales/tr/LC_MESSAGES/iso3166-2.mo | Bin 3524 -> 77217 bytes .../locales/tr/LC_MESSAGES/iso3166-3.mo | Bin 2771 -> 2771 bytes .../locales/tr/LC_MESSAGES/iso4217.mo | Bin 5508 -> 9113 bytes .../locales/tr/LC_MESSAGES/iso639-3.mo | Bin 309547 -> 349629 bytes .../locales/tt/LC_MESSAGES/iso3166-1.mo | Bin 22181 -> 21915 bytes .../locales/tt/LC_MESSAGES/iso3166-3.mo | Bin 542 -> 501 bytes .../locales/tt/LC_MESSAGES/iso639-3.mo | Bin 7247 -> 7206 bytes .../tt@iqtelif/LC_MESSAGES/iso3166-1.mo | Bin 18525 -> 18299 bytes .../tt@iqtelif/LC_MESSAGES/iso3166-3.mo | Bin 533 -> 492 bytes .../tt@iqtelif/LC_MESSAGES/iso639-3.mo | Bin 6422 -> 6381 bytes .../locales/tzm/LC_MESSAGES/iso15924.mo | Bin 0 -> 798 bytes .../locales/tzm/LC_MESSAGES/iso3166-1.mo | Bin 0 -> 608 bytes .../locales/tzm/LC_MESSAGES/iso4217.mo | Bin 0 -> 371 bytes .../locales/ug/LC_MESSAGES/iso3166-1.mo | Bin 29395 -> 29923 bytes .../locales/ug/LC_MESSAGES/iso3166-3.mo | Bin 3637 -> 3596 bytes .../locales/uk/LC_MESSAGES/iso15924.mo | Bin 12735 -> 12713 bytes .../locales/uk/LC_MESSAGES/iso3166-1.mo | Bin 29765 -> 29791 bytes .../locales/uk/LC_MESSAGES/iso3166-2.mo | Bin 251390 -> 265018 bytes .../locales/uk/LC_MESSAGES/iso3166-3.mo | Bin 3686 -> 3645 bytes .../locales/uk/LC_MESSAGES/iso4217.mo | Bin 11725 -> 11702 bytes .../locales/uk/LC_MESSAGES/iso639-3.mo | Bin 505203 -> 505163 bytes .../locales/uk/LC_MESSAGES/iso639-5.mo | Bin 0 -> 9363 bytes .../locales/ur/LC_MESSAGES/iso3166-1.mo | Bin 11830 -> 11743 bytes .../locales/uz/LC_MESSAGES/iso3166-1.mo | Bin 8971 -> 8894 bytes .../locales/ve/LC_MESSAGES/iso3166-1.mo | Bin 8018 -> 7943 bytes .../locales/ve/LC_MESSAGES/iso3166-2.mo | Bin 1003 -> 924 bytes .../locales/ve/LC_MESSAGES/iso3166-3.mo | Bin 555 -> 514 bytes .../locales/ve/LC_MESSAGES/iso639-3.mo | Bin 2038 -> 1997 bytes .../locales/vi/LC_MESSAGES/iso15924.mo | Bin 7498 -> 7457 bytes .../locales/vi/LC_MESSAGES/iso3166-1.mo | Bin 25123 -> 24794 bytes .../locales/vi/LC_MESSAGES/iso3166-2.mo | Bin 171173 -> 135733 bytes .../locales/vi/LC_MESSAGES/iso3166-3.mo | Bin 2834 -> 2793 bytes .../locales/vi/LC_MESSAGES/iso4217.mo | Bin 8549 -> 8508 bytes .../locales/vi/LC_MESSAGES/iso639-3.mo | Bin 16966 -> 16925 bytes .../locales/wa/LC_MESSAGES/iso3166-1.mo | Bin 22844 -> 22514 bytes .../locales/wa/LC_MESSAGES/iso3166-2.mo | Bin 3025 -> 2646 bytes .../locales/wa/LC_MESSAGES/iso3166-3.mo | Bin 2818 -> 2777 bytes .../locales/wa/LC_MESSAGES/iso639-3.mo | Bin 12981 -> 12940 bytes .../locales/wal/LC_MESSAGES/iso3166-1.mo | Bin 5811 -> 5770 bytes .../locales/wal/LC_MESSAGES/iso3166-3.mo | Bin 517 -> 476 bytes .../locales/wo/LC_MESSAGES/iso3166-1.mo | Bin 22099 -> 21792 bytes .../locales/wo/LC_MESSAGES/iso3166-3.mo | Bin 2696 -> 2655 bytes .../locales/xh/LC_MESSAGES/iso3166-1.mo | Bin 2892 -> 2851 bytes .../locales/xh/LC_MESSAGES/iso3166-3.mo | Bin 463 -> 422 bytes .../locales/xh/LC_MESSAGES/iso639-3.mo | Bin 2569 -> 2528 bytes .../locales/yo/LC_MESSAGES/iso3166-1.mo | Bin 11201 -> 11063 bytes .../locales/zh_CN/LC_MESSAGES/iso15924.mo | Bin 5620 -> 5672 bytes .../locales/zh_CN/LC_MESSAGES/iso3166-1.mo | Bin 22887 -> 23298 bytes .../locales/zh_CN/LC_MESSAGES/iso3166-2.mo | Bin 136925 -> 116903 bytes .../locales/zh_CN/LC_MESSAGES/iso3166-3.mo | Bin 2653 -> 2612 bytes .../locales/zh_CN/LC_MESSAGES/iso4217.mo | Bin 7288 -> 9093 bytes .../locales/zh_CN/LC_MESSAGES/iso639-3.mo | Bin 14047 -> 14531 bytes .../locales/zh_HK/LC_MESSAGES/iso15924.mo | Bin 3442 -> 3594 bytes .../locales/zh_HK/LC_MESSAGES/iso3166-1.mo | Bin 20308 -> 23163 bytes .../locales/zh_HK/LC_MESSAGES/iso3166-3.mo | Bin 574 -> 1637 bytes .../locales/zh_HK/LC_MESSAGES/iso4217.mo | Bin 5360 -> 6467 bytes .../locales/zh_Hans/LC_MESSAGES/iso639-5.mo | Bin 0 -> 634 bytes .../locales/zh_Hant/LC_MESSAGES/iso639-5.mo | Bin 0 -> 7424 bytes .../locales/zh_TW/LC_MESSAGES/iso15924.mo | Bin 9513 -> 10629 bytes .../locales/zh_TW/LC_MESSAGES/iso3166-1.mo | Bin 23297 -> 23204 bytes .../locales/zh_TW/LC_MESSAGES/iso3166-2.mo | Bin 20469 -> 18085 bytes .../locales/zh_TW/LC_MESSAGES/iso3166-3.mo | Bin 2667 -> 2695 bytes .../locales/zh_TW/LC_MESSAGES/iso4217.mo | Bin 7732 -> 9457 bytes .../locales/zh_TW/LC_MESSAGES/iso639-3.mo | Bin 14706 -> 32964 bytes .../locales/zu/LC_MESSAGES/iso3166-1.mo | Bin 5229 -> 5150 bytes .../locales/zu/LC_MESSAGES/iso3166-3.mo | Bin 456 -> 415 bytes .../locales/zu/LC_MESSAGES/iso639-3.mo | Bin 2637 -> 2596 bytes libs/pycountry/tests/test_general.py | 173 - libs/pyemitter.py | 235 - libs/pyga/requests.py | 2 +- libs/pygments/__init__.py | 3 +- libs/pygments/cmdline.py | 69 +- libs/pygments/formatters/_mapping.py | 2 + libs/pygments/formatters/groff.py | 168 + libs/pygments/formatters/html.py | 47 +- libs/pygments/formatters/irc.py | 4 +- libs/pygments/formatters/pangomarkup.py | 83 + libs/pygments/formatters/terminal.py | 2 - libs/pygments/formatters/terminal256.py | 4 +- libs/pygments/lexer.py | 17 +- libs/pygments/lexers/_csound_builtins.py | 69 +- libs/pygments/lexers/_julia_builtins.py | 411 + libs/pygments/lexers/_lilypond_builtins.py | 4803 +++ libs/pygments/lexers/_mapping.py | 140 +- libs/pygments/lexers/actionscript.py | 30 +- libs/pygments/lexers/algebra.py | 5 +- libs/pygments/lexers/ambient.py | 10 +- libs/pygments/lexers/amdgpu.py | 13 +- libs/pygments/lexers/ampl.py | 12 +- libs/pygments/lexers/apdlexer.py | 447 + libs/pygments/lexers/apl.py | 13 +- libs/pygments/lexers/archetype.py | 40 +- libs/pygments/lexers/arrow.py | 14 +- libs/pygments/lexers/asc.py | 51 + libs/pygments/lexers/asm.py | 249 +- libs/pygments/lexers/automation.py | 2 +- libs/pygments/lexers/bare.py | 28 +- libs/pygments/lexers/basic.py | 72 +- libs/pygments/lexers/bdd.py | 56 + libs/pygments/lexers/bibtex.py | 12 +- libs/pygments/lexers/boa.py | 4 +- libs/pygments/lexers/business.py | 22 +- libs/pygments/lexers/c_cpp.py | 110 +- libs/pygments/lexers/c_like.py | 121 +- libs/pygments/lexers/capnproto.py | 5 +- libs/pygments/lexers/cddl.py | 14 +- libs/pygments/lexers/chapel.py | 92 +- libs/pygments/lexers/clean.py | 12 +- libs/pygments/lexers/configs.py | 359 +- libs/pygments/lexers/console.py | 7 +- libs/pygments/lexers/crystal.py | 34 +- libs/pygments/lexers/csound.py | 36 +- libs/pygments/lexers/css.py | 52 +- libs/pygments/lexers/d.py | 19 +- libs/pygments/lexers/dalvik.py | 18 +- libs/pygments/lexers/data.py | 86 +- libs/pygments/lexers/devicetree.py | 12 +- libs/pygments/lexers/diff.py | 34 +- libs/pygments/lexers/dotnet.py | 132 +- libs/pygments/lexers/dsls.py | 89 +- libs/pygments/lexers/dylan.py | 18 +- libs/pygments/lexers/ecl.py | 6 +- libs/pygments/lexers/eiffel.py | 16 +- libs/pygments/lexers/elm.py | 15 +- libs/pygments/lexers/elpi.py | 143 + libs/pygments/lexers/erlang.py | 30 +- libs/pygments/lexers/esoteric.py | 30 +- libs/pygments/lexers/ezhil.py | 8 +- libs/pygments/lexers/factor.py | 178 +- libs/pygments/lexers/fantom.py | 58 +- libs/pygments/lexers/felix.py | 27 +- libs/pygments/lexers/floscript.py | 11 +- libs/pygments/lexers/forth.py | 11 +- libs/pygments/lexers/fortran.py | 16 +- libs/pygments/lexers/futhark.py | 15 +- libs/pygments/lexers/gcodelexer.py | 35 + libs/pygments/lexers/gdscript.py | 21 +- libs/pygments/lexers/go.py | 12 +- libs/pygments/lexers/grammar_notation.py | 24 +- libs/pygments/lexers/graph.py | 45 +- libs/pygments/lexers/graphics.py | 48 +- libs/pygments/lexers/graphviz.py | 9 +- libs/pygments/lexers/gsql.py | 91 + libs/pygments/lexers/haskell.py | 110 +- libs/pygments/lexers/haxe.py | 16 +- libs/pygments/lexers/hdl.py | 63 +- libs/pygments/lexers/hexdump.py | 40 +- libs/pygments/lexers/html.py | 16 +- libs/pygments/lexers/installers.py | 50 +- libs/pygments/lexers/javascript.py | 272 +- libs/pygments/lexers/jslt.py | 94 + libs/pygments/lexers/julia.py | 338 +- libs/pygments/lexers/jvm.py | 468 +- libs/pygments/lexers/kuin.py | 301 + libs/pygments/lexers/lilypond.py | 196 + libs/pygments/lexers/lisp.py | 91 +- libs/pygments/lexers/make.py | 12 +- libs/pygments/lexers/markup.py | 10 +- libs/pygments/lexers/matlab.py | 36 +- libs/pygments/lexers/maxima.py | 84 + libs/pygments/lexers/meson.py | 155 + libs/pygments/lexers/mime.py | 27 +- libs/pygments/lexers/ml.py | 2 +- libs/pygments/lexers/nimrod.py | 2 +- libs/pygments/lexers/objective.py | 6 +- libs/pygments/lexers/parsers.py | 2 +- libs/pygments/lexers/procfile.py | 43 + libs/pygments/lexers/prolog.py | 2 +- libs/pygments/lexers/promql.py | 2 +- libs/pygments/lexers/python.py | 44 +- libs/pygments/lexers/r.py | 2 +- libs/pygments/lexers/resource.py | 2 +- libs/pygments/lexers/rita.py | 44 + libs/pygments/lexers/rnc.py | 2 +- libs/pygments/lexers/robotframework.py | 2 +- libs/pygments/lexers/ruby.py | 4 +- libs/pygments/lexers/rust.py | 19 +- libs/pygments/lexers/savi.py | 159 + libs/pygments/lexers/scripting.py | 24 +- libs/pygments/lexers/sgf.py | 7 +- libs/pygments/lexers/shell.py | 20 +- libs/pygments/lexers/smithy.py | 79 + libs/pygments/lexers/smv.py | 2 +- libs/pygments/lexers/sophia.py | 103 + libs/pygments/lexers/special.py | 17 +- libs/pygments/lexers/spice.py | 60 + libs/pygments/lexers/sql.py | 25 +- libs/pygments/lexers/srcinfo.py | 57 + libs/pygments/lexers/supercollider.py | 2 +- libs/pygments/lexers/tcl.py | 6 +- libs/pygments/lexers/teal.py | 87 + libs/pygments/lexers/templates.py | 38 +- libs/pygments/lexers/teraterm.py | 2 +- libs/pygments/lexers/testing.py | 5 +- libs/pygments/lexers/textedit.py | 46 +- libs/pygments/lexers/theorem.py | 23 +- libs/pygments/lexers/thingsdb.py | 116 + libs/pygments/lexers/tnt.py | 40 +- libs/pygments/lexers/trafficscript.py | 4 +- libs/pygments/lexers/webassembly.py | 119 + libs/pygments/lexers/webmisc.py | 14 +- libs/pygments/lexers/x10.py | 5 +- libs/pygments/regexopt.py | 2 +- libs/pygments/style.py | 8 +- libs/pygments/styles/__init__.py | 6 + libs/pygments/styles/default.py | 24 +- libs/pygments/styles/dracula.py | 105 + libs/pygments/styles/friendly.py | 1 + libs/pygments/styles/friendly_grayscale.py | 76 + libs/pygments/styles/gruvbox.py | 109 + libs/pygments/styles/lilypond.py | 59 + libs/pygments/styles/monokai.py | 4 +- libs/pygments/styles/native.py | 1 + libs/pygments/styles/onedark.py | 59 + libs/pygments/styles/paraiso_dark.py | 3 - libs/pygments/styles/paraiso_light.py | 3 - libs/pygments/styles/rrt.py | 3 +- libs/pygments/styles/sas.py | 2 +- libs/pygments/styles/stata_dark.py | 6 +- libs/pygments/styles/tango.py | 2 +- libs/pygments/unistring.py | 2 - libs/pyjsparser/__init__.py | 5 +- libs/pyjsparser/parser.py | 1423 +- libs/pyjsparser/pyjsparserdata.py | 492 +- libs/pyjsparser/std_nodes.py | 152 +- libs/pymediainfo/__init__.py | 528 + libs/pyparsing.py | 7107 ---- libs/pyparsing/__init__.py | 328 + libs/pyparsing/actions.py | 207 + libs/pyparsing/common.py | 424 + libs/pyparsing/core.py | 5789 ++++ libs/pyparsing/diagram/__init__.py | 593 + libs/pyparsing/diagram/template.jinja2 | 26 + libs/pyparsing/exceptions.py | 267 + libs/pyparsing/helpers.py | 1069 + libs/pyparsing/results.py | 760 + libs/pyparsing/testing.py | 331 + libs/pyparsing/unicode.py | 332 + libs/pyparsing/util.py | 235 + libs/pysrt/commands.py | 12 +- libs/pysrt/srtfile.py | 2 +- libs/pysubs2/cli.py | 2 +- libs/pysubs2/common.py | 2 +- libs/pysubs2/ssafile.py | 49 +- libs/pysubs2/subrip.py | 21 +- libs/pysubs2/substation.py | 30 +- libs/pysubs2/tmp.py | 21 +- libs/pysubs2/webvtt.py | 8 +- libs/python_anticaptcha/__init__.py | 35 +- libs/python_anticaptcha/base.py | 178 +- libs/python_anticaptcha/compat.py | 15 + libs/python_anticaptcha/exceptions.py | 12 +- libs/python_anticaptcha/fields.py | 74 +- libs/python_anticaptcha/proxy.py | 28 - libs/python_anticaptcha/tasks.py | 181 +- libs/pytz/__init__.py | 7 +- libs/pytz/tests/test_tzinfo.py | 4 +- libs/pytz/zoneinfo/Africa/Accra | Bin 1060 -> 148 bytes libs/pytz/zoneinfo/America/Anguilla | Bin 148 -> 246 bytes libs/pytz/zoneinfo/America/Antigua | Bin 148 -> 246 bytes libs/pytz/zoneinfo/America/Aruba | Bin 186 -> 246 bytes libs/pytz/zoneinfo/America/Atikokan | Bin 336 -> 182 bytes libs/pytz/zoneinfo/America/Barbados | Bin 314 -> 436 bytes libs/pytz/zoneinfo/America/Blanc-Sablon | Bin 298 -> 246 bytes libs/pytz/zoneinfo/America/Coral_Harbour | Bin 336 -> 182 bytes libs/pytz/zoneinfo/America/Creston | Bin 208 -> 328 bytes libs/pytz/zoneinfo/America/Curacao | Bin 186 -> 246 bytes libs/pytz/zoneinfo/America/Dominica | Bin 148 -> 246 bytes libs/pytz/zoneinfo/America/Grenada | Bin 148 -> 246 bytes libs/pytz/zoneinfo/America/Guadeloupe | Bin 148 -> 246 bytes libs/pytz/zoneinfo/America/Guyana | Bin 236 -> 262 bytes libs/pytz/zoneinfo/America/Kralendijk | Bin 186 -> 246 bytes libs/pytz/zoneinfo/America/Lower_Princes | Bin 186 -> 246 bytes libs/pytz/zoneinfo/America/Marigot | Bin 148 -> 246 bytes libs/pytz/zoneinfo/America/Montserrat | Bin 148 -> 246 bytes libs/pytz/zoneinfo/America/Nassau | Bin 2388 -> 3494 bytes libs/pytz/zoneinfo/America/Port_of_Spain | Bin 148 -> 246 bytes libs/pytz/zoneinfo/America/St_Barthelemy | Bin 148 -> 246 bytes libs/pytz/zoneinfo/America/St_Kitts | Bin 148 -> 246 bytes libs/pytz/zoneinfo/America/St_Lucia | Bin 148 -> 246 bytes libs/pytz/zoneinfo/America/St_Thomas | Bin 148 -> 246 bytes libs/pytz/zoneinfo/America/St_Vincent | Bin 148 -> 246 bytes libs/pytz/zoneinfo/America/Tortola | Bin 148 -> 246 bytes libs/pytz/zoneinfo/America/Virgin | Bin 148 -> 246 bytes libs/pytz/zoneinfo/Antarctica/DumontDUrville | Bin 194 -> 186 bytes libs/pytz/zoneinfo/Antarctica/Syowa | Bin 165 -> 165 bytes libs/pytz/zoneinfo/Asia/Amman | Bin 1853 -> 1853 bytes libs/pytz/zoneinfo/Atlantic/Azores | Bin 3484 -> 3512 bytes libs/pytz/zoneinfo/Atlantic/Madeira | Bin 3475 -> 3503 bytes libs/pytz/zoneinfo/Europe/Lisbon | Bin 3469 -> 3497 bytes libs/pytz/zoneinfo/Pacific/Apia | Bin 1097 -> 612 bytes libs/pytz/zoneinfo/Pacific/Enderbury | Bin 234 -> 234 bytes libs/pytz/zoneinfo/Pacific/Kanton | Bin 0 -> 234 bytes libs/pytz/zoneinfo/Pacific/Niue | Bin 241 -> 203 bytes libs/pytz/zoneinfo/Pacific/Rarotonga | Bin 577 -> 603 bytes libs/pytz/zoneinfo/Pacific/Tongatapu | Bin 372 -> 372 bytes libs/pytz/zoneinfo/Portugal | Bin 3469 -> 3497 bytes libs/pytz/zoneinfo/leapseconds | 8 +- libs/pytz/zoneinfo/tzdata.zi | 134 +- libs/pytz/zoneinfo/zone.tab | 9 +- libs/pytz/zoneinfo/zone1970.tab | 29 +- libs/pytz_deprecation_shim/__init__.py | 34 + libs/pytz_deprecation_shim/_common.py | 13 + libs/pytz_deprecation_shim/_compat.py | 15 + libs/pytz_deprecation_shim/_compat_py2.py | 43 + libs/pytz_deprecation_shim/_compat_py3.py | 58 + libs/pytz_deprecation_shim/_exceptions.py | 75 + libs/pytz_deprecation_shim/_impl.py | 296 + libs/pytz_deprecation_shim/helpers.py | 90 + libs/rarfile.py | 1822 +- libs/rebulk/__version__.py | 2 +- libs/rebulk/builder.py | 45 +- libs/rebulk/chain.py | 8 +- libs/rebulk/debug.py | 2 +- libs/rebulk/loose.py | 4 +- libs/rebulk/match.py | 27 +- libs/rebulk/pattern.py | 12 +- libs/rebulk/rules.py | 6 +- libs/rebulk/test/test_rebulk.py | 56 +- libs/rebulk/utils.py | 7 +- libs/requests/__init__.py | 39 +- libs/requests/__version__.py | 6 +- libs/requests/adapters.py | 9 +- libs/requests/api.py | 2 - libs/requests/compat.py | 13 +- libs/requests/exceptions.py | 14 +- libs/requests/help.py | 20 +- libs/requests/models.py | 37 +- libs/requests/packages.py | 14 +- libs/requests/sessions.py | 24 +- libs/requests/utils.py | 90 +- libs/requests_oauthlib/__init__.py | 13 +- .../compliance_fixes/__init__.py | 1 + .../compliance_fixes/douban.py | 9 +- .../compliance_fixes/facebook.py | 16 +- .../compliance_fixes/fitbit.py | 11 +- .../compliance_fixes/instagram.py | 26 + .../compliance_fixes/linkedin.py | 15 +- .../compliance_fixes/mailchimp.py | 17 +- .../compliance_fixes/plentymarkets.py | 12 +- .../compliance_fixes/slack.py | 4 +- .../compliance_fixes/weibo.py | 10 +- libs/requests_oauthlib/oauth1_auth.py | 86 +- libs/requests_oauthlib/oauth1_session.py | 124 +- libs/requests_oauthlib/oauth2_auth.py | 5 +- libs/requests_oauthlib/oauth2_session.py | 390 +- libs/requests_toolbelt/__init__.py | 2 +- libs/requests_toolbelt/_compat.py | 4 +- libs/requests_toolbelt/adapters/__init__.py | 2 +- libs/requests_toolbelt/adapters/appengine.py | 2 +- libs/requests_toolbelt/adapters/x509.py | 6 +- libs/requests_toolbelt/auth/handler.py | 2 +- .../auth/http_proxy_digest.py | 2 +- .../requests_toolbelt/downloadutils/stream.py | 5 +- libs/requests_toolbelt/multipart/__init__.py | 2 +- libs/requests_toolbelt/multipart/decoder.py | 2 +- libs/requests_toolbelt/multipart/encoder.py | 6 +- libs/requests_toolbelt/sessions.py | 4 +- libs/rich/__init__.py | 68 +- libs/rich/__main__.py | 33 +- libs/rich/_emoji_replace.py | 29 +- libs/rich/_extension.py | 10 + libs/rich/_inspect.py | 29 +- libs/rich/_log_render.py | 16 +- libs/rich/_lru_cache.py | 2 +- libs/rich/_ratio.py | 13 +- libs/rich/_timer.py | 3 +- libs/rich/_windows.py | 13 +- libs/rich/align.py | 56 +- libs/rich/ansi.py | 2 +- libs/rich/bar.py | 4 +- libs/rich/box.py | 9 +- libs/rich/cells.py | 69 +- libs/rich/color.py | 26 +- libs/rich/color_triplet.py | 2 +- libs/rich/columns.py | 12 +- libs/rich/console.py | 524 +- libs/rich/containers.py | 12 +- libs/rich/control.py | 18 +- libs/rich/default_styles.py | 39 +- libs/rich/emoji.py | 34 +- libs/rich/file_proxy.py | 2 +- libs/rich/filesize.py | 37 +- libs/rich/highlighter.py | 24 +- libs/rich/json.py | 140 + libs/rich/jupyter.py | 26 +- libs/rich/layout.py | 22 +- libs/rich/live.py | 92 +- libs/rich/live_render.py | 18 +- libs/rich/logging.py | 28 +- libs/rich/markdown.py | 22 +- libs/rich/markup.py | 101 +- libs/rich/measure.py | 19 +- libs/rich/padding.py | 10 +- libs/rich/pager.py | 4 +- libs/rich/panel.py | 52 +- libs/rich/pretty.py | 431 +- libs/rich/progress.py | 84 +- libs/rich/progress_bar.py | 6 +- libs/rich/prompt.py | 36 +- libs/rich/protocol.py | 34 +- libs/rich/repr.py | 157 +- libs/rich/scope.py | 10 +- libs/rich/screen.py | 25 +- libs/rich/segment.py | 432 +- libs/rich/spinner.py | 82 +- libs/rich/status.py | 43 +- libs/rich/style.py | 216 +- libs/rich/syntax.py | 79 +- libs/rich/table.py | 227 +- libs/rich/tabulate.py | 10 +- libs/rich/terminal_theme.py | 4 +- libs/rich/text.py | 259 +- libs/rich/theme.py | 8 +- libs/rich/traceback.py | 183 +- libs/rich/tree.py | 61 +- libs/six.py | 104 +- libs/smmap/__init__.py | 11 - libs/smmap/buf.py | 166 - libs/smmap/exc.py | 11 - libs/smmap/mman.py | 590 - libs/smmap/test/lib.py | 72 - libs/smmap/test/test_buf.py | 128 - libs/smmap/test/test_mman.py | 226 - libs/smmap/test/test_tutorial.py | 81 - libs/smmap/test/test_util.py | 105 - libs/smmap/util.py | 276 - libs/socketio/__init__.py | 4 +- libs/socketio/asyncio_aiopika_manager.py | 2 +- libs/socketio/asyncio_client.py | 86 +- libs/socketio/asyncio_namespace.py | 34 +- libs/socketio/asyncio_pubsub_manager.py | 60 +- libs/socketio/asyncio_redis_manager.py | 90 +- libs/socketio/asyncio_server.py | 69 +- libs/socketio/base_manager.py | 21 +- libs/socketio/client.py | 104 +- libs/socketio/kafka_manager.py | 12 +- libs/socketio/msgpack_packet.py | 18 + libs/socketio/namespace.py | 37 +- libs/socketio/packet.py | 47 +- libs/socketio/pubsub_manager.py | 6 +- libs/socketio/redis_manager.py | 5 +- libs/socketio/server.py | 92 +- libs/socks.py | 6 +- libs/sockshandler.py | 79 - libs/soupsieve/__init__.py | 109 +- libs/soupsieve/__meta__.py | 28 +- libs/soupsieve/css_match.py | 538 +- libs/soupsieve/css_parser.py | 448 +- libs/soupsieve/css_types.py | 233 +- libs/soupsieve/pretty.py | 137 + .../__init__.py => soupsieve/py.typed} | 0 libs/soupsieve/util.py | 98 +- libs/srt.py | 25 +- libs/srt_tools/srt | 57 + libs/srt_tools/srt-deduplicate | 96 + libs/srt_tools/srt-fixed-timeshift | 47 + libs/srt_tools/srt-linear-timeshift | 105 + libs/srt_tools/srt-lines-matching | 85 + libs/srt_tools/srt-mux | 112 + libs/srt_tools/srt-normalise | 28 + libs/srt_tools/srt-play | 59 + libs/srt_tools/srt-process | 57 + libs/srt_tools/utils.py | 24 +- libs/stevedore/__init__.py | 1 - libs/stevedore/_cache.py | 203 + libs/stevedore/dispatch.py | 2 +- libs/stevedore/driver.py | 15 +- libs/stevedore/enabled.py | 2 +- libs/stevedore/example/base.py | 20 +- libs/stevedore/example/load_as_driver.py | 16 +- libs/stevedore/example/load_as_extension.py | 16 +- libs/stevedore/example/setup.py | 21 +- libs/stevedore/example/simple.py | 16 +- libs/stevedore/example2/fields.py | 15 + libs/stevedore/example2/setup.py | 21 +- libs/stevedore/extension.py | 95 +- libs/stevedore/hook.py | 4 +- libs/stevedore/named.py | 4 +- libs/stevedore/sphinxext.py | 43 +- libs/stevedore/tests/test_cache.py | 56 + libs/stevedore/tests/test_callback.py | 5 +- libs/stevedore/tests/test_dispatch.py | 2 +- libs/stevedore/tests/test_driver.py | 13 +- libs/stevedore/tests/test_extension.py | 77 +- libs/stevedore/tests/test_named.py | 4 +- libs/stevedore/tests/test_sphinxext.py | 20 +- libs/stevedore/tests/test_test_manager.py | 16 +- libs/stevedore/tests/utils.py | 4 +- libs/subliminal_patch/pitcher.py | 17 +- .../providers/embeddedsubtitles.py | 3 +- .../providers/subtitrarinoi.py | 5 - libs/subliminal_patch/providers/titlovi.py | 3 - libs/subliminal_patch/providers/titrari.py | 3 - libs/subzero/lib/json.py | 5 +- libs/tld/__init__.py | 24 +- libs/tld/base.py | 70 +- libs/tld/conf.py | 14 +- libs/tld/defaults.py | 10 +- libs/tld/exceptions.py | 22 +- libs/tld/helpers.py | 16 +- .../host/dom/__init__.py => tld/py.typed} | 0 libs/tld/registry.py | 53 +- .../effective_tld_names_public_only.dat.txt | 13726 ++++++++ .../effective_tld_names-2013-04-22.dat.txt | 4418 +++ .../effective_tld_names-2015-07-19.dat.txt | 10641 ++++++ .../effective_tld_names-2015-11-22.dat.txt | 11982 +++++++ libs/tld/result.py | 34 +- libs/tld/tests/__init__.py | 2 +- libs/tld/tests/base.py | 28 +- libs/tld/tests/test_commands.py | 20 +- libs/tld/tests/test_core.py | 812 +- libs/tld/tests/test_registry.py | 14 + libs/tld/trie.py | 16 +- libs/tld/utils.py | 240 +- libs/twine/__init__.py | 38 - libs/twine/__main__.py | 53 - libs/twine/auth.py | 100 - libs/twine/cli.py | 71 - libs/twine/commands/__init__.py | 48 - libs/twine/commands/check.py | 167 - libs/twine/commands/register.py | 63 - libs/twine/commands/upload.py | 154 - libs/twine/exceptions.py | 123 - libs/twine/package.py | 291 - libs/twine/repository.py | 264 - libs/twine/settings.py | 351 - libs/twine/utils.py | 297 - libs/twine/wheel.py | 91 - libs/twine/wininst.py | 61 - libs/typing_extensions.py | 2268 +- libs/tzlocal/__init__.py | 14 +- libs/tzlocal/unix.py | 219 +- libs/tzlocal/utils.py | 91 +- libs/tzlocal/win32.py | 119 +- libs/tzlocal/windows_tz.py | 20 +- libs/urllib3/__init__.py | 72 +- libs/urllib3/_collections.py | 46 +- libs/urllib3/_version.py | 2 + libs/urllib3/connection.py | 458 +- libs/urllib3/connectionpool.py | 601 +- libs/urllib3/contrib/_appengine_environ.py | 26 +- .../contrib/_securetransport/bindings.py | 306 +- .../contrib/_securetransport/low_level.py | 127 +- libs/urllib3/contrib/appengine.py | 131 +- libs/urllib3/contrib/ntlmpool.py | 105 +- libs/urllib3/contrib/pyopenssl.py | 174 +- libs/urllib3/contrib/securetransport.py | 237 +- libs/urllib3/contrib/socks.py | 121 +- libs/urllib3/exceptions.py | 173 +- libs/urllib3/fields.py | 98 +- libs/urllib3/filepost.py | 18 +- libs/urllib3/packages/__init__.py | 5 - libs/urllib3/packages/backports/makefile.py | 10 +- libs/urllib3/packages/rfc3986/__init__.py | 56 - libs/urllib3/packages/rfc3986/_mixin.py | 353 - libs/urllib3/packages/rfc3986/abnf_regexp.py | 267 - libs/urllib3/packages/rfc3986/api.py | 106 - libs/urllib3/packages/rfc3986/builder.py | 298 - libs/urllib3/packages/rfc3986/compat.py | 54 - libs/urllib3/packages/rfc3986/exceptions.py | 118 - libs/urllib3/packages/rfc3986/iri.py | 147 - libs/urllib3/packages/rfc3986/misc.py | 124 - libs/urllib3/packages/rfc3986/normalizers.py | 167 - libs/urllib3/packages/rfc3986/parseresult.py | 385 - libs/urllib3/packages/rfc3986/uri.py | 153 - libs/urllib3/packages/rfc3986/validators.py | 450 - libs/urllib3/packages/six.py | 391 +- .../packages/ssl_match_hostname/__init__.py | 19 - libs/urllib3/poolmanager.py | 289 +- libs/urllib3/request.py | 88 +- libs/urllib3/response.py | 269 +- libs/urllib3/util/__init__.py | 73 +- libs/urllib3/util/connection.py | 37 +- libs/urllib3/util/proxy.py | 57 + libs/urllib3/util/queue.py | 1 + libs/urllib3/util/request.py | 62 +- libs/urllib3/util/response.py | 40 +- libs/urllib3/util/retry.py | 336 +- libs/urllib3/util/ssl_.py | 327 +- .../ssl_match_hostname.py} | 59 +- libs/urllib3/util/ssltransport.py | 221 + libs/urllib3/util/timeout.py | 133 +- libs/urllib3/util/url.py | 357 +- libs/urllib3/util/wait.py | 9 +- libs/version.txt | 203 +- libs/wcwidth/__init__.py | 39 +- libs/wcwidth/table_wide.py | 1214 +- libs/wcwidth/table_zero.py | 4199 ++- libs/wcwidth/tests/__init__.py | 1 - libs/wcwidth/tests/test_core.py | 138 - libs/wcwidth/unicode_versions.py | 35 + libs/wcwidth/wcwidth.py | 286 +- libs/websocket/__init__.py | 26 +- libs/websocket/_abnf.py | 79 +- libs/websocket/_app.py | 138 +- libs/websocket/_cookiejar.py | 26 +- libs/websocket/_core.py | 235 +- libs/websocket/_exceptions.py | 26 +- libs/websocket/_handshake.py | 26 +- libs/websocket/_http.py | 190 +- libs/websocket/_logging.py | 24 +- libs/websocket/_socket.py | 28 +- libs/websocket/_ssl_compat.py | 29 +- libs/websocket/_url.py | 62 +- libs/websocket/_utils.py | 26 +- libs/websocket/_wsdump.py | 231 + libs/websocket/tests/echo-server.py | 21 + libs/websocket/tests/test_abnf.py | 29 +- libs/websocket/tests/test_app.py | 43 +- libs/websocket/tests/test_cookiejar.py | 29 +- libs/websocket/tests/test_http.py | 94 +- libs/websocket/tests/test_url.py | 30 +- libs/websocket/tests/test_websocket.py | 71 +- libs/werkzeug/__init__.py | 225 +- libs/werkzeug/_compat.py | 219 - libs/werkzeug/_internal.py | 410 +- libs/werkzeug/_reloader.py | 465 +- libs/werkzeug/contrib/__init__.py | 16 - libs/werkzeug/contrib/atom.py | 362 - libs/werkzeug/contrib/cache.py | 933 - libs/werkzeug/contrib/fixers.py | 262 - libs/werkzeug/contrib/iterio.py | 358 - libs/werkzeug/contrib/lint.py | 11 - libs/werkzeug/contrib/profiler.py | 42 - libs/werkzeug/contrib/securecookie.py | 362 - libs/werkzeug/contrib/sessions.py | 389 - libs/werkzeug/contrib/wrappers.py | 385 - libs/werkzeug/datastructures.py | 1251 +- libs/werkzeug/datastructures.pyi | 912 + libs/werkzeug/debug/__init__.py | 280 +- libs/werkzeug/debug/console.py | 151 +- libs/werkzeug/debug/repr.py | 203 +- libs/werkzeug/debug/shared/ICON_LICENSE.md | 6 + libs/werkzeug/debug/shared/debugger.js | 515 +- libs/werkzeug/debug/shared/jquery.js | 2 - libs/werkzeug/debug/shared/style.css | 11 +- libs/werkzeug/debug/tbtools.py | 337 +- libs/werkzeug/exceptions.py | 468 +- libs/werkzeug/filesystem.py | 19 +- libs/werkzeug/formparser.py | 529 +- libs/werkzeug/http.py | 841 +- libs/werkzeug/local.py | 704 +- libs/werkzeug/middleware/__init__.py | 3 - libs/werkzeug/middleware/dispatcher.py | 20 +- libs/werkzeug/middleware/http_proxy.py | 83 +- libs/werkzeug/middleware/lint.py | 212 +- libs/werkzeug/middleware/profiler.py | 51 +- libs/werkzeug/middleware/proxy_fix.py | 139 +- libs/werkzeug/middleware/shared_data.py | 211 +- libs/werkzeug/posixemulation.py | 117 - .../test/__init__.py => werkzeug/py.typed} | 0 libs/werkzeug/routing.py | 1222 +- libs/werkzeug/sansio/__init__.py | 0 libs/werkzeug/sansio/multipart.py | 260 + libs/werkzeug/sansio/request.py | 548 + libs/werkzeug/sansio/response.py | 704 + libs/werkzeug/sansio/utils.py | 142 + libs/werkzeug/security.py | 212 +- libs/werkzeug/serving.py | 882 +- libs/werkzeug/test.py | 1114 +- libs/werkzeug/testapp.py | 61 +- libs/werkzeug/urls.py | 689 +- libs/werkzeug/user_agent.py | 47 + libs/werkzeug/useragents.py | 261 +- libs/werkzeug/utils.py | 777 +- libs/werkzeug/wrappers/__init__.py | 24 +- libs/werkzeug/wrappers/accept.py | 58 +- libs/werkzeug/wrappers/auth.py | 51 +- libs/werkzeug/wrappers/base_request.py | 711 +- libs/werkzeug/wrappers/base_response.py | 716 +- libs/werkzeug/wrappers/common_descriptors.py | 340 +- libs/werkzeug/wrappers/cors.py | 26 + libs/werkzeug/wrappers/etag.py | 320 +- libs/werkzeug/wrappers/json.py | 152 +- libs/werkzeug/wrappers/request.py | 672 +- libs/werkzeug/wrappers/response.py | 892 +- libs/werkzeug/wrappers/user_agent.py | 24 +- libs/werkzeug/wsgi.py | 567 +- libs/whichcraft.py | 2 +- libs/xdg/AUTHORS | 6 - libs/xdg/BaseDirectory.py | 145 - libs/xdg/COPYING | 482 - libs/xdg/ChangeLog | 253 - libs/xdg/Config.py | 39 - libs/xdg/DesktopEntry.py | 417 - libs/xdg/Exceptions.py | 51 - libs/xdg/INSTALL | 3 - libs/xdg/IconTheme.py | 435 - libs/xdg/IniFile.py | 418 - libs/xdg/Locale.py | 79 - libs/xdg/Menu.py | 1134 - libs/xdg/MenuEditor.py | 511 - libs/xdg/Mime.py | 519 - libs/xdg/README | 21 - libs/xdg/RecentFiles.py | 181 - libs/xdg/__init__.py | 3 - libs/xdg/util.py | 11 - libs/yaml/__init__.py | 94 +- libs/yaml/constructor.py | 80 +- libs/yaml/cyaml.py | 2 +- libs/yaml/loader.py | 2 +- libs/yaml/representer.py | 4 +- libs/yaml/resolver.py | 6 +- libs/yaml/scanner.py | 12 +- 2108 files changed, 306789 insertions(+), 151391 deletions(-) delete mode 100644 libs/_markerlib/__init__.py delete mode 100644 libs/_markerlib/markers.py delete mode 100644 libs/arghelper.py delete mode 100644 libs/asio/__init__.py delete mode 100644 libs/asio/file.py delete mode 100644 libs/asio/file_opener.py delete mode 100644 libs/asio/interfaces/base.py delete mode 100644 libs/asio/interfaces/posix.py delete mode 100644 libs/asio/interfaces/windows/__init__.py delete mode 100644 libs/asio/interfaces/windows/interop.py delete mode 100644 libs/asio/open_parameters.py create mode 100755 libs/auditok/cmdline_util.py create mode 100755 libs/auditok/plotting.py create mode 100644 libs/auditok/signal.py create mode 100644 libs/auditok/signal_numpy.py create mode 100755 libs/auditok/workers.py create mode 100644 libs/backports/zoneinfo/__init__.py create mode 100644 libs/backports/zoneinfo/__init__.pyi create mode 100644 libs/backports/zoneinfo/_common.py create mode 100644 libs/backports/zoneinfo/_tzpath.py create mode 100644 libs/backports/zoneinfo/_version.py create mode 100644 libs/backports/zoneinfo/_zoneinfo.py rename libs/{twine => backports/zoneinfo}/py.typed (100%) delete mode 100644 libs/beaker/__init__.py delete mode 100644 libs/beaker/_compat.py delete mode 100644 libs/beaker/cache.py delete mode 100644 libs/beaker/container.py delete mode 100644 libs/beaker/converters.py delete mode 100644 libs/beaker/cookie.py delete mode 100644 libs/beaker/crypto/__init__.py delete mode 100644 libs/beaker/crypto/jcecrypto.py delete mode 100644 libs/beaker/crypto/noencryption.py delete mode 100644 libs/beaker/crypto/nsscrypto.py delete mode 100644 libs/beaker/crypto/pbkdf2.py delete mode 100644 libs/beaker/crypto/pyca_cryptography.py delete mode 100644 libs/beaker/crypto/pycrypto.py delete mode 100644 libs/beaker/crypto/util.py delete mode 100644 libs/beaker/exceptions.py delete mode 100644 libs/beaker/ext/database.py delete mode 100644 libs/beaker/ext/google.py delete mode 100644 libs/beaker/ext/memcached.py delete mode 100644 libs/beaker/ext/mongodb.py delete mode 100644 libs/beaker/ext/redisnm.py delete mode 100644 libs/beaker/ext/sqla.py delete mode 100644 libs/beaker/middleware.py delete mode 100644 libs/beaker/session.py delete mode 100644 libs/beaker/synchronization.py delete mode 100644 libs/beaker/util.py create mode 100644 libs/bidict/_delegating.py delete mode 100644 libs/bidict/_delegating_mixins.py rename libs/bidict/{_util.py => _iter.py} (52%) delete mode 100644 libs/bidict/_marker.py delete mode 100644 libs/bidict/_miss.py delete mode 100644 libs/bidict/_noop.py create mode 100644 libs/bidict/_typing.py delete mode 100644 libs/bidict/compat.py rename libs/{asio/interfaces/__init__.py => bidict/py.typed} (100%) delete mode 100644 libs/chardet/langcyrillicmodel.py create mode 100644 libs/chardet/langrussianmodel.py rename libs/{beaker/ext => chardet/metadata}/__init__.py (100%) create mode 100644 libs/chardet/metadata/languages.py delete mode 100644 libs/click/_bashcomplete.py rename libs/{git/test/fixtures/ls_tree_empty => click/py.typed} (100%) create mode 100644 libs/click/shell_completion.py delete mode 100644 libs/contextlib2.py delete mode 100644 libs/dateutil/parser.py create mode 100644 libs/dateutil/test/conftest.py create mode 100644 libs/dateutil/test/property/test_isoparse_prop.py create mode 100644 libs/dateutil/test/property/test_parser_prop.py create mode 100644 libs/dateutil/test/property/test_tz_prop.py create mode 100644 libs/dateutil/test/test_import_star.py create mode 100644 libs/dateutil/test/test_internals.py create mode 100644 libs/dateutil/test/test_isoparser.py create mode 100644 libs/dateutil/test/test_utils.py create mode 100644 libs/deep_translator/libre.py create mode 100644 libs/dns/_asyncbackend.py create mode 100644 libs/dns/_asyncio_backend.py delete mode 100644 libs/dns/_compat.py create mode 100644 libs/dns/_curio_backend.py create mode 100644 libs/dns/_immutable_attr.py create mode 100644 libs/dns/_immutable_ctx.py create mode 100644 libs/dns/_trio_backend.py create mode 100644 libs/dns/asyncbackend.py create mode 100644 libs/dns/asyncbackend.pyi create mode 100644 libs/dns/asyncquery.py create mode 100644 libs/dns/asyncquery.pyi create mode 100644 libs/dns/asyncresolver.py create mode 100644 libs/dns/asyncresolver.pyi create mode 100644 libs/dns/dnssec.pyi create mode 100644 libs/dns/e164.pyi create mode 100644 libs/dns/entropy.pyi create mode 100644 libs/dns/enum.py create mode 100644 libs/dns/exception.pyi create mode 100644 libs/dns/immutable.py create mode 100644 libs/dns/inet.pyi create mode 100644 libs/dns/message.pyi create mode 100644 libs/dns/name.pyi create mode 100644 libs/dns/node.pyi rename libs/{git/test/performance/__init__.py => dns/py.typed} (100%) create mode 100644 libs/dns/query.pyi create mode 100644 libs/dns/rdata.pyi create mode 100644 libs/dns/rdataset.pyi create mode 100644 libs/dns/rdtypes/ANY/AMTRELAY.py create mode 100644 libs/dns/rdtypes/ANY/L32.py create mode 100644 libs/dns/rdtypes/ANY/L64.py create mode 100644 libs/dns/rdtypes/ANY/LP.py create mode 100644 libs/dns/rdtypes/ANY/NID.py rename libs/dns/{hash.py => rdtypes/ANY/NINFO.py} (67%) create mode 100644 libs/dns/rdtypes/ANY/OPENPGPKEY.py create mode 100644 libs/dns/rdtypes/ANY/OPT.py create mode 100644 libs/dns/rdtypes/ANY/SMIMEA.py create mode 100644 libs/dns/rdtypes/ANY/TKEY.py create mode 100644 libs/dns/rdtypes/ANY/TSIG.py create mode 100644 libs/dns/rdtypes/ANY/ZONEMD.py create mode 100644 libs/dns/rdtypes/CH/A.py create mode 100644 libs/dns/rdtypes/CH/__init__.py create mode 100644 libs/dns/rdtypes/IN/HTTPS.py create mode 100644 libs/dns/rdtypes/IN/SVCB.py create mode 100644 libs/dns/rdtypes/dnskeybase.pyi create mode 100644 libs/dns/rdtypes/svcbbase.py create mode 100644 libs/dns/rdtypes/tlsabase.py create mode 100644 libs/dns/rdtypes/txtbase.pyi create mode 100644 libs/dns/rdtypes/util.py create mode 100644 libs/dns/resolver.pyi create mode 100644 libs/dns/reversename.pyi create mode 100644 libs/dns/rrset.pyi create mode 100644 libs/dns/serial.py create mode 100644 libs/dns/transaction.py create mode 100644 libs/dns/tsigkeyring.pyi create mode 100644 libs/dns/update.pyi create mode 100644 libs/dns/versioned.py create mode 100755 libs/dns/win32util.py create mode 100644 libs/dns/wire.py delete mode 100644 libs/dns/wiredata.py create mode 100644 libs/dns/xfr.py create mode 100644 libs/dns/zone.pyi create mode 100644 libs/dns/zonefile.py delete mode 100644 libs/dumprar.py delete mode 100644 libs/ffsubsync/suboffset.py delete mode 100644 libs/flask/_compat.py rename libs/{gitdb/utils/__init__.py => flask/py.typed} (100%) create mode 100644 libs/flask/scaffold.py create mode 100644 libs/flask/typing.py delete mode 100644 libs/flask_cors/__init__.py delete mode 100644 libs/flask_cors/core.py delete mode 100644 libs/flask_cors/decorator.py delete mode 100644 libs/flask_cors/extension.py delete mode 100644 libs/flask_cors/version.py delete mode 100644 libs/ftfy/build_data.py delete mode 100644 libs/ftfy/char_classes.dat delete mode 100644 libs/ftfy/compatibility.py delete mode 100644 libs/ftfy/streamtester/__init__.py delete mode 100644 libs/ftfy/streamtester/oauth.py delete mode 100644 libs/ftfy/streamtester/twitter_tester.py delete mode 100644 libs/funcsigs/__init__.py delete mode 100644 libs/funcsigs/version.py delete mode 100644 libs/git/__init__.py delete mode 100644 libs/git/cmd.py delete mode 100644 libs/git/compat.py delete mode 100644 libs/git/config.py delete mode 100644 libs/git/db.py delete mode 100644 libs/git/diff.py delete mode 100644 libs/git/exc.py delete mode 100644 libs/git/index/__init__.py delete mode 100644 libs/git/index/base.py delete mode 100644 libs/git/index/fun.py delete mode 100644 libs/git/index/typ.py delete mode 100644 libs/git/index/util.py delete mode 100644 libs/git/objects/__init__.py delete mode 100644 libs/git/objects/base.py delete mode 100644 libs/git/objects/blob.py delete mode 100644 libs/git/objects/commit.py delete mode 100644 libs/git/objects/fun.py delete mode 100644 libs/git/objects/submodule/__init__.py delete mode 100644 libs/git/objects/submodule/base.py delete mode 100644 libs/git/objects/submodule/root.py delete mode 100644 libs/git/objects/submodule/util.py delete mode 100644 libs/git/objects/tag.py delete mode 100644 libs/git/objects/tree.py delete mode 100644 libs/git/objects/util.py delete mode 100644 libs/git/refs/__init__.py delete mode 100644 libs/git/refs/head.py delete mode 100644 libs/git/refs/log.py delete mode 100644 libs/git/refs/reference.py delete mode 100644 libs/git/refs/remote.py delete mode 100644 libs/git/refs/symbolic.py delete mode 100644 libs/git/refs/tag.py delete mode 100644 libs/git/remote.py delete mode 100644 libs/git/repo/__init__.py delete mode 100644 libs/git/repo/base.py delete mode 100644 libs/git/repo/fun.py delete mode 100644 libs/git/test/__init__.py delete mode 100644 libs/git/test/fixtures/blame delete mode 100644 libs/git/test/fixtures/blame_binary delete mode 100644 libs/git/test/fixtures/blame_complex_revision delete mode 100644 libs/git/test/fixtures/blame_incremental delete mode 100644 libs/git/test/fixtures/blame_incremental_2.11.1_plus delete mode 100644 libs/git/test/fixtures/cat_file.py delete mode 100644 libs/git/test/fixtures/cat_file_blob delete mode 100644 libs/git/test/fixtures/cat_file_blob_nl delete mode 100644 libs/git/test/fixtures/cat_file_blob_size delete mode 100644 libs/git/test/fixtures/commit_invalid_data delete mode 100644 libs/git/test/fixtures/commit_with_gpgsig delete mode 100644 libs/git/test/fixtures/diff_2 delete mode 100644 libs/git/test/fixtures/diff_2f delete mode 100644 libs/git/test/fixtures/diff_abbrev-40_full-index_M_raw_no-color delete mode 100644 libs/git/test/fixtures/diff_change_in_type delete mode 100644 libs/git/test/fixtures/diff_change_in_type_raw delete mode 100644 libs/git/test/fixtures/diff_f delete mode 100644 libs/git/test/fixtures/diff_file_with_spaces delete mode 100644 libs/git/test/fixtures/diff_i delete mode 100644 libs/git/test/fixtures/diff_index_patch delete mode 100644 libs/git/test/fixtures/diff_index_raw delete mode 100644 libs/git/test/fixtures/diff_initial delete mode 100644 libs/git/test/fixtures/diff_mode_only delete mode 100644 libs/git/test/fixtures/diff_new_mode delete mode 100644 libs/git/test/fixtures/diff_numstat delete mode 100644 libs/git/test/fixtures/diff_p delete mode 100644 libs/git/test/fixtures/diff_patch_binary delete mode 100644 libs/git/test/fixtures/diff_patch_unsafe_paths delete mode 100644 libs/git/test/fixtures/diff_raw_binary delete mode 100644 libs/git/test/fixtures/diff_rename delete mode 100644 libs/git/test/fixtures/diff_rename_raw delete mode 100644 libs/git/test/fixtures/diff_tree_numstat_root delete mode 100644 libs/git/test/fixtures/for_each_ref_with_path_component delete mode 100644 libs/git/test/fixtures/git_config delete mode 100644 libs/git/test/fixtures/git_config-inc.cfg delete mode 100644 libs/git/test/fixtures/git_config_global delete mode 100644 libs/git/test/fixtures/git_config_with_comments delete mode 100644 libs/git/test/fixtures/git_config_with_empty_value delete mode 100644 libs/git/test/fixtures/git_file delete mode 100644 libs/git/test/fixtures/index delete mode 100644 libs/git/test/fixtures/index_merge delete mode 100644 libs/git/test/fixtures/issue-301_stderr delete mode 100644 libs/git/test/fixtures/ls_tree_a delete mode 100644 libs/git/test/fixtures/ls_tree_b delete mode 100644 libs/git/test/fixtures/ls_tree_commit delete mode 100644 libs/git/test/fixtures/reflog_HEAD delete mode 100644 libs/git/test/fixtures/reflog_invalid_date delete mode 100644 libs/git/test/fixtures/reflog_invalid_email delete mode 100644 libs/git/test/fixtures/reflog_invalid_newsha delete mode 100644 libs/git/test/fixtures/reflog_invalid_oldsha delete mode 100644 libs/git/test/fixtures/reflog_invalid_sep delete mode 100644 libs/git/test/fixtures/reflog_master delete mode 100644 libs/git/test/fixtures/rev_list delete mode 100644 libs/git/test/fixtures/rev_list_bisect_all delete mode 100644 libs/git/test/fixtures/rev_list_commit_diffs delete mode 100644 libs/git/test/fixtures/rev_list_commit_idabbrev delete mode 100644 libs/git/test/fixtures/rev_list_commit_stats delete mode 100644 libs/git/test/fixtures/rev_list_count delete mode 100644 libs/git/test/fixtures/rev_list_delta_a delete mode 100644 libs/git/test/fixtures/rev_list_delta_b delete mode 100644 libs/git/test/fixtures/rev_list_single delete mode 100644 libs/git/test/fixtures/rev_parse delete mode 100644 libs/git/test/fixtures/show_empty_commit delete mode 100644 libs/git/test/fixtures/uncommon_branch_prefix_FETCH_HEAD delete mode 100644 libs/git/test/fixtures/uncommon_branch_prefix_stderr delete mode 100644 libs/git/test/lib/__init__.py delete mode 100644 libs/git/test/lib/asserts.py delete mode 100644 libs/git/test/lib/helper.py delete mode 100644 libs/git/test/performance/lib.py delete mode 100644 libs/git/test/performance/test_commit.py delete mode 100644 libs/git/test/performance/test_odb.py delete mode 100644 libs/git/test/performance/test_streams.py delete mode 100644 libs/git/test/test_actor.py delete mode 100644 libs/git/test/test_base.py delete mode 100644 libs/git/test/test_blob.py delete mode 100644 libs/git/test/test_commit.py delete mode 100644 libs/git/test/test_config.py delete mode 100644 libs/git/test/test_db.py delete mode 100644 libs/git/test/test_diff.py delete mode 100644 libs/git/test/test_docs.py delete mode 100644 libs/git/test/test_exc.py delete mode 100644 libs/git/test/test_fun.py delete mode 100644 libs/git/test/test_git.py delete mode 100644 libs/git/test/test_index.py delete mode 100644 libs/git/test/test_reflog.py delete mode 100644 libs/git/test/test_refs.py delete mode 100644 libs/git/test/test_remote.py delete mode 100644 libs/git/test/test_repo.py delete mode 100644 libs/git/test/test_stats.py delete mode 100644 libs/git/test/test_submodule.py delete mode 100644 libs/git/test/test_tree.py delete mode 100644 libs/git/test/test_util.py delete mode 100644 libs/git/util.py delete mode 100644 libs/gitdb/__init__.py delete mode 100644 libs/gitdb/base.py delete mode 100644 libs/gitdb/const.py delete mode 100644 libs/gitdb/db/__init__.py delete mode 100644 libs/gitdb/db/base.py delete mode 100644 libs/gitdb/db/git.py delete mode 100644 libs/gitdb/db/loose.py delete mode 100644 libs/gitdb/db/mem.py delete mode 100644 libs/gitdb/db/pack.py delete mode 100644 libs/gitdb/db/ref.py delete mode 100644 libs/gitdb/exc.py delete mode 100644 libs/gitdb/fun.py delete mode 100644 libs/gitdb/pack.py delete mode 100644 libs/gitdb/stream.py delete mode 100644 libs/gitdb/test/__init__.py delete mode 100644 libs/gitdb/test/lib.py delete mode 100644 libs/gitdb/test/test_base.py delete mode 100644 libs/gitdb/test/test_example.py delete mode 100644 libs/gitdb/test/test_pack.py delete mode 100644 libs/gitdb/test/test_stream.py delete mode 100644 libs/gitdb/test/test_util.py delete mode 100644 libs/gitdb/typ.py delete mode 100644 libs/gitdb/util.py delete mode 100644 libs/gitdb/utils/compat.py delete mode 100644 libs/gitdb/utils/encoding.py delete mode 100644 libs/html5lib/tests/__init__.py delete mode 100644 libs/html5lib/tests/conftest.py delete mode 100644 libs/html5lib/tests/sanitizer-testdata/tests1.dat delete mode 100644 libs/html5lib/tests/sanitizer.py delete mode 100644 libs/html5lib/tests/serializer-testdata/core.test delete mode 100644 libs/html5lib/tests/serializer-testdata/injectmeta.test delete mode 100644 libs/html5lib/tests/serializer-testdata/optionaltags.test delete mode 100644 libs/html5lib/tests/serializer-testdata/options.test delete mode 100644 libs/html5lib/tests/serializer-testdata/whitespace.test delete mode 100644 libs/html5lib/tests/support.py delete mode 100644 libs/html5lib/tests/test_alphabeticalattributes.py delete mode 100644 libs/html5lib/tests/test_encoding.py delete mode 100644 libs/html5lib/tests/test_meta.py delete mode 100644 libs/html5lib/tests/test_optionaltags_filter.py delete mode 100644 libs/html5lib/tests/test_parser2.py delete mode 100644 libs/html5lib/tests/test_sanitizer.py delete mode 100644 libs/html5lib/tests/test_serializer.py delete mode 100644 libs/html5lib/tests/test_stream.py delete mode 100644 libs/html5lib/tests/test_tokenizer2.py delete mode 100644 libs/html5lib/tests/test_treeadapters.py delete mode 100644 libs/html5lib/tests/test_treewalkers.py delete mode 100644 libs/html5lib/tests/test_whitespace_filter.py delete mode 100644 libs/html5lib/tests/tokenizer.py delete mode 100644 libs/html5lib/tests/tokenizertotree.py delete mode 100644 libs/html5lib/tests/tree_construction.py delete mode 100644 libs/html5lib/tests/us-ascii.html delete mode 100644 libs/html5lib/tests/utf-8-bom.html rename libs/{importlib_resources/tests/__init__.py => idna/py.typed} (100%) create mode 100644 libs/importlib_metadata/__init__.py create mode 100644 libs/importlib_metadata/_adapters.py create mode 100644 libs/importlib_metadata/_collections.py create mode 100644 libs/importlib_metadata/_compat.py create mode 100644 libs/importlib_metadata/_functools.py create mode 100644 libs/importlib_metadata/_itertools.py create mode 100644 libs/importlib_metadata/_meta.py create mode 100644 libs/importlib_metadata/_text.py rename libs/{importlib_resources/tests/data01/__init__.py => importlib_metadata/py.typed} (100%) delete mode 100644 libs/importlib_resources/tests/_compat.py delete mode 100644 libs/importlib_resources/tests/data01/binary.file delete mode 100644 libs/importlib_resources/tests/data01/subdirectory/binary.file delete mode 100644 libs/importlib_resources/tests/data01/utf-16.file delete mode 100644 libs/importlib_resources/tests/data01/utf-8.file delete mode 100644 libs/importlib_resources/tests/data02/one/resource1.txt delete mode 100644 libs/importlib_resources/tests/data02/two/resource2.txt delete mode 100644 libs/importlib_resources/tests/namespacedata01/binary.file delete mode 100644 libs/importlib_resources/tests/namespacedata01/utf-16.file delete mode 100644 libs/importlib_resources/tests/namespacedata01/utf-8.file delete mode 100644 libs/importlib_resources/tests/test_compatibilty_files.py delete mode 100644 libs/importlib_resources/tests/test_contents.py delete mode 100644 libs/importlib_resources/tests/test_files.py delete mode 100644 libs/importlib_resources/tests/test_open.py delete mode 100644 libs/importlib_resources/tests/test_path.py delete mode 100644 libs/importlib_resources/tests/test_read.py delete mode 100644 libs/importlib_resources/tests/test_reader.py delete mode 100644 libs/importlib_resources/tests/test_resource.py delete mode 100755 libs/importlib_resources/tests/update-zips.py delete mode 100644 libs/importlib_resources/tests/util.py delete mode 100644 libs/importlib_resources/tests/zipdata01/ziptestdata.zip delete mode 100644 libs/importlib_resources/tests/zipdata02/ziptestdata.zip delete mode 100644 libs/ipaddress.py delete mode 100644 libs/itsdangerous/_compat.py rename libs/{importlib_resources/tests/data01/subdirectory/__init__.py => itsdangerous/py.typed} (100%) delete mode 100644 libs/jinja2/_compat.py create mode 100644 libs/jinja2/async_utils.py delete mode 100644 libs/jinja2/asyncfilters.py delete mode 100644 libs/jinja2/asyncsupport.py rename libs/{importlib_resources/tests/data02/__init__.py => jinja2/py.typed} (100%) delete mode 100644 libs/js2py/es6/babel.js delete mode 100644 libs/js2py/es6/buildBabel delete mode 100644 libs/js2py/legecy_translators/__init__.py delete mode 100644 libs/js2py/legecy_translators/constants.py delete mode 100644 libs/js2py/legecy_translators/exps.py delete mode 100644 libs/js2py/legecy_translators/flow.py delete mode 100644 libs/js2py/legecy_translators/functions.py delete mode 100644 libs/js2py/legecy_translators/jsparser.py delete mode 100644 libs/js2py/legecy_translators/nodevisitor.py delete mode 100644 libs/js2py/legecy_translators/nparser.py delete mode 100644 libs/js2py/legecy_translators/objects.py delete mode 100644 libs/js2py/legecy_translators/tokenize.py delete mode 100644 libs/js2py/legecy_translators/translator.py delete mode 100644 libs/js2py/legecy_translators/utils.py create mode 100644 libs/js2py/py_node_modules/crypto_js.py create mode 100644 libs/js2py/py_node_modules/escodegen.py create mode 100644 libs/js2py/py_node_modules/esprima.py create mode 100644 libs/json_tricks/_version.py delete mode 100644 libs/jstyleson.py create mode 100644 libs/knowit/properties/audio.py delete mode 100644 libs/knowit/properties/audio/__init__.py delete mode 100644 libs/knowit/properties/audio/bitratemode.py delete mode 100644 libs/knowit/properties/audio/channels.py delete mode 100644 libs/knowit/properties/audio/codec.py delete mode 100644 libs/knowit/properties/audio/compression.py delete mode 100644 libs/knowit/properties/audio/profile.py delete mode 100644 libs/knowit/properties/basic.py delete mode 100644 libs/knowit/properties/duration.py create mode 100644 libs/knowit/properties/general.py delete mode 100644 libs/knowit/properties/language.py delete mode 100644 libs/knowit/properties/quantity.py create mode 100644 libs/knowit/properties/subtitle.py delete mode 100644 libs/knowit/properties/subtitle/__init__.py delete mode 100644 libs/knowit/properties/subtitle/format.py create mode 100644 libs/knowit/properties/video.py delete mode 100644 libs/knowit/properties/video/__init__.py delete mode 100644 libs/knowit/properties/video/codec.py delete mode 100644 libs/knowit/properties/video/encoder.py delete mode 100644 libs/knowit/properties/video/profile.py delete mode 100644 libs/knowit/properties/video/ratio.py delete mode 100644 libs/knowit/properties/video/scantype.py delete mode 100644 libs/knowit/properties/yesno.py delete mode 100644 libs/knowit/property.py create mode 100644 libs/knowit/providers/mkvmerge.py delete mode 100644 libs/knowit/rule.py create mode 100644 libs/knowit/rules/audio.py delete mode 100644 libs/knowit/rules/audio/__init__.py delete mode 100644 libs/knowit/rules/audio/atmos.py delete mode 100644 libs/knowit/rules/audio/channels.py delete mode 100644 libs/knowit/rules/audio/codec.py delete mode 100644 libs/knowit/rules/audio/dtshd.py rename libs/knowit/rules/{language.py => general.py} (89%) rename libs/knowit/rules/{subtitle/closedcaption.py => subtitle.py} (52%) delete mode 100644 libs/knowit/rules/subtitle/__init__.py delete mode 100644 libs/knowit/rules/subtitle/hearingimpaired.py rename libs/knowit/rules/{video/resolution.py => video.py} (83%) delete mode 100644 libs/knowit/rules/video/__init__.py create mode 100644 libs/markdown/__meta__.py delete mode 100644 libs/markdown/__version__.py create mode 100644 libs/markdown/core.py delete mode 100644 libs/markdown/extensions/headerid.py create mode 100644 libs/markdown/extensions/legacy_attrs.py create mode 100644 libs/markdown/extensions/legacy_em.py create mode 100644 libs/markdown/extensions/md_in_html.py delete mode 100644 libs/markdown/extensions/smart_strong.py create mode 100644 libs/markdown/htmlparser.py delete mode 100644 libs/markdown/odict.py create mode 100644 libs/markdown/pep562.py create mode 100644 libs/markdown/test_tools.py delete mode 100644 libs/markupsafe/_compat.py delete mode 100644 libs/markupsafe/_constants.py create mode 100644 libs/markupsafe/_speedups.pyi rename libs/{importlib_resources/tests/data02/one/__init__.py => markupsafe/py.typed} (100%) create mode 100644 libs/msgpack/_cmsgpack.cpp create mode 100644 libs/oauthlib/oauth2/rfc6749/endpoints/introspect.py create mode 100644 libs/oauthlib/oauth2/rfc6749/endpoints/metadata.py create mode 100644 libs/oauthlib/openid/__init__.py rename libs/{importlib_resources/tests/data02/two => oauthlib/openid/connect}/__init__.py (100%) rename libs/{importlib_resources/tests/zipdata01 => oauthlib/openid/connect/core}/__init__.py (100%) create mode 100644 libs/oauthlib/openid/connect/core/endpoints/__init__.py create mode 100644 libs/oauthlib/openid/connect/core/endpoints/pre_configured.py create mode 100644 libs/oauthlib/openid/connect/core/endpoints/userinfo.py create mode 100644 libs/oauthlib/openid/connect/core/exceptions.py create mode 100644 libs/oauthlib/openid/connect/core/grant_types/__init__.py create mode 100644 libs/oauthlib/openid/connect/core/grant_types/authorization_code.py rename libs/oauthlib/{oauth2/rfc6749/grant_types/openid_connect.py => openid/connect/core/grant_types/base.py} (61%) create mode 100644 libs/oauthlib/openid/connect/core/grant_types/dispatchers.py create mode 100644 libs/oauthlib/openid/connect/core/grant_types/hybrid.py create mode 100644 libs/oauthlib/openid/connect/core/grant_types/implicit.py create mode 100644 libs/oauthlib/openid/connect/core/request_validator.py create mode 100644 libs/oauthlib/openid/connect/core/tokens.py create mode 100644 libs/playhouse/_sqlite_ext.c create mode 100644 libs/playhouse/_sqlite_udf.c create mode 100644 libs/playhouse/psycopg3_ext.py create mode 100644 libs/pycountry/databases/iso639-5.json create mode 100644 libs/pycountry/locales/ar/LC_MESSAGES/iso4217.mo create mode 100644 libs/pycountry/locales/ast/LC_MESSAGES/iso15924.mo create mode 100644 libs/pycountry/locales/ast/LC_MESSAGES/iso3166-2.mo create mode 100644 libs/pycountry/locales/ast/LC_MESSAGES/iso4217.mo create mode 100644 libs/pycountry/locales/ast/LC_MESSAGES/iso639-5.mo create mode 100644 libs/pycountry/locales/be/LC_MESSAGES/iso639-3.mo create mode 100644 libs/pycountry/locales/be/LC_MESSAGES/iso639-5.mo create mode 100644 libs/pycountry/locales/bn/LC_MESSAGES/iso4217.mo create mode 100644 libs/pycountry/locales/bn/LC_MESSAGES/iso639-5.mo create mode 100644 libs/pycountry/locales/bn_BD/LC_MESSAGES/iso15924.mo create mode 100644 libs/pycountry/locales/bn_BD/LC_MESSAGES/iso3166-2.mo create mode 100644 libs/pycountry/locales/bn_BD/LC_MESSAGES/iso639-5.mo create mode 100644 libs/pycountry/locales/cv/LC_MESSAGES/iso15924.mo create mode 100644 libs/pycountry/locales/cy/LC_MESSAGES/iso15924.mo create mode 100644 libs/pycountry/locales/da/LC_MESSAGES/iso639-5.mo create mode 100644 libs/pycountry/locales/de/LC_MESSAGES/iso639-5.mo create mode 100644 libs/pycountry/locales/el/LC_MESSAGES/iso639-5.mo create mode 100644 libs/pycountry/locales/et/LC_MESSAGES/iso639-5.mo create mode 100644 libs/pycountry/locales/eu/LC_MESSAGES/iso15924.mo create mode 100644 libs/pycountry/locales/eu/LC_MESSAGES/iso4217.mo create mode 100644 libs/pycountry/locales/fa/LC_MESSAGES/iso3166-2.mo create mode 100644 libs/pycountry/locales/fil/LC_MESSAGES/iso15924.mo create mode 100644 libs/pycountry/locales/fil/LC_MESSAGES/iso3166-1.mo create mode 100644 libs/pycountry/locales/fr/LC_MESSAGES/iso639-5.mo create mode 100644 libs/pycountry/locales/fur/LC_MESSAGES/iso3166-3.mo create mode 100644 libs/pycountry/locales/fur/LC_MESSAGES/iso639-5.mo create mode 100644 libs/pycountry/locales/he/LC_MESSAGES/iso3166-2.mo create mode 100644 libs/pycountry/locales/hr/LC_MESSAGES/iso15924.mo create mode 100644 libs/pycountry/locales/hr/LC_MESSAGES/iso3166-2.mo create mode 100644 libs/pycountry/locales/hr/LC_MESSAGES/iso639-5.mo create mode 100644 libs/pycountry/locales/hu/LC_MESSAGES/iso639-5.mo create mode 100644 libs/pycountry/locales/id/LC_MESSAGES/iso639-5.mo create mode 100644 libs/pycountry/locales/is/LC_MESSAGES/iso639-5.mo create mode 100644 libs/pycountry/locales/it/LC_MESSAGES/iso639-5.mo create mode 100644 libs/pycountry/locales/kab/LC_MESSAGES/iso15924.mo create mode 100644 libs/pycountry/locales/kab/LC_MESSAGES/iso3166-2.mo create mode 100644 libs/pycountry/locales/kab/LC_MESSAGES/iso4217.mo create mode 100644 libs/pycountry/locales/kab/LC_MESSAGES/iso639-3.mo create mode 100644 libs/pycountry/locales/kab/LC_MESSAGES/iso639-5.mo create mode 100644 libs/pycountry/locales/kmr/LC_MESSAGES/iso3166-1.mo create mode 100644 libs/pycountry/locales/kmr/LC_MESSAGES/iso3166-3.mo create mode 100644 libs/pycountry/locales/nb_NO/LC_MESSAGES/iso3166-2.mo create mode 100644 libs/pycountry/locales/nb_NO/LC_MESSAGES/iso639-5.mo create mode 100644 libs/pycountry/locales/nl/LC_MESSAGES/iso639-5.mo create mode 100644 libs/pycountry/locales/oc/LC_MESSAGES/iso639-5.mo create mode 100644 libs/pycountry/locales/pl/LC_MESSAGES/iso639-5.mo create mode 100644 libs/pycountry/locales/pt_BR/LC_MESSAGES/iso3166-2.mo create mode 100644 libs/pycountry/locales/pt_BR/LC_MESSAGES/iso639-5.mo create mode 100644 libs/pycountry/locales/ru/LC_MESSAGES/iso3166-2.mo create mode 100644 libs/pycountry/locales/sc/LC_MESSAGES/iso15924.mo create mode 100644 libs/pycountry/locales/sc/LC_MESSAGES/iso3166-2.mo create mode 100644 libs/pycountry/locales/sc/LC_MESSAGES/iso3166-3.mo create mode 100644 libs/pycountry/locales/sc/LC_MESSAGES/iso4217.mo create mode 100644 libs/pycountry/locales/sc/LC_MESSAGES/iso639-3.mo create mode 100644 libs/pycountry/locales/sc/LC_MESSAGES/iso639-5.mo create mode 100644 libs/pycountry/locales/si/LC_MESSAGES/iso15924.mo create mode 100644 libs/pycountry/locales/so/LC_MESSAGES/iso15924.mo create mode 100644 libs/pycountry/locales/so/LC_MESSAGES/iso3166-2.mo create mode 100644 libs/pycountry/locales/so/LC_MESSAGES/iso4217.mo create mode 100644 libs/pycountry/locales/so/LC_MESSAGES/iso639-3.mo create mode 100644 libs/pycountry/locales/sq/LC_MESSAGES/iso15924.mo create mode 100644 libs/pycountry/locales/sq/LC_MESSAGES/iso3166-2.mo create mode 100644 libs/pycountry/locales/sq/LC_MESSAGES/iso4217.mo create mode 100644 libs/pycountry/locales/sq/LC_MESSAGES/iso639-3.mo create mode 100644 libs/pycountry/locales/sq/LC_MESSAGES/iso639-5.mo create mode 100644 libs/pycountry/locales/sr/LC_MESSAGES/iso639-5.mo create mode 100644 libs/pycountry/locales/sr@latin/LC_MESSAGES/iso639-5.mo create mode 100644 libs/pycountry/locales/sv/LC_MESSAGES/iso639-5.mo create mode 100644 libs/pycountry/locales/tzm/LC_MESSAGES/iso15924.mo create mode 100644 libs/pycountry/locales/tzm/LC_MESSAGES/iso3166-1.mo create mode 100644 libs/pycountry/locales/tzm/LC_MESSAGES/iso4217.mo create mode 100644 libs/pycountry/locales/uk/LC_MESSAGES/iso639-5.mo create mode 100644 libs/pycountry/locales/zh_Hans/LC_MESSAGES/iso639-5.mo create mode 100644 libs/pycountry/locales/zh_Hant/LC_MESSAGES/iso639-5.mo delete mode 100644 libs/pycountry/tests/test_general.py delete mode 100644 libs/pyemitter.py create mode 100644 libs/pygments/formatters/groff.py create mode 100644 libs/pygments/formatters/pangomarkup.py create mode 100644 libs/pygments/lexers/_julia_builtins.py create mode 100644 libs/pygments/lexers/_lilypond_builtins.py create mode 100644 libs/pygments/lexers/apdlexer.py create mode 100644 libs/pygments/lexers/asc.py create mode 100644 libs/pygments/lexers/bdd.py create mode 100644 libs/pygments/lexers/elpi.py create mode 100644 libs/pygments/lexers/gcodelexer.py create mode 100755 libs/pygments/lexers/gsql.py create mode 100644 libs/pygments/lexers/jslt.py create mode 100644 libs/pygments/lexers/kuin.py create mode 100644 libs/pygments/lexers/lilypond.py create mode 100644 libs/pygments/lexers/maxima.py create mode 100644 libs/pygments/lexers/meson.py create mode 100644 libs/pygments/lexers/procfile.py create mode 100644 libs/pygments/lexers/rita.py create mode 100644 libs/pygments/lexers/savi.py create mode 100644 libs/pygments/lexers/smithy.py create mode 100644 libs/pygments/lexers/sophia.py create mode 100644 libs/pygments/lexers/spice.py create mode 100644 libs/pygments/lexers/srcinfo.py create mode 100644 libs/pygments/lexers/teal.py create mode 100644 libs/pygments/lexers/thingsdb.py create mode 100644 libs/pygments/lexers/webassembly.py create mode 100644 libs/pygments/styles/dracula.py create mode 100644 libs/pygments/styles/friendly_grayscale.py create mode 100644 libs/pygments/styles/gruvbox.py create mode 100644 libs/pygments/styles/lilypond.py create mode 100644 libs/pygments/styles/onedark.py create mode 100644 libs/pymediainfo/__init__.py delete mode 100644 libs/pyparsing.py create mode 100644 libs/pyparsing/__init__.py create mode 100644 libs/pyparsing/actions.py create mode 100644 libs/pyparsing/common.py create mode 100644 libs/pyparsing/core.py create mode 100644 libs/pyparsing/diagram/__init__.py create mode 100644 libs/pyparsing/diagram/template.jinja2 create mode 100644 libs/pyparsing/exceptions.py create mode 100644 libs/pyparsing/helpers.py create mode 100644 libs/pyparsing/results.py create mode 100644 libs/pyparsing/testing.py create mode 100644 libs/pyparsing/unicode.py create mode 100644 libs/pyparsing/util.py create mode 100644 libs/python_anticaptcha/compat.py delete mode 100644 libs/python_anticaptcha/proxy.py create mode 100644 libs/pytz/zoneinfo/Pacific/Kanton create mode 100644 libs/pytz_deprecation_shim/__init__.py create mode 100644 libs/pytz_deprecation_shim/_common.py create mode 100644 libs/pytz_deprecation_shim/_compat.py create mode 100644 libs/pytz_deprecation_shim/_compat_py2.py create mode 100644 libs/pytz_deprecation_shim/_compat_py3.py create mode 100644 libs/pytz_deprecation_shim/_exceptions.py create mode 100644 libs/pytz_deprecation_shim/_impl.py create mode 100644 libs/pytz_deprecation_shim/helpers.py create mode 100644 libs/requests_oauthlib/compliance_fixes/instagram.py create mode 100644 libs/rich/_extension.py create mode 100644 libs/rich/json.py delete mode 100644 libs/smmap/__init__.py delete mode 100644 libs/smmap/buf.py delete mode 100644 libs/smmap/exc.py delete mode 100644 libs/smmap/mman.py delete mode 100644 libs/smmap/test/lib.py delete mode 100644 libs/smmap/test/test_buf.py delete mode 100644 libs/smmap/test/test_mman.py delete mode 100644 libs/smmap/test/test_tutorial.py delete mode 100644 libs/smmap/test/test_util.py delete mode 100644 libs/smmap/util.py create mode 100644 libs/socketio/msgpack_packet.py delete mode 100644 libs/sockshandler.py create mode 100644 libs/soupsieve/pretty.py rename libs/{importlib_resources/tests/zipdata02/__init__.py => soupsieve/py.typed} (100%) create mode 100755 libs/srt_tools/srt create mode 100755 libs/srt_tools/srt-deduplicate create mode 100755 libs/srt_tools/srt-fixed-timeshift create mode 100755 libs/srt_tools/srt-linear-timeshift create mode 100755 libs/srt_tools/srt-lines-matching create mode 100755 libs/srt_tools/srt-mux create mode 100755 libs/srt_tools/srt-normalise create mode 100755 libs/srt_tools/srt-play create mode 100755 libs/srt_tools/srt-process create mode 100644 libs/stevedore/_cache.py create mode 100644 libs/stevedore/tests/test_cache.py rename libs/{js2py/host/dom/__init__.py => tld/py.typed} (100%) create mode 100644 libs/tld/res/effective_tld_names_public_only.dat.txt create mode 100644 libs/tld/res/old/effective_tld_names-2013-04-22.dat.txt create mode 100644 libs/tld/res/old/effective_tld_names-2015-07-19.dat.txt create mode 100644 libs/tld/res/old/effective_tld_names-2015-11-22.dat.txt create mode 100644 libs/tld/tests/test_registry.py delete mode 100644 libs/twine/__init__.py delete mode 100644 libs/twine/__main__.py delete mode 100644 libs/twine/auth.py delete mode 100644 libs/twine/cli.py delete mode 100644 libs/twine/commands/__init__.py delete mode 100644 libs/twine/commands/check.py delete mode 100644 libs/twine/commands/register.py delete mode 100644 libs/twine/commands/upload.py delete mode 100644 libs/twine/exceptions.py delete mode 100644 libs/twine/package.py delete mode 100644 libs/twine/repository.py delete mode 100644 libs/twine/settings.py delete mode 100644 libs/twine/utils.py delete mode 100644 libs/twine/wheel.py delete mode 100644 libs/twine/wininst.py create mode 100644 libs/urllib3/_version.py delete mode 100644 libs/urllib3/packages/rfc3986/__init__.py delete mode 100644 libs/urllib3/packages/rfc3986/_mixin.py delete mode 100644 libs/urllib3/packages/rfc3986/abnf_regexp.py delete mode 100644 libs/urllib3/packages/rfc3986/api.py delete mode 100644 libs/urllib3/packages/rfc3986/builder.py delete mode 100644 libs/urllib3/packages/rfc3986/compat.py delete mode 100644 libs/urllib3/packages/rfc3986/exceptions.py delete mode 100644 libs/urllib3/packages/rfc3986/iri.py delete mode 100644 libs/urllib3/packages/rfc3986/misc.py delete mode 100644 libs/urllib3/packages/rfc3986/normalizers.py delete mode 100644 libs/urllib3/packages/rfc3986/parseresult.py delete mode 100644 libs/urllib3/packages/rfc3986/uri.py delete mode 100644 libs/urllib3/packages/rfc3986/validators.py delete mode 100644 libs/urllib3/packages/ssl_match_hostname/__init__.py create mode 100644 libs/urllib3/util/proxy.py rename libs/urllib3/{packages/ssl_match_hostname/_implementation.py => util/ssl_match_hostname.py} (77%) create mode 100644 libs/urllib3/util/ssltransport.py delete mode 100644 libs/wcwidth/tests/__init__.py delete mode 100644 libs/wcwidth/tests/test_core.py create mode 100644 libs/wcwidth/unicode_versions.py create mode 100755 libs/websocket/_wsdump.py create mode 100644 libs/websocket/tests/echo-server.py delete mode 100644 libs/werkzeug/_compat.py delete mode 100644 libs/werkzeug/contrib/__init__.py delete mode 100644 libs/werkzeug/contrib/atom.py delete mode 100644 libs/werkzeug/contrib/cache.py delete mode 100644 libs/werkzeug/contrib/fixers.py delete mode 100644 libs/werkzeug/contrib/iterio.py delete mode 100644 libs/werkzeug/contrib/lint.py delete mode 100644 libs/werkzeug/contrib/profiler.py delete mode 100644 libs/werkzeug/contrib/securecookie.py delete mode 100644 libs/werkzeug/contrib/sessions.py delete mode 100644 libs/werkzeug/contrib/wrappers.py create mode 100644 libs/werkzeug/datastructures.pyi create mode 100644 libs/werkzeug/debug/shared/ICON_LICENSE.md delete mode 100644 libs/werkzeug/debug/shared/jquery.js delete mode 100644 libs/werkzeug/posixemulation.py rename libs/{smmap/test/__init__.py => werkzeug/py.typed} (100%) create mode 100644 libs/werkzeug/sansio/__init__.py create mode 100644 libs/werkzeug/sansio/multipart.py create mode 100644 libs/werkzeug/sansio/request.py create mode 100644 libs/werkzeug/sansio/response.py create mode 100644 libs/werkzeug/sansio/utils.py create mode 100644 libs/werkzeug/user_agent.py create mode 100644 libs/werkzeug/wrappers/cors.py delete mode 100644 libs/xdg/AUTHORS delete mode 100644 libs/xdg/BaseDirectory.py delete mode 100644 libs/xdg/COPYING delete mode 100644 libs/xdg/ChangeLog delete mode 100644 libs/xdg/Config.py delete mode 100644 libs/xdg/DesktopEntry.py delete mode 100644 libs/xdg/Exceptions.py delete mode 100644 libs/xdg/INSTALL delete mode 100644 libs/xdg/IconTheme.py delete mode 100644 libs/xdg/IniFile.py delete mode 100644 libs/xdg/Locale.py delete mode 100644 libs/xdg/Menu.py delete mode 100644 libs/xdg/MenuEditor.py delete mode 100644 libs/xdg/Mime.py delete mode 100644 libs/xdg/README delete mode 100644 libs/xdg/RecentFiles.py delete mode 100644 libs/xdg/__init__.py delete mode 100644 libs/xdg/util.py diff --git a/bazarr/get_subtitle/refiners/ffprobe.py b/bazarr/get_subtitle/refiners/ffprobe.py index 397da0f04..033c8eade 100644 --- a/bazarr/get_subtitle/refiners/ffprobe.py +++ b/bazarr/get_subtitle/refiners/ffprobe.py @@ -52,7 +52,10 @@ def refine_from_ffprobe(path, video): if isinstance(data['ffprobe']['video'][0]['frame_rate'], float): video.fps = data['ffprobe']['video'][0]['frame_rate'] else: - video.fps = data['ffprobe']['video'][0]['frame_rate'].magnitude + try: + video.fps = data['ffprobe']['video'][0]['frame_rate'].magnitude + except AttributeError: + video.fps = data['ffprobe']['video'][0]['frame_rate'] if 'audio' not in data['ffprobe']: logging.debug('BAZARR FFprobe was unable to find audio tracks in the file!') diff --git a/bazarr/init.py b/bazarr/init.py index 2d94b858c..98a920c2d 100644 --- a/bazarr/init.py +++ b/bazarr/init.py @@ -184,9 +184,6 @@ def init_binaries(): except Exception: logging.debug("custom check failed for: %s", exe) - rarfile.OPEN_ARGS = rarfile.ORIG_OPEN_ARGS - rarfile.EXTRACT_ARGS = rarfile.ORIG_EXTRACT_ARGS - rarfile.TEST_ARGS = rarfile.ORIG_TEST_ARGS logging.debug("Using UnRAR from: %s", exe) unrar = exe diff --git a/bazarr/logger.py b/bazarr/logger.py index c03f3270a..0285c731a 100644 --- a/bazarr/logger.py +++ b/bazarr/logger.py @@ -7,6 +7,8 @@ import platform import warnings from logging.handlers import TimedRotatingFileHandler +from pytz_deprecation_shim import PytzUsageWarning + from get_args import args from config import settings @@ -55,6 +57,7 @@ class NoExceptionFormatter(logging.Formatter): def configure_logging(debug=False): warnings.simplefilter('ignore', category=ResourceWarning) + warnings.simplefilter('ignore', category=PytzUsageWarning) if not debug: log_level = "INFO" diff --git a/libs/_markerlib/__init__.py b/libs/_markerlib/__init__.py deleted file mode 100644 index e2b237b1f..000000000 --- a/libs/_markerlib/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -try: - import ast - from _markerlib.markers import default_environment, compile, interpret -except ImportError: - if 'ast' in globals(): - raise - def default_environment(): - return {} - def compile(marker): - def marker_fn(environment=None, override=None): - # 'empty markers are True' heuristic won't install extra deps. - return not marker.strip() - marker_fn.__doc__ = marker - return marker_fn - def interpret(marker, environment=None, override=None): - return compile(marker)() diff --git a/libs/_markerlib/markers.py b/libs/_markerlib/markers.py deleted file mode 100644 index fa837061e..000000000 --- a/libs/_markerlib/markers.py +++ /dev/null @@ -1,119 +0,0 @@ -# -*- coding: utf-8 -*- -"""Interpret PEP 345 environment markers. - -EXPR [in|==|!=|not in] EXPR [or|and] ... - -where EXPR belongs to any of those: - - python_version = '%s.%s' % (sys.version_info[0], sys.version_info[1]) - python_full_version = sys.version.split()[0] - os.name = os.name - sys.platform = sys.platform - platform.version = platform.version() - platform.machine = platform.machine() - platform.python_implementation = platform.python_implementation() - a free string, like '2.6', or 'win32' -""" - -__all__ = ['default_environment', 'compile', 'interpret'] - -import ast -import os -import platform -import sys -import weakref - -_builtin_compile = compile - -try: - from platform import python_implementation -except ImportError: - if os.name == "java": - # Jython 2.5 has ast module, but not platform.python_implementation() function. - def python_implementation(): - return "Jython" - else: - raise - - -# restricted set of variables -_VARS = {'sys.platform': sys.platform, - 'python_version': '%s.%s' % sys.version_info[:2], - # FIXME parsing sys.platform is not reliable, but there is no other - # way to get e.g. 2.7.2+, and the PEP is defined with sys.version - 'python_full_version': sys.version.split(' ', 1)[0], - 'os.name': os.name, - 'platform.version': platform.version(), - 'platform.machine': platform.machine(), - 'platform.python_implementation': python_implementation(), - 'extra': None # wheel extension - } - -for var in list(_VARS.keys()): - if '.' in var: - _VARS[var.replace('.', '_')] = _VARS[var] - -def default_environment(): - """Return copy of default PEP 385 globals dictionary.""" - return dict(_VARS) - -class ASTWhitelist(ast.NodeTransformer): - def __init__(self, statement): - self.statement = statement # for error messages - - ALLOWED = (ast.Compare, ast.BoolOp, ast.Attribute, ast.Name, ast.Load, ast.Str) - # Bool operations - ALLOWED += (ast.And, ast.Or) - # Comparison operations - ALLOWED += (ast.Eq, ast.Gt, ast.GtE, ast.In, ast.Is, ast.IsNot, ast.Lt, ast.LtE, ast.NotEq, ast.NotIn) - - def visit(self, node): - """Ensure statement only contains allowed nodes.""" - if not isinstance(node, self.ALLOWED): - raise SyntaxError('Not allowed in environment markers.\n%s\n%s' % - (self.statement, - (' ' * node.col_offset) + '^')) - return ast.NodeTransformer.visit(self, node) - - def visit_Attribute(self, node): - """Flatten one level of attribute access.""" - new_node = ast.Name("%s.%s" % (node.value.id, node.attr), node.ctx) - return ast.copy_location(new_node, node) - -def parse_marker(marker): - tree = ast.parse(marker, mode='eval') - new_tree = ASTWhitelist(marker).generic_visit(tree) - return new_tree - -def compile_marker(parsed_marker): - return _builtin_compile(parsed_marker, '', 'eval', - dont_inherit=True) - -_cache = weakref.WeakValueDictionary() - -def compile(marker): - """Return compiled marker as a function accepting an environment dict.""" - try: - return _cache[marker] - except KeyError: - pass - if not marker.strip(): - def marker_fn(environment=None, override=None): - """""" - return True - else: - compiled_marker = compile_marker(parse_marker(marker)) - def marker_fn(environment=None, override=None): - """override updates environment""" - if override is None: - override = {} - if environment is None: - environment = default_environment() - environment.update(override) - return eval(compiled_marker, environment) - marker_fn.__doc__ = marker - _cache[marker] = marker_fn - return _cache[marker] - -def interpret(marker, environment=None): - return compile(marker)(environment) diff --git a/libs/appdirs.py b/libs/appdirs.py index 805fb2abd..2acd1debe 100644 --- a/libs/appdirs.py +++ b/libs/appdirs.py @@ -13,8 +13,8 @@ See for details and usage. # - Mac OS X: http://developer.apple.com/documentation/MacOSX/Conceptual/BPFileSystem/index.html # - XDG spec for Un*x: http://standards.freedesktop.org/basedir-spec/basedir-spec-latest.html -__version_info__ = (1, 4, 3) -__version__ = '.'.join(map(str, __version_info__)) +__version__ = "1.4.4" +__version_info__ = tuple(int(segment) for segment in __version__.split(".")) import sys @@ -98,7 +98,7 @@ def user_data_dir(appname=None, appauthor=None, version=None, roaming=False): def site_data_dir(appname=None, appauthor=None, version=None, multipath=False): - """Return full path to the user-shared data dir for this application. + r"""Return full path to the user-shared data dir for this application. "appname" is the name of application. If None, just the system directory is returned. @@ -204,7 +204,7 @@ def user_config_dir(appname=None, appauthor=None, version=None, roaming=False): def site_config_dir(appname=None, appauthor=None, version=None, multipath=False): - """Return full path to the user-shared data dir for this application. + r"""Return full path to the user-shared data dir for this application. "appname" is the name of application. If None, just the system directory is returned. diff --git a/libs/arghelper.py b/libs/arghelper.py deleted file mode 100644 index 882d2c031..000000000 --- a/libs/arghelper.py +++ /dev/null @@ -1,116 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -# Copyright (c) 2014-2016 The arghelper developers. All rights reserved. -# Project site: https://github.com/questrail/arghelper -# Use of this source code is governed by a MIT-style license that -# can be found in the LICENSE.txt file for the project. -"""Provide helper functions for argparse - -""" - -# Try to future proof code so that it's Python 3.x ready -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division -from __future__ import absolute_import - -# Standard module imports -import argparse -import sys -import os - - -def extant_file(arg): - """Facade for extant_item(arg, arg_type="file") - """ - return extant_item(arg, "file") - - -def extant_dir(arg): - """Facade for extant_item(arg, arg_type="directory") - """ - return extant_item(arg, "directory") - - -def extant_item(arg, arg_type): - """Determine if parser argument is an existing file or directory. - - This technique comes from http://stackoverflow.com/a/11541450/95592 - and from http://stackoverflow.com/a/11541495/95592 - - Args: - arg: parser argument containing filename to be checked - arg_type: string of either "file" or "directory" - - Returns: - If the file exists, return the filename or directory. - - Raises: - If the file does not exist, raise a parser error. - """ - if arg_type == "file": - if not os.path.isfile(arg): - raise argparse.ArgumentError( - None, - "The file {arg} does not exist.".format(arg=arg)) - else: - # File exists so return the filename - return arg - elif arg_type == "directory": - if not os.path.isdir(arg): - raise argparse.ArgumentError( - None, - "The directory {arg} does not exist.".format(arg=arg)) - else: - # Directory exists so return the directory name - return arg - - -def parse_config_input_output(args=sys.argv): - """Parse the args using the config_file, input_dir, output_dir pattern - - Args: - args: sys.argv - - Returns: - The populated namespace object from parser.parse_args(). - - Raises: - TBD - """ - parser = argparse.ArgumentParser( - description='Process the input files using the given config') - parser.add_argument( - 'config_file', - help='Configuration file.', - metavar='FILE', type=extant_file) - parser.add_argument( - 'input_dir', - help='Directory containing the input files.', - metavar='DIR', type=extant_dir) - parser.add_argument( - 'output_dir', - help='Directory where the output files should be saved.', - metavar='DIR', type=extant_dir) - return parser.parse_args(args[1:]) - - -def parse_config(args=sys.argv): - """Parse the args using the config_file pattern - - Args: - args: sys.argv - - Returns: - The populated namespace object from parser.parse_args(). - - Raises: - TBD - """ - parser = argparse.ArgumentParser( - description='Read in the config file') - parser.add_argument( - 'config_file', - help='Configuration file.', - metavar='FILE', type=extant_file) - return parser.parse_args(args[1:]) diff --git a/libs/asio/__init__.py b/libs/asio/__init__.py deleted file mode 100644 index ca8ded235..000000000 --- a/libs/asio/__init__.py +++ /dev/null @@ -1,61 +0,0 @@ -# Copyright 2013 Dean Gardiner -# -# 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 asio.file import SEEK_ORIGIN_CURRENT -from asio.file_opener import FileOpener -from asio.open_parameters import OpenParameters -from asio.interfaces.posix import PosixInterface -from asio.interfaces.windows import WindowsInterface - -import os - - -class ASIO(object): - platform_handler = None - - @classmethod - def get_handler(cls): - if cls.platform_handler: - return cls.platform_handler - - if os.name == 'nt': - cls.platform_handler = WindowsInterface - elif os.name == 'posix': - cls.platform_handler = PosixInterface - else: - raise NotImplementedError() - - return cls.platform_handler - - @classmethod - def open(cls, file_path, opener=True, parameters=None): - """Open file - - :type file_path: str - - :param opener: Use FileOpener, for use with the 'with' statement - :type opener: bool - - :rtype: asio.file.File - """ - if not parameters: - parameters = OpenParameters() - - if opener: - return FileOpener(file_path, parameters) - - return ASIO.get_handler().open( - file_path, - parameters=parameters.handlers.get(ASIO.get_handler()) - ) diff --git a/libs/asio/file.py b/libs/asio/file.py deleted file mode 100644 index a44970815..000000000 --- a/libs/asio/file.py +++ /dev/null @@ -1,92 +0,0 @@ -# Copyright 2013 Dean Gardiner -# -# 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 io import RawIOBase -import time - -DEFAULT_BUFFER_SIZE = 4096 - -SEEK_ORIGIN_BEGIN = 0 -SEEK_ORIGIN_CURRENT = 1 -SEEK_ORIGIN_END = 2 - - -class ReadTimeoutError(Exception): - pass - - -class File(RawIOBase): - platform_handler = None - - def __init__(self, *args, **kwargs): - super(File, self).__init__(*args, **kwargs) - - def get_handler(self): - """ - :rtype: asio.interfaces.base.Interface - """ - if not self.platform_handler: - raise ValueError() - - return self.platform_handler - - def get_size(self): - """Get the current file size - - :rtype: int - """ - return self.get_handler().get_size(self) - - def get_path(self): - """Get the path of this file - - :rtype: str - """ - return self.get_handler().get_path(self) - - def seek(self, offset, origin): - """Sets a reference point of a file to the given value. - - :param offset: The point relative to origin to move - :type offset: int - - :param origin: Reference point to seek (SEEK_ORIGIN_BEGIN, SEEK_ORIGIN_CURRENT, SEEK_ORIGIN_END) - :type origin: int - """ - return self.get_handler().seek(self, offset, origin) - - def read(self, n=-1): - """Read up to n bytes from the object and return them. - - :type n: int - :rtype: str - """ - return self.get_handler().read(self, n) - - def readinto(self, b): - """Read up to len(b) bytes into bytearray b and return the number of bytes read.""" - data = self.read(len(b)) - - if data is None: - return None - - b[:len(data)] = data - return len(data) - - def close(self): - """Close the file handle""" - return self.get_handler().close(self) - - def readable(self, *args, **kwargs): - return True diff --git a/libs/asio/file_opener.py b/libs/asio/file_opener.py deleted file mode 100644 index 990cc9804..000000000 --- a/libs/asio/file_opener.py +++ /dev/null @@ -1,21 +0,0 @@ -class FileOpener(object): - def __init__(self, file_path, parameters=None): - self.file_path = file_path - self.parameters = parameters - - self.file = None - - def __enter__(self): - self.file = ASIO.get_handler().open( - self.file_path, - self.parameters.handlers.get(ASIO.get_handler()) - ) - - return self.file - - def __exit__(self, exc_type, exc_val, exc_tb): - if not self.file: - return - - self.file.close() - self.file = None diff --git a/libs/asio/interfaces/base.py b/libs/asio/interfaces/base.py deleted file mode 100644 index 6188b000f..000000000 --- a/libs/asio/interfaces/base.py +++ /dev/null @@ -1,41 +0,0 @@ -# Copyright 2013 Dean Gardiner -# -# 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 asio.file import DEFAULT_BUFFER_SIZE - - -class Interface(object): - @classmethod - def open(cls, file_path, parameters=None): - raise NotImplementedError() - - @classmethod - def get_size(cls, fp): - raise NotImplementedError() - - @classmethod - def get_path(cls, fp): - raise NotImplementedError() - - @classmethod - def seek(cls, fp, pointer, distance): - raise NotImplementedError() - - @classmethod - def read(cls, fp, n=DEFAULT_BUFFER_SIZE): - raise NotImplementedError() - - @classmethod - def close(cls, fp): - raise NotImplementedError() diff --git a/libs/asio/interfaces/posix.py b/libs/asio/interfaces/posix.py deleted file mode 100644 index b235c02b9..000000000 --- a/libs/asio/interfaces/posix.py +++ /dev/null @@ -1,123 +0,0 @@ -# Copyright 2013 Dean Gardiner -# -# 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 asio.file import File, DEFAULT_BUFFER_SIZE -from asio.interfaces.base import Interface - -import sys -import os - -if os.name == 'posix': - import select - - # fcntl is only required on darwin - if sys.platform == 'darwin': - import fcntl - -F_GETPATH = 50 - - -class PosixInterface(Interface): - @classmethod - def open(cls, file_path, parameters=None): - """ - :type file_path: str - :rtype: asio.interfaces.posix.PosixFile - """ - if not parameters: - parameters = {} - - if not parameters.get('mode'): - parameters.pop('mode') - - if not parameters.get('buffering'): - parameters.pop('buffering') - - fd = os.open(file_path, os.O_RDONLY | os.O_NONBLOCK) - - return PosixFile(fd) - - @classmethod - def get_size(cls, fp): - """ - :type fp: asio.interfaces.posix.PosixFile - :rtype: int - """ - return os.fstat(fp.fd).st_size - - @classmethod - def get_path(cls, fp): - """ - :type fp: asio.interfaces.posix.PosixFile - :rtype: int - """ - - # readlink /dev/fd fails on darwin, so instead use fcntl F_GETPATH - if sys.platform == 'darwin': - return fcntl.fcntl(fp.fd, F_GETPATH, '\0' * 1024).rstrip('\0') - - # Use /proc/self/fd if available - if os.path.lexists("/proc/self/fd/"): - return os.readlink("/proc/self/fd/%s" % fp.fd) - - # Fallback to /dev/fd - if os.path.lexists("/dev/fd/"): - return os.readlink("/dev/fd/%s" % fp.fd) - - raise NotImplementedError('Environment not supported (fdescfs not mounted?)') - - @classmethod - def seek(cls, fp, offset, origin): - """ - :type fp: asio.interfaces.posix.PosixFile - :type offset: int - :type origin: int - """ - os.lseek(fp.fd, offset, origin) - - @classmethod - def read(cls, fp, n=DEFAULT_BUFFER_SIZE): - """ - :type fp: asio.interfaces.posix.PosixFile - :type n: int - :rtype: str - """ - r, w, x = select.select([fp.fd], [], [], 5) - - if r: - return os.read(fp.fd, n) - - return None - - @classmethod - def close(cls, fp): - """ - :type fp: asio.interfaces.posix.PosixFile - """ - os.close(fp.fd) - - -class PosixFile(File): - platform_handler = PosixInterface - - def __init__(self, fd, *args, **kwargs): - """ - :type fd: asio.file.File - """ - super(PosixFile, self).__init__(*args, **kwargs) - - self.fd = fd - - def __str__(self): - return "" % self.fd diff --git a/libs/asio/interfaces/windows/__init__.py b/libs/asio/interfaces/windows/__init__.py deleted file mode 100644 index 20ce0bdc2..000000000 --- a/libs/asio/interfaces/windows/__init__.py +++ /dev/null @@ -1,201 +0,0 @@ -# Copyright 2013 Dean Gardiner -# -# 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 asio.file import File, DEFAULT_BUFFER_SIZE -from asio.interfaces.base import Interface - -import os - - -NULL = 0 - -if os.name == 'nt': - from asio.interfaces.windows.interop import WindowsInterop - - -class WindowsInterface(Interface): - @classmethod - def open(cls, file_path, parameters=None): - """ - :type file_path: str - :rtype: asio.interfaces.windows.WindowsFile - """ - if not parameters: - parameters = {} - - return WindowsFile(WindowsInterop.create_file( - file_path, - parameters.get('desired_access', WindowsInterface.GenericAccess.READ), - parameters.get('share_mode', WindowsInterface.ShareMode.ALL), - parameters.get('creation_disposition', WindowsInterface.CreationDisposition.OPEN_EXISTING), - parameters.get('flags_and_attributes', NULL) - )) - - @classmethod - def get_size(cls, fp): - """ - :type fp: asio.interfaces.windows.WindowsFile - :rtype: int - """ - return WindowsInterop.get_file_size(fp.handle) - - @classmethod - def get_path(cls, fp): - """ - :type fp: asio.interfaces.windows.WindowsFile - :rtype: str - """ - - if not fp.file_map: - fp.file_map = WindowsInterop.create_file_mapping(fp.handle, WindowsInterface.Protection.READONLY) - - if not fp.map_view: - fp.map_view = WindowsInterop.map_view_of_file(fp.file_map, WindowsInterface.FileMapAccess.READ, 1) - - file_name = WindowsInterop.get_mapped_file_name(fp.map_view) - - return file_name - - @classmethod - def seek(cls, fp, offset, origin): - """ - :type fp: asio.interfaces.windows.WindowsFile - :type offset: int - :type origin: int - :rtype: int - """ - - return WindowsInterop.set_file_pointer( - fp.handle, - offset, - origin - ) - - @classmethod - def read(cls, fp, n=DEFAULT_BUFFER_SIZE): - """ - :type fp: asio.interfaces.windows.WindowsFile - :type n: int - :rtype: str - """ - return WindowsInterop.read(fp.handle, n) - - @classmethod - def read_into(cls, fp, b): - """ - :type fp: asio.interfaces.windows.WindowsFile - :type b: str - :rtype: int - """ - return WindowsInterop.read_into(fp.handle, b) - - @classmethod - def close(cls, fp): - """ - :type fp: asio.interfaces.windows.WindowsFile - :rtype: bool - """ - if fp.map_view: - WindowsInterop.unmap_view_of_file(fp.map_view) - - if fp.file_map: - WindowsInterop.close_handle(fp.file_map) - - return bool(WindowsInterop.close_handle(fp.handle)) - - class GenericAccess(object): - READ = 0x80000000 - WRITE = 0x40000000 - EXECUTE = 0x20000000 - ALL = 0x10000000 - - class ShareMode(object): - READ = 0x00000001 - WRITE = 0x00000002 - DELETE = 0x00000004 - ALL = READ | WRITE | DELETE - - class CreationDisposition(object): - CREATE_NEW = 1 - CREATE_ALWAYS = 2 - OPEN_EXISTING = 3 - OPEN_ALWAYS = 4 - TRUNCATE_EXISTING = 5 - - class Attribute(object): - READONLY = 0x00000001 - HIDDEN = 0x00000002 - SYSTEM = 0x00000004 - DIRECTORY = 0x00000010 - ARCHIVE = 0x00000020 - DEVICE = 0x00000040 - NORMAL = 0x00000080 - TEMPORARY = 0x00000100 - SPARSE_FILE = 0x00000200 - REPARSE_POINT = 0x00000400 - COMPRESSED = 0x00000800 - OFFLINE = 0x00001000 - NOT_CONTENT_INDEXED = 0x00002000 - ENCRYPTED = 0x00004000 - - class Flag(object): - WRITE_THROUGH = 0x80000000 - OVERLAPPED = 0x40000000 - NO_BUFFERING = 0x20000000 - RANDOM_ACCESS = 0x10000000 - SEQUENTIAL_SCAN = 0x08000000 - DELETE_ON_CLOSE = 0x04000000 - BACKUP_SEMANTICS = 0x02000000 - POSIX_SEMANTICS = 0x01000000 - OPEN_REPARSE_POINT = 0x00200000 - OPEN_NO_RECALL = 0x00100000 - FIRST_PIPE_INSTANCE = 0x00080000 - - class Protection(object): - NOACCESS = 0x01 - READONLY = 0x02 - READWRITE = 0x04 - WRITECOPY = 0x08 - EXECUTE = 0x10 - EXECUTE_READ = 0x20, - EXECUTE_READWRITE = 0x40 - EXECUTE_WRITECOPY = 0x80 - GUARD = 0x100 - NOCACHE = 0x200 - WRITECOMBINE = 0x400 - - class FileMapAccess(object): - COPY = 0x0001 - WRITE = 0x0002 - READ = 0x0004 - ALL_ACCESS = 0x001f - EXECUTE = 0x0020 - - -class WindowsFile(File): - platform_handler = WindowsInterface - - def __init__(self, handle, *args, **kwargs): - super(WindowsFile, self).__init__(*args, **kwargs) - - self.handle = handle - - self.file_map = None - self.map_view = None - - def readinto(self, b): - return self.get_handler().read_into(self, b) - - def __str__(self): - return "" % self.handle diff --git a/libs/asio/interfaces/windows/interop.py b/libs/asio/interfaces/windows/interop.py deleted file mode 100644 index 7bce197c2..000000000 --- a/libs/asio/interfaces/windows/interop.py +++ /dev/null @@ -1,230 +0,0 @@ -# Copyright 2013 Dean Gardiner -# -# 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 ctypes.wintypes import * -from ctypes import * -import logging - -log = logging.getLogger(__name__) - - -CreateFileW = windll.kernel32.CreateFileW -CreateFileW.argtypes = (LPCWSTR, DWORD, DWORD, c_void_p, DWORD, DWORD, HANDLE) -CreateFileW.restype = HANDLE - -ReadFile = windll.kernel32.ReadFile -ReadFile.argtypes = (HANDLE, c_void_p, DWORD, POINTER(DWORD), HANDLE) -ReadFile.restype = BOOL - - -NULL = 0 -MAX_PATH = 260 -DEFAULT_BUFFER_SIZE = 4096 -LPSECURITY_ATTRIBUTES = c_void_p - - -class WindowsInterop(object): - ri_buffer = None - - @classmethod - def create_file(cls, path, desired_access, share_mode, creation_disposition, flags_and_attributes): - h = CreateFileW( - path, - desired_access, - share_mode, - NULL, - creation_disposition, - flags_and_attributes, - NULL - ) - - error = GetLastError() - if error != 0: - raise Exception('[WindowsASIO.open] "%s"' % FormatError(error)) - - return h - - @classmethod - def read(cls, handle, buf_size=DEFAULT_BUFFER_SIZE): - buf = create_string_buffer(buf_size) - bytes_read = c_ulong(0) - - success = ReadFile(handle, buf, buf_size, byref(bytes_read), NULL) - - error = GetLastError() - if error: - log.debug('read_file - error: (%s) "%s"', error, FormatError(error)) - - if not success and error: - raise Exception('[WindowsInterop.read_file] (%s) "%s"' % (error, FormatError(error))) - - # Return if we have a valid buffer - if success and bytes_read.value: - return buf.value - - return None - - @classmethod - def read_into(cls, handle, b): - if cls.ri_buffer is None or len(cls.ri_buffer) < len(b): - cls.ri_buffer = create_string_buffer(len(b)) - - bytes_read = c_ulong(0) - - success = ReadFile(handle, cls.ri_buffer, len(b), byref(bytes_read), NULL) - bytes_read = int(bytes_read.value) - - b[:bytes_read] = cls.ri_buffer[:bytes_read] - - error = GetLastError() - - if not success and error: - raise Exception('[WindowsInterop.read_file] (%s) "%s"' % (error, FormatError(error))) - - # Return if we have a valid buffer - if success and bytes_read: - return bytes_read - - return None - - @classmethod - def set_file_pointer(cls, handle, distance, method): - pos_high = DWORD(NULL) - - result = windll.kernel32.SetFilePointer( - handle, - c_ulong(distance), - byref(pos_high), - DWORD(method) - ) - - if result == -1: - raise Exception('[WindowsASIO.seek] INVALID_SET_FILE_POINTER: "%s"' % FormatError(GetLastError())) - - return result - - @classmethod - def get_file_size(cls, handle): - return windll.kernel32.GetFileSize( - handle, - DWORD(NULL) - ) - - @classmethod - def close_handle(cls, handle): - return windll.kernel32.CloseHandle(handle) - - @classmethod - def create_file_mapping(cls, handle, protect, maximum_size_high=0, maximum_size_low=1): - return HANDLE(windll.kernel32.CreateFileMappingW( - handle, - LPSECURITY_ATTRIBUTES(NULL), - DWORD(protect), - DWORD(maximum_size_high), - DWORD(maximum_size_low), - LPCSTR(NULL) - )) - - @classmethod - def map_view_of_file(cls, map_handle, desired_access, num_bytes, file_offset_high=0, file_offset_low=0): - return HANDLE(windll.kernel32.MapViewOfFile( - map_handle, - DWORD(desired_access), - DWORD(file_offset_high), - DWORD(file_offset_low), - num_bytes - )) - - @classmethod - def unmap_view_of_file(cls, view_handle): - return windll.kernel32.UnmapViewOfFile(view_handle) - - @classmethod - def get_mapped_file_name(cls, view_handle, translate_device_name=True): - buf = create_string_buffer(MAX_PATH + 1) - - result = windll.psapi.GetMappedFileNameW( - cls.get_current_process(), - view_handle, - buf, - MAX_PATH - ) - - # Raise exception on error - error = GetLastError() - if result == 0: - raise Exception(FormatError(error)) - - # Retrieve a clean file name (skipping over NUL bytes) - file_name = cls.clean_buffer_value(buf) - - # If we are not translating the device name return here - if not translate_device_name: - return file_name - - drives = cls.get_logical_drive_strings() - - # Find the drive matching the file_name device name - translated = False - for drive in drives: - device_name = cls.query_dos_device(drive) - - if file_name.startswith(device_name): - file_name = drive + file_name[len(device_name):] - translated = True - break - - if not translated: - raise Exception('Unable to translate device name') - - return file_name - - @classmethod - def get_logical_drive_strings(cls, buf_size=512): - buf = create_string_buffer(buf_size) - - result = windll.kernel32.GetLogicalDriveStringsW(buf_size, buf) - - error = GetLastError() - if result == 0: - raise Exception(FormatError(error)) - - drive_strings = cls.clean_buffer_value(buf) - return [dr for dr in drive_strings.split('\\') if dr != ''] - - @classmethod - def query_dos_device(cls, drive, buf_size=MAX_PATH): - buf = create_string_buffer(buf_size) - - result = windll.kernel32.QueryDosDeviceA( - drive, - buf, - buf_size - ) - - return cls.clean_buffer_value(buf) - - @classmethod - def get_current_process(cls): - return HANDLE(windll.kernel32.GetCurrentProcess()) - - @classmethod - def clean_buffer_value(cls, buf): - value = "" - - for ch in buf.raw: - if ord(ch) != 0: - value += ch - - return value diff --git a/libs/asio/open_parameters.py b/libs/asio/open_parameters.py deleted file mode 100644 index a1463854d..000000000 --- a/libs/asio/open_parameters.py +++ /dev/null @@ -1,47 +0,0 @@ -from asio.interfaces.posix import PosixInterface -from asio.interfaces.windows import WindowsInterface - - -class OpenParameters(object): - def __init__(self): - self.handlers = {} - - # Update handler_parameters with defaults - self.posix() - self.windows() - - def posix(self, mode=None, buffering=None): - """ - :type mode: str - :type buffering: int - """ - self.handlers.update({PosixInterface: { - 'mode': mode, - 'buffering': buffering - }}) - - def windows(self, desired_access=WindowsInterface.GenericAccess.READ, - share_mode=WindowsInterface.ShareMode.ALL, - creation_disposition=WindowsInterface.CreationDisposition.OPEN_EXISTING, - flags_and_attributes=0): - - """ - :param desired_access: WindowsInterface.DesiredAccess - :type desired_access: int - - :param share_mode: WindowsInterface.ShareMode - :type share_mode: int - - :param creation_disposition: WindowsInterface.CreationDisposition - :type creation_disposition: int - - :param flags_and_attributes: WindowsInterface.Attribute, WindowsInterface.Flag - :type flags_and_attributes: int - """ - - self.handlers.update({WindowsInterface: { - 'desired_access': desired_access, - 'share_mode': share_mode, - 'creation_disposition': creation_disposition, - 'flags_and_attributes': flags_and_attributes - }}) diff --git a/libs/auditok/__init__.py b/libs/auditok/__init__.py index 4ea697b77..edd336cc3 100644 --- a/libs/auditok/__init__.py +++ b/libs/auditok/__init__.py @@ -2,20 +2,16 @@ :author: Amine SEHILI -2015-2016 +2015-2021 :License: -This package is published under GNU GPL Version 3. +This package is published under the MIT license. """ -from __future__ import absolute_import from .core import * from .io import * from .util import * -from . import dataset from .exceptions import * -__version__ = "0.1.5" - - +__version__ = "0.2.0" diff --git a/libs/auditok/cmdline.py b/libs/auditok/cmdline.py index b6a51d11b..7e7450762 100755 --- a/libs/auditok/cmdline.py +++ b/libs/auditok/cmdline.py @@ -1,789 +1,428 @@ #!/usr/bin/env python # encoding: utf-8 -''' -auditok.auditok -- Audio Activity Detection tool - -auditok.auditok is a program that can be used for Audio/Acoustic activity detection. -It can read audio data from audio files as well as from built-in device(s) or standard input +""" +`auditok` -- An Audio Activity Detection tool +`auditok` is a program that can be used for Audio/Acoustic +activity detection. It can read audio data from audio files as well +as from the microphone or standard input. @author: Mohamed El Amine SEHILI - -@copyright: 2015 Mohamed El Amine SEHILI - -@license: GPL v3 - +@copyright: 2015-2021 Mohamed El Amine SEHILI +@license: MIT @contact: amine.sehili@gmail.com -@deffield updated: 02 Dec 2015 -''' +@deffield updated: 01 Mar 2021 +""" import sys import os - -from optparse import OptionParser, OptionGroup -from threading import Thread -import tempfile -import wave +from argparse import ArgumentParser import time import threading -import logging -try: - import future - from queue import Queue, Empty -except ImportError: - if sys.version_info >= (3, 0): - from queue import Queue, Empty - else: - from Queue import Queue, Empty +from auditok import __version__, AudioRegion +from .util import AudioDataSource +from .exceptions import EndOfProcessing, AudioEncodingWarning +from .io import player_for +from .cmdline_util import make_logger, make_kwargs, initialize_workers +from . import workers -try: - from pydub import AudioSegment - WITH_PYDUB = True -except ImportError: - WITH_PYDUB = False - - -from .core import StreamTokenizer -from .io import PyAudioSource, BufferAudioSource, StdinAudioSource, player_for -from .util import ADSFactory, AudioEnergyValidator -from auditok import __version__ as version __all__ = [] -__version__ = version -__date__ = '2015-11-23' -__updated__ = '2015-03-11' - -DEBUG = 0 -TESTRUN = 1 -PROFILE = 0 - -LOGGER_NAME = "AUDITOK_LOGGER" - -class AudioFileFormatError(Exception): - pass - -class TimeFormatError(Exception): - pass - -def file_to_audio_source(filename, filetype=None, **kwargs): - - lower_fname = filename.lower() - rawdata = False - - if filetype is not None: - filetype = filetype.lower() - - if filetype == "raw" or (filetype is None and lower_fname.endswith(".raw")): - - srate = kwargs.pop("sampling_rate", None) - if srate is None: - srate = kwargs.pop("sr", None) - - swidth = kwargs.pop("sample_width", None) - if swidth is None: - swidth = kwargs.pop("sw", None) - - ch = kwargs.pop("channels", None) - if ch is None: - ch = kwargs.pop("ch", None) - - if None in (swidth, srate, ch): - raise Exception("All audio parameters are required for raw data") - - data = open(filename).read() - rawdata = True - - # try first with pydub - if WITH_PYDUB: - - use_channel = kwargs.pop("use_channel", None) - if use_channel is None: - use_channel = kwargs.pop("uc", None) - - if use_channel is None: - use_channel = 1 - else: - try: - use_channel = int(use_channel) - except ValueError: - pass - - if not isinstance(use_channel, (int)) and not use_channel.lower() in ["left", "right", "mix"] : - raise ValueError("channel must be an integer or one of 'left', 'right' or 'mix'") - - asegment = None - - if rawdata: - asegment = AudioSegment(data, sample_width=swidth, frame_rate=srate, channels=ch) - if filetype in("wave", "wav") or (filetype is None and lower_fname.endswith(".wav")): - asegment = AudioSegment.from_wav(filename) - elif filetype == "mp3" or (filetype is None and lower_fname.endswith(".mp3")): - asegment = AudioSegment.from_mp3(filename) - elif filetype == "ogg" or (filetype is None and lower_fname.endswith(".ogg")): - asegment = AudioSegment.from_ogg(filename) - elif filetype == "flv" or (filetype is None and lower_fname.endswith(".flv")): - asegment = AudioSegment.from_flv(filename) - else: - asegment = AudioSegment.from_file(filename) - - if asegment.channels > 1: - - if isinstance(use_channel, int): - if use_channel > asegment.channels: - raise ValueError("Can not use channel '{0}', audio file has only {1} channels".format(use_channel, asegment.channels)) - else: - asegment = asegment.split_to_mono()[use_channel - 1] - else: - ch_lower = use_channel.lower() - - if ch_lower == "mix": - asegment = asegment.set_channels(1) - - elif use_channel.lower() == "left": - asegment = asegment.split_to_mono()[0] - - elif use_channel.lower() == "right": - asegment = asegment.split_to_mono()[1] - - return BufferAudioSource(data_buffer = asegment._data, - sampling_rate = asegment.frame_rate, - sample_width = asegment.sample_width, - channels = asegment.channels) - # fall back to standard python - else: - if rawdata: - if ch != 1: - raise ValueError("Cannot handle multi-channel audio without pydub") - return BufferAudioSource(data, srate, swidth, ch) - - if filetype in ("wav", "wave") or (filetype is None and lower_fname.endswith(".wav")): - - wfp = wave.open(filename) - - ch = wfp.getnchannels() - if ch != 1: - wfp.close() - raise ValueError("Cannot handle multi-channel audio without pydub") - - srate = wfp.getframerate() - swidth = wfp.getsampwidth() - data = wfp.readframes(wfp.getnframes()) - wfp.close() - return BufferAudioSource(data, srate, swidth, ch) - - raise AudioFileFormatError("Cannot read audio file format") - - -def save_audio_data(data, filename, filetype=None, **kwargs): - - lower_fname = filename.lower() - if filetype is not None: - filetype = filetype.lower() - - # save raw data - if filetype == "raw" or (filetype is None and lower_fname.endswith(".raw")): - fp = open(filename, "w") - fp.write(data) - fp.close() - return - - # save other types of data - # requires all audio parameters - srate = kwargs.pop("sampling_rate", None) - if srate is None: - srate = kwargs.pop("sr", None) - - swidth = kwargs.pop("sample_width", None) - if swidth is None: - swidth = kwargs.pop("sw", None) - - ch = kwargs.pop("channels", None) - if ch is None: - ch = kwargs.pop("ch", None) - - if None in (swidth, srate, ch): - raise Exception("All audio parameters are required to save no raw data") - - if filetype in ("wav", "wave") or (filetype is None and lower_fname.endswith(".wav")): - # use standard python's wave module - fp = wave.open(filename, "w") - fp.setnchannels(ch) - fp.setsampwidth(swidth) - fp.setframerate(srate) - fp.writeframes(data) - fp.close() - - elif WITH_PYDUB: - - asegment = AudioSegment(data, sample_width=swidth, frame_rate=srate, channels=ch) - asegment.export(filename, format=filetype) - - else: - raise AudioFileFormatError("cannot write file format {0} (file name: {1})".format(filetype, filename)) - - -def plot_all(signal, sampling_rate, energy_as_amp, detections=[], show=True, save_as=None): - - import matplotlib.pyplot as plt - import numpy as np - t = np.arange(0., np.ceil(float(len(signal))) / sampling_rate, 1./sampling_rate ) - if len(t) > len(signal): - t = t[: len(signal) - len(t)] - - for start, end in detections: - p = plt.axvspan(start, end, facecolor='g', ec = 'r', lw = 2, alpha=0.4) - - line = plt.axhline(y=energy_as_amp, lw=1, ls="--", c="r", label="Energy threshold as normalized amplitude") - plt.plot(t, signal) - legend = plt.legend(["Detection threshold"], bbox_to_anchor=(0., 1.02, 1., .102), loc=1, fontsize=16) - ax = plt.gca().add_artist(legend) - - plt.xlabel("Time (s)", fontsize=24) - plt.ylabel("Amplitude (normalized)", fontsize=24) - - if save_as is not None: - plt.savefig(save_as, dpi=120) - - if show: - plt.show() - - -def seconds_to_str_fromatter(_format): - """ - Accepted format directives: %i %s %m %h - """ - # check directives are correct - - if _format == "%S": - def _fromatter(seconds): - return "{:.2f}".format(seconds) - - elif _format == "%I": - def _fromatter(seconds): - return "{0}".format(int(seconds * 1000)) - - else: - _format = _format.replace("%h", "{hrs:02d}") - _format = _format.replace("%m", "{mins:02d}") - _format = _format.replace("%s", "{secs:02d}") - _format = _format.replace("%i", "{millis:03d}") - - try: - i = _format.index("%") - raise TimeFormatError("Unknow time format directive '{0}'".format(_format[i:i+2])) - except ValueError: - pass - - def _fromatter(seconds): - millis = int(seconds * 1000) - hrs, millis = divmod(millis, 3600000) - mins, millis = divmod(millis, 60000) - secs, millis = divmod(millis, 1000) - return _format.format(hrs=hrs, mins=mins, secs=secs, millis=millis) - - return _fromatter - - - -class Worker(Thread): - - def __init__(self, timeout=0.2, debug=False, logger=None): - self.timeout = timeout - self.debug = debug - self.logger = logger - - if self.debug and self.logger is None: - self.logger = logging.getLogger(LOGGER_NAME) - self.logger.setLevel(logging.DEBUG) - handler = logging.StreamHandler(sys.stdout) - self.logger.addHandler(handler) - - self._inbox = Queue() - self._stop_request = Queue() - Thread.__init__(self) - - - def debug_message(self, message): - self.logger.debug(message) - - def _stop_requested(self): - - try: - message = self._stop_request.get_nowait() - if message == "stop": - return True - - except Empty: - return False - - def stop(self): - self._stop_request.put("stop") - self.join() - - def send(self, message): - self._inbox.put(message) - - def _get_message(self): - try: - message = self._inbox.get(timeout=self.timeout) - return message - except Empty: - return None - - -class TokenizerWorker(Worker): - - END_OF_PROCESSING = "END_OF_PROCESSING" - - def __init__(self, ads, tokenizer, analysis_window, observers): - self.ads = ads - self.tokenizer = tokenizer - self.analysis_window = analysis_window - self.observers = observers - self._inbox = Queue() - self.count = 0 - Worker.__init__(self) - - def run(self): - - def notify_observers(data, start, end): - audio_data = b''.join(data) - self.count += 1 - - start_time = start * self.analysis_window - end_time = (end+1) * self.analysis_window - duration = (end - start + 1) * self.analysis_window - - # notify observers - for observer in self.observers: - observer.notify({"id" : self.count, - "audio_data" : audio_data, - "start" : start, - "end" : end, - "start_time" : start_time, - "end_time" : end_time, - "duration" : duration} - ) - - self.ads.open() - self.tokenizer.tokenize(data_source=self, callback=notify_observers) - for observer in self.observers: - observer.notify(TokenizerWorker.END_OF_PROCESSING) - - def add_observer(self, observer): - self.observers.append(observer) - - def remove_observer(self, observer): - self.observers.remove(observer) - - def read(self): - if self._stop_requested(): - return None - else: - return self.ads.read() - - -class PlayerWorker(Worker): - - def __init__(self, player, timeout=0.2, debug=False, logger=None): - self.player = player - Worker.__init__(self, timeout=timeout, debug=debug, logger=logger) - - def run(self): - while True: - if self._stop_requested(): - break - - message = self._get_message() - if message is not None: - if message == TokenizerWorker.END_OF_PROCESSING: - break - - audio_data = message.pop("audio_data", None) - start_time = message.pop("start_time", None) - end_time = message.pop("end_time", None) - dur = message.pop("duration", None) - _id = message.pop("id", None) - - if audio_data is not None: - if self.debug: - self.debug_message("[PLAY]: Detection {id} played (start:{start}, end:{end}, dur:{dur})".format(id=_id, - start="{:5.2f}".format(start_time), end="{:5.2f}".format(end_time), dur="{:5.2f}".format(dur))) - self.player.play(audio_data) - - def notify(self, message): - self.send(message) - - -class CommandLineWorker(Worker): - - def __init__(self, command, timeout=0.2, debug=False, logger=None): - self.command = command - Worker.__init__(self, timeout=timeout, debug=debug, logger=logger) - - def run(self): - while True: - if self._stop_requested(): - break - - message = self._get_message() - if message is not None: - if message == TokenizerWorker.END_OF_PROCESSING: - break - - audio_data = message.pop("audio_data", None) - _id = message.pop("id", None) - if audio_data is not None: - raw_audio_file = tempfile.NamedTemporaryFile(delete=False) - raw_audio_file.write(audio_data) - cmd = self.command.replace("$", raw_audio_file.name) - if self.debug: - self.debug_message("[CMD ]: Detection {id} command: {cmd}".format(id=_id, cmd=cmd)) - os.system(cmd) - os.unlink(raw_audio_file.name) - - def notify(self, message): - self.send(message) - - -class TokenSaverWorker(Worker): - - def __init__(self, name_format, filetype, timeout=0.2, debug=False, logger=None, **kwargs): - self.name_format = name_format - self.filetype = filetype - self.kwargs = kwargs - Worker.__init__(self, timeout=timeout, debug=debug, logger=logger) - - def run(self): - while True: - if self._stop_requested(): - break - - message = self._get_message() - if message is not None: - if message == TokenizerWorker.END_OF_PROCESSING: - break - - audio_data = message.pop("audio_data", None) - start_time = message.pop("start_time", None) - end_time = message.pop("end_time", None) - _id = message.pop("id", None) - if audio_data is not None and len(audio_data) > 0: - fname = self.name_format.format(N=_id, start = "{:.2f}".format(start_time), end = "{:.2f}".format(end_time)) - try: - if self.debug: - self.debug_message("[SAVE]: Detection {id} saved as {fname}".format(id=_id, fname=fname)) - save_audio_data(audio_data, fname, filetype=self.filetype, **self.kwargs) - except Exception as e: - sys.stderr.write(str(e) + "\n") - - def notify(self, message): - self.send(message) - - -class LogWorker(Worker): - - def __init__(self, print_detections=False, output_format="{start} {end}", - time_formatter=seconds_to_str_fromatter("%S"), timeout=0.2, debug=False, logger=None): - - self.print_detections = print_detections - self.output_format = output_format - self.time_formatter = time_formatter - self.detections = [] - Worker.__init__(self, timeout=timeout, debug=debug, logger=logger) - - def run(self): - while True: - if self._stop_requested(): - break - - message = self._get_message() - - if message is not None: - - if message == TokenizerWorker.END_OF_PROCESSING: - break - - audio_data = message.pop("audio_data", None) - _id = message.pop("id", None) - start = message.pop("start", None) - end = message.pop("end", None) - start_time = message.pop("start_time", None) - end_time = message.pop("end_time", None) - if audio_data is not None and len(audio_data) > 0: - - if self.debug: - self.debug_message("[DET ]: Detection {id} (start:{start}, end:{end})".format(id=_id, - start="{:5.2f}".format(start_time), - end="{:5.2f}".format(end_time))) - - if self.print_detections: - print(self.output_format.format(id = _id, - start = self.time_formatter(start_time), - end = self.time_formatter(end_time))) - - self.detections.append((_id, start, end, start_time, end_time)) - - - def notify(self, message): - self.send(message) - +__date__ = "2015-11-23" +__updated__ = "2021-03-01" def main(argv=None): - '''Command line options.''' - program_name = os.path.basename(sys.argv[0]) - program_version = version - program_build_date = "%s" % __updated__ - - program_version_string = '%%prog %s (%s)' % (program_version, program_build_date) - #program_usage = '''usage: spam two eggs''' # optional - will be autogenerated by optparse - program_longdesc = '''''' # optional - give further explanation about what the program does - program_license = "Copyright 2015 Mohamed El Amine SEHILI \ - Licensed under the General Public License (GPL) Version 3 \nhttp://www.gnu.org/licenses/" - if argv is None: argv = sys.argv[1:] try: - # setup option parser - parser = OptionParser(version=program_version_string, epilog=program_longdesc, description=program_license) - - group = OptionGroup(parser, "[Input-Output options]") - group.add_option("-i", "--input", dest="input", help="Input audio or video file. Use - for stdin [default: read from microphone using pyaudio]", metavar="FILE") - group.add_option("-t", "--input-type", dest="input_type", help="Input audio file type. Mandatory if file name has no extension [default: %default]", type=str, default=None, metavar="String") - group.add_option("-M", "--max_time", dest="max_time", help="Max data (in seconds) to read from microphone/file [default: read until the end of file/stream]", type=float, default=None, metavar="FLOAT") - group.add_option("-O", "--output-main", dest="output_main", help="Save main stream as. If omitted main stream will not be saved [default: omitted]", type=str, default=None, metavar="FILE") - group.add_option("-o", "--output-tokens", dest="output_tokens", help="Output file name format for detections. Use {N} and {start} and {end} to build file names, example: 'Det_{N}_{start}-{end}.wav'", type=str, default=None, metavar="STRING") - group.add_option("-T", "--output-type", dest="output_type", help="Audio type used to save detections and/or main stream. If not supplied will: (1). guess from extension or (2). use wav format", type=str, default=None, metavar="STRING") - group.add_option("-u", "--use-channel", dest="use_channel", help="Choose channel to use from a multi-channel audio file (requires pydub). 'left', 'right' and 'mix' are accepted values. [Default: 1 (i.e. 1st or left channel)]", type=str, default="1", metavar="STRING") - parser.add_option_group(group) - - - group = OptionGroup(parser, "[Tokenization options]", "Set tokenizer options and energy threshold.") - group.add_option("-a", "--analysis-window", dest="analysis_window", help="Size of analysis window in seconds [default: %default (10ms)]", type=float, default=0.01, metavar="FLOAT") - group.add_option("-n", "--min-duration", dest="min_duration", help="Min duration of a valid audio event in seconds [default: %default]", type=float, default=0.2, metavar="FLOAT") - group.add_option("-m", "--max-duration", dest="max_duration", help="Max duration of a valid audio event in seconds [default: %default]", type=float, default=5, metavar="FLOAT") - group.add_option("-s", "--max-silence", dest="max_silence", help="Max duration of a consecutive silence within a valid audio event in seconds [default: %default]", type=float, default=0.3, metavar="FLOAT") - group.add_option("-d", "--drop-trailing-silence", dest="drop_trailing_silence", help="Drop trailing silence from a detection [default: keep trailing silence]", action="store_true", default=False) - group.add_option("-e", "--energy-threshold", dest="energy_threshold", help="Log energy threshold for detection [default: %default]", type=float, default=50, metavar="FLOAT") - parser.add_option_group(group) - - - group = OptionGroup(parser, "[Audio parameters]", "Define audio parameters if data is read from a headerless file (raw or stdin) or you want to use different microphone parameters.") - group.add_option("-r", "--rate", dest="sampling_rate", help="Sampling rate of audio data [default: %default]", type=int, default=16000, metavar="INT") - group.add_option("-c", "--channels", dest="channels", help="Number of channels of audio data [default: %default]", type=int, default=1, metavar="INT") - group.add_option("-w", "--width", dest="sample_width", help="Number of bytes per audio sample [default: %default]", type=int, default=2, metavar="INT") - parser.add_option_group(group) - - group = OptionGroup(parser, "[Do something with detections]", "Use these options to print, play or plot detections.") - group.add_option("-C", "--command", dest="command", help="Command to call when an audio detection occurs. Use $ to represent the file name to use with the command (e.g. -C 'du -h $')", default=None, type=str, metavar="STRING") - group.add_option("-E", "--echo", dest="echo", help="Play back each detection immediately using pyaudio [default: do not play]", action="store_true", default=False) - group.add_option("-p", "--plot", dest="plot", help="Plot and show audio signal and detections (requires matplotlib)", action="store_true", default=False) - group.add_option("", "--save-image", dest="save_image", help="Save plotted audio signal and detections as a picture or a PDF file (requires matplotlib)", type=str, default=None, metavar="FILE") - group.add_option("", "--printf", dest="printf", help="print detections one per line using a user supplied format (e.g. '[{id}]: {start} -- {end}'). Available keywords {id}, {start} and {end}", type=str, default="{id} {start} {end}", metavar="STRING") - group.add_option("", "--time-format", dest="time_format", help="format used to print {start} and {end}. [Default= %default]. %S: absolute time in sec. %I: absolute time in ms. If at least one of (%h, %m, %s, %i) is used, convert time into hours, minutes, seconds and millis (e.g. %h:%m:%s.%i). Only required fields are printed", type=str, default="%S", metavar="STRING") - parser.add_option_group(group) - - parser.add_option("-q", "--quiet", dest="quiet", help="Do not print any information about detections [default: print 'id', 'start' and 'end' of each detection]", action="store_true", default=False) - parser.add_option("-D", "--debug", dest="debug", help="Print processing operations to STDOUT", action="store_true", default=False) - parser.add_option("", "--debug-file", dest="debug_file", help="Print processing operations to FILE", type=str, default=None, metavar="FILE") - - + parser = ArgumentParser( + prog=program_name, description="An Audio Tokenization tool" + ) + parser.add_argument( + "--version", "-v", action="version", version=__version__ + ) + group = parser.add_argument_group("Input-Output options") + group.add_argument( + dest="input", + help="Input audio or video file. Use '-' for stdin " + "[default: read from microphone using pyaudio]", + metavar="input", + nargs="?", + default=None, + ) + group.add_argument( + "-I", + "--input-device-index", + dest="input_device_index", + help="Audio device index [default: %(default)s]. " + "Optional and only effective when using PyAudio", + type=int, + default=None, + metavar="INT", + ) + group.add_argument( + "-F", + "--audio-frame-per-buffer", + dest="frame_per_buffer", + help="Audio frame per buffer [default: %(default)s]. " + "Optional and only effective when using PyAudio", + type=int, + default=1024, + metavar="INT", + ) + group.add_argument( + "-f", + "--input-format", + dest="input_format", + type=str, + default=None, + help="Input audio file format. If not given, guess format from " + "extension. If output file name has no extension, guess format " + "from file header (requires pydub). If none of the previous is " + "true, raise an error", + metavar="STRING", + ) + group.add_argument( + "-M", + "--max-read", + dest="max_read", + type=float, + default=None, + help="Maximum data (in seconds) to read from microphone or file " + "[default: read until the end of file/stream]", + metavar="FLOAT", + ) + group.add_argument( + "-L", + "--large-file", + dest="large_file", + action="store_true", + default=False, + help="Whether input file should be treated as a large file. " + "If True, data will be read from file on demand, otherwise all " + "audio data is loaded to memory before tokenization.", + ) + group.add_argument( + "-O", + "--save-stream", + dest="save_stream", + type=str, + default=None, + help="Save acquired audio data (from file or microphone) to disk." + " If omitted no data will be saved. [default: omitted]", + metavar="FILE", + ) + group.add_argument( + "-o", + "--save-detections-as", + dest="save_detections_as", + type=str, + default=None, + help="File name format for detections." + "The following placeholders can be used to build output file name " + "for each detection: {id} (sequential, starts from 1), {start}, " + "{end} and {duration}. Time placeholders are in seconds. " + "Example: 'Event_{id}_{start}-{end}_{duration:.3f}.wav'", + metavar="STRING", + ) + group.add_argument( + "-T", + "--output-format", + dest="output_format", + type=str, + default=None, + help="Audio format used to save detections and/or main stream. " + "If not supplied, then it will: (1. be guessed from extension or " + "(2. use raw format", + metavar="STRING", + ) + group.add_argument( + "-u", + "--use-channel", + dest="use_channel", + type=str, + default=None, + help="Which channel to use for tokenization when input stream is " + "multi-channel (0 is the first channel). Default is None, meaning " + "that all channels will be considered for tokenization (i.e., get " + "any valid audio event regardless of the channel it occurs in). " + "This value can also be 'mix' (alias 'avg' or 'average') and " + "means mix down all audio channels into one channel (i.e. compute " + "average channel) and use the resulting channel for tokenization. " + "Whatever option is used, saved audio events will contain the same" + " number of channels as input stream. " + "[Default: None, use all channels]", + metavar="INT/STRING", + ) + + group = parser.add_argument_group( + "Tokenization options", "Set tokenizer options." + ) + group.add_argument( + "-a", + "--analysis-window", + dest="analysis_window", + default=0.01, + type=float, + help="Size of analysis window in seconds [default: %(default)s " + "(10ms)]", + metavar="FLOAT", + ) + group.add_argument( + "-n", + "--min-duration", + dest="min_duration", + type=float, + default=0.2, + help="Min duration of a valid audio event in seconds " + "[default: %(default)s]", + metavar="FLOAT", + ) + group.add_argument( + "-m", + "--max-duration", + dest="max_duration", + type=float, + default=5, + help="Max duration of a valid audio event in seconds " + "[default: %(default)s]", + metavar="FLOAT", + ) + group.add_argument( + "-s", + "--max-silence", + dest="max_silence", + type=float, + default=0.3, + help="Max duration of a consecutive silence within a valid audio " + "event in seconds [default: %(default)s]", + metavar="FLOAT", + ) + group.add_argument( + "-d", + "--drop-trailing-silence", + dest="drop_trailing_silence", + action="store_true", + default=False, + help="Drop trailing silence from a detection [default: keep " + "trailing silence]", + ) + group.add_argument( + "-R", + "--strict-min-duration", + dest="strict_min_duration", + action="store_true", + default=False, + help="Reject an event shorter than --min-duration even if it's " + "adjacent to the latest valid event that reached max-duration " + "[default: keep such events]", + ) + group.add_argument( + "-e", + "--energy-threshold", + dest="energy_threshold", + type=float, + default=50, + help="Log energy threshold for detection [default: %(default)s]", + metavar="FLOAT", + ) + + group = parser.add_argument_group( + "Audio parameters", + "Define audio parameters if data is read from a " + "headerless file (raw or stdin) or you want to use " + "different microphone parameters.", + ) + group.add_argument( + "-r", + "--rate", + dest="sampling_rate", + type=int, + default=16000, + help="Sampling rate of audio data [default: %(default)s]", + metavar="INT", + ) + group.add_argument( + "-c", + "--channels", + dest="channels", + type=int, + default=1, + help="Number of channels of audio data [default: %(default)s]", + metavar="INT", + ) + group.add_argument( + "-w", + "--width", + dest="sample_width", + type=int, + default=2, + help="Number of bytes per audio sample [default: %(default)s]", + metavar="INT", + ) + + group = parser.add_argument_group( + "Do something with audio events", + "Use these options to print, play back or plot detections.", + ) + group.add_argument( + "-C", + "--command", + dest="command", + type=str, + help="Command to call when an audio detection occurs. Use '{file}' " + "as a placeholder for the temporary wav file that will contain " + "event's data (e.g., \"-C 'du -h {file}'\" to print out file size " + " or \"-C 'play -q {file}'\" to play audio with sox)", + metavar="STRING", + ) + group.add_argument( + "-E", + "--echo", + dest="echo", + action="store_true", + default=False, + help="Play back each detection immediately using pyaudio", + ) + group.add_argument( + "-B", + "--progress-bar", + dest="progress_bar", + action="store_true", + default=False, + help="Show a progress bar when playing audio", + ) + group.add_argument( + "-p", + "--plot", + dest="plot", + action="store_true", + default=False, + help="Plot and show audio signal and detections (requires " + "matplotlib)", + ) + group.add_argument( + "--save-image", + dest="save_image", + type=str, + help="Save plotted audio signal and detections as a picture or a " + "PDF file (requires matplotlib)", + metavar="FILE", + ) + group.add_argument( + "--printf", + dest="printf", + type=str, + default="{id} {start} {end}", + help="Print audio events information, one per line, using this " + "format. Format can contain text with the following placeholders: " + "{id} (sequential, starts from 1), {start}, {end}, {duration} and " + "{timestamp}. The first 3 time placeholders are in seconds and " + "their format can be set using --time-format argument. " + "{timestamp} is the system timestamp (date and time) of the event " + "and can be set using --timestamp-format argument.\n" + "Example: '[{id}]: {start} -> {end} -- {timestamp}'", + metavar="STRING", + ) + group.add_argument( + "--time-format", + dest="time_format", + type=str, + default="%S", + help="Format used to print {start}, {end} and {duration} " + "placeholders used with --printf [default= %(default)s]. The " + "following formats are accepted:\n" + "%%S: absolute time in seconds. %%I: absolute time in ms. If at " + "least one of (%%h, %%m, %%s, %%i) is used, convert time into " + "hours, minutes, seconds and millis (e.g. %%h:%%m:%%s.%%i). Only " + "supplied fields are printed. Note that %%S and %%I can only be " + "used alone", + metavar="STRING", + ) + group.add_argument( + "--timestamp-format", + dest="timestamp_format", + type=str, + default="%Y/%m/%d %H:%M:%S", + help="Format used to print {timestamp}. Should be a format " + "accepted by 'datetime' standard module. Default: " + "'%%Y/%%m/%%d %%H:%%M:%%S'", + ) + parser.add_argument( + "-q", + "--quiet", + dest="quiet", + action="store_true", + default=False, + help="Do not print any information about detections [default: " + "print 'id', 'start' and 'end' of each detection]", + ) + parser.add_argument( + "-D", + "--debug", + dest="debug", + action="store_true", + default=False, + help="Print processing operations to STDOUT", + ) + parser.add_argument( + "--debug-file", + dest="debug_file", + type=str, + default=None, + help="Print processing operations to FILE", + metavar="FILE", + ) + + args = parser.parse_args(argv) + logger = make_logger(args.debug, args.debug_file) + kwargs = make_kwargs(args) + reader, observers = initialize_workers( + logger=logger, **kwargs.io, **kwargs.miscellaneous + ) + tokenizer_worker = workers.TokenizerWorker( + reader, observers, logger=logger, **kwargs.split + ) + tokenizer_worker.start_all() - # process options - (opts, args) = parser.parse_args(argv) - - if opts.input == "-": - asource = StdinAudioSource(sampling_rate = opts.sampling_rate, - sample_width = opts.sample_width, - channels = opts.channels) - #read data from a file - elif opts.input is not None: - asource = file_to_audio_source(filename=opts.input, filetype=opts.input_type, uc=opts.use_channel) - - # read data from microphone via pyaudio - else: - try: - asource = PyAudioSource(sampling_rate = opts.sampling_rate, - sample_width = opts.sample_width, - channels = opts.channels) - except Exception: - sys.stderr.write("Cannot read data from audio device!\n") - sys.stderr.write("You should either install pyaudio or read data from STDIN\n") - sys.exit(2) - - logger = logging.getLogger(LOGGER_NAME) - logger.setLevel(logging.DEBUG) - - handler = logging.StreamHandler(sys.stdout) - if opts.quiet or not opts.debug: - # only critical messages will be printed - handler.setLevel(logging.CRITICAL) - else: - handler.setLevel(logging.DEBUG) - - logger.addHandler(handler) - - if opts.debug_file is not None: - logger.setLevel(logging.DEBUG) - opts.debug = True - handler = logging.FileHandler(opts.debug_file, "w") - fmt = logging.Formatter('[%(asctime)s] | %(message)s') - handler.setFormatter(fmt) - handler.setLevel(logging.DEBUG) - logger.addHandler(handler) - - record = opts.output_main is not None or opts.plot or opts.save_image is not None - - ads = ADSFactory.ads(audio_source = asource, block_dur = opts.analysis_window, max_time = opts.max_time, record = record) - validator = AudioEnergyValidator(sample_width=asource.get_sample_width(), energy_threshold=opts.energy_threshold) - - - if opts.drop_trailing_silence: - mode = StreamTokenizer.DROP_TRAILING_SILENCE - else: - mode = 0 - - analysis_window_per_second = 1. / opts.analysis_window - tokenizer = StreamTokenizer(validator=validator, min_length=opts.min_duration * analysis_window_per_second, - max_length=int(opts.max_duration * analysis_window_per_second), - max_continuous_silence=opts.max_silence * analysis_window_per_second, - mode = mode) - - - observers = [] - tokenizer_worker = None - - if opts.output_tokens is not None: - - try: - # check user format is correct - fname = opts.output_tokens.format(N=0, start=0, end=0) - - # find file type for detections - tok_type = opts.output_type - if tok_type is None: - tok_type = os.path.splitext(opts.output_tokens)[1][1:] - if tok_type == "": - tok_type = "wav" - - token_saver = TokenSaverWorker(name_format=opts.output_tokens, filetype=tok_type, - debug=opts.debug, logger=logger, sr=asource.get_sampling_rate(), - sw=asource.get_sample_width(), - ch=asource.get_channels()) - observers.append(token_saver) - - except Exception: - sys.stderr.write("Wrong format for detections file name: '{0}'\n".format(opts.output_tokens)) - sys.exit(2) - - if opts.echo: - try: - player = player_for(asource) - player_worker = PlayerWorker(player=player, debug=opts.debug, logger=logger) - observers.append(player_worker) - except Exception: - sys.stderr.write("Cannot get an audio player!\n") - sys.stderr.write("You should either install pyaudio or supply a command (-C option) to play audio\n") - sys.exit(2) - - if opts.command is not None and len(opts.command) > 0: - cmd_worker = CommandLineWorker(command=opts.command, debug=opts.debug, logger=logger) - observers.append(cmd_worker) - - if not opts.quiet or opts.plot is not None or opts.save_image is not None: - oformat = opts.printf.replace("\\n", "\n").replace("\\t", "\t").replace("\\r", "\r") - converter = seconds_to_str_fromatter(opts.time_format) - log_worker = LogWorker(print_detections = not opts.quiet, output_format=oformat, - time_formatter=converter, logger=logger, debug=opts.debug) - observers.append(log_worker) - - tokenizer_worker = TokenizerWorker(ads, tokenizer, opts.analysis_window, observers) - - def _save_main_stream(): - # find file type - main_type = opts.output_type - if main_type is None: - main_type = os.path.splitext(opts.output_main)[1][1:] - if main_type == "": - main_type = "wav" - ads.close() - ads.rewind() - data = ads.get_audio_source().get_data_buffer() - if len(data) > 0: - save_audio_data(data=data, filename=opts.output_main, filetype=main_type, sr=asource.get_sampling_rate(), - sw = asource.get_sample_width(), - ch = asource.get_channels()) - - def _plot(): - import numpy as np - ads.close() - ads.rewind() - data = ads.get_audio_source().get_data_buffer() - signal = AudioEnergyValidator._convert(data, asource.get_sample_width()) - detections = [(det[3] , det[4]) for det in log_worker.detections] - max_amplitude = 2**(asource.get_sample_width() * 8 - 1) - 1 - energy_as_amp = np.sqrt(np.exp(opts.energy_threshold * np.log(10) / 10)) / max_amplitude - plot_all(signal / max_amplitude, asource.get_sampling_rate(), energy_as_amp, detections, show = opts.plot, save_as = opts.save_image) - - - # start observer threads - for obs in observers: - obs.start() - # start tokenization thread - tokenizer_worker.start() - while True: time.sleep(1) if len(threading.enumerate()) == 1: - break - - tokenizer_worker = None - - if opts.output_main is not None: - _save_main_stream() - if opts.plot or opts.save_image is not None: - _plot() - - return 0 - - except KeyboardInterrupt: - + raise EndOfProcessing + + except (KeyboardInterrupt, EndOfProcessing): if tokenizer_worker is not None: - tokenizer_worker.stop() - for obs in observers: - obs.stop() - - if opts.output_main is not None: - _save_main_stream() - if opts.plot or opts.save_image is not None: - _plot() - + tokenizer_worker.stop_all() + + if isinstance(reader, workers.StreamSaverWorker): + reader.join() + try: + reader.save_stream() + except AudioEncodingWarning as ae_warn: + print(str(ae_warn), file=sys.stderr) + + if args.plot or args.save_image is not None: + from .plotting import plot + + reader.rewind() + record = AudioRegion( + reader.data, reader.sr, reader.sw, reader.ch + ) + detections = ( + (det.start, det.end) for det in tokenizer_worker.detections + ) + plot( + record, + detections=detections, + energy_threshold=args.energy_threshold, + show=True, + save_as=args.save_image, + ) return 0 - except Exception as e: - sys.stderr.write(program_name + ": " + str(e) + "\n") - sys.stderr.write("for help use -h\n") - - return 2 if __name__ == "__main__": - if DEBUG: - sys.argv.append("-h") - if TESTRUN: - import doctest - doctest.testmod() - if PROFILE: - import cProfile - import pstats - profile_filename = 'auditok.auditok_profile.txt' - cProfile.run('main()', profile_filename) - statsfile = open("profile_stats.txt", "wb") - p = pstats.Stats(profile_filename, stream=statsfile) - stats = p.strip_dirs().sort_stats('cumulative') - stats.print_stats() - statsfile.close() - sys.exit(0) - sys.exit(main()) + sys.exit(main(None)) diff --git a/libs/auditok/cmdline_util.py b/libs/auditok/cmdline_util.py new file mode 100755 index 000000000..bde72aa36 --- /dev/null +++ b/libs/auditok/cmdline_util.py @@ -0,0 +1,126 @@ +import sys +import logging +from collections import namedtuple +from . import workers +from .util import AudioDataSource +from .io import player_for + +_AUDITOK_LOGGER = "AUDITOK_LOGGER" +KeywordArguments = namedtuple( + "KeywordArguments", ["io", "split", "miscellaneous"] +) + + +def make_kwargs(args_ns): + if args_ns.save_stream is None: + record = args_ns.plot or (args_ns.save_image is not None) + else: + record = False + try: + use_channel = int(args_ns.use_channel) + except (ValueError, TypeError): + use_channel = args_ns.use_channel + + io_kwargs = { + "input": args_ns.input, + "audio_format": args_ns.input_format, + "max_read": args_ns.max_read, + "block_dur": args_ns.analysis_window, + "sampling_rate": args_ns.sampling_rate, + "sample_width": args_ns.sample_width, + "channels": args_ns.channels, + "use_channel": use_channel, + "save_stream": args_ns.save_stream, + "save_detections_as": args_ns.save_detections_as, + "export_format": args_ns.output_format, + "large_file": args_ns.large_file, + "frames_per_buffer": args_ns.frame_per_buffer, + "input_device_index": args_ns.input_device_index, + "record": record, + } + + split_kwargs = { + "min_dur": args_ns.min_duration, + "max_dur": args_ns.max_duration, + "max_silence": args_ns.max_silence, + "drop_trailing_silence": args_ns.drop_trailing_silence, + "strict_min_dur": args_ns.strict_min_duration, + "energy_threshold": args_ns.energy_threshold, + } + + miscellaneous = { + "echo": args_ns.echo, + "progress_bar": args_ns.progress_bar, + "command": args_ns.command, + "quiet": args_ns.quiet, + "printf": args_ns.printf, + "time_format": args_ns.time_format, + "timestamp_format": args_ns.timestamp_format, + } + return KeywordArguments(io_kwargs, split_kwargs, miscellaneous) + + +def make_logger(stderr=False, file=None, name=_AUDITOK_LOGGER): + if not stderr and file is None: + return None + logger = logging.getLogger(name) + logger.setLevel(logging.INFO) + if stderr: + handler = logging.StreamHandler(sys.stderr) + handler.setLevel(logging.INFO) + logger.addHandler(handler) + + if file is not None: + handler = logging.FileHandler(file, "w") + fmt = logging.Formatter("[%(asctime)s] | %(message)s") + handler.setFormatter(fmt) + handler.setLevel(logging.INFO) + logger.addHandler(handler) + return logger + + +def initialize_workers(logger=None, **kwargs): + observers = [] + reader = AudioDataSource(source=kwargs["input"], **kwargs) + if kwargs["save_stream"] is not None: + reader = workers.StreamSaverWorker( + reader, + filename=kwargs["save_stream"], + export_format=kwargs["export_format"], + ) + reader.start() + + if kwargs["save_detections_as"] is not None: + worker = workers.RegionSaverWorker( + kwargs["save_detections_as"], + kwargs["export_format"], + logger=logger, + ) + observers.append(worker) + + if kwargs["echo"]: + player = player_for(reader) + worker = workers.PlayerWorker( + player, progress_bar=kwargs["progress_bar"], logger=logger + ) + observers.append(worker) + + if kwargs["command"] is not None: + worker = workers.CommandLineWorker( + command=kwargs["command"], logger=logger + ) + observers.append(worker) + + if not kwargs["quiet"]: + print_format = ( + kwargs["printf"] + .replace("\\n", "\n") + .replace("\\t", "\t") + .replace("\\r", "\r") + ) + worker = workers.PrintWorker( + print_format, kwargs["time_format"], kwargs["timestamp_format"] + ) + observers.append(worker) + + return reader, observers diff --git a/libs/auditok/core.py b/libs/auditok/core.py index 47441d2b7..af00dc7af 100644 --- a/libs/auditok/core.py +++ b/libs/auditok/core.py @@ -1,264 +1,1267 @@ """ -This module gathers processing (i.e. tokenization) classes. - -Class summary -============= - .. autosummary:: + :toctree: generated/ - StreamTokenizer + load + split + AudioRegion + StreamTokenizer """ +import os +import math +from .util import AudioReader, DataValidator, AudioEnergyValidator +from .io import check_audio_data, to_file, player_for, get_audio_source +from .exceptions import TooSamllBlockDuration -from auditok.util import DataValidator +try: + from . import signal_numpy as signal +except ImportError: + from . import signal -__all__ = ["StreamTokenizer"] +__all__ = ["load", "split", "AudioRegion", "StreamTokenizer"] -class StreamTokenizer(): +DEFAULT_ANALYSIS_WINDOW = 0.05 +DEFAULT_ENERGY_THRESHOLD = 50 +_EPSILON = 1e-10 + + +def load(input, skip=0, max_read=None, **kwargs): + """Load audio data from a source and return it as an :class:`AudioRegion`. + + Parameters + ---------- + input : None, str, bytes, AudioSource + source to read audio data from. If `str`, it should be a path to a + valid audio file. If `bytes`, it is used as raw audio data. If it is + "-", raw data will be read from stdin. If None, read audio data from + the microphone using PyAudio. If of type `bytes` or is a path to a + raw audio file then `sampling_rate`, `sample_width` and `channels` + parameters (or their alias) are required. If it's an + :class:`AudioSource` object it's used directly to read data. + skip : float, default: 0 + amount, in seconds, of audio data to skip from source. If read from + a microphone, `skip` must be 0, otherwise a `ValueError` is raised. + max_read : float, default: None + amount, in seconds, of audio data to read from source. If read from + microphone, `max_read` should not be None, otherwise a `ValueError` is + raised. + audio_format, fmt : str + type of audio data (e.g., wav, ogg, flac, raw, etc.). This will only + be used if `input` is a string path to an audio file. If not given, + audio type will be guessed from file name extension or from file + header. + sampling_rate, sr : int + sampling rate of audio data. Required if `input` is a raw audio file, + a `bytes` object or None (i.e., read from microphone). + sample_width, sw : int + number of bytes used to encode one audio sample, typically 1, 2 or 4. + Required for raw data, see `sampling_rate`. + channels, ch : int + number of channels of audio data. Required for raw data, see + `sampling_rate`. + large_file : bool, default: False + If True, AND if `input` is a path to a *wav* of a *raw* audio file + (and **only** these two formats) then audio file is not fully loaded to + memory in order to create the region (but the portion of data needed to + create the region is of course loaded to memory). Set to True if + `max_read` is significantly smaller then the size of a large audio file + that shouldn't be entirely loaded to memory. + + Returns + ------- + region: AudioRegion + + Raises + ------ + ValueError + raised if `input` is None (i.e., read data from microphone) and `skip` + != 0 or `input` is None `max_read` is None (meaning that when reading + from the microphone, no data should be skipped, and maximum amount of + data to read should be explicitly provided). + """ + return AudioRegion.load(input, skip, max_read, **kwargs) + + +def split( + input, + min_dur=0.2, + max_dur=5, + max_silence=0.3, + drop_trailing_silence=False, + strict_min_dur=False, + **kwargs +): + """ + Split audio data and return a generator of AudioRegions + + Parameters + ---------- + input : str, bytes, AudioSource, AudioReader, AudioRegion or None + input audio data. If str, it should be a path to an existing audio file. + "-" is interpreted as standard input. If bytes, input is considered as + raw audio data. If None, read audio from microphone. + Every object that is not an `AudioReader` will be transformed into an + `AudioReader` before processing. If it is an `str` that refers to a raw + audio file, `bytes` or None, audio parameters should be provided using + kwargs (i.e., `samplig_rate`, `sample_width` and `channels` or their + alias). + If `input` is str then audio format will be guessed from file extension. + `audio_format` (alias `fmt`) kwarg can also be given to specify audio + format explicitly. If none of these options is available, rely on + backend (currently only pydub is supported) to load data. + min_dur : float, default: 0.2 + minimun duration in seconds of a detected audio event. By using large + values for `min_dur`, very short audio events (e.g., very short 1-word + utterances like 'yes' or 'no') can be mis detected. Using very short + values might result in a high number of short, unuseful audio events. + max_dur : float, default: 5 + maximum duration in seconds of a detected audio event. If an audio event + lasts more than `max_dur` it will be truncated. If the continuation of a + truncated audio event is shorter than `min_dur` then this continuation + is accepted as a valid audio event if `strict_min_dur` is False. + Otherwise it is rejected. + max_silence : float, default: 0.3 + maximum duration of continuous silence within an audio event. There + might be many silent gaps of this duration within one audio event. If + the continuous silence happens at the end of the event than it's kept as + part of the event if `drop_trailing_silence` is False (default). + drop_trailing_silence : bool, default: False + Whether to remove trailing silence from detected events. To avoid abrupt + cuts in speech, trailing silence should be kept, therefore this + parameter should be False. + strict_min_dur : bool, default: False + strict minimum duration. Do not accept an audio event if it is shorter + than `min_dur` even if it is contiguous to the latest valid event. This + happens if the the latest detected event had reached `max_dur`. + + Other Parameters + ---------------- + analysis_window, aw : float, default: 0.05 (50 ms) + duration of analysis window in seconds. A value between 0.01 (10 ms) and + 0.1 (100 ms) should be good for most use-cases. + audio_format, fmt : str + type of audio data (e.g., wav, ogg, flac, raw, etc.). This will only be + used if `input` is a string path to an audio file. If not given, audio + type will be guessed from file name extension or from file header. + sampling_rate, sr : int + sampling rate of audio data. Required if `input` is a raw audio file, is + a bytes object or None (i.e., read from microphone). + sample_width, sw : int + number of bytes used to encode one audio sample, typically 1, 2 or 4. + Required for raw data, see `sampling_rate`. + channels, ch : int + number of channels of audio data. Required for raw data, see + `sampling_rate`. + use_channel, uc : {None, "mix"} or int + which channel to use for split if `input` has multiple audio channels. + Regardless of which channel is used for splitting, returned audio events + contain data from *all* channels, just as `input`. + The following values are accepted: + + - None (alias "any"): accept audio activity from any channel, even if + other channels are silent. This is the default behavior. + + - "mix" ("avg" or "average"): mix down all channels (i.e. compute + average channel) and split the resulting channel. + + - int (0 <=, > `channels`): use one channel, specified by integer id, + for split. + + large_file : bool, default: False + If True, AND if `input` is a path to a *wav* of a *raw* audio file + (and only these two formats) then audio data is lazily loaded to memory + (i.e., one analysis window a time). Otherwise the whole file is loaded + to memory before split. Set to True if the size of the file is larger + than available memory. + max_read, mr : float, default: None, read until end of stream + maximum data to read from source in seconds. + validator, val : callable, DataValidator + custom data validator. If `None` (default), an `AudioEnergyValidor` is + used with the given energy threshold. Can be a callable or an instance + of `DataValidator` that implements `is_valid`. In either case, it'll be + called with with a window of audio data as the first parameter. + energy_threshold, eth : float, default: 50 + energy threshold for audio activity detection. Audio regions that have + enough windows of with a signal energy equal to or above this threshold + are considered valid audio events. Here we are referring to this amount + as the energy of the signal but to be more accurate, it is the log + energy of computed as: `20 * log10(sqrt(dot(x, x) / len(x)))` (see + :class:`AudioEnergyValidator` and + :func:`calculate_energy_single_channel`). If `validator` is given, this + argument is ignored. + + Yields + ------ + AudioRegion + a generator of detected :class:`AudioRegion` s. + """ + if min_dur <= 0: + raise ValueError("'min_dur' ({}) must be > 0".format(min_dur)) + if max_dur <= 0: + raise ValueError("'max_dur' ({}) must be > 0".format(max_dur)) + if max_silence < 0: + raise ValueError("'max_silence' ({}) must be >= 0".format(max_silence)) + + if isinstance(input, AudioReader): + source = input + analysis_window = source.block_dur + else: + analysis_window = kwargs.get( + "analysis_window", kwargs.get("aw", DEFAULT_ANALYSIS_WINDOW) + ) + if analysis_window <= 0: + raise ValueError( + "'analysis_window' ({}) must be > 0".format(analysis_window) + ) + + params = kwargs.copy() + params["max_read"] = params.get("max_read", params.get("mr")) + params["audio_format"] = params.get("audio_format", params.get("fmt")) + if isinstance(input, AudioRegion): + params["sampling_rate"] = input.sr + params["sample_width"] = input.sw + params["channels"] = input.ch + input = bytes(input) + try: + source = AudioReader(input, block_dur=analysis_window, **params) + except TooSamllBlockDuration as exc: + err_msg = "Too small 'analysis_windows' ({0}) for sampling rate " + err_msg += "({1}). Analysis windows should at least be 1/{1} to " + err_msg += "cover one single data sample" + raise ValueError(err_msg.format(exc.block_dur, exc.sampling_rate)) + + validator = kwargs.get("validator", kwargs.get("val")) + if validator is None: + energy_threshold = kwargs.get( + "energy_threshold", kwargs.get("eth", DEFAULT_ENERGY_THRESHOLD) + ) + use_channel = kwargs.get("use_channel", kwargs.get("uc")) + validator = AudioEnergyValidator( + energy_threshold, source.sw, source.ch, use_channel=use_channel + ) + mode = StreamTokenizer.DROP_TRAILING_SILENCE if drop_trailing_silence else 0 + if strict_min_dur: + mode |= StreamTokenizer.STRICT_MIN_LENGTH + min_length = _duration_to_nb_windows(min_dur, analysis_window, math.ceil) + max_length = _duration_to_nb_windows( + max_dur, analysis_window, math.floor, _EPSILON + ) + max_continuous_silence = _duration_to_nb_windows( + max_silence, analysis_window, math.floor, _EPSILON + ) + + err_msg = "({0} sec.) results in {1} analysis window(s) " + err_msg += "({1} == {6}({0} / {2})) which is {5} the number " + err_msg += "of analysis window(s) for 'max_dur' ({3} == floor({4} / {2}))" + if min_length > max_length: + err_msg = "'min_dur' " + err_msg + raise ValueError( + err_msg.format( + min_dur, + min_length, + analysis_window, + max_length, + max_dur, + "higher than", + "ceil", + ) + ) + + if max_continuous_silence >= max_length: + err_msg = "'max_silence' " + err_msg + raise ValueError( + err_msg.format( + max_silence, + max_continuous_silence, + analysis_window, + max_length, + max_dur, + "higher or equal to", + "floor", + ) + ) + + tokenizer = StreamTokenizer( + validator, min_length, max_length, max_continuous_silence, mode=mode + ) + source.open() + token_gen = tokenizer.tokenize(source, generator=True) + region_gen = ( + _make_audio_region( + token[0], + token[1], + source.block_dur, + source.sr, + source.sw, + source.ch, + ) + for token in token_gen + ) + return region_gen + + +def _duration_to_nb_windows( + duration, analysis_window, round_fn=round, epsilon=0 +): + """ + Converts a given duration into a positive integer of analysis windows. + if `duration / analysis_window` is not an integer, the result will be + rounded to the closest bigger integer. If `duration == 0`, returns `0`. + If `duration < analysis_window`, returns 1. + `duration` and `analysis_window` can be in seconds or milliseconds but + must be in the same unit. + + Parameters + ---------- + duration : float + a given duration in seconds or ms. + analysis_window: float + size of analysis window, in the same unit as `duration`. + round_fn : callable + function called to round the result. Default: `round`. + epsilon : float + small value to add to the division result before rounding. + E.g., `0.3 / 0.1 = 2.9999999999999996`, when called with + `round_fn=math.floor` returns `2` instead of `3`. Adding a small value + to `0.3 / 0.1` avoids this error. + + Returns + ------- + nb_windows : int + minimum number of `analysis_window`'s to cover `durartion`. That means + that `analysis_window * nb_windows >= duration`. + """ + if duration < 0 or analysis_window <= 0: + err_msg = "'duration' ({}) must be >= 0 and 'analysis_window' ({}) > 0" + raise ValueError(err_msg.format(duration, analysis_window)) + if duration == 0: + return 0 + return int(round_fn(duration / analysis_window + epsilon)) + + +def _make_audio_region( + data_frames, + start_frame, + frame_duration, + sampling_rate, + sample_width, + channels, +): + """ + Helper function to create an `AudioRegion` from parameters returned by + tokenization object. It takes care of setting up region `start` and `end` + in metadata. + + Parameters + ---------- + frame_duration: float + duration of analysis window in seconds + start_frame : int + index of the fisrt analysis window + samling_rate : int + sampling rate of audio data + sample_width : int + number of bytes of one audio sample + channels : int + number of channels of audio data + + Returns + ------- + audio_region : AudioRegion + AudioRegion whose start time is calculeted as: + `1000 * start_frame * frame_duration` + """ + start = start_frame * frame_duration + data = b"".join(data_frames) + duration = len(data) / (sampling_rate * sample_width * channels) + meta = {"start": start, "end": start + duration} + return AudioRegion(data, sampling_rate, sample_width, channels, meta) + + +def _read_chunks_online(max_read, **kwargs): + """ + Helper function to read audio data from an online blocking source + (i.e., microphone). Used to build an `AudioRegion` and can intercept + KeyboardInterrupt so that reading stops as soon as this exception is + raised. Makes building `AudioRegion`s on [i]python sessions and jupyter + notebooks more user friendly. + + Parameters + ---------- + max_read : float + maximum amount of data to read in seconds. + kwargs : + audio parameters (sampling_rate, sample_width and channels). + + See also + -------- + `AudioRegion.build` + """ + reader = AudioReader(None, block_dur=0.5, max_read=max_read, **kwargs) + reader.open() + data = [] + try: + while True: + frame = reader.read() + if frame is None: + break + data.append(frame) + except KeyboardInterrupt: + # Stop data acquisition from microphone when pressing + # Ctrl+C on a [i]python session or a notebook + pass + reader.close() + return ( + b"".join(data), + reader.sampling_rate, + reader.sample_width, + reader.channels, + ) + + +def _read_offline(input, skip=0, max_read=None, **kwargs): + """ + Helper function to read audio data from an offline (i.e., file). Used to + build `AudioRegion`s. + + Parameters + ---------- + input : str, bytes + path to audio file (if str), or a bytes object representing raw audio + data. + skip : float, default 0 + amount of data to skip from the begining of audio source. + max_read : float, default: None + maximum amount of audio data to read. Default: None, means read until + end of stream. + kwargs : + audio parameters (sampling_rate, sample_width and channels). + + See also + -------- + `AudioRegion.build` + + """ + audio_source = get_audio_source(input, **kwargs) + audio_source.open() + if skip is not None and skip > 0: + skip_samples = round(skip * audio_source.sampling_rate) + audio_source.read(skip_samples) + if max_read is not None: + if max_read < 0: + max_read = None + else: + max_read = round(max_read * audio_source.sampling_rate) + data = audio_source.read(max_read) + audio_source.close() + return ( + data, + audio_source.sampling_rate, + audio_source.sample_width, + audio_source.channels, + ) + + +def _check_convert_index(index, types, err_msg): + if not isinstance(index, slice) or index.step is not None: + raise TypeError(err_msg) + start = index.start if index.start is not None else 0 + stop = index.stop + for index in (start, stop): + if index is not None and not isinstance(index, types): + raise TypeError(err_msg) + return start, stop + + +class _SecondsView: + """A class to create a view of `AudioRegion` that can be sliced using + indices in seconds. + """ + + def __init__(self, region): + self._region = region + + def __getitem__(self, index): + err_msg = "Slicing AudioRegion by seconds requires indices of type " + err_msg += "'int' or 'float' without a step (e.g. region.sec[7.5:10])" + start_s, stop_s = _check_convert_index(index, (int, float), err_msg) + sr = self._region.sampling_rate + start_sample = int(start_s * sr) + stop_sample = None if stop_s is None else round(stop_s * sr) + return self._region[start_sample:stop_sample] + + @property + def len(self): + """ + Return region duration in seconds. + """ + return self._region.duration + + +class _MillisView(_SecondsView): + """A class to create a view of `AudioRegion` that can be sliced using + indices in milliseconds. + """ + + def __getitem__(self, index): + err_msg = ( + "Slicing AudioRegion by milliseconds requires indices of type " + ) + err_msg += "'int' without a step (e.g. region.sec[500:1500])" + start_ms, stop_ms = _check_convert_index(index, (int), err_msg) + start_sec = start_ms / 1000 + stop_sec = None if stop_ms is None else stop_ms / 1000 + index = slice(start_sec, stop_sec) + return super(_MillisView, self).__getitem__(index) + + def __len__(self): + """ + Return region duration in milliseconds. + """ + return round(self._region.duration * 1000) + + @property + def len(self): + """ + Return region duration in milliseconds. + """ + return len(self) + + +class _AudioRegionMetadata(dict): + """A class to store `AudioRegion`'s metadata.""" + + def __getattr__(self, name): + if name in self: + return self[name] + else: + err_msg = "AudioRegion metadata has no entry '{}'" + raise AttributeError(err_msg.format(name)) + + def __setattr__(self, name, value): + self[name] = value + + def __str__(self): + return "\n".join("{}: {}".format(k, v) for k, v in self.items()) + + def __repr__(self): + return str(self) + + +class AudioRegion(object): + """ + AudioRegion encapsulates raw audio data and provides an interface to + perform simple operations on it. Use `AudioRegion.load` to build an + `AudioRegion` from different types of objects. + + Parameters + ---------- + data : bytes + raw audio data as a bytes object + sampling_rate : int + sampling rate of audio data + sample_width : int + number of bytes of one audio sample + channels : int + number of channels of audio data + meta : dict, default: None + any collection of elements used to build metadata for + this `AudioRegion`. Meta data can be accessed via `region.meta.key` + if `key` is a valid python attribute name, or via `region.meta[key]` + if not. Note that the :func:`split` function (or the + :meth:`AudioRegion.split` method) returns `AudioRegions` with a ``start`` + and a ``stop`` meta values that indicate the location in seconds of the + region in original audio data. + + See also + -------- + AudioRegion.load + + """ + + def __init__(self, data, sampling_rate, sample_width, channels, meta=None): + check_audio_data(data, sample_width, channels) + self._data = data + self._sampling_rate = sampling_rate + self._sample_width = sample_width + self._channels = channels + self._samples = None + self.splitp = self.split_and_plot + + if meta is not None: + self._meta = _AudioRegionMetadata(meta) + else: + self._meta = None + + self._seconds_view = _SecondsView(self) + self.sec = self.seconds + self.s = self.seconds + + self._millis_view = _MillisView(self) + self.ms = self.millis + + @property + def meta(self): + return self._meta + + @meta.setter + def meta(self, new_meta): + """Meta data of audio region.""" + self._meta = _AudioRegionMetadata(new_meta) + + @classmethod + def load(cls, input, skip=0, max_read=None, **kwargs): + """ + Create an `AudioRegion` by loading data from `input`. See :func:`load` + for parameters descripion. + + Returns + ------- + region: AudioRegion + + Raises + ------ + ValueError + raised if `input` is None and `skip` != 0 or `max_read` is None. + """ + if input is None: + if skip > 0: + raise ValueError( + "'skip' should be 0 when reading from microphone" + ) + if max_read is None or max_read < 0: + raise ValueError( + "'max_read' should not be None when reading from " + "microphone" + ) + data, sampling_rate, sample_width, channels = _read_chunks_online( + max_read, **kwargs + ) + else: + data, sampling_rate, sample_width, channels = _read_offline( + input, skip=skip, max_read=max_read, **kwargs + ) + + return cls(data, sampling_rate, sample_width, channels) + + @property + def seconds(self): + """ + A view to slice audio region by seconds (using ``region.seconds[start:end]``). + """ + return self._seconds_view + + @property + def millis(self): + """A view to slice audio region by milliseconds (using ``region.millis[start:end]``).""" + return self._millis_view + + @property + def duration(self): + """ + Returns region duration in seconds. + """ + return len(self._data) / ( + self.sampling_rate * self.sample_width * self.channels + ) + + @property + def sampling_rate(self): + """Samling rate of audio data.""" + return self._sampling_rate + + @property + def sr(self): + """Samling rate of audio data, alias for `sampling_rate`.""" + return self._sampling_rate + + @property + def sample_width(self): + """Number of bytes per sample, one channel considered.""" + return self._sample_width + + @property + def sw(self): + """Number of bytes per sample, alias for `sampling_rate`.""" + return self._sample_width + + @property + def channels(self): + """Number of channels of audio data.""" + return self._channels + + @property + def ch(self): + """Number of channels of audio data, alias for `channels`.""" + return self._channels + + def play(self, progress_bar=False, player=None, **progress_bar_kwargs): + """ + Play audio region. + + Parameters + ---------- + progress_bar : bool, default: False + whether to use a progress bar while playing audio. Default: False. + `progress_bar` requires `tqdm`, if not installed, no progress bar + will be shown. + player : AudioPalyer, default: None + audio player to use. if None (default), use `player_for()` + to get a new audio player. + progress_bar_kwargs : kwargs + keyword arguments to pass to `tqdm` progress_bar builder (e.g., + use `leave=False` to clean up the screen when play finishes). + """ + if player is None: + player = player_for(self) + player.play( + self._data, progress_bar=progress_bar, **progress_bar_kwargs + ) + + def save(self, file, audio_format=None, exists_ok=True, **audio_parameters): + """ + Save audio region to file. + + Parameters + ---------- + file : str + path to output audio file. May contain `{duration}` placeholder + as well as any place holder that this region's metadata might + contain (e.g., regions returned by `split` contain metadata with + `start` and `end` attributes that can be used to build output file + name as `{meta.start}` and `{meta.end}`. See examples using + placeholders with formatting. + + audio_format : str, default: None + format used to save audio data. If None (default), format is guessed + from file name's extension. If file name has no extension, audio + data is saved as a raw (headerless) audio file. + exists_ok : bool, default: True + If True, overwrite `file` if a file with the same name exists. + If False, raise an `IOError` if `file` exists. + audio_parameters: dict + any keyword arguments to be passed to audio saving backend. + + Returns + ------- + file: str + name of output file with replaced placehoders. + Raises + IOError if `file` exists and `exists_ok` is False. + + + Examples + -------- + >>> region = AudioRegion(b'\\0' * 2 * 24000, + >>> sampling_rate=16000, + >>> sample_width=2, + >>> channels=1) + >>> region.meta.start = 2.25 + >>> region.meta.end = 2.25 + region.duration + >>> region.save('audio_{meta.start}-{meta.end}.wav') + >>> audio_2.25-3.75.wav + >>> region.save('region_{meta.start:.3f}_{duration:.3f}.wav') + audio_2.250_1.500.wav + """ + if isinstance(file, str): + file = file.format(duration=self.duration, meta=self.meta) + if not exists_ok and os.path.exists(file): + raise FileExistsError("file '{file}' exists".format(file=file)) + to_file( + self._data, + file, + audio_format, + sr=self.sr, + sw=self.sw, + ch=self.ch, + audio_parameters=audio_parameters, + ) + return file + + def split( + self, + min_dur=0.2, + max_dur=5, + max_silence=0.3, + drop_trailing_silence=False, + strict_min_dur=False, + **kwargs + ): + """Split audio region. See :func:`auditok.split()` for a comprehensive + description of split parameters. + See Also :meth:`AudioRegio.split_and_plot`. + """ + if kwargs.get("max_read", kwargs.get("mr")) is not None: + warn_msg = "'max_read' (or 'mr') should not be used with " + warn_msg += "AudioRegion.split_and_plot(). You should rather " + warn_msg += "slice audio region before calling this method" + raise RuntimeWarning(warn_msg) + return split( + self, + min_dur=min_dur, + max_dur=max_dur, + max_silence=max_silence, + drop_trailing_silence=drop_trailing_silence, + strict_min_dur=strict_min_dur, + **kwargs + ) + + def plot( + self, + scale_signal=True, + show=True, + figsize=None, + save_as=None, + dpi=120, + theme="auditok", + ): + """Plot audio region, one sub-plot for each channel. + + Parameters + ---------- + scale_signal : bool, default: True + if true, scale signal by subtracting its mean and dividing by its + standard deviation before plotting. + show : bool + whether to show plotted signal right after the call. + figsize : tuple, default: None + width and height of the figure to pass to `matplotlib`. + save_as : str, default None. + if provided, also save plot to file. + dpi : int, default: 120 + plot dpi to pass to `matplotlib`. + theme : str or dict, default: "auditok" + plot theme to use. Currently only "auditok" theme is implemented. To + provide you own them see :attr:`auditok.plotting.AUDITOK_PLOT_THEME`. + """ + try: + from auditok.plotting import plot + + plot( + self, + scale_signal=scale_signal, + show=show, + figsize=figsize, + save_as=save_as, + dpi=dpi, + theme=theme, + ) + except ImportError: + raise RuntimeWarning("Plotting requires matplotlib") + + def split_and_plot( + self, + min_dur=0.2, + max_dur=5, + max_silence=0.3, + drop_trailing_silence=False, + strict_min_dur=False, + scale_signal=True, + show=True, + figsize=None, + save_as=None, + dpi=120, + theme="auditok", + **kwargs + ): + """Split region and plot signal and detections. Alias: :meth:`splitp`. + See :func:`auditok.split()` for a comprehensive description of split + parameters. Also see :meth:`plot` for plot parameters. + """ + try: + from auditok.plotting import plot + + regions = self.split( + min_dur=min_dur, + max_dur=max_dur, + max_silence=max_silence, + drop_trailing_silence=drop_trailing_silence, + strict_min_dur=strict_min_dur, + **kwargs + ) + regions = list(regions) + detections = ((reg.meta.start, reg.meta.end) for reg in regions) + eth = kwargs.get( + "energy_threshold", kwargs.get("eth", DEFAULT_ENERGY_THRESHOLD) + ) + plot( + self, + scale_signal=scale_signal, + detections=detections, + energy_threshold=eth, + show=show, + figsize=figsize, + save_as=save_as, + dpi=dpi, + theme=theme, + ) + return regions + except ImportError: + raise RuntimeWarning("Plotting requires matplotlib") + + def __array__(self): + return self.samples + + @property + def samples(self): + """Audio region as arrays of samples, one array per channel.""" + if self._samples is None: + self._samples = signal.to_array( + self._data, self.sample_width, self.channels + ) + return self._samples + + def __len__(self): + """ + Return region length in number of samples. + """ + return len(self._data) // (self.sample_width * self.channels) + + @property + def len(self): + """ + Return region length in number of samples. + """ + return len(self) + + def __bytes__(self): + return self._data + + def __str__(self): + return ( + "AudioRegion(duration={:.3f}, " + "sampling_rate={}, sample_width={}, channels={})".format( + self.duration, self.sr, self.sw, self.ch + ) + ) + + def __repr__(self): + return str(self) + + def __add__(self, other): + """ + Concatenates this region and `other` and return a new region. + Both regions must have the same sampling rate, sample width + and number of channels. If not, raises a `ValueError`. + """ + if not isinstance(other, AudioRegion): + raise TypeError( + "Can only concatenate AudioRegion, " + 'not "{}"'.format(type(other)) + ) + if other.sr != self.sr: + raise ValueError( + "Can only concatenate AudioRegions of the same " + "sampling rate ({} != {})".format(self.sr, other.sr) + ) + if other.sw != self.sw: + raise ValueError( + "Can only concatenate AudioRegions of the same " + "sample width ({} != {})".format(self.sw, other.sw) + ) + if other.ch != self.ch: + raise ValueError( + "Can only concatenate AudioRegions of the same " + "number of channels ({} != {})".format(self.ch, other.ch) + ) + data = self._data + other._data + return AudioRegion(data, self.sr, self.sw, self.ch) + + def __radd__(self, other): + """ + Concatenates `other` and this region. `other` should be an + `AudioRegion` with the same audio parameters as this region + but can exceptionally be `0` to make it possible to concatenate + many regions with `sum`. + """ + if other == 0: + return self + return other.add(self) + + def __mul__(self, n): + if not isinstance(n, int): + err_msg = "Can't multiply AudioRegion by a non-int of type '{}'" + raise TypeError(err_msg.format(type(n))) + data = self._data * n + return AudioRegion(data, self.sr, self.sw, self.ch) + + def __rmul__(self, n): + return self * n + + def __truediv__(self, n): + if not isinstance(n, int) or n <= 0: + raise TypeError("AudioRegion can only be divided by a positive int") + samples_per_sub_region, rest = divmod(len(self), n) + onset = 0 + sub_regions = [] + while onset < len(self): + offset = 0 + if rest > 0: + offset = 1 + rest -= 1 + offset += onset + samples_per_sub_region + sub_regions.append(self[onset:offset]) + onset = offset + return sub_regions + + def __eq__(self, other): + if other is self: + return True + if not isinstance(other, AudioRegion): + return False + return ( + (self._data == other._data) + and (self.sr == other.sr) + and (self.sw == other.sw) + and (self.ch == other.ch) + ) + + def __getitem__(self, index): + err_msg = "Slicing AudioRegion by samples requires indices of type " + err_msg += "'int' without a step (e.g. region.sec[1600:3200])" + start_sample, stop_sample = _check_convert_index(index, (int), err_msg) + + bytes_per_sample = self.sample_width * self.channels + len_samples = len(self._data) // bytes_per_sample + + if start_sample < 0: + start_sample = max(start_sample + len_samples, 0) + onset = start_sample * bytes_per_sample + + if stop_sample is not None: + if stop_sample < 0: + stop_sample = max(stop_sample + len_samples, 0) + offset = index.stop * bytes_per_sample + else: + offset = None + + data = self._data[onset:offset] + return AudioRegion(data, self.sr, self.sw, self.ch) + + +class StreamTokenizer: """ Class for stream tokenizers. It implements a 4-state automaton scheme to extract sub-sequences of interest on the fly. - - :Parameters: - - `validator` : - instance of `DataValidator` that implements `is_valid` method. - - `min_length` : *(int)* - Minimum number of frames of a valid token. This includes all \ - tolerated non valid frames within the token. - - `max_length` : *(int)* - Maximum number of frames of a valid token. This includes all \ - tolerated non valid frames within the token. - - `max_continuous_silence` : *(int)* - Maximum number of consecutive non-valid frames within a token. - Note that, within a valid token, there may be many tolerated \ - *silent* regions that contain each a number of non valid frames up to \ - `max_continuous_silence` - - `init_min` : *(int, default=0)* - Minimum number of consecutive valid frames that must be **initially** \ - gathered before any sequence of non valid frames can be tolerated. This - option is not always needed, it can be used to drop non-valid tokens as - early as possible. **Default = 0** means that the option is by default - ineffective. - - `init_max_silence` : *(int, default=0)* - Maximum number of tolerated consecutive non-valid frames if the \ - number already gathered valid frames has not yet reached 'init_min'. - This argument is normally used if `init_min` is used. **Default = 0**, - by default this argument is not taken into consideration. - - `mode` : *(int, default=0)* - `mode` can be: - - 1. `StreamTokenizer.STRICT_MIN_LENGTH`: - if token *i* is delivered because `max_length` - is reached, and token *i+1* is immediately adjacent to - token *i* (i.e. token *i* ends at frame *k* and token *i+1* starts - at frame *k+1*) then accept token *i+1* only of it has a size of at - least `min_length`. The default behavior is to accept token *i+1* - event if it is shorter than `min_length` (given that the above conditions - are fulfilled of course). - - :Examples: - - In the following code, without `STRICT_MIN_LENGTH`, the 'BB' token is - accepted although it is shorter than `min_length` (3), because it immediately - follows the latest delivered token: - - .. code:: python - - from auditok import StreamTokenizer, StringDataSource, DataValidator - - class UpperCaseChecker(DataValidator): - def is_valid(self, frame): - return frame.isupper() - - - dsource = StringDataSource("aaaAAAABBbbb") - tokenizer = StreamTokenizer(validator=UpperCaseChecker(), - min_length=3, - max_length=4, - max_continuous_silence=0) - - tokenizer.tokenize(dsource) - - - :output: - - .. code:: python - - [(['A', 'A', 'A', 'A'], 3, 6), (['B', 'B'], 7, 8)] + + Parameters + ---------- + validator : callable, DataValidator (must implement `is_valid`) + called with each data frame read from source. Should take one positional + argument and return True or False for valid and invalid frames + respectively. + + min_length : int + Minimum number of frames of a valid token. This includes all + tolerated non valid frames within the token. + + max_length : int + Maximum number of frames of a valid token. This includes all + tolerated non valid frames within the token. + + max_continuous_silence : int + Maximum number of consecutive non-valid frames within a token. + Note that, within a valid token, there may be many tolerated + *silent* regions that contain each a number of non valid frames up + to `max_continuous_silence` + + init_min : int + Minimum number of consecutive valid frames that must be + **initially** gathered before any sequence of non valid frames can + be tolerated. This option is not always needed, it can be used to + drop non-valid tokens as early as possible. **Default = 0** means + that the option is by default ineffective. + + init_max_silence : int + Maximum number of tolerated consecutive non-valid frames if the + number already gathered valid frames has not yet reached + 'init_min'.This argument is normally used if `init_min` is used. + **Default = 0**, by default this argument is not taken into + consideration. + + mode : int + mode can be one of the following: + + -1 `StreamTokenizer.NORMAL` : do not drop trailing silence, and + accept a token shorter than `min_length` if it is the continuation + of the latest delivered token. + + -2 `StreamTokenizer.STRICT_MIN_LENGTH`: if token `i` is delivered + because `max_length` is reached, and token `i+1` is immediately + adjacent to token `i` (i.e. token `i` ends at frame `k` and token + `i+1` starts at frame `k+1`) then accept token `i+1` only of it has + a size of at least `min_length`. The default behavior is to accept + token `i+1` event if it is shorter than `min_length` (provided that + the above conditions are fulfilled of course). + + -3 `StreamTokenizer.DROP_TRAILING_SILENCE`: drop all tailing + non-valid frames from a token to be delivered if and only if it + is not **truncated**. This can be a bit tricky. A token is actually + delivered if: + + - `max_continuous_silence` is reached. + + - Its length reaches `max_length`. This is referred to as a + **truncated** token. + + In the current implementation, a `StreamTokenizer`'s decision is only + based on already seen data and on incoming data. Thus, if a token is + truncated at a non-valid but tolerated frame (`max_length` is reached + but `max_continuous_silence` not yet) any tailing silence will be kept + because it can potentially be part of valid token (if `max_length` was + bigger). But if `max_continuous_silence` is reached before + `max_length`, the delivered token will not be considered as truncated + but a result of *normal* end of detection (i.e. no more valid data). + In that case the trailing silence can be removed if you use the + `StreamTokenizer.DROP_TRAILING_SILENCE` mode. + + -4 `(StreamTokenizer.STRICT_MIN_LENGTH | StreamTokenizer.DROP_TRAILING_SILENCE)`: + use both options. That means: first remove tailing silence, then + check if the token still has a length of at least `min_length`. - The following tokenizer will however reject the 'BB' token: - - .. code:: python - - dsource = StringDataSource("aaaAAAABBbbb") - tokenizer = StreamTokenizer(validator=UpperCaseChecker(), - min_length=3, max_length=4, - max_continuous_silence=0, - mode=StreamTokenizer.STRICT_MIN_LENGTH) - tokenizer.tokenize(dsource) - - :output: - - .. code:: python - - [(['A', 'A', 'A', 'A'], 3, 6)] - - - 2. `StreamTokenizer.DROP_TRAILING_SILENCE`: drop all tailing non-valid frames - from a token to be delivered if and only if it is not **truncated**. - This can be a bit tricky. A token is actually delivered if: - - - a. `max_continuous_silence` is reached - - :or: - - - b. Its length reaches `max_length`. This is called a **truncated** token - - In the current implementation, a `StreamTokenizer`'s decision is only based on already seen - data and on incoming data. Thus, if a token is truncated at a non-valid but tolerated - frame (`max_length` is reached but `max_continuous_silence` not yet) any tailing - silence will be kept because it can potentially be part of valid token (if `max_length` - was bigger). But if `max_continuous_silence` is reached before `max_length`, the delivered - token will not be considered as truncated but a result of *normal* end of detection - (i.e. no more valid data). In that case the tailing silence can be removed if you use - the `StreamTokenizer.DROP_TRAILING_SILENCE` mode. - - :Example: - - .. code:: python - - tokenizer = StreamTokenizer(validator=UpperCaseChecker(), min_length=3, - max_length=6, max_continuous_silence=3, - mode=StreamTokenizer.DROP_TRAILING_SILENCE) - - dsource = StringDataSource("aaaAAAaaaBBbbbb") - tokenizer.tokenize(dsource) - - :output: - - .. code:: python - - [(['A', 'A', 'A', 'a', 'a', 'a'], 3, 8), (['B', 'B'], 9, 10)] - - The first token is delivered with its tailing silence because it is truncated - while the second one has its tailing frames removed. - - Without `StreamTokenizer.DROP_TRAILING_SILENCE` the output would be: - - .. code:: python - - [(['A', 'A', 'A', 'a', 'a', 'a'], 3, 8), (['B', 'B', 'b', 'b', 'b'], 9, 13)] - - - - 3. `StreamTokenizer.STRICT_MIN_LENGTH | StreamTokenizer.DROP_TRAILING_SILENCE`: - use both options. That means: first remove tailing silence, then ckeck if the - token still has at least a length of `min_length`. + + + Examples + -------- + + In the following code, without `STRICT_MIN_LENGTH`, the 'BB' token is + accepted although it is shorter than `min_length` (3), because it + immediately follows the latest delivered token: + + >>> from auditok.core import StreamTokenizer + >>> from StringDataSource, DataValidator + + >>> class UpperCaseChecker(DataValidator): + >>> def is_valid(self, frame): + return frame.isupper() + >>> dsource = StringDataSource("aaaAAAABBbbb") + >>> tokenizer = StreamTokenizer(validator=UpperCaseChecker(), + min_length=3, + max_length=4, + max_continuous_silence=0) + >>> tokenizer.tokenize(dsource) + [(['A', 'A', 'A', 'A'], 3, 6), (['B', 'B'], 7, 8)] + + + The following tokenizer will however reject the 'BB' token: + + >>> dsource = StringDataSource("aaaAAAABBbbb") + >>> tokenizer = StreamTokenizer(validator=UpperCaseChecker(), + min_length=3, max_length=4, + max_continuous_silence=0, + mode=StreamTokenizer.STRICT_MIN_LENGTH) + >>> tokenizer.tokenize(dsource) + [(['A', 'A', 'A', 'A'], 3, 6)] + + + + >>> tokenizer = StreamTokenizer( + >>> validator=UpperCaseChecker(), + >>> min_length=3, + >>> max_length=6, + >>> max_continuous_silence=3, + >>> mode=StreamTokenizer.DROP_TRAILING_SILENCE + >>> ) + >>> dsource = StringDataSource("aaaAAAaaaBBbbbb") + >>> tokenizer.tokenize(dsource) + [(['A', 'A', 'A', 'a', 'a', 'a'], 3, 8), (['B', 'B'], 9, 10)] + + The first token is delivered with its tailing silence because it is + truncated while the second one has its tailing frames removed. + + Without `StreamTokenizer.DROP_TRAILING_SILENCE` the output would be: + + .. code:: python + + [ + (['A', 'A', 'A', 'a', 'a', 'a'], 3, 8), + (['B', 'B', 'b', 'b', 'b'], 9, 13) + ] + """ - - + SILENCE = 0 POSSIBLE_SILENCE = 1 - POSSIBLE_NOISE = 2 + POSSIBLE_NOISE = 2 NOISE = 3 - + NORMAL = 0 STRICT_MIN_LENGTH = 2 DROP_TRAILING_SILENCE = 4 - # alias - DROP_TAILING_SILENCE = 4 - - def __init__(self, validator, - min_length, max_length, max_continuous_silence, - init_min=0, init_max_silence=0, - mode=0): - - if not isinstance(validator, DataValidator): - raise TypeError("'validator' must be an instance of 'DataValidator'") - + + def __init__( + self, + validator, + min_length, + max_length, + max_continuous_silence, + init_min=0, + init_max_silence=0, + mode=0, + ): + if callable(validator): + self._is_valid = validator + elif isinstance(validator, DataValidator): + self._is_valid = validator.is_valid + else: + raise TypeError( + "'validator' must be a callable or an instance of " + "DataValidator" + ) + if max_length <= 0: - raise ValueError("'max_length' must be > 0 (value={0})".format(max_length)) - + raise ValueError( + "'max_length' must be > 0 (value={0})".format(max_length) + ) + if min_length <= 0 or min_length > max_length: - raise ValueError("'min_length' must be > 0 and <= 'max_length' (value={0})".format(min_length)) - + err_msg = "'min_length' must be > 0 and <= 'max_length' (value={0})" + raise ValueError(err_msg.format(min_length)) + if max_continuous_silence >= max_length: - raise ValueError("'max_continuous_silence' must be < 'max_length' (value={0})".format(max_continuous_silence)) - + err_msg = "'max_continuous_silence' must be < 'max_length' " + err_msg += "(value={0})" + raise ValueError(err_msg.format(max_continuous_silence)) + if init_min >= max_length: - raise ValueError("'init_min' must be < 'max_length' (value={0})".format(max_continuous_silence)) - + raise ValueError( + "'init_min' must be < 'max_length' (value={0})".format( + max_continuous_silence + ) + ) + self.validator = validator self.min_length = min_length self.max_length = max_length self.max_continuous_silence = max_continuous_silence self.init_min = init_min self.init_max_silent = init_max_silence - - self._mode = None - self.set_mode(mode) - self._strict_min_length = (mode & self.STRICT_MIN_LENGTH) != 0 - self._drop_tailing_silence = (mode & self.DROP_TRAILING_SILENCE) != 0 - + self._set_mode(mode) self._deliver = None self._tokens = None self._state = None self._data = None self._contiguous_token = False - self._init_count = 0 self._silence_length = 0 self._start_frame = 0 self._current_frame = 0 - - def set_mode(self, mode): - """ - :Parameters: - - `mode` : *(int)* - New mode, must be one of: - - - - `StreamTokenizer.STRICT_MIN_LENGTH` - - - `StreamTokenizer.DROP_TRAILING_SILENCE` - - - `StreamTokenizer.STRICT_MIN_LENGTH | StreamTokenizer.DROP_TRAILING_SILENCE` - - - `0` - - See `StreamTokenizer.__init__` for more information about the mode. - """ - - if not mode in [self.STRICT_MIN_LENGTH, self.DROP_TRAILING_SILENCE, - self.STRICT_MIN_LENGTH | self.DROP_TRAILING_SILENCE, 0]: - + + def _set_mode(self, mode): + strict_min_and_drop_trailing = StreamTokenizer.STRICT_MIN_LENGTH + strict_min_and_drop_trailing |= StreamTokenizer.DROP_TRAILING_SILENCE + if mode not in [ + StreamTokenizer.NORMAL, + StreamTokenizer.STRICT_MIN_LENGTH, + StreamTokenizer.DROP_TRAILING_SILENCE, + strict_min_and_drop_trailing, + ]: raise ValueError("Wrong value for mode") - self._mode = mode self._strict_min_length = (mode & self.STRICT_MIN_LENGTH) != 0 - self._drop_tailing_silence = (mode & self.DROP_TRAILING_SILENCE) != 0 - - - def get_mode(self): - """ - Return the current mode. To check whether a specific mode is activated use - the bitwise 'and' operator `&`. Example: - - .. code:: python - - if mode & self.STRICT_MIN_LENGTH != 0: - do_something() - """ - return self._mode - + self._drop_trailing_silence = (mode & self.DROP_TRAILING_SILENCE) != 0 + def _reinitialize(self): self._contiguous_token = False self._data = [] @@ -266,112 +1269,114 @@ class StreamTokenizer(): self._state = self.SILENCE self._current_frame = -1 self._deliver = self._append_token - - - def tokenize(self, data_source, callback=None): + + def tokenize(self, data_source, callback=None, generator=False): """ - Read data from `data_source`, one frame a time, and process the read frames in - order to detect sequences of frames that make up valid tokens. - + Read data from `data_source`, one frame a time, and process the read + frames in order to detect sequences of frames that make up valid + tokens. + :Parameters: - `data_source` : instance of the :class:`DataSource` class that implements a `read` method. - 'read' should return a slice of signal, i.e. frame (of whatever \ - type as long as it can be processed by validator) and None if \ - there is no more signal. - + `data_source` : instance of the :class:`DataSource` class that + implements a `read` method. 'read' should return a slice of + signal, i.e. frame (of whatever type as long as it can be + processed by validator) and None if there is no more signal. + `callback` : an optional 3-argument function. - If a `callback` function is given, it will be called each time a valid token - is found. - - + If a `callback` function is given, it will be called each time + a valid token is found. + + :Returns: - A list of tokens if `callback` is None. Each token is tuple with the following elements: - + A list of tokens if `callback` is None. Each token is tuple with the + following elements: + .. code python - + (data, start, end) - - where `data` is a list of read frames, `start`: index of the first frame in the - original data and `end` : index of the last frame. - + + where `data` is a list of read frames, `start`: index of the first + frame in the original data and `end` : index of the last frame. """ - + token_gen = self._iter_tokens(data_source) + if callback: + for token in token_gen: + callback(*token) + return + if generator: + return token_gen + return list(token_gen) + + def _iter_tokens(self, data_source): self._reinitialize() - - if callback is not None: - self._deliver = callback - while True: - frame = data_source.read() - if frame is None: - break + frame = data_source.read() self._current_frame += 1 - self._process(frame) - - self._post_process() - - if callback is None: - _ret = self._tokens - self._tokens = None - return _ret - - - def _process(self, frame): - - frame_is_valid = self.validator.is_valid(frame) - + if frame is None: + token = self._post_process() + if token is not None: + yield token + break + token = self._process(frame) + if token is not None: + yield token + + def _process(self, frame): # noqa: C901 + + frame_is_valid = self._is_valid(frame) + if self._state == self.SILENCE: - + if frame_is_valid: # seems we got a valid frame after a silence self._init_count = 1 self._silence_length = 0 self._start_frame = self._current_frame self._data.append(frame) - - if self._init_count >= self.init_min: + + if self._init_count >= self.init_min: self._state = self.NOISE if len(self._data) >= self.max_length: - self._process_end_of_detection(True) + return self._process_end_of_detection(True) else: self._state = self.POSSIBLE_NOISE - + elif self._state == self.POSSIBLE_NOISE: - + if frame_is_valid: self._silence_length = 0 self._init_count += 1 self._data.append(frame) - if self._init_count >= self.init_min: + if self._init_count >= self.init_min: self._state = self.NOISE if len(self._data) >= self.max_length: - self._process_end_of_detection(True) - - else: + return self._process_end_of_detection(True) + + else: self._silence_length += 1 - if self._silence_length > self.init_max_silent or \ - len(self._data) + 1 >= self.max_length: + if ( + self._silence_length > self.init_max_silent + or len(self._data) + 1 >= self.max_length + ): # either init_max_silent or max_length is reached # before _init_count, back to silence self._data = [] self._state = self.SILENCE else: self._data.append(frame) - - + elif self._state == self.NOISE: - + if frame_is_valid: self._data.append(frame) if len(self._data) >= self.max_length: - self._process_end_of_detection(True) - - elif self.max_continuous_silence <= 0 : - # max token reached at this frame will _deliver if _contiguous_token - # and not _strict_min_length - self._process_end_of_detection() + return self._process_end_of_detection(True) + + elif self.max_continuous_silence <= 0: + # max token reached at this frame will _deliver if + # _contiguous_token and not _strict_min_length self._state = self.SILENCE - + return self._process_end_of_detection() else: # this is the first silent frame following a valid one # and it is tolerated @@ -379,61 +1384,63 @@ class StreamTokenizer(): self._data.append(frame) self._state = self.POSSIBLE_SILENCE if len(self._data) == self.max_length: - self._process_end_of_detection(True) - # don't reset _silence_length because we still + return self._process_end_of_detection(True) + # don't reset _silence_length because we still # need to know the total number of silent frames - - - + elif self._state == self.POSSIBLE_SILENCE: - + if frame_is_valid: self._data.append(frame) self._silence_length = 0 self._state = self.NOISE if len(self._data) >= self.max_length: - self._process_end_of_detection(True) - + return self._process_end_of_detection(True) + else: if self._silence_length >= self.max_continuous_silence: - if self._silence_length < len(self._data): - # _deliver only gathered frames aren't all silent - self._process_end_of_detection() - else: - self._data = [] self._state = self.SILENCE + if self._silence_length < len(self._data): + # _deliver only gathered frames aren't all silent + return self._process_end_of_detection() + self._data = [] self._silence_length = 0 else: self._data.append(frame) self._silence_length += 1 if len(self._data) >= self.max_length: - self._process_end_of_detection(True) - # don't reset _silence_length because we still + return self._process_end_of_detection(True) + # don't reset _silence_length because we still # need to know the total number of silent frames - - + def _post_process(self): if self._state == self.NOISE or self._state == self.POSSIBLE_SILENCE: if len(self._data) > 0 and len(self._data) > self._silence_length: - self._process_end_of_detection() - - + return self._process_end_of_detection() + def _process_end_of_detection(self, truncated=False): - - if not truncated and self._drop_tailing_silence and self._silence_length > 0: + + if ( + not truncated + and self._drop_trailing_silence + and self._silence_length > 0 + ): # happens if max_continuous_silence is reached # or max_length is reached at a silent frame - self._data = self._data[0: - self._silence_length] - - if (len(self._data) >= self.min_length) or \ - (len(self._data) > 0 and \ - not self._strict_min_length and self._contiguous_token): - - - - _end_frame = self._start_frame + len(self._data) - 1 - self._deliver(self._data, self._start_frame, _end_frame) - + self._data = self._data[0 : -self._silence_length] + + if (len(self._data) >= self.min_length) or ( + len(self._data) > 0 + and not self._strict_min_length + and self._contiguous_token + ): + + start_frame = self._start_frame + end_frame = self._start_frame + len(self._data) - 1 + data = self._data + self._data = [] + token = (data, start_frame, end_frame) + if truncated: # next token (if any) will start at _current_frame + 1 self._start_frame = self._current_frame + 1 @@ -441,12 +1448,11 @@ class StreamTokenizer(): self._contiguous_token = True else: self._contiguous_token = False + return token else: - self._contiguous_token = False - + self._contiguous_token = False + self._data = [] - - - + def _append_token(self, data, start, end): self._tokens.append((data, start, end)) diff --git a/libs/auditok/dataset.py b/libs/auditok/dataset.py index 1a3a7af5c..98dc5d1d4 100644 --- a/libs/auditok/dataset.py +++ b/libs/auditok/dataset.py @@ -1,19 +1,31 @@ """ -This module contains links to audio files you can use for test purposes. +This module contains links to audio files that can be used for test purposes. + +.. autosummary:: + :toctree: generated/ + + one_to_six_arabic_16000_mono_bc_noise + was_der_mensch_saet_mono_44100_lead_trail_silence """ import os -__all__ = ["one_to_six_arabic_16000_mono_bc_noise", "was_der_mensch_saet_mono_44100_lead_trail_silence"] +__all__ = [ + "one_to_six_arabic_16000_mono_bc_noise", + "was_der_mensch_saet_mono_44100_lead_trail_silence", +] _current_dir = os.path.dirname(os.path.realpath(__file__)) one_to_six_arabic_16000_mono_bc_noise = "{cd}{sep}data{sep}1to6arabic_\ -16000_mono_bc_noise.wav".format(cd=_current_dir, sep=os.path.sep) +16000_mono_bc_noise.wav".format( + cd=_current_dir, sep=os.path.sep +) """A wave file that contains a pronunciation of Arabic numbers from 1 to 6""" - was_der_mensch_saet_mono_44100_lead_trail_silence = "{cd}{sep}data{sep}was_\ der_mensch_saet_das_wird_er_vielfach_ernten_44100Hz_mono_lead_trail_\ -silence.wav".format(cd=_current_dir, sep=os.path.sep) -""" A wave file that contains a sentence between long leading and trailing periods of silence""" \ No newline at end of file +silence.wav".format( + cd=_current_dir, sep=os.path.sep +) +"""A wave file that contains a sentence with a long leading and trailing silence""" diff --git a/libs/auditok/exceptions.py b/libs/auditok/exceptions.py index 0026a9d89..7bc5054ee 100644 --- a/libs/auditok/exceptions.py +++ b/libs/auditok/exceptions.py @@ -1,9 +1,41 @@ -""" -November 2015 -@author: Amine SEHILI -""" - class DuplicateArgument(Exception): pass +class TooSamllBlockDuration(ValueError): + """Raised when block_dur results in a block_size smaller than one sample.""" + + def __init__(self, message, block_dur, sampling_rate): + self.block_dur = block_dur + self.sampling_rate = sampling_rate + super(TooSamllBlockDuration, self).__init__(message) + + +class TimeFormatError(Exception): + """Raised when a duration formatting directive is unknown.""" + + +class EndOfProcessing(Exception): + """Raised within command line script's main function to jump to + postprocessing code.""" + + +class AudioIOError(Exception): + """Raised when a compressed audio file cannot be loaded or when trying + to read from a not yet open AudioSource""" + + +class AudioParameterError(AudioIOError): + """Raised when one audio parameter is missing when loading raw data or + saving data to a format other than raw. Also raised when an audio + parameter has a wrong value.""" + + +class AudioEncodingError(Exception): + """Raised if audio data can not be encoded in the provided format""" + + +class AudioEncodingWarning(RuntimeWarning): + """Raised if audio data can not be encoded in the provided format + but saved as wav. + """ diff --git a/libs/auditok/io.py b/libs/auditok/io.py index 665ab274d..b5fb61a76 100644 --- a/libs/auditok/io.py +++ b/libs/auditok/io.py @@ -1,499 +1,1021 @@ """ Module for low-level audio input-output operations. -Class summary -============= - .. autosummary:: + :toctree: generated/ - AudioSource - Rewindable - BufferAudioSource - WaveAudioSource - PyAudioSource - StdinAudioSource - PyAudioPlayer - - -Function summary -================ - -.. autosummary:: - - from_file - player_for + AudioSource + Rewindable + BufferAudioSource + WaveAudioSource + PyAudioSource + StdinAudioSource + PyAudioPlayer + from_file + to_file + player_for """ - -from abc import ABCMeta, abstractmethod -import wave +import os import sys +import wave +import warnings +from abc import ABC, abstractmethod +from functools import partial +from .exceptions import AudioIOError, AudioParameterError -__all__ = ["AudioSource", "Rewindable", "BufferAudioSource", "WaveAudioSource", - "PyAudioSource", "StdinAudioSource", "PyAudioPlayer", "from_file", "player_for"] +try: + from pydub import AudioSegment -DEFAULT_SAMPLE_RATE = 16000 + _WITH_PYDUB = True +except ImportError: + _WITH_PYDUB = False + +try: + from tqdm import tqdm as _tqdm + + DEFAULT_BAR_FORMAT_TQDM = "|" + "{bar}" + "|" + "[{elapsed}/{duration}]" + DEFAULT_NCOLS_TQDM = 30 + DEFAULT_NCOLS_TQDM = 30 + DEFAULT_MIN_INTERVAL_TQDM = 0.05 + _WITH_TQDM = True +except ImportError: + _WITH_TQDM = False + + +__all__ = [ + "AudioSource", + "Rewindable", + "BufferAudioSource", + "RawAudioSource", + "WaveAudioSource", + "PyAudioSource", + "StdinAudioSource", + "PyAudioPlayer", + "from_file", + "to_file", + "player_for", +] + +DEFAULT_SAMPLING_RATE = 16000 DEFAULT_SAMPLE_WIDTH = 2 DEFAULT_NB_CHANNELS = 1 -class AudioSource(): - """ - Base class for audio source objects. - - Subclasses should implement methods to open/close and audio stream - and read the desired amount of audio samples. - - :Parameters: - - `sampling_rate` : int - Number of samples per second of audio stream. Default = 16000. - - `sample_width` : int - Size in bytes of one audio sample. Possible values : 1, 2, 4. - Default = 2. - - `channels` : int - Number of channels of audio stream. The current version supports - only mono audio streams (i.e. one channel). - """ - - __metaclass__ = ABCMeta +def check_audio_data(data, sample_width, channels): + sample_size_bytes = int(sample_width * channels) + nb_samples = len(data) // sample_size_bytes + if nb_samples * sample_size_bytes != len(data): + raise AudioParameterError( + "The length of audio data must be an integer " + "multiple of `sample_width * channels`" + ) + + +def _guess_audio_format(fmt, filename): + if fmt is None: + extension = os.path.splitext(filename.lower())[1][1:] + if extension: + fmt = extension + else: + return None + fmt = fmt.lower() + if fmt == "wave": + fmt = "wav" + return fmt + + +def _get_audio_parameters(param_dict): + """ + Get audio parameters from a dictionary of parameters. An audio parameter can + have a long name or a short name. If the long name is present, the short + name will be ignored. If neither is present then `AudioParameterError` is + raised. + + Expected parameters are: + + - `sampling_rate`, `sr` : int, sampling rate. + + - `sample_width`, `sw` : int, sample size in bytes. + + - `channels`, `ch` : int, number of channels. + + Returns + ------- + audio_parameters : tuple + a tuple for audio parameters as (sampling_rate, sample_width, channels). + """ + err_message = ( + "'{ln}' (or '{sn}') must be a positive integer, found: '{val}'" + ) + parameters = [] + for (long_name, short_name) in ( + ("sampling_rate", "sr"), + ("sample_width", "sw"), + ("channels", "ch"), + ): + param = param_dict.get(long_name, param_dict.get(short_name)) + if param is None or not isinstance(param, int) or param <= 0: + raise AudioParameterError( + err_message.format(ln=long_name, sn=short_name, val=param) + ) + parameters.append(param) + sampling_rate, sample_width, channels = parameters + return sampling_rate, sample_width, channels + + +class AudioSource(ABC): + """ + Base class for audio source objects. + + Subclasses should implement methods to open/close and audio stream + and read the desired amount of audio samples. + + Parameters + ---------- + sampling_rate : int + number of samples per second of audio data. + sample_width : int + size in bytes of one audio sample. Possible values: 1, 2 or 4. + channels : int + number of channels of audio data. + """ + + def __init__( + self, sampling_rate, sample_width, channels, + ): + + if sample_width not in (1, 2, 4): + raise AudioParameterError( + "Sample width must be one of: 1, 2 or 4 (bytes)" + ) + + self._sampling_rate = sampling_rate + self._sample_width = sample_width + self._channels = channels - def __init__(self, sampling_rate = DEFAULT_SAMPLE_RATE, - sample_width = DEFAULT_SAMPLE_WIDTH, - channels = DEFAULT_NB_CHANNELS): - - if not sample_width in (1, 2, 4): - raise ValueError("Sample width must be one of: 1, 2 or 4 (bytes)") - - if channels != 1: - raise ValueError("Only mono audio is currently handled") - - self.sampling_rate = sampling_rate - self.sample_width = sample_width - self.channels = channels - @abstractmethod def is_open(self): - """ Return True if audio source is open, False otherwise """ - + """Return True if audio source is open, False otherwise.""" + @abstractmethod def open(self): - """ Open audio source """ - + """Open audio source.""" + @abstractmethod def close(self): - """ Close audio source """ - + """Close audio source.""" + @abstractmethod def read(self, size): """ Read and return `size` audio samples at most. - - :Parameters: - - `size` : int - the number of samples to read. - - :Returns: - - Audio data as a string of length 'N' * 'smaple_width' * 'channels', where 'N' is: - - - `size` if `size` < 'left_samples' - - - 'left_samples' if `size` > 'left_samples' - - """ - - def get_sampling_rate(self): - """ Return the number of samples per second of audio stream """ - return self.sampling_rate - - def get_sample_width(self): - """ Return the number of bytes used to represent one audio sample """ - return self.sample_width - - def get_channels(self): - """ Return the number of channels of this audio source """ + + Parameters + ----------- + size : int + Number of samples to read. + + Returns + ------- + data : bytes + Audio data as a bytes object of length `N * sample_width * channels` + where `N` equals: + + - `size` if `size` <= remaining samples + + - remaining samples if `size` > remaining samples + """ + + @property + def sampling_rate(self): + """Number of samples per second of audio stream.""" + return self._sampling_rate + + @property + def sr(self): + """Number of samples per second of audio stream (alias for + `sampling_rate)`.""" + return self._sampling_rate + + @property + def sample_width(self): + """Number of bytes used to represent one audio sample.""" + return self._sample_width + + @property + def sw(self): + """Number of bytes used to represent one audio sample (alias for + `sample_width`).""" + return self._sample_width + + @property + def channels(self): + """Number of channels in audio stream.""" + return self._channels + + @property + def ch(self): + """Number of channels in audio stream (alias for `channels`).""" return self.channels - -class Rewindable(): +class Rewindable(AudioSource): """ Base class for rewindable audio streams. - Subclasses should implement methods to return to the beginning of an - audio stream as well as method to move to an absolute audio position - expressed in time or in number of samples. + + Subclasses should implement a method to return back to the start of an the + stream (`rewind`), as well as a property getter/setter named `position` that + reads/sets stream position expressed in number of samples. """ - - __metaclass__ = ABCMeta - + @abstractmethod def rewind(self): - """ Go back to the beginning of audio stream """ - pass - - @abstractmethod - def get_position(self): - """ Return the total number of already read samples """ - - @abstractmethod - def get_time_position(self): - """ Return the total duration in seconds of already read data """ - - @abstractmethod - def set_position(self, position): - """ Move to an absolute position - - :Parameters: - - `position` : int - number of samples to skip from the start of the stream - """ - - @abstractmethod - def set_time_position(self, time_position): - """ Move to an absolute position expressed in seconds - - :Parameters: - - `time_position` : float - seconds to skip from the start of the stream - """ - pass + """Go back to the beginning of audio stream.""" - + @property + @abstractmethod + def position(self): + """Return stream position in number of samples.""" -class BufferAudioSource(AudioSource, Rewindable): + @position.setter + @abstractmethod + def position(self, position): + """Set stream position in number of samples.""" + + @property + def position_s(self): + """Return stream position in seconds.""" + return self.position / self.sampling_rate + + @position_s.setter + def position_s(self, position_s): + """Set stream position in seconds.""" + self.position = int(self.sampling_rate * position_s) + + @property + def position_ms(self): + """Return stream position in milliseconds.""" + return (self.position * 1000) // self.sampling_rate + + @position_ms.setter + def position_ms(self, position_ms): + """Set stream position in milliseconds.""" + if not isinstance(position_ms, int): + raise ValueError("position_ms should be an int") + self.position = int(self.sampling_rate * position_ms / 1000) + + +class BufferAudioSource(Rewindable): """ - An :class:`AudioSource` that encapsulates and reads data from a memory buffer. - It implements methods from :class:`Rewindable` and is therefore a navigable :class:`AudioSource`. + An `AudioSource` that encapsulates and reads data from a memory buffer. + + This class implements the `Rewindable` interface. + Parameters + ---------- + data : bytes + audio data + sampling_rate : int, default: 16000 + number of samples per second of audio data. + sample_width : int, default: 2 + size in bytes of one audio sample. Possible values: 1, 2 or 4. + channels : int, default: 1 + number of channels of audio data. """ - - def __init__(self, data_buffer, - sampling_rate = DEFAULT_SAMPLE_RATE, - sample_width = DEFAULT_SAMPLE_WIDTH, - channels = DEFAULT_NB_CHANNELS): - - if len(data_buffer) % (sample_width * channels) !=0: - raise ValueError("length of data_buffer must be a multiple of (sample_width * channels)") - + + def __init__( + self, data, sampling_rate=16000, sample_width=2, channels=1, + ): AudioSource.__init__(self, sampling_rate, sample_width, channels) - self._buffer = data_buffer - self._index = 0 - self._left = 0 if self._buffer is None else len(self._buffer) + check_audio_data(data, sample_width, channels) + self._data = data + self._sample_size_all_channels = sample_width * channels + self._current_position_bytes = 0 self._is_open = False - + def is_open(self): return self._is_open - + def open(self): self._is_open = True - + def close(self): self._is_open = False self.rewind() - + def read(self, size): if not self._is_open: - raise IOError("Stream is not open") - - if self._left > 0: - - to_read = size * self.sample_width * self.channels - if to_read > self._left: - to_read = self._left - - data = self._buffer[self._index: self._index + to_read] - self._index += to_read - self._left -= to_read - + raise AudioIOError("Stream is not open") + if size is None or size < 0: + offset = None + else: + bytes_to_read = self._sample_size_all_channels * size + offset = self._current_position_bytes + bytes_to_read + data = self._data[self._current_position_bytes : offset] + if data: + self._current_position_bytes += len(data) return data - return None - - def get_data_buffer(self): - """ Return all audio data as one string buffer. """ - return self._buffer - - def set_data(self, data_buffer): - """ Set new data for this audio stream. - - :Parameters: - - `data_buffer` : str, basestring, Bytes - a string buffer with a length multiple of (sample_width * channels) - """ - if len(data_buffer) % (self.sample_width * self.channels) !=0: - raise ValueError("length of data_buffer must be a multiple of (sample_width * channels)") - self._buffer = data_buffer - self._index = 0 - self._left = 0 if self._buffer is None else len(self._buffer) - - def append_data(self, data_buffer): - """ Append data to this audio stream - - :Parameters: - - `data_buffer` : str, basestring, Bytes - a buffer with a length multiple of (sample_width * channels) - """ - - if len(data_buffer) % (self.sample_width * self.channels) !=0: - raise ValueError("length of data_buffer must be a multiple of (sample_width * channels)") - - self._buffer += data_buffer - self._left += len(data_buffer) - + @property + def data(self): + """Get raw audio data as a `bytes` object.""" + return self._data + def rewind(self): - self.set_position(0) - - def get_position(self): - return self._index / self.sample_width - - def get_time_position(self): - return float(self._index) / (self.sample_width * self.sampling_rate) - - def set_position(self, position): + self.position = 0 + + @property + def position(self): + """Get stream position in number of samples""" + return self._current_position_bytes // self._sample_size_all_channels + + @position.setter + def position(self, position): + """Set stream position in number of samples.""" + position *= self._sample_size_all_channels if position < 0: - raise ValueError("position must be >= 0") - - if self._buffer is None: - self._index = 0 - self._left = 0 - return - - position *= self.sample_width - self._index = position if position < len(self._buffer) else len(self._buffer) - self._left = len(self._buffer) - self._index + position += len(self.data) + if position < 0 or position > len(self.data): + raise IndexError("Position out of range") + self._current_position_bytes = position + + @property + def position_ms(self): + """Get stream position in milliseconds.""" + return (self._current_position_bytes * 1000) // ( + self._sample_size_all_channels * self.sampling_rate + ) + + @position_ms.setter + def position_ms(self, position_ms): + """Set stream position in milliseconds.""" + if not isinstance(position_ms, int): + raise ValueError("position_ms should be an int") + self.position = int(self.sampling_rate * position_ms / 1000) - def set_time_position(self, time_position): # time in seconds - position = int(self.sampling_rate * time_position) - self.set_position(position) - - - -class WaveAudioSource(AudioSource): +class FileAudioSource(AudioSource): """ - A class for an `AudioSource` that reads data from a wave file. - - :Parameters: - - `filename` : - path to a valid wave file + Base class `AudioSource`s that read audio data from a file. + + Parameters + ---------- + sampling_rate : int, default: 16000 + number of samples per second of audio data. + sample_width : int, default: 2 + size in bytes of one audio sample. Possible values: 1, 2 or 4. + channels : int, default: 1 + number of channels of audio data. """ - - def __init__(self, filename): - - self._filename = filename + + def __init__(self, sampling_rate, sample_width, channels): + AudioSource.__init__(self, sampling_rate, sample_width, channels) self._audio_stream = None - - stream = wave.open(self._filename) - AudioSource.__init__(self, stream.getframerate(), - stream.getsampwidth(), - stream.getnchannels()) - stream.close() - - + + def __del__(self): + if self.is_open(): + self.close() + def is_open(self): return self._audio_stream is not None - - def open(self): - if(self._audio_stream is None): - self._audio_stream = wave.open(self._filename) - - + def close(self): if self._audio_stream is not None: self._audio_stream.close() self._audio_stream = None - - + + @abstractmethod + def _read_from_stream(self, size): + """Read data from stream""" + def read(self, size): + if not self.is_open(): + raise AudioIOError("Audio stream is not open") + data = self._read_from_stream(size) + if not data: + return None + return data + + +class RawAudioSource(FileAudioSource): + """ + A class for an `AudioSource` that reads data from a raw (headerless) audio + file. + + This class should be used for large raw audio files to avoid loading the + whole data to memory. + + Parameters + ---------- + filename : str + path to a raw audio file. + sampling_rate : int + Number of samples per second of audio data. + sample_width : int + Size in bytes of one audio sample. Possible values : 1, 2, 4. + channels : int + Number of channels of audio data. + """ + + def __init__(self, file, sampling_rate, sample_width, channels): + FileAudioSource.__init__(self, sampling_rate, sample_width, channels) + self._file = file + self._audio_stream = None + self._sample_size = sample_width * channels + + def open(self): if self._audio_stream is None: - raise IOError("Stream is not open") + self._audio_stream = open(self._file, "rb") + + def _read_from_stream(self, size): + if size is None or size < 0: + bytes_to_read = None else: - data = self._audio_stream.readframes(size) - if data is None or len(data) < 1: - return None - return data + bytes_to_read = size * self._sample_size + data = self._audio_stream.read(bytes_to_read) + return data + + +class WaveAudioSource(FileAudioSource): + """ + A class for an `AudioSource` that reads data from a wave file. + + This class should be used for large wave files to avoid loading the whole + data to memory. + + Parameters + ---------- + filename : str + path to a valid wave file. + """ + + def __init__(self, filename): + self._filename = filename + self._audio_stream = None + stream = wave.open(self._filename, "rb") + FileAudioSource.__init__( + self, + stream.getframerate(), + stream.getsampwidth(), + stream.getnchannels(), + ) + stream.close() + + def open(self): + if self._audio_stream is None: + self._audio_stream = wave.open(self._filename) + + def _read_from_stream(self, size): + if size is None or size < 0: + size = -1 + return self._audio_stream.readframes(size) class PyAudioSource(AudioSource): """ - A class for an `AudioSource` that reads data the built-in microphone using PyAudio. + A class for an `AudioSource` that reads data from built-in microphone using + PyAudio (https://people.csail.mit.edu/hubert/pyaudio/). + + Parameters + ---------- + sampling_rate : int, default: 16000 + number of samples per second of audio data. + sample_width : int, default: 2 + size in bytes of one audio sample. Possible values: 1, 2 or 4. + channels : int, default: 1 + number of channels of audio data. + frames_per_buffer : int, default: 1024 + PyAudio number of frames per buffer. + input_device_index: None or int, default: None + PyAudio index of audio device to read audio data from. If None default + device is used. """ - - def __init__(self, sampling_rate = DEFAULT_SAMPLE_RATE, - sample_width = DEFAULT_SAMPLE_WIDTH, - channels = DEFAULT_NB_CHANNELS, - frames_per_buffer = 1024): - - + + def __init__( + self, + sampling_rate=16000, + sample_width=2, + channels=1, + frames_per_buffer=1024, + input_device_index=None, + ): + AudioSource.__init__(self, sampling_rate, sample_width, channels) self._chunk_size = frames_per_buffer - + self.input_device_index = input_device_index + import pyaudio + self._pyaudio_object = pyaudio.PyAudio() - self._pyaudio_format = self._pyaudio_object.get_format_from_width(self.sample_width) + self._pyaudio_format = self._pyaudio_object.get_format_from_width( + self.sample_width + ) self._audio_stream = None - def is_open(self): return self._audio_stream is not None - + def open(self): - self._audio_stream = self._pyaudio_object.open(format = self._pyaudio_format, - channels = self.channels, - rate = self.sampling_rate, - input = True, - output = False, - frames_per_buffer = self._chunk_size) - - + self._audio_stream = self._pyaudio_object.open( + format=self._pyaudio_format, + channels=self.channels, + rate=self.sampling_rate, + input=True, + output=False, + input_device_index=self.input_device_index, + frames_per_buffer=self._chunk_size, + ) + def close(self): if self._audio_stream is not None: self._audio_stream.stop_stream() self._audio_stream.close() self._audio_stream = None - - + def read(self, size): if self._audio_stream is None: raise IOError("Stream is not open") - if self._audio_stream.is_active(): data = self._audio_stream.read(size) if data is None or len(data) < 1: return None return data - return None - -class StdinAudioSource(AudioSource): + +class StdinAudioSource(FileAudioSource): """ - A class for an :class:`AudioSource` that reads data from standard input. + A class for an `AudioSource` that reads data from standard input. + + Parameters + ---------- + sampling_rate : int, default: 16000 + number of samples per second of audio data. + sample_width : int, default: 2 + size in bytes of one audio sample. Possible values: 1, 2 or 4. + channels : int, default: 1 + number of channels of audio data. """ - - def __init__(self, sampling_rate = DEFAULT_SAMPLE_RATE, - sample_width = DEFAULT_SAMPLE_WIDTH, - channels = DEFAULT_NB_CHANNELS): - - AudioSource.__init__(self, sampling_rate, sample_width, channels) + + def __init__( + self, sampling_rate=16000, sample_width=2, channels=1, + ): + FileAudioSource.__init__(self, sampling_rate, sample_width, channels) self._is_open = False - - + self._sample_size = sample_width * channels + self._stream = sys.stdin.buffer + def is_open(self): return self._is_open - + def open(self): self._is_open = True - + def close(self): self._is_open = False - - def read(self, size): - if not self._is_open: - raise IOError("Stream is not open") - - to_read = size * self.sample_width * self.channels - data = sys.stdin.read(to_read) - - if data is None or len(data) < 1: - return None - - return data - - -class PyAudioPlayer(): + + def _read_from_stream(self, size): + bytes_to_read = size * self._sample_size + data = self._stream.read(bytes_to_read) + if data: + return data + return None + + +def _make_tqdm_progress_bar(iterable, total, duration, **tqdm_kwargs): + fmt = tqdm_kwargs.get("bar_format", DEFAULT_BAR_FORMAT_TQDM) + fmt = fmt.replace("{duration}", "{:.3f}".format(duration)) + tqdm_kwargs["bar_format"] = fmt + + tqdm_kwargs["ncols"] = tqdm_kwargs.get("ncols", DEFAULT_NCOLS_TQDM) + tqdm_kwargs["mininterval"] = tqdm_kwargs.get( + "mininterval", DEFAULT_MIN_INTERVAL_TQDM + ) + return _tqdm(iterable, total=total, **tqdm_kwargs) + + +class PyAudioPlayer: """ A class for audio playback using Pyaudio + (https://people.csail.mit.edu/hubert/pyaudio/). + + Parameters + ---------- + sampling_rate : int, default: 16000 + number of samples per second of audio data. + sample_width : int, default: 2 + size in bytes of one audio sample. Possible values: 1, 2 or 4. + channels : int, default: 1 + number of channels of audio data. """ - - def __init__(self, sampling_rate = DEFAULT_SAMPLE_RATE, - sample_width = DEFAULT_SAMPLE_WIDTH, - channels = DEFAULT_NB_CHANNELS): - if not sample_width in (1, 2, 4): - raise ValueError("Sample width must be one of: 1, 2 or 4 (bytes)") - + + def __init__( + self, sampling_rate=16000, sample_width=2, channels=1, + ): + if sample_width not in (1, 2, 4): + raise ValueError("Sample width in bytes must be one of 1, 2 or 4") + self.sampling_rate = sampling_rate self.sample_width = sample_width self.channels = channels - + import pyaudio + self._p = pyaudio.PyAudio() - self.stream = self._p.open(format = self._p.get_format_from_width(self.sample_width), - channels = self.channels, rate = self.sampling_rate, - input = False, output = True) - - def play(self, data): + self.stream = self._p.open( + format=self._p.get_format_from_width(self.sample_width), + channels=self.channels, + rate=self.sampling_rate, + input=False, + output=True, + ) + + def play(self, data, progress_bar=False, **progress_bar_kwargs): + chunk_gen, nb_chunks = self._chunk_data(data) + if progress_bar and _WITH_TQDM: + duration = len(data) / ( + self.sampling_rate * self.sample_width * self.channels + ) + chunk_gen = _make_tqdm_progress_bar( + chunk_gen, + total=nb_chunks, + duration=duration, + **progress_bar_kwargs + ) if self.stream.is_stopped(): self.stream.start_stream() - - for chunk in self._chunk_data(data): - self.stream.write(chunk) - + try: + for chunk in chunk_gen: + self.stream.write(chunk) + except KeyboardInterrupt: + pass self.stream.stop_stream() - - def stop(self): + + def stop(self): if not self.stream.is_stopped(): self.stream.stop_stream() self.stream.close() self._p.terminate() - + def _chunk_data(self, data): # make audio chunks of 100 ms to allow interruption (like ctrl+c) - chunk_size = int((self.sampling_rate * self.sample_width * self.channels) / 10) - start = 0 - while start < len(data): - yield data[start : start + chunk_size] - start += chunk_size - - -def from_file(filename): - """ - Create an `AudioSource` object using the audio file specified by `filename`. - The appropriate :class:`AudioSource` class is guessed from file's extension. - - :Parameters: - - `filename` : - path to an audio file. - - :Returns: - - an `AudioSource` object that reads data from the given file. - - """ - - if filename.lower().endswith(".wav"): - return WaveAudioSource(filename) - - raise Exception("Can not create an AudioSource object from '%s'" %(filename)) + bytes_1_sec = self.sampling_rate * self.sample_width * self.channels + chunk_size = bytes_1_sec // 10 + # make sure chunk_size is a multiple of sample_width * channels + chunk_size -= chunk_size % (self.sample_width * self.channels) + nb_chunks, rest = divmod(len(data), chunk_size) + if rest > 0: + nb_chunks += 1 + chunk_gen = ( + data[i : i + chunk_size] for i in range(0, len(data), chunk_size) + ) + return chunk_gen, nb_chunks -def player_for(audio_source): +def player_for(source): """ - Return a :class:`PyAudioPlayer` that can play data from `audio_source`. - - :Parameters: - - `audio_source` : - an `AudioSource` object. - - :Returns: - - `PyAudioPlayer` that has the same sampling rate, sample width and number of channels - as `audio_source`. - """ - - return PyAudioPlayer(audio_source.get_sampling_rate(), - audio_source.get_sample_width(), - audio_source.get_channels()) - - + Return an `AudioPlayer` compatible with `source` (i.e., has the same + sampling rate, sample width and number of channels). + Parameters + ---------- + source : AudioSource + An object that has `sampling_rate`, `sample_width` and `sample_width` + attributes. + + Returns + ------- + player : PyAudioPlayer + An audio player that has the same sampling rate, sample width + and number of channels as `source`. + """ + return PyAudioPlayer( + source.sampling_rate, source.sample_width, source.channels + ) + + +def get_audio_source(input=None, **kwargs): + """ + Create and return an AudioSource from input. + + Parameters + ---------- + input : str, bytes, "-" or None (default) + source to read audio data from. If `str`, it should be a path to a valid + audio file. If `bytes`, it is used as raw audio data. If it is "-", + raw data will be read from stdin. If None, read audio data from the + microphone using PyAudio. + kwargs + audio parameters used to build the `AudioSource` object. Depending on + the nature of `input`, theses may be omitted (e.g., when `input` is an + audio file in a popular audio format such as wav, ogg, flac, etc.) or + include parameters such as `sampling_rate`, `sample_width`, `channels` + (or their respective short name versions `sr`, `sw` and `ch`) if `input` + is a path to a raw (headerless) audio file, a bytes object for raw audio + data or None (to read data from built-in microphone). See the respective + `AudioSource` classes from more information about possible parameters. + + Returns + ------- + source : AudioSource + audio source created from input parameters + """ + if input == "-": + return StdinAudioSource(*_get_audio_parameters(kwargs)) + + if isinstance(input, bytes): + return BufferAudioSource(input, *_get_audio_parameters(kwargs)) + + # read data from a file + if input is not None: + return from_file(filename=input, **kwargs) + + # read data from microphone via pyaudio + else: + frames_per_buffer = kwargs.get("frames_per_buffer", 1024) + input_device_index = kwargs.get("input_device_index") + return PyAudioSource( + *_get_audio_parameters(kwargs), + frames_per_buffer=frames_per_buffer, + input_device_index=input_device_index + ) + + +def _load_raw(file, sampling_rate, sample_width, channels, large_file=False): + """ + Load a raw audio file with standard Python. If `large_file` is True, return + a `RawAudioSource` object that reads data lazily from disk, otherwise load + all data to memory and return a `BufferAudioSource` object. + + Parameters + ---------- + file : str + path to a raw audio data file. + sampling_rate : int + sampling rate of audio data. + sample_width : int + size in bytes of one audio sample. + channels : int + number of channels of audio data. + large_file : bool + if True, return a `RawAudioSource` otherwise a `BufferAudioSource` + object. + + Returns + ------- + source : RawAudioSource or BufferAudioSource + an `AudioSource` that reads data from input file. + """ + if None in (sampling_rate, sample_width, channels): + raise AudioParameterError( + "All audio parameters are required for raw audio files" + ) + + if large_file: + return RawAudioSource( + file, + sampling_rate=sampling_rate, + sample_width=sample_width, + channels=channels, + ) + + with open(file, "rb") as fp: + data = fp.read() + return BufferAudioSource( + data, + sampling_rate=sampling_rate, + sample_width=sample_width, + channels=channels, + ) + + +def _load_wave(file, large_file=False): + """ + Load a wave audio file with standard Python. If `large_file` is True, return + a `WaveAudioSource` object that reads data lazily from disk, otherwise load + all data to memory and return a `BufferAudioSource` object. + + Parameters + ---------- + file : str + path to a wav audio data file + large_file : bool + if True, return a `WaveAudioSource` otherwise a `BufferAudioSource` + object. + + Returns + ------- + source : WaveAudioSource or BufferAudioSource + an `AudioSource` that reads data from input file. + """ + if large_file: + return WaveAudioSource(file) + with wave.open(file) as fp: + channels = fp.getnchannels() + srate = fp.getframerate() + swidth = fp.getsampwidth() + data = fp.readframes(-1) + return BufferAudioSource( + data, sampling_rate=srate, sample_width=swidth, channels=channels + ) + + +def _load_with_pydub(file, audio_format=None): + """ + Open compressed audio or video file using pydub. If a video file + is passed, its audio track(s) are extracted and loaded. + + Parameters + ---------- + file : str + path to audio file. + audio_format : str, default: None + string, audio/video file format if known (e.g. raw, webm, wav, ogg) + + Returns + ------- + source : BufferAudioSource + an `AudioSource` that reads data from input file. + """ + func_dict = { + "mp3": AudioSegment.from_mp3, + "ogg": AudioSegment.from_ogg, + "flv": AudioSegment.from_flv, + } + open_function = func_dict.get(audio_format, AudioSegment.from_file) + segment = open_function(file) + return BufferAudioSource( + data=segment.raw_data, + sampling_rate=segment.frame_rate, + sample_width=segment.sample_width, + channels=segment.channels, + ) + + +def from_file(filename, audio_format=None, large_file=False, **kwargs): + """ + Read audio data from `filename` and return an `AudioSource` object. + if `audio_format` is None, the appropriate `AudioSource` class is guessed + from file's extension. `filename` can be a compressed audio or video file. + This will require installing `pydub` (https://github.com/jiaaro/pydub). + + The normal behavior is to load all audio data to memory from which a + :class:`BufferAudioSource` object is created. This should be convenient + most of the time unless audio file is very large. In that case, and + in order to load audio data in lazy manner (i.e. read data from disk each + time :func:`AudioSource.read` is called), `large_file` should be True. + + Note that the current implementation supports only wave and raw formats for + lazy audio loading. + + If an audio format is `raw`, the following keyword arguments are required: + + - `sampling_rate`, `sr`: int, sampling rate of audio data. + - `sample_width`, `sw`: int, size in bytes of one audio sample. + - `channels`, `ch`: int, number of channels of audio data. + + See also + -------- + :func:`to_file`. + + Parameters + ---------- + filename : str + path to input audio or video file. + audio_format : str + audio format used to save data (e.g. raw, webm, wav, ogg). + large_file : bool, default: False + if True, audio won't fully be loaded to memory but only when a window + is read from disk. + + + Other Parameters + ---------------- + sampling_rate, sr: int + sampling rate of audio data + sample_width : int + sample width (i.e. number of bytes used to represent one audio sample) + channels : int + number of channels of audio data + + Returns + ------- + audio_source : AudioSource + an :class:`AudioSource` object that reads data from input file. + + Raises + ------ + `AudioIOError` + raised if audio data cannot be read in the given + format or if `format` is `raw` and one or more audio parameters are missing. + """ + audio_format = _guess_audio_format(audio_format, filename) + + if audio_format == "raw": + srate, swidth, channels = _get_audio_parameters(kwargs) + return _load_raw(filename, srate, swidth, channels, large_file) + + if audio_format in ["wav", "wave"]: + return _load_wave(filename, large_file) + if large_file: + err_msg = "if 'large_file` is True file format should be raw or wav" + raise AudioIOError(err_msg) + if _WITH_PYDUB: + return _load_with_pydub(filename, audio_format=audio_format) + else: + raise AudioIOError( + "pydub is required for audio formats other than raw or wav" + ) + + +def _save_raw(data, file): + """ + Saves audio data as a headerless (i.e. raw) file. + See also :func:`to_file`. + """ + with open(file, "wb") as fp: + fp.write(data) + + +def _save_wave(data, file, sampling_rate, sample_width, channels): + """ + Saves audio data to a wave file. + See also :func:`to_file`. + """ + if None in (sampling_rate, sample_width, channels): + raise AudioParameterError( + "All audio parameters are required to save wave audio files" + ) + with wave.open(file, "w") as fp: + fp.setframerate(sampling_rate) + fp.setsampwidth(sample_width) + fp.setnchannels(channels) + fp.writeframes(data) + + +def _save_with_pydub( + data, file, audio_format, sampling_rate, sample_width, channels +): + """ + Saves audio data with pydub (https://github.com/jiaaro/pydub). + See also :func:`to_file`. + """ + segment = AudioSegment( + data, + frame_rate=sampling_rate, + sample_width=sample_width, + channels=channels, + ) + with open(file, "wb") as fp: + segment.export(fp, format=audio_format) + + +def to_file(data, file, audio_format=None, **kwargs): + """ + Writes audio data to file. If `audio_format` is `None`, output + audio format will be guessed from extension. If `audio_format` + is `None` and `file` comes without an extension then audio + data will be written as a raw audio file. + + Parameters + ---------- + data : bytes-like + audio data to be written. Can be a `bytes`, `bytearray`, + `memoryview`, `array` or `numpy.ndarray` object. + file : str + path to output audio file. + audio_format : str + audio format used to save data (e.g. raw, webm, wav, ogg) + kwargs: dict + If an audio format other than `raw` is used, the following keyword + arguments are required: + + - `sampling_rate`, `sr`: int, sampling rate of audio data. + - `sample_width`, `sw`: int, size in bytes of one audio sample. + - `channels`, `ch`: int, number of channels of audio data. + + Raises + ------ + `AudioParameterError` if output format is different than raw and one or more + audio parameters are missing. `AudioIOError` if audio data cannot be written + in the desired format. + """ + audio_format = _guess_audio_format(audio_format, file) + if audio_format in (None, "raw"): + _save_raw(data, file) + return + try: + sampling_rate, sample_width, channels = _get_audio_parameters(kwargs) + except AudioParameterError as exc: + err_message = "All audio parameters are required to save formats " + "other than raw. Error detail: {}".format(exc) + raise AudioParameterError(err_message) + if audio_format in ("wav", "wave"): + _save_wave(data, file, sampling_rate, sample_width, channels) + elif _WITH_PYDUB: + _save_with_pydub( + data, file, audio_format, sampling_rate, sample_width, channels + ) + else: + err_message = "cannot write file format {} (file name: {})" + raise AudioIOError(err_message.format(audio_format, file)) diff --git a/libs/auditok/plotting.py b/libs/auditok/plotting.py new file mode 100755 index 000000000..eca5877f4 --- /dev/null +++ b/libs/auditok/plotting.py @@ -0,0 +1,150 @@ +import matplotlib.pyplot as plt +import numpy as np + +AUDITOK_PLOT_THEME = { + "figure": {"facecolor": "#482a36", "alpha": 0.2}, + "plot": {"facecolor": "#282a36"}, + "energy_threshold": { + "color": "#e31f8f", + "linestyle": "--", + "linewidth": 1, + }, + "signal": {"color": "#40d970", "linestyle": "-", "linewidth": 1}, + "detections": { + "facecolor": "#777777", + "edgecolor": "#ff8c1a", + "linewidth": 1, + "alpha": 0.75, + }, +} + + +def _make_time_axis(nb_samples, sampling_rate): + sample_duration = 1 / sampling_rate + x = np.linspace(0, sample_duration * (nb_samples - 1), nb_samples) + return x + + +def _plot_line(x, y, theme, xlabel=None, ylabel=None, **kwargs): + color = theme.get("color", theme.get("c")) + ls = theme.get("linestyle", theme.get("ls")) + lw = theme.get("linewidth", theme.get("lw")) + plt.plot(x, y, c=color, ls=ls, lw=lw, **kwargs) + plt.xlabel(xlabel, fontsize=8) + plt.ylabel(ylabel, fontsize=8) + + +def _plot_detections(subplot, detections, theme): + fc = theme.get("facecolor", theme.get("fc")) + ec = theme.get("edgecolor", theme.get("ec")) + ls = theme.get("linestyle", theme.get("ls")) + lw = theme.get("linewidth", theme.get("lw")) + alpha = theme.get("alpha") + for (start, end) in detections: + subplot.axvspan(start, end, fc=fc, ec=ec, ls=ls, lw=lw, alpha=alpha) + + +def plot( + audio_region, + scale_signal=True, + detections=None, + energy_threshold=None, + show=True, + figsize=None, + save_as=None, + dpi=120, + theme="auditok", +): + y = np.asarray(audio_region) + if len(y.shape) == 1: + y = y.reshape(1, -1) + nb_subplots, nb_samples = y.shape + sampling_rate = audio_region.sampling_rate + time_axis = _make_time_axis(nb_samples, sampling_rate) + if energy_threshold is not None: + eth_log10 = energy_threshold * np.log(10) / 10 + amplitude_threshold = np.sqrt(np.exp(eth_log10)) + else: + amplitude_threshold = None + if detections is None: + detections = [] + else: + # End of detection corresponds to the end of the last sample but + # to stay compatible with the time axis of signal plotting we want end + # of detection to correspond to the *start* of the that last sample. + detections = [ + (start, end - (1 / sampling_rate)) for (start, end) in detections + ] + if theme == "auditok": + theme = AUDITOK_PLOT_THEME + + fig = plt.figure(figsize=figsize, dpi=dpi) + fig_theme = theme.get("figure", theme.get("fig", {})) + fig_fc = fig_theme.get("facecolor", fig_theme.get("ffc")) + fig_alpha = fig_theme.get("alpha", 1) + fig.patch.set_facecolor(fig_fc) + fig.patch.set_alpha(fig_alpha) + + plot_theme = theme.get("plot", {}) + plot_fc = plot_theme.get("facecolor", plot_theme.get("pfc")) + + if nb_subplots > 2 and nb_subplots % 2 == 0: + nb_rows = nb_subplots // 2 + nb_columns = 2 + else: + nb_rows = nb_subplots + nb_columns = 1 + + for sid, samples in enumerate(y, 1): + ax = fig.add_subplot(nb_rows, nb_columns, sid) + ax.set_facecolor(plot_fc) + if scale_signal: + std = samples.std() + if std > 0: + mean = samples.mean() + std = samples.std() + samples = (samples - mean) / std + max_ = samples.max() + plt.ylim(-1.5 * max_, 1.5 * max_) + if amplitude_threshold is not None: + if scale_signal and std > 0: + amp_th = (amplitude_threshold - mean) / std + else: + amp_th = amplitude_threshold + eth_theme = theme.get("energy_threshold", theme.get("eth", {})) + _plot_line( + [time_axis[0], time_axis[-1]], + [amp_th] * 2, + eth_theme, + label="Detection threshold", + ) + if sid == 1: + legend = plt.legend( + ["Detection threshold"], + facecolor=fig_fc, + framealpha=0.1, + bbox_to_anchor=(0.0, 1.15, 1.0, 0.102), + loc=2, + ) + legend = plt.gca().add_artist(legend) + + signal_theme = theme.get("signal", {}) + _plot_line( + time_axis, + samples, + signal_theme, + xlabel="Time (seconds)", + ylabel="Signal{}".format(" (scaled)" if scale_signal else ""), + ) + detections_theme = theme.get("detections", {}) + _plot_detections(ax, detections, detections_theme) + plt.title("Channel {}".format(sid), fontsize=10) + + plt.xticks(fontsize=8) + plt.yticks(fontsize=8) + plt.tight_layout() + + if save_as is not None: + plt.savefig(save_as, dpi=dpi) + if show: + plt.show() diff --git a/libs/auditok/signal.py b/libs/auditok/signal.py new file mode 100644 index 000000000..3f00fb9e5 --- /dev/null +++ b/libs/auditok/signal.py @@ -0,0 +1,179 @@ +""" +Module for basic audio signal processing and array operations. + +.. autosummary:: + :toctree: generated/ + + to_array + extract_single_channel + compute_average_channel + compute_average_channel_stereo + separate_channels + calculate_energy_single_channel + calculate_energy_multichannel +""" +from array import array as array_ +import audioop +import math + +FORMAT = {1: "b", 2: "h", 4: "i"} +_EPSILON = 1e-10 + + +def to_array(data, sample_width, channels): + """Extract individual channels of audio data and return a list of arrays of + numeric samples. This will always return a list of `array.array` objects + (one per channel) even if audio data is mono. + + Parameters + ---------- + data : bytes + raw audio data. + sample_width : int + size in bytes of one audio sample (one channel considered). + + Returns + ------- + samples_arrays : list + list of arrays of audio samples. + """ + fmt = FORMAT[sample_width] + if channels == 1: + return [array_(fmt, data)] + return separate_channels(data, fmt, channels) + + +def extract_single_channel(data, fmt, channels, selected): + samples = array_(fmt, data) + return samples[selected::channels] + + +def compute_average_channel(data, fmt, channels): + """ + Compute and return average channel of multi-channel audio data. If the + number of channels is 2, use :func:`compute_average_channel_stereo` (much + faster). This function uses satandard `array` module to convert `bytes` data + into an array of numeric values. + + Parameters + ---------- + data : bytes + multi-channel audio data to mix down. + fmt : str + format (single character) to pass to `array.array` to convert `data` + into an array of samples. This should be "b" if audio data's sample width + is 1, "h" if it's 2 and "i" if it's 4. + channels : int + number of channels of audio data. + + Returns + ------- + mono_audio : bytes + mixed down audio data. + """ + all_channels = array_(fmt, data) + mono_channels = [ + array_(fmt, all_channels[ch::channels]) for ch in range(channels) + ] + avg_arr = array_( + fmt, + (round(sum(samples) / channels) for samples in zip(*mono_channels)), + ) + return avg_arr + + +def compute_average_channel_stereo(data, sample_width): + """Compute and return average channel of stereo audio data. This function + should be used when the number of channels is exactly 2 because in that + case we can use standard `audioop` module which *much* faster then calling + :func:`compute_average_channel`. + + Parameters + ---------- + data : bytes + 2-channel audio data to mix down. + sample_width : int + size in bytes of one audio sample (one channel considered). + + Returns + ------- + mono_audio : bytes + mixed down audio data. + """ + fmt = FORMAT[sample_width] + arr = array_(fmt, audioop.tomono(data, sample_width, 0.5, 0.5)) + return arr + + +def separate_channels(data, fmt, channels): + """Create a list of arrays of audio samples (`array.array` objects), one for + each channel. + + Parameters + ---------- + data : bytes + multi-channel audio data to mix down. + fmt : str + format (single character) to pass to `array.array` to convert `data` + into an array of samples. This should be "b" if audio data's sample width + is 1, "h" if it's 2 and "i" if it's 4. + channels : int + number of channels of audio data. + + Returns + ------- + channels_arr : list + list of audio channels, each as a standard `array.array`. + """ + all_channels = array_(fmt, data) + mono_channels = [ + array_(fmt, all_channels[ch::channels]) for ch in range(channels) + ] + return mono_channels + + +def calculate_energy_single_channel(data, sample_width): + """Calculate the energy of mono audio data. Energy is computed as: + + .. math:: energy = 20 \log(\sqrt({1}/{N}\sum_{i}^{N}{a_i}^2)) % # noqa: W605 + + where `a_i` is the i-th audio sample and `N` is the number of audio samples + in data. + + Parameters + ---------- + data : bytes + single-channel audio data. + sample_width : int + size in bytes of one audio sample. + + Returns + ------- + energy : float + energy of audio signal. + """ + energy_sqrt = max(audioop.rms(data, sample_width), _EPSILON) + return 20 * math.log10(energy_sqrt) + + +def calculate_energy_multichannel(x, sample_width, aggregation_fn=max): + """Calculate the energy of multi-channel audio data. Energy is calculated + channel-wise. An aggregation function is applied to the resulting energies + (default: `max`). Also see :func:`calculate_energy_single_channel`. + + Parameters + ---------- + data : bytes + single-channel audio data. + sample_width : int + size in bytes of one audio sample (one channel considered). + aggregation_fn : callable, default: max + aggregation function to apply to the resulting per-channel energies. + + Returns + ------- + energy : float + aggregated energy of multi-channel audio signal. + """ + energies = (calculate_energy_single_channel(xi, sample_width) for xi in x) + return aggregation_fn(energies) diff --git a/libs/auditok/signal_numpy.py b/libs/auditok/signal_numpy.py new file mode 100644 index 000000000..bf5425197 --- /dev/null +++ b/libs/auditok/signal_numpy.py @@ -0,0 +1,30 @@ +import numpy as np +from .signal import ( + compute_average_channel_stereo, + calculate_energy_single_channel, + calculate_energy_multichannel, +) + +FORMAT = {1: np.int8, 2: np.int16, 4: np.int32} + + +def to_array(data, sample_width, channels): + fmt = FORMAT[sample_width] + if channels == 1: + return np.frombuffer(data, dtype=fmt).astype(np.float64) + return separate_channels(data, fmt, channels).astype(np.float64) + + +def extract_single_channel(data, fmt, channels, selected): + samples = np.frombuffer(data, dtype=fmt) + return np.asanyarray(samples[selected::channels], order="C") + + +def compute_average_channel(data, fmt, channels): + array = np.frombuffer(data, dtype=fmt).astype(np.float64) + return array.reshape(-1, channels).mean(axis=1).round().astype(fmt) + + +def separate_channels(data, fmt, channels): + array = np.frombuffer(data, dtype=fmt) + return np.asanyarray(array.reshape(-1, channels).T, order="C") diff --git a/libs/auditok/util.py b/libs/auditok/util.py index d46a8899c..f29eb9bf3 100644 --- a/libs/auditok/util.py +++ b/libs/auditok/util.py @@ -1,448 +1,624 @@ """ -Class summary -============= - .. autosummary:: + :toctree: generated/ - DataSource - StringDataSource - ADSFactory - ADSFactory.AudioDataSource - ADSFactory.ADSDecorator - ADSFactory.OverlapADS - ADSFactory.LimiterADS - ADSFactory.RecorderADS - DataValidator - AudioEnergyValidator - + AudioEnergyValidator + AudioReader + Recorder + make_duration_formatter + make_channel_selector """ - - -from abc import ABCMeta, abstractmethod -import math -from array import array -from .io import Rewindable, from_file, BufferAudioSource, PyAudioSource -from .exceptions import DuplicateArgument -import sys - +from abc import ABC, abstractmethod +import warnings +from functools import partial +from .io import ( + AudioIOError, + AudioSource, + from_file, + BufferAudioSource, + PyAudioSource, + get_audio_source, +) +from .exceptions import ( + DuplicateArgument, + TooSamllBlockDuration, + TimeFormatError, +) try: - import numpy - _WITH_NUMPY = True -except ImportError as e: - _WITH_NUMPY = False - -try: - from builtins import str - basestring = str -except ImportError as e: - if sys.version_info >= (3, 0): - basestring = str - - + from . import signal_numpy as signal +except ImportError: + from . import signal -__all__ = ["DataSource", "DataValidator", "StringDataSource", "ADSFactory", "AudioEnergyValidator"] - -class DataSource(): +__all__ = [ + "make_duration_formatter", + "make_channel_selector", + "DataSource", + "DataValidator", + "StringDataSource", + "ADSFactory", + "AudioDataSource", + "AudioReader", + "Recorder", + "AudioEnergyValidator", +] + + +def make_duration_formatter(fmt): """ - Base class for objects passed to :func:`auditok.core.StreamTokenizer.tokenize`. + Make and return a function used to format durations in seconds. Accepted + format directives are: + + - ``%S`` : absolute number of seconds with 3 decimals. This direction should + be used alone. + - ``%i`` : milliseconds + - ``%s`` : seconds + - ``%m`` : minutes + - ``%h`` : hours + + These last 4 directives should all be specified. They can be placed anywhere + in the input string. + + Parameters + ---------- + fmt : str + duration format. + + Returns + ------- + formatter : callable + a function that takes a duration in seconds (float) and returns a string + that corresponds to that duration. + + Raises + ------ + TimeFormatError + if the format contains an unknown directive. + + Examples + -------- + + Using ``%S``: + + .. code:: python + + formatter = make_duration_formatter("%S") + formatter(123.589) + '123.589' + formatter(123) + '123.000' + + Using the other directives: + + .. code:: python + + formatter = make_duration_formatter("%h:%m:%s.%i") + formatter(3600+120+3.25) + '01:02:03.250' + + formatter = make_duration_formatter("%h hrs, %m min, %s sec and %i ms") + formatter(3600+120+3.25) + '01 hrs, 02 min, 03 sec and 250 ms' + + # omitting one of the 4 directives might result in a wrong duration + formatter = make_duration_formatter("%m min, %s sec and %i ms") + formatter(3600+120+3.25) + '02 min, 03 sec and 250 ms' + """ + if fmt == "%S": + + def fromatter(seconds): + return "{:.3f}".format(seconds) + + elif fmt == "%I": + + def fromatter(seconds): + return "{0}".format(int(seconds * 1000)) + + else: + fmt = fmt.replace("%h", "{hrs:02d}") + fmt = fmt.replace("%m", "{mins:02d}") + fmt = fmt.replace("%s", "{secs:02d}") + fmt = fmt.replace("%i", "{millis:03d}") + try: + i = fmt.index("%") + raise TimeFormatError( + "Unknown time format directive '{0}'".format(fmt[i : i + 2]) + ) + except ValueError: + pass + + def fromatter(seconds): + millis = int(seconds * 1000) + hrs, millis = divmod(millis, 3600000) + mins, millis = divmod(millis, 60000) + secs, millis = divmod(millis, 1000) + return fmt.format(hrs=hrs, mins=mins, secs=secs, millis=millis) + + return fromatter + + +def make_channel_selector(sample_width, channels, selected=None): + """Create and return a callable used for audio channel selection. The + returned selector can be used as `selector(audio_data)` and returns data + that contains selected channel only. + + Importantly, if `selected` is None or equals "any", `selector(audio_data)` + will separate and return a list of available channels: + `[data_channe_1, data_channe_2, ...].` + + Note also that returned `selector` expects `bytes` format for input data but + does notnecessarily return a `bytes` object. In fact, in order to extract + the desired channel (or compute the average channel if `selected` = "avg"), + it first converts input data into a `array.array` (or `numpy.ndarray`) + object. After channel of interst is selected/computed, it is returned as + such, without any reconversion to `bytes`. This behavior is wanted for + efficiency purposes because returned objects can be directly used as buffers + of bytes. In any case, returned objects can be converted back to `bytes` + using `bytes(obj)`. + + Exception to this is the special case where `channels` = 1 in which input + data is returned without any processing. + + + Parameters + ---------- + sample_width : int + number of bytes used to encode one audio sample, should be 1, 2 or 4. + channels : int + number of channels of raw audio data that the returned selector should + expect. + selected : int or str, default: None + audio channel to select and return when calling `selector(raw_data)`. It + should be an int >= `-channels` and < `channels`. If one of "mix", + "avg" or "average" is passed then `selector` will return the average + channel of audio data. If None or "any", return a list of all available + channels at each call. + + Returns + ------- + selector : callable + a callable that can be used as `selector(audio_data)` and returns data + that contains channel of interst. + + Raises + ------ + ValueError + if `sample_width` is not one of 1, 2 or 4, or if `selected` has an + unexpected value. + """ + fmt = signal.FORMAT.get(sample_width) + if fmt is None: + err_msg = "'sample_width' must be 1, 2 or 4, given: {}" + raise ValueError(err_msg.format(sample_width)) + if channels == 1: + return lambda x: x + + if isinstance(selected, int): + if selected < 0: + selected += channels + if selected < 0 or selected >= channels: + err_msg = "Selected channel must be >= -channels and < channels" + err_msg += ", given: {}" + raise ValueError(err_msg.format(selected)) + return partial( + signal.extract_single_channel, + fmt=fmt, + channels=channels, + selected=selected, + ) + + if selected in ("mix", "avg", "average"): + if channels == 2: + # when data is stereo, using audioop when possible is much faster + return partial( + signal.compute_average_channel_stereo, + sample_width=sample_width, + ) + + return partial( + signal.compute_average_channel, fmt=fmt, channels=channels + ) + + if selected in (None, "any"): + return partial(signal.separate_channels, fmt=fmt, channels=channels) + + raise ValueError( + "Selected channel must be an integer, None (alias 'any') or 'average' " + "(alias 'avg' or 'mix')" + ) + + +class DataSource(ABC): + """ + Base class for objects passed to :func:`StreamTokenizer.tokenize`. Subclasses should implement a :func:`DataSource.read` method. """ - __metaclass__ = ABCMeta - + @abstractmethod def read(self): """ - Read a piece of data read from this source. + Read a block (i.e., window) of data read from this source. If no more data is available, return None. """ - - -class DataValidator(): + + +class DataValidator(ABC): """ - Base class for a validator object used by :class:`.core.StreamTokenizer` to check - if read data is valid. + Base class for a validator object used by :class:`.core.StreamTokenizer` + to check if read data is valid. Subclasses should implement :func:`is_valid` method. """ - __metaclass__ = ABCMeta - + @abstractmethod def is_valid(self, data): """ Check whether `data` is valid """ + +class AudioEnergyValidator(DataValidator): + """ + A validator based on audio signal energy. For an input window of `N` audio + samples (see :func:`AudioEnergyValidator.is_valid`), the energy is computed + as: + + .. math:: energy = 20 \log(\sqrt({1}/{N}\sum_{i}^{N}{a_i}^2)) % # noqa: W605 + + where `a_i` is the i-th audio sample. + + Parameters + ---------- + energy_threshold : float + minimum energy that audio window should have to be valid. + sample_width : int + size in bytes of one audio sample. + channels : int + number of channels of audio data. + use_channel : {None, "any", "mix", "avg", "average"} or int + channel to use for energy computation. The following values are + accepted: + + - None (alias "any") : compute energy for each of the channels and return + the maximum value. + - "mix" (alias "avg" or "average") : compute the average channel then + compute its energy. + - int (>= 0 , < `channels`) : compute the energy of the specified channel + and ignore the other ones. + + Returns + ------- + energy : float + energy of the audio window. + """ + + def __init__( + self, energy_threshold, sample_width, channels, use_channel=None + ): + self._sample_width = sample_width + self._selector = make_channel_selector( + sample_width, channels, use_channel + ) + if channels == 1 or use_channel not in (None, "any"): + self._energy_fn = signal.calculate_energy_single_channel + else: + self._energy_fn = signal.calculate_energy_multichannel + self._energy_threshold = energy_threshold + + def is_valid(self, data): + """ + + Parameters + ---------- + data : bytes-like + array of raw audio data + + Returns + ------- + bool + True if the energy of audio data is >= threshold, False otherwise. + """ + log_energy = self._energy_fn(self._selector(data), self._sample_width) + return log_energy >= self._energy_threshold + + class StringDataSource(DataSource): """ - A class that represent a :class:`DataSource` as a string buffer. - Each call to :func:`DataSource.read` returns on character and moves one step forward. - If the end of the buffer is reached, :func:`read` returns None. - - :Parameters: - - `data` : - a basestring object. - + Class that represent a :class:`DataSource` as a string buffer. + Each call to :func:`DataSource.read` returns on character and moves one + step forward. If the end of the buffer is reached, :func:`read` returns + None. + + Parameters + ---------- + data : str + a string object used as data. + """ - + def __init__(self, data): self._data = None self._current = 0 self.set_data(data) - - + def read(self): """ Read one character from buffer. - - :Returns: - - Current character or None if end of buffer is reached + + Returns + ------- + char : str + current character or None if end of buffer is reached. """ - + if self._current >= len(self._data): return None self._current += 1 return self._data[self._current - 1] - + def set_data(self, data): """ Set a new data buffer. - - :Parameters: - - `data` : a basestring object - New data buffer. + + Parameters + ---------- + data : str + new data buffer. """ - - if not isinstance(data, basestring): - raise ValueError("data must an instance of basestring") + + if not isinstance(data, str): + raise ValueError("data must an instance of str") self._data = data self._current = 0 - class ADSFactory: """ - Factory class that makes it easy to create an :class:`ADSFactory.AudioDataSource` object that implements - :class:`DataSource` and can therefore be passed to :func:`auditok.core.StreamTokenizer.tokenize`. - - Whether you read audio data from a file, the microphone or a memory buffer, this factory - instantiates and returns the right :class:`ADSFactory.AudioDataSource` object. - - There are many other features you want your :class:`ADSFactory.AudioDataSource` object to have, such as: - memorize all read audio data so that you can rewind and reuse it (especially useful when - reading data from the microphone), read a fixed amount of data (also useful when reading - from the microphone), read overlapping audio frames (often needed when dosing a spectral - analysis of data). - - :func:`ADSFactory.ads` automatically creates and return object with the desired behavior according - to the supplied keyword arguments. - + .. deprecated:: 2.0.0 + `ADSFactory` will be removed in auditok 2.0.1, use instances of + :class:`AudioReader` instead. + + Factory class that makes it easy to create an + :class:`AudioDataSource` object that implements + :class:`DataSource` and can therefore be passed to + :func:`auditok.core.StreamTokenizer.tokenize`. + + Whether you read audio data from a file, the microphone or a memory buffer, + this factory instantiates and returns the right + :class:`AudioDataSource` object. + + There are many other features you want a :class:`AudioDataSource` object to + have, such as: memorize all read audio data so that you can rewind and reuse + it (especially useful when reading data from the microphone), read a fixed + amount of data (also useful when reading from the microphone), read + overlapping audio frames (often needed when dosing a spectral analysis of + data). + + :func:`ADSFactory.ads` automatically creates and return object with the + desired behavior according to the supplied keyword arguments. """ - - @staticmethod + + @staticmethod # noqa: C901 def _check_normalize_args(kwargs): - + for k in kwargs: - if not k in ["block_dur", "hop_dur", "block_size", "hop_size", "max_time", "record", - "audio_source", "filename", "data_buffer", "frames_per_buffer", "sampling_rate", - "sample_width", "channels", "sr", "sw", "ch", "asrc", "fn", "fpb", "db", "mt", - "rec", "bd", "hd", "bs", "hs"]: + if k not in [ + "block_dur", + "hop_dur", + "block_size", + "hop_size", + "max_time", + "record", + "audio_source", + "filename", + "data_buffer", + "frames_per_buffer", + "sampling_rate", + "sample_width", + "channels", + "sr", + "sw", + "ch", + "asrc", + "fn", + "fpb", + "db", + "mt", + "rec", + "bd", + "hd", + "bs", + "hs", + ]: raise ValueError("Invalid argument: {0}".format(k)) - + if "block_dur" in kwargs and "bd" in kwargs: - raise DuplicateArgument("Either 'block_dur' or 'bd' must be specified, not both") - + raise DuplicateArgument( + "Either 'block_dur' or 'bd' must be specified, not both" + ) + if "hop_dur" in kwargs and "hd" in kwargs: - raise DuplicateArgument("Either 'hop_dur' or 'hd' must be specified, not both") - + raise DuplicateArgument( + "Either 'hop_dur' or 'hd' must be specified, not both" + ) + if "block_size" in kwargs and "bs" in kwargs: - raise DuplicateArgument("Either 'block_size' or 'bs' must be specified, not both") - + raise DuplicateArgument( + "Either 'block_size' or 'bs' must be specified, not both" + ) + if "hop_size" in kwargs and "hs" in kwargs: - raise DuplicateArgument("Either 'hop_size' or 'hs' must be specified, not both") - + raise DuplicateArgument( + "Either 'hop_size' or 'hs' must be specified, not both" + ) + if "max_time" in kwargs and "mt" in kwargs: - raise DuplicateArgument("Either 'max_time' or 'mt' must be specified, not both") - + raise DuplicateArgument( + "Either 'max_time' or 'mt' must be specified, not both" + ) + if "audio_source" in kwargs and "asrc" in kwargs: - raise DuplicateArgument("Either 'audio_source' or 'asrc' must be specified, not both") - + raise DuplicateArgument( + "Either 'audio_source' or 'asrc' must be specified, not both" + ) + if "filename" in kwargs and "fn" in kwargs: - raise DuplicateArgument("Either 'filename' or 'fn' must be specified, not both") - + raise DuplicateArgument( + "Either 'filename' or 'fn' must be specified, not both" + ) + if "data_buffer" in kwargs and "db" in kwargs: - raise DuplicateArgument("Either 'filename' or 'db' must be specified, not both") - + raise DuplicateArgument( + "Either 'filename' or 'db' must be specified, not both" + ) + if "frames_per_buffer" in kwargs and "fbb" in kwargs: - raise DuplicateArgument("Either 'frames_per_buffer' or 'fpb' must be specified, not both") - + raise DuplicateArgument( + "Either 'frames_per_buffer' or 'fpb' must be specified, not " + "both" + ) + if "sampling_rate" in kwargs and "sr" in kwargs: - raise DuplicateArgument("Either 'sampling_rate' or 'sr' must be specified, not both") - + raise DuplicateArgument( + "Either 'sampling_rate' or 'sr' must be specified, not both" + ) + if "sample_width" in kwargs and "sw" in kwargs: - raise DuplicateArgument("Either 'sample_width' or 'sw' must be specified, not both") - + raise DuplicateArgument( + "Either 'sample_width' or 'sw' must be specified, not both" + ) + if "channels" in kwargs and "ch" in kwargs: - raise DuplicateArgument("Either 'channels' or 'ch' must be specified, not both") - + raise DuplicateArgument( + "Either 'channels' or 'ch' must be specified, not both" + ) + if "record" in kwargs and "rec" in kwargs: - raise DuplicateArgument("Either 'record' or 'rec' must be specified, not both") - - + raise DuplicateArgument( + "Either 'record' or 'rec' must be specified, not both" + ) + kwargs["bd"] = kwargs.pop("block_dur", None) or kwargs.pop("bd", None) kwargs["hd"] = kwargs.pop("hop_dur", None) or kwargs.pop("hd", None) kwargs["bs"] = kwargs.pop("block_size", None) or kwargs.pop("bs", None) kwargs["hs"] = kwargs.pop("hop_size", None) or kwargs.pop("hs", None) kwargs["mt"] = kwargs.pop("max_time", None) or kwargs.pop("mt", None) - kwargs["asrc"] = kwargs.pop("audio_source", None) or kwargs.pop("asrc", None) + kwargs["asrc"] = kwargs.pop("audio_source", None) or kwargs.pop( + "asrc", None + ) kwargs["fn"] = kwargs.pop("filename", None) or kwargs.pop("fn", None) kwargs["db"] = kwargs.pop("data_buffer", None) or kwargs.pop("db", None) - + record = kwargs.pop("record", False) if not record: record = kwargs.pop("rec", False) if not isinstance(record, bool): raise TypeError("'record' must be a boolean") - + kwargs["rec"] = record - - # keep long names for arguments meant for BufferAudioSource and PyAudioSource + + # keep long names for arguments meant for BufferAudioSource + # and PyAudioSource if "frames_per_buffer" in kwargs or "fpb" in kwargs: - kwargs["frames_per_buffer"] = kwargs.pop("frames_per_buffer", None) or kwargs.pop("fpb", None) - + kwargs["frames_per_buffer"] = kwargs.pop( + "frames_per_buffer", None + ) or kwargs.pop("fpb", None) + if "sampling_rate" in kwargs or "sr" in kwargs: - kwargs["sampling_rate"] = kwargs.pop("sampling_rate", None) or kwargs.pop("sr", None) - - if "sample_width" in kwargs or "sw" in kwargs: - kwargs["sample_width"] = kwargs.pop("sample_width", None) or kwargs.pop("sw", None) - + kwargs["sampling_rate"] = kwargs.pop( + "sampling_rate", None + ) or kwargs.pop("sr", None) + + if "sample_width" in kwargs or "sw" in kwargs: + kwargs["sample_width"] = kwargs.pop( + "sample_width", None + ) or kwargs.pop("sw", None) + if "channels" in kwargs or "ch" in kwargs: - kwargs["channels"] = kwargs.pop("channels", None) or kwargs.pop("ch", None) - - - - - - - + kwargs["channels"] = kwargs.pop("channels", None) or kwargs.pop( + "ch", None + ) + @staticmethod def ads(**kwargs): - """ - Create an return an :class:`ADSFactory.AudioDataSource`. The type and behavior of the object is the result - of the supplied parameters. - - :Parameters: - - *No parameters* : - read audio data from the available built-in microphone with the default parameters. - The returned :class:`ADSFactory.AudioDataSource` encapsulate an :class:`io.PyAudioSource` object and hence - it accepts the next four parameters are passed to use instead of their default values. - - `sampling_rate`, `sr` : *(int)* - number of samples per second. Default = 16000. - - `sample_width`, `sw` : *(int)* - number of bytes per sample (must be in (1, 2, 4)). Default = 2 - - `channels`, `ch` : *(int)* - number of audio channels. Default = 1 (only this value is currently accepted) - - `frames_per_buffer`, `fpb` : *(int)* - number of samples of PyAudio buffer. Default = 1024. - - `audio_source`, `asrc` : an `AudioSource` object - read data from this audio source - - `filename`, `fn` : *(string)* - build an `io.AudioSource` object using this file (currently only wave format is supported) - - `data_buffer`, `db` : *(string)* - build an `io.BufferAudioSource` using data in `data_buffer`. If this keyword is used, - `sampling_rate`, `sample_width` and `channels` are passed to `io.BufferAudioSource` - constructor and used instead of default values. - - `max_time`, `mt` : *(float)* - maximum time (in seconds) to read. Default behavior: read until there is no more data - available. - - `record`, `rec` : *(bool)* - save all read data in cache. Provide a navigable object which boasts a `rewind` method. - Default = False. - - `block_dur`, `bd` : *(float)* - processing block duration in seconds. This represents the quantity of audio data to return - each time the :func:`read` method is invoked. If `block_dur` is 0.025 (i.e. 25 ms) and the sampling - rate is 8000 and the sample width is 2 bytes, :func:`read` returns a buffer of 0.025 * 8000 * 2 = 400 - bytes at most. This parameter will be looked for (and used if available) before `block_size`. - If neither parameter is given, `block_dur` will be set to 0.01 second (i.e. 10 ms) - - - `hop_dur`, `hd` : *(float)* - quantity of data to skip from current processing window. if `hop_dur` is supplied then there - will be an overlap of `block_dur` - `hop_dur` between two adjacent blocks. This - parameter will be looked for (and used if available) before `hop_size`. If neither parameter - is given, `hop_dur` will be set to `block_dur` which means that there will be no overlap - between two consecutively read blocks. - - `block_size`, `bs` : *(int)* - number of samples to read each time the `read` method is called. Default: a block size - that represents a window of 10ms, so for a sampling rate of 16000, the default `block_size` - is 160 samples, for a rate of 44100, `block_size` = 441 samples, etc. - - `hop_size`, `hs` : *(int)* - determines the number of overlapping samples between two adjacent read windows. For a - `hop_size` of value *N*, the overlap is `block_size` - *N*. Default : `hop_size` = `block_size`, - means that there is no overlap. - - :Returns: - - An AudioDataSource object that has the desired features. - - :Exampels: - - 1. **Create an AudioDataSource that reads data from the microphone (requires Pyaudio) with default audio parameters:** - - .. code:: python - - from auditok import ADSFactory - ads = ADSFactory.ads() - ads.get_sampling_rate() - 16000 - ads.get_sample_width() - 2 - ads.get_channels() - 1 - - - 2. **Create an AudioDataSource that reads data from the microphone with a sampling rate of 48KHz:** - - .. code:: python - - from auditok import ADSFactory - ads = ADSFactory.ads(sr=48000) - ads.get_sampling_rate() - 48000 - - 3. **Create an AudioDataSource that reads data from a wave file:** - - .. code:: python - - import auditok - from auditok import ADSFactory - ads = ADSFactory.ads(fn=auditok.dataset.was_der_mensch_saet_mono_44100_lead_trail_silence) - ads.get_sampling_rate() - 44100 - ads.get_sample_width() - 2 - ads.get_channels() - 1 - - 4. **Define size of read blocks as 20 ms** - - .. code:: python - - import auditok - from auditok import ADSFactory - ''' - we know samling rate for previous file is 44100 samples/second - so 10 ms are equivalent to 441 samples and 20 ms to 882 - ''' - block_size = 882 - ads = ADSFactory.ads(bs = 882, fn=auditok.dataset.was_der_mensch_saet_mono_44100_lead_trail_silence) - ads.open() - # read one block - data = ads.read() - ads.close() - len(data) - 1764 - assert len(data) == ads.get_sample_width() * block_size - - 5. **Define block size as a duration (use block_dur or bd):** - - .. code:: python - - import auditok - from auditok import ADSFactory - dur = 0.25 # second - ads = ADSFactory.ads(bd = dur, fn=auditok.dataset.was_der_mensch_saet_mono_44100_lead_trail_silence) - ''' - we know samling rate for previous file is 44100 samples/second - for a block duration of 250 ms, block size should be 0.25 * 44100 = 11025 - ''' - ads.get_block_size() - 11025 - assert ads.get_block_size() == int(0.25 * 44100) - ads.open() - # read one block - data = ads.read() - ads.close() - len(data) - 22050 - assert len(data) == ads.get_sample_width() * ads.get_block_size() - - 6. **Read overlapping blocks (one of hope_size, hs, hop_dur or hd > 0):** - - For better readability we'd better use :class:`auditok.io.BufferAudioSource` with a string buffer: + Create an return an :class:`AudioDataSource`. The type and + behavior of the object is the result + of the supplied parameters. Called without any parameters, the class + will read audio data from the available built-in microphone with the + default parameters. - .. code:: python + Parameters + ---------- + sampling_rate, sr : int, default: 16000 + number of audio samples per second of input audio stream. + sample_width, sw : int, default: 2 + number of bytes per sample, must be one of 1, 2 or 4 + channels, ch : int, default: 1 + number of audio channels, only a value of 1 is currently accepted. + frames_per_buffer, fpb : int, default: 1024 + number of samples of PyAudio buffer. + audio_source, asrc : `AudioSource` + `AudioSource` to read data from + filename, fn : str + create an `AudioSource` object using this file + data_buffer, db : str + build an `io.BufferAudioSource` using data in `data_buffer`. + If this keyword is used, + `sampling_rate`, `sample_width` and `channels` are passed to + `io.BufferAudioSource` constructor and used instead of default + values. + max_time, mt : float + maximum time (in seconds) to read. Default behavior: read until + there is no more data + available. + record, rec : bool, default = False + save all read data in cache. Provide a navigable object which has a + `rewind` method. + block_dur, bd : float + processing block duration in seconds. This represents the quantity + of audio data to return each time the :func:`read` method is + invoked. If `block_dur` is 0.025 (i.e. 25 ms) and the sampling rate + is 8000 and the sample width is 2 bytes, :func:`read` returns a + buffer of 0.025 * 8000 * 2 = 400 bytes at most. This parameter will + be looked for (and used if available) before `block_size`. If + neither parameter is given, `block_dur` will be set to 0.01 second + (i.e. 10 ms) + hop_dur, hd : float + quantity of data to skip from current processing window. if + `hop_dur` is supplied then there will be an overlap of `block_dur` + - `hop_dur` between two adjacent blocks. This parameter will be + looked for (and used if available) before `hop_size`. + If neither parameter is given, `hop_dur` will be set to `block_dur` + which means that there will be no overlap between two consecutively + read blocks. + block_size, bs : int + number of samples to read each time the `read` method is called. + Default: a block size that represents a window of 10ms, so for a + sampling rate of 16000, the default `block_size` is 160 samples, + for a rate of 44100, `block_size` = 441 samples, etc. + hop_size, hs : int + determines the number of overlapping samples between two adjacent + read windows. For a `hop_size` of value *N*, the overlap is + `block_size` - *N*. Default : `hop_size` = `block_size`, means that + there is no overlap. - import auditok - from auditok import ADSFactory - ''' - we supply a data beffer instead of a file (keyword 'bata_buffer' or 'db') - sr : sampling rate = 16 samples/sec - sw : sample width = 1 byte - ch : channels = 1 - ''' - buffer = "abcdefghijklmnop" # 16 bytes = 1 second of data - bd = 0.250 # block duration = 250 ms = 4 bytes - hd = 0.125 # hop duration = 125 ms = 2 bytes - ads = ADSFactory.ads(db = "abcdefghijklmnop", bd = bd, hd = hd, sr = 16, sw = 1, ch = 1) - ads.open() - ads.read() - 'abcd' - ads.read() - 'cdef' - ads.read() - 'efgh' - ads.read() - 'ghij' - data = ads.read() - assert data == 'ijkl' - - 7. **Limit amount of read data (use max_time or mt):** - - .. code:: python - - ''' - We know audio file is larger than 2.25 seconds - We want to read up to 2.25 seconds of audio data - ''' - ads = ADSFactory.ads(mt = 2.25, fn=auditok.dataset.was_der_mensch_saet_mono_44100_lead_trail_silence) - ads.open() - data = [] - while True: - d = ads.read() - if d is None: - break - data.append(d) - - ads.close() - data = b''.join(data) - assert len(data) == int(ads.get_sampling_rate() * 2.25 * ads.get_sample_width() * ads.get_channels()) + Returns + ------- + audio_data_source : AudioDataSource + an `AudioDataSource` object build with input parameters. """ - - # copy user's dicionary (shallow copy) - kwargs = kwargs.copy() - + warnings.warn( + "'ADSFactory' is deprecated and will be removed in a future " + "release. Please use AudioReader class instead.", + DeprecationWarning, + ) + # check and normalize keyword arguments ADSFactory._check_normalize_args(kwargs) - + block_dur = kwargs.pop("bd") hop_dur = kwargs.pop("hd") block_size = kwargs.pop("bs") @@ -452,431 +628,483 @@ class ADSFactory: filename = kwargs.pop("fn") data_buffer = kwargs.pop("db") record = kwargs.pop("rec") - + # Case 1: an audio source is supplied if audio_source is not None: if (filename, data_buffer) != (None, None): - raise Warning("You should provide one of 'audio_source', 'filename' or 'data_buffer'\ - keyword parameters. 'audio_source' will be used") - + raise Warning( + "You should provide one of 'audio_source', 'filename' or \ + 'data_buffer' keyword parameters. 'audio_source' will be \ + used" + ) + # Case 2: a file name is supplied elif filename is not None: if data_buffer is not None: - raise Warning("You should provide one of 'filename' or 'data_buffer'\ - keyword parameters. 'filename' will be used") + raise Warning( + "You should provide one of 'filename' or 'data_buffer'\ + keyword parameters. 'filename' will be used" + ) audio_source = from_file(filename) - - # Case 3: a data_buffer is supplied + + # Case 3: a data_buffer is supplied elif data_buffer is not None: - audio_source = BufferAudioSource(data_buffer = data_buffer, **kwargs) - + audio_source = BufferAudioSource(data=data_buffer, **kwargs) + # Case 4: try to access native audio input else: audio_source = PyAudioSource(**kwargs) - - + if block_dur is not None: if block_size is not None: - raise DuplicateArgument("Either 'block_dur' or 'block_size' can be specified, not both") - else: - block_size = int(audio_source.get_sampling_rate() * block_dur) - elif block_size is None: - # Set default block_size to 10 ms - block_size = int(audio_source.get_sampling_rate() / 100) + raise DuplicateArgument( + "Either 'block_dur' or 'block_size' can be specified, not \ + both" + ) + elif block_size is not None: + block_dur = block_size / audio_source.sr + else: + block_dur = 0.01 # 10 ms - # Instantiate base AudioDataSource - ads = ADSFactory.AudioDataSource(audio_source=audio_source, block_size=block_size) - - # Limit data to be read - if max_time is not None: - ads = ADSFactory.LimiterADS(ads=ads, max_time=max_time) - - # Record, rewind and reuse data - if record: - ads = ADSFactory.RecorderADS(ads=ads) - # Read overlapping blocks of data if hop_dur is not None: if hop_size is not None: - raise DuplicateArgument("Either 'hop_dur' or 'hop_size' can be specified, not both") - else: - hop_size = int(audio_source.get_sampling_rate() * hop_dur) - - if hop_size is not None: - if hop_size <= 0 or hop_size > block_size: - raise ValueError("hop_size must be > 0 and <= block_size") - if hop_size < block_size: - ads = ADSFactory.OverlapADS(ads=ads, hop_size=hop_size) - + raise DuplicateArgument( + "Either 'hop_dur' or 'hop_size' can be specified, not both" + ) + elif hop_size is not None: + hop_dur = hop_size / audio_source.sr + + ads = AudioDataSource( + audio_source, + block_dur=block_dur, + hop_dur=hop_dur, + record=record, + max_read=max_time, + ) return ads - - - class AudioDataSource(DataSource): - """ - Base class for AudioDataSource objects. - It inherits from DataSource and encapsulates an AudioSource object. - """ - - def __init__(self, audio_source, block_size): - - self.audio_source = audio_source - self.block_size = block_size - - def get_block_size(self): - return self.block_size - - def set_block_size(self, size): - self.block_size = size - - def get_audio_source(self): - return self.audio_source - - def set_audio_source(self, audio_source): - self.audio_source = audio_source - - def open(self): - self.audio_source.open() - - def close(self): - self.audio_source.close() - - def is_open(self): - return self.audio_source.is_open() - - def get_sampling_rate(self): - return self.audio_source.get_sampling_rate() - - def get_sample_width(self): - return self.audio_source.get_sample_width() - - def get_channels(self): - return self.audio_source.get_channels() - - - def rewind(self): - if isinstance(self.audio_source, Rewindable): - self.audio_source.rewind() - else: - raise Exception("Audio source is not rewindable") - - - - def is_rewindable(self): - return isinstance(self.audio_source, Rewindable) - - - def read(self): - return self.audio_source.read(self.block_size) - class ADSDecorator(AudioDataSource): - """ - Base decorator class for AudioDataSource objects. - """ - __metaclass__ = ABCMeta - - def __init__(self, ads): - self.ads = ads - - self.get_block_size = self.ads.get_block_size - self.set_block_size = self.ads.set_block_size - self.get_audio_source = self.ads.get_audio_source - self.open = self.ads.open - self.close = self.ads.close - self.is_open = self.ads.is_open - self.get_sampling_rate = self.ads.get_sampling_rate - self.get_sample_width = self.ads.get_sample_width - self.get_channels = self.ads.get_channels - - def is_rewindable(self): - return self.ads.is_rewindable - - def rewind(self): - self.ads.rewind() - self._reinit() - - def set_audio_source(self, audio_source): - self.ads.set_audio_source(audio_source) - self._reinit() - - def open(self): - if not self.ads.is_open(): - self.ads.open() - self._reinit() - - @abstractmethod - def _reinit(self): - pass - - - class OverlapADS(ADSDecorator): - """ - A class for AudioDataSource objects that can read and return overlapping audio frames - """ - - def __init__(self, ads, hop_size): - ADSFactory.ADSDecorator.__init__(self, ads) - - if hop_size <= 0 or hop_size > self.get_block_size(): - raise ValueError("hop_size must be either 'None' or \ - between 1 and block_size (both inclusive)") - self.hop_size = hop_size - self._actual_block_size = self.get_block_size() - self._reinit() - - - def _get_block_size(): - return self._actual_block_size - - - def _read_first_block(self): - # For the first call, we need an entire block of size 'block_size' - block = self.ads.read() - if block is None: - return None - - # Keep a slice of data in cache and append it in the next call - if len(block) > self._hop_size_bytes: - self._cache = block[self._hop_size_bytes:] - - # Up from the next call, we will use '_read_next_blocks' - # and we only read 'hop_size' - self.ads.set_block_size(self.hop_size) - self.read = self._read_next_blocks - - return block - - def _read_next_blocks(self): - block = self.ads.read() - if block is None: - return None - - # Append block to cache data to ensure overlap - block = self._cache + block - # Keep a slice of data in cache only if we have a full length block - # if we don't that means that this is the last block - if len(block) == self._block_size_bytes: - self._cache = block[self._hop_size_bytes:] - else: - self._cache = None - - return block +class _AudioReadingProxy: + def __init__(self, audio_source): - def read(self): - pass - - def _reinit(self): + self._audio_source = audio_source + + def rewind(self): + if self.rewindable: + self._audio_source.rewind() + else: + raise AudioIOError("Audio stream is not rewindable") + + def rewindable(self): + try: + return self._audio_source.rewindable + except AttributeError: + return False + + def is_open(self): + return self._audio_source.is_open() + + def open(self): + self._audio_source.open() + + def close(self): + self._audio_source.close() + + def read(self, size): + return self._audio_source.read(size) + + @property + def data(self): + err_msg = "This AudioReader is not a recorder, no recorded data can " + err_msg += "be retrieved" + raise AttributeError(err_msg) + + def __getattr__(self, name): + return getattr(self._audio_source, name) + + +class _Recorder(_AudioReadingProxy): + """ + Class for `AudioReader` objects that can record all data they read. Useful + when reading data from microphone. + """ + + def __init__(self, audio_source): + super(_Recorder, self).__init__(audio_source) + self._cache = [] + self._read_block = self._read_and_cache + self._read_from_cache = False + self._data = None + + def read(self, size): + return self._read_block(size) + + @property + def data(self): + if self._data is None: + err_msg = "Unrewinded recorder. `rewind` should be called before " + err_msg += "accessing recorded data" + raise RuntimeError(err_msg) + return self._data + + def rewindable(self): + return True + + def rewind(self): + if self._read_from_cache: + self._audio_source.rewind() + else: + self._data = b"".join(self._cache) self._cache = None - self.ads.set_block_size(self._actual_block_size) - self._hop_size_bytes = self.hop_size * \ - self.get_sample_width() * \ - self.get_channels() - self._block_size_bytes = self.get_block_size() * \ - self.get_sample_width() * \ - self.get_channels() - self.read = self._read_first_block + self._audio_source = BufferAudioSource( + self._data, self.sr, self.sw, self.ch + ) + self._read_block = self._audio_source.read + self.open() + self._read_from_cache = True + + def _read_and_cache(self, size): + # Read and save read data + block = self._audio_source.read(size) + if block is not None: + self._cache.append(block) + return block - - class LimiterADS(ADSDecorator): - """ - A class for AudioDataSource objects that can read a fixed amount of data. - This can be useful when reading data from the microphone or from large audio files. - """ - - def __init__(self, ads, max_time): - ADSFactory.ADSDecorator.__init__(self, ads) - - self.max_time = max_time - self._reinit() - - def read(self): - if self._total_read_bytes >= self._max_read_bytes: - return None - block = self.ads.read() - if block is None: - return None - self._total_read_bytes += len(block) - - if self._total_read_bytes >= self._max_read_bytes: - self.close() - - return block - - - def _reinit(self): - self._max_read_bytes = int(self.max_time * self.get_sampling_rate()) * \ - self.get_sample_width() * \ - self.get_channels() - self._total_read_bytes = 0 - - - - class RecorderADS(ADSDecorator): - """ - A class for AudioDataSource objects that can record all audio data they read, - with a rewind facility. - """ - - def __init__(self, ads): - ADSFactory.ADSDecorator.__init__(self, ads) - - self._reinit() - - def read(self): - pass - - def _read_and_rec(self): - # Read and save read data - block = self.ads.read() - if block is not None: - self._cache.append(block) - - return block - - - def _read_simple(self): - # Read without recording - return self.ads.read() - - def rewind(self): - if self._record: - # If has been recording, create a new BufferAudioSource - # from recorded data - dbuffer = self._concatenate(self._cache) - asource = BufferAudioSource(dbuffer, self.get_sampling_rate(), - self.get_sample_width(), - self.get_channels()) - - - self.set_audio_source(asource) - self.open() - self._cache = [] - self._record = False - self.read = self._read_simple - - else: - self.ads.rewind() - if not self.is_open(): - self.open() - - - def is_rewindable(self): - return True - - def _reinit(self): - # when audio_source is replaced, start recording again - self._record = True - self._cache = [] - self.read = self._read_and_rec - - def _concatenate(self, data): - try: - # should always work for python 2 - # work for python 3 ONLY if data is a list (or an iterator) - # whose each element is a 'bytes' objects - return b''.join(data) - except TypeError: - # work for 'str' in python 2 and python 3 - return ''.join(data) - - -class AudioEnergyValidator(DataValidator): +class _Limiter(_AudioReadingProxy): """ - The most basic auditok audio frame validator. - This validator computes the log energy of an input audio frame - and return True if the result is >= a given threshold, False - otherwise. - - :Parameters: - - `sample_width` : *(int)* - Number of bytes of one audio sample. This is used to convert data from `basestring` or `Bytes` to - an array of floats. - - `energy_threshold` : *(float)* - A threshold used to check whether an input data buffer is valid. + Class for `AudioReader` objects that can read a fixed amount of data. + This can be useful when reading data from the microphone or from large + audio files. """ - - - if _WITH_NUMPY: - - _formats = {1: numpy.int8 , 2: numpy.int16, 4: numpy.int32} - @staticmethod - def _convert(signal, sample_width): - return numpy.array(numpy.frombuffer(signal, dtype=AudioEnergyValidator._formats[sample_width]), dtype=numpy.float64) - - @staticmethod - def _signal_energy(signal): - return float(numpy.dot(signal, signal)) / len(signal) - - @staticmethod - def _signal_log_energy(signal): - energy = AudioEnergyValidator._signal_energy(signal) - if energy <= 0: - return -200 - return 10. * numpy.log10(energy) - - else: - - - _formats = {1: 'b' , 2: 'h', 4: 'i'} - - @staticmethod - def _convert(signal, sample_width): - return array("d", array(AudioEnergyValidator._formats[sample_width], signal)) - - @staticmethod - def _signal_energy(signal): - energy = 0. - for a in signal: - energy += a * a - return energy / len(signal) - - @staticmethod - def _signal_log_energy(signal): - energy = AudioEnergyValidator._signal_energy(signal) - if energy <= 0: - return -200 - return 10. * math.log10(energy) - - - def __init__(self, sample_width, energy_threshold=45): - self.sample_width = sample_width - self._energy_threshold = energy_threshold - - - def is_valid(self, data): - """ - Check if data is valid. Audio data will be converted into an array (of - signed values) of which the log energy is computed. Log energy is computed - as follows: - - .. code:: python - - arr = AudioEnergyValidator._convert(signal, sample_width) - energy = float(numpy.dot(arr, arr)) / len(arr) - log_energy = 10. * numpy.log10(energy) - - - :Parameters: - - `data` : either a *string* or a *Bytes* buffer - `data` is converted into a numerical array using the `sample_width` - given in the constructor. - - :Retruns: - - True if `log_energy` >= `energy_threshold`, False otherwise. - """ - - signal = AudioEnergyValidator._convert(data, self.sample_width) - return AudioEnergyValidator._signal_log_energy(signal) >= self._energy_threshold - - def get_energy_threshold(self): - return self._energy_threshold - - def set_energy_threshold(self, threshold): - self._energy_threshold = threshold + def __init__(self, audio_source, max_read): + super(_Limiter, self).__init__(audio_source) + self._max_read = max_read + self._max_samples = round(max_read * self.sr) + self._bytes_per_sample = self.sw * self.ch + self._read_samples = 0 + @property + def data(self): + data = self._audio_source.data + max_read_bytes = self._max_samples * self._bytes_per_sample + return data[:max_read_bytes] + + @property + def max_read(self): + return self._max_read + + def read(self, size): + size = min(self._max_samples - self._read_samples, size) + if size <= 0: + return None + block = self._audio_source.read(size) + if block is None: + return None + self._read_samples += len(block) // self._bytes_per_sample + return block + + def rewind(self): + super(_Limiter, self).rewind() + self._read_samples = 0 + + +class _FixedSizeAudioReader(_AudioReadingProxy): + """ + Class to read fixed-size audio windows from source. + """ + + def __init__(self, audio_source, block_dur): + super(_FixedSizeAudioReader, self).__init__(audio_source) + + if block_dur <= 0: + raise ValueError( + "block_dur must be > 0, given: {}".format(block_dur) + ) + + self._block_size = int(block_dur * self.sr) + if self._block_size == 0: + err_msg = "Too small block_dur ({0:f}) for sampling rate ({1}). " + err_msg += "block_dur should cover at least one sample " + err_msg += "(i.e. 1/{1})" + raise TooSamllBlockDuration( + err_msg.format(block_dur, self.sr), block_dur, self.sr + ) + + def read(self): + return self._audio_source.read(self._block_size) + + @property + def block_size(self): + return self._block_size + + @property + def block_dur(self): + return self._block_size / self.sr + + def __getattr__(self, name): + return getattr(self._audio_source, name) + + +class _OverlapAudioReader(_FixedSizeAudioReader): + """ + Class for `AudioReader` objects that can read and return overlapping audio + windows. + """ + + def __init__(self, audio_source, block_dur, hop_dur): + + if hop_dur >= block_dur: + raise ValueError('"hop_dur" should be < "block_dur"') + + super(_OverlapAudioReader, self).__init__(audio_source, block_dur) + + self._hop_size = int(hop_dur * self.sr) + self._blocks = self._iter_blocks_with_overlap() + + def _iter_blocks_with_overlap(self): + while not self.is_open(): + yield AudioIOError + block = self._audio_source.read(self._block_size) + if block is None: + yield None + + _hop_size_bytes = ( + self._hop_size * self._audio_source.sw * self._audio_source.ch + ) + cache = block[_hop_size_bytes:] + yield block + + while True: + block = self._audio_source.read(self._hop_size) + if block: + block = cache + block + cache = block[_hop_size_bytes:] + yield block + continue + yield None + + def read(self): + try: + block = next(self._blocks) + if block == AudioIOError: + raise AudioIOError("Audio Stream is not open.") + return block + except StopIteration: + return None + + def rewind(self): + super(_OverlapAudioReader, self).rewind() + self._blocks = self._iter_blocks_with_overlap() + + @property + def hop_size(self): + return self._hop_size + + @property + def hop_dur(self): + return self._hop_size / self.sr + + def __getattr__(self, name): + return getattr(self._audio_source, name) + + +class AudioReader(DataSource): + """ + Class to read fixed-size chunks of audio data from a source. A source can + be a file on disk, standard input (with `input` = "-") or microphone. This + is normally used by tokenization algorithms that expect source objects with + a `read` function that returns a windows of data of the same size at each + call expect when remaining data does not make up a full window. + + Objects of this class can be set up to return audio windows with a given + overlap and to record the whole stream for later access (useful when + reading data from the microphone). They can also have + a limit for the maximum amount of data to read. + + Parameters + ---------- + input : str, bytes, AudioSource, AudioReader, AudioRegion or None + input audio data. If the type of the passed argument is `str`, it should + be a path to an existing audio file. "-" is interpreted as standardinput. + If the type is `bytes`, input is considered as a buffer of raw audio + data. If None, read audio from microphone. Every object that is not an + :class:`AudioReader` will be transformed, when possible, into an + :class:`AudioSource` before processing. If it is an `str` that refers to + a raw audio file, `bytes` or None, audio parameters should be provided + using kwargs (i.e., `samplig_rate`, `sample_width` and `channels` or + their alias). + block_dur: float, default: 0.01 + length in seconds of audio windows to return at each `read` call. + hop_dur: float, default: None + length in seconds of data amount to skip from previous window. If + defined, it is used to compute the temporal overlap between previous and + current window (nameply `overlap = block_dur - hop_dur`). Default, None, + means that consecutive windows do not overlap. + record: bool, default: False + whether to record read audio data for later access. If True, audio data + can be retrieved by first calling `rewind()`, then using the `data` + property. Note that once `rewind()` is called, no new data will be read + from source (subsequent `read()` call will read data from cache) and + that there's no need to call `rewind()` again to access `data` property. + max_read: float, default: None + maximum amount of audio data to read in seconds. Default is None meaning + that data will be read until end of stream is reached or, when reading + from microphone a Ctrl-C is sent. + + When `input` is None, of type bytes or a raw audio files some of the + follwing kwargs are mandatory. + + Other Parameters + ---------------- + audio_format, fmt : str + type of audio data (e.g., wav, ogg, flac, raw, etc.). This will only be + used if `input` is a string path to an audio file. If not given, audio + type will be guessed from file name extension or from file header. + sampling_rate, sr : int + sampling rate of audio data. Required if `input` is a raw audio file, is + a bytes object or None (i.e., read from microphone). + sample_width, sw : int + number of bytes used to encode one audio sample, typically 1, 2 or 4. + Required for raw data, see `sampling_rate`. + channels, ch : int + number of channels of audio data. Required for raw data, see + `sampling_rate`. + use_channel, uc : {None, "any", "mix", "avg", "average"} or int + which channel to use for split if `input` has multiple audio channels. + Regardless of which channel is used for splitting, returned audio events + contain data from *all* the channels of `input`. The following values + are accepted: + + - None (alias "any"): accept audio activity from any channel, even if + other channels are silent. This is the default behavior. + + - "mix" (alias "avg" or "average"): mix down all channels (i.e., compute + average channel) and split the resulting channel. + + - int (>= 0 , < `channels`): use one channel, specified by its integer + id, for split. + + large_file : bool, default: False + If True, AND if `input` is a path to a *wav* of a *raw* audio file + (and only these two formats) then audio data is lazily loaded to memory + (i.e., one analysis window a time). Otherwise the whole file is loaded + to memory before split. Set to True if the size of the file is larger + than available memory. + """ + + def __init__( + self, + input, + block_dur=0.01, + hop_dur=None, + record=False, + max_read=None, + **kwargs + ): + if not isinstance(input, AudioSource): + input = get_audio_source(input, **kwargs) + self._record = record + if record: + input = _Recorder(input) + if max_read is not None: + input = _Limiter(input, max_read) + self._max_read = max_read + if hop_dur is not None: + input = _OverlapAudioReader(input, block_dur, hop_dur) + else: + input = _FixedSizeAudioReader(input, block_dur) + self._audio_source = input + + def __repr__(self): + block_dur, hop_dur, max_read = None, None, None + if self.block_dur is not None: + block_dur = "{:.3f}".format(self.block_dur) + if self.hop_dur is not None: + hop_dur = "{:.3f}".format(self.hop_dur) + if self.max_read is not None: + max_read = "{:.3f}".format(self.max_read) + return ( + "{cls}(block_dur={block_dur}, " + "hop_dur={hop_dur}, record={rewindable}, " + "max_read={max_read})" + ).format( + cls=self.__class__.__name__, + block_dur=block_dur, + hop_dur=hop_dur, + rewindable=self._record, + max_read=max_read, + ) + + @property + def rewindable(self): + return self._record + + @property + def block_dur(self): + return self._audio_source.block_size / self._audio_source.sr + + @property + def hop_dur(self): + if hasattr(self._audio_source, "hop_dur"): + return self._audio_source.hop_size / self._audio_source.sr + return self.block_dur + + @property + def hop_size(self): + if hasattr(self._audio_source, "hop_size"): + return self._audio_source.hop_size + return self.block_size + + @property + def max_read(self): + try: + return self._audio_source.max_read + except AttributeError: + return None + + def read(self): + return self._audio_source.read() + + def __getattr__(self, name): + if name in ("data", "rewind") and not self.rewindable: + raise AttributeError( + "'AudioReader' has no attribute '{}'".format(name) + ) + try: + return getattr(self._audio_source, name) + except AttributeError: + raise AttributeError( + "'AudioReader' has no attribute '{}'".format(name) + ) + + +# Keep AudioDataSource for compatibility +# Remove in a future version when ADSFactory is removed +AudioDataSource = AudioReader + + +class Recorder(AudioReader): + """Class to read fixed-size chunks of audio data from a source and keeps + data in a cache. Using this class is equivalent to initializing + :class:`AudioReader` with `record=True`. For more information about the + other parameters see :class:`AudioReader`. + + Once the desired amount of data is read, you can call the :func:`rewind` + method then get the recorded data via the :attr:`data` attribute. You can also + re-read cached data one window a time by calling :func:`read`. + """ + + def __init__( + self, input, block_dur=0.01, hop_dur=None, max_read=None, **kwargs + ): + super().__init__( + input, + block_dur=block_dur, + hop_dur=hop_dur, + record=True, + max_read=max_read, + **kwargs + ) diff --git a/libs/auditok/workers.py b/libs/auditok/workers.py new file mode 100755 index 000000000..bb6d54a98 --- /dev/null +++ b/libs/auditok/workers.py @@ -0,0 +1,427 @@ +import os +import sys +from tempfile import NamedTemporaryFile +from abc import ABCMeta, abstractmethod +from threading import Thread +from datetime import datetime, timedelta +from collections import namedtuple +import wave +import subprocess +from queue import Queue, Empty +from .io import _guess_audio_format +from .util import AudioDataSource, make_duration_formatter +from .core import split +from .exceptions import ( + EndOfProcessing, + AudioEncodingError, + AudioEncodingWarning, +) + + +_STOP_PROCESSING = "STOP_PROCESSING" +_Detection = namedtuple("_Detection", "id start end duration") + + +def _run_subprocess(command): + try: + with subprocess.Popen( + command, + stdin=open(os.devnull, "rb"), + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) as proc: + stdout, stderr = proc.communicate() + return proc.returncode, stdout, stderr + except Exception: + err_msg = "Couldn't export audio using command: '{}'".format(command) + raise AudioEncodingError(err_msg) + + +class Worker(Thread, metaclass=ABCMeta): + def __init__(self, timeout=0.5, logger=None): + self._timeout = timeout + self._logger = logger + self._inbox = Queue() + Thread.__init__(self) + + def run(self): + while True: + message = self._get_message() + if message == _STOP_PROCESSING: + break + if message is not None: + self._process_message(message) + self._post_process() + + @abstractmethod + def _process_message(self, message): + """Process incoming messages""" + + def _post_process(self): + pass + + def _log(self, message): + self._logger.info(message) + + def _stop_requested(self): + try: + message = self._inbox.get_nowait() + if message == _STOP_PROCESSING: + return True + except Empty: + return False + + def stop(self): + self.send(_STOP_PROCESSING) + self.join() + + def send(self, message): + self._inbox.put(message) + + def _get_message(self): + try: + message = self._inbox.get(timeout=self._timeout) + return message + except Empty: + return None + + +class TokenizerWorker(Worker, AudioDataSource): + def __init__(self, reader, observers=None, logger=None, **kwargs): + self._observers = observers if observers is not None else [] + self._reader = reader + self._audio_region_gen = split(self, **kwargs) + self._detections = [] + self._log_format = "[DET]: Detection {0.id} (start: {0.start:.3f}, " + self._log_format += "end: {0.end:.3f}, duration: {0.duration:.3f})" + Worker.__init__(self, timeout=0.2, logger=logger) + + def _process_message(self): + pass + + @property + def detections(self): + return self._detections + + def _notify_observers(self, message): + for observer in self._observers: + observer.send(message) + + def run(self): + self._reader.open() + start_processing_timestamp = datetime.now() + for _id, audio_region in enumerate(self._audio_region_gen, start=1): + timestamp = start_processing_timestamp + timedelta( + seconds=audio_region.meta.start + ) + audio_region.meta.timestamp = timestamp + detection = _Detection( + _id, + audio_region.meta.start, + audio_region.meta.end, + audio_region.duration, + ) + self._detections.append(detection) + if self._logger is not None: + message = self._log_format.format(detection) + self._log(message) + self._notify_observers((_id, audio_region)) + self._notify_observers(_STOP_PROCESSING) + self._reader.close() + + def start_all(self): + for observer in self._observers: + observer.start() + self.start() + + def stop_all(self): + self.stop() + for observer in self._observers: + observer.stop() + self._reader.close() + + def read(self): + if self._stop_requested(): + return None + else: + return self._reader.read() + + def __getattr__(self, name): + return getattr(self._reader, name) + + +class StreamSaverWorker(Worker): + def __init__( + self, + audio_reader, + filename, + export_format=None, + cache_size_sec=0.5, + timeout=0.2, + ): + self._reader = audio_reader + sample_size_bytes = self._reader.sw * self._reader.ch + self._cache_size = cache_size_sec * self._reader.sr * sample_size_bytes + self._output_filename = filename + self._export_format = _guess_audio_format(export_format, filename) + if self._export_format is None: + self._export_format = "wav" + self._init_output_stream() + self._exported = False + self._cache = [] + self._total_cached = 0 + Worker.__init__(self, timeout=timeout) + + def _get_non_existent_filename(self): + filename = self._output_filename + ".wav" + i = 0 + while os.path.exists(filename): + i += 1 + filename = self._output_filename + "({}).wav".format(i) + return filename + + def _init_output_stream(self): + if self._export_format != "wav": + self._tmp_output_filename = self._get_non_existent_filename() + else: + self._tmp_output_filename = self._output_filename + self._wfp = wave.open(self._tmp_output_filename, "wb") + self._wfp.setframerate(self._reader.sr) + self._wfp.setsampwidth(self._reader.sw) + self._wfp.setnchannels(self._reader.ch) + + @property + def sr(self): + return self._reader.sampling_rate + + @property + def sw(self): + return self._reader.sample_width + + @property + def ch(self): + return self._reader.channels + + def __del__(self): + self._post_process() + + if ( + (self._tmp_output_filename != self._output_filename) + and self._exported + and os.path.exists(self._tmp_output_filename) + ): + os.remove(self._tmp_output_filename) + + def _process_message(self, data): + self._cache.append(data) + self._total_cached += len(data) + if self._total_cached >= self._cache_size: + self._write_cached_data() + + def _post_process(self): + while True: + try: + data = self._inbox.get_nowait() + if data != _STOP_PROCESSING: + self._cache.append(data) + self._total_cached += len(data) + except Empty: + break + self._write_cached_data() + self._wfp.close() + + def _write_cached_data(self): + if self._cache: + data = b"".join(self._cache) + self._wfp.writeframes(data) + self._cache = [] + self._total_cached = 0 + + def open(self): + self._reader.open() + + def close(self): + self._reader.close() + self.stop() + + def rewind(self): + # ensure compatibility with AudioDataSource with record=True + pass + + @property + def data(self): + with wave.open(self._tmp_output_filename, "rb") as wfp: + return wfp.readframes(-1) + + def save_stream(self): + if self._exported: + return self._output_filename + + if self._export_format in ("raw", "wav"): + if self._export_format == "raw": + self._export_raw() + self._exported = True + return self._output_filename + try: + self._export_with_ffmpeg_or_avconv() + except AudioEncodingError: + try: + self._export_with_sox() + except AudioEncodingError: + warn_msg = "Couldn't save audio data in the desired format " + warn_msg += "'{}'. Either none of 'ffmpeg', 'avconv' or 'sox' " + warn_msg += "is installed or this format is not recognized.\n" + warn_msg += "Audio file was saved as '{}'" + raise AudioEncodingWarning( + warn_msg.format( + self._export_format, self._tmp_output_filename + ) + ) + finally: + self._exported = True + return self._output_filename + + def _export_raw(self): + with open(self._output_filename, "wb") as wfp: + wfp.write(self.data) + + def _export_with_ffmpeg_or_avconv(self): + command = [ + "-y", + "-f", + "wav", + "-i", + self._tmp_output_filename, + "-f", + self._export_format, + self._output_filename, + ] + returncode, stdout, stderr = _run_subprocess(["ffmpeg"] + command) + if returncode != 0: + returncode, stdout, stderr = _run_subprocess(["avconv"] + command) + if returncode != 0: + raise AudioEncodingError(stderr) + return stdout, stderr + + def _export_with_sox(self): + command = [ + "sox", + "-t", + "wav", + self._tmp_output_filename, + self._output_filename, + ] + returncode, stdout, stderr = _run_subprocess(command) + if returncode != 0: + raise AudioEncodingError(stderr) + return stdout, stderr + + def close_output(self): + self._wfp.close() + + def read(self): + data = self._reader.read() + if data is not None: + self.send(data) + else: + self.send(_STOP_PROCESSING) + return data + + def __getattr__(self, name): + if name == "data": + return self.data + return getattr(self._reader, name) + + +class PlayerWorker(Worker): + def __init__(self, player, progress_bar=False, timeout=0.2, logger=None): + self._player = player + self._progress_bar = progress_bar + self._log_format = "[PLAY]: Detection {id} played" + Worker.__init__(self, timeout=timeout, logger=logger) + + def _process_message(self, message): + _id, audio_region = message + if self._logger is not None: + message = self._log_format.format(id=_id) + self._log(message) + audio_region.play( + player=self._player, progress_bar=self._progress_bar, leave=False + ) + + +class RegionSaverWorker(Worker): + def __init__( + self, + filename_format, + audio_format=None, + timeout=0.2, + logger=None, + **audio_parameters + ): + self._filename_format = filename_format + self._audio_format = audio_format + self._audio_parameters = audio_parameters + self._debug_format = "[SAVE]: Detection {id} saved as '{filename}'" + Worker.__init__(self, timeout=timeout, logger=logger) + + def _process_message(self, message): + _id, audio_region = message + filename = self._filename_format.format( + id=_id, + start=audio_region.meta.start, + end=audio_region.meta.end, + duration=audio_region.duration, + ) + filename = audio_region.save( + filename, self._audio_format, **self._audio_parameters + ) + if self._logger: + message = self._debug_format.format(id=_id, filename=filename) + self._log(message) + + +class CommandLineWorker(Worker): + def __init__(self, command, timeout=0.2, logger=None): + self._command = command + Worker.__init__(self, timeout=timeout, logger=logger) + self._debug_format = "[COMMAND]: Detection {id} command: '{command}'" + + def _process_message(self, message): + _id, audio_region = message + with NamedTemporaryFile(delete=False) as file: + filename = audio_region.save(file.name, audio_format="wav") + command = self._command.format(file=filename) + os.system(command) + if self._logger is not None: + message = self._debug_format.format(id=_id, command=command) + self._log(message) + + +class PrintWorker(Worker): + def __init__( + self, + print_format="{start} {end}", + time_format="%S", + timestamp_format="%Y/%m/%d %H:%M:%S.%f", + timeout=0.2, + ): + + self._print_format = print_format + self._format_time = make_duration_formatter(time_format) + self._timestamp_format = timestamp_format + self.detections = [] + Worker.__init__(self, timeout=timeout) + + def _process_message(self, message): + _id, audio_region = message + timestamp = audio_region.meta.timestamp + timestamp = timestamp.strftime(self._timestamp_format) + text = self._print_format.format( + id=_id, + start=self._format_time(audio_region.meta.start), + end=self._format_time(audio_region.meta.end), + duration=self._format_time(audio_region.duration), + timestamp=timestamp, + ) + print(text) diff --git a/libs/backports/__init__.py b/libs/backports/__init__.py index f84d25cf9..0d1f7edf5 100644 --- a/libs/backports/__init__.py +++ b/libs/backports/__init__.py @@ -1,11 +1 @@ -# A Python "namespace package" http://www.python.org/dev/peps/pep-0382/ -# This always goes inside of a namespace package's __init__.py - -from pkgutil import extend_path -__path__ = extend_path(__path__, __name__) - -try: - import pkg_resources - pkg_resources.declare_namespace(__name__) -except ImportError: - pass +__path__ = __import__('pkgutil').extend_path(__path__, __name__) # type: ignore diff --git a/libs/backports/functools_lru_cache.py b/libs/backports/functools_lru_cache.py index 707c6c766..8be4515fe 100644 --- a/libs/backports/functools_lru_cache.py +++ b/libs/backports/functools_lru_cache.py @@ -4,14 +4,16 @@ import functools from collections import namedtuple from threading import RLock -_CacheInfo = namedtuple("CacheInfo", ["hits", "misses", "maxsize", "currsize"]) +_CacheInfo = namedtuple("_CacheInfo", ["hits", "misses", "maxsize", "currsize"]) @functools.wraps(functools.update_wrapper) -def update_wrapper(wrapper, - wrapped, - assigned = functools.WRAPPER_ASSIGNMENTS, - updated = functools.WRAPPER_UPDATES): +def update_wrapper( + wrapper, + wrapped, + assigned=functools.WRAPPER_ASSIGNMENTS, + updated=functools.WRAPPER_UPDATES, +): """ Patch two bugs in functools.update_wrapper. """ @@ -34,10 +36,17 @@ class _HashedSeq(list): return self.hashvalue -def _make_key(args, kwds, typed, - kwd_mark=(object(),), - fasttypes=set([int, str, frozenset, type(None)]), - sorted=sorted, tuple=tuple, type=type, len=len): +def _make_key( + args, + kwds, + typed, + kwd_mark=(object(),), + fasttypes=set([int, str, frozenset, type(None)]), + sorted=sorted, + tuple=tuple, + type=type, + len=len, +): 'Make a cache key from optionally typed positional and keyword arguments' key = args if kwds: @@ -54,7 +63,7 @@ def _make_key(args, kwds, typed, return _HashedSeq(key) -def lru_cache(maxsize=100, typed=False): +def lru_cache(maxsize=100, typed=False): # noqa: C901 """Least-recently-used cache decorator. If *maxsize* is set to None, the LRU features are disabled and the cache @@ -82,16 +91,16 @@ def lru_cache(maxsize=100, typed=False): def decorating_function(user_function): cache = dict() - stats = [0, 0] # make statistics updateable non-locally - HITS, MISSES = 0, 1 # names for the stats fields + stats = [0, 0] # make statistics updateable non-locally + HITS, MISSES = 0, 1 # names for the stats fields make_key = _make_key - cache_get = cache.get # bound method to lookup key or return None - _len = len # localize the global len() function - lock = RLock() # because linkedlist updates aren't threadsafe - root = [] # root of the circular doubly linked list - root[:] = [root, root, None, None] # initialize by pointing to self - nonlocal_root = [root] # make updateable non-locally - PREV, NEXT, KEY, RESULT = 0, 1, 2, 3 # names for the link fields + cache_get = cache.get # bound method to lookup key or return None + _len = len # localize the global len() function + lock = RLock() # because linkedlist updates aren't threadsafe + root = [] # root of the circular doubly linked list + root[:] = [root, root, None, None] # initialize by pointing to self + nonlocal_root = [root] # make updateable non-locally + PREV, NEXT, KEY, RESULT = 0, 1, 2, 3 # names for the link fields if maxsize == 0: @@ -106,7 +115,9 @@ def lru_cache(maxsize=100, typed=False): def wrapper(*args, **kwds): # simple caching without ordering or size limit key = make_key(args, kwds, typed) - result = cache_get(key, root) # root used here as a unique not-found sentinel + result = cache_get( + key, root + ) # root used here as a unique not-found sentinel if result is not root: stats[HITS] += 1 return result @@ -123,8 +134,9 @@ def lru_cache(maxsize=100, typed=False): with lock: link = cache_get(key) if link is not None: - # record recent use of the key by moving it to the front of the list - root, = nonlocal_root + # record recent use of the key by moving it + # to the front of the list + (root,) = nonlocal_root link_prev, link_next, key, result = link link_prev[NEXT] = link_next link_next[PREV] = link_prev @@ -136,7 +148,7 @@ def lru_cache(maxsize=100, typed=False): return result result = user_function(*args, **kwds) with lock: - root, = nonlocal_root + (root,) = nonlocal_root if key in cache: # getting here means that this same key was added to the # cache while the lock was released. since the link diff --git a/libs/backports/zoneinfo/__init__.py b/libs/backports/zoneinfo/__init__.py new file mode 100644 index 000000000..861fc48e5 --- /dev/null +++ b/libs/backports/zoneinfo/__init__.py @@ -0,0 +1,49 @@ +__all__ = [ + "ZoneInfo", + "reset_tzpath", + "available_timezones", + "TZPATH", + "ZoneInfoNotFoundError", + "InvalidTZPathWarning", +] +import sys + +from . import _tzpath +from ._common import ZoneInfoNotFoundError +from ._version import __version__ + +try: + from ._czoneinfo import ZoneInfo +except ImportError: # pragma: nocover + from ._zoneinfo import ZoneInfo + +reset_tzpath = _tzpath.reset_tzpath +available_timezones = _tzpath.available_timezones +InvalidTZPathWarning = _tzpath.InvalidTZPathWarning + +if sys.version_info < (3, 7): + # Module-level __getattr__ was added in Python 3.7, so instead of lazily + # populating TZPATH on every access, we will register a callback with + # reset_tzpath to update the top-level tuple. + TZPATH = _tzpath.TZPATH + + def _tzpath_callback(new_tzpath): + global TZPATH + TZPATH = new_tzpath + + _tzpath.TZPATH_CALLBACKS.append(_tzpath_callback) + del _tzpath_callback + +else: + + def __getattr__(name): + if name == "TZPATH": + return _tzpath.TZPATH + else: + raise AttributeError( + f"module {__name__!r} has no attribute {name!r}" + ) + + +def __dir__(): + return sorted(list(globals()) + ["TZPATH"]) diff --git a/libs/backports/zoneinfo/__init__.pyi b/libs/backports/zoneinfo/__init__.pyi new file mode 100644 index 000000000..6e56abf2c --- /dev/null +++ b/libs/backports/zoneinfo/__init__.pyi @@ -0,0 +1,45 @@ +import os +import typing +from datetime import datetime, tzinfo +from typing import ( + Any, + Iterable, + Optional, + Protocol, + Sequence, + Set, + Type, + Union, +) + +_T = typing.TypeVar("_T", bound="ZoneInfo") + +class _IOBytes(Protocol): + def read(self, __size: int) -> bytes: ... + def seek(self, __size: int, __whence: int = ...) -> Any: ... + +class ZoneInfo(tzinfo): + @property + def key(self) -> str: ... + def __init__(self, key: str) -> None: ... + @classmethod + def no_cache(cls: Type[_T], key: str) -> _T: ... + @classmethod + def from_file( + cls: Type[_T], __fobj: _IOBytes, key: Optional[str] = ... + ) -> _T: ... + @classmethod + def clear_cache(cls, *, only_keys: Iterable[str] = ...) -> None: ... + +# Note: Both here and in clear_cache, the types allow the use of `str` where +# a sequence of strings is required. This should be remedied if a solution +# to this typing bug is found: https://github.com/python/typing/issues/256 +def reset_tzpath( + to: Optional[Sequence[Union[os.PathLike, str]]] = ... +) -> None: ... +def available_timezones() -> Set[str]: ... + +TZPATH: Sequence[str] + +class ZoneInfoNotFoundError(KeyError): ... +class InvalidTZPathWarning(RuntimeWarning): ... diff --git a/libs/backports/zoneinfo/_common.py b/libs/backports/zoneinfo/_common.py new file mode 100644 index 000000000..27a6ab026 --- /dev/null +++ b/libs/backports/zoneinfo/_common.py @@ -0,0 +1,171 @@ +import struct + + +def load_tzdata(key): + try: + import importlib.resources as importlib_resources + except ImportError: + import importlib_resources + + components = key.split("/") + package_name = ".".join(["tzdata.zoneinfo"] + components[:-1]) + resource_name = components[-1] + + try: + return importlib_resources.open_binary(package_name, resource_name) + except (ImportError, FileNotFoundError, UnicodeEncodeError): + # There are three types of exception that can be raised that all amount + # to "we cannot find this key": + # + # ImportError: If package_name doesn't exist (e.g. if tzdata is not + # installed, or if there's an error in the folder name like + # Amrica/New_York) + # FileNotFoundError: If resource_name doesn't exist in the package + # (e.g. Europe/Krasnoy) + # UnicodeEncodeError: If package_name or resource_name are not UTF-8, + # such as keys containing a surrogate character. + raise ZoneInfoNotFoundError(f"No time zone found with key {key}") + + +def load_data(fobj): + header = _TZifHeader.from_file(fobj) + + if header.version == 1: + time_size = 4 + time_type = "l" + else: + # Version 2+ has 64-bit integer transition times + time_size = 8 + time_type = "q" + + # Version 2+ also starts with a Version 1 header and data, which + # we need to skip now + skip_bytes = ( + header.timecnt * 5 # Transition times and types + + header.typecnt * 6 # Local time type records + + header.charcnt # Time zone designations + + header.leapcnt * 8 # Leap second records + + header.isstdcnt # Standard/wall indicators + + header.isutcnt # UT/local indicators + ) + + fobj.seek(skip_bytes, 1) + + # Now we need to read the second header, which is not the same + # as the first + header = _TZifHeader.from_file(fobj) + + typecnt = header.typecnt + timecnt = header.timecnt + charcnt = header.charcnt + + # The data portion starts with timecnt transitions and indices + if timecnt: + trans_list_utc = struct.unpack( + f">{timecnt}{time_type}", fobj.read(timecnt * time_size) + ) + trans_idx = struct.unpack(f">{timecnt}B", fobj.read(timecnt)) + else: + trans_list_utc = () + trans_idx = () + + # Read the ttinfo struct, (utoff, isdst, abbrind) + if typecnt: + utcoff, isdst, abbrind = zip( + *(struct.unpack(">lbb", fobj.read(6)) for i in range(typecnt)) + ) + else: + utcoff = () + isdst = () + abbrind = () + + # Now read the abbreviations. They are null-terminated strings, indexed + # not by position in the array but by position in the unsplit + # abbreviation string. I suppose this makes more sense in C, which uses + # null to terminate the strings, but it's inconvenient here... + abbr_vals = {} + abbr_chars = fobj.read(charcnt) + + def get_abbr(idx): + # Gets a string starting at idx and running until the next \x00 + # + # We cannot pre-populate abbr_vals by splitting on \x00 because there + # are some zones that use subsets of longer abbreviations, like so: + # + # LMT\x00AHST\x00HDT\x00 + # + # Where the idx to abbr mapping should be: + # + # {0: "LMT", 4: "AHST", 5: "HST", 9: "HDT"} + if idx not in abbr_vals: + span_end = abbr_chars.find(b"\x00", idx) + abbr_vals[idx] = abbr_chars[idx:span_end].decode() + + return abbr_vals[idx] + + abbr = tuple(get_abbr(idx) for idx in abbrind) + + # The remainder of the file consists of leap seconds (currently unused) and + # the standard/wall and ut/local indicators, which are metadata we don't need. + # In version 2 files, we need to skip the unnecessary data to get at the TZ string: + if header.version >= 2: + # Each leap second record has size (time_size + 4) + skip_bytes = header.isutcnt + header.isstdcnt + header.leapcnt * 12 + fobj.seek(skip_bytes, 1) + + c = fobj.read(1) # Should be \n + assert c == b"\n", c + + tz_bytes = b"" + while True: + c = fobj.read(1) + if c == b"\n": + break + tz_bytes += c + + tz_str = tz_bytes + else: + tz_str = None + + return trans_idx, trans_list_utc, utcoff, isdst, abbr, tz_str + + +class _TZifHeader: + __slots__ = [ + "version", + "isutcnt", + "isstdcnt", + "leapcnt", + "timecnt", + "typecnt", + "charcnt", + ] + + def __init__(self, *args): + assert len(self.__slots__) == len(args) + for attr, val in zip(self.__slots__, args): + setattr(self, attr, val) + + @classmethod + def from_file(cls, stream): + # The header starts with a 4-byte "magic" value + if stream.read(4) != b"TZif": + raise ValueError("Invalid TZif file: magic not found") + + _version = stream.read(1) + if _version == b"\x00": + version = 1 + else: + version = int(_version) + stream.read(15) + + args = (version,) + + # Slots are defined in the order that the bytes are arranged + args = args + struct.unpack(">6l", stream.read(24)) + + return cls(*args) + + +class ZoneInfoNotFoundError(KeyError): + """Exception raised when a ZoneInfo key is not found.""" diff --git a/libs/backports/zoneinfo/_tzpath.py b/libs/backports/zoneinfo/_tzpath.py new file mode 100644 index 000000000..9baaf6bce --- /dev/null +++ b/libs/backports/zoneinfo/_tzpath.py @@ -0,0 +1,207 @@ +import os +import sys + +PY36 = sys.version_info < (3, 7) + + +def reset_tzpath(to=None): + global TZPATH + + tzpaths = to + if tzpaths is not None: + if isinstance(tzpaths, (str, bytes)): + raise TypeError( + f"tzpaths must be a list or tuple, " + + f"not {type(tzpaths)}: {tzpaths!r}" + ) + + if not all(map(os.path.isabs, tzpaths)): + raise ValueError(_get_invalid_paths_message(tzpaths)) + base_tzpath = tzpaths + else: + env_var = os.environ.get("PYTHONTZPATH", None) + if env_var is not None: + base_tzpath = _parse_python_tzpath(env_var) + elif sys.platform != "win32": + base_tzpath = [ + "/usr/share/zoneinfo", + "/usr/lib/zoneinfo", + "/usr/share/lib/zoneinfo", + "/etc/zoneinfo", + ] + + base_tzpath.sort(key=lambda x: not os.path.exists(x)) + else: + base_tzpath = () + + TZPATH = tuple(base_tzpath) + + if TZPATH_CALLBACKS: + for callback in TZPATH_CALLBACKS: + callback(TZPATH) + + +def _parse_python_tzpath(env_var): + if not env_var: + return () + + raw_tzpath = env_var.split(os.pathsep) + new_tzpath = tuple(filter(os.path.isabs, raw_tzpath)) + + # If anything has been filtered out, we will warn about it + if len(new_tzpath) != len(raw_tzpath): + import warnings + + msg = _get_invalid_paths_message(raw_tzpath) + + warnings.warn( + "Invalid paths specified in PYTHONTZPATH environment variable." + + msg, + InvalidTZPathWarning, + ) + + return new_tzpath + + +def _get_invalid_paths_message(tzpaths): + invalid_paths = (path for path in tzpaths if not os.path.isabs(path)) + + prefix = "\n " + indented_str = prefix + prefix.join(invalid_paths) + + return ( + "Paths should be absolute but found the following relative paths:" + + indented_str + ) + + +if sys.version_info < (3, 8): + + def _isfile(path): + # bpo-33721: In Python 3.8 non-UTF8 paths return False rather than + # raising an error. See https://bugs.python.org/issue33721 + try: + return os.path.isfile(path) + except ValueError: + return False + + +else: + _isfile = os.path.isfile + + +def find_tzfile(key): + """Retrieve the path to a TZif file from a key.""" + _validate_tzfile_path(key) + for search_path in TZPATH: + filepath = os.path.join(search_path, key) + if _isfile(filepath): + return filepath + + return None + + +_TEST_PATH = os.path.normpath(os.path.join("_", "_"))[:-1] + + +def _validate_tzfile_path(path, _base=_TEST_PATH): + if os.path.isabs(path): + raise ValueError( + f"ZoneInfo keys may not be absolute paths, got: {path}" + ) + + # We only care about the kinds of path normalizations that would change the + # length of the key - e.g. a/../b -> a/b, or a/b/ -> a/b. On Windows, + # normpath will also change from a/b to a\b, but that would still preserve + # the length. + new_path = os.path.normpath(path) + if len(new_path) != len(path): + raise ValueError( + f"ZoneInfo keys must be normalized relative paths, got: {path}" + ) + + resolved = os.path.normpath(os.path.join(_base, new_path)) + if not resolved.startswith(_base): + raise ValueError( + f"ZoneInfo keys must refer to subdirectories of TZPATH, got: {path}" + ) + + +del _TEST_PATH + + +def available_timezones(): + """Returns a set containing all available time zones. + + .. caution:: + + This may attempt to open a large number of files, since the best way to + determine if a given file on the time zone search path is to open it + and check for the "magic string" at the beginning. + """ + try: + from importlib import resources + except ImportError: + import importlib_resources as resources + + valid_zones = set() + + # Start with loading from the tzdata package if it exists: this has a + # pre-assembled list of zones that only requires opening one file. + try: + with resources.open_text("tzdata", "zones") as f: + for zone in f: + zone = zone.strip() + if zone: + valid_zones.add(zone) + except (ImportError, FileNotFoundError): + pass + + def valid_key(fpath): + try: + with open(fpath, "rb") as f: + return f.read(4) == b"TZif" + except Exception: # pragma: nocover + return False + + for tz_root in TZPATH: + if not os.path.exists(tz_root): + continue + + for root, dirnames, files in os.walk(tz_root): + if root == tz_root: + # right/ and posix/ are special directories and shouldn't be + # included in the output of available zones + if "right" in dirnames: + dirnames.remove("right") + if "posix" in dirnames: + dirnames.remove("posix") + + for file in files: + fpath = os.path.join(root, file) + + key = os.path.relpath(fpath, start=tz_root) + if os.sep != "/": # pragma: nocover + key = key.replace(os.sep, "/") + + if not key or key in valid_zones: + continue + + if valid_key(fpath): + valid_zones.add(key) + + if "posixrules" in valid_zones: + # posixrules is a special symlink-only time zone where it exists, it + # should not be included in the output + valid_zones.remove("posixrules") + + return valid_zones + + +class InvalidTZPathWarning(RuntimeWarning): + """Warning raised if an invalid path is specified in PYTHONTZPATH.""" + + +TZPATH = () +TZPATH_CALLBACKS = [] +reset_tzpath() diff --git a/libs/backports/zoneinfo/_version.py b/libs/backports/zoneinfo/_version.py new file mode 100644 index 000000000..3ced3581b --- /dev/null +++ b/libs/backports/zoneinfo/_version.py @@ -0,0 +1 @@ +__version__ = "0.2.1" diff --git a/libs/backports/zoneinfo/_zoneinfo.py b/libs/backports/zoneinfo/_zoneinfo.py new file mode 100644 index 000000000..c15a55349 --- /dev/null +++ b/libs/backports/zoneinfo/_zoneinfo.py @@ -0,0 +1,754 @@ +import bisect +import calendar +import collections +import functools +import re +import weakref +from datetime import datetime, timedelta, tzinfo + +from . import _common, _tzpath + +EPOCH = datetime(1970, 1, 1) +EPOCHORDINAL = datetime(1970, 1, 1).toordinal() + +# It is relatively expensive to construct new timedelta objects, and in most +# cases we're looking at the same deltas, like integer numbers of hours, etc. +# To improve speed and memory use, we'll keep a dictionary with references +# to the ones we've already used so far. +# +# Loading every time zone in the 2020a version of the time zone database +# requires 447 timedeltas, which requires approximately the amount of space +# that ZoneInfo("America/New_York") with 236 transitions takes up, so we will +# set the cache size to 512 so that in the common case we always get cache +# hits, but specifically crafted ZoneInfo objects don't leak arbitrary amounts +# of memory. +@functools.lru_cache(maxsize=512) +def _load_timedelta(seconds): + return timedelta(seconds=seconds) + + +class ZoneInfo(tzinfo): + _strong_cache_size = 8 + _strong_cache = collections.OrderedDict() + _weak_cache = weakref.WeakValueDictionary() + __module__ = "backports.zoneinfo" + + def __init_subclass__(cls): + cls._strong_cache = collections.OrderedDict() + cls._weak_cache = weakref.WeakValueDictionary() + + def __new__(cls, key): + instance = cls._weak_cache.get(key, None) + if instance is None: + instance = cls._weak_cache.setdefault(key, cls._new_instance(key)) + instance._from_cache = True + + # Update the "strong" cache + cls._strong_cache[key] = cls._strong_cache.pop(key, instance) + + if len(cls._strong_cache) > cls._strong_cache_size: + cls._strong_cache.popitem(last=False) + + return instance + + @classmethod + def no_cache(cls, key): + obj = cls._new_instance(key) + obj._from_cache = False + + return obj + + @classmethod + def _new_instance(cls, key): + obj = super().__new__(cls) + obj._key = key + obj._file_path = obj._find_tzfile(key) + + if obj._file_path is not None: + file_obj = open(obj._file_path, "rb") + else: + file_obj = _common.load_tzdata(key) + + with file_obj as f: + obj._load_file(f) + + return obj + + @classmethod + def from_file(cls, fobj, key=None): + obj = super().__new__(cls) + obj._key = key + obj._file_path = None + obj._load_file(fobj) + obj._file_repr = repr(fobj) + + # Disable pickling for objects created from files + obj.__reduce__ = obj._file_reduce + + return obj + + @classmethod + def clear_cache(cls, *, only_keys=None): + if only_keys is not None: + for key in only_keys: + cls._weak_cache.pop(key, None) + cls._strong_cache.pop(key, None) + + else: + cls._weak_cache.clear() + cls._strong_cache.clear() + + @property + def key(self): + return self._key + + def utcoffset(self, dt): + return self._find_trans(dt).utcoff + + def dst(self, dt): + return self._find_trans(dt).dstoff + + def tzname(self, dt): + return self._find_trans(dt).tzname + + def fromutc(self, dt): + """Convert from datetime in UTC to datetime in local time""" + + if not isinstance(dt, datetime): + raise TypeError("fromutc() requires a datetime argument") + if dt.tzinfo is not self: + raise ValueError("dt.tzinfo is not self") + + timestamp = self._get_local_timestamp(dt) + num_trans = len(self._trans_utc) + + if num_trans >= 1 and timestamp < self._trans_utc[0]: + tti = self._tti_before + fold = 0 + elif ( + num_trans == 0 or timestamp > self._trans_utc[-1] + ) and not isinstance(self._tz_after, _ttinfo): + tti, fold = self._tz_after.get_trans_info_fromutc( + timestamp, dt.year + ) + elif num_trans == 0: + tti = self._tz_after + fold = 0 + else: + idx = bisect.bisect_right(self._trans_utc, timestamp) + + if num_trans > 1 and timestamp >= self._trans_utc[1]: + tti_prev, tti = self._ttinfos[idx - 2 : idx] + elif timestamp > self._trans_utc[-1]: + tti_prev = self._ttinfos[-1] + tti = self._tz_after + else: + tti_prev = self._tti_before + tti = self._ttinfos[0] + + # Detect fold + shift = tti_prev.utcoff - tti.utcoff + fold = shift.total_seconds() > timestamp - self._trans_utc[idx - 1] + dt += tti.utcoff + if fold: + return dt.replace(fold=1) + else: + return dt + + def _find_trans(self, dt): + if dt is None: + if self._fixed_offset: + return self._tz_after + else: + return _NO_TTINFO + + ts = self._get_local_timestamp(dt) + + lt = self._trans_local[dt.fold] + + num_trans = len(lt) + + if num_trans and ts < lt[0]: + return self._tti_before + elif not num_trans or ts > lt[-1]: + if isinstance(self._tz_after, _TZStr): + return self._tz_after.get_trans_info(ts, dt.year, dt.fold) + else: + return self._tz_after + else: + # idx is the transition that occurs after this timestamp, so we + # subtract off 1 to get the current ttinfo + idx = bisect.bisect_right(lt, ts) - 1 + assert idx >= 0 + return self._ttinfos[idx] + + def _get_local_timestamp(self, dt): + return ( + (dt.toordinal() - EPOCHORDINAL) * 86400 + + dt.hour * 3600 + + dt.minute * 60 + + dt.second + ) + + def __str__(self): + if self._key is not None: + return f"{self._key}" + else: + return repr(self) + + def __repr__(self): + if self._key is not None: + return f"{self.__class__.__name__}(key={self._key!r})" + else: + return f"{self.__class__.__name__}.from_file({self._file_repr})" + + def __reduce__(self): + return (self.__class__._unpickle, (self._key, self._from_cache)) + + def _file_reduce(self): + import pickle + + raise pickle.PicklingError( + "Cannot pickle a ZoneInfo file created from a file stream." + ) + + @classmethod + def _unpickle(cls, key, from_cache): + if from_cache: + return cls(key) + else: + return cls.no_cache(key) + + def _find_tzfile(self, key): + return _tzpath.find_tzfile(key) + + def _load_file(self, fobj): + # Retrieve all the data as it exists in the zoneinfo file + trans_idx, trans_utc, utcoff, isdst, abbr, tz_str = _common.load_data( + fobj + ) + + # Infer the DST offsets (needed for .dst()) from the data + dstoff = self._utcoff_to_dstoff(trans_idx, utcoff, isdst) + + # Convert all the transition times (UTC) into "seconds since 1970-01-01 local time" + trans_local = self._ts_to_local(trans_idx, trans_utc, utcoff) + + # Construct `_ttinfo` objects for each transition in the file + _ttinfo_list = [ + _ttinfo( + _load_timedelta(utcoffset), _load_timedelta(dstoffset), tzname + ) + for utcoffset, dstoffset, tzname in zip(utcoff, dstoff, abbr) + ] + + self._trans_utc = trans_utc + self._trans_local = trans_local + self._ttinfos = [_ttinfo_list[idx] for idx in trans_idx] + + # Find the first non-DST transition + for i in range(len(isdst)): + if not isdst[i]: + self._tti_before = _ttinfo_list[i] + break + else: + if self._ttinfos: + self._tti_before = self._ttinfos[0] + else: + self._tti_before = None + + # Set the "fallback" time zone + if tz_str is not None and tz_str != b"": + self._tz_after = _parse_tz_str(tz_str.decode()) + else: + if not self._ttinfos and not _ttinfo_list: + raise ValueError("No time zone information found.") + + if self._ttinfos: + self._tz_after = self._ttinfos[-1] + else: + self._tz_after = _ttinfo_list[-1] + + # Determine if this is a "fixed offset" zone, meaning that the output + # of the utcoffset, dst and tzname functions does not depend on the + # specific datetime passed. + # + # We make three simplifying assumptions here: + # + # 1. If _tz_after is not a _ttinfo, it has transitions that might + # actually occur (it is possible to construct TZ strings that + # specify STD and DST but no transitions ever occur, such as + # AAA0BBB,0/0,J365/25). + # 2. If _ttinfo_list contains more than one _ttinfo object, the objects + # represent different offsets. + # 3. _ttinfo_list contains no unused _ttinfos (in which case an + # otherwise fixed-offset zone with extra _ttinfos defined may + # appear to *not* be a fixed offset zone). + # + # Violations to these assumptions would be fairly exotic, and exotic + # zones should almost certainly not be used with datetime.time (the + # only thing that would be affected by this). + if len(_ttinfo_list) > 1 or not isinstance(self._tz_after, _ttinfo): + self._fixed_offset = False + elif not _ttinfo_list: + self._fixed_offset = True + else: + self._fixed_offset = _ttinfo_list[0] == self._tz_after + + @staticmethod + def _utcoff_to_dstoff(trans_idx, utcoffsets, isdsts): + # Now we must transform our ttis and abbrs into `_ttinfo` objects, + # but there is an issue: .dst() must return a timedelta with the + # difference between utcoffset() and the "standard" offset, but + # the "base offset" and "DST offset" are not encoded in the file; + # we can infer what they are from the isdst flag, but it is not + # sufficient to to just look at the last standard offset, because + # occasionally countries will shift both DST offset and base offset. + + typecnt = len(isdsts) + dstoffs = [0] * typecnt # Provisionally assign all to 0. + dst_cnt = sum(isdsts) + dst_found = 0 + + for i in range(1, len(trans_idx)): + if dst_cnt == dst_found: + break + + idx = trans_idx[i] + + dst = isdsts[idx] + + # We're only going to look at daylight saving time + if not dst: + continue + + # Skip any offsets that have already been assigned + if dstoffs[idx] != 0: + continue + + dstoff = 0 + utcoff = utcoffsets[idx] + + comp_idx = trans_idx[i - 1] + + if not isdsts[comp_idx]: + dstoff = utcoff - utcoffsets[comp_idx] + + if not dstoff and idx < (typecnt - 1): + comp_idx = trans_idx[i + 1] + + # If the following transition is also DST and we couldn't + # find the DST offset by this point, we're going ot have to + # skip it and hope this transition gets assigned later + if isdsts[comp_idx]: + continue + + dstoff = utcoff - utcoffsets[comp_idx] + + if dstoff: + dst_found += 1 + dstoffs[idx] = dstoff + else: + # If we didn't find a valid value for a given index, we'll end up + # with dstoff = 0 for something where `isdst=1`. This is obviously + # wrong - one hour will be a much better guess than 0 + for idx in range(typecnt): + if not dstoffs[idx] and isdsts[idx]: + dstoffs[idx] = 3600 + + return dstoffs + + @staticmethod + def _ts_to_local(trans_idx, trans_list_utc, utcoffsets): + """Generate number of seconds since 1970 *in the local time*. + + This is necessary to easily find the transition times in local time""" + if not trans_list_utc: + return [[], []] + + # Start with the timestamps and modify in-place + trans_list_wall = [list(trans_list_utc), list(trans_list_utc)] + + if len(utcoffsets) > 1: + offset_0 = utcoffsets[0] + offset_1 = utcoffsets[trans_idx[0]] + if offset_1 > offset_0: + offset_1, offset_0 = offset_0, offset_1 + else: + offset_0 = offset_1 = utcoffsets[0] + + trans_list_wall[0][0] += offset_0 + trans_list_wall[1][0] += offset_1 + + for i in range(1, len(trans_idx)): + offset_0 = utcoffsets[trans_idx[i - 1]] + offset_1 = utcoffsets[trans_idx[i]] + + if offset_1 > offset_0: + offset_1, offset_0 = offset_0, offset_1 + + trans_list_wall[0][i] += offset_0 + trans_list_wall[1][i] += offset_1 + + return trans_list_wall + + +class _ttinfo: + __slots__ = ["utcoff", "dstoff", "tzname"] + + def __init__(self, utcoff, dstoff, tzname): + self.utcoff = utcoff + self.dstoff = dstoff + self.tzname = tzname + + def __eq__(self, other): + return ( + self.utcoff == other.utcoff + and self.dstoff == other.dstoff + and self.tzname == other.tzname + ) + + def __repr__(self): # pragma: nocover + return ( + f"{self.__class__.__name__}" + + f"({self.utcoff}, {self.dstoff}, {self.tzname})" + ) + + +_NO_TTINFO = _ttinfo(None, None, None) + + +class _TZStr: + __slots__ = ( + "std", + "dst", + "start", + "end", + "get_trans_info", + "get_trans_info_fromutc", + "dst_diff", + ) + + def __init__( + self, std_abbr, std_offset, dst_abbr, dst_offset, start=None, end=None + ): + self.dst_diff = dst_offset - std_offset + std_offset = _load_timedelta(std_offset) + self.std = _ttinfo( + utcoff=std_offset, dstoff=_load_timedelta(0), tzname=std_abbr + ) + + self.start = start + self.end = end + + dst_offset = _load_timedelta(dst_offset) + delta = _load_timedelta(self.dst_diff) + self.dst = _ttinfo(utcoff=dst_offset, dstoff=delta, tzname=dst_abbr) + + # These are assertions because the constructor should only be called + # by functions that would fail before passing start or end + assert start is not None, "No transition start specified" + assert end is not None, "No transition end specified" + + self.get_trans_info = self._get_trans_info + self.get_trans_info_fromutc = self._get_trans_info_fromutc + + def transitions(self, year): + start = self.start.year_to_epoch(year) + end = self.end.year_to_epoch(year) + return start, end + + def _get_trans_info(self, ts, year, fold): + """Get the information about the current transition - tti""" + start, end = self.transitions(year) + + # With fold = 0, the period (denominated in local time) with the + # smaller offset starts at the end of the gap and ends at the end of + # the fold; with fold = 1, it runs from the start of the gap to the + # beginning of the fold. + # + # So in order to determine the DST boundaries we need to know both + # the fold and whether DST is positive or negative (rare), and it + # turns out that this boils down to fold XOR is_positive. + if fold == (self.dst_diff >= 0): + end -= self.dst_diff + else: + start += self.dst_diff + + if start < end: + isdst = start <= ts < end + else: + isdst = not (end <= ts < start) + + return self.dst if isdst else self.std + + def _get_trans_info_fromutc(self, ts, year): + start, end = self.transitions(year) + start -= self.std.utcoff.total_seconds() + end -= self.dst.utcoff.total_seconds() + + if start < end: + isdst = start <= ts < end + else: + isdst = not (end <= ts < start) + + # For positive DST, the ambiguous period is one dst_diff after the end + # of DST; for negative DST, the ambiguous period is one dst_diff before + # the start of DST. + if self.dst_diff > 0: + ambig_start = end + ambig_end = end + self.dst_diff + else: + ambig_start = start + ambig_end = start - self.dst_diff + + fold = ambig_start <= ts < ambig_end + + return (self.dst if isdst else self.std, fold) + + +def _post_epoch_days_before_year(year): + """Get the number of days between 1970-01-01 and YEAR-01-01""" + y = year - 1 + return y * 365 + y // 4 - y // 100 + y // 400 - EPOCHORDINAL + + +class _DayOffset: + __slots__ = ["d", "julian", "hour", "minute", "second"] + + def __init__(self, d, julian, hour=2, minute=0, second=0): + if not (0 + julian) <= d <= 365: + min_day = 0 + julian + raise ValueError(f"d must be in [{min_day}, 365], not: {d}") + + self.d = d + self.julian = julian + self.hour = hour + self.minute = minute + self.second = second + + def year_to_epoch(self, year): + days_before_year = _post_epoch_days_before_year(year) + + d = self.d + if self.julian and d >= 59 and calendar.isleap(year): + d += 1 + + epoch = (days_before_year + d) * 86400 + epoch += self.hour * 3600 + self.minute * 60 + self.second + + return epoch + + +class _CalendarOffset: + __slots__ = ["m", "w", "d", "hour", "minute", "second"] + + _DAYS_BEFORE_MONTH = ( + -1, + 0, + 31, + 59, + 90, + 120, + 151, + 181, + 212, + 243, + 273, + 304, + 334, + ) + + def __init__(self, m, w, d, hour=2, minute=0, second=0): + if not 0 < m <= 12: + raise ValueError("m must be in (0, 12]") + + if not 0 < w <= 5: + raise ValueError("w must be in (0, 5]") + + if not 0 <= d <= 6: + raise ValueError("d must be in [0, 6]") + + self.m = m + self.w = w + self.d = d + self.hour = hour + self.minute = minute + self.second = second + + @classmethod + def _ymd2ord(cls, year, month, day): + return ( + _post_epoch_days_before_year(year) + + cls._DAYS_BEFORE_MONTH[month] + + (month > 2 and calendar.isleap(year)) + + day + ) + + # TODO: These are not actually epoch dates as they are expressed in local time + def year_to_epoch(self, year): + """Calculates the datetime of the occurrence from the year""" + # We know year and month, we need to convert w, d into day of month + # + # Week 1 is the first week in which day `d` (where 0 = Sunday) appears. + # Week 5 represents the last occurrence of day `d`, so we need to know + # the range of the month. + first_day, days_in_month = calendar.monthrange(year, self.m) + + # This equation seems magical, so I'll break it down: + # 1. calendar says 0 = Monday, POSIX says 0 = Sunday + # so we need first_day + 1 to get 1 = Monday -> 7 = Sunday, + # which is still equivalent because this math is mod 7 + # 2. Get first day - desired day mod 7: -1 % 7 = 6, so we don't need + # to do anything to adjust negative numbers. + # 3. Add 1 because month days are a 1-based index. + month_day = (self.d - (first_day + 1)) % 7 + 1 + + # Now use a 0-based index version of `w` to calculate the w-th + # occurrence of `d` + month_day += (self.w - 1) * 7 + + # month_day will only be > days_in_month if w was 5, and `w` means + # "last occurrence of `d`", so now we just check if we over-shot the + # end of the month and if so knock off 1 week. + if month_day > days_in_month: + month_day -= 7 + + ordinal = self._ymd2ord(year, self.m, month_day) + epoch = ordinal * 86400 + epoch += self.hour * 3600 + self.minute * 60 + self.second + return epoch + + +def _parse_tz_str(tz_str): + # The tz string has the format: + # + # std[offset[dst[offset],start[/time],end[/time]]] + # + # std and dst must be 3 or more characters long and must not contain + # a leading colon, embedded digits, commas, nor a plus or minus signs; + # The spaces between "std" and "offset" are only for display and are + # not actually present in the string. + # + # The format of the offset is ``[+|-]hh[:mm[:ss]]`` + + offset_str, *start_end_str = tz_str.split(",", 1) + + # fmt: off + parser_re = re.compile( + r"(?P[^<0-9:.+-]+|<[a-zA-Z0-9+\-]+>)" + + r"((?P[+-]?\d{1,2}(:\d{2}(:\d{2})?)?)" + + r"((?P[^0-9:.+-]+|<[a-zA-Z0-9+\-]+>)" + + r"((?P[+-]?\d{1,2}(:\d{2}(:\d{2})?)?))?" + + r")?" + # dst + r")?$" # stdoff + ) + # fmt: on + + m = parser_re.match(offset_str) + + if m is None: + raise ValueError(f"{tz_str} is not a valid TZ string") + + std_abbr = m.group("std") + dst_abbr = m.group("dst") + dst_offset = None + + std_abbr = std_abbr.strip("<>") + + if dst_abbr: + dst_abbr = dst_abbr.strip("<>") + + std_offset = m.group("stdoff") + if std_offset: + try: + std_offset = _parse_tz_delta(std_offset) + except ValueError as e: + raise ValueError(f"Invalid STD offset in {tz_str}") from e + else: + std_offset = 0 + + if dst_abbr is not None: + dst_offset = m.group("dstoff") + if dst_offset: + try: + dst_offset = _parse_tz_delta(dst_offset) + except ValueError as e: + raise ValueError(f"Invalid DST offset in {tz_str}") from e + else: + dst_offset = std_offset + 3600 + + if not start_end_str: + raise ValueError(f"Missing transition rules: {tz_str}") + + start_end_strs = start_end_str[0].split(",", 1) + try: + start, end = (_parse_dst_start_end(x) for x in start_end_strs) + except ValueError as e: + raise ValueError(f"Invalid TZ string: {tz_str}") from e + + return _TZStr(std_abbr, std_offset, dst_abbr, dst_offset, start, end) + elif start_end_str: + raise ValueError(f"Transition rule present without DST: {tz_str}") + else: + # This is a static ttinfo, don't return _TZStr + return _ttinfo( + _load_timedelta(std_offset), _load_timedelta(0), std_abbr + ) + + +def _parse_dst_start_end(dststr): + date, *time = dststr.split("/") + if date[0] == "M": + n_is_julian = False + m = re.match(r"M(\d{1,2})\.(\d).(\d)$", date) + if m is None: + raise ValueError(f"Invalid dst start/end date: {dststr}") + date_offset = tuple(map(int, m.groups())) + offset = _CalendarOffset(*date_offset) + else: + if date[0] == "J": + n_is_julian = True + date = date[1:] + else: + n_is_julian = False + + doy = int(date) + offset = _DayOffset(doy, n_is_julian) + + if time: + time_components = list(map(int, time[0].split(":"))) + n_components = len(time_components) + if n_components < 3: + time_components.extend([0] * (3 - n_components)) + offset.hour, offset.minute, offset.second = time_components + + return offset + + +def _parse_tz_delta(tz_delta): + match = re.match( + r"(?P[+-])?(?P\d{1,2})(:(?P\d{2})(:(?P\d{2}))?)?", + tz_delta, + ) + # Anything passed to this function should already have hit an equivalent + # regular expression to find the section to parse. + assert match is not None, tz_delta + + h, m, s = ( + int(v) if v is not None else 0 + for v in map(match.group, ("h", "m", "s")) + ) + + total = h * 3600 + m * 60 + s + + if not -86400 < total < 86400: + raise ValueError( + "Offset must be strictly between -24h and +24h:" + tz_delta + ) + + # Yes, +5 maps to an offset of -5h + if match.group("sign") != "-": + total *= -1 + + return total diff --git a/libs/twine/py.typed b/libs/backports/zoneinfo/py.typed similarity index 100% rename from libs/twine/py.typed rename to libs/backports/zoneinfo/py.typed diff --git a/libs/beaker/__init__.py b/libs/beaker/__init__.py deleted file mode 100644 index 52af183e5..000000000 --- a/libs/beaker/__init__.py +++ /dev/null @@ -1 +0,0 @@ -__version__ = '1.10.0' diff --git a/libs/beaker/_compat.py b/libs/beaker/_compat.py deleted file mode 100644 index d7bac1746..000000000 --- a/libs/beaker/_compat.py +++ /dev/null @@ -1,169 +0,0 @@ -from __future__ import absolute_import -import sys - -# True if we are running on Python 2. -PY2 = sys.version_info[0] == 2 -PYVER = sys.version_info[:2] -JYTHON = sys.platform.startswith('java') - -if PY2 and not JYTHON: # pragma: no cover - import cPickle as pickle -else: # pragma: no cover - import pickle - - -if not PY2: # pragma: no cover - xrange_ = range - NoneType = type(None) - - string_type = str - unicode_text = str - byte_string = bytes - - from urllib.parse import urlencode as url_encode - from urllib.parse import quote as url_quote - from urllib.parse import unquote as url_unquote - from urllib.parse import urlparse as url_parse - from urllib.request import url2pathname - import http.cookies as http_cookies - from base64 import b64decode as _b64decode, b64encode as _b64encode - - try: - import dbm as anydbm - except: - import dumbdbm as anydbm - - def b64decode(b): - return _b64decode(b.encode('ascii')) - - def b64encode(s): - return _b64encode(s).decode('ascii') - - def u_(s): - return str(s) - - def bytes_(s): - if isinstance(s, byte_string): - return s - return str(s).encode('ascii', 'strict') - - def dictkeyslist(d): - return list(d.keys()) - -else: - xrange_ = xrange - from types import NoneType - - string_type = basestring - unicode_text = unicode - byte_string = str - - from urllib import urlencode as url_encode - from urllib import quote as url_quote - from urllib import unquote as url_unquote - from urlparse import urlparse as url_parse - from urllib import url2pathname - import Cookie as http_cookies - from base64 import b64decode, b64encode - import anydbm - - def u_(s): - if isinstance(s, unicode_text): - return s - - if not isinstance(s, byte_string): - s = str(s) - return unicode(s, 'utf-8') - - def bytes_(s): - if isinstance(s, byte_string): - return s - return str(s) - - def dictkeyslist(d): - return d.keys() - - -def im_func(f): - if not PY2: # pragma: no cover - return getattr(f, '__func__', None) - else: - return getattr(f, 'im_func', None) - - -def default_im_func(f): - if not PY2: # pragma: no cover - return getattr(f, '__func__', f) - else: - return getattr(f, 'im_func', f) - - -def im_self(f): - if not PY2: # pragma: no cover - return getattr(f, '__self__', None) - else: - return getattr(f, 'im_self', None) - - -def im_class(f): - if not PY2: # pragma: no cover - self = im_self(f) - if self is not None: - return self.__class__ - else: - return None - else: - return getattr(f, 'im_class', None) - - -def add_metaclass(metaclass): - """Class decorator for creating a class with a metaclass.""" - def wrapper(cls): - orig_vars = cls.__dict__.copy() - slots = orig_vars.get('__slots__') - if slots is not None: - if isinstance(slots, str): - slots = [slots] - for slots_var in slots: - orig_vars.pop(slots_var) - orig_vars.pop('__dict__', None) - orig_vars.pop('__weakref__', None) - return metaclass(cls.__name__, cls.__bases__, orig_vars) - return wrapper - - -if not PY2: # pragma: no cover - import builtins - exec_ = getattr(builtins, "exec") - - def reraise(tp, value, tb=None): - if value.__traceback__ is not tb: - raise value.with_traceback(tb) - raise value -else: # pragma: no cover - def exec_(code, globs=None, locs=None): - """Execute code in a namespace.""" - if globs is None: - frame = sys._getframe(1) - globs = frame.f_globals - if locs is None: - locs = frame.f_locals - del frame - elif locs is None: - locs = globs - exec("""exec code in globs, locs""") - - exec_("""def reraise(tp, value, tb=None): - raise tp, value, tb -""") - - -try: - from inspect import signature as func_signature -except ImportError: - from funcsigs import signature as func_signature - - -def bindfuncargs(arginfo, args, kwargs): - boundargs = arginfo.bind(*args, **kwargs) - return boundargs.args, boundargs.kwargs diff --git a/libs/beaker/cache.py b/libs/beaker/cache.py deleted file mode 100644 index 5fe85a23f..000000000 --- a/libs/beaker/cache.py +++ /dev/null @@ -1,615 +0,0 @@ -"""This package contains the "front end" classes and functions -for Beaker caching. - -Included are the :class:`.Cache` and :class:`.CacheManager` classes, -as well as the function decorators :func:`.region_decorate`, -:func:`.region_invalidate`. - -""" -import warnings -from itertools import chain - -from beaker._compat import u_, unicode_text, func_signature, bindfuncargs -import beaker.container as container -import beaker.util as util -from beaker.crypto.util import sha1 -from beaker.exceptions import BeakerException, InvalidCacheBackendError -from beaker.synchronization import _threading - -import beaker.ext.memcached as memcached -import beaker.ext.database as database -import beaker.ext.sqla as sqla -import beaker.ext.google as google -import beaker.ext.mongodb as mongodb -import beaker.ext.redisnm as redisnm -from functools import wraps - -# Initialize the cache region dict -cache_regions = {} -"""Dictionary of 'region' arguments. - -A "region" is a string name that refers to a series of cache -configuration arguments. An application may have multiple -"regions" - one which stores things in a memory cache, one -which writes data to files, etc. - -The dictionary stores string key names mapped to dictionaries -of configuration arguments. Example:: - - from beaker.cache import cache_regions - cache_regions.update({ - 'short_term':{ - 'expire':60, - 'type':'memory' - }, - 'long_term':{ - 'expire':1800, - 'type':'dbm', - 'data_dir':'/tmp', - } - }) -""" - - -cache_managers = {} - - -class _backends(object): - initialized = False - - def __init__(self, clsmap): - self._clsmap = clsmap - self._mutex = _threading.Lock() - - def __getitem__(self, key): - try: - return self._clsmap[key] - except KeyError as e: - if not self.initialized: - self._mutex.acquire() - try: - if not self.initialized: - self._init() - self.initialized = True - - return self._clsmap[key] - finally: - self._mutex.release() - - raise e - - def _init(self): - try: - import pkg_resources - - # Load up the additional entry point defined backends - for entry_point in pkg_resources.iter_entry_points('beaker.backends'): - try: - namespace_manager = entry_point.load() - name = entry_point.name - if name in self._clsmap: - raise BeakerException("NamespaceManager name conflict,'%s' " - "already loaded" % name) - self._clsmap[name] = namespace_manager - except (InvalidCacheBackendError, SyntaxError): - # Ignore invalid backends - pass - except: - import sys - from pkg_resources import DistributionNotFound - # Warn when there's a problem loading a NamespaceManager - if not isinstance(sys.exc_info()[1], DistributionNotFound): - import traceback - try: - from StringIO import StringIO # Python2 - except ImportError: - from io import StringIO # Python3 - - tb = StringIO() - traceback.print_exc(file=tb) - warnings.warn( - "Unable to load NamespaceManager " - "entry point: '%s': %s" % ( - entry_point, - tb.getvalue()), - RuntimeWarning, 2) - except ImportError: - pass - -# Initialize the basic available backends -clsmap = _backends({ - 'memory': container.MemoryNamespaceManager, - 'dbm': container.DBMNamespaceManager, - 'file': container.FileNamespaceManager, - 'ext:memcached': memcached.MemcachedNamespaceManager, - 'ext:database': database.DatabaseNamespaceManager, - 'ext:sqla': sqla.SqlaNamespaceManager, - 'ext:google': google.GoogleNamespaceManager, - 'ext:mongodb': mongodb.MongoNamespaceManager, - 'ext:redis': redisnm.RedisNamespaceManager -}) - - -def cache_region(region, *args): - """Decorate a function such that its return result is cached, - using a "region" to indicate the cache arguments. - - Example:: - - from beaker.cache import cache_regions, cache_region - - # configure regions - cache_regions.update({ - 'short_term':{ - 'expire':60, - 'type':'memory' - } - }) - - @cache_region('short_term', 'load_things') - def load(search_term, limit, offset): - '''Load from a database given a search term, limit, offset.''' - return database.query(search_term)[offset:offset + limit] - - The decorator can also be used with object methods. The ``self`` - argument is not part of the cache key. This is based on the - actual string name ``self`` being in the first argument - position (new in 1.6):: - - class MyThing(object): - @cache_region('short_term', 'load_things') - def load(self, search_term, limit, offset): - '''Load from a database given a search term, limit, offset.''' - return database.query(search_term)[offset:offset + limit] - - Classmethods work as well - use ``cls`` as the name of the class argument, - and place the decorator around the function underneath ``@classmethod`` - (new in 1.6):: - - class MyThing(object): - @classmethod - @cache_region('short_term', 'load_things') - def load(cls, search_term, limit, offset): - '''Load from a database given a search term, limit, offset.''' - return database.query(search_term)[offset:offset + limit] - - :param region: String name of the region corresponding to the desired - caching arguments, established in :attr:`.cache_regions`. - - :param \*args: Optional ``str()``-compatible arguments which will uniquely - identify the key used by this decorated function, in addition - to the positional arguments passed to the function itself at call time. - This is recommended as it is needed to distinguish between any two functions - or methods that have the same name (regardless of parent class or not). - - .. note:: - - The function being decorated must only be called with - positional arguments, and the arguments must support - being stringified with ``str()``. The concatenation - of the ``str()`` version of each argument, combined - with that of the ``*args`` sent to the decorator, - forms the unique cache key. - - .. note:: - - When a method on a class is decorated, the ``self`` or ``cls`` - argument in the first position is - not included in the "key" used for caching. New in 1.6. - - """ - return _cache_decorate(args, None, None, region) - - -def region_invalidate(namespace, region, *args): - """Invalidate a cache region corresponding to a function - decorated with :func:`.cache_region`. - - :param namespace: The namespace of the cache to invalidate. This is typically - a reference to the original function (as returned by the :func:`.cache_region` - decorator), where the :func:`.cache_region` decorator applies a "memo" to - the function in order to locate the string name of the namespace. - - :param region: String name of the region used with the decorator. This can be - ``None`` in the usual case that the decorated function itself is passed, - not the string name of the namespace. - - :param args: Stringifyable arguments that are used to locate the correct - key. This consists of the ``*args`` sent to the :func:`.cache_region` - decorator itself, plus the ``*args`` sent to the function itself - at runtime. - - Example:: - - from beaker.cache import cache_regions, cache_region, region_invalidate - - # configure regions - cache_regions.update({ - 'short_term':{ - 'expire':60, - 'type':'memory' - } - }) - - @cache_region('short_term', 'load_data') - def load(search_term, limit, offset): - '''Load from a database given a search term, limit, offset.''' - return database.query(search_term)[offset:offset + limit] - - def invalidate_search(search_term, limit, offset): - '''Invalidate the cached storage for a given search term, limit, offset.''' - region_invalidate(load, 'short_term', 'load_data', search_term, limit, offset) - - Note that when a method on a class is decorated, the first argument ``cls`` - or ``self`` is not included in the cache key. This means you don't send - it to :func:`.region_invalidate`:: - - class MyThing(object): - @cache_region('short_term', 'some_data') - def load(self, search_term, limit, offset): - '''Load from a database given a search term, limit, offset.''' - return database.query(search_term)[offset:offset + limit] - - def invalidate_search(self, search_term, limit, offset): - '''Invalidate the cached storage for a given search term, limit, offset.''' - region_invalidate(self.load, 'short_term', 'some_data', search_term, limit, offset) - - """ - if callable(namespace): - if not region: - region = namespace._arg_region - namespace = namespace._arg_namespace - - if not region: - raise BeakerException("Region or callable function " - "namespace is required") - else: - region = cache_regions[region] - - cache = Cache._get_cache(namespace, region) - _cache_decorator_invalidate(cache, - region.get('key_length', util.DEFAULT_CACHE_KEY_LENGTH), - args) - - -class Cache(object): - """Front-end to the containment API implementing a data cache. - - :param namespace: the namespace of this Cache - - :param type: type of cache to use - - :param expire: seconds to keep cached data - - :param expiretime: seconds to keep cached data (legacy support) - - :param starttime: time when cache was cache was - - """ - def __init__(self, namespace, type='memory', expiretime=None, - starttime=None, expire=None, **nsargs): - try: - cls = clsmap[type] - if isinstance(cls, InvalidCacheBackendError): - raise cls - except KeyError: - raise TypeError("Unknown cache implementation %r" % type) - - if expire is not None: - expire = int(expire) - - self.namespace_name = namespace - self.namespace = cls(namespace, **nsargs) - self.expiretime = expiretime or expire - self.starttime = starttime - self.nsargs = nsargs - - @classmethod - def _get_cache(cls, namespace, kw): - key = namespace + str(kw) - try: - return cache_managers[key] - except KeyError: - cache_managers[key] = cache = cls(namespace, **kw) - return cache - - def put(self, key, value, **kw): - self._get_value(key, **kw).set_value(value) - set_value = put - - def get(self, key, **kw): - """Retrieve a cached value from the container""" - return self._get_value(key, **kw).get_value() - get_value = get - - def remove_value(self, key, **kw): - mycontainer = self._get_value(key, **kw) - mycontainer.clear_value() - remove = remove_value - - def _get_value(self, key, **kw): - if isinstance(key, unicode_text): - key = key.encode('ascii', 'backslashreplace') - - if 'type' in kw: - return self._legacy_get_value(key, **kw) - - kw.setdefault('expiretime', self.expiretime) - kw.setdefault('starttime', self.starttime) - - return container.Value(key, self.namespace, **kw) - - @util.deprecated("Specifying a " - "'type' and other namespace configuration with cache.get()/put()/etc. " - "is deprecated. Specify 'type' and other namespace configuration to " - "cache_manager.get_cache() and/or the Cache constructor instead.") - def _legacy_get_value(self, key, type, **kw): - expiretime = kw.pop('expiretime', self.expiretime) - starttime = kw.pop('starttime', None) - createfunc = kw.pop('createfunc', None) - kwargs = self.nsargs.copy() - kwargs.update(kw) - c = Cache(self.namespace.namespace, type=type, **kwargs) - return c._get_value(key, expiretime=expiretime, createfunc=createfunc, - starttime=starttime) - - def clear(self): - """Clear all the values from the namespace""" - self.namespace.remove() - - # dict interface - def __getitem__(self, key): - return self.get(key) - - def __contains__(self, key): - return self._get_value(key).has_current_value() - - def has_key(self, key): - return key in self - - def __delitem__(self, key): - self.remove_value(key) - - def __setitem__(self, key, value): - self.put(key, value) - - -class CacheManager(object): - def __init__(self, **kwargs): - """Initialize a CacheManager object with a set of options - - Options should be parsed with the - :func:`~beaker.util.parse_cache_config_options` function to - ensure only valid options are used. - - """ - self.kwargs = kwargs - self.regions = kwargs.pop('cache_regions', {}) - - # Add these regions to the module global - cache_regions.update(self.regions) - - def get_cache(self, name, **kwargs): - kw = self.kwargs.copy() - kw.update(kwargs) - return Cache._get_cache(name, kw) - - def get_cache_region(self, name, region): - if region not in self.regions: - raise BeakerException('Cache region not configured: %s' % region) - kw = self.regions[region] - return Cache._get_cache(name, kw) - - def region(self, region, *args): - """Decorate a function to cache itself using a cache region - - The region decorator requires arguments if there are more than - two of the same named function, in the same module. This is - because the namespace used for the functions cache is based on - the functions name and the module. - - - Example:: - - # Assuming a cache object is available like: - cache = CacheManager(dict_of_config_options) - - - def populate_things(): - - @cache.region('short_term', 'some_data') - def load(search_term, limit, offset): - return load_the_data(search_term, limit, offset) - - return load('rabbits', 20, 0) - - .. note:: - - The function being decorated must only be called with - positional arguments. - - """ - return cache_region(region, *args) - - def region_invalidate(self, namespace, region, *args): - """Invalidate a cache region namespace or decorated function - - This function only invalidates cache spaces created with the - cache_region decorator. - - :param namespace: Either the namespace of the result to invalidate, or the - cached function - - :param region: The region the function was cached to. If the function was - cached to a single region then this argument can be None - - :param args: Arguments that were used to differentiate the cached - function as well as the arguments passed to the decorated - function - - Example:: - - # Assuming a cache object is available like: - cache = CacheManager(dict_of_config_options) - - def populate_things(invalidate=False): - - @cache.region('short_term', 'some_data') - def load(search_term, limit, offset): - return load_the_data(search_term, limit, offset) - - # If the results should be invalidated first - if invalidate: - cache.region_invalidate(load, None, 'some_data', - 'rabbits', 20, 0) - return load('rabbits', 20, 0) - - - """ - return region_invalidate(namespace, region, *args) - - def cache(self, *args, **kwargs): - """Decorate a function to cache itself with supplied parameters - - :param args: Used to make the key unique for this function, as in region() - above. - - :param kwargs: Parameters to be passed to get_cache(), will override defaults - - Example:: - - # Assuming a cache object is available like: - cache = CacheManager(dict_of_config_options) - - - def populate_things(): - - @cache.cache('mycache', expire=15) - def load(search_term, limit, offset): - return load_the_data(search_term, limit, offset) - - return load('rabbits', 20, 0) - - .. note:: - - The function being decorated must only be called with - positional arguments. - - """ - return _cache_decorate(args, self, kwargs, None) - - def invalidate(self, func, *args, **kwargs): - """Invalidate a cache decorated function - - This function only invalidates cache spaces created with the - cache decorator. - - :param func: Decorated function to invalidate - - :param args: Used to make the key unique for this function, as in region() - above. - - :param kwargs: Parameters that were passed for use by get_cache(), note that - this is only required if a ``type`` was specified for the - function - - Example:: - - # Assuming a cache object is available like: - cache = CacheManager(dict_of_config_options) - - - def populate_things(invalidate=False): - - @cache.cache('mycache', type="file", expire=15) - def load(search_term, limit, offset): - return load_the_data(search_term, limit, offset) - - # If the results should be invalidated first - if invalidate: - cache.invalidate(load, 'mycache', 'rabbits', 20, 0, type="file") - return load('rabbits', 20, 0) - - """ - namespace = func._arg_namespace - - cache = self.get_cache(namespace, **kwargs) - if hasattr(func, '_arg_region'): - cachereg = cache_regions[func._arg_region] - key_length = cachereg.get('key_length', util.DEFAULT_CACHE_KEY_LENGTH) - else: - key_length = kwargs.pop('key_length', util.DEFAULT_CACHE_KEY_LENGTH) - _cache_decorator_invalidate(cache, key_length, args) - - -def _cache_decorate(deco_args, manager, options, region): - """Return a caching function decorator.""" - - cache = [None] - - def decorate(func): - namespace = util.func_namespace(func) - skip_self = util.has_self_arg(func) - signature = func_signature(func) - - @wraps(func) - def cached(*args, **kwargs): - if not cache[0]: - if region is not None: - if region not in cache_regions: - raise BeakerException( - 'Cache region not configured: %s' % region) - reg = cache_regions[region] - if not reg.get('enabled', True): - return func(*args, **kwargs) - cache[0] = Cache._get_cache(namespace, reg) - elif manager: - cache[0] = manager.get_cache(namespace, **options) - else: - raise Exception("'manager + kwargs' or 'region' " - "argument is required") - - cache_key_kwargs = [] - if kwargs: - # kwargs provided, merge them in positional args - # to avoid having different cache keys. - args, kwargs = bindfuncargs(signature, args, kwargs) - cache_key_kwargs = [u_(':').join((u_(key), u_(value))) for key, value in kwargs.items()] - - cache_key_args = args - if skip_self: - cache_key_args = args[1:] - - cache_key = u_(" ").join(map(u_, chain(deco_args, cache_key_args, cache_key_kwargs))) - - if region: - cachereg = cache_regions[region] - key_length = cachereg.get('key_length', util.DEFAULT_CACHE_KEY_LENGTH) - else: - key_length = options.pop('key_length', util.DEFAULT_CACHE_KEY_LENGTH) - - # TODO: This is probably a bug as length is checked before converting to UTF8 - # which will cause cache_key to grow in size. - if len(cache_key) + len(namespace) > int(key_length): - cache_key = sha1(cache_key.encode('utf-8')).hexdigest() - - def go(): - return func(*args, **kwargs) - # save org function name - go.__name__ = '_cached_%s' % (func.__name__,) - - return cache[0].get_value(cache_key, createfunc=go) - cached._arg_namespace = namespace - if region is not None: - cached._arg_region = region - return cached - return decorate - - -def _cache_decorator_invalidate(cache, key_length, args): - """Invalidate a cache key based on function arguments.""" - - cache_key = u_(" ").join(map(u_, args)) - if len(cache_key) + len(cache.namespace_name) > key_length: - cache_key = sha1(cache_key.encode('utf-8')).hexdigest() - cache.remove_value(cache_key) diff --git a/libs/beaker/container.py b/libs/beaker/container.py deleted file mode 100644 index f3f5b4f82..000000000 --- a/libs/beaker/container.py +++ /dev/null @@ -1,760 +0,0 @@ -"""Container and Namespace classes""" -import errno - -from ._compat import pickle, anydbm, add_metaclass, PYVER, unicode_text - -import beaker.util as util -import logging -import os -import time - -from beaker.exceptions import CreationAbortedError, MissingCacheParameter -from beaker.synchronization import _threading, file_synchronizer, \ - mutex_synchronizer, NameLock, null_synchronizer - -__all__ = ['Value', 'Container', 'ContainerContext', - 'MemoryContainer', 'DBMContainer', 'NamespaceManager', - 'MemoryNamespaceManager', 'DBMNamespaceManager', 'FileContainer', - 'OpenResourceNamespaceManager', - 'FileNamespaceManager', 'CreationAbortedError'] - - -logger = logging.getLogger('beaker.container') -if logger.isEnabledFor(logging.DEBUG): - debug = logger.debug -else: - def debug(message, *args): - pass - - -class NamespaceManager(object): - """Handles dictionary operations and locking for a namespace of - values. - - :class:`.NamespaceManager` provides a dictionary-like interface, - implementing ``__getitem__()``, ``__setitem__()``, and - ``__contains__()``, as well as functions related to lock - acquisition. - - The implementation for setting and retrieving the namespace data is - handled by subclasses. - - NamespaceManager may be used alone, or may be accessed by - one or more :class:`.Value` objects. :class:`.Value` objects provide per-key - services like expiration times and automatic recreation of values. - - Multiple NamespaceManagers created with a particular name will all - share access to the same underlying datasource and will attempt to - synchronize against a common mutex object. The scope of this - sharing may be within a single process or across multiple - processes, depending on the type of NamespaceManager used. - - The NamespaceManager itself is generally threadsafe, except in the - case of the DBMNamespaceManager in conjunction with the gdbm dbm - implementation. - - """ - - @classmethod - def _init_dependencies(cls): - """Initialize module-level dependent libraries required - by this :class:`.NamespaceManager`.""" - - def __init__(self, namespace): - self._init_dependencies() - self.namespace = namespace - - def get_creation_lock(self, key): - """Return a locking object that is used to synchronize - multiple threads or processes which wish to generate a new - cache value. - - This function is typically an instance of - :class:`.FileSynchronizer`, :class:`.ConditionSynchronizer`, - or :class:`.null_synchronizer`. - - The creation lock is only used when a requested value - does not exist, or has been expired, and is only used - by the :class:`.Value` key-management object in conjunction - with a "createfunc" value-creation function. - - """ - raise NotImplementedError() - - def do_remove(self): - """Implement removal of the entire contents of this - :class:`.NamespaceManager`. - - e.g. for a file-based namespace, this would remove - all the files. - - The front-end to this method is the - :meth:`.NamespaceManager.remove` method. - - """ - raise NotImplementedError() - - def acquire_read_lock(self): - """Establish a read lock. - - This operation is called before a key is read. By - default the function does nothing. - - """ - - def release_read_lock(self): - """Release a read lock. - - This operation is called after a key is read. By - default the function does nothing. - - """ - - def acquire_write_lock(self, wait=True, replace=False): - """Establish a write lock. - - This operation is called before a key is written. - A return value of ``True`` indicates the lock has - been acquired. - - By default the function returns ``True`` unconditionally. - - 'replace' is a hint indicating the full contents - of the namespace may be safely discarded. Some backends - may implement this (i.e. file backend won't unpickle the - current contents). - - """ - return True - - def release_write_lock(self): - """Release a write lock. - - This operation is called after a new value is written. - By default this function does nothing. - - """ - - def has_key(self, key): - """Return ``True`` if the given key is present in this - :class:`.Namespace`. - """ - return self.__contains__(key) - - def __getitem__(self, key): - raise NotImplementedError() - - def __setitem__(self, key, value): - raise NotImplementedError() - - def set_value(self, key, value, expiretime=None): - """Sets a value in this :class:`.NamespaceManager`. - - This is the same as ``__setitem__()``, but - also allows an expiration time to be passed - at the same time. - - """ - self[key] = value - - def __contains__(self, key): - raise NotImplementedError() - - def __delitem__(self, key): - raise NotImplementedError() - - def keys(self): - """Return the list of all keys. - - This method may not be supported by all - :class:`.NamespaceManager` implementations. - - """ - raise NotImplementedError() - - def remove(self): - """Remove the entire contents of this - :class:`.NamespaceManager`. - - e.g. for a file-based namespace, this would remove - all the files. - """ - self.do_remove() - - -class OpenResourceNamespaceManager(NamespaceManager): - """A NamespaceManager where read/write operations require opening/ - closing of a resource which is possibly mutexed. - - """ - def __init__(self, namespace): - NamespaceManager.__init__(self, namespace) - self.access_lock = self.get_access_lock() - self.openers = 0 - self.mutex = _threading.Lock() - - def get_access_lock(self): - raise NotImplementedError() - - def do_open(self, flags, replace): - raise NotImplementedError() - - def do_close(self): - raise NotImplementedError() - - def acquire_read_lock(self): - self.access_lock.acquire_read_lock() - try: - self.open('r', checkcount=True) - except: - self.access_lock.release_read_lock() - raise - - def release_read_lock(self): - try: - self.close(checkcount=True) - finally: - self.access_lock.release_read_lock() - - def acquire_write_lock(self, wait=True, replace=False): - r = self.access_lock.acquire_write_lock(wait) - try: - if (wait or r): - self.open('c', checkcount=True, replace=replace) - return r - except: - self.access_lock.release_write_lock() - raise - - def release_write_lock(self): - try: - self.close(checkcount=True) - finally: - self.access_lock.release_write_lock() - - def open(self, flags, checkcount=False, replace=False): - self.mutex.acquire() - try: - if checkcount: - if self.openers == 0: - self.do_open(flags, replace) - self.openers += 1 - else: - self.do_open(flags, replace) - self.openers = 1 - finally: - self.mutex.release() - - def close(self, checkcount=False): - self.mutex.acquire() - try: - if checkcount: - self.openers -= 1 - if self.openers == 0: - self.do_close() - else: - if self.openers > 0: - self.do_close() - self.openers = 0 - finally: - self.mutex.release() - - def remove(self): - self.access_lock.acquire_write_lock() - try: - self.close(checkcount=False) - self.do_remove() - finally: - self.access_lock.release_write_lock() - - -class Value(object): - """Implements synchronization, expiration, and value-creation logic - for a single value stored in a :class:`.NamespaceManager`. - - """ - - __slots__ = 'key', 'createfunc', 'expiretime', 'expire_argument', 'starttime', 'storedtime',\ - 'namespace' - - def __init__(self, key, namespace, createfunc=None, expiretime=None, starttime=None): - self.key = key - self.createfunc = createfunc - self.expire_argument = expiretime - self.starttime = starttime - self.storedtime = -1 - self.namespace = namespace - - def has_value(self): - """return true if the container has a value stored. - - This is regardless of it being expired or not. - - """ - self.namespace.acquire_read_lock() - try: - return self.key in self.namespace - finally: - self.namespace.release_read_lock() - - def can_have_value(self): - return self.has_current_value() or self.createfunc is not None - - def has_current_value(self): - self.namespace.acquire_read_lock() - try: - has_value = self.key in self.namespace - if has_value: - try: - stored, expired, value = self._get_value() - return not self._is_expired(stored, expired) - except KeyError: - pass - return False - finally: - self.namespace.release_read_lock() - - def _is_expired(self, storedtime, expiretime): - """Return true if this container's value is expired.""" - return ( - ( - self.starttime is not None and - storedtime < self.starttime - ) - or - ( - expiretime is not None and - time.time() >= expiretime + storedtime - ) - ) - - def get_value(self): - self.namespace.acquire_read_lock() - try: - has_value = self.has_value() - if has_value: - try: - stored, expired, value = self._get_value() - if not self._is_expired(stored, expired): - return value - except KeyError: - # guard against un-mutexed backends raising KeyError - has_value = False - - if not self.createfunc: - raise KeyError(self.key) - finally: - self.namespace.release_read_lock() - - has_createlock = False - creation_lock = self.namespace.get_creation_lock(self.key) - if has_value: - if not creation_lock.acquire(wait=False): - debug("get_value returning old value while new one is created") - return value - else: - debug("lock_creatfunc (didnt wait)") - has_createlock = True - - if not has_createlock: - debug("lock_createfunc (waiting)") - creation_lock.acquire() - debug("lock_createfunc (waited)") - - try: - # see if someone created the value already - self.namespace.acquire_read_lock() - try: - if self.has_value(): - try: - stored, expired, value = self._get_value() - if not self._is_expired(stored, expired): - return value - except KeyError: - # guard against un-mutexed backends raising KeyError - pass - finally: - self.namespace.release_read_lock() - - debug("get_value creating new value") - v = self.createfunc() - self.set_value(v) - return v - finally: - creation_lock.release() - debug("released create lock") - - def _get_value(self): - value = self.namespace[self.key] - try: - stored, expired, value = value - except ValueError: - if not len(value) == 2: - raise - # Old format: upgrade - stored, value = value - expired = self.expire_argument - debug("get_value upgrading time %r expire time %r", stored, self.expire_argument) - self.namespace.release_read_lock() - self.set_value(value, stored) - self.namespace.acquire_read_lock() - except TypeError: - # occurs when the value is None. memcached - # may yank the rug from under us in which case - # that's the result - raise KeyError(self.key) - return stored, expired, value - - def set_value(self, value, storedtime=None): - self.namespace.acquire_write_lock() - try: - if storedtime is None: - storedtime = time.time() - debug("set_value stored time %r expire time %r", storedtime, self.expire_argument) - self.namespace.set_value(self.key, (storedtime, self.expire_argument, value), - expiretime=self.expire_argument) - finally: - self.namespace.release_write_lock() - - def clear_value(self): - self.namespace.acquire_write_lock() - try: - debug("clear_value") - if self.key in self.namespace: - try: - del self.namespace[self.key] - except KeyError: - # guard against un-mutexed backends raising KeyError - pass - self.storedtime = -1 - finally: - self.namespace.release_write_lock() - - -class AbstractDictionaryNSManager(NamespaceManager): - """A subclassable NamespaceManager that places data in a dictionary. - - Subclasses should provide a "dictionary" attribute or descriptor - which returns a dict-like object. The dictionary will store keys - that are local to the "namespace" attribute of this manager, so - ensure that the dictionary will not be used by any other namespace. - - e.g.:: - - import collections - cached_data = collections.defaultdict(dict) - - class MyDictionaryManager(AbstractDictionaryNSManager): - def __init__(self, namespace): - AbstractDictionaryNSManager.__init__(self, namespace) - self.dictionary = cached_data[self.namespace] - - The above stores data in a global dictionary called "cached_data", - which is structured as a dictionary of dictionaries, keyed - first on namespace name to a sub-dictionary, then on actual - cache key to value. - - """ - - def get_creation_lock(self, key): - return NameLock( - identifier="memorynamespace/funclock/%s/%s" % - (self.namespace, key), - reentrant=True - ) - - def __getitem__(self, key): - return self.dictionary[key] - - def __contains__(self, key): - return self.dictionary.__contains__(key) - - def has_key(self, key): - return self.dictionary.__contains__(key) - - def __setitem__(self, key, value): - self.dictionary[key] = value - - def __delitem__(self, key): - del self.dictionary[key] - - def do_remove(self): - self.dictionary.clear() - - def keys(self): - return self.dictionary.keys() - - -class MemoryNamespaceManager(AbstractDictionaryNSManager): - """:class:`.NamespaceManager` that uses a Python dictionary for storage.""" - - namespaces = util.SyncDict() - - def __init__(self, namespace, **kwargs): - AbstractDictionaryNSManager.__init__(self, namespace) - self.dictionary = MemoryNamespaceManager.\ - namespaces.get(self.namespace, dict) - - -class DBMNamespaceManager(OpenResourceNamespaceManager): - """:class:`.NamespaceManager` that uses ``dbm`` files for storage.""" - - def __init__(self, namespace, dbmmodule=None, data_dir=None, - dbm_dir=None, lock_dir=None, - digest_filenames=True, **kwargs): - self.digest_filenames = digest_filenames - - if not dbm_dir and not data_dir: - raise MissingCacheParameter("data_dir or dbm_dir is required") - elif dbm_dir: - self.dbm_dir = dbm_dir - else: - self.dbm_dir = data_dir + "/container_dbm" - util.verify_directory(self.dbm_dir) - - if not lock_dir and not data_dir: - raise MissingCacheParameter("data_dir or lock_dir is required") - elif lock_dir: - self.lock_dir = lock_dir - else: - self.lock_dir = data_dir + "/container_dbm_lock" - util.verify_directory(self.lock_dir) - - self.dbmmodule = dbmmodule or anydbm - - self.dbm = None - OpenResourceNamespaceManager.__init__(self, namespace) - - self.file = util.encoded_path(root=self.dbm_dir, - identifiers=[self.namespace], - extension='.dbm', - digest_filenames=self.digest_filenames) - - debug("data file %s", self.file) - self._checkfile() - - def get_access_lock(self): - return file_synchronizer(identifier=self.namespace, - lock_dir=self.lock_dir) - - def get_creation_lock(self, key): - return file_synchronizer( - identifier="dbmcontainer/funclock/%s/%s" % ( - self.namespace, key - ), - lock_dir=self.lock_dir - ) - - def file_exists(self, file): - if os.access(file, os.F_OK): - return True - else: - for ext in ('db', 'dat', 'pag', 'dir'): - if os.access(file + os.extsep + ext, os.F_OK): - return True - - return False - - def _ensuredir(self, filename): - dirname = os.path.dirname(filename) - if not os.path.exists(dirname): - util.verify_directory(dirname) - - def _checkfile(self): - if not self.file_exists(self.file): - self._ensuredir(self.file) - g = self.dbmmodule.open(self.file, 'c') - g.close() - - def get_filenames(self): - list = [] - if os.access(self.file, os.F_OK): - list.append(self.file) - - for ext in ('pag', 'dir', 'db', 'dat'): - if os.access(self.file + os.extsep + ext, os.F_OK): - list.append(self.file + os.extsep + ext) - return list - - def do_open(self, flags, replace): - debug("opening dbm file %s", self.file) - try: - self.dbm = self.dbmmodule.open(self.file, flags) - except: - self._checkfile() - self.dbm = self.dbmmodule.open(self.file, flags) - - def do_close(self): - if self.dbm is not None: - debug("closing dbm file %s", self.file) - self.dbm.close() - - def do_remove(self): - for f in self.get_filenames(): - os.remove(f) - - def __getitem__(self, key): - return pickle.loads(self.dbm[key]) - - def __contains__(self, key): - if PYVER == (3, 2): - # Looks like this is a bug that got solved in PY3.3 and PY3.4 - # http://bugs.python.org/issue19288 - if isinstance(key, unicode_text): - key = key.encode('UTF-8') - return key in self.dbm - - def __setitem__(self, key, value): - self.dbm[key] = pickle.dumps(value) - - def __delitem__(self, key): - del self.dbm[key] - - def keys(self): - return self.dbm.keys() - - -class FileNamespaceManager(OpenResourceNamespaceManager): - """:class:`.NamespaceManager` that uses binary files for storage. - - Each namespace is implemented as a single file storing a - dictionary of key/value pairs, serialized using the Python - ``pickle`` module. - - """ - def __init__(self, namespace, data_dir=None, file_dir=None, lock_dir=None, - digest_filenames=True, **kwargs): - self.digest_filenames = digest_filenames - - if not file_dir and not data_dir: - raise MissingCacheParameter("data_dir or file_dir is required") - elif file_dir: - self.file_dir = file_dir - else: - self.file_dir = data_dir + "/container_file" - util.verify_directory(self.file_dir) - - if not lock_dir and not data_dir: - raise MissingCacheParameter("data_dir or lock_dir is required") - elif lock_dir: - self.lock_dir = lock_dir - else: - self.lock_dir = data_dir + "/container_file_lock" - util.verify_directory(self.lock_dir) - OpenResourceNamespaceManager.__init__(self, namespace) - - self.file = util.encoded_path(root=self.file_dir, - identifiers=[self.namespace], - extension='.cache', - digest_filenames=self.digest_filenames) - self.hash = {} - - debug("data file %s", self.file) - - def get_access_lock(self): - return file_synchronizer(identifier=self.namespace, - lock_dir=self.lock_dir) - - def get_creation_lock(self, key): - return file_synchronizer( - identifier="dbmcontainer/funclock/%s/%s" % ( - self.namespace, key - ), - lock_dir=self.lock_dir - ) - - def file_exists(self, file): - return os.access(file, os.F_OK) - - def do_open(self, flags, replace): - if not replace and self.file_exists(self.file): - try: - with open(self.file, 'rb') as fh: - self.hash = pickle.load(fh) - except IOError as e: - # Ignore EACCES and ENOENT as it just means we are no longer - # able to access the file or that it no longer exists - if e.errno not in [errno.EACCES, errno.ENOENT]: - raise - - self.flags = flags - - def do_close(self): - if self.flags == 'c' or self.flags == 'w': - pickled = pickle.dumps(self.hash) - util.safe_write(self.file, pickled) - - self.hash = {} - self.flags = None - - def do_remove(self): - try: - os.remove(self.file) - except OSError: - # for instance, because we haven't yet used this cache, - # but client code has asked for a clear() operation... - pass - self.hash = {} - - def __getitem__(self, key): - return self.hash[key] - - def __contains__(self, key): - return key in self.hash - - def __setitem__(self, key, value): - self.hash[key] = value - - def __delitem__(self, key): - del self.hash[key] - - def keys(self): - return self.hash.keys() - - -#### legacy stuff to support the old "Container" class interface - -namespace_classes = {} - -ContainerContext = dict - - -class ContainerMeta(type): - def __init__(cls, classname, bases, dict_): - namespace_classes[cls] = cls.namespace_class - return type.__init__(cls, classname, bases, dict_) - - def __call__(self, key, context, namespace, createfunc=None, - expiretime=None, starttime=None, **kwargs): - if namespace in context: - ns = context[namespace] - else: - nscls = namespace_classes[self] - context[namespace] = ns = nscls(namespace, **kwargs) - return Value(key, ns, createfunc=createfunc, - expiretime=expiretime, starttime=starttime) - -@add_metaclass(ContainerMeta) -class Container(object): - """Implements synchronization and value-creation logic - for a 'value' stored in a :class:`.NamespaceManager`. - - :class:`.Container` and its subclasses are deprecated. The - :class:`.Value` class is now used for this purpose. - - """ - namespace_class = NamespaceManager - - -class FileContainer(Container): - namespace_class = FileNamespaceManager - - -class MemoryContainer(Container): - namespace_class = MemoryNamespaceManager - - -class DBMContainer(Container): - namespace_class = DBMNamespaceManager - -DbmContainer = DBMContainer diff --git a/libs/beaker/converters.py b/libs/beaker/converters.py deleted file mode 100644 index a8fb3c93a..000000000 --- a/libs/beaker/converters.py +++ /dev/null @@ -1,29 +0,0 @@ -from beaker._compat import string_type - -# (c) 2005 Ian Bicking and contributors; written for Paste (http://pythonpaste.org) -# Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php -def asbool(obj): - if isinstance(obj, string_type): - obj = obj.strip().lower() - if obj in ['true', 'yes', 'on', 'y', 't', '1']: - return True - elif obj in ['false', 'no', 'off', 'n', 'f', '0']: - return False - else: - raise ValueError( - "String is not true/false: %r" % obj) - return bool(obj) - - -def aslist(obj, sep=None, strip=True): - if isinstance(obj, string_type): - lst = obj.split(sep) - if strip: - lst = [v.strip() for v in lst] - return lst - elif isinstance(obj, (list, tuple)): - return obj - elif obj is None: - return [] - else: - return [obj] diff --git a/libs/beaker/cookie.py b/libs/beaker/cookie.py deleted file mode 100644 index 729fbe3cb..000000000 --- a/libs/beaker/cookie.py +++ /dev/null @@ -1,72 +0,0 @@ -import sys -from ._compat import http_cookies - -# Some versions of Python 2.7 and later won't need this encoding bug fix: -_cookie_encodes_correctly = http_cookies.SimpleCookie().value_encode(';') == (';', '"\\073"') - -# Cookie pickling bug is fixed in Python 2.7.9 and Python 3.4.3+ -# http://bugs.python.org/issue22775 -cookie_pickles_properly = ( - (sys.version_info[:2] == (2, 7) and sys.version_info >= (2, 7, 9)) or - sys.version_info >= (3, 4, 3) -) - -# Add support for the SameSite attribute (obsolete when PY37 is unsupported). -http_cookies.Morsel._reserved.setdefault('samesite', 'SameSite') - - -# Adapted from Django.http.cookies and always enabled the bad_cookies -# behaviour to cope with any invalid cookie key while keeping around -# the session. -class SimpleCookie(http_cookies.SimpleCookie): - if not cookie_pickles_properly: - def __setitem__(self, key, value): - # Apply the fix from http://bugs.python.org/issue22775 where - # it's not fixed in Python itself - if isinstance(value, http_cookies.Morsel): - # allow assignment of constructed Morsels (e.g. for pickling) - dict.__setitem__(self, key, value) - else: - super(SimpleCookie, self).__setitem__(key, value) - - if not _cookie_encodes_correctly: - def value_encode(self, val): - # Some browsers do not support quoted-string from RFC 2109, - # including some versions of Safari and Internet Explorer. - # These browsers split on ';', and some versions of Safari - # are known to split on ', '. Therefore, we encode ';' and ',' - - # SimpleCookie already does the hard work of encoding and decoding. - # It uses octal sequences like '\\012' for newline etc. - # and non-ASCII chars. We just make use of this mechanism, to - # avoid introducing two encoding schemes which would be confusing - # and especially awkward for javascript. - - # NB, contrary to Python docs, value_encode returns a tuple containing - # (real val, encoded_val) - val, encoded = super(SimpleCookie, self).value_encode(val) - - encoded = encoded.replace(";", "\\073").replace(",", "\\054") - # If encoded now contains any quoted chars, we need double quotes - # around the whole string. - if "\\" in encoded and not encoded.startswith('"'): - encoded = '"' + encoded + '"' - - return val, encoded - - def load(self, rawdata): - self.bad_cookies = set() - super(SimpleCookie, self).load(rawdata) - for key in self.bad_cookies: - del self[key] - - # override private __set() method: - # (needed for using our Morsel, and for laxness with CookieError - def _BaseCookie__set(self, key, real_value, coded_value): - try: - super(SimpleCookie, self)._BaseCookie__set(key, real_value, coded_value) - except http_cookies.CookieError: - if not hasattr(self, 'bad_cookies'): - self.bad_cookies = set() - self.bad_cookies.add(key) - dict.__setitem__(self, key, http_cookies.Morsel()) diff --git a/libs/beaker/crypto/__init__.py b/libs/beaker/crypto/__init__.py deleted file mode 100644 index 84bc258fa..000000000 --- a/libs/beaker/crypto/__init__.py +++ /dev/null @@ -1,83 +0,0 @@ -from .._compat import JYTHON - - -from beaker.crypto.pbkdf2 import pbkdf2 -from beaker.crypto.util import hmac, sha1, hmac_sha1, md5 -from beaker import util -from beaker.exceptions import InvalidCryptoBackendError - -keyLength = None -DEFAULT_NONCE_BITS = 128 - -CRYPTO_MODULES = {} - - -def load_default_module(): - """ Load the default crypto module - """ - if JYTHON: - try: - from beaker.crypto import jcecrypto - return jcecrypto - except ImportError: - pass - else: - try: - from beaker.crypto import nsscrypto - return nsscrypto - except ImportError: - try: - from beaker.crypto import pycrypto - return pycrypto - except ImportError: - pass - from beaker.crypto import noencryption - return noencryption - - -def register_crypto_module(name, mod): - """ - Register the given module under the name given. - """ - CRYPTO_MODULES[name] = mod - - -def get_crypto_module(name): - """ - Get the active crypto module for this name - """ - if name not in CRYPTO_MODULES: - if name == 'default': - register_crypto_module('default', load_default_module()) - elif name == 'nss': - from beaker.crypto import nsscrypto - register_crypto_module(name, nsscrypto) - elif name == 'pycrypto': - from beaker.crypto import pycrypto - register_crypto_module(name, pycrypto) - elif name == 'cryptography': - from beaker.crypto import pyca_cryptography - register_crypto_module(name, pyca_cryptography) - else: - raise InvalidCryptoBackendError( - "No crypto backend with name '%s' is registered." % name) - - return CRYPTO_MODULES[name] - - - -def generateCryptoKeys(master_key, salt, iterations, keylen): - # NB: We XOR parts of the keystream into the randomly-generated parts, just - # in case os.urandom() isn't as random as it should be. Note that if - # os.urandom() returns truly random data, this will have no effect on the - # overall security. - return pbkdf2(master_key, salt, iterations=iterations, dklen=keylen) - - -def get_nonce_size(number_of_bits): - if number_of_bits % 8: - raise ValueError('Nonce complexity currently supports multiples of 8') - - bytes = number_of_bits // 8 - b64bytes = ((4 * bytes // 3) + 3) & ~3 - return bytes, b64bytes diff --git a/libs/beaker/crypto/jcecrypto.py b/libs/beaker/crypto/jcecrypto.py deleted file mode 100644 index dc070c735..000000000 --- a/libs/beaker/crypto/jcecrypto.py +++ /dev/null @@ -1,41 +0,0 @@ -""" -Encryption module that uses the Java Cryptography Extensions (JCE). - -Note that in default installations of the Java Runtime Environment, the -maximum key length is limited to 128 bits due to US export -restrictions. This makes the generated keys incompatible with the ones -generated by pycryptopp, which has no such restrictions. To fix this, -download the "Unlimited Strength Jurisdiction Policy Files" from Sun, -which will allow encryption using 256 bit AES keys. -""" -from warnings import warn - -from javax.crypto import Cipher -from javax.crypto.spec import SecretKeySpec, IvParameterSpec - -import jarray - -# Initialization vector filled with zeros -_iv = IvParameterSpec(jarray.zeros(16, 'b')) - - -def aesEncrypt(data, key): - cipher = Cipher.getInstance('AES/CTR/NoPadding') - skeySpec = SecretKeySpec(key, 'AES') - cipher.init(Cipher.ENCRYPT_MODE, skeySpec, _iv) - return cipher.doFinal(data).tostring() - -# magic. -aesDecrypt = aesEncrypt - -has_aes = True - -def getKeyLength(): - maxlen = Cipher.getMaxAllowedKeyLength('AES/CTR/NoPadding') - return min(maxlen, 256) / 8 - - -if getKeyLength() < 32: - warn('Crypto implementation only supports key lengths up to %d bits. ' - 'Generated session cookies may be incompatible with other ' - 'environments' % (getKeyLength() * 8)) diff --git a/libs/beaker/crypto/noencryption.py b/libs/beaker/crypto/noencryption.py deleted file mode 100644 index a4af84fa0..000000000 --- a/libs/beaker/crypto/noencryption.py +++ /dev/null @@ -1,12 +0,0 @@ -"""Encryption module that does nothing""" - -def aesEncrypt(data, key): - return data - -def aesDecrypt(data, key): - return data - -has_aes = False - -def getKeyLength(): - return 32 diff --git a/libs/beaker/crypto/nsscrypto.py b/libs/beaker/crypto/nsscrypto.py deleted file mode 100644 index 684a017db..000000000 --- a/libs/beaker/crypto/nsscrypto.py +++ /dev/null @@ -1,47 +0,0 @@ -"""Encryption module that uses nsscrypto""" -import nss.nss - -nss.nss.nss_init_nodb() - -# Apparently the rest of beaker doesn't care about the particluar cipher, -# mode and padding used. -# NOTE: A constant IV!!! This is only secure if the KEY is never reused!!! -_mech = nss.nss.CKM_AES_CBC_PAD -_iv = '\0' * nss.nss.get_iv_length(_mech) - -def aesEncrypt(data, key): - slot = nss.nss.get_best_slot(_mech) - - key_obj = nss.nss.import_sym_key(slot, _mech, nss.nss.PK11_OriginGenerated, - nss.nss.CKA_ENCRYPT, nss.nss.SecItem(key)) - - param = nss.nss.param_from_iv(_mech, nss.nss.SecItem(_iv)) - ctx = nss.nss.create_context_by_sym_key(_mech, nss.nss.CKA_ENCRYPT, key_obj, - param) - l1 = ctx.cipher_op(data) - # Yes, DIGEST. This needs fixing in NSS, but apparently nobody (including - # me :( ) cares enough. - l2 = ctx.digest_final() - - return l1 + l2 - -def aesDecrypt(data, key): - slot = nss.nss.get_best_slot(_mech) - - key_obj = nss.nss.import_sym_key(slot, _mech, nss.nss.PK11_OriginGenerated, - nss.nss.CKA_DECRYPT, nss.nss.SecItem(key)) - - param = nss.nss.param_from_iv(_mech, nss.nss.SecItem(_iv)) - ctx = nss.nss.create_context_by_sym_key(_mech, nss.nss.CKA_DECRYPT, key_obj, - param) - l1 = ctx.cipher_op(data) - # Yes, DIGEST. This needs fixing in NSS, but apparently nobody (including - # me :( ) cares enough. - l2 = ctx.digest_final() - - return l1 + l2 - -has_aes = True - -def getKeyLength(): - return 32 diff --git a/libs/beaker/crypto/pbkdf2.py b/libs/beaker/crypto/pbkdf2.py deleted file mode 100644 index 3dca73763..000000000 --- a/libs/beaker/crypto/pbkdf2.py +++ /dev/null @@ -1,94 +0,0 @@ -""" -PBKDF2 Implementation adapted from django.utils.crypto. - -This is used to generate the encryption key for enciphered sessions. -""" -from beaker._compat import bytes_, xrange_ - -import hmac -import struct -import hashlib -import binascii - - -def _bin_to_long(x): - """Convert a binary string into a long integer""" - return int(binascii.hexlify(x), 16) - - -def _long_to_bin(x, hex_format_string): - """ - Convert a long integer into a binary string. - hex_format_string is like "%020x" for padding 10 characters. - """ - return binascii.unhexlify((hex_format_string % x).encode('ascii')) - - -if hasattr(hashlib, "pbkdf2_hmac"): - def pbkdf2(password, salt, iterations, dklen=0, digest=None): - """ - Implements PBKDF2 using the stdlib. This is used in Python 2.7.8+ and 3.4+. - - HMAC+SHA256 is used as the default pseudo random function. - - As of 2014, 100,000 iterations was the recommended default which took - 100ms on a 2.7Ghz Intel i7 with an optimized implementation. This is - probably the bare minimum for security given 1000 iterations was - recommended in 2001. - """ - if digest is None: - digest = hashlib.sha1 - if not dklen: - dklen = None - password = bytes_(password) - salt = bytes_(salt) - return hashlib.pbkdf2_hmac( - digest().name, password, salt, iterations, dklen) -else: - def pbkdf2(password, salt, iterations, dklen=0, digest=None): - """ - Implements PBKDF2 as defined in RFC 2898, section 5.2 - - HMAC+SHA256 is used as the default pseudo random function. - - As of 2014, 100,000 iterations was the recommended default which took - 100ms on a 2.7Ghz Intel i7 with an optimized implementation. This is - probably the bare minimum for security given 1000 iterations was - recommended in 2001. This code is very well optimized for CPython and - is about five times slower than OpenSSL's implementation. - """ - assert iterations > 0 - if not digest: - digest = hashlib.sha1 - password = bytes_(password) - salt = bytes_(salt) - hlen = digest().digest_size - if not dklen: - dklen = hlen - if dklen > (2 ** 32 - 1) * hlen: - raise OverflowError('dklen too big') - l = -(-dklen // hlen) - r = dklen - (l - 1) * hlen - - hex_format_string = "%%0%ix" % (hlen * 2) - - inner, outer = digest(), digest() - if len(password) > inner.block_size: - password = digest(password).digest() - password += b'\x00' * (inner.block_size - len(password)) - inner.update(password.translate(hmac.trans_36)) - outer.update(password.translate(hmac.trans_5C)) - - def F(i): - u = salt + struct.pack(b'>I', i) - result = 0 - for j in xrange_(int(iterations)): - dig1, dig2 = inner.copy(), outer.copy() - dig1.update(u) - dig2.update(dig1.digest()) - u = dig2.digest() - result ^= _bin_to_long(u) - return _long_to_bin(result, hex_format_string) - - T = [F(x) for x in xrange_(1, l)] - return b''.join(T) + F(l)[:r] diff --git a/libs/beaker/crypto/pyca_cryptography.py b/libs/beaker/crypto/pyca_cryptography.py deleted file mode 100644 index ae273b7cc..000000000 --- a/libs/beaker/crypto/pyca_cryptography.py +++ /dev/null @@ -1,52 +0,0 @@ -"""Encryption module that uses pyca/cryptography""" - -import os -import json - -from cryptography.hazmat.backends import default_backend -from cryptography.hazmat.primitives.ciphers import ( - Cipher, algorithms, modes -) - - -def aesEncrypt(data, key): - # Generate a random 96-bit IV. - iv = os.urandom(12) - - # Construct an AES-GCM Cipher object with the given key and a - # randomly generated IV. - encryptor = Cipher( - algorithms.AES(key), - modes.GCM(iv), - backend=default_backend() - ).encryptor() - - # Encrypt the plaintext and get the associated ciphertext. - # GCM does not require padding. - ciphertext = encryptor.update(data) + encryptor.finalize() - - return iv + encryptor.tag + ciphertext - - -def aesDecrypt(data, key): - iv = data[:12] - tag = data[12:28] - ciphertext = data[28:] - - # Construct a Cipher object, with the key, iv, and additionally the - # GCM tag used for authenticating the message. - decryptor = Cipher( - algorithms.AES(key), - modes.GCM(iv, tag), - backend=default_backend() - ).decryptor() - - # Decryption gets us the authenticated plaintext. - # If the tag does not match an InvalidTag exception will be raised. - return decryptor.update(ciphertext) + decryptor.finalize() - - -has_aes = True - -def getKeyLength(): - return 32 diff --git a/libs/beaker/crypto/pycrypto.py b/libs/beaker/crypto/pycrypto.py deleted file mode 100644 index 55b319ccd..000000000 --- a/libs/beaker/crypto/pycrypto.py +++ /dev/null @@ -1,34 +0,0 @@ -"""Encryption module that uses pycryptopp or pycrypto""" -try: - # Pycryptopp is preferred over Crypto because Crypto has had - # various periods of not being maintained, and pycryptopp uses - # the Crypto++ library which is generally considered the 'gold standard' - # of crypto implementations - from pycryptopp.cipher import aes - - def aesEncrypt(data, key): - cipher = aes.AES(key) - return cipher.process(data) - - # magic. - aesDecrypt = aesEncrypt - -except ImportError: - from Crypto.Cipher import AES - from Crypto.Util import Counter - - def aesEncrypt(data, key): - cipher = AES.new(key, AES.MODE_CTR, - counter=Counter.new(128, initial_value=0)) - - return cipher.encrypt(data) - - def aesDecrypt(data, key): - cipher = AES.new(key, AES.MODE_CTR, - counter=Counter.new(128, initial_value=0)) - return cipher.decrypt(data) - -has_aes = True - -def getKeyLength(): - return 32 diff --git a/libs/beaker/crypto/util.py b/libs/beaker/crypto/util.py deleted file mode 100644 index 07d1418b9..000000000 --- a/libs/beaker/crypto/util.py +++ /dev/null @@ -1,16 +0,0 @@ -from hashlib import md5 - -try: - # Use PyCrypto (if available) - from Crypto.Hash import HMAC as hmac, SHA as hmac_sha1 - sha1 = hmac_sha1.new - -except ImportError: - - # PyCrypto not available. Use the Python standard library. - import hmac - - # NOTE: We have to use the callable with hashlib (hashlib.sha1), - # otherwise hmac only accepts the sha module object itself - from hashlib import sha1 - hmac_sha1 = sha1 \ No newline at end of file diff --git a/libs/beaker/exceptions.py b/libs/beaker/exceptions.py deleted file mode 100644 index 4f81e456d..000000000 --- a/libs/beaker/exceptions.py +++ /dev/null @@ -1,29 +0,0 @@ -"""Beaker exception classes""" - - -class BeakerException(Exception): - pass - - -class BeakerWarning(RuntimeWarning): - """Issued at runtime.""" - - -class CreationAbortedError(Exception): - """Deprecated.""" - - -class InvalidCacheBackendError(BeakerException, ImportError): - pass - - -class MissingCacheParameter(BeakerException): - pass - - -class LockError(BeakerException): - pass - - -class InvalidCryptoBackendError(BeakerException): - pass diff --git a/libs/beaker/ext/database.py b/libs/beaker/ext/database.py deleted file mode 100644 index 98e90933f..000000000 --- a/libs/beaker/ext/database.py +++ /dev/null @@ -1,180 +0,0 @@ -from beaker._compat import pickle - -import logging -import pickle -from datetime import datetime - -from beaker.container import OpenResourceNamespaceManager, Container -from beaker.exceptions import InvalidCacheBackendError, MissingCacheParameter -from beaker.synchronization import file_synchronizer, null_synchronizer -from beaker.util import verify_directory, SyncDict - -log = logging.getLogger(__name__) - -sa = None -pool = None -types = None - - -class DatabaseNamespaceManager(OpenResourceNamespaceManager): - metadatas = SyncDict() - tables = SyncDict() - - @classmethod - def _init_dependencies(cls): - global sa, pool, types - if sa is not None: - return - try: - import sqlalchemy as sa - import sqlalchemy.pool as pool - from sqlalchemy import types - except ImportError: - raise InvalidCacheBackendError("Database cache backend requires " - "the 'sqlalchemy' library") - - def __init__(self, namespace, url=None, sa_opts=None, optimistic=False, - table_name='beaker_cache', data_dir=None, lock_dir=None, - schema_name=None, **params): - """Creates a database namespace manager - - ``url`` - SQLAlchemy compliant db url - ``sa_opts`` - A dictionary of SQLAlchemy keyword options to initialize the engine - with. - ``optimistic`` - Use optimistic session locking, note that this will result in an - additional select when updating a cache value to compare version - numbers. - ``table_name`` - The table name to use in the database for the cache. - ``schema_name`` - The schema name to use in the database for the cache. - """ - OpenResourceNamespaceManager.__init__(self, namespace) - - if sa_opts is None: - sa_opts = {} - - self.lock_dir = None - - if lock_dir: - self.lock_dir = lock_dir - elif data_dir: - self.lock_dir = data_dir + "/container_db_lock" - if self.lock_dir: - verify_directory(self.lock_dir) - - # Check to see if the table's been created before - url = url or sa_opts['sa.url'] - table_key = url + table_name - - def make_cache(): - # Check to see if we have a connection pool open already - meta_key = url + table_name - - def make_meta(): - # SQLAlchemy pops the url, this ensures it sticks around - # later - sa_opts['sa.url'] = url - engine = sa.engine_from_config(sa_opts, 'sa.') - meta = sa.MetaData() - meta.bind = engine - return meta - meta = DatabaseNamespaceManager.metadatas.get(meta_key, make_meta) - # Create the table object and cache it now - cache = sa.Table(table_name, meta, - sa.Column('id', types.Integer, primary_key=True), - sa.Column('namespace', types.String(255), nullable=False), - sa.Column('accessed', types.DateTime, nullable=False), - sa.Column('created', types.DateTime, nullable=False), - sa.Column('data', types.PickleType, nullable=False), - sa.UniqueConstraint('namespace'), - schema=schema_name if schema_name else meta.schema - ) - cache.create(checkfirst=True) - return cache - self.hash = {} - self._is_new = False - self.loaded = False - self.cache = DatabaseNamespaceManager.tables.get(table_key, make_cache) - - def get_access_lock(self): - return null_synchronizer() - - def get_creation_lock(self, key): - return file_synchronizer( - identifier="databasecontainer/funclock/%s/%s" % ( - self.namespace, key - ), - lock_dir=self.lock_dir) - - def do_open(self, flags, replace): - # If we already loaded the data, don't bother loading it again - if self.loaded: - self.flags = flags - return - - cache = self.cache - result_proxy = sa.select([cache.c.data], - cache.c.namespace == self.namespace - ).execute() - result = result_proxy.fetchone() - result_proxy.close() - - if not result: - self._is_new = True - self.hash = {} - else: - self._is_new = False - try: - self.hash = result['data'] - except (IOError, OSError, EOFError, pickle.PickleError, - pickle.PickleError): - log.debug("Couln't load pickle data, creating new storage") - self.hash = {} - self._is_new = True - self.flags = flags - self.loaded = True - - def do_close(self): - if self.flags is not None and (self.flags == 'c' or self.flags == 'w'): - cache = self.cache - if self._is_new: - cache.insert().execute(namespace=self.namespace, data=self.hash, - accessed=datetime.now(), - created=datetime.now()) - self._is_new = False - else: - cache.update(cache.c.namespace == self.namespace).execute( - data=self.hash, accessed=datetime.now()) - self.flags = None - - def do_remove(self): - cache = self.cache - cache.delete(cache.c.namespace == self.namespace).execute() - self.hash = {} - - # We can retain the fact that we did a load attempt, but since the - # file is gone this will be a new namespace should it be saved. - self._is_new = True - - def __getitem__(self, key): - return self.hash[key] - - def __contains__(self, key): - return key in self.hash - - def __setitem__(self, key, value): - self.hash[key] = value - - def __delitem__(self, key): - del self.hash[key] - - def keys(self): - return self.hash.keys() - - -class DatabaseContainer(Container): - namespace_manager = DatabaseNamespaceManager diff --git a/libs/beaker/ext/google.py b/libs/beaker/ext/google.py deleted file mode 100644 index d3583cb44..000000000 --- a/libs/beaker/ext/google.py +++ /dev/null @@ -1,122 +0,0 @@ -from beaker._compat import pickle - -import logging -from datetime import datetime - -from beaker.container import OpenResourceNamespaceManager, Container -from beaker.exceptions import InvalidCacheBackendError -from beaker.synchronization import null_synchronizer - -log = logging.getLogger(__name__) - -db = None - - -class GoogleNamespaceManager(OpenResourceNamespaceManager): - tables = {} - - @classmethod - def _init_dependencies(cls): - global db - if db is not None: - return - try: - db = __import__('google.appengine.ext.db').appengine.ext.db - except ImportError: - raise InvalidCacheBackendError("Datastore cache backend requires the " - "'google.appengine.ext' library") - - def __init__(self, namespace, table_name='beaker_cache', **params): - """Creates a datastore namespace manager""" - OpenResourceNamespaceManager.__init__(self, namespace) - - def make_cache(): - table_dict = dict(created=db.DateTimeProperty(), - accessed=db.DateTimeProperty(), - data=db.BlobProperty()) - table = type(table_name, (db.Model,), table_dict) - return table - self.table_name = table_name - self.cache = GoogleNamespaceManager.tables.setdefault(table_name, make_cache()) - self.hash = {} - self._is_new = False - self.loaded = False - self.log_debug = logging.DEBUG >= log.getEffectiveLevel() - - # Google wants namespaces to start with letters, change the namespace - # to start with a letter - self.namespace = 'p%s' % self.namespace - - def get_access_lock(self): - return null_synchronizer() - - def get_creation_lock(self, key): - # this is weird, should probably be present - return null_synchronizer() - - def do_open(self, flags, replace): - # If we already loaded the data, don't bother loading it again - if self.loaded: - self.flags = flags - return - - item = self.cache.get_by_key_name(self.namespace) - - if not item: - self._is_new = True - self.hash = {} - else: - self._is_new = False - try: - self.hash = pickle.loads(str(item.data)) - except (IOError, OSError, EOFError, pickle.PickleError): - if self.log_debug: - log.debug("Couln't load pickle data, creating new storage") - self.hash = {} - self._is_new = True - self.flags = flags - self.loaded = True - - def do_close(self): - if self.flags is not None and (self.flags == 'c' or self.flags == 'w'): - if self._is_new: - item = self.cache(key_name=self.namespace) - item.data = pickle.dumps(self.hash) - item.created = datetime.now() - item.accessed = datetime.now() - item.put() - self._is_new = False - else: - item = self.cache.get_by_key_name(self.namespace) - item.data = pickle.dumps(self.hash) - item.accessed = datetime.now() - item.put() - self.flags = None - - def do_remove(self): - item = self.cache.get_by_key_name(self.namespace) - item.delete() - self.hash = {} - - # We can retain the fact that we did a load attempt, but since the - # file is gone this will be a new namespace should it be saved. - self._is_new = True - - def __getitem__(self, key): - return self.hash[key] - - def __contains__(self, key): - return key in self.hash - - def __setitem__(self, key, value): - self.hash[key] = value - - def __delitem__(self, key): - del self.hash[key] - - def keys(self): - return self.hash.keys() - - -class GoogleContainer(Container): - namespace_class = GoogleNamespaceManager diff --git a/libs/beaker/ext/memcached.py b/libs/beaker/ext/memcached.py deleted file mode 100644 index 49bb3f234..000000000 --- a/libs/beaker/ext/memcached.py +++ /dev/null @@ -1,218 +0,0 @@ -from .._compat import PY2 - -from beaker.container import NamespaceManager, Container -from beaker.crypto.util import sha1 -from beaker.exceptions import InvalidCacheBackendError, MissingCacheParameter -from beaker.synchronization import file_synchronizer -from beaker.util import verify_directory, SyncDict, parse_memcached_behaviors -import warnings - -MAX_KEY_LENGTH = 250 - -_client_libs = {} - - -def _load_client(name='auto'): - if name in _client_libs: - return _client_libs[name] - - def _pylibmc(): - global pylibmc - import pylibmc - return pylibmc - - def _cmemcache(): - global cmemcache - import cmemcache - warnings.warn("cmemcache is known to have serious " - "concurrency issues; consider using 'memcache' " - "or 'pylibmc'") - return cmemcache - - def _memcache(): - global memcache - import memcache - return memcache - - def _bmemcached(): - global bmemcached - import bmemcached - return bmemcached - - def _auto(): - for _client in (_pylibmc, _cmemcache, _memcache, _bmemcached): - try: - return _client() - except ImportError: - pass - else: - raise InvalidCacheBackendError( - "Memcached cache backend requires one " - "of: 'pylibmc' or 'memcache' to be installed.") - - clients = { - 'pylibmc': _pylibmc, - 'cmemcache': _cmemcache, - 'memcache': _memcache, - 'bmemcached': _bmemcached, - 'auto': _auto - } - _client_libs[name] = clib = clients[name]() - return clib - - -def _is_configured_for_pylibmc(memcache_module_config, memcache_client): - return memcache_module_config == 'pylibmc' or \ - memcache_client.__name__.startswith('pylibmc') - - -class MemcachedNamespaceManager(NamespaceManager): - """Provides the :class:`.NamespaceManager` API over a memcache client library.""" - - clients = SyncDict() - - def __new__(cls, *args, **kw): - memcache_module = kw.pop('memcache_module', 'auto') - - memcache_client = _load_client(memcache_module) - - if _is_configured_for_pylibmc(memcache_module, memcache_client): - return object.__new__(PyLibMCNamespaceManager) - else: - return object.__new__(MemcachedNamespaceManager) - - def __init__(self, namespace, url, - memcache_module='auto', - data_dir=None, lock_dir=None, - **kw): - NamespaceManager.__init__(self, namespace) - - _memcache_module = _client_libs[memcache_module] - - if not url: - raise MissingCacheParameter("url is required") - - self.lock_dir = None - - if lock_dir: - self.lock_dir = lock_dir - elif data_dir: - self.lock_dir = data_dir + "/container_mcd_lock" - if self.lock_dir: - verify_directory(self.lock_dir) - - # Check for pylibmc namespace manager, in which case client will be - # instantiated by subclass __init__, to handle behavior passing to the - # pylibmc client - if not _is_configured_for_pylibmc(memcache_module, _memcache_module): - self.mc = MemcachedNamespaceManager.clients.get( - (memcache_module, url), - _memcache_module.Client, - url.split(';')) - - def get_creation_lock(self, key): - return file_synchronizer( - identifier="memcachedcontainer/funclock/%s/%s" % - (self.namespace, key), lock_dir=self.lock_dir) - - def _format_key(self, key): - if not isinstance(key, str): - key = key.decode('ascii') - formated_key = (self.namespace + '_' + key).replace(' ', '\302\267') - if len(formated_key) > MAX_KEY_LENGTH: - if not PY2: - formated_key = formated_key.encode('utf-8') - formated_key = sha1(formated_key).hexdigest() - return formated_key - - def __getitem__(self, key): - return self.mc.get(self._format_key(key)) - - def __contains__(self, key): - value = self.mc.get(self._format_key(key)) - return value is not None - - def has_key(self, key): - return key in self - - def set_value(self, key, value, expiretime=None): - if expiretime: - self.mc.set(self._format_key(key), value, time=expiretime) - else: - self.mc.set(self._format_key(key), value) - - def __setitem__(self, key, value): - self.set_value(key, value) - - def __delitem__(self, key): - self.mc.delete(self._format_key(key)) - - def do_remove(self): - self.mc.flush_all() - - def keys(self): - raise NotImplementedError( - "Memcache caching does not " - "support iteration of all cache keys") - - -class PyLibMCNamespaceManager(MemcachedNamespaceManager): - """Provide thread-local support for pylibmc.""" - - pools = SyncDict() - - def __init__(self, *arg, **kw): - super(PyLibMCNamespaceManager, self).__init__(*arg, **kw) - - memcache_module = kw.get('memcache_module', 'auto') - _memcache_module = _client_libs[memcache_module] - protocol = kw.get('protocol', 'text') - username = kw.get('username', None) - password = kw.get('password', None) - url = kw.get('url') - behaviors = parse_memcached_behaviors(kw) - - self.mc = MemcachedNamespaceManager.clients.get( - (memcache_module, url), - _memcache_module.Client, - servers=url.split(';'), behaviors=behaviors, - binary=(protocol == 'binary'), username=username, - password=password) - self.pool = PyLibMCNamespaceManager.pools.get( - (memcache_module, url), - pylibmc.ThreadMappedPool, self.mc) - - def __getitem__(self, key): - with self.pool.reserve() as mc: - return mc.get(self._format_key(key)) - - def __contains__(self, key): - with self.pool.reserve() as mc: - value = mc.get(self._format_key(key)) - return value is not None - - def has_key(self, key): - return key in self - - def set_value(self, key, value, expiretime=None): - with self.pool.reserve() as mc: - if expiretime: - mc.set(self._format_key(key), value, time=expiretime) - else: - mc.set(self._format_key(key), value) - - def __setitem__(self, key, value): - self.set_value(key, value) - - def __delitem__(self, key): - with self.pool.reserve() as mc: - mc.delete(self._format_key(key)) - - def do_remove(self): - with self.pool.reserve() as mc: - mc.flush_all() - - -class MemcachedContainer(Container): - """Container class which invokes :class:`.MemcacheNamespaceManager`.""" - namespace_class = MemcachedNamespaceManager diff --git a/libs/beaker/ext/mongodb.py b/libs/beaker/ext/mongodb.py deleted file mode 100644 index 64c54c30f..000000000 --- a/libs/beaker/ext/mongodb.py +++ /dev/null @@ -1,184 +0,0 @@ -import datetime -import os -import threading -import time -import pickle - -try: - import pymongo - import pymongo.errors - import bson -except ImportError: - pymongo = None - bson = None - -from beaker.container import NamespaceManager -from beaker.synchronization import SynchronizerImpl -from beaker.util import SyncDict, machine_identifier -from beaker.crypto.util import sha1 -from beaker._compat import string_type, PY2 - - -class MongoNamespaceManager(NamespaceManager): - """Provides the :class:`.NamespaceManager` API over MongoDB. - - Provided ``url`` can be both a mongodb connection string or - an already existing MongoClient instance. - - The data will be stored into ``beaker_cache`` collection of the - *default database*, so make sure your connection string or - MongoClient point to a default database. - """ - MAX_KEY_LENGTH = 1024 - - clients = SyncDict() - - def __init__(self, namespace, url, **kw): - super(MongoNamespaceManager, self).__init__(namespace) - self.lock_dir = None # MongoDB uses mongo itself for locking. - - if pymongo is None: - raise RuntimeError('pymongo3 is not available') - - if isinstance(url, string_type): - self.client = MongoNamespaceManager.clients.get(url, pymongo.MongoClient, url) - else: - self.client = url - self.db = self.client.get_default_database() - - def _format_key(self, key): - if not isinstance(key, str): - key = key.decode('ascii') - if len(key) > (self.MAX_KEY_LENGTH - len(self.namespace) - 1): - if not PY2: - key = key.encode('utf-8') - key = sha1(key).hexdigest() - return '%s:%s' % (self.namespace, key) - - def get_creation_lock(self, key): - return MongoSynchronizer(self._format_key(key), self.client) - - def __getitem__(self, key): - self._clear_expired() - entry = self.db.backer_cache.find_one({'_id': self._format_key(key)}) - if entry is None: - raise KeyError(key) - return pickle.loads(entry['value']) - - def __contains__(self, key): - self._clear_expired() - entry = self.db.backer_cache.find_one({'_id': self._format_key(key)}) - return entry is not None - - def has_key(self, key): - return key in self - - def set_value(self, key, value, expiretime=None): - self._clear_expired() - - expiration = None - if expiretime is not None: - expiration = time.time() + expiretime - - value = pickle.dumps(value) - self.db.backer_cache.update_one({'_id': self._format_key(key)}, - {'$set': {'value': bson.Binary(value), - 'expiration': expiration}}, - upsert=True) - - def __setitem__(self, key, value): - self.set_value(key, value) - - def __delitem__(self, key): - self._clear_expired() - self.db.backer_cache.delete_many({'_id': self._format_key(key)}) - - def do_remove(self): - self.db.backer_cache.delete_many({'_id': {'$regex': '^%s' % self.namespace}}) - - def keys(self): - return [e['key'].split(':', 1)[-1] for e in self.db.backer_cache.find_all( - {'_id': {'$regex': '^%s' % self.namespace}} - )] - - def _clear_expired(self): - now = time.time() - self.db.backer_cache.delete_many({'_id': {'$regex': '^%s' % self.namespace}, - 'expiration': {'$ne': None, '$lte': now}}) - - -class MongoSynchronizer(SynchronizerImpl): - """Provides a Writer/Reader lock based on MongoDB. - - Provided ``url`` can be both a mongodb connection string or - an already existing MongoClient instance. - - The data will be stored into ``beaker_locks`` collection of the - *default database*, so make sure your connection string or - MongoClient point to a default database. - - Locks are identified by local machine, PID and threadid, so - are suitable for use in both local and distributed environments. - """ - # If a cache entry generation function can take a lot, - # but 15 minutes is more than a reasonable time. - LOCK_EXPIRATION = 900 - MACHINE_ID = machine_identifier() - - def __init__(self, identifier, url): - super(MongoSynchronizer, self).__init__() - self.identifier = identifier - if isinstance(url, string_type): - self.client = MongoNamespaceManager.clients.get(url, pymongo.MongoClient, url) - else: - self.client = url - self.db = self.client.get_default_database() - - def _clear_expired_locks(self): - now = datetime.datetime.utcnow() - expired = now - datetime.timedelta(seconds=self.LOCK_EXPIRATION) - self.db.beaker_locks.delete_many({'_id': self.identifier, 'timestamp': {'$lte': expired}}) - return now - - def _get_owner_id(self): - return '%s-%s-%s' % (self.MACHINE_ID, os.getpid(), threading.current_thread().ident) - - def do_release_read_lock(self): - owner_id = self._get_owner_id() - self.db.beaker_locks.update_one({'_id': self.identifier, 'readers': owner_id}, - {'$pull': {'readers': owner_id}}) - - def do_acquire_read_lock(self, wait): - now = self._clear_expired_locks() - owner_id = self._get_owner_id() - while True: - try: - self.db.beaker_locks.update_one({'_id': self.identifier, 'owner': None}, - {'$set': {'timestamp': now}, - '$push': {'readers': owner_id}}, - upsert=True) - return True - except pymongo.errors.DuplicateKeyError: - if not wait: - return False - time.sleep(0.2) - - def do_release_write_lock(self): - self.db.beaker_locks.delete_one({'_id': self.identifier, 'owner': self._get_owner_id()}) - - def do_acquire_write_lock(self, wait): - now = self._clear_expired_locks() - owner_id = self._get_owner_id() - while True: - try: - self.db.beaker_locks.update_one({'_id': self.identifier, 'owner': None, - 'readers': []}, - {'$set': {'owner': owner_id, - 'timestamp': now}}, - upsert=True) - return True - except pymongo.errors.DuplicateKeyError: - if not wait: - return False - time.sleep(0.2) - diff --git a/libs/beaker/ext/redisnm.py b/libs/beaker/ext/redisnm.py deleted file mode 100644 index 113ab7b80..000000000 --- a/libs/beaker/ext/redisnm.py +++ /dev/null @@ -1,144 +0,0 @@ -import os -import threading -import time -import pickle - -try: - import redis -except ImportError: - redis = None - -from beaker.container import NamespaceManager -from beaker.synchronization import SynchronizerImpl -from beaker.util import SyncDict, machine_identifier -from beaker.crypto.util import sha1 -from beaker._compat import string_type, PY2 - - -class RedisNamespaceManager(NamespaceManager): - """Provides the :class:`.NamespaceManager` API over Redis. - - Provided ``url`` can be both a redis connection string or - an already existing StrictRedis instance. - - The data will be stored into redis keys, with their name - starting with ``beaker_cache:``. So make sure you provide - a specific database number if you don't want to mix them - with your own data. - """ - MAX_KEY_LENGTH = 1024 - - clients = SyncDict() - - def __init__(self, namespace, url, timeout=None, **kw): - super(RedisNamespaceManager, self).__init__(namespace) - self.lock_dir = None # Redis uses redis itself for locking. - self.timeout = timeout - - if redis is None: - raise RuntimeError('redis is not available') - - if isinstance(url, string_type): - self.client = RedisNamespaceManager.clients.get(url, redis.StrictRedis.from_url, url) - else: - self.client = url - - def _format_key(self, key): - if not isinstance(key, str): - key = key.decode('ascii') - if len(key) > (self.MAX_KEY_LENGTH - len(self.namespace) - len('beaker_cache:') - 1): - if not PY2: - key = key.encode('utf-8') - key = sha1(key).hexdigest() - return 'beaker_cache:%s:%s' % (self.namespace, key) - - def get_creation_lock(self, key): - return RedisSynchronizer(self._format_key(key), self.client) - - def __getitem__(self, key): - entry = self.client.get(self._format_key(key)) - if entry is None: - raise KeyError(key) - return pickle.loads(entry) - - def __contains__(self, key): - return self.client.exists(self._format_key(key)) - - def has_key(self, key): - return key in self - - def set_value(self, key, value, expiretime=None): - value = pickle.dumps(value) - if expiretime is None and self.timeout is not None: - expiretime = self.timeout - if expiretime is not None: - self.client.setex(self._format_key(key), int(expiretime), value) - else: - self.client.set(self._format_key(key), value) - - def __setitem__(self, key, value): - self.set_value(key, value) - - def __delitem__(self, key): - self.client.delete(self._format_key(key)) - - def do_remove(self): - for k in self.keys(): - self.client.delete(k) - - def keys(self): - return self.client.keys('beaker_cache:%s:*' % self.namespace) - - -class RedisSynchronizer(SynchronizerImpl): - """Synchronizer based on redis. - - Provided ``url`` can be both a redis connection string or - an already existing StrictRedis instance. - - This Synchronizer only supports 1 reader or 1 writer at time, not concurrent readers. - """ - # If a cache entry generation function can take a lot, - # but 15 minutes is more than a reasonable time. - LOCK_EXPIRATION = 900 - MACHINE_ID = machine_identifier() - - def __init__(self, identifier, url): - super(RedisSynchronizer, self).__init__() - self.identifier = 'beaker_lock:%s' % identifier - if isinstance(url, string_type): - self.client = RedisNamespaceManager.clients.get(url, redis.StrictRedis.from_url, url) - else: - self.client = url - - def _get_owner_id(self): - return ( - '%s-%s-%s' % (self.MACHINE_ID, os.getpid(), threading.current_thread().ident) - ).encode('ascii') - - def do_release_read_lock(self): - self.do_release_write_lock() - - def do_acquire_read_lock(self, wait): - self.do_acquire_write_lock(wait) - - def do_release_write_lock(self): - identifier = self.identifier - owner_id = self._get_owner_id() - def execute_release(pipe): - lock_value = pipe.get(identifier) - if lock_value == owner_id: - pipe.delete(identifier) - self.client.transaction(execute_release, identifier) - - def do_acquire_write_lock(self, wait): - owner_id = self._get_owner_id() - while True: - if self.client.setnx(self.identifier, owner_id): - self.client.pexpire(self.identifier, self.LOCK_EXPIRATION * 1000) - return True - - if not wait: - return False - time.sleep(0.2) - diff --git a/libs/beaker/ext/sqla.py b/libs/beaker/ext/sqla.py deleted file mode 100644 index 2c060728d..000000000 --- a/libs/beaker/ext/sqla.py +++ /dev/null @@ -1,137 +0,0 @@ -from beaker._compat import pickle - -import logging -import pickle -from datetime import datetime - -from beaker.container import OpenResourceNamespaceManager, Container -from beaker.exceptions import InvalidCacheBackendError, MissingCacheParameter -from beaker.synchronization import file_synchronizer, null_synchronizer -from beaker.util import verify_directory, SyncDict - - -log = logging.getLogger(__name__) - -sa = None - - -class SqlaNamespaceManager(OpenResourceNamespaceManager): - binds = SyncDict() - tables = SyncDict() - - @classmethod - def _init_dependencies(cls): - global sa - if sa is not None: - return - try: - import sqlalchemy as sa - except ImportError: - raise InvalidCacheBackendError("SQLAlchemy, which is required by " - "this backend, is not installed") - - def __init__(self, namespace, bind, table, data_dir=None, lock_dir=None, - **kwargs): - """Create a namespace manager for use with a database table via - SQLAlchemy. - - ``bind`` - SQLAlchemy ``Engine`` or ``Connection`` object - - ``table`` - SQLAlchemy ``Table`` object in which to store namespace data. - This should usually be something created by ``make_cache_table``. - """ - OpenResourceNamespaceManager.__init__(self, namespace) - - if lock_dir: - self.lock_dir = lock_dir - elif data_dir: - self.lock_dir = data_dir + "/container_db_lock" - if self.lock_dir: - verify_directory(self.lock_dir) - - self.bind = self.__class__.binds.get(str(bind.url), lambda: bind) - self.table = self.__class__.tables.get('%s:%s' % (bind.url, table.name), - lambda: table) - self.hash = {} - self._is_new = False - self.loaded = False - - def get_access_lock(self): - return null_synchronizer() - - def get_creation_lock(self, key): - return file_synchronizer( - identifier="databasecontainer/funclock/%s" % self.namespace, - lock_dir=self.lock_dir) - - def do_open(self, flags, replace): - if self.loaded: - self.flags = flags - return - select = sa.select([self.table.c.data], - (self.table.c.namespace == self.namespace)) - result = self.bind.execute(select).fetchone() - if not result: - self._is_new = True - self.hash = {} - else: - self._is_new = False - try: - self.hash = result['data'] - except (IOError, OSError, EOFError, pickle.PickleError, - pickle.PickleError): - log.debug("Couln't load pickle data, creating new storage") - self.hash = {} - self._is_new = True - self.flags = flags - self.loaded = True - - def do_close(self): - if self.flags is not None and (self.flags == 'c' or self.flags == 'w'): - if self._is_new: - insert = self.table.insert() - self.bind.execute(insert, namespace=self.namespace, data=self.hash, - accessed=datetime.now(), created=datetime.now()) - self._is_new = False - else: - update = self.table.update(self.table.c.namespace == self.namespace) - self.bind.execute(update, data=self.hash, accessed=datetime.now()) - self.flags = None - - def do_remove(self): - delete = self.table.delete(self.table.c.namespace == self.namespace) - self.bind.execute(delete) - self.hash = {} - self._is_new = True - - def __getitem__(self, key): - return self.hash[key] - - def __contains__(self, key): - return key in self.hash - - def __setitem__(self, key, value): - self.hash[key] = value - - def __delitem__(self, key): - del self.hash[key] - - def keys(self): - return self.hash.keys() - - -class SqlaContainer(Container): - namespace_manager = SqlaNamespaceManager - - -def make_cache_table(metadata, table_name='beaker_cache', schema_name=None): - """Return a ``Table`` object suitable for storing cached values for the - namespace manager. Do not create the table.""" - return sa.Table(table_name, metadata, - sa.Column('namespace', sa.String(255), primary_key=True), - sa.Column('accessed', sa.DateTime, nullable=False), - sa.Column('created', sa.DateTime, nullable=False), - sa.Column('data', sa.PickleType, nullable=False), - schema=schema_name if schema_name else metadata.schema) diff --git a/libs/beaker/middleware.py b/libs/beaker/middleware.py deleted file mode 100644 index 319f2b767..000000000 --- a/libs/beaker/middleware.py +++ /dev/null @@ -1,169 +0,0 @@ -import warnings - -try: - from paste.registry import StackedObjectProxy - beaker_session = StackedObjectProxy(name="Beaker Session") - beaker_cache = StackedObjectProxy(name="Cache Manager") -except: - beaker_cache = None - beaker_session = None - -from beaker.cache import CacheManager -from beaker.session import Session, SessionObject -from beaker.util import coerce_cache_params, coerce_session_params, \ - parse_cache_config_options - - -class CacheMiddleware(object): - cache = beaker_cache - - def __init__(self, app, config=None, environ_key='beaker.cache', **kwargs): - """Initialize the Cache Middleware - - The Cache middleware will make a CacheManager instance available - every request under the ``environ['beaker.cache']`` key by - default. The location in environ can be changed by setting - ``environ_key``. - - ``config`` - dict All settings should be prefixed by 'cache.'. This - method of passing variables is intended for Paste and other - setups that accumulate multiple component settings in a - single dictionary. If config contains *no cache. prefixed - args*, then *all* of the config options will be used to - intialize the Cache objects. - - ``environ_key`` - Location where the Cache instance will keyed in the WSGI - environ - - ``**kwargs`` - All keyword arguments are assumed to be cache settings and - will override any settings found in ``config`` - - """ - self.app = app - config = config or {} - - self.options = {} - - # Update the options with the parsed config - self.options.update(parse_cache_config_options(config)) - - # Add any options from kwargs, but leave out the defaults this - # time - self.options.update( - parse_cache_config_options(kwargs, include_defaults=False)) - - # Assume all keys are intended for cache if none are prefixed with - # 'cache.' - if not self.options and config: - self.options = config - - self.options.update(kwargs) - self.cache_manager = CacheManager(**self.options) - self.environ_key = environ_key - - def __call__(self, environ, start_response): - if environ.get('paste.registry'): - if environ['paste.registry'].reglist: - environ['paste.registry'].register(self.cache, - self.cache_manager) - environ[self.environ_key] = self.cache_manager - return self.app(environ, start_response) - - -class SessionMiddleware(object): - session = beaker_session - - def __init__(self, wrap_app, config=None, environ_key='beaker.session', - **kwargs): - """Initialize the Session Middleware - - The Session middleware will make a lazy session instance - available every request under the ``environ['beaker.session']`` - key by default. The location in environ can be changed by - setting ``environ_key``. - - ``config`` - dict All settings should be prefixed by 'session.'. This - method of passing variables is intended for Paste and other - setups that accumulate multiple component settings in a - single dictionary. If config contains *no session. prefixed - args*, then *all* of the config options will be used to - intialize the Session objects. - - ``environ_key`` - Location where the Session instance will keyed in the WSGI - environ - - ``**kwargs`` - All keyword arguments are assumed to be session settings and - will override any settings found in ``config`` - - """ - config = config or {} - - # Load up the default params - self.options = dict(invalidate_corrupt=True, type=None, - data_dir=None, key='beaker.session.id', - timeout=None, save_accessed_time=True, secret=None, - log_file=None) - - # Pull out any config args meant for beaker session. if there are any - for dct in [config, kwargs]: - for key, val in dct.items(): - if key.startswith('beaker.session.'): - self.options[key[15:]] = val - if key.startswith('session.'): - self.options[key[8:]] = val - if key.startswith('session_'): - warnings.warn('Session options should start with session. ' - 'instead of session_.', DeprecationWarning, 2) - self.options[key[8:]] = val - - # Coerce and validate session params - coerce_session_params(self.options) - - # Assume all keys are intended for session if none are prefixed with - # 'session.' - if not self.options and config: - self.options = config - - self.options.update(kwargs) - self.wrap_app = self.app = wrap_app - self.environ_key = environ_key - - def __call__(self, environ, start_response): - session = SessionObject(environ, **self.options) - if environ.get('paste.registry'): - if environ['paste.registry'].reglist: - environ['paste.registry'].register(self.session, session) - environ[self.environ_key] = session - environ['beaker.get_session'] = self._get_session - - if 'paste.testing_variables' in environ and 'webtest_varname' in self.options: - environ['paste.testing_variables'][self.options['webtest_varname']] = session - - def session_start_response(status, headers, exc_info=None): - if session.accessed(): - session.persist() - if session.__dict__['_headers']['set_cookie']: - cookie = session.__dict__['_headers']['cookie_out'] - if cookie: - headers.append(('Set-cookie', cookie)) - return start_response(status, headers, exc_info) - return self.wrap_app(environ, session_start_response) - - def _get_session(self): - return Session({}, use_cookies=False, **self.options) - - -def session_filter_factory(global_conf, **kwargs): - def filter(app): - return SessionMiddleware(app, global_conf, **kwargs) - return filter - - -def session_filter_app_factory(app, global_conf, **kwargs): - return SessionMiddleware(app, global_conf, **kwargs) diff --git a/libs/beaker/session.py b/libs/beaker/session.py deleted file mode 100644 index 6b536d2f6..000000000 --- a/libs/beaker/session.py +++ /dev/null @@ -1,845 +0,0 @@ -from ._compat import PY2, pickle, http_cookies, unicode_text, b64encode, b64decode, string_type - -import os -import time -from datetime import datetime, timedelta -from beaker.crypto import hmac as HMAC, hmac_sha1 as SHA1, sha1, get_nonce_size, DEFAULT_NONCE_BITS, get_crypto_module -from beaker import crypto, util -from beaker.cache import clsmap -from beaker.exceptions import BeakerException, InvalidCryptoBackendError -from beaker.cookie import SimpleCookie - -__all__ = ['SignedCookie', 'Session', 'InvalidSignature'] - - -class _InvalidSignatureType(object): - """Returned from SignedCookie when the value's signature was invalid.""" - def __nonzero__(self): - return False - - def __bool__(self): - return False - - -InvalidSignature = _InvalidSignatureType() - - -try: - import uuid - - def _session_id(): - return uuid.uuid4().hex -except ImportError: - import random - if hasattr(os, 'getpid'): - getpid = os.getpid - else: - def getpid(): - return '' - - def _session_id(): - id_str = "%f%s%f%s" % ( - time.time(), - id({}), - random.random(), - getpid() - ) - # NB: nothing against second parameter to b64encode, but it seems - # to be slower than simple chained replacement - if not PY2: - raw_id = b64encode(sha1(id_str.encode('ascii')).digest()) - return str(raw_id.replace(b'+', b'-').replace(b'/', b'_').rstrip(b'=')) - else: - raw_id = b64encode(sha1(id_str).digest()) - return raw_id.replace('+', '-').replace('/', '_').rstrip('=') - - -class SignedCookie(SimpleCookie): - """Extends python cookie to give digital signature support""" - def __init__(self, secret, input=None): - self.secret = secret.encode('UTF-8') - http_cookies.BaseCookie.__init__(self, input) - - def value_decode(self, val): - val = val.strip('"') - if not val: - return None, val - - sig = HMAC.new(self.secret, val[40:].encode('utf-8'), SHA1).hexdigest() - - # Avoid timing attacks - invalid_bits = 0 - input_sig = val[:40] - if len(sig) != len(input_sig): - return InvalidSignature, val - - for a, b in zip(sig, input_sig): - invalid_bits += a != b - - if invalid_bits: - return InvalidSignature, val - else: - return val[40:], val - - def value_encode(self, val): - sig = HMAC.new(self.secret, val.encode('utf-8'), SHA1).hexdigest() - return str(val), ("%s%s" % (sig, val)) - - -class Session(dict): - """Session object that uses container package for storage. - - :param invalidate_corrupt: How to handle corrupt data when loading. When - set to True, then corrupt data will be silently - invalidated and a new session created, - otherwise invalid data will cause an exception. - :type invalidate_corrupt: bool - :param use_cookies: Whether or not cookies should be created. When set to - False, it is assumed the user will handle storing the - session on their own. - :type use_cookies: bool - :param type: What data backend type should be used to store the underlying - session data - :param key: The name the cookie should be set to. - :param timeout: How long session data is considered valid. This is used - regardless of the cookie being present or not to determine - whether session data is still valid. Can be set to None to - disable session time out. - :type timeout: int or None - :param save_accessed_time: Whether beaker should save the session's access - time (True) or only modification time (False). - Defaults to True. - :param cookie_expires: Expiration date for cookie - :param cookie_domain: Domain to use for the cookie. - :param cookie_path: Path to use for the cookie. - :param data_serializer: If ``"json"`` or ``"pickle"`` should be used - to serialize data. Can also be an object with - ``loads` and ``dumps`` methods. By default - ``"pickle"`` is used. - :param secure: Whether or not the cookie should only be sent over SSL. - :param httponly: Whether or not the cookie should only be accessible by - the browser not by JavaScript. - :param encrypt_key: The key to use for the local session encryption, if not - provided the session will not be encrypted. - :param validate_key: The key used to sign the local encrypted session - :param encrypt_nonce_bits: Number of bits used to generate nonce for encryption key salt. - For security reason this is 128bits be default. If you want - to keep backward compatibility with sessions generated before 1.8.0 - set this to 48. - :param crypto_type: encryption module to use - :param samesite: SameSite value for the cookie -- should be either 'Lax', - 'Strict', or None. - """ - def __init__(self, request, id=None, invalidate_corrupt=False, - use_cookies=True, type=None, data_dir=None, - key='beaker.session.id', timeout=None, save_accessed_time=True, - cookie_expires=True, cookie_domain=None, cookie_path='/', - data_serializer='pickle', secret=None, - secure=False, namespace_class=None, httponly=False, - encrypt_key=None, validate_key=None, encrypt_nonce_bits=DEFAULT_NONCE_BITS, - crypto_type='default', samesite='Lax', - **namespace_args): - if not type: - if data_dir: - self.type = 'file' - else: - self.type = 'memory' - else: - self.type = type - - self.namespace_class = namespace_class or clsmap[self.type] - - self.namespace_args = namespace_args - - self.request = request - self.data_dir = data_dir - self.key = key - - if timeout and not save_accessed_time: - raise BeakerException("timeout requires save_accessed_time") - self.timeout = timeout - - # If a timeout was provided, forward it to the backend too, so the backend - # can automatically expire entries if it's supported. - if self.timeout is not None: - # The backend expiration should always be a bit longer than the - # session expiration itself to prevent the case where the backend data expires while - # the session is being read (PR#153). 2 Minutes seems a reasonable time. - self.namespace_args['timeout'] = self.timeout + 60 * 2 - - self.save_atime = save_accessed_time - self.use_cookies = use_cookies - self.cookie_expires = cookie_expires - - self._set_serializer(data_serializer) - - # Default cookie domain/path - self._domain = cookie_domain - self._path = cookie_path - self.was_invalidated = False - self.secret = secret - self.secure = secure - self.httponly = httponly - self.samesite = samesite - self.encrypt_key = encrypt_key - self.validate_key = validate_key - self.encrypt_nonce_size = get_nonce_size(encrypt_nonce_bits) - self.crypto_module = get_crypto_module(crypto_type) - self.id = id - self.accessed_dict = {} - self.invalidate_corrupt = invalidate_corrupt - - if self.use_cookies: - cookieheader = request.get('cookie', '') - if secret: - try: - self.cookie = SignedCookie( - secret, - input=cookieheader, - ) - except http_cookies.CookieError: - self.cookie = SignedCookie( - secret, - input=None, - ) - else: - self.cookie = SimpleCookie(input=cookieheader) - - if not self.id and self.key in self.cookie: - cookie_data = self.cookie[self.key].value - # Should we check invalidate_corrupt here? - if cookie_data is InvalidSignature: - cookie_data = None - self.id = cookie_data - - self.is_new = self.id is None - if self.is_new: - self._create_id() - self['_accessed_time'] = self['_creation_time'] = time.time() - else: - try: - self.load() - except Exception as e: - if self.invalidate_corrupt: - util.warn( - "Invalidating corrupt session %s; " - "error was: %s. Set invalidate_corrupt=False " - "to propagate this exception." % (self.id, e)) - self.invalidate() - else: - raise - - def _set_serializer(self, data_serializer): - self.data_serializer = data_serializer - if self.data_serializer == 'json': - self.serializer = util.JsonSerializer() - elif self.data_serializer == 'pickle': - self.serializer = util.PickleSerializer() - elif isinstance(self.data_serializer, string_type): - raise BeakerException('Invalid value for data_serializer: %s' % data_serializer) - else: - self.serializer = data_serializer - - def has_key(self, name): - return name in self - - def _set_cookie_values(self, expires=None): - self.cookie[self.key] = self.id - if self._domain: - self.cookie[self.key]['domain'] = self._domain - if self.secure: - self.cookie[self.key]['secure'] = True - if self.samesite: - self.cookie[self.key]['samesite'] = self.samesite - self._set_cookie_http_only() - self.cookie[self.key]['path'] = self._path - - self._set_cookie_expires(expires) - - def _set_cookie_expires(self, expires): - if expires is None: - expires = self.cookie_expires - if expires is False: - expires_date = datetime.fromtimestamp(0x7FFFFFFF) - elif isinstance(expires, timedelta): - expires_date = datetime.utcnow() + expires - elif isinstance(expires, datetime): - expires_date = expires - elif expires is not True: - raise ValueError("Invalid argument for cookie_expires: %s" - % repr(self.cookie_expires)) - self.cookie_expires = expires - if not self.cookie or self.key not in self.cookie: - self.cookie[self.key] = self.id - if expires is True: - self.cookie[self.key]['expires'] = '' - return True - self.cookie[self.key]['expires'] = \ - expires_date.strftime("%a, %d-%b-%Y %H:%M:%S GMT") - return expires_date - - def _update_cookie_out(self, set_cookie=True): - self._set_cookie_values() - self.request['cookie_out'] = self.cookie[self.key].output(header='') - self.request['set_cookie'] = set_cookie - - def _set_cookie_http_only(self): - try: - if self.httponly: - self.cookie[self.key]['httponly'] = True - except http_cookies.CookieError as e: - if 'Invalid Attribute httponly' not in str(e): - raise - util.warn('Python 2.6+ is required to use httponly') - - def _create_id(self, set_new=True): - self.id = _session_id() - - if set_new: - self.is_new = True - self.last_accessed = None - if self.use_cookies: - sc = set_new is False - self._update_cookie_out(set_cookie=sc) - - @property - def created(self): - return self['_creation_time'] - - def _set_domain(self, domain): - self['_domain'] = self._domain = domain - self._update_cookie_out() - - def _get_domain(self): - return self._domain - - domain = property(_get_domain, _set_domain) - - def _set_path(self, path): - self['_path'] = self._path = path - self._update_cookie_out() - - def _get_path(self): - return self._path - - path = property(_get_path, _set_path) - - def _encrypt_data(self, session_data=None): - """Serialize, encipher, and base64 the session dict""" - session_data = session_data or self.copy() - if self.encrypt_key: - nonce_len, nonce_b64len = self.encrypt_nonce_size - nonce = b64encode(os.urandom(nonce_len))[:nonce_b64len] - encrypt_key = crypto.generateCryptoKeys(self.encrypt_key, - self.validate_key + nonce, - 1, - self.crypto_module.getKeyLength()) - data = self.serializer.dumps(session_data) - return nonce + b64encode(self.crypto_module.aesEncrypt(data, encrypt_key)) - else: - data = self.serializer.dumps(session_data) - return b64encode(data) - - def _decrypt_data(self, session_data): - """Base64, decipher, then un-serialize the data for the session - dict""" - if self.encrypt_key: - __, nonce_b64len = self.encrypt_nonce_size - nonce = session_data[:nonce_b64len] - encrypt_key = crypto.generateCryptoKeys(self.encrypt_key, - self.validate_key + nonce, - 1, - self.crypto_module.getKeyLength()) - payload = b64decode(session_data[nonce_b64len:]) - data = self.crypto_module.aesDecrypt(payload, encrypt_key) - else: - data = b64decode(session_data) - - return self.serializer.loads(data) - - def _delete_cookie(self): - self.request['set_cookie'] = True - expires = datetime.utcnow() - timedelta(365) - self._set_cookie_values(expires) - self._update_cookie_out() - - def delete(self): - """Deletes the session from the persistent storage, and sends - an expired cookie out""" - if self.use_cookies: - self._delete_cookie() - self.clear() - - def invalidate(self): - """Invalidates this session, creates a new session id, returns - to the is_new state""" - self.clear() - self.was_invalidated = True - self._create_id() - self.load() - - def load(self): - "Loads the data from this session from persistent storage" - self.namespace = self.namespace_class(self.id, - data_dir=self.data_dir, - digest_filenames=False, - **self.namespace_args) - now = time.time() - if self.use_cookies: - self.request['set_cookie'] = True - - self.namespace.acquire_read_lock() - timed_out = False - try: - self.clear() - try: - session_data = self.namespace['session'] - - if (session_data is not None and self.encrypt_key): - session_data = self._decrypt_data(session_data) - - # Memcached always returns a key, its None when its not - # present - if session_data is None: - session_data = { - '_creation_time': now, - '_accessed_time': now - } - self.is_new = True - except (KeyError, TypeError): - session_data = { - '_creation_time': now, - '_accessed_time': now - } - self.is_new = True - - if session_data is None or len(session_data) == 0: - session_data = { - '_creation_time': now, - '_accessed_time': now - } - self.is_new = True - - if self.timeout is not None and \ - now - session_data['_accessed_time'] > self.timeout: - timed_out = True - else: - # Properly set the last_accessed time, which is different - # than the *currently* _accessed_time - if self.is_new or '_accessed_time' not in session_data: - self.last_accessed = None - else: - self.last_accessed = session_data['_accessed_time'] - - # Update the current _accessed_time - session_data['_accessed_time'] = now - - # Set the path if applicable - if '_path' in session_data: - self._path = session_data['_path'] - self.update(session_data) - self.accessed_dict = session_data.copy() - finally: - self.namespace.release_read_lock() - if timed_out: - self.invalidate() - - def save(self, accessed_only=False): - """Saves the data for this session to persistent storage - - If accessed_only is True, then only the original data loaded - at the beginning of the request will be saved, with the updated - last accessed time. - - """ - # Look to see if its a new session that was only accessed - # Don't save it under that case - if accessed_only and (self.is_new or not self.save_atime): - return None - - # this session might not have a namespace yet or the session id - # might have been regenerated - if not hasattr(self, 'namespace') or self.namespace.namespace != self.id: - self.namespace = self.namespace_class( - self.id, - data_dir=self.data_dir, - digest_filenames=False, - **self.namespace_args) - - self.namespace.acquire_write_lock(replace=True) - try: - if accessed_only: - data = dict(self.accessed_dict.items()) - else: - data = dict(self.items()) - - if self.encrypt_key: - data = self._encrypt_data(data) - - # Save the data - if not data and 'session' in self.namespace: - del self.namespace['session'] - else: - self.namespace['session'] = data - finally: - self.namespace.release_write_lock() - if self.use_cookies and self.is_new: - self.request['set_cookie'] = True - - def revert(self): - """Revert the session to its original state from its first - access in the request""" - self.clear() - self.update(self.accessed_dict) - - def regenerate_id(self): - """ - creates a new session id, retains all session data - - Its a good security practice to regnerate the id after a client - elevates privileges. - - """ - self._create_id(set_new=False) - - # TODO: I think both these methods should be removed. They're from - # the original mod_python code i was ripping off but they really - # have no use here. - def lock(self): - """Locks this session against other processes/threads. This is - automatic when load/save is called. - - ***use with caution*** and always with a corresponding 'unlock' - inside a "finally:" block, as a stray lock typically cannot be - unlocked without shutting down the whole application. - - """ - self.namespace.acquire_write_lock() - - def unlock(self): - """Unlocks this session against other processes/threads. This - is automatic when load/save is called. - - ***use with caution*** and always within a "finally:" block, as - a stray lock typically cannot be unlocked without shutting down - the whole application. - - """ - self.namespace.release_write_lock() - - -class CookieSession(Session): - """Pure cookie-based session - - Options recognized when using cookie-based sessions are slightly - more restricted than general sessions. - - :param key: The name the cookie should be set to. - :param timeout: How long session data is considered valid. This is used - regardless of the cookie being present or not to determine - whether session data is still valid. - :type timeout: int - :param save_accessed_time: Whether beaker should save the session's access - time (True) or only modification time (False). - Defaults to True. - :param cookie_expires: Expiration date for cookie - :param cookie_domain: Domain to use for the cookie. - :param cookie_path: Path to use for the cookie. - :param data_serializer: If ``"json"`` or ``"pickle"`` should be used - to serialize data. Can also be an object with - ``loads` and ``dumps`` methods. By default - ``"pickle"`` is used. - :param secure: Whether or not the cookie should only be sent over SSL. - :param httponly: Whether or not the cookie should only be accessible by - the browser not by JavaScript. - :param encrypt_key: The key to use for the local session encryption, if not - provided the session will not be encrypted. - :param validate_key: The key used to sign the local encrypted session - :param invalidate_corrupt: How to handle corrupt data when loading. When - set to True, then corrupt data will be silently - invalidated and a new session created, - otherwise invalid data will cause an exception. - :type invalidate_corrupt: bool - :param crypto_type: The crypto module to use. - :param samesite: SameSite value for the cookie -- should be either 'Lax', - 'Strict', or None. - """ - def __init__(self, request, key='beaker.session.id', timeout=None, - save_accessed_time=True, cookie_expires=True, cookie_domain=None, - cookie_path='/', encrypt_key=None, validate_key=None, secure=False, - httponly=False, data_serializer='pickle', - encrypt_nonce_bits=DEFAULT_NONCE_BITS, invalidate_corrupt=False, - crypto_type='default', samesite='Lax', - **kwargs): - - self.crypto_module = get_crypto_module(crypto_type) - - if encrypt_key and not self.crypto_module.has_aes: - raise InvalidCryptoBackendError("No AES library is installed, can't generate " - "encrypted cookie-only Session.") - - self.request = request - self.key = key - self.timeout = timeout - self.save_atime = save_accessed_time - self.cookie_expires = cookie_expires - self.encrypt_key = encrypt_key - self.validate_key = validate_key - self.encrypt_nonce_size = get_nonce_size(encrypt_nonce_bits) - self.request['set_cookie'] = False - self.secure = secure - self.httponly = httponly - self.samesite = samesite - self._domain = cookie_domain - self._path = cookie_path - self.invalidate_corrupt = invalidate_corrupt - self._set_serializer(data_serializer) - - try: - cookieheader = request['cookie'] - except KeyError: - cookieheader = '' - - if validate_key is None: - raise BeakerException("No validate_key specified for Cookie only " - "Session.") - if timeout and not save_accessed_time: - raise BeakerException("timeout requires save_accessed_time") - - try: - self.cookie = SignedCookie( - validate_key, - input=cookieheader, - ) - except http_cookies.CookieError: - self.cookie = SignedCookie( - validate_key, - input=None, - ) - - self['_id'] = _session_id() - self.is_new = True - - # If we have a cookie, load it - if self.key in self.cookie and self.cookie[self.key].value is not None: - self.is_new = False - try: - cookie_data = self.cookie[self.key].value - if cookie_data is InvalidSignature: - raise BeakerException("Invalid signature") - self.update(self._decrypt_data(cookie_data)) - self._path = self.get('_path', '/') - except Exception as e: - if self.invalidate_corrupt: - util.warn( - "Invalidating corrupt session %s; " - "error was: %s. Set invalidate_corrupt=False " - "to propagate this exception." % (self.id, e)) - self.invalidate() - else: - raise - - if self.timeout is not None: - now = time.time() - last_accessed_time = self.get('_accessed_time', now) - if now - last_accessed_time > self.timeout: - self.clear() - - self.accessed_dict = self.copy() - self._create_cookie() - - def created(self): - return self['_creation_time'] - created = property(created) - - def id(self): - return self['_id'] - id = property(id) - - def _set_domain(self, domain): - self['_domain'] = domain - self._domain = domain - - def _get_domain(self): - return self._domain - - domain = property(_get_domain, _set_domain) - - def _set_path(self, path): - self['_path'] = self._path = path - - def _get_path(self): - return self._path - - path = property(_get_path, _set_path) - - def save(self, accessed_only=False): - """Saves the data for this session to persistent storage""" - if accessed_only and (self.is_new or not self.save_atime): - return - if accessed_only: - self.clear() - self.update(self.accessed_dict) - self._create_cookie() - - def expire(self): - """Delete the 'expires' attribute on this Session, if any.""" - - self.pop('_expires', None) - - def _create_cookie(self): - if '_creation_time' not in self: - self['_creation_time'] = time.time() - if '_id' not in self: - self['_id'] = _session_id() - self['_accessed_time'] = time.time() - - val = self._encrypt_data() - if len(val) > 4064: - raise BeakerException("Cookie value is too long to store") - - self.cookie[self.key] = val - - if '_expires' in self: - expires = self['_expires'] - else: - expires = None - expires = self._set_cookie_expires(expires) - if expires is not None: - self['_expires'] = expires - - if '_domain' in self: - self.cookie[self.key]['domain'] = self['_domain'] - elif self._domain: - self.cookie[self.key]['domain'] = self._domain - if self.secure: - self.cookie[self.key]['secure'] = True - self._set_cookie_http_only() - - self.cookie[self.key]['path'] = self.get('_path', '/') - - self.request['cookie_out'] = self.cookie[self.key].output(header='') - self.request['set_cookie'] = True - - def delete(self): - """Delete the cookie, and clear the session""" - # Send a delete cookie request - self._delete_cookie() - self.clear() - - def invalidate(self): - """Clear the contents and start a new session""" - self.clear() - self['_id'] = _session_id() - - -class SessionObject(object): - """Session proxy/lazy creator - - This object proxies access to the actual session object, so that in - the case that the session hasn't been used before, it will be - setup. This avoid creating and loading the session from persistent - storage unless its actually used during the request. - - """ - def __init__(self, environ, **params): - self.__dict__['_params'] = params - self.__dict__['_environ'] = environ - self.__dict__['_sess'] = None - self.__dict__['_headers'] = {} - - def _session(self): - """Lazy initial creation of session object""" - if self.__dict__['_sess'] is None: - params = self.__dict__['_params'] - environ = self.__dict__['_environ'] - self.__dict__['_headers'] = req = {'cookie_out': None} - req['cookie'] = environ.get('HTTP_COOKIE') - session_cls = params.get('session_class', None) - if session_cls is None: - if params.get('type') == 'cookie': - session_cls = CookieSession - else: - session_cls = Session - else: - assert issubclass(session_cls, Session),\ - "Not a Session: " + session_cls - self.__dict__['_sess'] = session_cls(req, **params) - return self.__dict__['_sess'] - - def __getattr__(self, attr): - return getattr(self._session(), attr) - - def __setattr__(self, attr, value): - setattr(self._session(), attr, value) - - def __delattr__(self, name): - self._session().__delattr__(name) - - def __getitem__(self, key): - return self._session()[key] - - def __setitem__(self, key, value): - self._session()[key] = value - - def __delitem__(self, key): - self._session().__delitem__(key) - - def __repr__(self): - return self._session().__repr__() - - def __iter__(self): - """Only works for proxying to a dict""" - return iter(self._session().keys()) - - def __contains__(self, key): - return key in self._session() - - def has_key(self, key): - return key in self._session() - - def get_by_id(self, id): - """Loads a session given a session ID""" - params = self.__dict__['_params'] - session = Session({}, use_cookies=False, id=id, **params) - if session.is_new: - return None - return session - - def save(self): - self.__dict__['_dirty'] = True - - def delete(self): - self.__dict__['_dirty'] = True - self._session().delete() - - def persist(self): - """Persist the session to the storage - - Always saves the whole session if save() or delete() have been called. - If they haven't: - - - If autosave is set to true, saves the the entire session regardless. - - If save_accessed_time is set to true or unset, only saves the updated - access time. - - If save_accessed_time is set to false, doesn't save anything. - - """ - if self.__dict__['_params'].get('auto'): - self._session().save() - elif self.__dict__['_params'].get('save_accessed_time', True): - if self.dirty(): - self._session().save() - else: - self._session().save(accessed_only=True) - else: # save_accessed_time is false - if self.dirty(): - self._session().save() - - def dirty(self): - """Returns True if save() or delete() have been called""" - return self.__dict__.get('_dirty', False) - - def accessed(self): - """Returns whether or not the session has been accessed""" - return self.__dict__['_sess'] is not None diff --git a/libs/beaker/synchronization.py b/libs/beaker/synchronization.py deleted file mode 100644 index f2e3e9540..000000000 --- a/libs/beaker/synchronization.py +++ /dev/null @@ -1,392 +0,0 @@ -"""Synchronization functions. - -File- and mutex-based mutual exclusion synchronizers are provided, -as well as a name-based mutex which locks within an application -based on a string name. - -""" -import errno -import os -import sys -import tempfile - -try: - import threading as _threading -except ImportError: - import dummy_threading as _threading - -# check for fcntl module -try: - sys.getwindowsversion() - has_flock = False -except: - try: - import fcntl - has_flock = True - except ImportError: - has_flock = False - -from beaker import util -from beaker.exceptions import LockError - -__all__ = ["file_synchronizer", "mutex_synchronizer", "null_synchronizer", - "NameLock", "_threading"] - - -class NameLock(object): - """a proxy for an RLock object that is stored in a name based - registry. - - Multiple threads can get a reference to the same RLock based on the - name alone, and synchronize operations related to that name. - - """ - locks = util.WeakValuedRegistry() - - class NLContainer(object): - def __init__(self, reentrant): - if reentrant: - self.lock = _threading.RLock() - else: - self.lock = _threading.Lock() - - def __call__(self): - return self.lock - - def __init__(self, identifier=None, reentrant=False): - if identifier is None: - self._lock = NameLock.NLContainer(reentrant) - else: - self._lock = NameLock.locks.get(identifier, NameLock.NLContainer, - reentrant) - - def acquire(self, wait=True): - return self._lock().acquire(wait) - - def release(self): - self._lock().release() - - -_synchronizers = util.WeakValuedRegistry() - - -def _synchronizer(identifier, cls, **kwargs): - return _synchronizers.sync_get((identifier, cls), cls, identifier, **kwargs) - - -def file_synchronizer(identifier, **kwargs): - if not has_flock or 'lock_dir' not in kwargs: - return mutex_synchronizer(identifier) - else: - return _synchronizer(identifier, FileSynchronizer, **kwargs) - - -def mutex_synchronizer(identifier, **kwargs): - return _synchronizer(identifier, ConditionSynchronizer, **kwargs) - - -class null_synchronizer(object): - """A 'null' synchronizer, which provides the :class:`.SynchronizerImpl` interface - without any locking. - - """ - def acquire_write_lock(self, wait=True): - return True - - def acquire_read_lock(self): - pass - - def release_write_lock(self): - pass - - def release_read_lock(self): - pass - acquire = acquire_write_lock - release = release_write_lock - - -class SynchronizerImpl(object): - """Base class for a synchronization object that allows - multiple readers, single writers. - - """ - def __init__(self): - self._state = util.ThreadLocal() - - class SyncState(object): - __slots__ = 'reentrantcount', 'writing', 'reading' - - def __init__(self): - self.reentrantcount = 0 - self.writing = False - self.reading = False - - def state(self): - if not self._state.has(): - state = SynchronizerImpl.SyncState() - self._state.put(state) - return state - else: - return self._state.get() - state = property(state) - - def release_read_lock(self): - state = self.state - - if state.writing: - raise LockError("lock is in writing state") - if not state.reading: - raise LockError("lock is not in reading state") - - if state.reentrantcount == 1: - self.do_release_read_lock() - state.reading = False - - state.reentrantcount -= 1 - - def acquire_read_lock(self, wait=True): - state = self.state - - if state.writing: - raise LockError("lock is in writing state") - - if state.reentrantcount == 0: - x = self.do_acquire_read_lock(wait) - if (wait or x): - state.reentrantcount += 1 - state.reading = True - return x - elif state.reading: - state.reentrantcount += 1 - return True - - def release_write_lock(self): - state = self.state - - if state.reading: - raise LockError("lock is in reading state") - if not state.writing: - raise LockError("lock is not in writing state") - - if state.reentrantcount == 1: - self.do_release_write_lock() - state.writing = False - - state.reentrantcount -= 1 - - release = release_write_lock - - def acquire_write_lock(self, wait=True): - state = self.state - - if state.reading: - raise LockError("lock is in reading state") - - if state.reentrantcount == 0: - x = self.do_acquire_write_lock(wait) - if (wait or x): - state.reentrantcount += 1 - state.writing = True - return x - elif state.writing: - state.reentrantcount += 1 - return True - - acquire = acquire_write_lock - - def do_release_read_lock(self): - raise NotImplementedError() - - def do_acquire_read_lock(self, wait): - raise NotImplementedError() - - def do_release_write_lock(self): - raise NotImplementedError() - - def do_acquire_write_lock(self, wait): - raise NotImplementedError() - - -class FileSynchronizer(SynchronizerImpl): - """A synchronizer which locks using flock(). - - """ - def __init__(self, identifier, lock_dir): - super(FileSynchronizer, self).__init__() - self._filedescriptor = util.ThreadLocal() - - if lock_dir is None: - lock_dir = tempfile.gettempdir() - else: - lock_dir = lock_dir - - self.filename = util.encoded_path( - lock_dir, - [identifier], - extension='.lock' - ) - self.lock_dir = os.path.dirname(self.filename) - - def _filedesc(self): - return self._filedescriptor.get() - _filedesc = property(_filedesc) - - def _ensuredir(self): - if not os.path.exists(self.lock_dir): - util.verify_directory(self.lock_dir) - - def _open(self, mode): - filedescriptor = self._filedesc - if filedescriptor is None: - self._ensuredir() - filedescriptor = os.open(self.filename, mode) - self._filedescriptor.put(filedescriptor) - return filedescriptor - - def do_acquire_read_lock(self, wait): - filedescriptor = self._open(os.O_CREAT | os.O_RDONLY) - if not wait: - try: - fcntl.flock(filedescriptor, fcntl.LOCK_SH | fcntl.LOCK_NB) - return True - except IOError: - os.close(filedescriptor) - self._filedescriptor.remove() - return False - else: - fcntl.flock(filedescriptor, fcntl.LOCK_SH) - return True - - def do_acquire_write_lock(self, wait): - filedescriptor = self._open(os.O_CREAT | os.O_WRONLY) - if not wait: - try: - fcntl.flock(filedescriptor, fcntl.LOCK_EX | fcntl.LOCK_NB) - return True - except IOError: - os.close(filedescriptor) - self._filedescriptor.remove() - return False - else: - fcntl.flock(filedescriptor, fcntl.LOCK_EX) - return True - - def do_release_read_lock(self): - self._release_all_locks() - - def do_release_write_lock(self): - self._release_all_locks() - - def _release_all_locks(self): - filedescriptor = self._filedesc - if filedescriptor is not None: - fcntl.flock(filedescriptor, fcntl.LOCK_UN) - os.close(filedescriptor) - self._filedescriptor.remove() - - -class ConditionSynchronizer(SynchronizerImpl): - """a synchronizer using a Condition.""" - - def __init__(self, identifier): - super(ConditionSynchronizer, self).__init__() - - # counts how many asynchronous methods are executing - self.asynch = 0 - - # pointer to thread that is the current sync operation - self.current_sync_operation = None - - # condition object to lock on - self.condition = _threading.Condition(_threading.Lock()) - - def do_acquire_read_lock(self, wait=True): - self.condition.acquire() - try: - # see if a synchronous operation is waiting to start - # or is already running, in which case we wait (or just - # give up and return) - if wait: - while self.current_sync_operation is not None: - self.condition.wait() - else: - if self.current_sync_operation is not None: - return False - - self.asynch += 1 - finally: - self.condition.release() - - if not wait: - return True - - def do_release_read_lock(self): - self.condition.acquire() - try: - self.asynch -= 1 - - # check if we are the last asynchronous reader thread - # out the door. - if self.asynch == 0: - # yes. so if a sync operation is waiting, notifyAll to wake - # it up - if self.current_sync_operation is not None: - self.condition.notifyAll() - elif self.asynch < 0: - raise LockError("Synchronizer error - too many " - "release_read_locks called") - finally: - self.condition.release() - - def do_acquire_write_lock(self, wait=True): - self.condition.acquire() - try: - # here, we are not a synchronous reader, and after returning, - # assuming waiting or immediate availability, we will be. - - if wait: - # if another sync is working, wait - while self.current_sync_operation is not None: - self.condition.wait() - else: - # if another sync is working, - # we dont want to wait, so forget it - if self.current_sync_operation is not None: - return False - - # establish ourselves as the current sync - # this indicates to other read/write operations - # that they should wait until this is None again - self.current_sync_operation = _threading.currentThread() - - # now wait again for asyncs to finish - if self.asynch > 0: - if wait: - # wait - self.condition.wait() - else: - # we dont want to wait, so forget it - self.current_sync_operation = None - return False - finally: - self.condition.release() - - if not wait: - return True - - def do_release_write_lock(self): - self.condition.acquire() - try: - if self.current_sync_operation is not _threading.currentThread(): - raise LockError("Synchronizer error - current thread doesnt " - "have the write lock") - - # reset the current sync operation so - # another can get it - self.current_sync_operation = None - - # tell everyone to get ready - self.condition.notifyAll() - finally: - # everyone go !! - self.condition.release() diff --git a/libs/beaker/util.py b/libs/beaker/util.py deleted file mode 100644 index cb818954d..000000000 --- a/libs/beaker/util.py +++ /dev/null @@ -1,507 +0,0 @@ -"""Beaker utilities""" -import hashlib -import socket - -import binascii - -from ._compat import PY2, string_type, unicode_text, NoneType, dictkeyslist, im_class, im_func, pickle, func_signature, \ - default_im_func - -try: - import threading as _threading -except ImportError: - import dummy_threading as _threading - -from datetime import datetime, timedelta -import os -import re -import string -import types -import weakref -import warnings -import sys -import inspect -import json -import zlib - -from beaker.converters import asbool -from beaker import exceptions -from threading import local as _tlocal - -DEFAULT_CACHE_KEY_LENGTH = 250 - -__all__ = ["ThreadLocal", "WeakValuedRegistry", "SyncDict", "encoded_path", - "verify_directory", - "serialize", "deserialize"] - - -def function_named(fn, name): - """Return a function with a given __name__. - - Will assign to __name__ and return the original function if possible on - the Python implementation, otherwise a new function will be constructed. - - """ - fn.__name__ = name - return fn - - -def skip_if(predicate, reason=None): - """Skip a test if predicate is true.""" - reason = reason or predicate.__name__ - - from nose import SkipTest - - def decorate(fn): - fn_name = fn.__name__ - - def maybe(*args, **kw): - if predicate(): - msg = "'%s' skipped: %s" % ( - fn_name, reason) - raise SkipTest(msg) - else: - return fn(*args, **kw) - return function_named(maybe, fn_name) - return decorate - - -def assert_raises(except_cls, callable_, *args, **kw): - """Assert the given exception is raised by the given function + arguments.""" - - try: - callable_(*args, **kw) - success = False - except except_cls: - success = True - - # assert outside the block so it works for AssertionError too ! - assert success, "Callable did not raise an exception" - - -def verify_directory(dir): - """verifies and creates a directory. tries to - ignore collisions with other threads and processes.""" - - tries = 0 - while not os.access(dir, os.F_OK): - try: - tries += 1 - os.makedirs(dir) - except: - if tries > 5: - raise - - -def has_self_arg(func): - """Return True if the given function has a 'self' argument.""" - args = list(func_signature(func).parameters) - if args and args[0] in ('self', 'cls'): - return True - else: - return False - - -def warn(msg, stacklevel=3): - """Issue a warning.""" - if isinstance(msg, string_type): - warnings.warn(msg, exceptions.BeakerWarning, stacklevel=stacklevel) - else: - warnings.warn(msg, stacklevel=stacklevel) - - -def deprecated(message): - def wrapper(fn): - def deprecated_method(*args, **kargs): - warnings.warn(message, DeprecationWarning, 2) - return fn(*args, **kargs) - # TODO: use decorator ? functools.wrapper ? - deprecated_method.__name__ = fn.__name__ - deprecated_method.__doc__ = "%s\n\n%s" % (message, fn.__doc__) - return deprecated_method - return wrapper - - -class ThreadLocal(object): - """stores a value on a per-thread basis""" - - __slots__ = '_tlocal' - - def __init__(self): - self._tlocal = _tlocal() - - def put(self, value): - self._tlocal.value = value - - def has(self): - return hasattr(self._tlocal, 'value') - - def get(self, default=None): - return getattr(self._tlocal, 'value', default) - - def remove(self): - del self._tlocal.value - - -class SyncDict(object): - """ - An efficient/threadsafe singleton map algorithm, a.k.a. - "get a value based on this key, and create if not found or not - valid" paradigm: - - exists && isvalid ? get : create - - Designed to work with weakref dictionaries to expect items - to asynchronously disappear from the dictionary. - - Use python 2.3.3 or greater ! a major bug was just fixed in Nov. - 2003 that was driving me nuts with garbage collection/weakrefs in - this section. - - """ - def __init__(self): - self.mutex = _threading.Lock() - self.dict = {} - - def get(self, key, createfunc, *args, **kwargs): - try: - if key in self.dict: - return self.dict[key] - else: - return self.sync_get(key, createfunc, *args, **kwargs) - except KeyError: - return self.sync_get(key, createfunc, *args, **kwargs) - - def sync_get(self, key, createfunc, *args, **kwargs): - self.mutex.acquire() - try: - try: - if key in self.dict: - return self.dict[key] - else: - return self._create(key, createfunc, *args, **kwargs) - except KeyError: - return self._create(key, createfunc, *args, **kwargs) - finally: - self.mutex.release() - - def _create(self, key, createfunc, *args, **kwargs): - self[key] = obj = createfunc(*args, **kwargs) - return obj - - def has_key(self, key): - return key in self.dict - - def __contains__(self, key): - return self.dict.__contains__(key) - - def __getitem__(self, key): - return self.dict.__getitem__(key) - - def __setitem__(self, key, value): - self.dict.__setitem__(key, value) - - def __delitem__(self, key): - return self.dict.__delitem__(key) - - def clear(self): - self.dict.clear() - - -class WeakValuedRegistry(SyncDict): - def __init__(self): - self.mutex = _threading.RLock() - self.dict = weakref.WeakValueDictionary() - -sha1 = None - - -def encoded_path(root, identifiers, extension=".enc", depth=3, - digest_filenames=True): - - """Generate a unique file-accessible path from the given list of - identifiers starting at the given root directory.""" - ident = "_".join(identifiers) - - global sha1 - if sha1 is None: - from beaker.crypto import sha1 - - if digest_filenames: - if isinstance(ident, unicode_text): - ident = sha1(ident.encode('utf-8')).hexdigest() - else: - ident = sha1(ident).hexdigest() - - ident = os.path.basename(ident) - - tokens = [] - for d in range(1, depth): - tokens.append(ident[0:d]) - - dir = os.path.join(root, *tokens) - verify_directory(dir) - - return os.path.join(dir, ident + extension) - - -def asint(obj): - if isinstance(obj, int): - return obj - elif isinstance(obj, string_type) and re.match(r'^\d+$', obj): - return int(obj) - else: - raise Exception("This is not a proper int") - - -def verify_options(opt, types, error): - if not isinstance(opt, types): - if not isinstance(types, tuple): - types = (types,) - coerced = False - for typ in types: - try: - if typ in (list, tuple): - opt = [x.strip() for x in opt.split(',')] - else: - if typ == bool: - typ = asbool - elif typ == int: - typ = asint - elif typ in (timedelta, datetime): - if not isinstance(opt, typ): - raise Exception("%s requires a timedelta type", typ) - opt = typ(opt) - coerced = True - except: - pass - if coerced: - break - if not coerced: - raise Exception(error) - elif isinstance(opt, str) and not opt.strip(): - raise Exception("Empty strings are invalid for: %s" % error) - return opt - - -def verify_rules(params, ruleset): - for key, types, message in ruleset: - if key in params: - params[key] = verify_options(params[key], types, message) - return params - - -def coerce_session_params(params): - rules = [ - ('data_dir', (str, NoneType), "data_dir must be a string referring to a directory."), - ('lock_dir', (str, NoneType), "lock_dir must be a string referring to a directory."), - ('type', (str, NoneType), "Session type must be a string."), - ('cookie_expires', (bool, datetime, timedelta, int), - "Cookie expires was not a boolean, datetime, int, or timedelta instance."), - ('cookie_domain', (str, NoneType), "Cookie domain must be a string."), - ('cookie_path', (str, NoneType), "Cookie path must be a string."), - ('id', (str,), "Session id must be a string."), - ('key', (str,), "Session key must be a string."), - ('secret', (str, NoneType), "Session secret must be a string."), - ('validate_key', (str, NoneType), "Session encrypt_key must be a string."), - ('encrypt_key', (str, NoneType), "Session validate_key must be a string."), - ('encrypt_nonce_bits', (int, NoneType), "Session encrypt_nonce_bits must be a number"), - ('secure', (bool, NoneType), "Session secure must be a boolean."), - ('httponly', (bool, NoneType), "Session httponly must be a boolean."), - ('timeout', (int, NoneType), "Session timeout must be an integer."), - ('save_accessed_time', (bool, NoneType), - "Session save_accessed_time must be a boolean (defaults to true)."), - ('auto', (bool, NoneType), "Session is created if accessed."), - ('webtest_varname', (str, NoneType), "Session varname must be a string."), - ('data_serializer', (str,), "data_serializer must be a string.") - ] - opts = verify_rules(params, rules) - cookie_expires = opts.get('cookie_expires') - if cookie_expires and isinstance(cookie_expires, int) and \ - not isinstance(cookie_expires, bool): - opts['cookie_expires'] = timedelta(seconds=cookie_expires) - - if opts.get('timeout') is not None and not opts.get('save_accessed_time', True): - raise Exception("save_accessed_time must be true to use timeout") - - return opts - - -def coerce_cache_params(params): - rules = [ - ('data_dir', (str, NoneType), "data_dir must be a string referring to a directory."), - ('lock_dir', (str, NoneType), "lock_dir must be a string referring to a directory."), - ('type', (str,), "Cache type must be a string."), - ('enabled', (bool, NoneType), "enabled must be true/false if present."), - ('expire', (int, NoneType), - "expire must be an integer representing how many seconds the cache is valid for"), - ('regions', (list, tuple, NoneType), - "Regions must be a comma separated list of valid regions"), - ('key_length', (int, NoneType), - "key_length must be an integer which indicates the longest a key can be before hashing"), - ] - return verify_rules(params, rules) - - -def coerce_memcached_behaviors(behaviors): - rules = [ - ('cas', (bool, int), 'cas must be a boolean or an integer'), - ('no_block', (bool, int), 'no_block must be a boolean or an integer'), - ('receive_timeout', (int,), 'receive_timeout must be an integer'), - ('send_timeout', (int,), 'send_timeout must be an integer'), - ('ketama_hash', (str,), - 'ketama_hash must be a string designating a valid hashing strategy option'), - ('_poll_timeout', (int,), '_poll_timeout must be an integer'), - ('auto_eject', (bool, int), 'auto_eject must be an integer'), - ('retry_timeout', (int,), 'retry_timeout must be an integer'), - ('_sort_hosts', (bool, int), '_sort_hosts must be an integer'), - ('_io_msg_watermark', (int,), '_io_msg_watermark must be an integer'), - ('ketama', (bool, int), 'ketama must be a boolean or an integer'), - ('ketama_weighted', (bool, int), 'ketama_weighted must be a boolean or an integer'), - ('_io_key_prefetch', (int, bool), '_io_key_prefetch must be a boolean or an integer'), - ('_hash_with_prefix_key', (bool, int), - '_hash_with_prefix_key must be a boolean or an integer'), - ('tcp_nodelay', (bool, int), 'tcp_nodelay must be a boolean or an integer'), - ('failure_limit', (int,), 'failure_limit must be an integer'), - ('buffer_requests', (bool, int), 'buffer_requests must be a boolean or an integer'), - ('_socket_send_size', (int,), '_socket_send_size must be an integer'), - ('num_replicas', (int,), 'num_replicas must be an integer'), - ('remove_failed', (int,), 'remove_failed must be an integer'), - ('_noreply', (bool, int), '_noreply must be a boolean or an integer'), - ('_io_bytes_watermark', (int,), '_io_bytes_watermark must be an integer'), - ('_socket_recv_size', (int,), '_socket_recv_size must be an integer'), - ('distribution', (str,), - 'distribution must be a string designating a valid distribution option'), - ('connect_timeout', (int,), 'connect_timeout must be an integer'), - ('hash', (str,), 'hash must be a string designating a valid hashing option'), - ('verify_keys', (bool, int), 'verify_keys must be a boolean or an integer'), - ('dead_timeout', (int,), 'dead_timeout must be an integer') - ] - return verify_rules(behaviors, rules) - - -def parse_cache_config_options(config, include_defaults=True): - """Parse configuration options and validate for use with the - CacheManager""" - - # Load default cache options - if include_defaults: - options = dict(type='memory', data_dir=None, expire=None, - log_file=None) - else: - options = {} - for key, val in config.items(): - if key.startswith('beaker.cache.'): - options[key[13:]] = val - if key.startswith('cache.'): - options[key[6:]] = val - coerce_cache_params(options) - - # Set cache to enabled if not turned off - if 'enabled' not in options and include_defaults: - options['enabled'] = True - - # Configure region dict if regions are available - regions = options.pop('regions', None) - if regions: - region_configs = {} - for region in regions: - if not region: # ensure region name is valid - continue - # Setup the default cache options - region_options = dict(data_dir=options.get('data_dir'), - lock_dir=options.get('lock_dir'), - type=options.get('type'), - enabled=options['enabled'], - expire=options.get('expire'), - key_length=options.get('key_length', DEFAULT_CACHE_KEY_LENGTH)) - region_prefix = '%s.' % region - region_len = len(region_prefix) - for key in dictkeyslist(options): - if key.startswith(region_prefix): - region_options[key[region_len:]] = options.pop(key) - coerce_cache_params(region_options) - region_configs[region] = region_options - options['cache_regions'] = region_configs - return options - - -def parse_memcached_behaviors(config): - """Parse behavior options and validate for use with pylibmc - client/PylibMCNamespaceManager, or potentially other memcached - NamespaceManagers that support behaviors""" - behaviors = {} - - for key, val in config.items(): - if key.startswith('behavior.'): - behaviors[key[9:]] = val - - coerce_memcached_behaviors(behaviors) - return behaviors - - -def func_namespace(func): - """Generates a unique namespace for a function""" - kls = None - if hasattr(func, 'im_func') or hasattr(func, '__func__'): - kls = im_class(func) - func = im_func(func) - - if kls: - return '%s.%s' % (kls.__module__, kls.__name__) - else: - return '%s|%s' % (inspect.getsourcefile(func), func.__name__) - - -class PickleSerializer(object): - def loads(self, data_string): - return pickle.loads(data_string) - - def dumps(self, data): - return pickle.dumps(data, 2) - - -class JsonSerializer(object): - def loads(self, data_string): - return json.loads(zlib.decompress(data_string).decode('utf-8')) - - def dumps(self, data): - return zlib.compress(json.dumps(data).encode('utf-8')) - - -def serialize(data, method): - if method == 'json': - serializer = JsonSerializer() - else: - serializer = PickleSerializer() - return serializer.dumps(data) - - -def deserialize(data_string, method): - if method == 'json': - serializer = JsonSerializer() - else: - serializer = PickleSerializer() - return serializer.loads(data_string) - - -def machine_identifier(): - machine_hash = hashlib.md5() - if not PY2: - machine_hash.update(socket.gethostname().encode()) - else: - machine_hash.update(socket.gethostname()) - return binascii.hexlify(machine_hash.digest()[0:3]).decode('ascii') - - -def safe_write (filepath, contents): - if os.name == 'posix': - tempname = '%s.temp' % (filepath) - fh = open(tempname, 'wb') - fh.write(contents) - fh.close() - os.rename(tempname, filepath) - else: - fh = open(filepath, 'wb') - fh.write(contents) - fh.close() diff --git a/libs/bidict/__init__.py b/libs/bidict/__init__.py index 725e18750..e83811052 100644 --- a/libs/bidict/__init__.py +++ b/libs/bidict/__init__.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2009-2019 Joshua Bronson. All Rights Reserved. +# Copyright 2009-2021 Joshua Bronson. All Rights Reserved. # # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this @@ -26,8 +26,9 @@ #============================================================================== -""" -Efficient, Pythonic bidirectional map implementation and related functionality. +"""The bidirectional mapping library for Python. + +bidict by example: .. code-block:: python @@ -44,66 +45,45 @@ https://bidict.readthedocs.io for the most up-to-date documentation if you are reading this elsewhere. -.. :copyright: (c) 2019 Joshua Bronson. +.. :copyright: (c) 2009-2021 Joshua Bronson. .. :license: MPLv2. See LICENSE for details. """ -# This __init__.py only collects functionality implemented in the rest of the -# source and exports it under the `bidict` module namespace (via `__all__`). +# Use private aliases to not re-export these publicly (for Sphinx automodule with imported-members). +from sys import version_info as _version_info -from ._abc import BidirectionalMapping + +if _version_info < (3, 6): # pragma: no cover + raise ImportError('Python 3.6+ is required.') + +from ._abc import BidirectionalMapping, MutableBidirectionalMapping from ._base import BidictBase from ._mut import MutableBidict from ._bidict import bidict -from ._dup import DuplicationPolicy, IGNORE, OVERWRITE, RAISE -from ._exc import ( - BidictException, DuplicationError, - KeyDuplicationError, ValueDuplicationError, KeyAndValueDuplicationError) -from ._util import inverted from ._frozenbidict import frozenbidict from ._frozenordered import FrozenOrderedBidict from ._named import namedbidict from ._orderedbase import OrderedBidictBase from ._orderedbidict import OrderedBidict +from ._dup import ON_DUP_DEFAULT, ON_DUP_RAISE, ON_DUP_DROP_OLD, RAISE, DROP_OLD, DROP_NEW, OnDup, OnDupAction +from ._exc import BidictException, DuplicationError, KeyDuplicationError, ValueDuplicationError, KeyAndValueDuplicationError +from ._iter import inverted from .metadata import ( __author__, __maintainer__, __copyright__, __email__, __credits__, __url__, - __license__, __status__, __description__, __keywords__, __version__, __version_info__) - - -__all__ = ( - '__author__', - '__maintainer__', - '__copyright__', - '__email__', - '__credits__', - '__license__', - '__status__', - '__description__', - '__keywords__', - '__url__', - '__version__', - '__version_info__', - 'BidirectionalMapping', - 'BidictException', - 'DuplicationPolicy', - 'IGNORE', - 'OVERWRITE', - 'RAISE', - 'DuplicationError', - 'KeyDuplicationError', - 'ValueDuplicationError', - 'KeyAndValueDuplicationError', - 'BidictBase', - 'MutableBidict', - 'frozenbidict', - 'bidict', - 'namedbidict', - 'FrozenOrderedBidict', - 'OrderedBidictBase', - 'OrderedBidict', - 'inverted', + __license__, __status__, __description__, __keywords__, __version__, ) +# Set __module__ of re-exported classes to the 'bidict' top-level module name +# so that private/internal submodules are not exposed to users e.g. in repr strings. +_locals = tuple(locals().items()) +for _name, _obj in _locals: # pragma: no cover + if not getattr(_obj, '__module__', '').startswith('bidict.'): + continue + try: + _obj.__module__ = 'bidict' + except AttributeError: # raised when __module__ is read-only (as in OnDup) + pass + # * Code review nav * #============================================================================== diff --git a/libs/bidict/_abc.py b/libs/bidict/_abc.py index 268b00480..2fd130a29 100644 --- a/libs/bidict/_abc.py +++ b/libs/bidict/_abc.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2009-2019 Joshua Bronson. All Rights Reserved. +# Copyright 2009-2021 Joshua Bronson. All Rights Reserved. # # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this @@ -26,12 +26,15 @@ #============================================================================== -"""Provides the :class:`BidirectionalMapping` abstract base class.""" +"""Provide the :class:`BidirectionalMapping` abstract base class.""" -from .compat import Mapping, abstractproperty, iteritems +import typing as _t +from abc import abstractmethod + +from ._typing import KT, VT -class BidirectionalMapping(Mapping): # pylint: disable=abstract-method,no-init +class BidirectionalMapping(_t.Mapping[KT, VT]): """Abstract base class (ABC) for bidirectional mapping types. Extends :class:`collections.abc.Mapping` primarily by adding the @@ -43,8 +46,9 @@ class BidirectionalMapping(Mapping): # pylint: disable=abstract-method,no-init __slots__ = () - @abstractproperty - def inverse(self): + @property + @abstractmethod + def inverse(self) -> 'BidirectionalMapping[VT, KT]': """The inverse of this bidirectional mapping instance. *See also* :attr:`bidict.BidictBase.inverse`, :attr:`bidict.BidictBase.inv` @@ -58,7 +62,7 @@ class BidirectionalMapping(Mapping): # pylint: disable=abstract-method,no-init # clear there's no reason to call this implementation (e.g. via super() after overriding). raise NotImplementedError - def __inverted__(self): + def __inverted__(self) -> _t.Iterator[_t.Tuple[VT, KT]]: """Get an iterator over the items in :attr:`inverse`. This is functionally equivalent to iterating over the items in the @@ -72,7 +76,27 @@ class BidirectionalMapping(Mapping): # pylint: disable=abstract-method,no-init *See also* :func:`bidict.inverted` """ - return iteritems(self.inverse) + return iter(self.inverse.items()) + + def values(self) -> _t.KeysView[VT]: # type: ignore [override] # https://github.com/python/typeshed/issues/4435 + """A set-like object providing a view on the contained values. + + Override the implementation inherited from + :class:`~collections.abc.Mapping`. + Because the values of a :class:`~bidict.BidirectionalMapping` + are the keys of its inverse, + this returns a :class:`~collections.abc.KeysView` + rather than a :class:`~collections.abc.ValuesView`, + which has the advantages of constant-time containment checks + and supporting set operations. + """ + return self.inverse.keys() # type: ignore [return-value] + + +class MutableBidirectionalMapping(BidirectionalMapping[KT, VT], _t.MutableMapping[KT, VT]): + """Abstract base class (ABC) for mutable bidirectional mapping types.""" + + __slots__ = () # * Code review nav * diff --git a/libs/bidict/_base.py b/libs/bidict/_base.py index b2a852df2..f1b40a416 100644 --- a/libs/bidict/_base.py +++ b/libs/bidict/_base.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2009-2019 Joshua Bronson. All Rights Reserved. +# Copyright 2009-2021 Joshua Bronson. All Rights Reserved. # # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this @@ -22,139 +22,118 @@ # * Code review nav * #============================================================================== -# ↠Prev: _abc.py Current: _base.py Next: _delegating_mixins.py → +# ↠Prev: _abc.py Current: _base.py Next: _frozenbidict.py → #============================================================================== -"""Provides :class:`BidictBase`.""" +"""Provide :class:`BidictBase`.""" +import typing as _t from collections import namedtuple +from copy import copy from weakref import ref from ._abc import BidirectionalMapping -from ._dup import RAISE, OVERWRITE, IGNORE, _OnDup -from ._exc import ( - DuplicationError, KeyDuplicationError, ValueDuplicationError, KeyAndValueDuplicationError) -from ._miss import _MISS -from ._noop import _NOOP -from ._util import _iteritems_args_kw -from .compat import PY2, KeysView, ItemsView, Mapping, iteritems +from ._dup import ON_DUP_DEFAULT, RAISE, DROP_OLD, DROP_NEW, OnDup +from ._exc import DuplicationError, KeyDuplicationError, ValueDuplicationError, KeyAndValueDuplicationError +from ._iter import _iteritems_args_kw +from ._typing import _NONE, KT, VT, OKT, OVT, IterItems, MapOrIterItems -_DedupResult = namedtuple('_DedupResult', 'isdupkey isdupval invbyval fwdbykey') _WriteResult = namedtuple('_WriteResult', 'key val oldkey oldval') -_NODUP = _DedupResult(False, False, _MISS, _MISS) +_DedupResult = namedtuple('_DedupResult', 'isdupkey isdupval invbyval fwdbykey') +_NODUP = _DedupResult(False, False, _NONE, _NONE) + +BT = _t.TypeVar('BT', bound='BidictBase') # typevar for BidictBase.copy -class BidictBase(BidirectionalMapping): +class BidictBase(BidirectionalMapping[KT, VT]): """Base class implementing :class:`BidirectionalMapping`.""" - __slots__ = ('_fwdm', '_invm', '_inv', '_invweak', '_hash') + (() if PY2 else ('__weakref__',)) + __slots__ = ['_fwdm', '_invm', '_inv', '_invweak', '__weakref__'] - #: The default :class:`DuplicationPolicy` - #: (in effect during e.g. :meth:`~bidict.bidict.__init__` calls) + #: The default :class:`~bidict.OnDup` #: that governs behavior when a provided item - #: duplicates only the key of another item. - #: - #: Defaults to :attr:`~bidict.OVERWRITE` - #: to match :class:`dict`'s behavior. + #: duplicates the key or value of other item(s). #: #: *See also* :ref:`basic-usage:Values Must Be Unique`, :doc:`extending` - on_dup_key = OVERWRITE + on_dup = ON_DUP_DEFAULT - #: The default :class:`DuplicationPolicy` - #: (in effect during e.g. :meth:`~bidict.bidict.__init__` calls) - #: that governs behavior when a provided item - #: duplicates only the value of another item. - #: - #: Defaults to :attr:`~bidict.RAISE` - #: to prevent unintended overwrite of another item. - #: - #: *See also* :ref:`basic-usage:Values Must Be Unique`, :doc:`extending` - on_dup_val = RAISE - - #: The default :class:`DuplicationPolicy` - #: (in effect during e.g. :meth:`~bidict.bidict.__init__` calls) - #: that governs behavior when a provided item - #: duplicates the key of another item and the value of a third item. - #: - #: Defaults to ``None``, which causes the *on_dup_kv* policy to match - #: whatever *on_dup_val* policy is in effect. - #: - #: *See also* :ref:`basic-usage:Values Must Be Unique`, :doc:`extending` - on_dup_kv = None - - _fwdm_cls = dict - _invm_cls = dict + _fwdm_cls: _t.Type[_t.MutableMapping[KT, VT]] = dict #: class of the backing forward mapping + _invm_cls: _t.Type[_t.MutableMapping[VT, KT]] = dict #: class of the backing inverse mapping #: The object used by :meth:`__repr__` for printing the contained items. - _repr_delegate = dict + _repr_delegate: _t.Callable = dict - def __init__(self, *args, **kw): # pylint: disable=super-init-not-called + _inv: 'BidictBase[VT, KT]' + _inv_cls: '_t.Type[BidictBase[VT, KT]]' + + def __init_subclass__(cls, **kw): + super().__init_subclass__(**kw) + # Compute and set _inv_cls, the inverse of this bidict class. + if '_inv_cls' in cls.__dict__: + return + if cls._fwdm_cls is cls._invm_cls: + cls._inv_cls = cls + return + inv_cls = type(cls.__name__ + 'Inv', cls.__bases__, { + **cls.__dict__, + '_inv_cls': cls, + '_fwdm_cls': cls._invm_cls, + '_invm_cls': cls._fwdm_cls, + }) + cls._inv_cls = inv_cls + + @_t.overload + def __init__(self, __arg: _t.Mapping[KT, VT], **kw: VT) -> None: ... + @_t.overload + def __init__(self, __arg: IterItems[KT, VT], **kw: VT) -> None: ... + @_t.overload + def __init__(self, **kw: VT) -> None: ... + def __init__(self, *args: MapOrIterItems[KT, VT], **kw: VT) -> None: """Make a new bidirectional dictionary. - The signature is the same as that of regular dictionaries. + The signature behaves like that of :class:`dict`. Items passed in are added in the order they are passed, - respecting the current duplication policies in the process. - - *See also* :attr:`on_dup_key`, :attr:`on_dup_val`, :attr:`on_dup_kv` + respecting the :attr:`on_dup` class attribute in the process. """ #: The backing :class:`~collections.abc.Mapping` #: storing the forward mapping data (*key* → *value*). - self._fwdm = self._fwdm_cls() + self._fwdm: _t.MutableMapping[KT, VT] = self._fwdm_cls() #: The backing :class:`~collections.abc.Mapping` #: storing the inverse mapping data (*value* → *key*). - self._invm = self._invm_cls() - self._init_inv() # lgtm [py/init-calls-subclass] + self._invm: _t.MutableMapping[VT, KT] = self._invm_cls() + self._init_inv() if args or kw: - self._update(True, None, *args, **kw) + self._update(True, self.on_dup, *args, **kw) - def _init_inv(self): - # Compute the type for this bidict's inverse bidict (will be different from this - # bidict's type if _fwdm_cls and _invm_cls are different). - inv_cls = self._inv_cls() + def _init_inv(self) -> None: # Create the inverse bidict instance via __new__, bypassing its __init__ so that its # _fwdm and _invm can be assigned to this bidict's _invm and _fwdm. Store it in self._inv, # which holds a strong reference to a bidict's inverse, if one is available. - self._inv = inv = inv_cls.__new__(inv_cls) - inv._fwdm = self._invm # pylint: disable=protected-access - inv._invm = self._fwdm # pylint: disable=protected-access + self._inv = inv = self._inv_cls.__new__(self._inv_cls) + inv._fwdm = self._invm + inv._invm = self._fwdm # Only give the inverse a weak reference to this bidict to avoid creating a reference cycle, # stored in the _invweak attribute. See also the docs in # :ref:`addendum:Bidict Avoids Reference Cycles` - inv._inv = None # pylint: disable=protected-access - inv._invweak = ref(self) # pylint: disable=protected-access + inv._inv = None + inv._invweak = ref(self) # Since this bidict has a strong reference to its inverse already, set its _invweak to None. self._invweak = None - @classmethod - def _inv_cls(cls): - """The inverse of this bidict type, i.e. one with *_fwdm_cls* and *_invm_cls* swapped.""" - if cls._fwdm_cls is cls._invm_cls: - return cls - if not getattr(cls, '_inv_cls_', None): - class _Inv(cls): - _fwdm_cls = cls._invm_cls - _invm_cls = cls._fwdm_cls - _inv_cls_ = cls - _Inv.__name__ = cls.__name__ + 'Inv' - cls._inv_cls_ = _Inv - return cls._inv_cls_ - @property - def _isinv(self): + def _isinv(self) -> bool: return self._inv is None @property - def inverse(self): - """The inverse of this bidict. - - *See also* :attr:`inv` - """ + def inverse(self) -> 'BidictBase[VT, KT]': + """The inverse of this bidict.""" # Resolve and return a strong reference to the inverse bidict. # One may be stored in self._inv already. if self._inv is not None: return self._inv # Otherwise a weakref is stored in self._invweak. Try to get a strong ref from it. + assert self._invweak is not None inv = self._invweak() if inv is not None: return inv @@ -162,12 +141,10 @@ class BidictBase(BidirectionalMapping): self._init_inv() # Now this bidict will retain a strong ref to its inverse. return self._inv - @property - def inv(self): - """Alias for :attr:`inverse`.""" - return self.inverse + #: Alias for :attr:`inverse`. + inv = inverse - def __getstate__(self): + def __getstate__(self) -> dict: """Needed to enable pickling due to use of :attr:`__slots__` and weakrefs. *See also* :meth:`object.__getstate__` @@ -183,27 +160,27 @@ class BidictBase(BidirectionalMapping): state.pop('__weakref__', None) # Not added back in __setstate__. Python manages this one. return state - def __setstate__(self, state): + def __setstate__(self, state: dict) -> None: """Implemented because use of :attr:`__slots__` would prevent unpickling otherwise. *See also* :meth:`object.__setstate__` """ - for slot, value in iteritems(state): + for slot, value in state.items(): setattr(self, slot, value) self._init_inv() - def __repr__(self): + def __repr__(self) -> str: """See :func:`repr`.""" clsname = self.__class__.__name__ if not self: - return '%s()' % clsname - return '%s(%r)' % (clsname, self._repr_delegate(iteritems(self))) + return f'{clsname}()' + return f'{clsname}({self._repr_delegate(self.items())})' # The inherited Mapping.__eq__ implementation would work, but it's implemented in terms of an # inefficient ``dict(self.items()) == dict(other.items())`` comparison, so override it with a # more efficient implementation. - def __eq__(self, other): - u"""*x.__eq__(other) ⟺ x == other* + def __eq__(self, other: object) -> bool: + """*x.__eq__(other) ⟺ x == other* Equivalent to *dict(x.items()) == dict(other.items())* but more efficient. @@ -216,101 +193,98 @@ class BidictBase(BidirectionalMapping): *See also* :meth:`bidict.FrozenOrderedBidict.equals_order_sensitive` """ - if not isinstance(other, Mapping) or len(self) != len(other): + if not isinstance(other, _t.Mapping) or len(self) != len(other): return False selfget = self.get - return all(selfget(k, _MISS) == v for (k, v) in iteritems(other)) + return all(selfget(k, _NONE) == v for (k, v) in other.items()) # type: ignore [arg-type] + + def equals_order_sensitive(self, other: object) -> bool: + """Order-sensitive equality check. + + *See also* :ref:`eq-order-insensitive` + """ + # Same short-circuit as in __eq__ above. Factoring out not worth function call overhead. + if not isinstance(other, _t.Mapping) or len(self) != len(other): + return False + return all(i == j for (i, j) in zip(self.items(), other.items())) # The following methods are mutating and so are not public. But they are implemented in this # non-mutable base class (rather than the mutable `bidict` subclass) because they are used here # during initialization (starting with the `_update` method). (Why is this? Because `__init__` # and `update` share a lot of the same behavior (inserting the provided items while respecting - # the active duplication policies), so it makes sense for them to share implementation too.) - def _pop(self, key): + # `on_dup`), so it makes sense for them to share implementation too.) + def _pop(self, key: KT) -> VT: val = self._fwdm.pop(key) del self._invm[val] return val - def _put(self, key, val, on_dup): + def _put(self, key: KT, val: VT, on_dup: OnDup) -> None: dedup_result = self._dedup_item(key, val, on_dup) - if dedup_result is not _NOOP: + if dedup_result is not None: self._write_item(key, val, dedup_result) - def _dedup_item(self, key, val, on_dup): - """ - Check *key* and *val* for any duplication in self. + def _dedup_item(self, key: KT, val: VT, on_dup: OnDup) -> _t.Optional[_DedupResult]: + """Check *key* and *val* for any duplication in self. - Handle any duplication as per the duplication policies given in *on_dup*. + Handle any duplication as per the passed in *on_dup*. (key, val) already present is construed as a no-op, not a duplication. - If duplication is found and the corresponding duplication policy is + If duplication is found and the corresponding :class:`~bidict.OnDupAction` is + :attr:`~bidict.DROP_NEW`, return None. + + If duplication is found and the corresponding :class:`~bidict.OnDupAction` is :attr:`~bidict.RAISE`, raise the appropriate error. - If duplication is found and the corresponding duplication policy is - :attr:`~bidict.IGNORE`, return *None*. - - If duplication is found and the corresponding duplication policy is - :attr:`~bidict.OVERWRITE`, + If duplication is found and the corresponding :class:`~bidict.OnDupAction` is + :attr:`~bidict.DROP_OLD`, or if no duplication is found, - return the _DedupResult *(isdupkey, isdupval, oldkey, oldval)*. + return the :class:`_DedupResult` *(isdupkey, isdupval, oldkey, oldval)*. """ fwdm = self._fwdm invm = self._invm - oldval = fwdm.get(key, _MISS) - oldkey = invm.get(val, _MISS) - isdupkey = oldval is not _MISS - isdupval = oldkey is not _MISS + oldval: OVT = fwdm.get(key, _NONE) + oldkey: OKT = invm.get(val, _NONE) + isdupkey = oldval is not _NONE + isdupval = oldkey is not _NONE dedup_result = _DedupResult(isdupkey, isdupval, oldkey, oldval) if isdupkey and isdupval: - if self._isdupitem(key, val, dedup_result): + if self._already_have(key, val, oldkey, oldval): # (key, val) duplicates an existing item -> no-op. - return _NOOP + return None # key and val each duplicate a different existing item. if on_dup.kv is RAISE: raise KeyAndValueDuplicationError(key, val) - elif on_dup.kv is IGNORE: - return _NOOP - assert on_dup.kv is OVERWRITE, 'invalid on_dup_kv: %r' % on_dup.kv + if on_dup.kv is DROP_NEW: + return None + assert on_dup.kv is DROP_OLD # Fall through to the return statement on the last line. elif isdupkey: if on_dup.key is RAISE: raise KeyDuplicationError(key) - elif on_dup.key is IGNORE: - return _NOOP - assert on_dup.key is OVERWRITE, 'invalid on_dup.key: %r' % on_dup.key + if on_dup.key is DROP_NEW: + return None + assert on_dup.key is DROP_OLD # Fall through to the return statement on the last line. elif isdupval: if on_dup.val is RAISE: raise ValueDuplicationError(val) - elif on_dup.val is IGNORE: - return _NOOP - assert on_dup.val is OVERWRITE, 'invalid on_dup.val: %r' % on_dup.val + if on_dup.val is DROP_NEW: + return None + assert on_dup.val is DROP_OLD # Fall through to the return statement on the last line. # else neither isdupkey nor isdupval. return dedup_result @staticmethod - def _isdupitem(key, val, dedup_result): - isdupkey, isdupval, oldkey, oldval = dedup_result - isdupitem = oldkey == key - assert isdupitem == (oldval == val), '%r %r %r' % (key, val, dedup_result) - if isdupitem: - assert isdupkey - assert isdupval - return isdupitem + def _already_have(key: KT, val: VT, oldkey: OKT, oldval: OVT) -> bool: + # Overridden by _orderedbase.OrderedBidictBase. + isdup = oldkey == key + assert isdup == (oldval == val), f'{key} {val} {oldkey} {oldval}' + return isdup - @classmethod - def _get_on_dup(cls, on_dup=None): - if on_dup is None: - on_dup = _OnDup(cls.on_dup_key, cls.on_dup_val, cls.on_dup_kv) - elif not isinstance(on_dup, _OnDup): - on_dup = _OnDup(*on_dup) - if on_dup.kv is None: - on_dup = on_dup._replace(kv=on_dup.val) - return on_dup - - def _write_item(self, key, val, dedup_result): + def _write_item(self, key: KT, val: VT, dedup_result: _DedupResult) -> _WriteResult: + # Overridden by _orderedbase.OrderedBidictBase. isdupkey, isdupval, oldkey, oldval = dedup_result fwdm = self._fwdm invm = self._invm @@ -322,35 +296,34 @@ class BidictBase(BidirectionalMapping): del fwdm[oldkey] return _WriteResult(key, val, oldkey, oldval) - def _update(self, init, on_dup, *args, **kw): + def _update(self, init: bool, on_dup: OnDup, *args: MapOrIterItems[KT, VT], **kw: VT) -> None: # args[0] may be a generator that yields many items, so process input in a single pass. if not args and not kw: return can_skip_dup_check = not self and not kw and isinstance(args[0], BidirectionalMapping) if can_skip_dup_check: - self._update_no_dup_check(args[0]) + self._update_no_dup_check(args[0]) # type: ignore [arg-type] return - on_dup = self._get_on_dup(on_dup) can_skip_rollback = init or RAISE not in on_dup if can_skip_rollback: self._update_no_rollback(on_dup, *args, **kw) else: self._update_with_rollback(on_dup, *args, **kw) - def _update_no_dup_check(self, other, _nodup=_NODUP): + def _update_no_dup_check(self, other: BidirectionalMapping[KT, VT]) -> None: write_item = self._write_item - for (key, val) in iteritems(other): - write_item(key, val, _nodup) + for (key, val) in other.items(): + write_item(key, val, _NODUP) - def _update_no_rollback(self, on_dup, *args, **kw): + def _update_no_rollback(self, on_dup: OnDup, *args: MapOrIterItems[KT, VT], **kw: VT) -> None: put = self._put for (key, val) in _iteritems_args_kw(*args, **kw): put(key, val, on_dup) - def _update_with_rollback(self, on_dup, *args, **kw): + def _update_with_rollback(self, on_dup: OnDup, *args: MapOrIterItems[KT, VT], **kw: VT) -> None: """Update, rolling back on failure.""" - writelog = [] - appendlog = writelog.append + writes: _t.List[_t.Tuple[_DedupResult, _WriteResult]] = [] + append_write = writes.append dedup_item = self._dedup_item write_item = self._write_item for (key, val) in _iteritems_args_kw(*args, **kw): @@ -358,14 +331,14 @@ class BidictBase(BidirectionalMapping): dedup_result = dedup_item(key, val, on_dup) except DuplicationError: undo_write = self._undo_write - for dedup_result, write_result in reversed(writelog): + for dedup_result, write_result in reversed(writes): undo_write(dedup_result, write_result) raise - if dedup_result is not _NOOP: + if dedup_result is not None: write_result = write_item(key, val, dedup_result) - appendlog((dedup_result, write_result)) + append_write((dedup_result, write_result)) - def _undo_write(self, dedup_result, write_result): + def _undo_write(self, dedup_result: _DedupResult, write_result: _WriteResult) -> None: isdupkey, isdupval, _, _ = dedup_result key, val, oldkey, oldval = write_result if not isdupkey and not isdupval: @@ -384,79 +357,48 @@ class BidictBase(BidirectionalMapping): if not isdupkey: del fwdm[key] - def copy(self): + def copy(self: BT) -> BT: """A shallow copy.""" # Could just ``return self.__class__(self)`` here instead, but the below is faster. It uses # __new__ to create a copy instance while bypassing its __init__, which would result # in copying this bidict's items into the copy instance one at a time. Instead, make whole # copies of each of the backing mappings, and make them the backing mappings of the copy, # avoiding copying items one at a time. - copy = self.__class__.__new__(self.__class__) - copy._fwdm = self._fwdm.copy() # pylint: disable=protected-access - copy._invm = self._invm.copy() # pylint: disable=protected-access - copy._init_inv() # pylint: disable=protected-access - return copy + cp: BT = self.__class__.__new__(self.__class__) + cp._fwdm = copy(self._fwdm) + cp._invm = copy(self._invm) + cp._init_inv() + return cp - def __copy__(self): - """Used for the copy protocol. + #: Used for the copy protocol. + #: *See also* the :mod:`copy` module + __copy__ = copy - *See also* the :mod:`copy` module - """ - return self.copy() - - def __len__(self): + def __len__(self) -> int: """The number of contained items.""" return len(self._fwdm) - def __iter__(self): # lgtm [py/inheritance/incorrect-overridden-signature] - """Iterator over the contained items.""" - # No default implementation for __iter__ inherited from Mapping -> - # always delegate to _fwdm. + def __iter__(self) -> _t.Iterator[KT]: + """Iterator over the contained keys.""" return iter(self._fwdm) - def __getitem__(self, key): - u"""*x.__getitem__(key) ⟺ x[key]*""" + def __getitem__(self, key: KT) -> VT: + """*x.__getitem__(key) ⟺ x[key]*""" return self._fwdm[key] - def values(self): - """A set-like object providing a view on the contained values. + # On Python 3.8+, dicts are reversible, so even non-Ordered bidicts can provide an efficient + # __reversed__ implementation. (On Python < 3.8, they cannot.) Once support is dropped for + # Python < 3.8, can remove the following if statement to provide __reversed__ unconditionally. + if hasattr(_fwdm_cls, '__reversed__'): + def __reversed__(self) -> _t.Iterator[KT]: + """Iterator over the contained keys in reverse order.""" + return reversed(self._fwdm) # type: ignore [no-any-return,call-overload] - Note that because the values of a :class:`~bidict.BidirectionalMapping` - are the keys of its inverse, - this returns a :class:`~collections.abc.KeysView` - rather than a :class:`~collections.abc.ValuesView`, - which has the advantages of constant-time containment checks - and supporting set operations. - """ - return self.inverse.keys() - - if PY2: - # For iterkeys and iteritems, inheriting from Mapping already provides - # the best default implementations so no need to define here. - - def itervalues(self): - """An iterator over the contained values.""" - return self.inverse.iterkeys() - - def viewkeys(self): # noqa: D102; pylint: disable=missing-docstring - return KeysView(self) - - def viewvalues(self): # noqa: D102; pylint: disable=missing-docstring - return self.inverse.viewkeys() - - viewvalues.__doc__ = values.__doc__ - values.__doc__ = 'A list of the contained values.' - - def viewitems(self): # noqa: D102; pylint: disable=missing-docstring - return ItemsView(self) - - # __ne__ added automatically in Python 3 when you implement __eq__, but not in Python 2. - def __ne__(self, other): # noqa: N802 - u"""*x.__ne__(other) ⟺ x != other*""" - return not self == other # Implement __ne__ in terms of __eq__. +# Work around weakref slot with Generics bug on Python 3.6 (https://bugs.python.org/issue41451): +BidictBase.__slots__.remove('__weakref__') # * Code review nav * #============================================================================== -# ↠Prev: _abc.py Current: _base.py Next: _delegating_mixins.py → +# ↠Prev: _abc.py Current: _base.py Next: _frozenbidict.py → #============================================================================== diff --git a/libs/bidict/_bidict.py b/libs/bidict/_bidict.py index 9082775b6..3a98a61df 100644 --- a/libs/bidict/_bidict.py +++ b/libs/bidict/_bidict.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2009-2019 Joshua Bronson. All Rights Reserved. +# Copyright 2009-2021 Joshua Bronson. All Rights Reserved. # # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this @@ -26,18 +26,23 @@ #============================================================================== -"""Provides :class:`bidict`.""" +"""Provide :class:`bidict`.""" +import typing as _t + +from ._delegating import _DelegatingBidict from ._mut import MutableBidict -from ._delegating_mixins import _DelegateKeysAndItemsToFwdm +from ._typing import KT, VT -class bidict(_DelegateKeysAndItemsToFwdm, MutableBidict): # noqa: N801,E501; pylint: disable=invalid-name +class bidict(_DelegatingBidict[KT, VT], MutableBidict[KT, VT]): """Base class for mutable bidirectional mappings.""" __slots__ = () - __hash__ = None # since this class is mutable; explicit > implicit. + if _t.TYPE_CHECKING: + @property + def inverse(self) -> 'bidict[VT, KT]': ... # * Code review nav * diff --git a/libs/bidict/_delegating.py b/libs/bidict/_delegating.py new file mode 100644 index 000000000..fa935dd2e --- /dev/null +++ b/libs/bidict/_delegating.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- +# Copyright 2009-2021 Joshua Bronson. All Rights Reserved. +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +"""Provide :class:`_DelegatingBidict`.""" + +import typing as _t + +from ._base import BidictBase +from ._typing import KT, VT + + +class _DelegatingBidict(BidictBase[KT, VT]): + """Provide optimized implementations of several methods by delegating to backing dicts. + + Used to override less efficient implementations inherited by :class:`~collections.abc.Mapping`. + """ + + __slots__ = () + + def __iter__(self) -> _t.Iterator[KT]: + """Iterator over the contained keys.""" + return iter(self._fwdm) + + def keys(self) -> _t.KeysView[KT]: + """A set-like object providing a view on the contained keys.""" + return self._fwdm.keys() # type: ignore [return-value] + + def values(self) -> _t.KeysView[VT]: # type: ignore [override] # https://github.com/python/typeshed/issues/4435 + """A set-like object providing a view on the contained values.""" + return self._invm.keys() # type: ignore [return-value] + + def items(self) -> _t.ItemsView[KT, VT]: + """A set-like object providing a view on the contained items.""" + return self._fwdm.items() # type: ignore [return-value] diff --git a/libs/bidict/_delegating_mixins.py b/libs/bidict/_delegating_mixins.py deleted file mode 100644 index 8772490c7..000000000 --- a/libs/bidict/_delegating_mixins.py +++ /dev/null @@ -1,92 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2009-2019 Joshua Bronson. All Rights Reserved. -# -# This Source Code Form is subject to the terms of the Mozilla Public -# License, v. 2.0. If a copy of the MPL was not distributed with this -# file, You can obtain one at http://mozilla.org/MPL/2.0/. - - -#============================================================================== -# * Welcome to the bidict source code * -#============================================================================== - -# Doing a code review? You'll find a "Code review nav" comment like the one -# below at the top and bottom of the most important source files. This provides -# a suggested initial path through the source when reviewing. -# -# Note: If you aren't reading this on https://github.com/jab/bidict, you may be -# viewing an outdated version of the code. Please head to GitHub to review the -# latest version, which contains important improvements over older versions. -# -# Thank you for reading and for any feedback you provide. - -# * Code review nav * -#============================================================================== -# ↠Prev: _base.py Current: _delegating_mixins.py Next: _frozenbidict.py → -#============================================================================== - - -r"""Provides mixin classes that delegate to ``self._fwdm`` for various operations. - -This allows methods such as :meth:`bidict.bidict.items` -to be implemented in terms of a ``self._fwdm.items()`` call, -which is potentially much more efficient (e.g. in CPython 2) -compared to the implementation inherited from :class:`~collections.abc.Mapping` -(which returns ``[(key, self[key]) for key in self]`` in Python 2). - -Because this depends on implementation details that aren't necessarily true -(such as the bidict's values being the same as its ``self._fwdm.values()``, -which is not true for e.g. ordered bidicts where ``_fwdm``\'s values are nodes), -these should always be mixed in at a layer below a more general layer, -as they are in e.g. :class:`~bidict.frozenbidict` -which extends :class:`~bidict.BidictBase`. - -See the :ref:`extending:Sorted Bidict Recipes` -for another example of where this comes into play. -``SortedBidict`` extends :class:`bidict.MutableBidict` -rather than :class:`bidict.bidict` -to avoid inheriting these mixins, -which are incompatible with the backing -:class:`sortedcontainers.SortedDict`s. -""" - -from .compat import PY2 - - -_KEYS_METHODS = ('keys',) + (('viewkeys', 'iterkeys') if PY2 else ()) -_ITEMS_METHODS = ('items',) + (('viewitems', 'iteritems') if PY2 else ()) -_DOCSTRING_BY_METHOD = { - 'keys': 'A set-like object providing a view on the contained keys.', - 'items': 'A set-like object providing a view on the contained items.', -} -if PY2: - _DOCSTRING_BY_METHOD['viewkeys'] = _DOCSTRING_BY_METHOD['keys'] - _DOCSTRING_BY_METHOD['viewitems'] = _DOCSTRING_BY_METHOD['items'] - _DOCSTRING_BY_METHOD['keys'] = 'A list of the contained keys.' - _DOCSTRING_BY_METHOD['items'] = 'A list of the contained items.' - - -def _make_method(methodname): - def method(self): - return getattr(self._fwdm, methodname)() # pylint: disable=protected-access - method.__name__ = methodname - method.__doc__ = _DOCSTRING_BY_METHOD.get(methodname, '') - return method - - -def _make_fwdm_delegating_mixin(clsname, methodnames): - clsdict = dict({name: _make_method(name) for name in methodnames}, __slots__=()) - return type(clsname, (object,), clsdict) - - -_DelegateKeysToFwdm = _make_fwdm_delegating_mixin('_DelegateKeysToFwdm', _KEYS_METHODS) -_DelegateItemsToFwdm = _make_fwdm_delegating_mixin('_DelegateItemsToFwdm', _ITEMS_METHODS) -_DelegateKeysAndItemsToFwdm = type( - '_DelegateKeysAndItemsToFwdm', - (_DelegateKeysToFwdm, _DelegateItemsToFwdm), - {'__slots__': ()}) - -# * Code review nav * -#============================================================================== -# ↠Prev: _base.py Current: _delegating_mixins.py Next: _frozenbidict.py → -#============================================================================== diff --git a/libs/bidict/_dup.py b/libs/bidict/_dup.py index 4670dcc57..3693424a7 100644 --- a/libs/bidict/_dup.py +++ b/libs/bidict/_dup.py @@ -1,36 +1,58 @@ # -*- coding: utf-8 -*- -# Copyright 2009-2019 Joshua Bronson. All Rights Reserved. +# Copyright 2009-2021 Joshua Bronson. All Rights Reserved. # # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. -"""Provides bidict duplication policies and the :class:`_OnDup` class.""" +"""Provide :class:`OnDup` and related functionality.""" from collections import namedtuple - -from ._marker import _Marker +from enum import Enum -_OnDup = namedtuple('_OnDup', 'key val kv') +class OnDupAction(Enum): + """An action to take to prevent duplication from occurring.""" + + #: Raise a :class:`~bidict.DuplicationError`. + RAISE = 'RAISE' + #: Overwrite existing items with new items. + DROP_OLD = 'DROP_OLD' + #: Keep existing items and drop new items. + DROP_NEW = 'DROP_NEW' + + def __repr__(self) -> str: + return f'<{self.name}>' -class DuplicationPolicy(_Marker): - """Base class for bidict's duplication policies. +RAISE = OnDupAction.RAISE +DROP_OLD = OnDupAction.DROP_OLD +DROP_NEW = OnDupAction.DROP_NEW + + +class OnDup(namedtuple('_OnDup', 'key val kv')): + r"""A 3-tuple of :class:`OnDupAction`\s specifying how to handle the 3 kinds of duplication. *See also* :ref:`basic-usage:Values Must Be Unique` + + If *kv* is not specified, *val* will be used for *kv*. """ __slots__ = () + def __new__(cls, key: OnDupAction = DROP_OLD, val: OnDupAction = RAISE, kv: OnDupAction = RAISE) -> 'OnDup': + """Override to provide user-friendly default values.""" + return super().__new__(cls, key, val, kv or val) -#: Raise an exception when a duplication is encountered. -RAISE = DuplicationPolicy('DUP_POLICY.RAISE') -#: Overwrite an existing item when a duplication is encountered. -OVERWRITE = DuplicationPolicy('DUP_POLICY.OVERWRITE') - -#: Keep the existing item and ignore the new item when a duplication is encountered. -IGNORE = DuplicationPolicy('DUP_POLICY.IGNORE') +#: Default :class:`OnDup` used for the +#: :meth:`~bidict.bidict.__init__`, +#: :meth:`~bidict.bidict.__setitem__`, and +#: :meth:`~bidict.bidict.update` methods. +ON_DUP_DEFAULT = OnDup() +#: An :class:`OnDup` whose members are all :obj:`RAISE`. +ON_DUP_RAISE = OnDup(key=RAISE, val=RAISE, kv=RAISE) +#: An :class:`OnDup` whose members are all :obj:`DROP_OLD`. +ON_DUP_DROP_OLD = OnDup(key=DROP_OLD, val=DROP_OLD, kv=DROP_OLD) diff --git a/libs/bidict/_exc.py b/libs/bidict/_exc.py index 5370361e6..d6f2af5ea 100644 --- a/libs/bidict/_exc.py +++ b/libs/bidict/_exc.py @@ -1,12 +1,12 @@ # -*- coding: utf-8 -*- -# Copyright 2009-2019 Joshua Bronson. All Rights Reserved. +# Copyright 2009-2021 Joshua Bronson. All Rights Reserved. # # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. -"""Provides all bidict exceptions.""" +"""Provide all bidict exceptions.""" class BidictException(Exception): @@ -15,7 +15,7 @@ class BidictException(Exception): class DuplicationError(BidictException): """Base class for exceptions raised when uniqueness is violated - as per the RAISE duplication policy. + as per the :attr:~bidict.RAISE` :class:`~bidict.OnDupAction`. """ diff --git a/libs/bidict/_frozenbidict.py b/libs/bidict/_frozenbidict.py index 07831fd91..2b652029d 100644 --- a/libs/bidict/_frozenbidict.py +++ b/libs/bidict/_frozenbidict.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2009-2019 Joshua Bronson. All Rights Reserved. +# Copyright 2009-2021 Joshua Bronson. All Rights Reserved. # # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this @@ -22,30 +22,39 @@ # * Code review nav * #============================================================================== -# ↠Prev: _delegating_mixins.py Current: _frozenbidict.py Next: _mut.py → +# ↠Prev: _base.py Current: _frozenbidict.py Next: _mut.py → #============================================================================== -"""Provides :class:`frozenbidict`, an immutable, hashable bidirectional mapping type.""" +"""Provide :class:`frozenbidict`, an immutable, hashable bidirectional mapping type.""" -from ._base import BidictBase -from ._delegating_mixins import _DelegateKeysAndItemsToFwdm -from .compat import ItemsView +import typing as _t + +from ._delegating import _DelegatingBidict +from ._typing import KT, VT -class frozenbidict(_DelegateKeysAndItemsToFwdm, BidictBase): # noqa: N801,E501; pylint: disable=invalid-name +class frozenbidict(_DelegatingBidict[KT, VT]): """Immutable, hashable bidict type.""" - __slots__ = () + __slots__ = ('_hash',) - def __hash__(self): # lgtm [py/equals-hash-mismatch] + _hash: int + + # Work around lack of support for higher-kinded types in mypy. + # Ref: https://github.com/python/typing/issues/548#issuecomment-621571821 + # Remove this and similar type stubs from other classes if support is ever added. + if _t.TYPE_CHECKING: + @property + def inverse(self) -> 'frozenbidict[VT, KT]': ... + + def __hash__(self) -> int: """The hash of this bidict as determined by its items.""" if getattr(self, '_hash', None) is None: - # pylint: disable=protected-access,attribute-defined-outside-init - self._hash = ItemsView(self)._hash() + self._hash = _t.ItemsView(self)._hash() # type: ignore [attr-defined] return self._hash # * Code review nav * #============================================================================== -# ↠Prev: _delegating_mixins.py Current: _frozenbidict.py Next: _mut.py → +# ↠Prev: _base.py Current: _frozenbidict.py Next: _mut.py → #============================================================================== diff --git a/libs/bidict/_frozenordered.py b/libs/bidict/_frozenordered.py index 25cbace3b..c9f47a766 100644 --- a/libs/bidict/_frozenordered.py +++ b/libs/bidict/_frozenordered.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2009-2019 Joshua Bronson. All Rights Reserved. +# Copyright 2009-2021 Joshua Bronson. All Rights Reserved. # # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this @@ -25,38 +25,61 @@ #↠Prev: _orderedbase.py Current: _frozenordered.py Next: _orderedbidict.py → #============================================================================== -"""Provides :class:`FrozenOrderedBidict`, an immutable, hashable, ordered bidict.""" +"""Provide :class:`FrozenOrderedBidict`, an immutable, hashable, ordered bidict.""" + +import typing as _t -from ._delegating_mixins import _DelegateKeysToFwdm from ._frozenbidict import frozenbidict from ._orderedbase import OrderedBidictBase -from .compat import DICTS_ORDERED, PY2, izip +from ._typing import KT, VT -# If the Python implementation's dict type is ordered (e.g. PyPy or CPython >= 3.6), then -# `FrozenOrderedBidict` can delegate to `_fwdm` for keys: Both `_fwdm` and `_invm` will always -# be initialized with the provided items in the correct order, and since `FrozenOrderedBidict` -# is immutable, their respective orders can't get out of sync after a mutation. (Can't delegate -# to `_fwdm` for items though because values in `_fwdm` are nodes.) -_BASES = ((_DelegateKeysToFwdm,) if DICTS_ORDERED else ()) + (OrderedBidictBase,) -_CLSDICT = dict( - __slots__=(), - # Must set __hash__ explicitly, Python prevents inheriting it. - # frozenbidict.__hash__ can be reused for FrozenOrderedBidict: - # FrozenOrderedBidict inherits BidictBase.__eq__ which is order-insensitive, - # and frozenbidict.__hash__ is consistent with BidictBase.__eq__. - __hash__=frozenbidict.__hash__.__func__ if PY2 else frozenbidict.__hash__, - __doc__='Hashable, immutable, ordered bidict type.', - __module__=__name__, # Otherwise unpickling fails in Python 2. -) +class FrozenOrderedBidict(OrderedBidictBase[KT, VT]): + """Hashable, immutable, ordered bidict type. -# When PY2 (so we provide iteritems) and DICTS_ORDERED, e.g. on PyPy, the following implementation -# of iteritems may be more efficient than that inherited from `Mapping`. This exploits the property -# that the keys in `_fwdm` and `_invm` are already in the right order: -if PY2 and DICTS_ORDERED: - _CLSDICT['iteritems'] = lambda self: izip(self._fwdm, self._invm) # noqa: E501; pylint: disable=protected-access + Like a hashable :class:`bidict.OrderedBidict` + without the mutating APIs, or like a + reversible :class:`bidict.frozenbidict` even on Python < 3.8. + (All bidicts are order-preserving when never mutated, so frozenbidict is + already order-preserving, but only on Python 3.8+, where dicts are + reversible, are all bidicts (including frozenbidict) also reversible.) -FrozenOrderedBidict = type('FrozenOrderedBidict', _BASES, _CLSDICT) # pylint: disable=invalid-name + If you are using Python 3.8+, frozenbidict gives you everything that + FrozenOrderedBidict gives you, but with less space overhead. + """ + + __slots__ = ('_hash',) + __hash__ = frozenbidict.__hash__ + + if _t.TYPE_CHECKING: + @property + def inverse(self) -> 'FrozenOrderedBidict[VT, KT]': ... + + # Delegate to backing dicts for more efficient implementations of keys() and values(). + # Possible with FrozenOrderedBidict but not OrderedBidict since FrozenOrderedBidict + # is immutable, i.e. these can't get out of sync after initialization due to mutation. + def keys(self) -> _t.KeysView[KT]: + """A set-like object providing a view on the contained keys.""" + return self._fwdm._fwdm.keys() # type: ignore [return-value] + + def values(self) -> _t.KeysView[VT]: # type: ignore [override] + """A set-like object providing a view on the contained values.""" + return self._invm._fwdm.keys() # type: ignore [return-value] + + # Can't delegate for items() because values in _fwdm and _invm are nodes. + + # On Python 3.8+, delegate to backing dicts for a more efficient implementation + # of __iter__ and __reversed__ (both of which call this _iter() method): + if hasattr(dict, '__reversed__'): + def _iter(self, *, reverse: bool = False) -> _t.Iterator[KT]: + itfn = reversed if reverse else iter + return itfn(self._fwdm._fwdm) # type: ignore [operator,no-any-return] + else: + # On Python < 3.8, just optimize __iter__: + def _iter(self, *, reverse: bool = False) -> _t.Iterator[KT]: + if not reverse: + return iter(self._fwdm._fwdm) + return super()._iter(reverse=True) # * Code review nav * diff --git a/libs/bidict/_util.py b/libs/bidict/_iter.py similarity index 52% rename from libs/bidict/_util.py rename to libs/bidict/_iter.py index 89636e66c..6a5996d9b 100644 --- a/libs/bidict/_util.py +++ b/libs/bidict/_iter.py @@ -1,50 +1,56 @@ # -*- coding: utf-8 -*- -# Copyright 2009-2019 Joshua Bronson. All Rights Reserved. +# Copyright 2009-2021 Joshua Bronson. All Rights Reserved. # # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. -"""Useful functions for working with bidirectional mappings and related data.""" +"""Functions for iterating over items in a mapping.""" -from itertools import chain, repeat +import typing as _t +from collections.abc import Mapping +from itertools import chain -from .compat import iteritems, Mapping +from ._typing import KT, VT, IterItems, MapOrIterItems -_NULL_IT = repeat(None, 0) # repeat 0 times -> raise StopIteration from the start +_NULL_IT: IterItems = iter(()) -def _iteritems_mapping_or_iterable(arg): +def _iteritems_mapping_or_iterable(arg: MapOrIterItems[KT, VT]) -> IterItems[KT, VT]: """Yield the items in *arg*. If *arg* is a :class:`~collections.abc.Mapping`, return an iterator over its items. Otherwise return an iterator over *arg* itself. """ - return iteritems(arg) if isinstance(arg, Mapping) else iter(arg) + return iter(arg.items() if isinstance(arg, Mapping) else arg) -def _iteritems_args_kw(*args, **kw): +def _iteritems_args_kw(*args: MapOrIterItems[KT, VT], **kw: VT) -> IterItems[KT, VT]: """Yield the items from the positional argument (if given) and then any from *kw*. :raises TypeError: if more than one positional argument is given. """ args_len = len(args) if args_len > 1: - raise TypeError('Expected at most 1 positional argument, got %d' % args_len) - itemchain = None + raise TypeError(f'Expected at most 1 positional argument, got {args_len}') + it: IterItems = () if args: arg = args[0] if arg: - itemchain = _iteritems_mapping_or_iterable(arg) + it = _iteritems_mapping_or_iterable(arg) if kw: - iterkw = iteritems(kw) - itemchain = chain(itemchain, iterkw) if itemchain else iterkw - return itemchain or _NULL_IT + iterkw = iter(kw.items()) + it = chain(it, iterkw) if it else iterkw + return it or _NULL_IT -def inverted(arg): +@_t.overload +def inverted(arg: _t.Mapping[KT, VT]) -> IterItems[VT, KT]: ... +@_t.overload +def inverted(arg: IterItems[KT, VT]) -> IterItems[VT, KT]: ... +def inverted(arg: MapOrIterItems[KT, VT]) -> IterItems[VT, KT]: """Yield the inverse items of the provided object. If *arg* has a :func:`callable` ``__inverted__`` attribute, @@ -57,5 +63,5 @@ def inverted(arg): """ inv = getattr(arg, '__inverted__', None) if callable(inv): - return inv() + return inv() # type: ignore [no-any-return] return ((val, key) for (key, val) in _iteritems_mapping_or_iterable(arg)) diff --git a/libs/bidict/_marker.py b/libs/bidict/_marker.py deleted file mode 100644 index f2f9c57cb..000000000 --- a/libs/bidict/_marker.py +++ /dev/null @@ -1,19 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2009-2019 Joshua Bronson. All Rights Reserved. -# -# This Source Code Form is subject to the terms of the Mozilla Public -# License, v. 2.0. If a copy of the MPL was not distributed with this -# file, You can obtain one at http://mozilla.org/MPL/2.0/. - - -"""Provides :class:`_Marker`, an internal type for representing singletons.""" - -from collections import namedtuple - - -class _Marker(namedtuple('_Marker', 'name')): - - __slots__ = () - - def __repr__(self): - return '<%s>' % self.name # pragma: no cover diff --git a/libs/bidict/_miss.py b/libs/bidict/_miss.py deleted file mode 100644 index 32d02c584..000000000 --- a/libs/bidict/_miss.py +++ /dev/null @@ -1,14 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2009-2019 Joshua Bronson. All Rights Reserved. -# -# This Source Code Form is subject to the terms of the Mozilla Public -# License, v. 2.0. If a copy of the MPL was not distributed with this -# file, You can obtain one at http://mozilla.org/MPL/2.0/. - - -"""Provides the :obj:`_MISS` sentinel, for internally signaling "missing/not found".""" - -from ._marker import _Marker - - -_MISS = _Marker('MISSING') diff --git a/libs/bidict/_mut.py b/libs/bidict/_mut.py index 1a117c2ab..1787e6eaa 100644 --- a/libs/bidict/_mut.py +++ b/libs/bidict/_mut.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2009-2019 Joshua Bronson. All Rights Reserved. +# Copyright 2009-2021 Joshua Bronson. All Rights Reserved. # # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this @@ -26,32 +26,31 @@ #============================================================================== -"""Provides :class:`bidict`.""" +"""Provide :class:`MutableBidict`.""" +import typing as _t + +from ._abc import MutableBidirectionalMapping from ._base import BidictBase -from ._dup import OVERWRITE, RAISE, _OnDup -from ._miss import _MISS -from .compat import MutableMapping +from ._dup import OnDup, ON_DUP_RAISE, ON_DUP_DROP_OLD +from ._typing import _NONE, KT, VT, VDT, IterItems, MapOrIterItems -# Extend MutableMapping explicitly because it doesn't implement __subclasshook__, as well as to -# inherit method implementations it provides that we can reuse (namely `setdefault`). -class MutableBidict(BidictBase, MutableMapping): +class MutableBidict(BidictBase[KT, VT], MutableBidirectionalMapping[KT, VT]): """Base class for mutable bidirectional mappings.""" __slots__ = () - __hash__ = None # since this class is mutable; explicit > implicit. + if _t.TYPE_CHECKING: + @property + def inverse(self) -> 'MutableBidict[VT, KT]': ... - _ON_DUP_OVERWRITE = _OnDup(key=OVERWRITE, val=OVERWRITE, kv=OVERWRITE) - - def __delitem__(self, key): - u"""*x.__delitem__(y) ⟺ del x[y]*""" + def __delitem__(self, key: KT) -> None: + """*x.__delitem__(y) ⟺ del x[y]*""" self._pop(key) - def __setitem__(self, key, val): - """ - Set the value for *key* to *val*. + def __setitem__(self, key: KT, val: VT) -> None: + """Set the value for *key* to *val*. If *key* is already associated with *val*, this is a no-op. @@ -64,7 +63,7 @@ class MutableBidict(BidictBase, MutableMapping): to protect against accidental removal of the key that's currently associated with *val*. - Use :meth:`put` instead if you want to specify different policy in + Use :meth:`put` instead if you want to specify different behavior in the case that the provided key or value duplicates an existing one. Or use :meth:`forceput` to unconditionally associate *key* with *val*, replacing any existing items as necessary to preserve uniqueness. @@ -76,16 +75,12 @@ class MutableBidict(BidictBase, MutableMapping): existing item and *val* duplicates the value of a different existing item. """ - on_dup = self._get_on_dup() - self._put(key, val, on_dup) + self._put(key, val, self.on_dup) - def put(self, key, val, on_dup_key=RAISE, on_dup_val=RAISE, on_dup_kv=None): - """ - Associate *key* with *val* with the specified duplication policies. + def put(self, key: KT, val: VT, on_dup: OnDup = ON_DUP_RAISE) -> None: + """Associate *key* with *val*, honoring the :class:`OnDup` given in *on_dup*. - If *on_dup_kv* is ``None``, the *on_dup_val* policy will be used for it. - - For example, if all given duplication policies are :attr:`~bidict.RAISE`, + For example, if *on_dup* is :attr:`~bidict.ON_DUP_RAISE`, then *key* will be associated with *val* if and only if *key* is not already associated with an existing value and *val* is not already associated with an existing key, @@ -94,37 +89,39 @@ class MutableBidict(BidictBase, MutableMapping): If *key* is already associated with *val*, this is a no-op. :raises bidict.KeyDuplicationError: if attempting to insert an item - whose key only duplicates an existing item's, and *on_dup_key* is + whose key only duplicates an existing item's, and *on_dup.key* is :attr:`~bidict.RAISE`. :raises bidict.ValueDuplicationError: if attempting to insert an item - whose value only duplicates an existing item's, and *on_dup_val* is + whose value only duplicates an existing item's, and *on_dup.val* is :attr:`~bidict.RAISE`. :raises bidict.KeyAndValueDuplicationError: if attempting to insert an item whose key duplicates one existing item's, and whose value - duplicates another existing item's, and *on_dup_kv* is + duplicates another existing item's, and *on_dup.kv* is :attr:`~bidict.RAISE`. """ - on_dup = self._get_on_dup((on_dup_key, on_dup_val, on_dup_kv)) self._put(key, val, on_dup) - def forceput(self, key, val): - """ - Associate *key* with *val* unconditionally. + def forceput(self, key: KT, val: VT) -> None: + """Associate *key* with *val* unconditionally. Replace any existing mappings containing key *key* or value *val* as necessary to preserve uniqueness. """ - self._put(key, val, self._ON_DUP_OVERWRITE) + self._put(key, val, ON_DUP_DROP_OLD) - def clear(self): + def clear(self) -> None: """Remove all items.""" self._fwdm.clear() self._invm.clear() - def pop(self, key, default=_MISS): - u"""*x.pop(k[, d]) → v* + @_t.overload + def pop(self, key: KT) -> VT: ... + @_t.overload + def pop(self, key: KT, default: VDT = ...) -> VDT: ... + def pop(self, key: KT, default: VDT = _NONE) -> VDT: + """*x.pop(k[, d]) → v* Remove specified key and return the corresponding value. @@ -133,12 +130,12 @@ class MutableBidict(BidictBase, MutableMapping): try: return self._pop(key) except KeyError: - if default is _MISS: + if default is _NONE: raise return default - def popitem(self): - u"""*x.popitem() → (k, v)* + def popitem(self) -> _t.Tuple[KT, VT]: + """*x.popitem() → (k, v)* Remove and return some item as a (key, value) pair. @@ -150,24 +147,38 @@ class MutableBidict(BidictBase, MutableMapping): del self._invm[val] return key, val - def update(self, *args, **kw): - """Like :meth:`putall` with default duplication policies.""" + @_t.overload + def update(self, __arg: _t.Mapping[KT, VT], **kw: VT) -> None: ... + @_t.overload + def update(self, __arg: IterItems[KT, VT], **kw: VT) -> None: ... + @_t.overload + def update(self, **kw: VT) -> None: ... + def update(self, *args: MapOrIterItems[KT, VT], **kw: VT) -> None: + """Like calling :meth:`putall` with *self.on_dup* passed for *on_dup*.""" if args or kw: - self._update(False, None, *args, **kw) + self._update(False, self.on_dup, *args, **kw) - def forceupdate(self, *args, **kw): + @_t.overload + def forceupdate(self, __arg: _t.Mapping[KT, VT], **kw: VT) -> None: ... + @_t.overload + def forceupdate(self, __arg: IterItems[KT, VT], **kw: VT) -> None: ... + @_t.overload + def forceupdate(self, **kw: VT) -> None: ... + def forceupdate(self, *args: MapOrIterItems[KT, VT], **kw: VT) -> None: """Like a bulk :meth:`forceput`.""" - self._update(False, self._ON_DUP_OVERWRITE, *args, **kw) + self._update(False, ON_DUP_DROP_OLD, *args, **kw) - def putall(self, items, on_dup_key=RAISE, on_dup_val=RAISE, on_dup_kv=None): - """ - Like a bulk :meth:`put`. + @_t.overload + def putall(self, items: _t.Mapping[KT, VT], on_dup: OnDup) -> None: ... + @_t.overload + def putall(self, items: IterItems[KT, VT], on_dup: OnDup = ON_DUP_RAISE) -> None: ... + def putall(self, items: MapOrIterItems[KT, VT], on_dup: OnDup = ON_DUP_RAISE) -> None: + """Like a bulk :meth:`put`. If one of the given items causes an exception to be raised, none of the items is inserted. """ if items: - on_dup = self._get_on_dup((on_dup_key, on_dup_val, on_dup_kv)) self._update(False, on_dup, items) diff --git a/libs/bidict/_named.py b/libs/bidict/_named.py index 8748b98cc..b761060b7 100644 --- a/libs/bidict/_named.py +++ b/libs/bidict/_named.py @@ -1,34 +1,35 @@ # -*- coding: utf-8 -*- -# Copyright 2009-2019 Joshua Bronson. All Rights Reserved. +# Copyright 2009-2021 Joshua Bronson. All Rights Reserved. # # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. -"""Provides :func:`bidict.namedbidict`.""" +"""Provide :func:`bidict.namedbidict`.""" -import re +import typing as _t +from sys import _getframe -from ._abc import BidirectionalMapping +from ._abc import BidirectionalMapping, KT, VT from ._bidict import bidict -from .compat import PY2 -_isidentifier = ( # pylint: disable=invalid-name - re.compile('[A-Za-z_][A-Za-z0-9_]*$').match if PY2 else str.isidentifier -) - - -def namedbidict(typename, keyname, valname, base_type=bidict): +def namedbidict( + typename: str, + keyname: str, + valname: str, + *, + base_type: _t.Type[BidirectionalMapping[KT, VT]] = bidict, +) -> _t.Type[BidirectionalMapping[KT, VT]]: r"""Create a new subclass of *base_type* with custom accessors. - Analagous to :func:`collections.namedtuple`. + Like :func:`collections.namedtuple` for bidicts. - The new class's ``__name__`` and ``__qualname__`` - will be set based on *typename*. + The new class's ``__name__`` and ``__qualname__`` will be set to *typename*, + and its ``__module__`` will be set to the caller's module. - Instances of it will provide access to their - :attr:`inverse `\s + Instances of the new class will provide access to their + :attr:`inverse ` instances via the custom *keyname*\_for property, and access to themselves via the custom *valname*\_for property. @@ -39,63 +40,58 @@ def namedbidict(typename, keyname, valname, base_type=bidict): :raises ValueError: if any of the *typename*, *keyname*, or *valname* strings is not a valid Python identifier, or if *keyname == valname*. - :raises TypeError: if *base_type* is not a subclass of - :class:`BidirectionalMapping`. - (This function requires slightly more of *base_type*, - e.g. the availability of an ``_isinv`` attribute, - but all the :ref:`concrete bidict types - ` - that the :mod:`bidict` module provides can be passed in. - Check out the code if you actually need to pass in something else.) + :raises TypeError: if *base_type* is not a :class:`BidirectionalMapping` subclass + that provides ``_isinv`` and :meth:`~object.__getstate__` attributes. + (Any :class:`~bidict.BidictBase` subclass can be passed in, including all the + concrete bidict types pictured in the :ref:`other-bidict-types:Bidict Types Diagram`. """ - # Re the `base_type` docs above: - # The additional requirements (providing _isinv and __getstate__) do not belong in the - # BidirectionalMapping interface, and it's overkill to create additional interface(s) for this. - # On the other hand, it's overkill to require that base_type be a subclass of BidictBase, since - # that's too specific. The BidirectionalMapping check along with the docs above should suffice. - if not issubclass(base_type, BidirectionalMapping): + if not issubclass(base_type, BidirectionalMapping) or not all(hasattr(base_type, i) for i in ('_isinv', '__getstate__')): raise TypeError(base_type) names = (typename, keyname, valname) - if not all(map(_isidentifier, names)) or keyname == valname: + if not all(map(str.isidentifier, names)) or keyname == valname: raise ValueError(names) - class _Named(base_type): # pylint: disable=too-many-ancestors + class _Named(base_type): # type: ignore [valid-type,misc] __slots__ = () - def _getfwd(self): - return self.inverse if self._isinv else self + def _getfwd(self) -> '_Named': + return self.inverse if self._isinv else self # type: ignore [no-any-return] - def _getinv(self): - return self if self._isinv else self.inverse + def _getinv(self) -> '_Named': + return self if self._isinv else self.inverse # type: ignore [no-any-return] @property - def _keyname(self): + def _keyname(self) -> str: return valname if self._isinv else keyname @property - def _valname(self): + def _valname(self) -> str: return keyname if self._isinv else valname - def __reduce__(self): + def __reduce__(self) -> '_t.Tuple[_t.Callable[[str, str, str, _t.Type[BidirectionalMapping]], BidirectionalMapping], _t.Tuple[str, str, str, _t.Type[BidirectionalMapping]], dict]': return (_make_empty, (typename, keyname, valname, base_type), self.__getstate__()) bname = base_type.__name__ fname = valname + '_for' iname = keyname + '_for' - names = dict(typename=typename, bname=bname, keyname=keyname, valname=valname) - fdoc = u'{typename} forward {bname}: {keyname} → {valname}'.format(**names) - idoc = u'{typename} inverse {bname}: {valname} → {keyname}'.format(**names) - setattr(_Named, fname, property(_Named._getfwd, doc=fdoc)) # pylint: disable=protected-access - setattr(_Named, iname, property(_Named._getinv, doc=idoc)) # pylint: disable=protected-access + fdoc = f'{typename} forward {bname}: {keyname} → {valname}' + idoc = f'{typename} inverse {bname}: {valname} → {keyname}' + setattr(_Named, fname, property(_Named._getfwd, doc=fdoc)) + setattr(_Named, iname, property(_Named._getinv, doc=idoc)) - if not PY2: - _Named.__qualname__ = _Named.__qualname__[:-len(_Named.__name__)] + typename _Named.__name__ = typename + _Named.__qualname__ = typename + _Named.__module__ = _getframe(1).f_globals.get('__name__') # type: ignore [assignment] return _Named -def _make_empty(typename, keyname, valname, base_type): +def _make_empty( + typename: str, + keyname: str, + valname: str, + base_type: _t.Type[BidirectionalMapping] = bidict, +) -> BidirectionalMapping: """Create a named bidict with the indicated arguments and return an empty instance. Used to make :func:`bidict.namedbidict` instances picklable. """ diff --git a/libs/bidict/_noop.py b/libs/bidict/_noop.py deleted file mode 100644 index b045e8d72..000000000 --- a/libs/bidict/_noop.py +++ /dev/null @@ -1,14 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2009-2019 Joshua Bronson. All Rights Reserved. -# -# This Source Code Form is subject to the terms of the Mozilla Public -# License, v. 2.0. If a copy of the MPL was not distributed with this -# file, You can obtain one at http://mozilla.org/MPL/2.0/. - - -"""Provides the :obj:`_NOOP` sentinel, for internally signaling "no-op".""" - -from ._marker import _Marker - - -_NOOP = _Marker('NO-OP') diff --git a/libs/bidict/_orderedbase.py b/libs/bidict/_orderedbase.py index aa085a2d5..8681d3be5 100644 --- a/libs/bidict/_orderedbase.py +++ b/libs/bidict/_orderedbase.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2009-2019 Joshua Bronson. All Rights Reserved. +# Copyright 2009-2021 Joshua Bronson. All Rights Reserved. # # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this @@ -26,17 +26,19 @@ #============================================================================== -"""Provides :class:`OrderedBidictBase`.""" +"""Provide :class:`OrderedBidictBase`.""" +import typing as _t +from copy import copy from weakref import ref -from ._base import _WriteResult, BidictBase +from ._abc import MutableBidirectionalMapping +from ._base import _NONE, _DedupResult, _WriteResult, BidictBase, BT from ._bidict import bidict -from ._miss import _MISS -from .compat import Mapping, PY2, iteritems, izip +from ._typing import KT, VT, OKT, OVT, IterItems, MapOrIterItems -class _Node(object): # pylint: disable=too-few-public-methods +class _Node: """A node in a circular doubly-linked list used to encode the order of items in an ordered bidict. @@ -55,33 +57,33 @@ class _Node(object): # pylint: disable=too-few-public-methods __slots__ = ('_prv', '_nxt', '__weakref__') - def __init__(self, prv=None, nxt=None): + def __init__(self, prv: '_Node' = None, nxt: '_Node' = None) -> None: self._setprv(prv) self._setnxt(nxt) - def __repr__(self): # pragma: no cover + def __repr__(self) -> str: clsname = self.__class__.__name__ prv = id(self.prv) nxt = id(self.nxt) - return '%s(prv=%s, self=%s, nxt=%s)' % (clsname, prv, id(self), nxt) + return f'{clsname}(prv={prv}, self={id(self)}, nxt={nxt})' - def _getprv(self): + def _getprv(self) -> '_t.Optional[_Node]': return self._prv() if isinstance(self._prv, ref) else self._prv - def _setprv(self, prv): + def _setprv(self, prv: '_t.Optional[_Node]') -> None: self._prv = prv and ref(prv) prv = property(_getprv, _setprv) - def _getnxt(self): + def _getnxt(self) -> '_t.Optional[_Node]': return self._nxt() if isinstance(self._nxt, ref) else self._nxt - def _setnxt(self, nxt): + def _setnxt(self, nxt: '_t.Optional[_Node]') -> None: self._nxt = nxt and ref(nxt) nxt = property(_getnxt, _setnxt) - def __getstate__(self): + def __getstate__(self) -> dict: """Return the instance state dictionary but with weakrefs converted to strong refs so that it can be pickled. @@ -90,13 +92,13 @@ class _Node(object): # pylint: disable=too-few-public-methods """ return dict(_prv=self.prv, _nxt=self.nxt) - def __setstate__(self, state): + def __setstate__(self, state: dict) -> None: """Set the instance state from *state*.""" self._setprv(state['_prv']) self._setnxt(state['_nxt']) -class _Sentinel(_Node): # pylint: disable=too-few-public-methods +class _SentinelNode(_Node): """Special node in a circular doubly-linked list that links the first node with the last node. When its next and previous references point back to itself @@ -105,19 +107,16 @@ class _Sentinel(_Node): # pylint: disable=too-few-public-methods __slots__ = () - def __init__(self, prv=None, nxt=None): - super(_Sentinel, self).__init__(prv or self, nxt or self) + def __init__(self, prv: _Node = None, nxt: _Node = None) -> None: + super().__init__(prv or self, nxt or self) - def __repr__(self): # pragma: no cover - return '' + def __repr__(self) -> str: + return '' - def __bool__(self): + def __bool__(self) -> bool: return False - if PY2: - __nonzero__ = __bool__ - - def __iter__(self, reverse=False): + def _iter(self, *, reverse: bool = False) -> _t.Iterator[_Node]: """Iterator yielding nodes in the requested order, i.e. traverse the linked list via :attr:`nxt` (or :attr:`prv` if *reverse* is truthy) @@ -130,26 +129,35 @@ class _Sentinel(_Node): # pylint: disable=too-few-public-methods node = getattr(node, attr) -class OrderedBidictBase(BidictBase): +class OrderedBidictBase(BidictBase[KT, VT]): """Base class implementing an ordered :class:`BidirectionalMapping`.""" __slots__ = ('_sntl',) - _fwdm_cls = bidict - _invm_cls = bidict + _fwdm_cls: _t.Type[MutableBidirectionalMapping[KT, _Node]] = bidict # type: ignore [assignment] + _invm_cls: _t.Type[MutableBidirectionalMapping[VT, _Node]] = bidict # type: ignore [assignment] + _fwdm: bidict[KT, _Node] # type: ignore [assignment] + _invm: bidict[VT, _Node] # type: ignore [assignment] #: The object used by :meth:`__repr__` for printing the contained items. _repr_delegate = list - def __init__(self, *args, **kw): + @_t.overload + def __init__(self, __arg: _t.Mapping[KT, VT], **kw: VT) -> None: ... + @_t.overload + def __init__(self, __arg: IterItems[KT, VT], **kw: VT) -> None: ... + @_t.overload + def __init__(self, **kw: VT) -> None: ... + def __init__(self, *args: MapOrIterItems[KT, VT], **kw: VT) -> None: """Make a new ordered bidirectional mapping. - The signature is the same as that of regular dictionaries. + The signature behaves like that of :class:`dict`. Items passed in are added in the order they are passed, - respecting this bidict type's duplication policies along the way. + respecting the :attr:`on_dup` class attribute in the process. + The order in which items are inserted is remembered, similar to :class:`collections.OrderedDict`. """ - self._sntl = _Sentinel() + self._sntl = _SentinelNode() # Like unordered bidicts, ordered bidicts also store two backing one-directional mappings # `_fwdm` and `_invm`. But rather than mapping `key` to `val` and `val` to `key` @@ -159,55 +167,58 @@ class OrderedBidictBase(BidictBase): # To effect this difference, `_write_item` and `_undo_write` are overridden. But much of the # rest of BidictBase's implementation, including BidictBase.__init__ and BidictBase._update, # are inherited and are able to be reused without modification. - super(OrderedBidictBase, self).__init__(*args, **kw) + super().__init__(*args, **kw) - def _init_inv(self): - super(OrderedBidictBase, self)._init_inv() - self.inverse._sntl = self._sntl # pylint: disable=protected-access + if _t.TYPE_CHECKING: + @property + def inverse(self) -> 'OrderedBidictBase[VT, KT]': ... + + def _init_inv(self) -> None: + super()._init_inv() + self.inverse._sntl = self._sntl # Can't reuse BidictBase.copy since ordered bidicts have different internal structure. - def copy(self): + def copy(self: BT) -> BT: """A shallow copy of this ordered bidict.""" # Fast copy implementation bypassing __init__. See comments in :meth:`BidictBase.copy`. - copy = self.__class__.__new__(self.__class__) - sntl = _Sentinel() - fwdm = self._fwdm.copy() - invm = self._invm.copy() + cp: BT = self.__class__.__new__(self.__class__) + sntl = _SentinelNode() + fwdm = copy(self._fwdm) + invm = copy(self._invm) cur = sntl nxt = sntl.nxt - for (key, val) in iteritems(self): + for (key, val) in self.items(): nxt = _Node(cur, sntl) cur.nxt = fwdm[key] = invm[val] = nxt cur = nxt sntl.prv = nxt - copy._sntl = sntl # pylint: disable=protected-access - copy._fwdm = fwdm # pylint: disable=protected-access - copy._invm = invm # pylint: disable=protected-access - copy._init_inv() # pylint: disable=protected-access - return copy + cp._sntl = sntl # type: ignore [attr-defined] + cp._fwdm = fwdm + cp._invm = invm + cp._init_inv() + return cp - def __getitem__(self, key): + __copy__ = copy + + def __getitem__(self, key: KT) -> VT: nodefwd = self._fwdm[key] val = self._invm.inverse[nodefwd] return val - def _pop(self, key): + def _pop(self, key: KT) -> VT: nodefwd = self._fwdm.pop(key) val = self._invm.inverse.pop(nodefwd) nodefwd.prv.nxt = nodefwd.nxt nodefwd.nxt.prv = nodefwd.prv return val - def _isdupitem(self, key, val, dedup_result): - """Return whether (key, val) duplicates an existing item.""" - isdupkey, isdupval, nodeinv, nodefwd = dedup_result - isdupitem = nodeinv is nodefwd - if isdupitem: - assert isdupkey - assert isdupval - return isdupitem + @staticmethod + def _already_have(key: KT, val: VT, nodeinv: _Node, nodefwd: _Node) -> bool: # type: ignore [override] + # Overrides _base.BidictBase. + return nodeinv is nodefwd - def _write_item(self, key, val, dedup_result): # pylint: disable=too-many-locals + def _write_item(self, key: KT, val: VT, dedup_result: _DedupResult) -> _WriteResult: + # Overrides _base.BidictBase. fwdm = self._fwdm # bidict mapping keys to nodes invm = self._invm # bidict mapping vals to nodes isdupkey, isdupval, nodeinv, nodefwd = dedup_result @@ -217,7 +228,8 @@ class OrderedBidictBase(BidictBase): last = sntl.prv node = _Node(last, sntl) last.nxt = sntl.prv = fwdm[key] = invm[val] = node - oldkey = oldval = _MISS + oldkey: OKT = _NONE + oldval: OVT = _NONE elif isdupkey and isdupval: # Key and value duplication across two different nodes. assert nodefwd is not nodeinv @@ -239,19 +251,19 @@ class OrderedBidictBase(BidictBase): fwdm[key] = invm[val] = nodefwd elif isdupkey: oldval = invm.inverse[nodefwd] - oldkey = _MISS + oldkey = _NONE oldnodeinv = invm.pop(oldval) assert oldnodeinv is nodefwd invm[val] = nodefwd else: # isdupval oldkey = fwdm.inverse[nodeinv] - oldval = _MISS + oldval = _NONE oldnodefwd = fwdm.pop(oldkey) assert oldnodefwd is nodeinv fwdm[key] = nodeinv return _WriteResult(key, val, oldkey, oldval) - def _undo_write(self, dedup_result, write_result): # pylint: disable=too-many-locals + def _undo_write(self, dedup_result: _DedupResult, write_result: _WriteResult) -> None: fwdm = self._fwdm invm = self._invm isdupkey, isdupval, nodeinv, nodefwd = dedup_result @@ -274,26 +286,18 @@ class OrderedBidictBase(BidictBase): fwdm[oldkey] = nodeinv assert invm[val] is nodeinv - def __iter__(self, reverse=False): - """An iterator over this bidict's items in order.""" + def __iter__(self) -> _t.Iterator[KT]: + """Iterator over the contained keys in insertion order.""" + return self._iter() + + def _iter(self, *, reverse: bool = False) -> _t.Iterator[KT]: fwdm_inv = self._fwdm.inverse - for node in self._sntl.__iter__(reverse=reverse): + for node in self._sntl._iter(reverse=reverse): yield fwdm_inv[node] - def __reversed__(self): - """An iterator over this bidict's items in reverse order.""" - for key in self.__iter__(reverse=True): - yield key - - def equals_order_sensitive(self, other): - """Order-sensitive equality check. - - *See also* :ref:`eq-order-insensitive` - """ - # Same short-circuit as BidictBase.__eq__. Factoring out not worth function call overhead. - if not isinstance(other, Mapping) or len(self) != len(other): - return False - return all(i == j for (i, j) in izip(iteritems(self), iteritems(other))) + def __reversed__(self) -> _t.Iterator[KT]: + """Iterator over the contained keys in reverse insertion order.""" + yield from self._iter(reverse=True) # * Code review nav * diff --git a/libs/bidict/_orderedbidict.py b/libs/bidict/_orderedbidict.py index 874954838..f7aed3b65 100644 --- a/libs/bidict/_orderedbidict.py +++ b/libs/bidict/_orderedbidict.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2009-2019 Joshua Bronson. All Rights Reserved. +# Copyright 2009-2021 Joshua Bronson. All Rights Reserved. # # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this @@ -26,26 +26,32 @@ #============================================================================== -"""Provides :class:`OrderedBidict`.""" +"""Provide :class:`OrderedBidict`.""" + +import typing as _t from ._mut import MutableBidict from ._orderedbase import OrderedBidictBase +from ._typing import KT, VT -class OrderedBidict(OrderedBidictBase, MutableBidict): +class OrderedBidict(OrderedBidictBase[KT, VT], MutableBidict[KT, VT]): """Mutable bidict type that maintains items in insertion order.""" __slots__ = () - __hash__ = None # since this class is mutable; explicit > implicit. - def clear(self): + if _t.TYPE_CHECKING: + @property + def inverse(self) -> 'OrderedBidict[VT, KT]': ... + + def clear(self) -> None: """Remove all items.""" self._fwdm.clear() self._invm.clear() self._sntl.nxt = self._sntl.prv = self._sntl - def popitem(self, last=True): # pylint: disable=arguments-differ - u"""*x.popitem() → (k, v)* + def popitem(self, last: bool = True) -> _t.Tuple[KT, VT]: + """*x.popitem() → (k, v)* Remove and return the most recently added item as a (key, value) pair if *last* is True, else the least recently added item. @@ -54,11 +60,13 @@ class OrderedBidict(OrderedBidictBase, MutableBidict): """ if not self: raise KeyError('mapping is empty') - key = next((reversed if last else iter)(self)) + itfn: _t.Callable = reversed if last else iter # type: ignore [assignment] + it = itfn(self) + key = next(it) val = self._pop(key) return key, val - def move_to_end(self, key, last=True): + def move_to_end(self, key: KT, last: bool = True) -> None: """Move an existing key to the beginning or end of this ordered bidict. The item is moved to the end if *last* is True, else to the beginning. @@ -70,15 +78,15 @@ class OrderedBidict(OrderedBidictBase, MutableBidict): node.nxt.prv = node.prv sntl = self._sntl if last: - last = sntl.prv - node.prv = last + lastnode = sntl.prv + node.prv = lastnode node.nxt = sntl - sntl.prv = last.nxt = node + sntl.prv = lastnode.nxt = node else: - first = sntl.nxt + firstnode = sntl.nxt node.prv = sntl - node.nxt = first - sntl.nxt = first.prv = node + node.nxt = firstnode + sntl.nxt = firstnode.prv = node # * Code review nav * diff --git a/libs/bidict/_typing.py b/libs/bidict/_typing.py new file mode 100644 index 000000000..cb10baae3 --- /dev/null +++ b/libs/bidict/_typing.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8 -*- +# Copyright 2009-2021 Joshua Bronson. All Rights Reserved. +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +"""Provide typing-related objects.""" + +import typing as _t + + +KT = _t.TypeVar('KT') +VT = _t.TypeVar('VT') +IterItems = _t.Iterable[_t.Tuple[KT, VT]] +MapOrIterItems = _t.Union[_t.Mapping[KT, VT], IterItems[KT, VT]] + +DT = _t.TypeVar('DT') #: for default arguments +VDT = _t.Union[VT, DT] + + +class _BareReprMeta(type): + def __repr__(cls) -> str: + return f'<{cls.__name__}>' + + +class _NONE(metaclass=_BareReprMeta): + """Sentinel type used to represent 'missing'.""" + + +OKT = _t.Union[KT, _NONE] #: optional key type +OVT = _t.Union[VT, _NONE] #: optional value type diff --git a/libs/bidict/compat.py b/libs/bidict/compat.py deleted file mode 100644 index dc095c920..000000000 --- a/libs/bidict/compat.py +++ /dev/null @@ -1,78 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2009-2019 Joshua Bronson. All Rights Reserved. -# -# This Source Code Form is subject to the terms of the Mozilla Public -# License, v. 2.0. If a copy of the MPL was not distributed with this -# file, You can obtain one at http://mozilla.org/MPL/2.0/. - - -"""Compatibility helpers.""" - -from operator import methodcaller -from platform import python_implementation -from sys import version_info -from warnings import warn - - -# Use #: (before or) at the end of each line with a member we want to show up in the docs, -# otherwise Sphinx won't include (even though we configure automodule with undoc-members). - -PYMAJOR, PYMINOR = version_info[:2] #: -PY2 = PYMAJOR == 2 #: -PYIMPL = python_implementation() #: -CPY = PYIMPL == 'CPython' #: -PYPY = PYIMPL == 'PyPy' #: -DICTS_ORDERED = PYPY or (CPY and (PYMAJOR, PYMINOR) >= (3, 6)) #: - -# Without the following, pylint gives lots of false positives. -# pylint: disable=invalid-name,unused-import,ungrouped-imports,no-name-in-module - -if PY2: - if PYMINOR < 7: # pragma: no cover - raise ImportError('Python 2.7 or 3.5+ is required.') - warn('Python 2 support will be dropped in a future release.') - - # abstractproperty deprecated in Python 3.3 in favor of using @property with @abstractmethod. - # Before 3.3, this silently fails to detect when an abstract property has not been overridden. - from abc import abstractproperty #: - - from itertools import izip #: - - # In Python 3, the collections ABCs were moved into collections.abc, which does not exist in - # Python 2. Support for importing them directly from collections is dropped in Python 3.8. - import collections as collections_abc # noqa: F401 (imported but unused) - from collections import ( # noqa: F401 (imported but unused) - Mapping, MutableMapping, KeysView, ValuesView, ItemsView) - - viewkeys = lambda m: m.viewkeys() if hasattr(m, 'viewkeys') else KeysView(m) #: - viewvalues = lambda m: m.viewvalues() if hasattr(m, 'viewvalues') else ValuesView(m) #: - viewitems = lambda m: m.viewitems() if hasattr(m, 'viewitems') else ItemsView(m) #: - - iterkeys = lambda m: m.iterkeys() if hasattr(m, 'iterkeys') else iter(m.keys()) #: - itervalues = lambda m: m.itervalues() if hasattr(m, 'itervalues') else iter(m.values()) #: - iteritems = lambda m: m.iteritems() if hasattr(m, 'iteritems') else iter(m.items()) #: - -else: - # Assume Python 3 when not PY2, but explicitly check before showing this warning. - if PYMAJOR == 3 and PYMINOR < 5: # pragma: no cover - warn('Python 3.4 and below are not supported.') - - import collections.abc as collections_abc # noqa: F401 (imported but unused) - from collections.abc import ( # noqa: F401 (imported but unused) - Mapping, MutableMapping, KeysView, ValuesView, ItemsView) - - viewkeys = methodcaller('keys') #: - viewvalues = methodcaller('values') #: - viewitems = methodcaller('items') #: - - def _compose(f, g): - return lambda x: f(g(x)) - - iterkeys = _compose(iter, viewkeys) #: - itervalues = _compose(iter, viewvalues) #: - iteritems = _compose(iter, viewitems) #: - - from abc import abstractmethod - abstractproperty = _compose(property, abstractmethod) #: - - izip = zip #: diff --git a/libs/bidict/metadata.py b/libs/bidict/metadata.py index 95ec8af78..733bffe93 100644 --- a/libs/bidict/metadata.py +++ b/libs/bidict/metadata.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2009-2019 Joshua Bronson. All Rights Reserved. +# Copyright 2009-2021 Joshua Bronson. All Rights Reserved. # # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this @@ -8,42 +8,22 @@ """Define bidict package metadata.""" -__version__ = '0.0.0.VERSION_NOT_FOUND' - -# _version.py is generated by setuptools_scm (via its `write_to` param, see setup.py) -try: - from ._version import version as __version__ # pylint: disable=unused-import -except (ImportError, ValueError, SystemError): # pragma: no cover - try: - import pkg_resources - except ImportError: - pass - else: - try: - __version__ = pkg_resources.get_distribution('bidict').version - except pkg_resources.DistributionNotFound: - pass - -try: - __version_info__ = tuple(int(p) if i < 3 else p for (i, p) in enumerate(__version__.split('.'))) -except Exception: # noqa: E722; pragma: no cover; pylint: disable=broad-except - __vesion_info__ = (0, 0, 0, 'PARSE FAILURE: __version__=%s' % __version__) - -__author__ = u'Joshua Bronson' -__maintainer__ = u'Joshua Bronson' -__copyright__ = u'Copyright 2019 Joshua Bronson' -__email__ = u'jab@math.brown.edu' +__version__ = '0.21.4' +__author__ = 'Joshua Bronson' +__maintainer__ = 'Joshua Bronson' +__copyright__ = 'Copyright 2009-2021 Joshua Bronson' +__email__ = 'jabronson@gmail.com' # See: ../docs/thanks.rst -__credits__ = [i.strip() for i in u""" +__credits__ = [i.strip() for i in """ Joshua Bronson, Michael Arntzenius, Francis Carr, Gregory Ewing, Raymond Hettinger, Jozef Knaperek, Daniel Pope, Terry Reedy, David Turner, Tom Viner, Richard Sanger, Zeyi Wang -""".split(u',')] +""".split(',')] -__description__ = u'Efficient, Pythonic bidirectional map implementation and related functionality' +__description__ = 'The bidirectional mapping library for Python.' __keywords__ = 'dict dictionary mapping datastructure bimap bijection bijective ' \ 'injective inverse reverse bidirectional two-way 2-way' -__license__ = u'MPL 2.0' -__status__ = u'Beta' -__url__ = u'https://bidict.readthedocs.io' +__license__ = 'MPL 2.0' +__status__ = 'Beta' +__url__ = 'https://bidict.readthedocs.io' diff --git a/libs/asio/interfaces/__init__.py b/libs/bidict/py.typed similarity index 100% rename from libs/asio/interfaces/__init__.py rename to libs/bidict/py.typed diff --git a/libs/bs4/__init__.py b/libs/bs4/__init__.py index 95ca229c1..2a436d343 100644 --- a/libs/bs4/__init__.py +++ b/libs/bs4/__init__.py @@ -1,6 +1,5 @@ -"""Beautiful Soup -Elixir and Tonic -"The Screen-Scraper's Friend" +"""Beautiful Soup Elixir and Tonic - "The Screen-Scraper's Friend". + http://www.crummy.com/software/BeautifulSoup/ Beautiful Soup uses a pluggable XML or HTML parser to parse a @@ -8,29 +7,34 @@ Beautiful Soup uses a pluggable XML or HTML parser to parse a provides methods and Pythonic idioms that make it easy to navigate, search, and modify the parse tree. -Beautiful Soup works with Python 2.7 and up. It works better if lxml +Beautiful Soup works with Python 3.5 and up. It works better if lxml and/or html5lib is installed. For more than you ever wanted to know about Beautiful Soup, see the -documentation: -http://www.crummy.com/software/BeautifulSoup/bs4/doc/ - +documentation: http://www.crummy.com/software/BeautifulSoup/bs4/doc/ """ __author__ = "Leonard Richardson (leonardr@segfault.org)" -__version__ = "4.8.0" -__copyright__ = "Copyright (c) 2004-2019 Leonard Richardson" +__version__ = "4.10.0" +__copyright__ = "Copyright (c) 2004-2021 Leonard Richardson" # Use of this source code is governed by the MIT license. __license__ = "MIT" __all__ = ['BeautifulSoup'] + +from collections import Counter import os import re import sys import traceback import warnings +# The very first thing we do is give a useful error if someone is +# running this code under Python 2. +if sys.version_info.major < 3: + raise ImportError('You are trying to use a Python 3-specific version of Beautiful Soup under Python 2. This will not work. The final version of Beautiful Soup to support Python 2 was 4.9.3.') + from .builder import builder_registry, ParserRejectedMarkup from .dammit import UnicodeDammit from .element import ( @@ -42,28 +46,49 @@ from .element import ( NavigableString, PageElement, ProcessingInstruction, + PYTHON_SPECIFIC_ENCODINGS, ResultSet, + Script, + Stylesheet, SoupStrainer, Tag, + TemplateString, ) -# The very first thing we do is give a useful error if someone is -# running this code under Python 3 without converting it. -'You are trying to run the Python 2 version of Beautiful Soup under Python 3. This will not work.'!='You need to convert the code, either by installing it (`python setup.py install`) or by running 2to3 (`2to3 -w bs4`).' +# Define some custom warnings. +class GuessedAtParserWarning(UserWarning): + """The warning issued when BeautifulSoup has to guess what parser to + use -- probably because no parser was specified in the constructor. + """ + +class MarkupResemblesLocatorWarning(UserWarning): + """The warning issued when BeautifulSoup is given 'markup' that + actually looks like a resource locator -- a URL or a path to a file + on disk. + """ + class BeautifulSoup(Tag): - """ - This class defines the basic interface called by the tree builders. + """A data structure representing a parsed HTML or XML document. - These methods will be called by the parser: - reset() - feed(markup) + Most of the methods you'll call on a BeautifulSoup object are inherited from + PageElement or Tag. + + Internally, this class defines the basic interface called by the + tree builders when converting an HTML/XML document into a data + structure. The interface abstracts away the differences between + parsers. To write a new tree builder, you'll need to understand + these methods as a whole. + + These methods will be called by the BeautifulSoup constructor: + * reset() + * feed(markup) The tree builder may call these methods from its feed() implementation: - handle_starttag(name, attrs) # See note about return value - handle_endtag(name) - handle_data(data) # Appends to the current data node - endData(containerClass=NavigableString) # Ends the current data node + * handle_starttag(name, attrs) # See note about return value + * handle_endtag(name) + * handle_data(data) # Appends to the current data node + * endData(containerClass) # Ends the current data node No matter how complicated the underlying parser is, you should be able to build a tree using 'start tag' events, 'end tag' events, @@ -73,62 +98,75 @@ class BeautifulSoup(Tag): like HTML's
tag), call handle_starttag and then handle_endtag. """ + + # Since BeautifulSoup subclasses Tag, it's possible to treat it as + # a Tag with a .name. This name makes it clear the BeautifulSoup + # object isn't a real markup tag. ROOT_TAG_NAME = '[document]' # If the end-user gives no indication which tree builder they # want, look for one with these features. DEFAULT_BUILDER_FEATURES = ['html', 'fast'] + # A string containing all ASCII whitespace characters, used in + # endData() to detect data chunks that seem 'empty'. ASCII_SPACES = '\x20\x0a\x09\x0c\x0d' NO_PARSER_SPECIFIED_WARNING = "No parser was explicitly specified, so I'm using the best available %(markup_type)s parser for this system (\"%(parser)s\"). This usually isn't a problem, but if you run this code on another system, or in a different virtual environment, it may use a different parser and behave differently.\n\nThe code that caused this warning is on line %(line_number)s of the file %(filename)s. To get rid of this warning, pass the additional argument 'features=\"%(parser)s\"' to the BeautifulSoup constructor.\n" - + def __init__(self, markup="", features=None, builder=None, parse_only=None, from_encoding=None, exclude_encodings=None, - **kwargs): + element_classes=None, **kwargs): """Constructor. :param markup: A string or a file-like object representing - markup to be parsed. + markup to be parsed. - :param features: Desirable features of the parser to be used. This - may be the name of a specific parser ("lxml", "lxml-xml", - "html.parser", or "html5lib") or it may be the type of markup - to be used ("html", "html5", "xml"). It's recommended that you - name a specific parser, so that Beautiful Soup gives you the - same results across platforms and virtual environments. + :param features: Desirable features of the parser to be + used. This may be the name of a specific parser ("lxml", + "lxml-xml", "html.parser", or "html5lib") or it may be the + type of markup to be used ("html", "html5", "xml"). It's + recommended that you name a specific parser, so that + Beautiful Soup gives you the same results across platforms + and virtual environments. :param builder: A TreeBuilder subclass to instantiate (or - instance to use) instead of looking one up based on - `features`. You only need to use this if you've implemented a - custom TreeBuilder. + instance to use) instead of looking one up based on + `features`. You only need to use this if you've implemented a + custom TreeBuilder. :param parse_only: A SoupStrainer. Only parts of the document - matching the SoupStrainer will be considered. This is useful - when parsing part of a document that would otherwise be too - large to fit into memory. + matching the SoupStrainer will be considered. This is useful + when parsing part of a document that would otherwise be too + large to fit into memory. :param from_encoding: A string indicating the encoding of the - document to be parsed. Pass this in if Beautiful Soup is - guessing wrongly about the document's encoding. + document to be parsed. Pass this in if Beautiful Soup is + guessing wrongly about the document's encoding. :param exclude_encodings: A list of strings indicating - encodings known to be wrong. Pass this in if you don't know - the document's encoding but you know Beautiful Soup's guess is - wrong. + encodings known to be wrong. Pass this in if you don't know + the document's encoding but you know Beautiful Soup's guess is + wrong. + + :param element_classes: A dictionary mapping BeautifulSoup + classes like Tag and NavigableString, to other classes you'd + like to be instantiated instead as the parse tree is + built. This is useful for subclassing Tag or NavigableString + to modify default behavior. :param kwargs: For backwards compatibility purposes, the - constructor accepts certain keyword arguments used in - Beautiful Soup 3. None of these arguments do anything in - Beautiful Soup 4; they will result in a warning and then be ignored. - - Apart from this, any keyword arguments passed into the BeautifulSoup - constructor are propagated to the TreeBuilder constructor. This - makes it possible to configure a TreeBuilder beyond saying - which one to use. - + constructor accepts certain keyword arguments used in + Beautiful Soup 3. None of these arguments do anything in + Beautiful Soup 4; they will result in a warning and then be + ignored. + + Apart from this, any keyword arguments passed into the + BeautifulSoup constructor are propagated to the TreeBuilder + constructor. This makes it possible to configure a + TreeBuilder by passing in arguments, not just by saying which + one to use. """ - if 'convertEntities' in kwargs: del kwargs['convertEntities'] warnings.warn( @@ -185,6 +223,8 @@ class BeautifulSoup(Tag): warnings.warn("You provided Unicode markup but also provided a value for from_encoding. Your from_encoding will be ignored.") from_encoding = None + self.element_classes = element_classes or dict() + # We need this information to track whether or not the builder # was specified well enough that we can omit the 'you need to # specify a parser' warning. @@ -215,7 +255,9 @@ class BeautifulSoup(Tag): if not original_builder and not ( original_features == builder.NAME or original_features in builder.ALTERNATE_NAMES - ): + ) and markup: + # The user did not tell us which TreeBuilder to use, + # and we had to guess. Issue a warning. if builder.is_xml: markup_type = "XML" else: @@ -249,7 +291,10 @@ class BeautifulSoup(Tag): parser=builder.NAME, markup_type=markup_type ) - warnings.warn(self.NO_PARSER_SPECIFIED_WARNING % values, stacklevel=2) + warnings.warn( + self.NO_PARSER_SPECIFIED_WARNING % values, + GuessedAtParserWarning, stacklevel=2 + ) else: if kwargs: warnings.warn("Keyword arguments to the BeautifulSoup constructor will be ignored. These would normally be passed into the TreeBuilder constructor, but a TreeBuilder instance was passed in as `builder`.") @@ -278,22 +323,36 @@ class BeautifulSoup(Tag): else: possible_filename = markup is_file = False + is_directory = False try: is_file = os.path.exists(possible_filename) + if is_file: + is_directory = os.path.isdir(possible_filename) except Exception as e: # This is almost certainly a problem involving # characters not valid in filenames on this # system. Just let it go. pass - if is_file: - if isinstance(markup, str): - markup = markup.encode("utf8") + if is_directory: + warnings.warn( + '"%s" looks like a directory name, not markup. You may' + ' want to open a file found in this directory and pass' + ' the filehandle into Beautiful Soup.' % ( + self._decode_markup(markup) + ), + MarkupResemblesLocatorWarning + ) + elif is_file: warnings.warn( '"%s" looks like a filename, not markup. You should' ' probably open this file and pass the filehandle into' - ' Beautiful Soup.' % markup) + ' Beautiful Soup.' % self._decode_markup(markup), + MarkupResemblesLocatorWarning + ) self._check_markup_is_url(markup) + rejections = [] + success = False for (self.markup, self.original_encoding, self.declared_html_encoding, self.contains_replacement_characters) in ( self.builder.prepare_markup( @@ -301,16 +360,25 @@ class BeautifulSoup(Tag): self.reset() try: self._feed() + success = True break - except ParserRejectedMarkup: + except ParserRejectedMarkup as e: + rejections.append(e) pass + if not success: + other_exceptions = [str(e) for e in rejections] + raise ParserRejectedMarkup( + "The markup you provided was rejected by the parser. Trying a different parser or a different encoding may help.\n\nOriginal exception(s) from parser:\n " + "\n ".join(other_exceptions) + ) + # Clear out the markup and remove the builder's circular # reference to this object. self.markup = None self.builder.soup = None def __copy__(self): + """Copy a BeautifulSoup object by converting the document to a string and parsing it again.""" copy = type(self)( self.encode('utf-8'), builder=self.builder, from_encoding='utf-8' ) @@ -329,11 +397,25 @@ class BeautifulSoup(Tag): d['builder'] = None return d - @staticmethod - def _check_markup_is_url(markup): - """ - Check if markup looks like it's actually a url and raise a warning - if so. Markup can be unicode or str (py2) / bytes (py3). + @classmethod + def _decode_markup(cls, markup): + """Ensure `markup` is bytes so it's safe to send into warnings.warn. + + TODO: warnings.warn had this problem back in 2010 but it might not + anymore. + """ + if isinstance(markup, bytes): + decoded = markup.decode('utf-8', 'replace') + else: + decoded = markup + return decoded + + @classmethod + def _check_markup_is_url(cls, markup): + """Error-handling method to raise a warning if incoming markup looks + like a URL. + + :param markup: A string. """ if isinstance(markup, bytes): space = b' ' @@ -346,18 +428,20 @@ class BeautifulSoup(Tag): if any(markup.startswith(prefix) for prefix in cant_start_with): if not space in markup: - if isinstance(markup, bytes): - decoded_markup = markup.decode('utf-8', 'replace') - else: - decoded_markup = markup warnings.warn( '"%s" looks like a URL. Beautiful Soup is not an' ' HTTP client. You should probably use an HTTP client like' ' requests to get the document behind the URL, and feed' - ' that document to Beautiful Soup.' % decoded_markup + ' that document to Beautiful Soup.' % cls._decode_markup( + markup + ), + MarkupResemblesLocatorWarning ) def _feed(self): + """Internal method that parses previously set markup, creating a large + number of Tag and NavigableString objects. + """ # Convert the document to Unicode. self.builder.reset() @@ -368,49 +452,110 @@ class BeautifulSoup(Tag): self.popTag() def reset(self): + """Reset this object to a state as though it had never parsed any + markup. + """ Tag.__init__(self, self, self.builder, self.ROOT_TAG_NAME) self.hidden = 1 self.builder.reset() self.current_data = [] self.currentTag = None self.tagStack = [] + self.open_tag_counter = Counter() self.preserve_whitespace_tag_stack = [] + self.string_container_stack = [] self.pushTag(self) - def new_tag(self, name, namespace=None, nsprefix=None, attrs={}, **kwattrs): - """Create a new tag associated with this soup.""" + def new_tag(self, name, namespace=None, nsprefix=None, attrs={}, + sourceline=None, sourcepos=None, **kwattrs): + """Create a new Tag associated with this BeautifulSoup object. + + :param name: The name of the new Tag. + :param namespace: The URI of the new Tag's XML namespace, if any. + :param prefix: The prefix for the new Tag's XML namespace, if any. + :param attrs: A dictionary of this Tag's attribute values; can + be used instead of `kwattrs` for attributes like 'class' + that are reserved words in Python. + :param sourceline: The line number where this tag was + (purportedly) found in its source document. + :param sourcepos: The character position within `sourceline` where this + tag was (purportedly) found. + :param kwattrs: Keyword arguments for the new Tag's attribute values. + + """ kwattrs.update(attrs) - return Tag(None, self.builder, name, namespace, nsprefix, kwattrs) + return self.element_classes.get(Tag, Tag)( + None, self.builder, name, namespace, nsprefix, kwattrs, + sourceline=sourceline, sourcepos=sourcepos + ) - def new_string(self, s, subclass=NavigableString): - """Create a new NavigableString associated with this soup.""" - return subclass(s) + def string_container(self, base_class=None): + container = base_class or NavigableString + + # There may be a general override of NavigableString. + container = self.element_classes.get( + container, container + ) - def insert_before(self, successor): + # On top of that, we may be inside a tag that needs a special + # container class. + if self.string_container_stack and container is NavigableString: + container = self.builder.string_containers.get( + self.string_container_stack[-1].name, container + ) + return container + + def new_string(self, s, subclass=None): + """Create a new NavigableString associated with this BeautifulSoup + object. + """ + container = self.string_container(subclass) + return container(s) + + def insert_before(self, *args): + """This method is part of the PageElement API, but `BeautifulSoup` doesn't implement + it because there is nothing before or after it in the parse tree. + """ raise NotImplementedError("BeautifulSoup objects don't support insert_before().") - def insert_after(self, successor): + def insert_after(self, *args): + """This method is part of the PageElement API, but `BeautifulSoup` doesn't implement + it because there is nothing before or after it in the parse tree. + """ raise NotImplementedError("BeautifulSoup objects don't support insert_after().") def popTag(self): + """Internal method called by _popToTag when a tag is closed.""" tag = self.tagStack.pop() + if tag.name in self.open_tag_counter: + self.open_tag_counter[tag.name] -= 1 if self.preserve_whitespace_tag_stack and tag == self.preserve_whitespace_tag_stack[-1]: self.preserve_whitespace_tag_stack.pop() - #print "Pop", tag.name + if self.string_container_stack and tag == self.string_container_stack[-1]: + self.string_container_stack.pop() + #print("Pop", tag.name) if self.tagStack: self.currentTag = self.tagStack[-1] return self.currentTag def pushTag(self, tag): - #print "Push", tag.name + """Internal method called by handle_starttag when a tag is opened.""" + #print("Push", tag.name) if self.currentTag is not None: self.currentTag.contents.append(tag) self.tagStack.append(tag) self.currentTag = self.tagStack[-1] + if tag.name != self.ROOT_TAG_NAME: + self.open_tag_counter[tag.name] += 1 if tag.name in self.builder.preserve_whitespace_tags: self.preserve_whitespace_tag_stack.append(tag) + if tag.name in self.builder.string_containers: + self.string_container_stack.append(tag) - def endData(self, containerClass=NavigableString): + def endData(self, containerClass=None): + """Method called by the TreeBuilder when the end of a data segment + occurs. + """ if self.current_data: current_data = ''.join(self.current_data) # If whitespace is not preserved, and this string contains @@ -437,11 +582,12 @@ class BeautifulSoup(Tag): not self.parse_only.search(current_data)): return + containerClass = self.string_container(containerClass) o = containerClass(current_data) self.object_was_parsed(o) def object_was_parsed(self, o, parent=None, most_recent_element=None): - """Add an object to the parse tree.""" + """Method called by the TreeBuilder to integrate an object into the parse tree.""" if parent is None: parent = self.currentTag if most_recent_element is not None: @@ -510,10 +656,19 @@ class BeautifulSoup(Tag): def _popToTag(self, name, nsprefix=None, inclusivePop=True): """Pops the tag stack up to and including the most recent - instance of the given tag. If inclusivePop is false, pops the tag - stack up to but *not* including the most recent instqance of - the given tag.""" - #print "Popping to %s" % name + instance of the given tag. + + If there are no open tags with the given name, nothing will be + popped. + + :param name: Pop up to the most recent tag with this name. + :param nsprefix: The namespace prefix that goes with `name`. + :param inclusivePop: It this is false, pops the tag stack up + to but *not* including the most recent instqance of the + given tag. + + """ + #print("Popping to %s" % name) if name == self.ROOT_TAG_NAME: # The BeautifulSoup object itself can never be popped. return @@ -522,6 +677,8 @@ class BeautifulSoup(Tag): stack_size = len(self.tagStack) for i in range(stack_size - 1, 0, -1): + if not self.open_tag_counter.get(name): + break t = self.tagStack[i] if (name == t.name and nsprefix == t.prefix): if inclusivePop: @@ -531,16 +688,24 @@ class BeautifulSoup(Tag): return most_recently_popped - def handle_starttag(self, name, namespace, nsprefix, attrs): - """Push a start tag on to the stack. + def handle_starttag(self, name, namespace, nsprefix, attrs, sourceline=None, + sourcepos=None): + """Called by the tree builder when a new tag is encountered. - If this method returns None, the tag was rejected by the + :param name: Name of the tag. + :param nsprefix: Namespace prefix for the tag. + :param attrs: A dictionary of attribute values. + :param sourceline: The line number where this tag was found in its + source document. + :param sourcepos: The character position within `sourceline` where this + tag was found. + + If this method returns None, the tag was rejected by an active SoupStrainer. You should proceed as if the tag had not occurred in the document. For instance, if this was a self-closing tag, don't call handle_endtag. """ - - # print "Start tag %s: %s" % (name, attrs) + # print("Start tag %s: %s" % (name, attrs)) self.endData() if (self.parse_only and len(self.tagStack) <= 1 @@ -548,8 +713,11 @@ class BeautifulSoup(Tag): or not self.parse_only.search_tag(name, attrs))): return None - tag = Tag(self, self.builder, name, namespace, nsprefix, attrs, - self.currentTag, self._most_recent_element) + tag = self.element_classes.get(Tag, Tag)( + self, self.builder, name, namespace, nsprefix, attrs, + self.currentTag, self._most_recent_element, + sourceline=sourceline, sourcepos=sourcepos + ) if tag is None: return tag if self._most_recent_element is not None: @@ -559,22 +727,38 @@ class BeautifulSoup(Tag): return tag def handle_endtag(self, name, nsprefix=None): - #print "End tag: " + name + """Called by the tree builder when an ending tag is encountered. + + :param name: Name of the tag. + :param nsprefix: Namespace prefix for the tag. + """ + #print("End tag: " + name) self.endData() self._popToTag(name, nsprefix) def handle_data(self, data): + """Called by the tree builder when a chunk of textual data is encountered.""" self.current_data.append(data) - + def decode(self, pretty_print=False, eventual_encoding=DEFAULT_OUTPUT_ENCODING, formatter="minimal"): - """Returns a string or Unicode representation of this document. - To get Unicode, pass None for encoding.""" + """Returns a string or Unicode representation of the parse tree + as an HTML or XML document. + :param pretty_print: If this is True, indentation will be used to + make the document more readable. + :param eventual_encoding: The encoding of the final document. + If this is None, the document will be a Unicode string. + """ if self.is_xml: # Print the XML declaration encoding_part = '' + if eventual_encoding in PYTHON_SPECIFIC_ENCODINGS: + # This is a special Python encoding; it can't actually + # go into an XML document because it means nothing + # outside of Python. + eventual_encoding = None if eventual_encoding != None: encoding_part = ' encoding="%s"' % eventual_encoding prefix = '\n' % encoding_part @@ -587,7 +771,7 @@ class BeautifulSoup(Tag): return prefix + super(BeautifulSoup, self).decode( indent_level, eventual_encoding, formatter) -# Alias to make it easier to type import: 'from bs4 import _soup' +# Aliases to make it easier to get started quickly, e.g. 'from bs4 import _soup' _s = BeautifulSoup _soup = BeautifulSoup @@ -603,14 +787,18 @@ class BeautifulStoneSoup(BeautifulSoup): class StopParsing(Exception): + """Exception raised by a TreeBuilder if it's unable to continue parsing.""" pass class FeatureNotFound(ValueError): + """Exception raised by the BeautifulSoup constructor if no parser with the + requested features is found. + """ pass -#By default, act as an HTML pretty-printer. +#If this file is run as a script, act as an HTML pretty-printer. if __name__ == '__main__': import sys soup = BeautifulSoup(sys.stdin) - print(soup.prettify()) + print((soup.prettify())) diff --git a/libs/bs4/builder/__init__.py b/libs/bs4/builder/__init__.py index cc497cf0b..bd44905e0 100644 --- a/libs/bs4/builder/__init__.py +++ b/libs/bs4/builder/__init__.py @@ -7,8 +7,11 @@ import sys from bs4.element import ( CharsetMetaAttributeValue, ContentMetaAttributeValue, + Stylesheet, + Script, + TemplateString, nonwhitespace_re - ) +) __all__ = [ 'HTMLTreeBuilder', @@ -27,18 +30,33 @@ HTML_5 = 'html5' class TreeBuilderRegistry(object): - + """A way of looking up TreeBuilder subclasses by their name or by desired + features. + """ + def __init__(self): self.builders_for_feature = defaultdict(list) self.builders = [] def register(self, treebuilder_class): - """Register a treebuilder based on its advertised features.""" + """Register a treebuilder based on its advertised features. + + :param treebuilder_class: A subclass of Treebuilder. its .features + attribute should list its features. + """ for feature in treebuilder_class.features: self.builders_for_feature[feature].insert(0, treebuilder_class) self.builders.insert(0, treebuilder_class) def lookup(self, *features): + """Look up a TreeBuilder subclass with the desired features. + + :param features: A list of features to look for. If none are + provided, the most recently registered TreeBuilder subclass + will be used. + :return: A TreeBuilder subclass, or None if there's no + registered subclass with all the requested features. + """ if len(self.builders) == 0: # There are no builders at all. return None @@ -81,7 +99,7 @@ class TreeBuilderRegistry(object): builder_registry = TreeBuilderRegistry() class TreeBuilder(object): - """Turn a document into a Beautiful Soup object tree.""" + """Turn a textual document into a Beautiful Soup object tree.""" NAME = "[Unknown tree builder]" ALTERNATE_NAMES = [] @@ -96,24 +114,53 @@ class TreeBuilder(object): # comma-separated list of CDATA, rather than a single CDATA. DEFAULT_CDATA_LIST_ATTRIBUTES = {} + # Whitespace should be preserved inside these tags. DEFAULT_PRESERVE_WHITESPACE_TAGS = set() + + # The textual contents of tags with these names should be + # instantiated with some class other than NavigableString. + DEFAULT_STRING_CONTAINERS = {} USE_DEFAULT = object() + + # Most parsers don't keep track of line numbers. + TRACKS_LINE_NUMBERS = False - def __init__(self, multi_valued_attributes=USE_DEFAULT, preserve_whitespace_tags=USE_DEFAULT): + def __init__(self, multi_valued_attributes=USE_DEFAULT, + preserve_whitespace_tags=USE_DEFAULT, + store_line_numbers=USE_DEFAULT, + string_containers=USE_DEFAULT, + ): """Constructor. :param multi_valued_attributes: If this is set to None, the - TreeBuilder will not turn any values for attributes like - 'class' into lists. Setting this do a dictionary will - customize this behavior; look at DEFAULT_CDATA_LIST_ATTRIBUTES - for an example. + TreeBuilder will not turn any values for attributes like + 'class' into lists. Setting this to a dictionary will + customize this behavior; look at DEFAULT_CDATA_LIST_ATTRIBUTES + for an example. - Internally, these are called "CDATA list attributes", but that - probably doesn't make sense to an end-user, so the argument name - is `multi_valued_attributes`. + Internally, these are called "CDATA list attributes", but that + probably doesn't make sense to an end-user, so the argument name + is `multi_valued_attributes`. - :param preserve_whitespace_tags: + :param preserve_whitespace_tags: A list of tags to treat + the way
 tags are treated in HTML. Tags in this list
+         are immune from pretty-printing; their contents will always be
+         output as-is.
+
+        :param string_containers: A dictionary mapping tag names to
+        the classes that should be instantiated to contain the textual
+        contents of those tags. The default is to use NavigableString
+        for every tag, no matter what the name. You can override the
+        default by changing DEFAULT_STRING_CONTAINERS.
+
+        :param store_line_numbers: If the parser keeps track of the
+         line numbers and positions of the original markup, that
+         information will, by default, be stored in each corresponding
+         `Tag` object. You can turn this off by passing
+         store_line_numbers=False. If the parser you're using doesn't 
+         keep track of this information, then setting store_line_numbers=True
+         will do nothing.
         """
         self.soup = None
         if multi_valued_attributes is self.USE_DEFAULT:
@@ -122,14 +169,27 @@ class TreeBuilder(object):
         if preserve_whitespace_tags is self.USE_DEFAULT:
             preserve_whitespace_tags = self.DEFAULT_PRESERVE_WHITESPACE_TAGS
         self.preserve_whitespace_tags = preserve_whitespace_tags
-            
+        if store_line_numbers == self.USE_DEFAULT:
+            store_line_numbers = self.TRACKS_LINE_NUMBERS
+        self.store_line_numbers = store_line_numbers 
+        if string_containers == self.USE_DEFAULT:
+            string_containers = self.DEFAULT_STRING_CONTAINERS
+        self.string_containers = string_containers
+        
     def initialize_soup(self, soup):
         """The BeautifulSoup object has been initialized and is now
         being associated with the TreeBuilder.
+
+        :param soup: A BeautifulSoup object.
         """
         self.soup = soup
         
     def reset(self):
+        """Do any work necessary to reset the underlying parser
+        for a new document.
+
+        By default, this does nothing.
+        """
         pass
 
     def can_be_empty_element(self, tag_name):
@@ -141,24 +201,58 @@ class TreeBuilder(object):
         For instance: an HTMLBuilder does not consider a 

tag to be an empty-element tag (it's not in HTMLBuilder.empty_element_tags). This means an empty

tag - will be presented as "

", not "

". + will be presented as "

", not "

" or "

". The default implementation has no opinion about which tags are empty-element tags, so a tag will be presented as an - empty-element tag if and only if it has no contents. - "" will become "", and "bar" will + empty-element tag if and only if it has no children. + "" will become "", and "bar" will be left alone. + + :param tag_name: The name of a markup tag. """ if self.empty_element_tags is None: return True return tag_name in self.empty_element_tags def feed(self, markup): + """Run some incoming markup through some parsing process, + populating the `BeautifulSoup` object in self.soup. + + This method is not implemented in TreeBuilder; it must be + implemented in subclasses. + + :return: None. + """ raise NotImplementedError() def prepare_markup(self, markup, user_specified_encoding=None, - document_declared_encoding=None): - return markup, None, None, False + document_declared_encoding=None, exclude_encodings=None): + """Run any preliminary steps necessary to make incoming markup + acceptable to the parser. + + :param markup: Some markup -- probably a bytestring. + :param user_specified_encoding: The user asked to try this encoding. + :param document_declared_encoding: The markup itself claims to be + in this encoding. NOTE: This argument is not used by the + calling code and can probably be removed. + :param exclude_encodings: The user asked _not_ to try any of + these encodings. + + :yield: A series of 4-tuples: + (markup, encoding, declared encoding, + has undergone character replacement) + + Each 4-tuple represents a strategy for converting the + document to Unicode and parsing it. Each strategy will be tried + in turn. + + By default, the only strategy is to parse the markup + as-is. See `LXMLTreeBuilderForXML` and + `HTMLParserTreeBuilder` for implementations that take into + account the quirks of particular parsers. + """ + yield markup, None, None, False def test_fragment_to_document(self, fragment): """Wrap an HTML fragment to make it look like a document. @@ -170,16 +264,36 @@ class TreeBuilder(object): results against other HTML fragments. This method should not be used outside of tests. + + :param fragment: A string -- fragment of HTML. + :return: A string -- a full HTML document. """ return fragment def set_up_substitutions(self, tag): + """Set up any substitutions that will need to be performed on + a `Tag` when it's output as a string. + + By default, this does nothing. See `HTMLTreeBuilder` for a + case where this is used. + + :param tag: A `Tag` + :return: Whether or not a substitution was performed. + """ return False def _replace_cdata_list_attribute_values(self, tag_name, attrs): - """Replaces class="foo bar" with class=["foo", "bar"] + """When an attribute value is associated with a tag that can + have multiple values for that attribute, convert the string + value to a list of strings. - Modifies its input in place. + Basically, replaces class="foo bar" with class=["foo", "bar"] + + NOTE: This method modifies its input in place. + + :param tag_name: The name of a tag. + :param attrs: A dictionary containing the tag's attributes. + Any appropriate attribute values will be modified in place. """ if not attrs: return attrs @@ -207,7 +321,11 @@ class TreeBuilder(object): return attrs class SAXTreeBuilder(TreeBuilder): - """A Beautiful Soup treebuilder that listens for SAX events.""" + """A Beautiful Soup treebuilder that listens for SAX events. + + This is not currently used for anything, but it demonstrates + how a simple TreeBuilder would work. + """ def feed(self, markup): raise NotImplementedError() @@ -217,11 +335,11 @@ class SAXTreeBuilder(TreeBuilder): def startElement(self, name, attrs): attrs = dict((key[1], value) for key, value in list(attrs.items())) - #print "Start %s, %r" % (name, attrs) + #print("Start %s, %r" % (name, attrs)) self.soup.handle_starttag(name, attrs) def endElement(self, name): - #print "End %s" % name + #print("End %s" % name) self.soup.handle_endtag(name) def startElementNS(self, nsTuple, nodeName, attrs): @@ -271,6 +389,22 @@ class HTMLTreeBuilder(TreeBuilder): # but it may do so eventually, and this information is available if # you need to use it. block_elements = set(["address", "article", "aside", "blockquote", "canvas", "dd", "div", "dl", "dt", "fieldset", "figcaption", "figure", "footer", "form", "h1", "h2", "h3", "h4", "h5", "h6", "header", "hr", "li", "main", "nav", "noscript", "ol", "output", "p", "pre", "section", "table", "tfoot", "ul", "video"]) + + # The HTML standard defines an unusual content model for these tags. + # We represent this by using a string class other than NavigableString + # inside these tags. + # + # I made this list by going through the HTML spec + # (https://html.spec.whatwg.org/#metadata-content) and looking for + # "metadata content" elements that can contain strings. + # + # TODO: Arguably

 in HTML
+        documents) should not.
+        """
         return (
             indent_level is not None
-            and self.name not in self.preserve_whitespace_tags
+            and (
+                not self.preserve_whitespace_tags
+                or self.name not in self.preserve_whitespace_tags
+            )
         )
 
     def prettify(self, encoding=None, formatter="minimal"):
+        """Pretty-print this PageElement as a string.
+
+        :param encoding: The eventual encoding of the string. If this is None,
+            a Unicode string will be returned.
+        :param formatter: A Formatter object, or a string naming one of
+            the standard formatters.
+        :return: A Unicode string (if encoding==None) or a bytestring 
+            (otherwise).
+        """
         if encoding is None:
             return self.decode(True, formatter=formatter)
         else:
@@ -1184,7 +1749,8 @@ class Tag(PageElement):
         """Renders the contents of this tag as a Unicode string.
 
         :param indent_level: Each line of the rendering will be
-           indented this many spaces.
+           indented this many spaces. Used internally in
+           recursive calls while pretty-printing.
 
         :param eventual_encoding: The tag is destined to be
            encoded into this encoding. decode_contents() is _not_
@@ -1226,23 +1792,26 @@ class Tag(PageElement):
     def encode_contents(
         self, indent_level=None, encoding=DEFAULT_OUTPUT_ENCODING,
         formatter="minimal"):
-        """Renders the contents of this tag as a bytestring.
+        """Renders the contents of this PageElement as a bytestring.
 
         :param indent_level: Each line of the rendering will be
-           indented this many spaces.
+           indented this many spaces. Used internally in
+           recursive calls while pretty-printing.
 
         :param eventual_encoding: The bytestring will be in this encoding.
 
-        :param formatter: The output formatter responsible for converting
-           entities to Unicode characters.
-        """
+        :param formatter: A Formatter object, or a string naming one of
+            the standard Formatters.
 
+        :return: A bytestring.
+        """
         contents = self.decode_contents(indent_level, encoding, formatter)
         return contents.encode(encoding)
 
     # Old method for BS3 compatibility
     def renderContents(self, encoding=DEFAULT_OUTPUT_ENCODING,
                        prettyPrint=False, indentLevel=0):
+        """Deprecated method for BS3 compatibility."""
         if not prettyPrint:
             indentLevel = None
         return self.encode_contents(
@@ -1252,27 +1821,47 @@ class Tag(PageElement):
 
     def find(self, name=None, attrs={}, recursive=True, text=None,
              **kwargs):
-        """Return only the first child of this Tag matching the given
-        criteria."""
+        """Look in the children of this PageElement and find the first
+        PageElement that matches the given criteria.
+
+        All find_* methods take a common set of arguments. See the online
+        documentation for detailed explanations.
+
+        :param name: A filter on tag name.
+        :param attrs: A dictionary of filters on attribute values.
+        :param recursive: If this is True, find() will perform a
+            recursive search of this PageElement's children. Otherwise,
+            only the direct children will be considered.
+        :param limit: Stop looking after finding this many results.
+        :kwargs: A dictionary of filters on attribute values.
+        :return: A PageElement.
+        :rtype: bs4.element.Tag | bs4.element.NavigableString
+        """
         r = None
         l = self.find_all(name, attrs, recursive, text, 1, **kwargs)
         if l:
             r = l[0]
         return r
-    findChild = find
+    findChild = find #BS2
 
     def find_all(self, name=None, attrs={}, recursive=True, text=None,
                  limit=None, **kwargs):
-        """Extracts a list of Tag objects that match the given
-        criteria.  You can specify the name of the Tag and any
-        attributes you want the Tag to have.
+        """Look in the children of this PageElement and find all
+        PageElements that match the given criteria.
 
-        The value of a key-value pair in the 'attrs' map can be a
-        string, a list of strings, a regular expression object, or a
-        callable that takes a string and returns whether or not the
-        string matches for some custom definition of 'matches'. The
-        same is true of the tag name."""
+        All find_* methods take a common set of arguments. See the online
+        documentation for detailed explanations.
 
+        :param name: A filter on tag name.
+        :param attrs: A dictionary of filters on attribute values.
+        :param recursive: If this is True, find_all() will perform a
+            recursive search of this PageElement's children. Otherwise,
+            only the direct children will be considered.
+        :param limit: Stop looking after finding this many results.
+        :kwargs: A dictionary of filters on attribute values.
+        :return: A ResultSet of PageElements.
+        :rtype: bs4.element.ResultSet
+        """
         generator = self.descendants
         if not recursive:
             generator = self.children
@@ -1283,11 +1872,20 @@ class Tag(PageElement):
     #Generator methods
     @property
     def children(self):
+        """Iterate over all direct children of this PageElement.
+
+        :yield: A sequence of PageElements.
+        """
         # return iter() to make the purpose of the method clear
         return iter(self.contents)  # XXX This seems to be untested.
 
     @property
     def descendants(self):
+        """Iterate over all children of this PageElement in a
+        breadth-first sequence.
+
+        :yield: A sequence of PageElements.
+        """
         if not len(self.contents):
             return
         stopNode = self._last_descendant().next_element
@@ -1298,7 +1896,21 @@ class Tag(PageElement):
 
     # CSS selector code
     def select_one(self, selector, namespaces=None, **kwargs):
-        """Perform a CSS selection operation on the current element."""
+        """Perform a CSS selection operation on the current element.
+
+        :param selector: A CSS selector.
+
+        :param namespaces: A dictionary mapping namespace prefixes
+           used in the CSS selector to namespace URIs. By default,
+           Beautiful Soup will use the prefixes it encountered while
+           parsing the document.
+
+        :param kwargs: Keyword arguments to be passed into SoupSieve's 
+           soupsieve.select() method.
+
+        :return: A Tag.
+        :rtype: bs4.element.Tag
+        """
         value = self.select(selector, namespaces, 1, **kwargs)
         if value:
             return value[0]
@@ -1312,14 +1924,17 @@ class Tag(PageElement):
         :param selector: A string containing a CSS selector.
 
         :param namespaces: A dictionary mapping namespace prefixes
-        used in the CSS selector to namespace URIs. By default,
-        Beautiful Soup will use the prefixes it encountered while
-        parsing the document.
+           used in the CSS selector to namespace URIs. By default,
+           Beautiful Soup will use the prefixes it encountered while
+           parsing the document.
 
         :param limit: After finding this number of results, stop looking.
 
-        :param kwargs: Any extra arguments you'd like to pass in to
-        soupsieve.select().
+        :param kwargs: Keyword arguments to be passed into SoupSieve's 
+           soupsieve.select() method.
+
+        :return: A ResultSet of Tags.
+        :rtype: bs4.element.ResultSet
         """
         if namespaces is None:
             namespaces = self._namespaces
@@ -1331,19 +1946,27 @@ class Tag(PageElement):
                 "Cannot execute CSS selectors because the soupsieve package is not installed."
             )
             
-        return soupsieve.select(selector, self, namespaces, limit, **kwargs)
+        results = soupsieve.select(selector, self, namespaces, limit, **kwargs)
+
+        # We do this because it's more consistent and because
+        # ResultSet.__getattr__ has a helpful error message.
+        return ResultSet(None, results)
 
     # Old names for backwards compatibility
     def childGenerator(self):
+        """Deprecated generator."""
         return self.children
 
     def recursiveChildGenerator(self):
+        """Deprecated generator."""
         return self.descendants
 
     def has_key(self, key):
-        """This was kind of misleading because has_key() (attributes)
-        was different from __in__ (contents). has_key() is gone in
-        Python 3, anyway."""
+        """Deprecated method. This was kind of misleading because has_key()
+        (attributes) was different from __in__ (contents).
+
+        has_key() is gone in Python 3, anyway.
+        """
         warnings.warn('has_key is deprecated. Use has_attr("%s") instead.' % (
                 key))
         return self.has_attr(key)
@@ -1351,9 +1974,26 @@ class Tag(PageElement):
 # Next, a couple classes to represent queries and their results.
 class SoupStrainer(object):
     """Encapsulates a number of ways of matching a markup element (tag or
-    text)."""
+    string).
+
+    This is primarily used to underpin the find_* methods, but you can
+    create one yourself and pass it in as `parse_only` to the
+    `BeautifulSoup` constructor, to parse a subset of a large
+    document.
+    """
 
     def __init__(self, name=None, attrs={}, text=None, **kwargs):
+        """Constructor.
+
+        The SoupStrainer constructor takes the same arguments passed
+        into the find_* methods. See the online documentation for
+        detailed explanations.
+
+        :param name: A filter on tag name.
+        :param attrs: A dictionary of filters on attribute values.
+        :param text: A filter for a NavigableString with specific text.
+        :kwargs: A dictionary of filters on attribute values.
+        """        
         self.name = self._normalize_search_value(name)
         if not isinstance(attrs, dict):
             # Treat a non-dict value for attrs as a search for the 'class'
@@ -1411,17 +2051,38 @@ class SoupStrainer(object):
         return str(str(value))
 
     def __str__(self):
+        """A human-readable representation of this SoupStrainer."""
         if self.text:
             return self.text
         else:
             return "%s|%s" % (self.name, self.attrs)
 
     def search_tag(self, markup_name=None, markup_attrs={}):
+        """Check whether a Tag with the given name and attributes would
+        match this SoupStrainer.
+
+        Used prospectively to decide whether to even bother creating a Tag
+        object.
+
+        :param markup_name: A tag name as found in some markup.
+        :param markup_attrs: A dictionary of attributes as found in some markup.
+
+        :return: True if the prospective tag would match this SoupStrainer;
+            False otherwise.
+        """
         found = None
         markup = None
         if isinstance(markup_name, Tag):
             markup = markup_name
             markup_attrs = markup
+
+        if isinstance(self.name, str):
+            # Optimization for a very common case where the user is
+            # searching for a tag with one specific name, and we're
+            # looking at a tag with a different name.
+            if markup and not markup.prefix and self.name != markup.name:
+                 return False
+            
         call_function_with_tag_data = (
             isinstance(self.name, Callable)
             and not isinstance(markup_name, Tag))
@@ -1455,10 +2116,19 @@ class SoupStrainer(object):
         if found and self.text and not self._matches(found.string, self.text):
             found = None
         return found
+
+    # For BS3 compatibility.
     searchTag = search_tag
 
     def search(self, markup):
-        # print 'looking for %s in %s' % (self, markup)
+        """Find all items in `markup` that match this SoupStrainer.
+
+        Used by the core _find_all() method, which is ultimately
+        called by all find_* methods.
+
+        :param markup: A PageElement or a list of them.
+        """
+        # print('looking for %s in %s' % (self, markup))
         found = None
         # If given a list of items, scan it for a text element that
         # matches.
@@ -1484,7 +2154,7 @@ class SoupStrainer(object):
         return found
 
     def _matches(self, markup, match_against, already_tried=None):
-        # print u"Matching %s against %s" % (markup, match_against)
+        # print(u"Matching %s against %s" % (markup, match_against))
         result = False
         if isinstance(markup, list) or isinstance(markup, tuple):
             # This should only happen when searching a multi-valued attribute
@@ -1570,10 +2240,16 @@ class ResultSet(list):
     """A ResultSet is just a list that keeps track of the SoupStrainer
     that created it."""
     def __init__(self, source, result=()):
+        """Constructor.
+
+        :param source: A SoupStrainer.
+        :param result: A list of PageElements.
+        """
         super(ResultSet, self).__init__(result)
         self.source = source
 
     def __getattr__(self, key):
+        """Raise a helpful exception to explain a common code fix."""
         raise AttributeError(
-            "ResultSet object has no attribute '%s'. You're probably treating a list of items like a single item. Did you call find_all() when you meant to call find()?" % key
+            "ResultSet object has no attribute '%s'. You're probably treating a list of elements like a single element. Did you call find_all() when you meant to call find()?" % key
         )
diff --git a/libs/bs4/formatter.py b/libs/bs4/formatter.py
index 7dbaa3850..3bd9f8598 100644
--- a/libs/bs4/formatter.py
+++ b/libs/bs4/formatter.py
@@ -5,6 +5,28 @@ class Formatter(EntitySubstitution):
 
     Some parts of this strategy come from the distinction between
     HTML4, HTML5, and XML. Others are configurable by the user.
+
+    Formatters are passed in as the `formatter` argument to methods
+    like `PageElement.encode`. Most people won't need to think about
+    formatters, and most people who need to think about them can pass
+    in one of these predefined strings as `formatter` rather than
+    making a new Formatter object:
+
+    For HTML documents:
+     * 'html' - HTML entity substitution for generic HTML documents. (default)
+     * 'html5' - HTML entity substitution for HTML5 documents, as
+                 well as some optimizations in the way tags are rendered.
+     * 'minimal' - Only make the substitutions necessary to guarantee
+                   valid HTML.
+     * None - Do not perform any substitution. This will be faster
+              but may result in invalid markup.
+
+    For XML documents:
+     * 'html' - Entity substitution for XHTML documents.
+     * 'minimal' - Only make the substitutions necessary to guarantee
+                   valid XML. (default)
+     * None - Do not perform any substitution. This will be faster
+              but may result in invalid markup.
     """
     # Registries of XML and HTML formatters.
     XML_FORMATTERS = {}
@@ -27,11 +49,26 @@ class Formatter(EntitySubstitution):
     def __init__(
             self, language=None, entity_substitution=None,
             void_element_close_prefix='/', cdata_containing_tags=None,
+            empty_attributes_are_booleans=False,
     ):
-        """
+        """Constructor.
 
-        :param void_element_close_prefix: By default, represent void
-        elements as  rather than 
+        :param language: This should be Formatter.XML if you are formatting
+           XML markup and Formatter.HTML if you are formatting HTML markup.
+
+        :param entity_substitution: A function to call to replace special
+           characters with XML/HTML entities. For examples, see 
+           bs4.dammit.EntitySubstitution.substitute_html and substitute_xml.
+        :param void_element_close_prefix: By default, void elements
+           are represented as  (XML rules) rather than 
+           (HTML rules). To get , pass in the empty string.
+        :param cdata_containing_tags: The list of tags that are defined
+           as containing CDATA in this dialect. For example, in HTML,
+           "
+        )
+        assert isinstance(soup.style.string, Stylesheet)
+        assert isinstance(soup.script.string, Script)
+
+        soup = self.soup(
+            ""
+        )
+        assert isinstance(soup.style.string, Stylesheet)
+        # The contents of the style tag resemble an HTML comment, but
+        # it's not treated as a comment.
+        self.assertEqual("", soup.style.string)
+        assert isinstance(soup.style.string, Stylesheet)
+        
     def test_pickle_and_unpickle_identity(self):
         # Pickling a tree, then unpickling it, yields a tree identical
         # to the original.
@@ -250,18 +318,21 @@ class HTMLTreeBuilderSmokeTest(object):
         doctype = soup.contents[0]
         self.assertEqual(doctype.__class__, Doctype)
         self.assertEqual(doctype, doctype_fragment)
-        self.assertEqual(str(soup)[:len(doctype_str)], doctype_str)
+        self.assertEqual(
+            soup.encode("utf8")[:len(doctype_str)],
+            doctype_str
+        )
 
         # Make sure that the doctype was correctly associated with the
         # parse tree and that the rest of the document parsed.
         self.assertEqual(soup.p.contents[0], 'foo')
 
-    def _document_with_doctype(self, doctype_fragment):
+    def _document_with_doctype(self, doctype_fragment, doctype_string="DOCTYPE"):
         """Generate and parse a document with the given doctype."""
-        doctype = '' % doctype_fragment
+        doctype = '' % (doctype_string, doctype_fragment)
         markup = doctype + '\n

foo

' soup = self.soup(markup) - return doctype, soup + return doctype.encode("utf8"), soup def test_normal_doctypes(self): """Make sure normal, everyday HTML doctypes are handled correctly.""" @@ -274,6 +345,27 @@ class HTMLTreeBuilderSmokeTest(object): doctype = soup.contents[0] self.assertEqual("", doctype.strip()) + def test_mixed_case_doctype(self): + # A lowercase or mixed-case doctype becomes a Doctype. + for doctype_fragment in ("doctype", "DocType"): + doctype_str, soup = self._document_with_doctype( + "html", doctype_fragment + ) + + # Make sure a Doctype object was created and that the DOCTYPE + # is uppercase. + doctype = soup.contents[0] + self.assertEqual(doctype.__class__, Doctype) + self.assertEqual(doctype, "html") + self.assertEqual( + soup.encode("utf8")[:len(doctype_str)], + b"" + ) + + # Make sure that the doctype was correctly associated with the + # parse tree and that the rest of the document parsed. + self.assertEqual(soup.p.contents[0], 'foo') + def test_public_doctype_with_url(self): doctype = 'html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"' self.assertDoctypeHandled(doctype) @@ -532,7 +624,7 @@ Hello, world! self.assertSoupEquals("�", expect) self.assertSoupEquals("�", expect) self.assertSoupEquals("�", expect) - + def test_multipart_strings(self): "Mostly to prevent a recurrence of a bug in the html5lib treebuilder." soup = self.soup("

\nfoo

") @@ -594,7 +686,7 @@ Hello, world! markup = b'' soup = self.soup(markup) self.assertEqual(['foo', 'bar'], soup.a['class']) - + # # Generally speaking, tests below this point are more tests of # Beautiful Soup than tests of the tree builders. But parsers are @@ -779,11 +871,44 @@ Hello, world! # encoding. self.assertEqual('utf8', charset.encode("utf8")) + def test_python_specific_encodings_not_used_in_charset(self): + # You can encode an HTML document using a Python-specific + # encoding, but that encoding won't be mentioned _inside_ the + # resulting document. Instead, the document will appear to + # have no encoding. + for markup in [ + b'' + b'' + ]: + soup = self.soup(markup) + for encoding in PYTHON_SPECIFIC_ENCODINGS: + if encoding in ( + 'idna', 'mbcs', 'oem', 'undefined', + 'string_escape', 'string-escape' + ): + # For one reason or another, these will raise an + # exception if we actually try to use them, so don't + # bother. + continue + encoded = soup.encode(encoding) + assert b'meta charset=""' in encoded + assert encoding.encode("ascii") not in encoded + def test_tag_with_no_attributes_can_have_attributes_added(self): data = self.soup("text") data.a['foo'] = 'bar' self.assertEqual('text', data.a.decode()) + def test_closing_tag_with_no_opening_tag(self): + # Without BeautifulSoup.open_tag_counter, the tag will + # cause _popToTag to be called over and over again as we look + # for a tag that wasn't there. The result is that 'text2' + # will show up outside the body of the document. + soup = self.soup("

text1

text2
") + self.assertEqual( + "

text1

text2
", soup.body.decode() + ) + def test_worst_case(self): """Test the worst case (currently) for linking issues.""" @@ -791,7 +916,7 @@ Hello, world! self.linkage_validator(soup) -class XMLTreeBuilderSmokeTest(object): +class XMLTreeBuilderSmokeTest(TreeBuilderSmokeTest): def test_pickle_and_unpickle_identity(self): # Pickling a tree, then unpickling it, yields a tree identical @@ -812,6 +937,25 @@ class XMLTreeBuilderSmokeTest(object): soup = self.soup(markup) self.assertEqual(markup, soup.encode("utf8")) + def test_python_specific_encodings_not_used_in_xml_declaration(self): + # You can encode an XML document using a Python-specific + # encoding, but that encoding won't be mentioned _inside_ the + # resulting document. + markup = b"""\n""" + soup = self.soup(markup) + for encoding in PYTHON_SPECIFIC_ENCODINGS: + if encoding in ( + 'idna', 'mbcs', 'oem', 'undefined', + 'string_escape', 'string-escape' + ): + # For one reason or another, these will raise an + # exception if we actually try to use them, so don't + # bother. + continue + encoded = soup.encode(encoding) + assert b'' in encoded + assert encoding.encode("ascii") not in encoded + def test_processing_instruction(self): markup = b"""\n""" soup = self.soup(markup) @@ -828,7 +972,7 @@ class XMLTreeBuilderSmokeTest(object): soup = self.soup(markup) self.assertEqual( soup.encode("utf-8"), markup) - + def test_nested_namespaces(self): doc = b""" diff --git a/libs/bs4/tests/test_html5lib.py b/libs/bs4/tests/test_html5lib.py index 96529b0b3..f8902ad78 100644 --- a/libs/bs4/tests/test_html5lib.py +++ b/libs/bs4/tests/test_html5lib.py @@ -168,3 +168,59 @@ class HTML5LibBuilderSmokeTest(SoupTest, HTML5TreeBuilderSmokeTest): for form in soup.find_all('form'): inputs.extend(form.find_all('input')) self.assertEqual(len(inputs), 1) + + def test_tracking_line_numbers(self): + # The html.parser TreeBuilder keeps track of line number and + # position of each element. + markup = "\n

\n\n\ntext

" + soup = self.soup(markup) + self.assertEqual(2, soup.p.sourceline) + self.assertEqual(5, soup.p.sourcepos) + self.assertEqual("sourceline", soup.p.find('sourceline').name) + + # You can deactivate this behavior. + soup = self.soup(markup, store_line_numbers=False) + self.assertEqual("sourceline", soup.p.sourceline.name) + self.assertEqual("sourcepos", soup.p.sourcepos.name) + + def test_special_string_containers(self): + # The html5lib tree builder doesn't support this standard feature, + # because there's no way of knowing, when a string is created, + # where in the tree it will eventually end up. + pass + + def test_html5_attributes(self): + # The html5lib TreeBuilder can convert any entity named in + # the HTML5 spec to a sequence of Unicode characters, and + # convert those Unicode characters to a (potentially + # different) named entity on the way out. + # + # This is a copy of the same test from + # HTMLParserTreeBuilderSmokeTest. It's not in the superclass + # because the lxml HTML TreeBuilder _doesn't_ work this way. + for input_element, output_unicode, output_element in ( + ("⇄", '\u21c4', b'⇄'), + ('⊧', '\u22a7', b'⊧'), + ('𝔑', '\U0001d511', b'𝔑'), + ('≧̸', '\u2267\u0338', b'≧̸'), + ('¬', '\xac', b'¬'), + ('⫬', '\u2aec', b'⫬'), + ('"', '"', b'"'), + ('∴', '\u2234', b'∴'), + ('∴', '\u2234', b'∴'), + ('∴', '\u2234', b'∴'), + ("fj", 'fj', b'fj'), + ("⊔", '\u2294', b'⊔'), + ("⊔︀", '\u2294\ufe00', b'⊔︀'), + ("'", "'", b"'"), + ("|", "|", b"|"), + ): + markup = '
%s
' % input_element + div = self.soup(markup).div + without_element = div.encode() + expect = b"
%s
" % output_unicode.encode("utf8") + self.assertEqual(without_element, expect) + + with_element = div.encode(formatter="html") + expect = b"
%s
" % output_element + self.assertEqual(with_element, expect) diff --git a/libs/bs4/tests/test_htmlparser.py b/libs/bs4/tests/test_htmlparser.py index 790489aa1..0d8161efc 100644 --- a/libs/bs4/tests/test_htmlparser.py +++ b/libs/bs4/tests/test_htmlparser.py @@ -3,6 +3,7 @@ trees.""" from pdb import set_trace import pickle +import warnings from bs4.testing import SoupTest, HTMLTreeBuilderSmokeTest from bs4.builder import HTMLParserTreeBuilder from bs4.builder._htmlparser import BeautifulSoupHTMLParser @@ -37,6 +38,88 @@ class HTMLParserTreeBuilderSmokeTest(SoupTest, HTMLTreeBuilderSmokeTest): # finishes working is handled. self.assertSoupEquals("foo &# bar", "foo &# bar") + def test_tracking_line_numbers(self): + # The html.parser TreeBuilder keeps track of line number and + # position of each element. + markup = "\n

\n\n\ntext

" + soup = self.soup(markup) + self.assertEqual(2, soup.p.sourceline) + self.assertEqual(3, soup.p.sourcepos) + self.assertEqual("sourceline", soup.p.find('sourceline').name) + + # You can deactivate this behavior. + soup = self.soup(markup, store_line_numbers=False) + self.assertEqual("sourceline", soup.p.sourceline.name) + self.assertEqual("sourcepos", soup.p.sourcepos.name) + + def test_on_duplicate_attribute(self): + # The html.parser tree builder has a variety of ways of + # handling a tag that contains the same attribute multiple times. + + markup = '' + + # If you don't provide any particular value for + # on_duplicate_attribute, later values replace earlier values. + soup = self.soup(markup) + self.assertEqual("url3", soup.a['href']) + self.assertEqual(["cls"], soup.a['class']) + self.assertEqual("id", soup.a['id']) + + # You can also get this behavior explicitly. + def assert_attribute(on_duplicate_attribute, expected): + soup = self.soup( + markup, on_duplicate_attribute=on_duplicate_attribute + ) + self.assertEqual(expected, soup.a['href']) + + # Verify that non-duplicate attributes are treated normally. + self.assertEqual(["cls"], soup.a['class']) + self.assertEqual("id", soup.a['id']) + assert_attribute(None, "url3") + assert_attribute(BeautifulSoupHTMLParser.REPLACE, "url3") + + # You can ignore subsequent values in favor of the first. + assert_attribute(BeautifulSoupHTMLParser.IGNORE, "url1") + + # And you can pass in a callable that does whatever you want. + def accumulate(attrs, key, value): + if not isinstance(attrs[key], list): + attrs[key] = [attrs[key]] + attrs[key].append(value) + assert_attribute(accumulate, ["url1", "url2", "url3"]) + + def test_html5_attributes(self): + # The html.parser TreeBuilder can convert any entity named in + # the HTML5 spec to a sequence of Unicode characters, and + # convert those Unicode characters to a (potentially + # different) named entity on the way out. + for input_element, output_unicode, output_element in ( + ("⇄", '\u21c4', b'⇄'), + ('⊧', '\u22a7', b'⊧'), + ('𝔑', '\U0001d511', b'𝔑'), + ('≧̸', '\u2267\u0338', b'≧̸'), + ('¬', '\xac', b'¬'), + ('⫬', '\u2aec', b'⫬'), + ('"', '"', b'"'), + ('∴', '\u2234', b'∴'), + ('∴', '\u2234', b'∴'), + ('∴', '\u2234', b'∴'), + ("fj", 'fj', b'fj'), + ("⊔", '\u2294', b'⊔'), + ("⊔︀", '\u2294\ufe00', b'⊔︀'), + ("'", "'", b"'"), + ("|", "|", b"|"), + ): + markup = '
%s
' % input_element + div = self.soup(markup).div + without_element = div.encode() + expect = b"
%s
" % output_unicode.encode("utf8") + self.assertEqual(without_element, expect) + + with_element = div.encode(formatter="html") + expect = b"
%s
" % output_element + self.assertEqual(with_element, expect) + class TestHTMLParserSubclass(SoupTest): def test_error(self): @@ -44,4 +127,8 @@ class TestHTMLParserSubclass(SoupTest): that doesn't cause a crash. """ parser = BeautifulSoupHTMLParser() - parser.error("don't crash") + with warnings.catch_warnings(record=True) as warns: + parser.error("don't crash") + [warning] = warns + assert "don't crash" == str(warning.message) + diff --git a/libs/bs4/tests/test_lxml.py b/libs/bs4/tests/test_lxml.py index 29da71149..71931ffe9 100644 --- a/libs/bs4/tests/test_lxml.py +++ b/libs/bs4/tests/test_lxml.py @@ -45,7 +45,7 @@ class LXMLTreeBuilderSmokeTest(SoupTest, HTMLTreeBuilderSmokeTest): "

foo�bar

", "

foobar

") self.assertSoupEquals( "

foo�bar

", "

foobar

") - + def test_entities_in_foreign_document_encoding(self): # We can't implement this case correctly because by the time we # hear about markup like "“", it's been (incorrectly) converted into @@ -71,6 +71,21 @@ class LXMLTreeBuilderSmokeTest(SoupTest, HTMLTreeBuilderSmokeTest): self.assertEqual("", str(soup.b)) self.assertTrue("BeautifulStoneSoup class is deprecated" in str(w[0].message)) + def test_tracking_line_numbers(self): + # The lxml TreeBuilder cannot keep track of line numbers from + # the original markup. Even if you ask for line numbers, we + # don't have 'em. + # + # This means that if you have a tag like or + # , attribute access will find it rather than + # giving you a numeric answer. + soup = self.soup( + "\n

\n\n\ntext

", + store_line_numbers=True + ) + self.assertEqual("sourceline", soup.p.sourceline.name) + self.assertEqual("sourcepos", soup.p.sourcepos.name) + @skipIf( not LXML_PRESENT, "lxml seems not to be present, not testing its XML tree builder.") diff --git a/libs/bs4/tests/test_soup.py b/libs/bs4/tests/test_soup.py index 1eda9484b..4d00845d0 100644 --- a/libs/bs4/tests/test_soup.py +++ b/libs/bs4/tests/test_soup.py @@ -3,6 +3,7 @@ from pdb import set_trace import logging +import os import unittest import sys import tempfile @@ -10,18 +11,27 @@ import tempfile from bs4 import ( BeautifulSoup, BeautifulStoneSoup, + GuessedAtParserWarning, + MarkupResemblesLocatorWarning, +) +from bs4.builder import ( + TreeBuilder, + ParserRejectedMarkup, ) from bs4.element import ( CharsetMetaAttributeValue, + Comment, ContentMetaAttributeValue, SoupStrainer, NamespacedAttribute, + Tag, + NavigableString, ) + import bs4.dammit from bs4.dammit import ( EntitySubstitution, UnicodeDammit, - EncodingDetector, ) from bs4.testing import ( default_builder, @@ -62,10 +72,21 @@ class TestConstructor(SoupTest): def __init__(self, **kwargs): self.called_with = kwargs self.is_xml = True + self.store_line_numbers = False + self.cdata_list_attributes = [] + self.preserve_whitespace_tags = [] + self.string_containers = {} def initialize_soup(self, soup): pass + def feed(self, markup): + self.fed = markup + def reset(self): + pass + def ignore(self, ignore): + pass + set_up_substitutions = can_be_empty_element = ignore def prepare_markup(self, *args, **kwargs): - return '' + yield "prepared markup", "original encoding", "declared encoding", "contains replacement characters" kwargs = dict( var="value", @@ -77,7 +98,8 @@ class TestConstructor(SoupTest): soup = BeautifulSoup('', builder=Mock, **kwargs) assert isinstance(soup.builder, Mock) self.assertEqual(dict(var="value"), soup.builder.called_with) - + self.assertEqual("prepared markup", soup.builder.fed) + # You can also instantiate the TreeBuilder yourself. In this # case, that specific object is used and any keyword arguments # to the BeautifulSoup constructor are ignored. @@ -91,6 +113,26 @@ class TestConstructor(SoupTest): self.assertEqual(builder, soup.builder) self.assertEqual(kwargs, builder.called_with) + def test_parser_markup_rejection(self): + # If markup is completely rejected by the parser, an + # explanatory ParserRejectedMarkup exception is raised. + class Mock(TreeBuilder): + def feed(self, *args, **kwargs): + raise ParserRejectedMarkup("Nope.") + + def prepare_markup(self, *args, **kwargs): + # We're going to try two different ways of preparing this markup, + # but feed() will reject both of them. + yield markup, None, None, False + yield markup, None, None, False + + import re + self.assertRaisesRegex( + ParserRejectedMarkup, + "The markup you provided was rejected by the parser. Trying a different parser or a different encoding may help.", + BeautifulSoup, '', builder=Mock, + ) + def test_cdata_list_attributes(self): # Most attribute values are represented as scalars, but the # HTML standard says that some attributes, like 'class' have @@ -120,28 +162,96 @@ class TestConstructor(SoupTest): self.assertEqual(["an", "id"], a['id']) self.assertEqual(" a class ", a['class']) + def test_replacement_classes(self): + # Test the ability to pass in replacements for element classes + # which will be used when building the tree. + class TagPlus(Tag): + pass + + class StringPlus(NavigableString): + pass + + class CommentPlus(Comment): + pass + soup = self.soup( + "
foobar", + element_classes = { + Tag: TagPlus, + NavigableString: StringPlus, + Comment: CommentPlus, + } + ) + + # The tree was built with TagPlus, StringPlus, and CommentPlus objects, + # rather than Tag, String, and Comment objects. + assert all( + isinstance(x, (TagPlus, StringPlus, CommentPlus)) + for x in soup.recursiveChildGenerator() + ) + + def test_alternate_string_containers(self): + # Test the ability to customize the string containers for + # different types of tags. + class PString(NavigableString): + pass + + class BString(NavigableString): + pass + + soup = self.soup( + "
Hello.

Here is some bolded text", + string_containers = { + 'b': BString, + 'p': PString, + } + ) + + # The string before the

tag is a regular NavigableString. + assert isinstance(soup.div.contents[0], NavigableString) + + # The string inside the

tag, but not inside the tag, + # is a PString. + assert isinstance(soup.p.contents[0], PString) + + # Every string inside the tag is a BString, even the one that + # was also inside an tag. + for s in soup.b.strings: + assert isinstance(s, BString) + + # Now that parsing was complete, the string_container_stack + # (where this information was kept) has been cleared out. + self.assertEqual([], soup.string_container_stack) + + class TestWarnings(SoupTest): - def _no_parser_specified(self, s, is_there=True): - v = s.startswith(BeautifulSoup.NO_PARSER_SPECIFIED_WARNING[:80]) - self.assertTrue(v) + def _assert_warning(self, warnings, cls): + for w in warnings: + if isinstance(w.message, cls): + return w + raise Exception("%s warning not found in %r" % cls, warnings) + + def _assert_no_parser_specified(self, w): + warning = self._assert_warning(w, GuessedAtParserWarning) + message = str(warning.message) + self.assertTrue( + message.startswith(BeautifulSoup.NO_PARSER_SPECIFIED_WARNING[:60]) + ) def test_warning_if_no_parser_specified(self): with warnings.catch_warnings(record=True) as w: - soup = self.soup("") - msg = str(w[0].message) - self._assert_no_parser_specified(msg) + soup = BeautifulSoup("") + self._assert_no_parser_specified(w) def test_warning_if_parser_specified_too_vague(self): with warnings.catch_warnings(record=True) as w: - soup = self.soup("", "html") - msg = str(w[0].message) - self._assert_no_parser_specified(msg) + soup = BeautifulSoup("", "html") + self._assert_no_parser_specified(w) def test_no_warning_if_explicit_parser_specified(self): with warnings.catch_warnings(record=True) as w: - soup = self.soup("", "html.parser") + soup = BeautifulSoup("", "html.parser") self.assertEqual([], w) def test_parseOnlyThese_renamed_to_parse_only(self): @@ -165,41 +275,58 @@ class TestWarnings(SoupTest): self.assertRaises( TypeError, self.soup, "", no_such_argument=True) -class TestWarnings(SoupTest): - def test_disk_file_warning(self): filehandle = tempfile.NamedTemporaryFile() filename = filehandle.name try: with warnings.catch_warnings(record=True) as w: soup = self.soup(filename) - msg = str(w[0].message) - self.assertTrue("looks like a filename" in msg) + warning = self._assert_warning(w, MarkupResemblesLocatorWarning) + self.assertTrue("looks like a filename" in str(warning.message)) finally: filehandle.close() # The file no longer exists, so Beautiful Soup will no longer issue the warning. with warnings.catch_warnings(record=True) as w: soup = self.soup(filename) - self.assertEqual(0, len(w)) + self.assertEqual([], w) + def test_directory_warning(self): + try: + filename = tempfile.mkdtemp() + with warnings.catch_warnings(record=True) as w: + soup = self.soup(filename) + warning = self._assert_warning(w, MarkupResemblesLocatorWarning) + self.assertTrue("looks like a directory" in str(warning.message)) + finally: + os.rmdir(filename) + + # The directory no longer exists, so Beautiful Soup will no longer issue the warning. + with warnings.catch_warnings(record=True) as w: + soup = self.soup(filename) + self.assertEqual([], w) + def test_url_warning_with_bytes_url(self): with warnings.catch_warnings(record=True) as warning_list: soup = self.soup(b"http://www.crummybytes.com/") - # Be aware this isn't the only warning that can be raised during - # execution.. - self.assertTrue(any("looks like a URL" in str(w.message) - for w in warning_list)) + warning = self._assert_warning( + warning_list, MarkupResemblesLocatorWarning + ) + self.assertTrue("looks like a URL" in str(warning.message)) def test_url_warning_with_unicode_url(self): with warnings.catch_warnings(record=True) as warning_list: # note - this url must differ from the bytes one otherwise # python's warnings system swallows the second warning soup = self.soup("http://www.crummyunicode.com/") - self.assertTrue(any("looks like a URL" in str(w.message) - for w in warning_list)) + warning = self._assert_warning( + warning_list, MarkupResemblesLocatorWarning + ) + self.assertTrue("looks like a URL" in str(warning.message)) def test_url_warning_with_bytes_and_space(self): + # Here the markup contains something besides a URL, so no warning + # is issued. with warnings.catch_warnings(record=True) as warning_list: soup = self.soup(b"http://www.crummybytes.com/ is great") self.assertFalse(any("looks like a URL" in str(w.message) @@ -241,6 +368,51 @@ class TestEntitySubstitution(unittest.TestCase): self.assertEqual(self.sub.substitute_html(dammit.markup), "‘’foo“”") + def test_html5_entity(self): + # Some HTML5 entities correspond to single- or multi-character + # Unicode sequences. + + for entity, u in ( + # A few spot checks of our ability to recognize + # special character sequences and convert them + # to named entities. + ('⊧', '\u22a7'), + ('𝔑', '\U0001d511'), + ('≧̸', '\u2267\u0338'), + ('¬', '\xac'), + ('⫬', '\u2aec'), + + # We _could_ convert | to &verbarr;, but we don't, because + # | is an ASCII character. + ('|' '|'), + + # Similarly for the fj ligature, which we could convert to + # fj, but we don't. + ("fj", "fj"), + + # We do convert _these_ ASCII characters to HTML entities, + # because that's required to generate valid HTML. + ('>', '>'), + ('<', '<'), + ('&', '&'), + ): + template = '3 %s 4' + raw = template % u + with_entities = template % entity + self.assertEqual(self.sub.substitute_html(raw), with_entities) + + def test_html5_entity_with_variation_selector(self): + # Some HTML5 entities correspond either to a single-character + # Unicode sequence _or_ to the same character plus U+FE00, + # VARIATION SELECTOR 1. We can handle this. + data = "fjords \u2294 penguins" + markup = "fjords ⊔ penguins" + self.assertEqual(self.sub.substitute_html(data), markup) + + data = "fjords \u2294\ufe00 penguins" + markup = "fjords ⊔︀ penguins" + self.assertEqual(self.sub.substitute_html(data), markup) + def test_xml_converstion_includes_no_quotes_if_make_quoted_attribute_is_false(self): s = 'Welcome to "my bar"' self.assertEqual(self.sub.substitute_xml(s, False), s) @@ -350,186 +522,26 @@ class TestEncodingConversion(SoupTest): markup = '

' self.assertEqual(self.soup(markup).div.encode("utf8"), markup.encode("utf8")) -class TestUnicodeDammit(unittest.TestCase): - """Standalone tests of UnicodeDammit.""" - - def test_unicode_input(self): - markup = "I'm already Unicode! \N{SNOWMAN}" - dammit = UnicodeDammit(markup) - self.assertEqual(dammit.unicode_markup, markup) - - def test_smart_quotes_to_unicode(self): - markup = b"\x91\x92\x93\x94" - dammit = UnicodeDammit(markup) - self.assertEqual( - dammit.unicode_markup, "\u2018\u2019\u201c\u201d") - - def test_smart_quotes_to_xml_entities(self): - markup = b"\x91\x92\x93\x94" - dammit = UnicodeDammit(markup, smart_quotes_to="xml") - self.assertEqual( - dammit.unicode_markup, "‘’“”") - - def test_smart_quotes_to_html_entities(self): - markup = b"\x91\x92\x93\x94" - dammit = UnicodeDammit(markup, smart_quotes_to="html") - self.assertEqual( - dammit.unicode_markup, "‘’“”") - - def test_smart_quotes_to_ascii(self): - markup = b"\x91\x92\x93\x94" - dammit = UnicodeDammit(markup, smart_quotes_to="ascii") - self.assertEqual( - dammit.unicode_markup, """''""""") - - def test_detect_utf8(self): - utf8 = b"Sacr\xc3\xa9 bleu! \xe2\x98\x83" - dammit = UnicodeDammit(utf8) - self.assertEqual(dammit.original_encoding.lower(), 'utf-8') - self.assertEqual(dammit.unicode_markup, 'Sacr\xe9 bleu! \N{SNOWMAN}') - - - def test_convert_hebrew(self): - hebrew = b"\xed\xe5\xec\xf9" - dammit = UnicodeDammit(hebrew, ["iso-8859-8"]) - self.assertEqual(dammit.original_encoding.lower(), 'iso-8859-8') - self.assertEqual(dammit.unicode_markup, '\u05dd\u05d5\u05dc\u05e9') - - def test_dont_see_smart_quotes_where_there_are_none(self): - utf_8 = b"\343\202\261\343\203\274\343\202\277\343\202\244 Watch" - dammit = UnicodeDammit(utf_8) - self.assertEqual(dammit.original_encoding.lower(), 'utf-8') - self.assertEqual(dammit.unicode_markup.encode("utf-8"), utf_8) - - def test_ignore_inappropriate_codecs(self): - utf8_data = "RäksmörgÃ¥s".encode("utf-8") - dammit = UnicodeDammit(utf8_data, ["iso-8859-8"]) - self.assertEqual(dammit.original_encoding.lower(), 'utf-8') - - def test_ignore_invalid_codecs(self): - utf8_data = "RäksmörgÃ¥s".encode("utf-8") - for bad_encoding in ['.utf8', '...', 'utF---16.!']: - dammit = UnicodeDammit(utf8_data, [bad_encoding]) - self.assertEqual(dammit.original_encoding.lower(), 'utf-8') - - def test_exclude_encodings(self): - # This is UTF-8. - utf8_data = "RäksmörgÃ¥s".encode("utf-8") - - # But if we exclude UTF-8 from consideration, the guess is - # Windows-1252. - dammit = UnicodeDammit(utf8_data, exclude_encodings=["utf-8"]) - self.assertEqual(dammit.original_encoding.lower(), 'windows-1252') - - # And if we exclude that, there is no valid guess at all. - dammit = UnicodeDammit( - utf8_data, exclude_encodings=["utf-8", "windows-1252"]) - self.assertEqual(dammit.original_encoding, None) - - def test_encoding_detector_replaces_junk_in_encoding_name_with_replacement_character(self): - detected = EncodingDetector( - b'') - encodings = list(detected.encodings) - assert 'utf-\N{REPLACEMENT CHARACTER}' in encodings - - def test_detect_html5_style_meta_tag(self): - - for data in ( - b'', - b"", - b"", - b""): - dammit = UnicodeDammit(data, is_html=True) - self.assertEqual( - "euc-jp", dammit.original_encoding) - - def test_last_ditch_entity_replacement(self): - # This is a UTF-8 document that contains bytestrings - # completely incompatible with UTF-8 (ie. encoded with some other - # encoding). - # - # Since there is no consistent encoding for the document, - # Unicode, Dammit will eventually encode the document as UTF-8 - # and encode the incompatible characters as REPLACEMENT - # CHARACTER. - # - # If chardet is installed, it will detect that the document - # can be converted into ISO-8859-1 without errors. This happens - # to be the wrong encoding, but it is a consistent encoding, so the - # code we're testing here won't run. - # - # So we temporarily disable chardet if it's present. - doc = b"""\357\273\277 -\330\250\330\252\330\261 -\310\322\321\220\312\321\355\344""" - chardet = bs4.dammit.chardet_dammit - logging.disable(logging.WARNING) - try: - def noop(str): - return None - bs4.dammit.chardet_dammit = noop - dammit = UnicodeDammit(doc) - self.assertEqual(True, dammit.contains_replacement_characters) - self.assertTrue("\ufffd" in dammit.unicode_markup) - - soup = BeautifulSoup(doc, "html.parser") - self.assertTrue(soup.contains_replacement_characters) - finally: - logging.disable(logging.NOTSET) - bs4.dammit.chardet_dammit = chardet - - def test_byte_order_mark_removed(self): - # A document written in UTF-16LE will have its byte order marker stripped. - data = b'\xff\xfe<\x00a\x00>\x00\xe1\x00\xe9\x00<\x00/\x00a\x00>\x00' - dammit = UnicodeDammit(data) - self.assertEqual("áé", dammit.unicode_markup) - self.assertEqual("utf-16le", dammit.original_encoding) - - def test_detwingle(self): - # Here's a UTF8 document. - utf8 = ("\N{SNOWMAN}" * 3).encode("utf8") - - # Here's a Windows-1252 document. - windows_1252 = ( - "\N{LEFT DOUBLE QUOTATION MARK}Hi, I like Windows!" - "\N{RIGHT DOUBLE QUOTATION MARK}").encode("windows_1252") - - # Through some unholy alchemy, they've been stuck together. - doc = utf8 + windows_1252 + utf8 - - # The document can't be turned into UTF-8: - self.assertRaises(UnicodeDecodeError, doc.decode, "utf8") - - # Unicode, Dammit thinks the whole document is Windows-1252, - # and decodes it into "☃☃☃“Hi, I like Windows!â€Ã¢ËœÆ’☃☃" - - # But if we run it through fix_embedded_windows_1252, it's fixed: - - fixed = UnicodeDammit.detwingle(doc) - self.assertEqual( - "☃☃☃“Hi, I like Windows!â€â˜ƒâ˜ƒâ˜ƒ", fixed.decode("utf8")) - - def test_detwingle_ignores_multibyte_characters(self): - # Each of these characters has a UTF-8 representation ending - # in \x93. \x93 is a smart quote if interpreted as - # Windows-1252. But our code knows to skip over multibyte - # UTF-8 characters, so they'll survive the process unscathed. - for tricky_unicode_char in ( - "\N{LATIN SMALL LIGATURE OE}", # 2-byte char '\xc5\x93' - "\N{LATIN SUBSCRIPT SMALL LETTER X}", # 3-byte char '\xe2\x82\x93' - "\xf0\x90\x90\x93", # This is a CJK character, not sure which one. - ): - input = tricky_unicode_char.encode("utf8") - self.assertTrue(input.endswith(b'\x93')) - output = UnicodeDammit.detwingle(input) - self.assertEqual(output, input) class TestNamedspacedAttribute(SoupTest): - def test_name_may_be_none(self): + def test_name_may_be_none_or_missing(self): a = NamespacedAttribute("xmlns", None) self.assertEqual(a, "xmlns") + a = NamespacedAttribute("xmlns", "") + self.assertEqual(a, "xmlns") + + a = NamespacedAttribute("xmlns") + self.assertEqual(a, "xmlns") + + def test_namespace_may_be_none_or_missing(self): + a = NamespacedAttribute(None, "tag") + self.assertEqual(a, "tag") + + a = NamespacedAttribute("", "tag") + self.assertEqual(a, "tag") + def test_attribute_is_equivalent_to_colon_separated_string(self): a = NamespacedAttribute("a", "b") self.assertEqual("a:b", a) diff --git a/libs/bs4/tests/test_tree.py b/libs/bs4/tests/test_tree.py index 3b4beeb8f..59b51d0b3 100644 --- a/libs/bs4/tests/test_tree.py +++ b/libs/bs4/tests/test_tree.py @@ -27,13 +27,17 @@ from bs4.element import ( Doctype, Formatter, NavigableString, + Script, SoupStrainer, + Stylesheet, Tag, + TemplateString, ) from bs4.testing import ( SoupTest, skipIf, ) +from soupsieve import SelectorSyntaxError XML_BUILDER_PRESENT = (builder_registry.lookup("xml") is not None) LXML_PRESENT = (builder_registry.lookup("lxml") is not None) @@ -741,6 +745,30 @@ class TestPreviousSibling(SiblingTest): self.assertEqual(start.find_previous_sibling(text="nonesuch"), None) +class TestTag(SoupTest): + + # Test various methods of Tag. + + def test__should_pretty_print(self): + # Test the rules about when a tag should be pretty-printed. + tag = self.soup("").new_tag("a_tag") + + # No list of whitespace-preserving tags -> pretty-print + tag._preserve_whitespace_tags = None + self.assertEqual(True, tag._should_pretty_print(0)) + + # List exists but tag is not on the list -> pretty-print + tag.preserve_whitespace_tags = ["some_other_tag"] + self.assertEqual(True, tag._should_pretty_print(1)) + + # Indent level is None -> don't pretty-print + self.assertEqual(False, tag._should_pretty_print(None)) + + # Tag is on the whitespace-preserving list -> don't pretty-print + tag.preserve_whitespace_tags = ["some_other_tag", "a_tag"] + self.assertEqual(False, tag._should_pretty_print(1)) + + class TestTagCreation(SoupTest): """Test the ability to create new tags.""" def test_new_tag(self): @@ -981,6 +1009,15 @@ class TestTreeModification(SoupTest): soup.a.extend(l) self.assertEqual("", soup.decode()) + def test_extend_with_another_tags_contents(self): + data = '
' + soup = self.soup(data) + d1 = soup.find('div', id='d1') + d2 = soup.find('div', id='d2') + d2.extend(d1) + self.assertEqual('
', d1.decode()) + self.assertEqual('', d2.decode()) + def test_move_tag_to_beginning_of_parent(self): data = "" soup = self.soup(data) @@ -1093,6 +1130,37 @@ class TestTreeModification(SoupTest): self.assertEqual(no.next_element, "no") self.assertEqual(no.next_sibling, " business") + def test_replace_with_errors(self): + # Can't replace a tag that's not part of a tree. + a_tag = Tag(name="a") + self.assertRaises(ValueError, a_tag.replace_with, "won't work") + + # Can't replace a tag with its parent. + a_tag = self.soup("").a + self.assertRaises(ValueError, a_tag.b.replace_with, a_tag) + + # Or with a list that includes its parent. + self.assertRaises(ValueError, a_tag.b.replace_with, + "string1", a_tag, "string2") + + def test_replace_with_multiple(self): + data = "" + soup = self.soup(data) + d_tag = soup.new_tag("d") + d_tag.string = "Text In D Tag" + e_tag = soup.new_tag("e") + f_tag = soup.new_tag("f") + a_string = "Random Text" + soup.c.replace_with(d_tag, e_tag, a_string, f_tag) + self.assertEqual( + "Text In D TagRandom Text", + soup.decode() + ) + assert soup.b.next_element == d_tag + assert d_tag.string.next_element==e_tag + assert e_tag.next_element.string == a_string + assert e_tag.next_element.next_element == f_tag + def test_replace_first_child(self): data = "" soup = self.soup(data) @@ -1251,6 +1319,23 @@ class TestTreeModification(SoupTest): a.clear(decompose=True) self.assertEqual(0, len(em.contents)) + + def test_decompose(self): + # Test PageElement.decompose() and PageElement.decomposed + soup = self.soup("

String Italicized

Another para

") + p1, p2 = soup.find_all('p') + a = p1.a + text = p1.em.string + for i in [p1, p2, a, text]: + self.assertEqual(False, i.decomposed) + + # This sets p1 and everything beneath it to decomposed. + p1.decompose() + for i in [p1, a, text]: + self.assertEqual(True, i.decomposed) + # p2 is unaffected. + self.assertEqual(False, p2.decomposed) + def test_string_set(self): """Tag.string = 'string'""" soup = self.soup(" ") @@ -1367,7 +1452,7 @@ class TestElementObjects(SoupTest): self.assertEqual(soup.a.get_text(","), "a,r, , t ") self.assertEqual(soup.a.get_text(",", strip=True), "a,r,t") - def test_get_text_ignores_comments(self): + def test_get_text_ignores_special_string_containers(self): soup = self.soup("foobar") self.assertEqual(soup.get_text(), "foobar") @@ -1376,10 +1461,51 @@ class TestElementObjects(SoupTest): self.assertEqual( soup.get_text(types=None), "fooIGNOREbar") - def test_all_strings_ignores_comments(self): + soup = self.soup("foobar") + self.assertEqual(soup.get_text(), "foobar") + + def test_all_strings_ignores_special_string_containers(self): soup = self.soup("foobar") self.assertEqual(['foo', 'bar'], list(soup.strings)) + soup = self.soup("foobar") + self.assertEqual(['foo', 'bar'], list(soup.strings)) + + def test_string_methods_inside_special_string_container_tags(self): + # Strings inside tags like
") + + self.assertEqual(style.div.get_text(), "a") + self.assertEqual(list(style.div.strings), ["a"]) + self.assertEqual(style.div.style.get_text(), "Some CSS") + self.assertEqual(list(style.div.style.strings), + ['Some CSS']) + + # The comment is not picked up here. That's because it was + # parsed into a Comment object, which is not considered + # interesting by template.strings. + self.assertEqual(template.div.get_text(), "a") + self.assertEqual(list(template.div.strings), ["a"]) + self.assertEqual(template.div.template.get_text(), "Templated text.") + self.assertEqual(list(template.div.template.strings), + ["Templated ", "text", "."]) + + # The comment is included here, because it didn't get parsed + # into a Comment object--it's part of the Script string. + self.assertEqual(script.div.get_text(), "a") + self.assertEqual(list(script.div.strings), ["a"]) + self.assertEqual(script.div.script.get_text(), + "Some text") + self.assertEqual(list(script.div.script.strings), + ['Some text']) + class TestCDAtaListAttributes(SoupTest): """Testing cdata-list attributes like 'class'. @@ -1466,6 +1592,31 @@ class TestPersistence(SoupTest): self.assertEqual("

 

", str(copy)) self.assertEqual(encoding, copy.original_encoding) + def test_copy_preserves_builder_information(self): + + tag = self.soup('

').p + + # Simulate a tag obtained from a source file. + tag.sourceline = 10 + tag.sourcepos = 33 + + copied = tag.__copy__() + + # The TreeBuilder object is no longer availble, but information + # obtained from it gets copied over to the new Tag object. + self.assertEqual(tag.sourceline, copied.sourceline) + self.assertEqual(tag.sourcepos, copied.sourcepos) + self.assertEqual( + tag.can_be_empty_element, copied.can_be_empty_element + ) + self.assertEqual( + tag.cdata_list_attributes, copied.cdata_list_attributes + ) + self.assertEqual( + tag.preserve_whitespace_tags, copied.preserve_whitespace_tags + ) + + def test_unicode_pickle(self): # A tree containing Unicode characters can be pickled. html = "\N{SNOWMAN}" @@ -1726,71 +1877,7 @@ class TestEncoding(SoupTest): else: self.assertEqual(b'\\u2603', repr(soup)) -class TestFormatter(SoupTest): - - def test_sort_attributes(self): - # Test the ability to override Formatter.attributes() to, - # e.g., disable the normal sorting of attributes. - class UnsortedFormatter(Formatter): - def attributes(self, tag): - self.called_with = tag - for k, v in sorted(tag.attrs.items()): - if k == 'ignore': - continue - yield k,v - - soup = self.soup('

') - formatter = UnsortedFormatter() - decoded = soup.decode(formatter=formatter) - - # attributes() was called on the

tag. It filtered out one - # attribute and sorted the other two. - self.assertEqual(formatter.called_with, soup.p) - self.assertEqual('

', decoded) - - -class TestNavigableStringSubclasses(SoupTest): - - def test_cdata(self): - # None of the current builders turn CDATA sections into CData - # objects, but you can create them manually. - soup = self.soup("") - cdata = CData("foo") - soup.insert(1, cdata) - self.assertEqual(str(soup), "") - self.assertEqual(soup.find(text="foo"), "foo") - self.assertEqual(soup.contents[0], "foo") - - def test_cdata_is_never_formatted(self): - """Text inside a CData object is passed into the formatter. - - But the return value is ignored. - """ - - self.count = 0 - def increment(*args): - self.count += 1 - return "BITTER FAILURE" - - soup = self.soup("") - cdata = CData("<><><>") - soup.insert(1, cdata) - self.assertEqual( - b"<><>]]>", soup.encode(formatter=increment)) - self.assertEqual(1, self.count) - - def test_doctype_ends_in_newline(self): - # Unlike other NavigableString subclasses, a DOCTYPE always ends - # in a newline. - doctype = Doctype("foo") - soup = self.soup("") - soup.insert(1, doctype) - self.assertEqual(soup.encode(), b"\n") - - def test_declaration(self): - d = Declaration("foo") - self.assertEqual("", d.output_ready()) - + class TestSoupSelector(TreeTest): HTML = """ @@ -1900,7 +1987,7 @@ class TestSoupSelector(TreeTest): self.assertEqual(len(self.soup.select('del')), 0) def test_invalid_tag(self): - self.assertRaises(SyntaxError, self.soup.select, 'tag%t') + self.assertRaises(SelectorSyntaxError, self.soup.select, 'tag%t') def test_select_dashed_tag_ids(self): self.assertSelects('custom-dashed-tag', ['dash1', 'dash2']) @@ -2091,7 +2178,7 @@ class TestSoupSelector(TreeTest): NotImplementedError, self.soup.select, "a:no-such-pseudoclass") self.assertRaises( - SyntaxError, self.soup.select, "a:nth-of-type(a)") + SelectorSyntaxError, self.soup.select, "a:nth-of-type(a)") def test_nth_of_type(self): # Try to select first paragraph @@ -2147,7 +2234,7 @@ class TestSoupSelector(TreeTest): self.assertEqual([], self.soup.select('#inner ~ h2')) def test_dangling_combinator(self): - self.assertRaises(SyntaxError, self.soup.select, 'h1 >') + self.assertRaises(SelectorSyntaxError, self.soup.select, 'h1 >') def test_sibling_combinator_wont_select_same_tag_twice(self): self.assertSelects('p[lang] ~ p', ['lang-en-gb', 'lang-en-us', 'lang-fr']) @@ -2178,8 +2265,8 @@ class TestSoupSelector(TreeTest): self.assertSelects('div x,y, z', ['xid', 'yid', 'zida', 'zidb', 'zidab', 'zidac']) def test_invalid_multiple_select(self): - self.assertRaises(SyntaxError, self.soup.select, ',x, y') - self.assertRaises(SyntaxError, self.soup.select, 'x,,y') + self.assertRaises(SelectorSyntaxError, self.soup.select, ',x, y') + self.assertRaises(SelectorSyntaxError, self.soup.select, 'x,,y') def test_multiple_select_attrs(self): self.assertSelects('p[lang=en], p[lang=en-gb]', ['lang-en', 'lang-en-gb']) diff --git a/libs/certifi/__init__.py b/libs/certifi/__init__.py index 8e358e4c8..8db1a0e55 100644 --- a/libs/certifi/__init__.py +++ b/libs/certifi/__init__.py @@ -1,3 +1,3 @@ -from .core import where +from .core import contents, where -__version__ = "2019.09.11" +__version__ = "2021.10.08" diff --git a/libs/certifi/__main__.py b/libs/certifi/__main__.py index 5f1da0dd0..8945b5da8 100644 --- a/libs/certifi/__main__.py +++ b/libs/certifi/__main__.py @@ -1,2 +1,12 @@ -from certifi import where -print(where()) +import argparse + +from certifi import contents, where + +parser = argparse.ArgumentParser() +parser.add_argument("-c", "--contents", action="store_true") +args = parser.parse_args() + +if args.contents: + print(contents()) +else: + print(where()) diff --git a/libs/certifi/cacert.pem b/libs/certifi/cacert.pem index 70fa91f61..6d0ccc0d1 100644 --- a/libs/certifi/cacert.pem +++ b/libs/certifi/cacert.pem @@ -58,38 +58,6 @@ AfvDbbnvRG15RjF+Cv6pgsH/76tuIMRQyV+dTZsXjAzlAcmgQWpzU/qlULRuJQ/7 TBj0/VLZjmmx6BEP3ojY+x1J96relc8geMJgEtslQIxq/H5COEBkEveegeGTLg== -----END CERTIFICATE----- -# Issuer: CN=VeriSign Class 3 Public Primary Certification Authority - G3 O=VeriSign, Inc. OU=VeriSign Trust Network/(c) 1999 VeriSign, Inc. - For authorized use only -# Subject: CN=VeriSign Class 3 Public Primary Certification Authority - G3 O=VeriSign, Inc. OU=VeriSign Trust Network/(c) 1999 VeriSign, Inc. - For authorized use only -# Label: "Verisign Class 3 Public Primary Certification Authority - G3" -# Serial: 206684696279472310254277870180966723415 -# MD5 Fingerprint: cd:68:b6:a7:c7:c4:ce:75:e0:1d:4f:57:44:61:92:09 -# SHA1 Fingerprint: 13:2d:0d:45:53:4b:69:97:cd:b2:d5:c3:39:e2:55:76:60:9b:5c:c6 -# SHA256 Fingerprint: eb:04:cf:5e:b1:f3:9a:fa:76:2f:2b:b1:20:f2:96:cb:a5:20:c1:b9:7d:b1:58:95:65:b8:1c:b9:a1:7b:72:44 ------BEGIN CERTIFICATE----- -MIIEGjCCAwICEQCbfgZJoz5iudXukEhxKe9XMA0GCSqGSIb3DQEBBQUAMIHKMQsw -CQYDVQQGEwJVUzEXMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xHzAdBgNVBAsTFlZl -cmlTaWduIFRydXN0IE5ldHdvcmsxOjA4BgNVBAsTMShjKSAxOTk5IFZlcmlTaWdu -LCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxRTBDBgNVBAMTPFZlcmlT -aWduIENsYXNzIDMgUHVibGljIFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3Jp -dHkgLSBHMzAeFw05OTEwMDEwMDAwMDBaFw0zNjA3MTYyMzU5NTlaMIHKMQswCQYD -VQQGEwJVUzEXMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xHzAdBgNVBAsTFlZlcmlT -aWduIFRydXN0IE5ldHdvcmsxOjA4BgNVBAsTMShjKSAxOTk5IFZlcmlTaWduLCBJ -bmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxRTBDBgNVBAMTPFZlcmlTaWdu -IENsYXNzIDMgUHVibGljIFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkg -LSBHMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMu6nFL8eB8aHm8b -N3O9+MlrlBIwT/A2R/XQkQr1F8ilYcEWQE37imGQ5XYgwREGfassbqb1EUGO+i2t -KmFZpGcmTNDovFJbcCAEWNF6yaRpvIMXZK0Fi7zQWM6NjPXr8EJJC52XJ2cybuGu -kxUccLwgTS8Y3pKI6GyFVxEa6X7jJhFUokWWVYPKMIno3Nij7SqAP395ZVc+FSBm -CC+Vk7+qRy+oRpfwEuL+wgorUeZ25rdGt+INpsyow0xZVYnm6FNcHOqd8GIWC6fJ -Xwzw3sJ2zq/3avL6QaaiMxTJ5Xpj055iN9WFZZ4O5lMkdBteHRJTW8cs54NJOxWu -imi5V5cCAwEAATANBgkqhkiG9w0BAQUFAAOCAQEAERSWwauSCPc/L8my/uRan2Te -2yFPhpk0djZX3dAVL8WtfxUfN2JzPtTnX84XA9s1+ivbrmAJXx5fj267Cz3qWhMe -DGBvtcC1IyIuBwvLqXTLR7sdwdela8wv0kL9Sd2nic9TutoAWii/gt/4uhMdUIaC -/Y4wjylGsB49Ndo4YhYYSq3mtlFs3q9i6wHQHiT+eo8SGhJouPtmmRQURVyu565p -F4ErWjfJXir0xuKhXFSbplQAz/DxwceYMBo7Nhbbo27q/a2ywtrvAkcTisDxszGt -TxzhT5yvDwyd93gN2PQ1VoDat20Xj50egWTh/sVFuq1ruQp6Tk9LhO5L8X3dEQ== ------END CERTIFICATE----- - # Issuer: CN=Entrust.net Certification Authority (2048) O=Entrust.net OU=www.entrust.net/CPS_2048 incorp. by ref. (limits liab.)/(c) 1999 Entrust.net Limited # Subject: CN=Entrust.net Certification Authority (2048) O=Entrust.net OU=www.entrust.net/CPS_2048 incorp. by ref. (limits liab.)/(c) 1999 Entrust.net Limited # Label: "Entrust.net Premium 2048 Secure Server CA" @@ -152,39 +120,6 @@ ksLi4xaNmjICq44Y3ekQEe5+NauQrz4wlHrQMz2nZQ/1/I6eYs9HRCwBXbsdtTLS R9I4LtD+gdwyah617jzV/OeBHRnDJELqYzmp -----END CERTIFICATE----- -# Issuer: CN=AddTrust External CA Root O=AddTrust AB OU=AddTrust External TTP Network -# Subject: CN=AddTrust External CA Root O=AddTrust AB OU=AddTrust External TTP Network -# Label: "AddTrust External Root" -# Serial: 1 -# MD5 Fingerprint: 1d:35:54:04:85:78:b0:3f:42:42:4d:bf:20:73:0a:3f -# SHA1 Fingerprint: 02:fa:f3:e2:91:43:54:68:60:78:57:69:4d:f5:e4:5b:68:85:18:68 -# SHA256 Fingerprint: 68:7f:a4:51:38:22:78:ff:f0:c8:b1:1f:8d:43:d5:76:67:1c:6e:b2:bc:ea:b4:13:fb:83:d9:65:d0:6d:2f:f2 ------BEGIN CERTIFICATE----- -MIIENjCCAx6gAwIBAgIBATANBgkqhkiG9w0BAQUFADBvMQswCQYDVQQGEwJTRTEU -MBIGA1UEChMLQWRkVHJ1c3QgQUIxJjAkBgNVBAsTHUFkZFRydXN0IEV4dGVybmFs -IFRUUCBOZXR3b3JrMSIwIAYDVQQDExlBZGRUcnVzdCBFeHRlcm5hbCBDQSBSb290 -MB4XDTAwMDUzMDEwNDgzOFoXDTIwMDUzMDEwNDgzOFowbzELMAkGA1UEBhMCU0Ux -FDASBgNVBAoTC0FkZFRydXN0IEFCMSYwJAYDVQQLEx1BZGRUcnVzdCBFeHRlcm5h -bCBUVFAgTmV0d29yazEiMCAGA1UEAxMZQWRkVHJ1c3QgRXh0ZXJuYWwgQ0EgUm9v -dDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALf3GjPm8gAELTngTlvt -H7xsD821+iO2zt6bETOXpClMfZOfvUq8k+0DGuOPz+VtUFrWlymUWoCwSXrbLpX9 -uMq/NzgtHj6RQa1wVsfwTz/oMp50ysiQVOnGXw94nZpAPA6sYapeFI+eh6FqUNzX -mk6vBbOmcZSccbNQYArHE504B4YCqOmoaSYYkKtMsE8jqzpPhNjfzp/haW+710LX -a0Tkx63ubUFfclpxCDezeWWkWaCUN/cALw3CknLa0Dhy2xSoRcRdKn23tNbE7qzN -E0S3ySvdQwAl+mG5aWpYIxG3pzOPVnVZ9c0p10a3CitlttNCbxWyuHv77+ldU9U0 -WicCAwEAAaOB3DCB2TAdBgNVHQ4EFgQUrb2YejS0Jvf6xCZU7wO94CTLVBowCwYD -VR0PBAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wgZkGA1UdIwSBkTCBjoAUrb2YejS0 -Jvf6xCZU7wO94CTLVBqhc6RxMG8xCzAJBgNVBAYTAlNFMRQwEgYDVQQKEwtBZGRU -cnVzdCBBQjEmMCQGA1UECxMdQWRkVHJ1c3QgRXh0ZXJuYWwgVFRQIE5ldHdvcmsx -IjAgBgNVBAMTGUFkZFRydXN0IEV4dGVybmFsIENBIFJvb3SCAQEwDQYJKoZIhvcN -AQEFBQADggEBALCb4IUlwtYj4g+WBpKdQZic2YR5gdkeWxQHIzZlj7DYd7usQWxH -YINRsPkyPef89iYTx4AWpb9a/IfPeHmJIZriTAcKhjW88t5RxNKWt9x+Tu5w/Rw5 -6wwCURQtjr0W4MHfRnXnJK3s9EK0hZNwEGe6nQY1ShjTK3rMUUKhemPR5ruhxSvC -Nr4TDea9Y355e6cJDUCrat2PisP29owaQgVR1EX1n6diIWgVIEM8med8vSTYqZEX -c4g/VhsxOBi0cQ+azcgOno4uG+GMmIPLHzHxREzGBHNJdmAPx/i9F4BrLunMTA5a -mnkPIAou1Z5jJh5VkpTYghdae9C8x49OhgQ= ------END CERTIFICATE----- - # Issuer: CN=Entrust Root Certification Authority O=Entrust, Inc. OU=www.entrust.net/CPS is incorporated by reference/(c) 2006 Entrust, Inc. # Subject: CN=Entrust Root Certification Authority O=Entrust, Inc. OU=www.entrust.net/CPS is incorporated by reference/(c) 2006 Entrust, Inc. # Label: "Entrust Root Certification Authority" @@ -220,112 +155,6 @@ eu6FSqdQgPCnXEqULl8FmTxSQeDNtGPPAUO6nIPcj2A781q0tHuu2guQOHXvgR1m 0vdXcDazv/wor3ElhVsT/h5/WrQ8 -----END CERTIFICATE----- -# Issuer: CN=GeoTrust Global CA O=GeoTrust Inc. -# Subject: CN=GeoTrust Global CA O=GeoTrust Inc. -# Label: "GeoTrust Global CA" -# Serial: 144470 -# MD5 Fingerprint: f7:75:ab:29:fb:51:4e:b7:77:5e:ff:05:3c:99:8e:f5 -# SHA1 Fingerprint: de:28:f4:a4:ff:e5:b9:2f:a3:c5:03:d1:a3:49:a7:f9:96:2a:82:12 -# SHA256 Fingerprint: ff:85:6a:2d:25:1d:cd:88:d3:66:56:f4:50:12:67:98:cf:ab:aa:de:40:79:9c:72:2d:e4:d2:b5:db:36:a7:3a ------BEGIN CERTIFICATE----- -MIIDVDCCAjygAwIBAgIDAjRWMA0GCSqGSIb3DQEBBQUAMEIxCzAJBgNVBAYTAlVT -MRYwFAYDVQQKEw1HZW9UcnVzdCBJbmMuMRswGQYDVQQDExJHZW9UcnVzdCBHbG9i -YWwgQ0EwHhcNMDIwNTIxMDQwMDAwWhcNMjIwNTIxMDQwMDAwWjBCMQswCQYDVQQG -EwJVUzEWMBQGA1UEChMNR2VvVHJ1c3QgSW5jLjEbMBkGA1UEAxMSR2VvVHJ1c3Qg -R2xvYmFsIENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2swYYzD9 -9BcjGlZ+W988bDjkcbd4kdS8odhM+KhDtgPpTSEHCIjaWC9mOSm9BXiLnTjoBbdq -fnGk5sRgprDvgOSJKA+eJdbtg/OtppHHmMlCGDUUna2YRpIuT8rxh0PBFpVXLVDv -iS2Aelet8u5fa9IAjbkU+BQVNdnARqN7csiRv8lVK83Qlz6cJmTM386DGXHKTubU -1XupGc1V3sjs0l44U+VcT4wt/lAjNvxm5suOpDkZALeVAjmRCw7+OC7RHQWa9k0+ -bw8HHa8sHo9gOeL6NlMTOdReJivbPagUvTLrGAMoUgRx5aszPeE4uwc2hGKceeoW -MPRfwCvocWvk+QIDAQABo1MwUTAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTA -ephojYn7qwVkDBF9qn1luMrMTjAfBgNVHSMEGDAWgBTAephojYn7qwVkDBF9qn1l -uMrMTjANBgkqhkiG9w0BAQUFAAOCAQEANeMpauUvXVSOKVCUn5kaFOSPeCpilKIn -Z57QzxpeR+nBsqTP3UEaBU6bS+5Kb1VSsyShNwrrZHYqLizz/Tt1kL/6cdjHPTfS -tQWVYrmm3ok9Nns4d0iXrKYgjy6myQzCsplFAMfOEVEiIuCl6rYVSAlk6l5PdPcF -PseKUgzbFbS9bZvlxrFUaKnjaZC2mqUPuLk/IH2uSrW4nOQdtqvmlKXBx4Ot2/Un -hw4EbNX/3aBd7YdStysVAq45pmp06drE57xNNB6pXE0zX5IJL4hmXXeXxx12E6nV -5fEWCRE11azbJHFwLJhWC9kXtNHjUStedejV0NxPNO3CBWaAocvmMw== ------END CERTIFICATE----- - -# Issuer: CN=GeoTrust Universal CA O=GeoTrust Inc. -# Subject: CN=GeoTrust Universal CA O=GeoTrust Inc. -# Label: "GeoTrust Universal CA" -# Serial: 1 -# MD5 Fingerprint: 92:65:58:8b:a2:1a:31:72:73:68:5c:b4:a5:7a:07:48 -# SHA1 Fingerprint: e6:21:f3:35:43:79:05:9a:4b:68:30:9d:8a:2f:74:22:15:87:ec:79 -# SHA256 Fingerprint: a0:45:9b:9f:63:b2:25:59:f5:fa:5d:4c:6d:b3:f9:f7:2f:f1:93:42:03:35:78:f0:73:bf:1d:1b:46:cb:b9:12 ------BEGIN CERTIFICATE----- -MIIFaDCCA1CgAwIBAgIBATANBgkqhkiG9w0BAQUFADBFMQswCQYDVQQGEwJVUzEW -MBQGA1UEChMNR2VvVHJ1c3QgSW5jLjEeMBwGA1UEAxMVR2VvVHJ1c3QgVW5pdmVy -c2FsIENBMB4XDTA0MDMwNDA1MDAwMFoXDTI5MDMwNDA1MDAwMFowRTELMAkGA1UE -BhMCVVMxFjAUBgNVBAoTDUdlb1RydXN0IEluYy4xHjAcBgNVBAMTFUdlb1RydXN0 -IFVuaXZlcnNhbCBDQTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAKYV -VaCjxuAfjJ0hUNfBvitbtaSeodlyWL0AG0y/YckUHUWCq8YdgNY96xCcOq9tJPi8 -cQGeBvV8Xx7BDlXKg5pZMK4ZyzBIle0iN430SppyZj6tlcDgFgDgEB8rMQ7XlFTT -QjOgNB0eRXbdT8oYN+yFFXoZCPzVx5zw8qkuEKmS5j1YPakWaDwvdSEYfyh3peFh -F7em6fgemdtzbvQKoiFs7tqqhZJmr/Z6a4LauiIINQ/PQvE1+mrufislzDoR5G2v -c7J2Ha3QsnhnGqQ5HFELZ1aD/ThdDc7d8Lsrlh/eezJS/R27tQahsiFepdaVaH/w -mZ7cRQg+59IJDTWU3YBOU5fXtQlEIGQWFwMCTFMNaN7VqnJNk22CDtucvc+081xd -VHppCZbW2xHBjXWotM85yM48vCR85mLK4b19p71XZQvk/iXttmkQ3CgaRr0BHdCX -teGYO8A3ZNY9lO4L4fUorgtWv3GLIylBjobFS1J72HGrH4oVpjuDWtdYAVHGTEHZ -f9hBZ3KiKN9gg6meyHv8U3NyWfWTehd2Ds735VzZC1U0oqpbtWpU5xPKV+yXbfRe -Bi9Fi1jUIxaS5BZuKGNZMN9QAZxjiRqf2xeUgnA3wySemkfWWspOqGmJch+RbNt+ -nhutxx9z3SxPGWX9f5NAEC7S8O08ni4oPmkmM8V7AgMBAAGjYzBhMA8GA1UdEwEB -/wQFMAMBAf8wHQYDVR0OBBYEFNq7LqqwDLiIJlF0XG0D08DYj3rWMB8GA1UdIwQY -MBaAFNq7LqqwDLiIJlF0XG0D08DYj3rWMA4GA1UdDwEB/wQEAwIBhjANBgkqhkiG -9w0BAQUFAAOCAgEAMXjmx7XfuJRAyXHEqDXsRh3ChfMoWIawC/yOsjmPRFWrZIRc -aanQmjg8+uUfNeVE44B5lGiku8SfPeE0zTBGi1QrlaXv9z+ZhP015s8xxtxqv6fX -IwjhmF7DWgh2qaavdy+3YL1ERmrvl/9zlcGO6JP7/TG37FcREUWbMPEaiDnBTzyn -ANXH/KttgCJwpQzgXQQpAvvLoJHRfNbDflDVnVi+QTjruXU8FdmbyUqDWcDaU/0z -uzYYm4UPFd3uLax2k7nZAY1IEKj79TiG8dsKxr2EoyNB3tZ3b4XUhRxQ4K5RirqN -Pnbiucon8l+f725ZDQbYKxek0nxru18UGkiPGkzns0ccjkxFKyDuSN/n3QmOGKja -QI2SJhFTYXNd673nxE0pN2HrrDktZy4W1vUAg4WhzH92xH3kt0tm7wNFYGm2DFKW -koRepqO1pD4r2czYG0eq8kTaT/kD6PAUyz/zg97QwVTjt+gKN02LIFkDMBmhLMi9 -ER/frslKxfMnZmaGrGiR/9nmUxwPi1xpZQomyB40w11Re9epnAahNt3ViZS82eQt -DF4JbAiXfKM9fJP/P6EUp8+1Xevb2xzEdt+Iub1FBZUbrvxGakyvSOPOrg/Sfuvm -bJxPgWp6ZKy7PtXny3YuxadIwVyQD8vIP/rmMuGNG2+k5o7Y+SlIis5z/iw= ------END CERTIFICATE----- - -# Issuer: CN=GeoTrust Universal CA 2 O=GeoTrust Inc. -# Subject: CN=GeoTrust Universal CA 2 O=GeoTrust Inc. -# Label: "GeoTrust Universal CA 2" -# Serial: 1 -# MD5 Fingerprint: 34:fc:b8:d0:36:db:9e:14:b3:c2:f2:db:8f:e4:94:c7 -# SHA1 Fingerprint: 37:9a:19:7b:41:85:45:35:0c:a6:03:69:f3:3c:2e:af:47:4f:20:79 -# SHA256 Fingerprint: a0:23:4f:3b:c8:52:7c:a5:62:8e:ec:81:ad:5d:69:89:5d:a5:68:0d:c9:1d:1c:b8:47:7f:33:f8:78:b9:5b:0b ------BEGIN CERTIFICATE----- -MIIFbDCCA1SgAwIBAgIBATANBgkqhkiG9w0BAQUFADBHMQswCQYDVQQGEwJVUzEW -MBQGA1UEChMNR2VvVHJ1c3QgSW5jLjEgMB4GA1UEAxMXR2VvVHJ1c3QgVW5pdmVy -c2FsIENBIDIwHhcNMDQwMzA0MDUwMDAwWhcNMjkwMzA0MDUwMDAwWjBHMQswCQYD -VQQGEwJVUzEWMBQGA1UEChMNR2VvVHJ1c3QgSW5jLjEgMB4GA1UEAxMXR2VvVHJ1 -c3QgVW5pdmVyc2FsIENBIDIwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoIC -AQCzVFLByT7y2dyxUxpZKeexw0Uo5dfR7cXFS6GqdHtXr0om/Nj1XqduGdt0DE81 -WzILAePb63p3NeqqWuDW6KFXlPCQo3RWlEQwAx5cTiuFJnSCegx2oG9NzkEtoBUG -FF+3Qs17j1hhNNwqCPkuwwGmIkQcTAeC5lvO0Ep8BNMZcyfwqph/Lq9O64ceJHdq -XbboW0W63MOhBW9Wjo8QJqVJwy7XQYci4E+GymC16qFjwAGXEHm9ADwSbSsVsaxL -se4YuU6W3Nx2/zu+z18DwPw76L5GG//aQMJS9/7jOvdqdzXQ2o3rXhhqMcceujwb -KNZrVMaqW9eiLBsZzKIC9ptZvTdrhrVtgrrY6slWvKk2WP0+GfPtDCapkzj4T8Fd -IgbQl+rhrcZV4IErKIM6+vR7IVEAvlI4zs1meaj0gVbi0IMJR1FbUGrP20gaXT73 -y/Zl92zxlfgCOzJWgjl6W70viRu/obTo/3+NjN8D8WBOWBFM66M/ECuDmgFz2ZRt -hAAnZqzwcEAJQpKtT5MNYQlRJNiS1QuUYbKHsu3/mjX/hVTK7URDrBs8FmtISgoc -QIgfksILAAX/8sgCSqSqqcyZlpwvWOB94b67B9xfBHJcMTTD7F8t4D1kkCLm0ey4 -Lt1ZrtmhN79UNdxzMk+MBB4zsslG8dhcyFVQyWi9qLo2CQIDAQABo2MwYTAPBgNV -HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR281Xh+qQ2+/CfXGJx7Tz0RzgQKzAfBgNV -HSMEGDAWgBR281Xh+qQ2+/CfXGJx7Tz0RzgQKzAOBgNVHQ8BAf8EBAMCAYYwDQYJ -KoZIhvcNAQEFBQADggIBAGbBxiPz2eAubl/oz66wsCVNK/g7WJtAJDday6sWSf+z -dXkzoS9tcBc0kf5nfo/sm+VegqlVHy/c1FEHEv6sFj4sNcZj/NwQ6w2jqtB8zNHQ -L1EuxBRa3ugZ4T7GzKQp5y6EqgYweHZUcyiYWTjgAA1i00J9IZ+uPTqM1fp3DRgr -Fg5fNuH8KrUwJM/gYwx7WBr+mbpCErGR9Hxo4sjoryzqyX6uuyo9DRXcNJW2GHSo -ag/HtPQTxORb7QrSpJdMKu0vbBKJPfEncKpqA1Ihn0CoZ1Dy81of398j9tx4TuaY -T1U6U+Pv8vSfx3zYWK8pIpe44L2RLrB27FcRz+8pRPPphXpgY+RdM4kX2TGq2tbz -GDVyz4crL2MjhF2EjD9XoIj8mZEoJmmZ1I+XRL6O1UixpCgp8RW04eWe3fiPpm8m -1wk8OhwRDqZsN/etRIcsKMfYdIKz0G9KV7s1KSegi+ghp4dkNl3M2Basx7InQJJV -OCiNUW7dFGdTbHFcJoRNdVq2fmBWqU2t+5sel/MN2dKXVHfaPRK34B7vCAas+YWH -6aLcr34YEoP9VhdBLtUpgn2Z9DH2canPLAEnpQW5qrJITirvn5NSUZU8UnOOVkwX -QMAJKOSLakhT2+zNVVXxxvjpoixMptEmX36vWkzaH6byHCx+rgIW0lbQL1dTR+iS ------END CERTIFICATE----- - # Issuer: CN=AAA Certificate Services O=Comodo CA Limited # Subject: CN=AAA Certificate Services O=Comodo CA Limited # Label: "Comodo AAA Services root" @@ -359,48 +188,6 @@ l2D4kF501KKaU73yqWjgom7C12yxow+ev+to51byrvLjKzg6CYG1a4XXvi3tPxq3 smPi9WIsgtRqAEFQ8TmDn5XpNpaYbg== -----END CERTIFICATE----- -# Issuer: CN=QuoVadis Root Certification Authority O=QuoVadis Limited OU=Root Certification Authority -# Subject: CN=QuoVadis Root Certification Authority O=QuoVadis Limited OU=Root Certification Authority -# Label: "QuoVadis Root CA" -# Serial: 985026699 -# MD5 Fingerprint: 27:de:36:fe:72:b7:00:03:00:9d:f4:f0:1e:6c:04:24 -# SHA1 Fingerprint: de:3f:40:bd:50:93:d3:9b:6c:60:f6:da:bc:07:62:01:00:89:76:c9 -# SHA256 Fingerprint: a4:5e:de:3b:bb:f0:9c:8a:e1:5c:72:ef:c0:72:68:d6:93:a2:1c:99:6f:d5:1e:67:ca:07:94:60:fd:6d:88:73 ------BEGIN CERTIFICATE----- -MIIF0DCCBLigAwIBAgIEOrZQizANBgkqhkiG9w0BAQUFADB/MQswCQYDVQQGEwJC -TTEZMBcGA1UEChMQUXVvVmFkaXMgTGltaXRlZDElMCMGA1UECxMcUm9vdCBDZXJ0 -aWZpY2F0aW9uIEF1dGhvcml0eTEuMCwGA1UEAxMlUXVvVmFkaXMgUm9vdCBDZXJ0 -aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0wMTAzMTkxODMzMzNaFw0yMTAzMTcxODMz -MzNaMH8xCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBMaW1pdGVkMSUw -IwYDVQQLExxSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MS4wLAYDVQQDEyVR -dW9WYWRpcyBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIIBIjANBgkqhkiG -9w0BAQEFAAOCAQ8AMIIBCgKCAQEAv2G1lVO6V/z68mcLOhrfEYBklbTRvM16z/Yp -li4kVEAkOPcahdxYTMukJ0KX0J+DisPkBgNbAKVRHnAEdOLB1Dqr1607BxgFjv2D -rOpm2RgbaIr1VxqYuvXtdj182d6UajtLF8HVj71lODqV0D1VNk7feVcxKh7YWWVJ -WCCYfqtffp/p1k3sg3Spx2zY7ilKhSoGFPlU5tPaZQeLYzcS19Dsw3sgQUSj7cug -F+FxZc4dZjH3dgEZyH0DWLaVSR2mEiboxgx24ONmy+pdpibu5cxfvWenAScOospU -xbF6lR1xHkopigPcakXBpBlebzbNw6Kwt/5cOOJSvPhEQ+aQuwIDAQABo4ICUjCC -Ak4wPQYIKwYBBQUHAQEEMTAvMC0GCCsGAQUFBzABhiFodHRwczovL29jc3AucXVv -dmFkaXNvZmZzaG9yZS5jb20wDwYDVR0TAQH/BAUwAwEB/zCCARoGA1UdIASCAREw -ggENMIIBCQYJKwYBBAG+WAABMIH7MIHUBggrBgEFBQcCAjCBxxqBxFJlbGlhbmNl -IG9uIHRoZSBRdW9WYWRpcyBSb290IENlcnRpZmljYXRlIGJ5IGFueSBwYXJ0eSBh -c3N1bWVzIGFjY2VwdGFuY2Ugb2YgdGhlIHRoZW4gYXBwbGljYWJsZSBzdGFuZGFy -ZCB0ZXJtcyBhbmQgY29uZGl0aW9ucyBvZiB1c2UsIGNlcnRpZmljYXRpb24gcHJh -Y3RpY2VzLCBhbmQgdGhlIFF1b1ZhZGlzIENlcnRpZmljYXRlIFBvbGljeS4wIgYI -KwYBBQUHAgEWFmh0dHA6Ly93d3cucXVvdmFkaXMuYm0wHQYDVR0OBBYEFItLbe3T -KbkGGew5Oanwl4Rqy+/fMIGuBgNVHSMEgaYwgaOAFItLbe3TKbkGGew5Oanwl4Rq -y+/foYGEpIGBMH8xCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBMaW1p -dGVkMSUwIwYDVQQLExxSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MS4wLAYD -VQQDEyVRdW9WYWRpcyBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5ggQ6tlCL -MA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQUFAAOCAQEAitQUtf70mpKnGdSk -fnIYj9lofFIk3WdvOXrEql494liwTXCYhGHoG+NpGA7O+0dQoE7/8CQfvbLO9Sf8 -7C9TqnN7Az10buYWnuulLsS/VidQK2K6vkscPFVcQR0kvoIgR13VRH56FmjffU1R -cHhXHTMe/QKZnAzNCgVPx7uOpHX6Sm2xgI4JVrmcGmD+XcHXetwReNDWXcG31a0y -mQM6isxUJTkxgXsTIlG6Rmyhu576BGxJJnSP0nPrzDCi5upZIof4l/UO/erMkqQW -xFIY6iHOsfHmhIHluqmGKPJDWl0Snawe2ajlCmqnf6CHKc/yiU3U7MXi5nrQNiOK -SnQ2+Q== ------END CERTIFICATE----- - # Issuer: CN=QuoVadis Root CA 2 O=QuoVadis Limited # Subject: CN=QuoVadis Root CA 2 O=QuoVadis Limited # Label: "QuoVadis Root CA 2" @@ -516,33 +303,6 @@ JRDL8Try2frbSVa7pv6nQTXD4IhhyYjH3zYQIphZ6rBK+1YWc26sTfcioU+tHXot RSflMMFe8toTyyVCUZVHA4xsIcx0Qu1T/zOLjw9XARYvz6buyXAiFL39vmwLAw== -----END CERTIFICATE----- -# Issuer: CN=Sonera Class2 CA O=Sonera -# Subject: CN=Sonera Class2 CA O=Sonera -# Label: "Sonera Class 2 Root CA" -# Serial: 29 -# MD5 Fingerprint: a3:ec:75:0f:2e:88:df:fa:48:01:4e:0b:5c:48:6f:fb -# SHA1 Fingerprint: 37:f7:6d:e6:07:7c:90:c5:b1:3e:93:1a:b7:41:10:b4:f2:e4:9a:27 -# SHA256 Fingerprint: 79:08:b4:03:14:c1:38:10:0b:51:8d:07:35:80:7f:fb:fc:f8:51:8a:00:95:33:71:05:ba:38:6b:15:3d:d9:27 ------BEGIN CERTIFICATE----- -MIIDIDCCAgigAwIBAgIBHTANBgkqhkiG9w0BAQUFADA5MQswCQYDVQQGEwJGSTEP -MA0GA1UEChMGU29uZXJhMRkwFwYDVQQDExBTb25lcmEgQ2xhc3MyIENBMB4XDTAx -MDQwNjA3Mjk0MFoXDTIxMDQwNjA3Mjk0MFowOTELMAkGA1UEBhMCRkkxDzANBgNV -BAoTBlNvbmVyYTEZMBcGA1UEAxMQU29uZXJhIENsYXNzMiBDQTCCASIwDQYJKoZI -hvcNAQEBBQADggEPADCCAQoCggEBAJAXSjWdyvANlsdE+hY3/Ei9vX+ALTU74W+o -Z6m/AxxNjG8yR9VBaKQTBME1DJqEQ/xcHf+Js+gXGM2RX/uJ4+q/Tl18GybTdXnt -5oTjV+WtKcT0OijnpXuENmmz/V52vaMtmdOQTiMofRhj8VQ7Jp12W5dCsv+u8E7s -3TmVToMGf+dJQMjFAbJUWmYdPfz56TwKnoG4cPABi+QjVHzIrviQHgCWctRUz2Ej -vOr7nQKV0ba5cTppCD8PtOFCx4j1P5iop7oc4HFx71hXgVB6XGt0Rg6DA5jDjqhu -8nYybieDwnPz3BjotJPqdURrBGAgcVeHnfO+oJAjPYok4doh28MCAwEAAaMzMDEw -DwYDVR0TAQH/BAUwAwEB/zARBgNVHQ4ECgQISqCqWITTXjwwCwYDVR0PBAQDAgEG -MA0GCSqGSIb3DQEBBQUAA4IBAQBazof5FnIVV0sd2ZvnoiYw7JNn39Yt0jSv9zil -zqsWuasvfDXLrNAPtEwr/IDva4yRXzZ299uzGxnq9LIR/WFxRL8oszodv7ND6J+/ -3DEIcbCdjdY0RzKQxmUk96BKfARzjzlvF4xytb1LyHr4e4PDKE6cCepnP7JnBBvD -FNr450kkkdAdavphOe9r5yF1BgfYErQhIHBCcYHaPJo2vqZbDWpsmh+Re/n570K6 -Tk6ezAyNlNzZRZxe7EJQY670XcSxEtzKO6gunRRaBXW37Ndj4ro1tgQIkejanZz2 -ZrUYrAqmVCY0M9IbwdR/GjqOC6oybtv8TyWf2TLHllpwrN9M ------END CERTIFICATE----- - # Issuer: CN=XRamp Global Certification Authority O=XRamp Security Services Inc OU=www.xrampsecurity.com # Subject: CN=XRamp Global Certification Authority O=XRamp Security Services Inc OU=www.xrampsecurity.com # Label: "XRamp Global CA Root" @@ -640,46 +400,6 @@ VSJYACPq4xJDKVtHCN2MQWplBqjlIapBtJUhlbl90TSrE9atvNziPTnNvT51cKEY WQPJIrSPnNVeKtelttQKbfi3QBFGmh95DmK/D5fs4C8fF5Q= -----END CERTIFICATE----- -# Issuer: O=Government Root Certification Authority -# Subject: O=Government Root Certification Authority -# Label: "Taiwan GRCA" -# Serial: 42023070807708724159991140556527066870 -# MD5 Fingerprint: 37:85:44:53:32:45:1f:20:f0:f3:95:e1:25:c4:43:4e -# SHA1 Fingerprint: f4:8b:11:bf:de:ab:be:94:54:20:71:e6:41:de:6b:be:88:2b:40:b9 -# SHA256 Fingerprint: 76:00:29:5e:ef:e8:5b:9e:1f:d6:24:db:76:06:2a:aa:ae:59:81:8a:54:d2:77:4c:d4:c0:b2:c0:11:31:e1:b3 ------BEGIN CERTIFICATE----- -MIIFcjCCA1qgAwIBAgIQH51ZWtcvwgZEpYAIaeNe9jANBgkqhkiG9w0BAQUFADA/ -MQswCQYDVQQGEwJUVzEwMC4GA1UECgwnR292ZXJubWVudCBSb290IENlcnRpZmlj -YXRpb24gQXV0aG9yaXR5MB4XDTAyMTIwNTEzMjMzM1oXDTMyMTIwNTEzMjMzM1ow -PzELMAkGA1UEBhMCVFcxMDAuBgNVBAoMJ0dvdmVybm1lbnQgUm9vdCBDZXJ0aWZp -Y2F0aW9uIEF1dGhvcml0eTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIB -AJoluOzMonWoe/fOW1mKydGGEghU7Jzy50b2iPN86aXfTEc2pBsBHH8eV4qNw8XR -IePaJD9IK/ufLqGU5ywck9G/GwGHU5nOp/UKIXZ3/6m3xnOUT0b3EEk3+qhZSV1q -gQdW8or5BtD3cCJNtLdBuTK4sfCxw5w/cP1T3YGq2GN49thTbqGsaoQkclSGxtKy -yhwOeYHWtXBiCAEuTk8O1RGvqa/lmr/czIdtJuTJV6L7lvnM4T9TjGxMfptTCAts -F/tnyMKtsc2AtJfcdgEWFelq16TheEfOhtX7MfP6Mb40qij7cEwdScevLJ1tZqa2 -jWR+tSBqnTuBto9AAGdLiYa4zGX+FVPpBMHWXx1E1wovJ5pGfaENda1UhhXcSTvx -ls4Pm6Dso3pdvtUqdULle96ltqqvKKyskKw4t9VoNSZ63Pc78/1Fm9G7Q3hub/FC -VGqY8A2tl+lSXunVanLeavcbYBT0peS2cWeqH+riTcFCQP5nRhc4L0c/cZyu5SHK -YS1tB6iEfC3uUSXxY5Ce/eFXiGvviiNtsea9P63RPZYLhY3Naye7twWb7LuRqQoH -EgKXTiCQ8P8NHuJBO9NAOueNXdpm5AKwB1KYXA6OM5zCppX7VRluTI6uSw+9wThN -Xo+EHWbNxWCWtFJaBYmOlXqYwZE8lSOyDvR5tMl8wUohAgMBAAGjajBoMB0GA1Ud -DgQWBBTMzO/MKWCkO7GStjz6MmKPrCUVOzAMBgNVHRMEBTADAQH/MDkGBGcqBwAE -MTAvMC0CAQAwCQYFKw4DAhoFADAHBgVnKgMAAAQUA5vwIhP/lSg209yewDL7MTqK -UWUwDQYJKoZIhvcNAQEFBQADggIBAECASvomyc5eMN1PhnR2WPWus4MzeKR6dBcZ -TulStbngCnRiqmjKeKBMmo4sIy7VahIkv9Ro04rQ2JyftB8M3jh+Vzj8jeJPXgyf -qzvS/3WXy6TjZwj/5cAWtUgBfen5Cv8b5Wppv3ghqMKnI6mGq3ZW6A4M9hPdKmaK -ZEk9GhiHkASfQlK3T8v+R0F2Ne//AHY2RTKbxkaFXeIksB7jSJaYV0eUVXoPQbFE -JPPB/hprv4j9wabak2BegUqZIJxIZhm1AHlUD7gsL0u8qV1bYH+Mh6XgUmMqvtg7 -hUAV/h62ZT/FS9p+tXo1KaMuephgIqP0fSdOLeq0dDzpD6QzDxARvBMB1uUO07+1 -EqLhRSPAzAhuYbeJq4PjJB7mXQfnHyA+z2fI56wwbSdLaG5LKlwCCDTb+HbkZ6Mm -nD+iMsJKxYEYMRBWqoTvLQr/uB930r+lWKBi5NdLkXWNiYCYfm3LU05er/ayl4WX -udpVBrkk7tfGOB5jGxI7leFYrPLfhNVfmS8NVVvmONsuP3LpSIXLuykTjx44Vbnz -ssQwmSNOXfJIoRIM3BKQCZBUkQM8R+XVyWXgt0t97EfTsws+rZ7QdAAO671RrcDe -LMDDav7v3Aun+kbfYNucpllQdSNpc5Oy+fwC00fmcc4QAu4njIT/rEUNE1yDMuAl -pYYsfPQS ------END CERTIFICATE----- - # Issuer: CN=DigiCert Assured ID Root CA O=DigiCert Inc OU=www.digicert.com # Subject: CN=DigiCert Assured ID Root CA O=DigiCert Inc OU=www.digicert.com # Label: "DigiCert Assured ID Root CA" @@ -881,104 +601,6 @@ hAhm0sQ2fac+EPyI4NSA5QC9qvNOBqN6avlicuMJT+ubDgEj8Z+7fNzcbBGXJbLy tGMU0gYqZ4yD9c7qB9iaah7s5Aq7KkzrCWA5zspi2C5u -----END CERTIFICATE----- -# Issuer: CN=GeoTrust Primary Certification Authority O=GeoTrust Inc. -# Subject: CN=GeoTrust Primary Certification Authority O=GeoTrust Inc. -# Label: "GeoTrust Primary Certification Authority" -# Serial: 32798226551256963324313806436981982369 -# MD5 Fingerprint: 02:26:c3:01:5e:08:30:37:43:a9:d0:7d:cf:37:e6:bf -# SHA1 Fingerprint: 32:3c:11:8e:1b:f7:b8:b6:52:54:e2:e2:10:0d:d6:02:90:37:f0:96 -# SHA256 Fingerprint: 37:d5:10:06:c5:12:ea:ab:62:64:21:f1:ec:8c:92:01:3f:c5:f8:2a:e9:8e:e5:33:eb:46:19:b8:de:b4:d0:6c ------BEGIN CERTIFICATE----- -MIIDfDCCAmSgAwIBAgIQGKy1av1pthU6Y2yv2vrEoTANBgkqhkiG9w0BAQUFADBY -MQswCQYDVQQGEwJVUzEWMBQGA1UEChMNR2VvVHJ1c3QgSW5jLjExMC8GA1UEAxMo -R2VvVHJ1c3QgUHJpbWFyeSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0wNjEx -MjcwMDAwMDBaFw0zNjA3MTYyMzU5NTlaMFgxCzAJBgNVBAYTAlVTMRYwFAYDVQQK -Ew1HZW9UcnVzdCBJbmMuMTEwLwYDVQQDEyhHZW9UcnVzdCBQcmltYXJ5IENlcnRp -ZmljYXRpb24gQXV0aG9yaXR5MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC -AQEAvrgVe//UfH1nrYNke8hCUy3f9oQIIGHWAVlqnEQRr+92/ZV+zmEwu3qDXwK9 -AWbK7hWNb6EwnL2hhZ6UOvNWiAAxz9juapYC2e0DjPt1befquFUWBRaa9OBesYjA -ZIVcFU2Ix7e64HXprQU9nceJSOC7KMgD4TCTZF5SwFlwIjVXiIrxlQqD17wxcwE0 -7e9GceBrAqg1cmuXm2bgyxx5X9gaBGgeRwLmnWDiNpcB3841kt++Z8dtd1k7j53W -kBWUvEI0EME5+bEnPn7WinXFsq+W06Lem+SYvn3h6YGttm/81w7a4DSwDRp35+MI -mO9Y+pyEtzavwt+s0vQQBnBxNQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4G -A1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQULNVQQZcVi/CPNmFbSvtr2ZnJM5IwDQYJ -KoZIhvcNAQEFBQADggEBAFpwfyzdtzRP9YZRqSa+S7iq8XEN3GHHoOo0Hnp3DwQ1 -6CePbJC/kRYkRj5KTs4rFtULUh38H2eiAkUxT87z+gOneZ1TatnaYzr4gNfTmeGl -4b7UVXGYNTq+k+qurUKykG/g/CFNNWMziUnWm07Kx+dOCQD32sfvmWKZd7aVIl6K -oKv0uHiYyjgZmclynnjNS6yvGaBzEi38wkG6gZHaFloxt/m0cYASSJlyc1pZU8Fj -UjPtp8nSOQJw+uCxQmYpqptR7TBUIhRf2asdweSU8Pj1K/fqynhG1riR/aYNKxoU -AT6A8EKglQdebc3MS6RFjasS6LPeWuWgfOgPIh1a6Vk= ------END CERTIFICATE----- - -# Issuer: CN=thawte Primary Root CA O=thawte, Inc. OU=Certification Services Division/(c) 2006 thawte, Inc. - For authorized use only -# Subject: CN=thawte Primary Root CA O=thawte, Inc. OU=Certification Services Division/(c) 2006 thawte, Inc. - For authorized use only -# Label: "thawte Primary Root CA" -# Serial: 69529181992039203566298953787712940909 -# MD5 Fingerprint: 8c:ca:dc:0b:22:ce:f5:be:72:ac:41:1a:11:a8:d8:12 -# SHA1 Fingerprint: 91:c6:d6:ee:3e:8a:c8:63:84:e5:48:c2:99:29:5c:75:6c:81:7b:81 -# SHA256 Fingerprint: 8d:72:2f:81:a9:c1:13:c0:79:1d:f1:36:a2:96:6d:b2:6c:95:0a:97:1d:b4:6b:41:99:f4:ea:54:b7:8b:fb:9f ------BEGIN CERTIFICATE----- -MIIEIDCCAwigAwIBAgIQNE7VVyDV7exJ9C/ON9srbTANBgkqhkiG9w0BAQUFADCB -qTELMAkGA1UEBhMCVVMxFTATBgNVBAoTDHRoYXd0ZSwgSW5jLjEoMCYGA1UECxMf -Q2VydGlmaWNhdGlvbiBTZXJ2aWNlcyBEaXZpc2lvbjE4MDYGA1UECxMvKGMpIDIw -MDYgdGhhd3RlLCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxHzAdBgNV -BAMTFnRoYXd0ZSBQcmltYXJ5IFJvb3QgQ0EwHhcNMDYxMTE3MDAwMDAwWhcNMzYw -NzE2MjM1OTU5WjCBqTELMAkGA1UEBhMCVVMxFTATBgNVBAoTDHRoYXd0ZSwgSW5j -LjEoMCYGA1UECxMfQ2VydGlmaWNhdGlvbiBTZXJ2aWNlcyBEaXZpc2lvbjE4MDYG -A1UECxMvKGMpIDIwMDYgdGhhd3RlLCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNl -IG9ubHkxHzAdBgNVBAMTFnRoYXd0ZSBQcmltYXJ5IFJvb3QgQ0EwggEiMA0GCSqG -SIb3DQEBAQUAA4IBDwAwggEKAoIBAQCsoPD7gFnUnMekz52hWXMJEEUMDSxuaPFs -W0hoSVk3/AszGcJ3f8wQLZU0HObrTQmnHNK4yZc2AreJ1CRfBsDMRJSUjQJib+ta -3RGNKJpchJAQeg29dGYvajig4tVUROsdB58Hum/u6f1OCyn1PoSgAfGcq/gcfomk -6KHYcWUNo1F77rzSImANuVud37r8UVsLr5iy6S7pBOhih94ryNdOwUxkHt3Ph1i6 -Sk/KaAcdHJ1KxtUvkcx8cXIcxcBn6zL9yZJclNqFwJu/U30rCfSMnZEfl2pSy94J -NqR32HuHUETVPm4pafs5SSYeCaWAe0At6+gnhcn+Yf1+5nyXHdWdAgMBAAGjQjBA -MA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBR7W0XP -r87Lev0xkhpqtvNG61dIUDANBgkqhkiG9w0BAQUFAAOCAQEAeRHAS7ORtvzw6WfU -DW5FvlXok9LOAz/t2iWwHVfLHjp2oEzsUHboZHIMpKnxuIvW1oeEuzLlQRHAd9mz -YJ3rG9XRbkREqaYB7FViHXe4XI5ISXycO1cRrK1zN44veFyQaEfZYGDm/Ac9IiAX -xPcW6cTYcvnIc3zfFi8VqT79aie2oetaupgf1eNNZAqdE8hhuvU5HIe6uL17In/2 -/qxAeeWsEG89jxt5dovEN7MhGITlNgDrYyCZuen+MwS7QcjBAvlEYyCegc5C09Y/ -LHbTY5xZ3Y+m4Q6gLkH3LpVHz7z9M/P2C2F+fpErgUfCJzDupxBdN49cOSvkBPB7 -jVaMaA== ------END CERTIFICATE----- - -# Issuer: CN=VeriSign Class 3 Public Primary Certification Authority - G5 O=VeriSign, Inc. OU=VeriSign Trust Network/(c) 2006 VeriSign, Inc. - For authorized use only -# Subject: CN=VeriSign Class 3 Public Primary Certification Authority - G5 O=VeriSign, Inc. OU=VeriSign Trust Network/(c) 2006 VeriSign, Inc. - For authorized use only -# Label: "VeriSign Class 3 Public Primary Certification Authority - G5" -# Serial: 33037644167568058970164719475676101450 -# MD5 Fingerprint: cb:17:e4:31:67:3e:e2:09:fe:45:57:93:f3:0a:fa:1c -# SHA1 Fingerprint: 4e:b6:d5:78:49:9b:1c:cf:5f:58:1e:ad:56:be:3d:9b:67:44:a5:e5 -# SHA256 Fingerprint: 9a:cf:ab:7e:43:c8:d8:80:d0:6b:26:2a:94:de:ee:e4:b4:65:99:89:c3:d0:ca:f1:9b:af:64:05:e4:1a:b7:df ------BEGIN CERTIFICATE----- -MIIE0zCCA7ugAwIBAgIQGNrRniZ96LtKIVjNzGs7SjANBgkqhkiG9w0BAQUFADCB -yjELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQL -ExZWZXJpU2lnbiBUcnVzdCBOZXR3b3JrMTowOAYDVQQLEzEoYykgMjAwNiBWZXJp -U2lnbiwgSW5jLiAtIEZvciBhdXRob3JpemVkIHVzZSBvbmx5MUUwQwYDVQQDEzxW -ZXJpU2lnbiBDbGFzcyAzIFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0 -aG9yaXR5IC0gRzUwHhcNMDYxMTA4MDAwMDAwWhcNMzYwNzE2MjM1OTU5WjCByjEL -MAkGA1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQLExZW -ZXJpU2lnbiBUcnVzdCBOZXR3b3JrMTowOAYDVQQLEzEoYykgMjAwNiBWZXJpU2ln -biwgSW5jLiAtIEZvciBhdXRob3JpemVkIHVzZSBvbmx5MUUwQwYDVQQDEzxWZXJp -U2lnbiBDbGFzcyAzIFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0aG9y -aXR5IC0gRzUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCvJAgIKXo1 -nmAMqudLO07cfLw8RRy7K+D+KQL5VwijZIUVJ/XxrcgxiV0i6CqqpkKzj/i5Vbex -t0uz/o9+B1fs70PbZmIVYc9gDaTY3vjgw2IIPVQT60nKWVSFJuUrjxuf6/WhkcIz -SdhDY2pSS9KP6HBRTdGJaXvHcPaz3BJ023tdS1bTlr8Vd6Gw9KIl8q8ckmcY5fQG -BO+QueQA5N06tRn/Arr0PO7gi+s3i+z016zy9vA9r911kTMZHRxAy3QkGSGT2RT+ -rCpSx4/VBEnkjWNHiDxpg8v+R70rfk/Fla4OndTRQ8Bnc+MUCH7lP59zuDMKz10/ -NIeWiu5T6CUVAgMBAAGjgbIwga8wDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8E -BAMCAQYwbQYIKwYBBQUHAQwEYTBfoV2gWzBZMFcwVRYJaW1hZ2UvZ2lmMCEwHzAH -BgUrDgMCGgQUj+XTGoasjY5rw8+AatRIGCx7GS4wJRYjaHR0cDovL2xvZ28udmVy -aXNpZ24uY29tL3ZzbG9nby5naWYwHQYDVR0OBBYEFH/TZafC3ey78DAJ80M5+gKv -MzEzMA0GCSqGSIb3DQEBBQUAA4IBAQCTJEowX2LP2BqYLz3q3JktvXf2pXkiOOzE -p6B4Eq1iDkVwZMXnl2YtmAl+X6/WzChl8gGqCBpH3vn5fJJaCGkgDdk+bW48DW7Y -5gaRQBi5+MHt39tBquCWIMnNZBU4gcmU7qKEKQsTb47bDN0lAtukixlE0kF6BWlK -WE9gyn6CagsCqiUXObXbf+eEZSqVir2G3l6BFoMtEMze/aiCKm0oHw0LxOXnGiYZ -4fQRbxC1lfznQgUy286dUV4otp6F01vvpX1FQHKOtw5rDgb7MzVIcbidJ4vEZV8N -hnacRHr2lVz2XTIIM6RUthg/aFzyQkqFOFSDX9HoLPKsEdao7WNq ------END CERTIFICATE----- - # Issuer: CN=SecureTrust CA O=SecureTrust Corporation # Subject: CN=SecureTrust CA O=SecureTrust Corporation # Label: "SecureTrust CA" @@ -1127,38 +749,6 @@ fQjGGoe9GKhzvSbKYAydzpmfz1wPMOG+FDHqAjAU9JM8SaczepBGR7NjfRObTrdv GDeAU/7dIOA1mjbRxwG55tzd8/8dLDoWV9mSOdY= -----END CERTIFICATE----- -# Issuer: CN=OISTE WISeKey Global Root GA CA O=WISeKey OU=Copyright (c) 2005/OISTE Foundation Endorsed -# Subject: CN=OISTE WISeKey Global Root GA CA O=WISeKey OU=Copyright (c) 2005/OISTE Foundation Endorsed -# Label: "OISTE WISeKey Global Root GA CA" -# Serial: 86718877871133159090080555911823548314 -# MD5 Fingerprint: bc:6c:51:33:a7:e9:d3:66:63:54:15:72:1b:21:92:93 -# SHA1 Fingerprint: 59:22:a1:e1:5a:ea:16:35:21:f8:98:39:6a:46:46:b0:44:1b:0f:a9 -# SHA256 Fingerprint: 41:c9:23:86:6a:b4:ca:d6:b7:ad:57:80:81:58:2e:02:07:97:a6:cb:df:4f:ff:78:ce:83:96:b3:89:37:d7:f5 ------BEGIN CERTIFICATE----- -MIID8TCCAtmgAwIBAgIQQT1yx/RrH4FDffHSKFTfmjANBgkqhkiG9w0BAQUFADCB -ijELMAkGA1UEBhMCQ0gxEDAOBgNVBAoTB1dJU2VLZXkxGzAZBgNVBAsTEkNvcHly -aWdodCAoYykgMjAwNTEiMCAGA1UECxMZT0lTVEUgRm91bmRhdGlvbiBFbmRvcnNl -ZDEoMCYGA1UEAxMfT0lTVEUgV0lTZUtleSBHbG9iYWwgUm9vdCBHQSBDQTAeFw0w -NTEyMTExNjAzNDRaFw0zNzEyMTExNjA5NTFaMIGKMQswCQYDVQQGEwJDSDEQMA4G -A1UEChMHV0lTZUtleTEbMBkGA1UECxMSQ29weXJpZ2h0IChjKSAyMDA1MSIwIAYD -VQQLExlPSVNURSBGb3VuZGF0aW9uIEVuZG9yc2VkMSgwJgYDVQQDEx9PSVNURSBX -SVNlS2V5IEdsb2JhbCBSb290IEdBIENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A -MIIBCgKCAQEAy0+zAJs9Nt350UlqaxBJH+zYK7LG+DKBKUOVTJoZIyEVRd7jyBxR -VVuuk+g3/ytr6dTqvirdqFEr12bDYVxgAsj1znJ7O7jyTmUIms2kahnBAbtzptf2 -w93NvKSLtZlhuAGio9RN1AU9ka34tAhxZK9w8RxrfvbDd50kc3vkDIzh2TbhmYsF -mQvtRTEJysIA2/dyoJaqlYfQjse2YXMNdmaM3Bu0Y6Kff5MTMPGhJ9vZ/yxViJGg -4E8HsChWjBgbl0SOid3gF27nKu+POQoxhILYQBRJLnpB5Kf+42TMwVlxSywhp1t9 -4B3RLoGbw9ho972WG6xwsRYUC9tguSYBBQIDAQABo1EwTzALBgNVHQ8EBAMCAYYw -DwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUswN+rja8sHnR3JQmthG+IbJphpQw -EAYJKwYBBAGCNxUBBAMCAQAwDQYJKoZIhvcNAQEFBQADggEBAEuh/wuHbrP5wUOx -SPMowB0uyQlB+pQAHKSkq0lPjz0e701vvbyk9vImMMkQyh2I+3QZH4VFvbBsUfk2 -ftv1TDI6QU9bR8/oCy22xBmddMVHxjtqD6wU2zz0c5ypBd8A3HR4+vg1YFkCExh8 -vPtNsCBtQ7tgMHpnM1zFmdH4LTlSc/uMqpclXHLZCB6rTjzjgTGfA6b7wP4piFXa -hNVQA7bihKOmNqoROgHhGEvWRGizPflTdISzRpFGlgC3gCy24eMQ4tui5yiPAZZi -Fj4A4xylNoEYokxSdsARo27mHbrjWr42U8U+dY+GaSlYU7Wcu2+fXMUY7N0v4ZjJ -/L7fCg0= ------END CERTIFICATE----- - # Issuer: CN=Certigna O=Dhimyotis # Subject: CN=Certigna O=Dhimyotis # Label: "Certigna" @@ -1288,185 +878,6 @@ i/nDhDwTqn6Sm1dTk/pwwpEOMfmbZ13pljheX7NzTogVZ96edhBiIL5VaZVDADlN 9u6wWk5JRFRYX0KD -----END CERTIFICATE----- -# Issuer: CN=GeoTrust Primary Certification Authority - G3 O=GeoTrust Inc. OU=(c) 2008 GeoTrust Inc. - For authorized use only -# Subject: CN=GeoTrust Primary Certification Authority - G3 O=GeoTrust Inc. OU=(c) 2008 GeoTrust Inc. - For authorized use only -# Label: "GeoTrust Primary Certification Authority - G3" -# Serial: 28809105769928564313984085209975885599 -# MD5 Fingerprint: b5:e8:34:36:c9:10:44:58:48:70:6d:2e:83:d4:b8:05 -# SHA1 Fingerprint: 03:9e:ed:b8:0b:e7:a0:3c:69:53:89:3b:20:d2:d9:32:3a:4c:2a:fd -# SHA256 Fingerprint: b4:78:b8:12:25:0d:f8:78:63:5c:2a:a7:ec:7d:15:5e:aa:62:5e:e8:29:16:e2:cd:29:43:61:88:6c:d1:fb:d4 ------BEGIN CERTIFICATE----- -MIID/jCCAuagAwIBAgIQFaxulBmyeUtB9iepwxgPHzANBgkqhkiG9w0BAQsFADCB -mDELMAkGA1UEBhMCVVMxFjAUBgNVBAoTDUdlb1RydXN0IEluYy4xOTA3BgNVBAsT -MChjKSAyMDA4IEdlb1RydXN0IEluYy4gLSBGb3IgYXV0aG9yaXplZCB1c2Ugb25s -eTE2MDQGA1UEAxMtR2VvVHJ1c3QgUHJpbWFyeSBDZXJ0aWZpY2F0aW9uIEF1dGhv -cml0eSAtIEczMB4XDTA4MDQwMjAwMDAwMFoXDTM3MTIwMTIzNTk1OVowgZgxCzAJ -BgNVBAYTAlVTMRYwFAYDVQQKEw1HZW9UcnVzdCBJbmMuMTkwNwYDVQQLEzAoYykg -MjAwOCBHZW9UcnVzdCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxNjA0 -BgNVBAMTLUdlb1RydXN0IFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkg -LSBHMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANziXmJYHTNXOTIz -+uvLh4yn1ErdBojqZI4xmKU4kB6Yzy5jK/BGvESyiaHAKAxJcCGVn2TAppMSAmUm -hsalifD614SgcK9PGpc/BkTVyetyEH3kMSj7HGHmKAdEc5IiaacDiGydY8hS2pgn -5whMcD60yRLBxWeDXTPzAxHsatBT4tG6NmCUgLthY2xbF37fQJQeqw3CIShwiP/W -JmxsYAQlTlV+fe+/lEjetx3dcI0FX4ilm/LC7urRQEFtYjgdVgbFA0dRIBn8exAL -DmKudlW/X3e+PkkBUz2YJQN2JFodtNuJ6nnltrM7P7pMKEF/BqxqjsHQ9gUdfeZC -huOl1UcCAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYw -HQYDVR0OBBYEFMR5yo6hTgMdHNxr2zFblD4/MH8tMA0GCSqGSIb3DQEBCwUAA4IB -AQAtxRPPVoB7eni9n64smefv2t+UXglpp+duaIy9cr5HqQ6XErhK8WTTOd8lNNTB -zU6B8A8ExCSzNJbGpqow32hhc9f5joWJ7w5elShKKiePEI4ufIbEAp7aDHdlDkQN -kv39sxY2+hENHYwOB4lqKVb3cvTdFZx3NWZXqxNT2I7BQMXXExZacse3aQHEerGD -AWh9jUGhlBjBJVz88P6DAod8DQ3PLghcSkANPuyBYeYk28rgDi0Hsj5W3I31QYUH -SJsMC8tJP33st/3LjWeJGqvtux6jAAgIFyqCXDFdRootD4abdNlF+9RAsXqqaC2G -spki4cErx5z481+oghLrGREt ------END CERTIFICATE----- - -# Issuer: CN=thawte Primary Root CA - G2 O=thawte, Inc. OU=(c) 2007 thawte, Inc. - For authorized use only -# Subject: CN=thawte Primary Root CA - G2 O=thawte, Inc. OU=(c) 2007 thawte, Inc. - For authorized use only -# Label: "thawte Primary Root CA - G2" -# Serial: 71758320672825410020661621085256472406 -# MD5 Fingerprint: 74:9d:ea:60:24:c4:fd:22:53:3e:cc:3a:72:d9:29:4f -# SHA1 Fingerprint: aa:db:bc:22:23:8f:c4:01:a1:27:bb:38:dd:f4:1d:db:08:9e:f0:12 -# SHA256 Fingerprint: a4:31:0d:50:af:18:a6:44:71:90:37:2a:86:af:af:8b:95:1f:fb:43:1d:83:7f:1e:56:88:b4:59:71:ed:15:57 ------BEGIN CERTIFICATE----- -MIICiDCCAg2gAwIBAgIQNfwmXNmET8k9Jj1Xm67XVjAKBggqhkjOPQQDAzCBhDEL -MAkGA1UEBhMCVVMxFTATBgNVBAoTDHRoYXd0ZSwgSW5jLjE4MDYGA1UECxMvKGMp -IDIwMDcgdGhhd3RlLCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxJDAi -BgNVBAMTG3RoYXd0ZSBQcmltYXJ5IFJvb3QgQ0EgLSBHMjAeFw0wNzExMDUwMDAw -MDBaFw0zODAxMTgyMzU5NTlaMIGEMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMdGhh -d3RlLCBJbmMuMTgwNgYDVQQLEy8oYykgMjAwNyB0aGF3dGUsIEluYy4gLSBGb3Ig -YXV0aG9yaXplZCB1c2Ugb25seTEkMCIGA1UEAxMbdGhhd3RlIFByaW1hcnkgUm9v -dCBDQSAtIEcyMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEotWcgnuVnfFSeIf+iha/ -BebfowJPDQfGAFG6DAJSLSKkQjnE/o/qycG+1E3/n3qe4rF8mq2nhglzh9HnmuN6 -papu+7qzcMBniKI11KOasf2twu8x+qi58/sIxpHR+ymVo0IwQDAPBgNVHRMBAf8E -BTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUmtgAMADna3+FGO6Lts6K -DPgR4bswCgYIKoZIzj0EAwMDaQAwZgIxAN344FdHW6fmCsO99YCKlzUNG4k8VIZ3 -KMqh9HneteY4sPBlcIx/AlTCv//YoT7ZzwIxAMSNlPzcU9LcnXgWHxUzI1NS41ox -XZ3Krr0TKUQNJ1uo52icEvdYPy5yAlejj6EULg== ------END CERTIFICATE----- - -# Issuer: CN=thawte Primary Root CA - G3 O=thawte, Inc. OU=Certification Services Division/(c) 2008 thawte, Inc. - For authorized use only -# Subject: CN=thawte Primary Root CA - G3 O=thawte, Inc. OU=Certification Services Division/(c) 2008 thawte, Inc. - For authorized use only -# Label: "thawte Primary Root CA - G3" -# Serial: 127614157056681299805556476275995414779 -# MD5 Fingerprint: fb:1b:5d:43:8a:94:cd:44:c6:76:f2:43:4b:47:e7:31 -# SHA1 Fingerprint: f1:8b:53:8d:1b:e9:03:b6:a6:f0:56:43:5b:17:15:89:ca:f3:6b:f2 -# SHA256 Fingerprint: 4b:03:f4:58:07:ad:70:f2:1b:fc:2c:ae:71:c9:fd:e4:60:4c:06:4c:f5:ff:b6:86:ba:e5:db:aa:d7:fd:d3:4c ------BEGIN CERTIFICATE----- -MIIEKjCCAxKgAwIBAgIQYAGXt0an6rS0mtZLL/eQ+zANBgkqhkiG9w0BAQsFADCB -rjELMAkGA1UEBhMCVVMxFTATBgNVBAoTDHRoYXd0ZSwgSW5jLjEoMCYGA1UECxMf -Q2VydGlmaWNhdGlvbiBTZXJ2aWNlcyBEaXZpc2lvbjE4MDYGA1UECxMvKGMpIDIw -MDggdGhhd3RlLCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxJDAiBgNV -BAMTG3RoYXd0ZSBQcmltYXJ5IFJvb3QgQ0EgLSBHMzAeFw0wODA0MDIwMDAwMDBa -Fw0zNzEyMDEyMzU5NTlaMIGuMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMdGhhd3Rl -LCBJbmMuMSgwJgYDVQQLEx9DZXJ0aWZpY2F0aW9uIFNlcnZpY2VzIERpdmlzaW9u -MTgwNgYDVQQLEy8oYykgMjAwOCB0aGF3dGUsIEluYy4gLSBGb3IgYXV0aG9yaXpl -ZCB1c2Ugb25seTEkMCIGA1UEAxMbdGhhd3RlIFByaW1hcnkgUm9vdCBDQSAtIEcz -MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsr8nLPvb2FvdeHsbnndm -gcs+vHyu86YnmjSjaDFxODNi5PNxZnmxqWWjpYvVj2AtP0LMqmsywCPLLEHd5N/8 -YZzic7IilRFDGF/Eth9XbAoFWCLINkw6fKXRz4aviKdEAhN0cXMKQlkC+BsUa0Lf -b1+6a4KinVvnSr0eAXLbS3ToO39/fR8EtCab4LRarEc9VbjXsCZSKAExQGbY2SS9 -9irY7CFJXJv2eul/VTV+lmuNk5Mny5K76qxAwJ/C+IDPXfRa3M50hqY+bAtTyr2S -zhkGcuYMXDhpxwTWvGzOW/b3aJzcJRVIiKHpqfiYnODz1TEoYRFsZ5aNOZnLwkUk -OQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNV -HQ4EFgQUrWyqlGCc7eT/+j4KdCtjA/e2Wb8wDQYJKoZIhvcNAQELBQADggEBABpA -2JVlrAmSicY59BDlqQ5mU1143vokkbvnRFHfxhY0Cu9qRFHqKweKA3rD6z8KLFIW -oCtDuSWQP3CpMyVtRRooOyfPqsMpQhvfO0zAMzRbQYi/aytlryjvsvXDqmbOe1bu -t8jLZ8HJnBoYuMTDSQPxYA5QzUbF83d597YV4Djbxy8ooAw/dyZ02SUS2jHaGh7c -KUGRIjxpp7sC8rZcJwOJ9Abqm+RyguOhCcHpABnTPtRwa7pxpqpYrvS76Wy274fM -m7v/OeZWYdMKp8RcTGB7BXcmer/YB1IsYvdwY9k5vG8cwnncdimvzsUsZAReiDZu -MdRAGmI0Nj81Aa6sY6A= ------END CERTIFICATE----- - -# Issuer: CN=GeoTrust Primary Certification Authority - G2 O=GeoTrust Inc. OU=(c) 2007 GeoTrust Inc. - For authorized use only -# Subject: CN=GeoTrust Primary Certification Authority - G2 O=GeoTrust Inc. OU=(c) 2007 GeoTrust Inc. - For authorized use only -# Label: "GeoTrust Primary Certification Authority - G2" -# Serial: 80682863203381065782177908751794619243 -# MD5 Fingerprint: 01:5e:d8:6b:bd:6f:3d:8e:a1:31:f8:12:e0:98:73:6a -# SHA1 Fingerprint: 8d:17:84:d5:37:f3:03:7d:ec:70:fe:57:8b:51:9a:99:e6:10:d7:b0 -# SHA256 Fingerprint: 5e:db:7a:c4:3b:82:a0:6a:87:61:e8:d7:be:49:79:eb:f2:61:1f:7d:d7:9b:f9:1c:1c:6b:56:6a:21:9e:d7:66 ------BEGIN CERTIFICATE----- -MIICrjCCAjWgAwIBAgIQPLL0SAoA4v7rJDteYD7DazAKBggqhkjOPQQDAzCBmDEL -MAkGA1UEBhMCVVMxFjAUBgNVBAoTDUdlb1RydXN0IEluYy4xOTA3BgNVBAsTMChj -KSAyMDA3IEdlb1RydXN0IEluYy4gLSBGb3IgYXV0aG9yaXplZCB1c2Ugb25seTE2 -MDQGA1UEAxMtR2VvVHJ1c3QgUHJpbWFyeSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0 -eSAtIEcyMB4XDTA3MTEwNTAwMDAwMFoXDTM4MDExODIzNTk1OVowgZgxCzAJBgNV -BAYTAlVTMRYwFAYDVQQKEw1HZW9UcnVzdCBJbmMuMTkwNwYDVQQLEzAoYykgMjAw -NyBHZW9UcnVzdCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxNjA0BgNV -BAMTLUdlb1RydXN0IFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgLSBH -MjB2MBAGByqGSM49AgEGBSuBBAAiA2IABBWx6P0DFUPlrOuHNxFi79KDNlJ9RVcL -So17VDs6bl8VAsBQps8lL33KSLjHUGMcKiEIfJo22Av+0SbFWDEwKCXzXV2juLal -tJLtbCyf691DiaI8S0iRHVDsJt/WYC69IaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAO -BgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFBVfNVdRVfslsq0DafwBo/q+EVXVMAoG -CCqGSM49BAMDA2cAMGQCMGSWWaboCd6LuvpaiIjwH5HTRqjySkwCY/tsXzjbLkGT -qQ7mndwxHLKgpxgceeHHNgIwOlavmnRs9vuD4DPTCF+hnMJbn0bWtsuRBmOiBucz -rD6ogRLQy7rQkgu2npaqBA+K ------END CERTIFICATE----- - -# Issuer: CN=VeriSign Universal Root Certification Authority O=VeriSign, Inc. OU=VeriSign Trust Network/(c) 2008 VeriSign, Inc. - For authorized use only -# Subject: CN=VeriSign Universal Root Certification Authority O=VeriSign, Inc. OU=VeriSign Trust Network/(c) 2008 VeriSign, Inc. - For authorized use only -# Label: "VeriSign Universal Root Certification Authority" -# Serial: 85209574734084581917763752644031726877 -# MD5 Fingerprint: 8e:ad:b5:01:aa:4d:81:e4:8c:1d:d1:e1:14:00:95:19 -# SHA1 Fingerprint: 36:79:ca:35:66:87:72:30:4d:30:a5:fb:87:3b:0f:a7:7b:b7:0d:54 -# SHA256 Fingerprint: 23:99:56:11:27:a5:71:25:de:8c:ef:ea:61:0d:df:2f:a0:78:b5:c8:06:7f:4e:82:82:90:bf:b8:60:e8:4b:3c ------BEGIN CERTIFICATE----- -MIIEuTCCA6GgAwIBAgIQQBrEZCGzEyEDDrvkEhrFHTANBgkqhkiG9w0BAQsFADCB -vTELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQL -ExZWZXJpU2lnbiBUcnVzdCBOZXR3b3JrMTowOAYDVQQLEzEoYykgMjAwOCBWZXJp -U2lnbiwgSW5jLiAtIEZvciBhdXRob3JpemVkIHVzZSBvbmx5MTgwNgYDVQQDEy9W -ZXJpU2lnbiBVbml2ZXJzYWwgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAe -Fw0wODA0MDIwMDAwMDBaFw0zNzEyMDEyMzU5NTlaMIG9MQswCQYDVQQGEwJVUzEX -MBUGA1UEChMOVmVyaVNpZ24sIEluYy4xHzAdBgNVBAsTFlZlcmlTaWduIFRydXN0 -IE5ldHdvcmsxOjA4BgNVBAsTMShjKSAyMDA4IFZlcmlTaWduLCBJbmMuIC0gRm9y -IGF1dGhvcml6ZWQgdXNlIG9ubHkxODA2BgNVBAMTL1ZlcmlTaWduIFVuaXZlcnNh -bCBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIIBIjANBgkqhkiG9w0BAQEF -AAOCAQ8AMIIBCgKCAQEAx2E3XrEBNNti1xWb/1hajCMj1mCOkdeQmIN65lgZOIzF -9uVkhbSicfvtvbnazU0AtMgtc6XHaXGVHzk8skQHnOgO+k1KxCHfKWGPMiJhgsWH -H26MfF8WIFFE0XBPV+rjHOPMee5Y2A7Cs0WTwCznmhcrewA3ekEzeOEz4vMQGn+H -LL729fdC4uW/h2KJXwBL38Xd5HVEMkE6HnFuacsLdUYI0crSK5XQz/u5QGtkjFdN -/BMReYTtXlT2NJ8IAfMQJQYXStrxHXpma5hgZqTZ79IugvHw7wnqRMkVauIDbjPT -rJ9VAMf2CGqUuV/c4DPxhGD5WycRtPwW8rtWaoAljQIDAQABo4GyMIGvMA8GA1Ud -EwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMG0GCCsGAQUFBwEMBGEwX6FdoFsw -WTBXMFUWCWltYWdlL2dpZjAhMB8wBwYFKw4DAhoEFI/l0xqGrI2Oa8PPgGrUSBgs -exkuMCUWI2h0dHA6Ly9sb2dvLnZlcmlzaWduLmNvbS92c2xvZ28uZ2lmMB0GA1Ud -DgQWBBS2d/ppSEefUxLVwuoHMnYH0ZcHGTANBgkqhkiG9w0BAQsFAAOCAQEASvj4 -sAPmLGd75JR3Y8xuTPl9Dg3cyLk1uXBPY/ok+myDjEedO2Pzmvl2MpWRsXe8rJq+ -seQxIcaBlVZaDrHC1LGmWazxY8u4TB1ZkErvkBYoH1quEPuBUDgMbMzxPcP1Y+Oz -4yHJJDnp/RVmRvQbEdBNc6N9Rvk97ahfYtTxP/jgdFcrGJ2BtMQo2pSXpXDrrB2+ -BxHw1dvd5Yzw1TKwg+ZX4o+/vqGqvz0dtdQ46tewXDpPaj+PwGZsY6rp2aQW9IHR -lRQOfc2VNNnSj3BzgXucfr2YYdhFh5iQxeuGMMY1v/D/w1WIg0vvBZIGcfK4mJO3 -7M2CYfE45k+XmCpajQ== ------END CERTIFICATE----- - -# Issuer: CN=VeriSign Class 3 Public Primary Certification Authority - G4 O=VeriSign, Inc. OU=VeriSign Trust Network/(c) 2007 VeriSign, Inc. - For authorized use only -# Subject: CN=VeriSign Class 3 Public Primary Certification Authority - G4 O=VeriSign, Inc. OU=VeriSign Trust Network/(c) 2007 VeriSign, Inc. - For authorized use only -# Label: "VeriSign Class 3 Public Primary Certification Authority - G4" -# Serial: 63143484348153506665311985501458640051 -# MD5 Fingerprint: 3a:52:e1:e7:fd:6f:3a:e3:6f:f3:6f:99:1b:f9:22:41 -# SHA1 Fingerprint: 22:d5:d8:df:8f:02:31:d1:8d:f7:9d:b7:cf:8a:2d:64:c9:3f:6c:3a -# SHA256 Fingerprint: 69:dd:d7:ea:90:bb:57:c9:3e:13:5d:c8:5e:a6:fc:d5:48:0b:60:32:39:bd:c4:54:fc:75:8b:2a:26:cf:7f:79 ------BEGIN CERTIFICATE----- -MIIDhDCCAwqgAwIBAgIQL4D+I4wOIg9IZxIokYesszAKBggqhkjOPQQDAzCByjEL -MAkGA1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQLExZW -ZXJpU2lnbiBUcnVzdCBOZXR3b3JrMTowOAYDVQQLEzEoYykgMjAwNyBWZXJpU2ln -biwgSW5jLiAtIEZvciBhdXRob3JpemVkIHVzZSBvbmx5MUUwQwYDVQQDEzxWZXJp -U2lnbiBDbGFzcyAzIFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0aG9y -aXR5IC0gRzQwHhcNMDcxMTA1MDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCByjELMAkG -A1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQLExZWZXJp -U2lnbiBUcnVzdCBOZXR3b3JrMTowOAYDVQQLEzEoYykgMjAwNyBWZXJpU2lnbiwg -SW5jLiAtIEZvciBhdXRob3JpemVkIHVzZSBvbmx5MUUwQwYDVQQDEzxWZXJpU2ln -biBDbGFzcyAzIFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0aG9yaXR5 -IC0gRzQwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAASnVnp8Utpkmw4tXNherJI9/gHm -GUo9FANL+mAnINmDiWn6VMaaGF5VKmTeBvaNSjutEDxlPZCIBIngMGGzrl0Bp3ve -fLK+ymVhAIau2o970ImtTR1ZmkGxvEeA3J5iw/mjgbIwga8wDwYDVR0TAQH/BAUw -AwEB/zAOBgNVHQ8BAf8EBAMCAQYwbQYIKwYBBQUHAQwEYTBfoV2gWzBZMFcwVRYJ -aW1hZ2UvZ2lmMCEwHzAHBgUrDgMCGgQUj+XTGoasjY5rw8+AatRIGCx7GS4wJRYj -aHR0cDovL2xvZ28udmVyaXNpZ24uY29tL3ZzbG9nby5naWYwHQYDVR0OBBYEFLMW -kf3upm7ktS5Jj4d4gYDs5bG1MAoGCCqGSM49BAMDA2gAMGUCMGYhDBgmYFo4e1ZC -4Kf8NoRRkSAsdk1DPcQdhCPQrNZ8NQbOzWm9kA3bbEhCHQ6qQgIxAJw9SDkjOVga -FRJZap7v1VmyHVIsmXHNxynfGyphe3HR3vPA5Q06Sqotp9iGKt0uEA== ------END CERTIFICATE----- - # Issuer: CN=NetLock Arany (Class Gold) F\u0151tan\xfas\xedtv\xe1ny O=NetLock Kft. OU=Tan\xfas\xedtv\xe1nykiad\xf3k (Certification Services) # Subject: CN=NetLock Arany (Class Gold) F\u0151tan\xfas\xedtv\xe1ny O=NetLock Kft. OU=Tan\xfas\xedtv\xe1nykiad\xf3k (Certification Services) # Label: "NetLock Arany (Class Gold) F\u0151tan\xfas\xedtv\xe1ny" @@ -1499,47 +910,6 @@ uLjbvrW5KfnaNwUASZQDhETnv0Mxz3WLJdH0pmT1kvarBes96aULNmLazAZfNou2 XjG4Kvte9nHfRCaexOYNkbQudZWAUWpLMKawYqGT8ZvYzsRjdT9ZR7E= -----END CERTIFICATE----- -# Issuer: CN=Staat der Nederlanden Root CA - G2 O=Staat der Nederlanden -# Subject: CN=Staat der Nederlanden Root CA - G2 O=Staat der Nederlanden -# Label: "Staat der Nederlanden Root CA - G2" -# Serial: 10000012 -# MD5 Fingerprint: 7c:a5:0f:f8:5b:9a:7d:6d:30:ae:54:5a:e3:42:a2:8a -# SHA1 Fingerprint: 59:af:82:79:91:86:c7:b4:75:07:cb:cf:03:57:46:eb:04:dd:b7:16 -# SHA256 Fingerprint: 66:8c:83:94:7d:a6:3b:72:4b:ec:e1:74:3c:31:a0:e6:ae:d0:db:8e:c5:b3:1b:e3:77:bb:78:4f:91:b6:71:6f ------BEGIN CERTIFICATE----- -MIIFyjCCA7KgAwIBAgIEAJiWjDANBgkqhkiG9w0BAQsFADBaMQswCQYDVQQGEwJO -TDEeMBwGA1UECgwVU3RhYXQgZGVyIE5lZGVybGFuZGVuMSswKQYDVQQDDCJTdGFh -dCBkZXIgTmVkZXJsYW5kZW4gUm9vdCBDQSAtIEcyMB4XDTA4MDMyNjExMTgxN1oX -DTIwMDMyNTExMDMxMFowWjELMAkGA1UEBhMCTkwxHjAcBgNVBAoMFVN0YWF0IGRl -ciBOZWRlcmxhbmRlbjErMCkGA1UEAwwiU3RhYXQgZGVyIE5lZGVybGFuZGVuIFJv -b3QgQ0EgLSBHMjCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMVZ5291 -qj5LnLW4rJ4L5PnZyqtdj7U5EILXr1HgO+EASGrP2uEGQxGZqhQlEq0i6ABtQ8Sp -uOUfiUtnvWFI7/3S4GCI5bkYYCjDdyutsDeqN95kWSpGV+RLufg3fNU254DBtvPU -Z5uW6M7XxgpT0GtJlvOjCwV3SPcl5XCsMBQgJeN/dVrlSPhOewMHBPqCYYdu8DvE -pMfQ9XQ+pV0aCPKbJdL2rAQmPlU6Yiile7Iwr/g3wtG61jj99O9JMDeZJiFIhQGp -5Rbn3JBV3w/oOM2ZNyFPXfUib2rFEhZgF1XyZWampzCROME4HYYEhLoaJXhena/M -UGDWE4dS7WMfbWV9whUYdMrhfmQpjHLYFhN9C0lK8SgbIHRrxT3dsKpICT0ugpTN -GmXZK4iambwYfp/ufWZ8Pr2UuIHOzZgweMFvZ9C+X+Bo7d7iscksWXiSqt8rYGPy -5V6548r6f1CGPqI0GAwJaCgRHOThuVw+R7oyPxjMW4T182t0xHJ04eOLoEq9jWYv -6q012iDTiIJh8BIitrzQ1aTsr1SIJSQ8p22xcik/Plemf1WvbibG/ufMQFxRRIEK -eN5KzlW/HdXZt1bv8Hb/C3m1r737qWmRRpdogBQ2HbN/uymYNqUg+oJgYjOk7Na6 -B6duxc8UpufWkjTYgfX8HV2qXB72o007uPc5AgMBAAGjgZcwgZQwDwYDVR0TAQH/ -BAUwAwEB/zBSBgNVHSAESzBJMEcGBFUdIAAwPzA9BggrBgEFBQcCARYxaHR0cDov -L3d3dy5wa2lvdmVyaGVpZC5ubC9wb2xpY2llcy9yb290LXBvbGljeS1HMjAOBgNV -HQ8BAf8EBAMCAQYwHQYDVR0OBBYEFJFoMocVHYnitfGsNig0jQt8YojrMA0GCSqG -SIb3DQEBCwUAA4ICAQCoQUpnKpKBglBu4dfYszk78wIVCVBR7y29JHuIhjv5tLyS -CZa59sCrI2AGeYwRTlHSeYAz+51IvuxBQ4EffkdAHOV6CMqqi3WtFMTC6GY8ggen -5ieCWxjmD27ZUD6KQhgpxrRW/FYQoAUXvQwjf/ST7ZwaUb7dRUG/kSS0H4zpX897 -IZmflZ85OkYcbPnNe5yQzSipx6lVu6xiNGI1E0sUOlWDuYaNkqbG9AclVMwWVxJK -gnjIFNkXgiYtXSAfea7+1HAWFpWD2DU5/1JddRwWxRNVz0fMdWVSSt7wsKfkCpYL -+63C4iWEst3kvX5ZbJvw8NjnyvLplzh+ib7M+zkXYT9y2zqR2GUBGR2tUKRXCnxL -vJxxcypFURmFzI79R6d0lR2o0a9OF7FpJsKqeFdbxU2n5Z4FF5TKsl+gSRiNNOkm -bEgeqmiSBeGCc1qb3AdbCG19ndeNIdn8FCCqwkXfP+cAslHkwvgFuXkajDTznlvk -N1trSt8sV4pAWja63XVECDdCcAz+3F4hoKOKwJCcaNpQ5kUQR3i2TtJlycM33+FC -Y7BXN0Ute4qcvwXqZVUz9zkQxSgqIXobisQk+T8VyJoVIPVVYpbtbZNQvOSqeK3Z -ywplh6ZmwcSBo3c6WB4L7oOLnR7SUqTMHW+wmG2UMbX4cQrcufx9MmDm66+KAQ== ------END CERTIFICATE----- - # Issuer: CN=Hongkong Post Root CA 1 O=Hongkong Post # Subject: CN=Hongkong Post Root CA 1 O=Hongkong Post # Label: "Hongkong Post Root CA 1" @@ -1743,105 +1113,6 @@ naM8THLCV8Sg1Mw4J87VBp6iSNnpn86CcDaTmjvfliHjWbcM2pE38P1ZWrOZyGls QyYBNWNgVYkDOnXYukrZVP/u3oDYLdE41V4tC5h9Pmzb/CaIxw== -----END CERTIFICATE----- -# Issuer: CN=Chambers of Commerce Root - 2008 O=AC Camerfirma S.A. -# Subject: CN=Chambers of Commerce Root - 2008 O=AC Camerfirma S.A. -# Label: "Chambers of Commerce Root - 2008" -# Serial: 11806822484801597146 -# MD5 Fingerprint: 5e:80:9e:84:5a:0e:65:0b:17:02:f3:55:18:2a:3e:d7 -# SHA1 Fingerprint: 78:6a:74:ac:76:ab:14:7f:9c:6a:30:50:ba:9e:a8:7e:fe:9a:ce:3c -# SHA256 Fingerprint: 06:3e:4a:fa:c4:91:df:d3:32:f3:08:9b:85:42:e9:46:17:d8:93:d7:fe:94:4e:10:a7:93:7e:e2:9d:96:93:c0 ------BEGIN CERTIFICATE----- -MIIHTzCCBTegAwIBAgIJAKPaQn6ksa7aMA0GCSqGSIb3DQEBBQUAMIGuMQswCQYD -VQQGEwJFVTFDMEEGA1UEBxM6TWFkcmlkIChzZWUgY3VycmVudCBhZGRyZXNzIGF0 -IHd3dy5jYW1lcmZpcm1hLmNvbS9hZGRyZXNzKTESMBAGA1UEBRMJQTgyNzQzMjg3 -MRswGQYDVQQKExJBQyBDYW1lcmZpcm1hIFMuQS4xKTAnBgNVBAMTIENoYW1iZXJz -IG9mIENvbW1lcmNlIFJvb3QgLSAyMDA4MB4XDTA4MDgwMTEyMjk1MFoXDTM4MDcz -MTEyMjk1MFowga4xCzAJBgNVBAYTAkVVMUMwQQYDVQQHEzpNYWRyaWQgKHNlZSBj -dXJyZW50IGFkZHJlc3MgYXQgd3d3LmNhbWVyZmlybWEuY29tL2FkZHJlc3MpMRIw -EAYDVQQFEwlBODI3NDMyODcxGzAZBgNVBAoTEkFDIENhbWVyZmlybWEgUy5BLjEp -MCcGA1UEAxMgQ2hhbWJlcnMgb2YgQ29tbWVyY2UgUm9vdCAtIDIwMDgwggIiMA0G -CSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCvAMtwNyuAWko6bHiUfaN/Gh/2NdW9 -28sNRHI+JrKQUrpjOyhYb6WzbZSm891kDFX29ufyIiKAXuFixrYp4YFs8r/lfTJq -VKAyGVn+H4vXPWCGhSRv4xGzdz4gljUha7MI2XAuZPeEklPWDrCQiorjh40G072Q -DuKZoRuGDtqaCrsLYVAGUvGef3bsyw/QHg3PmTA9HMRFEFis1tPo1+XqxQEHd9ZR -5gN/ikilTWh1uem8nk4ZcfUyS5xtYBkL+8ydddy/Js2Pk3g5eXNeJQ7KXOt3EgfL -ZEFHcpOrUMPrCXZkNNI5t3YRCQ12RcSprj1qr7V9ZS+UWBDsXHyvfuK2GNnQm05a -Sd+pZgvMPMZ4fKecHePOjlO+Bd5gD2vlGts/4+EhySnB8esHnFIbAURRPHsl18Tl -UlRdJQfKFiC4reRB7noI/plvg6aRArBsNlVq5331lubKgdaX8ZSD6e2wsWsSaR6s -+12pxZjptFtYer49okQ6Y1nUCyXeG0+95QGezdIp1Z8XGQpvvwyQ0wlf2eOKNcx5 -Wk0ZN5K3xMGtr/R5JJqyAQuxr1yW84Ay+1w9mPGgP0revq+ULtlVmhduYJ1jbLhj -ya6BXBg14JC7vjxPNyK5fuvPnnchpj04gftI2jE9K+OJ9dC1vX7gUMQSibMjmhAx -hduub+84Mxh2EQIDAQABo4IBbDCCAWgwEgYDVR0TAQH/BAgwBgEB/wIBDDAdBgNV -HQ4EFgQU+SSsD7K1+HnA+mCIG8TZTQKeFxkwgeMGA1UdIwSB2zCB2IAU+SSsD7K1 -+HnA+mCIG8TZTQKeFxmhgbSkgbEwga4xCzAJBgNVBAYTAkVVMUMwQQYDVQQHEzpN -YWRyaWQgKHNlZSBjdXJyZW50IGFkZHJlc3MgYXQgd3d3LmNhbWVyZmlybWEuY29t -L2FkZHJlc3MpMRIwEAYDVQQFEwlBODI3NDMyODcxGzAZBgNVBAoTEkFDIENhbWVy -ZmlybWEgUy5BLjEpMCcGA1UEAxMgQ2hhbWJlcnMgb2YgQ29tbWVyY2UgUm9vdCAt -IDIwMDiCCQCj2kJ+pLGu2jAOBgNVHQ8BAf8EBAMCAQYwPQYDVR0gBDYwNDAyBgRV -HSAAMCowKAYIKwYBBQUHAgEWHGh0dHA6Ly9wb2xpY3kuY2FtZXJmaXJtYS5jb20w -DQYJKoZIhvcNAQEFBQADggIBAJASryI1wqM58C7e6bXpeHxIvj99RZJe6dqxGfwW -PJ+0W2aeaufDuV2I6A+tzyMP3iU6XsxPpcG1Lawk0lgH3qLPaYRgM+gQDROpI9CF -5Y57pp49chNyM/WqfcZjHwj0/gF/JM8rLFQJ3uIrbZLGOU8W6jx+ekbURWpGqOt1 -glanq6B8aBMz9p0w8G8nOSQjKpD9kCk18pPfNKXG9/jvjA9iSnyu0/VU+I22mlaH -FoI6M6taIgj3grrqLuBHmrS1RaMFO9ncLkVAO+rcf+g769HsJtg1pDDFOqxXnrN2 -pSB7+R5KBWIBpih1YJeSDW4+TTdDDZIVnBgizVGZoCkaPF+KMjNbMMeJL0eYD6MD -xvbxrN8y8NmBGuScvfaAFPDRLLmF9dijscilIeUcE5fuDr3fKanvNFNb0+RqE4QG -tjICxFKuItLcsiFCGtpA8CnJ7AoMXOLQusxI0zcKzBIKinmwPQN/aUv0NCB9szTq -jktk9T79syNnFQ0EuPAtwQlRPLJsFfClI9eDdOTlLsn+mCdCxqvGnrDQWzilm1De -fhiYtUU79nm06PcaewaD+9CL2rvHvRirCG88gGtAPxkZumWK5r7VXNM21+9AUiRg -OGcEMeyP84LG3rlV8zsxkVrctQgVrXYlCg17LofiDKYGvCYQbTed7N14jHyAxfDZ -d0jQ ------END CERTIFICATE----- - -# Issuer: CN=Global Chambersign Root - 2008 O=AC Camerfirma S.A. -# Subject: CN=Global Chambersign Root - 2008 O=AC Camerfirma S.A. -# Label: "Global Chambersign Root - 2008" -# Serial: 14541511773111788494 -# MD5 Fingerprint: 9e:80:ff:78:01:0c:2e:c1:36:bd:fe:96:90:6e:08:f3 -# SHA1 Fingerprint: 4a:bd:ee:ec:95:0d:35:9c:89:ae:c7:52:a1:2c:5b:29:f6:d6:aa:0c -# SHA256 Fingerprint: 13:63:35:43:93:34:a7:69:80:16:a0:d3:24:de:72:28:4e:07:9d:7b:52:20:bb:8f:bd:74:78:16:ee:be:ba:ca ------BEGIN CERTIFICATE----- -MIIHSTCCBTGgAwIBAgIJAMnN0+nVfSPOMA0GCSqGSIb3DQEBBQUAMIGsMQswCQYD -VQQGEwJFVTFDMEEGA1UEBxM6TWFkcmlkIChzZWUgY3VycmVudCBhZGRyZXNzIGF0 -IHd3dy5jYW1lcmZpcm1hLmNvbS9hZGRyZXNzKTESMBAGA1UEBRMJQTgyNzQzMjg3 -MRswGQYDVQQKExJBQyBDYW1lcmZpcm1hIFMuQS4xJzAlBgNVBAMTHkdsb2JhbCBD -aGFtYmVyc2lnbiBSb290IC0gMjAwODAeFw0wODA4MDExMjMxNDBaFw0zODA3MzEx -MjMxNDBaMIGsMQswCQYDVQQGEwJFVTFDMEEGA1UEBxM6TWFkcmlkIChzZWUgY3Vy -cmVudCBhZGRyZXNzIGF0IHd3dy5jYW1lcmZpcm1hLmNvbS9hZGRyZXNzKTESMBAG -A1UEBRMJQTgyNzQzMjg3MRswGQYDVQQKExJBQyBDYW1lcmZpcm1hIFMuQS4xJzAl -BgNVBAMTHkdsb2JhbCBDaGFtYmVyc2lnbiBSb290IC0gMjAwODCCAiIwDQYJKoZI -hvcNAQEBBQADggIPADCCAgoCggIBAMDfVtPkOpt2RbQT2//BthmLN0EYlVJH6xed -KYiONWwGMi5HYvNJBL99RDaxccy9Wglz1dmFRP+RVyXfXjaOcNFccUMd2drvXNL7 -G706tcuto8xEpw2uIRU/uXpbknXYpBI4iRmKt4DS4jJvVpyR1ogQC7N0ZJJ0YPP2 -zxhPYLIj0Mc7zmFLmY/CDNBAspjcDahOo7kKrmCgrUVSY7pmvWjg+b4aqIG7HkF4 -ddPB/gBVsIdU6CeQNR1MM62X/JcumIS/LMmjv9GYERTtY/jKmIhYF5ntRQOXfjyG -HoiMvvKRhI9lNNgATH23MRdaKXoKGCQwoze1eqkBfSbW+Q6OWfH9GzO1KTsXO0G2 -Id3UwD2ln58fQ1DJu7xsepeY7s2MH/ucUa6LcL0nn3HAa6x9kGbo1106DbDVwo3V -yJ2dwW3Q0L9R5OP4wzg2rtandeavhENdk5IMagfeOx2YItaswTXbo6Al/3K1dh3e -beksZixShNBFks4c5eUzHdwHU1SjqoI7mjcv3N2gZOnm3b2u/GSFHTynyQbehP9r -6GsaPMWis0L7iwk+XwhSx2LE1AVxv8Rk5Pihg+g+EpuoHtQ2TS9x9o0o9oOpE9Jh -wZG7SMA0j0GMS0zbaRL/UJScIINZc+18ofLx/d33SdNDWKBWY8o9PeU1VlnpDsog -zCtLkykPAgMBAAGjggFqMIIBZjASBgNVHRMBAf8ECDAGAQH/AgEMMB0GA1UdDgQW -BBS5CcqcHtvTbDprru1U8VuTBjUuXjCB4QYDVR0jBIHZMIHWgBS5CcqcHtvTbDpr -ru1U8VuTBjUuXqGBsqSBrzCBrDELMAkGA1UEBhMCRVUxQzBBBgNVBAcTOk1hZHJp -ZCAoc2VlIGN1cnJlbnQgYWRkcmVzcyBhdCB3d3cuY2FtZXJmaXJtYS5jb20vYWRk -cmVzcykxEjAQBgNVBAUTCUE4Mjc0MzI4NzEbMBkGA1UEChMSQUMgQ2FtZXJmaXJt -YSBTLkEuMScwJQYDVQQDEx5HbG9iYWwgQ2hhbWJlcnNpZ24gUm9vdCAtIDIwMDiC -CQDJzdPp1X0jzjAOBgNVHQ8BAf8EBAMCAQYwPQYDVR0gBDYwNDAyBgRVHSAAMCow -KAYIKwYBBQUHAgEWHGh0dHA6Ly9wb2xpY3kuY2FtZXJmaXJtYS5jb20wDQYJKoZI -hvcNAQEFBQADggIBAICIf3DekijZBZRG/5BXqfEv3xoNa/p8DhxJJHkn2EaqbylZ -UohwEurdPfWbU1Rv4WCiqAm57OtZfMY18dwY6fFn5a+6ReAJ3spED8IXDneRRXoz -X1+WLGiLwUePmJs9wOzL9dWCkoQ10b42OFZyMVtHLaoXpGNR6woBrX/sdZ7LoR/x -fxKxueRkf2fWIyr0uDldmOghp+G9PUIadJpwr2hsUF1Jz//7Dl3mLEfXgTpZALVz -a2Mg9jFFCDkO9HB+QHBaP9BrQql0PSgvAm11cpUJjUhjxsYjV5KTXjXBjfkK9yyd -Yhz2rXzdpjEetrHHfoUm+qRqtdpjMNHvkzeyZi99Bffnt0uYlDXA2TopwZ2yUDMd -SqlapskD7+3056huirRXhOukP9DuqqqHW2Pok+JrqNS4cnhrG+055F3Lm6qH1U9O -AP7Zap88MQ8oAgF9mOinsKJknnn4SPIVqczmyETrP3iZ8ntxPjzxmKfFGBI/5rso -M0LpRQp8bfKGeS/Fghl9CYl8slR2iK7ewfPM4W7bMdaTrpmg7yVqc5iJWzouE4ge -v8CSlDQb4ye3ix5vQv/n6TebUB0tovkC7stYWDpxvGjjqsGvHCgfotwjZT+B6q6Z -09gwzxMNTxXJhLynSC34MCN32EZLeW32jO06f2ARePTpm67VVMB0gNELQp/B ------END CERTIFICATE----- - # Issuer: CN=Go Daddy Root Certificate Authority - G2 O=GoDaddy.com, Inc. # Subject: CN=Go Daddy Root Certificate Authority - G2 O=GoDaddy.com, Inc. # Label: "Go Daddy Root Certificate Authority - G2" @@ -2140,6 +1411,45 @@ t/2jioSgrGK+KwmHNPBqAbubKVY8/gA3zyNs8U6qtnRGEmyR7jTV7JqR50S+kDFy SjnRBUkLp7Y3gaVdjKozXoEofKd9J+sAro03 -----END CERTIFICATE----- +# Issuer: CN=EC-ACC O=Agencia Catalana de Certificacio (NIF Q-0801176-I) OU=Serveis Publics de Certificacio/Vegeu https://www.catcert.net/verarrel (c)03/Jerarquia Entitats de Certificacio Catalanes +# Subject: CN=EC-ACC O=Agencia Catalana de Certificacio (NIF Q-0801176-I) OU=Serveis Publics de Certificacio/Vegeu https://www.catcert.net/verarrel (c)03/Jerarquia Entitats de Certificacio Catalanes +# Label: "EC-ACC" +# Serial: -23701579247955709139626555126524820479 +# MD5 Fingerprint: eb:f5:9d:29:0d:61:f9:42:1f:7c:c2:ba:6d:e3:15:09 +# SHA1 Fingerprint: 28:90:3a:63:5b:52:80:fa:e6:77:4c:0b:6d:a7:d6:ba:a6:4a:f2:e8 +# SHA256 Fingerprint: 88:49:7f:01:60:2f:31:54:24:6a:e2:8c:4d:5a:ef:10:f1:d8:7e:bb:76:62:6f:4a:e0:b7:f9:5b:a7:96:87:99 +-----BEGIN CERTIFICATE----- +MIIFVjCCBD6gAwIBAgIQ7is969Qh3hSoYqwE893EATANBgkqhkiG9w0BAQUFADCB +8zELMAkGA1UEBhMCRVMxOzA5BgNVBAoTMkFnZW5jaWEgQ2F0YWxhbmEgZGUgQ2Vy +dGlmaWNhY2lvIChOSUYgUS0wODAxMTc2LUkpMSgwJgYDVQQLEx9TZXJ2ZWlzIFB1 +YmxpY3MgZGUgQ2VydGlmaWNhY2lvMTUwMwYDVQQLEyxWZWdldSBodHRwczovL3d3 +dy5jYXRjZXJ0Lm5ldC92ZXJhcnJlbCAoYykwMzE1MDMGA1UECxMsSmVyYXJxdWlh +IEVudGl0YXRzIGRlIENlcnRpZmljYWNpbyBDYXRhbGFuZXMxDzANBgNVBAMTBkVD +LUFDQzAeFw0wMzAxMDcyMzAwMDBaFw0zMTAxMDcyMjU5NTlaMIHzMQswCQYDVQQG +EwJFUzE7MDkGA1UEChMyQWdlbmNpYSBDYXRhbGFuYSBkZSBDZXJ0aWZpY2FjaW8g +KE5JRiBRLTA4MDExNzYtSSkxKDAmBgNVBAsTH1NlcnZlaXMgUHVibGljcyBkZSBD +ZXJ0aWZpY2FjaW8xNTAzBgNVBAsTLFZlZ2V1IGh0dHBzOi8vd3d3LmNhdGNlcnQu +bmV0L3ZlcmFycmVsIChjKTAzMTUwMwYDVQQLEyxKZXJhcnF1aWEgRW50aXRhdHMg +ZGUgQ2VydGlmaWNhY2lvIENhdGFsYW5lczEPMA0GA1UEAxMGRUMtQUNDMIIBIjAN +BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsyLHT+KXQpWIR4NA9h0X84NzJB5R +85iKw5K4/0CQBXCHYMkAqbWUZRkiFRfCQ2xmRJoNBD45b6VLeqpjt4pEndljkYRm +4CgPukLjbo73FCeTae6RDqNfDrHrZqJyTxIThmV6PttPB/SnCWDaOkKZx7J/sxaV +HMf5NLWUhdWZXqBIoH7nF2W4onW4HvPlQn2v7fOKSGRdghST2MDk/7NQcvJ29rNd +QlB50JQ+awwAvthrDk4q7D7SzIKiGGUzE3eeml0aE9jD2z3Il3rucO2n5nzbcc8t +lGLfbdb1OL4/pYUKGbio2Al1QnDE6u/LDsg0qBIimAy4E5S2S+zw0JDnJwIDAQAB +o4HjMIHgMB0GA1UdEQQWMBSBEmVjX2FjY0BjYXRjZXJ0Lm5ldDAPBgNVHRMBAf8E +BTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUoMOLRKo3pUW/l4Ba0fF4 +opvpXY0wfwYDVR0gBHgwdjB0BgsrBgEEAfV4AQMBCjBlMCwGCCsGAQUFBwIBFiBo +dHRwczovL3d3dy5jYXRjZXJ0Lm5ldC92ZXJhcnJlbDA1BggrBgEFBQcCAjApGidW +ZWdldSBodHRwczovL3d3dy5jYXRjZXJ0Lm5ldC92ZXJhcnJlbCAwDQYJKoZIhvcN +AQEFBQADggEBAKBIW4IB9k1IuDlVNZyAelOZ1Vr/sXE7zDkJlF7W2u++AVtd0x7Y +/X1PzaBB4DSTv8vihpw3kpBWHNzrKQXlxJ7HNd+KDM3FIUPpqojlNcAZQmNaAl6k +SBg6hW/cnbw/nZzBh7h6YQjpdwt/cKt63dmXLGQehb+8dJahw3oS7AwaboMMPOhy +Rp/7SNVel+axofjk70YllJyJ22k4vuxcDlbHZVHlUIiIv0LVKz3l+bqeLrPK9HOS +Agu+TGbrIP65y7WZf+a2E/rKS03Z7lNGBjvGTq2TWoF+bCpLagVFjPIhpDGQh2xl +nJ2lYJU6Un/10asIbvPuW/mIPX64b24D5EI= +-----END CERTIFICATE----- + # Issuer: CN=Hellenic Academic and Research Institutions RootCA 2011 O=Hellenic Academic and Research Institutions Cert. Authority # Subject: CN=Hellenic Academic and Research Institutions RootCA 2011 O=Hellenic Academic and Research Institutions Cert. Authority # Label: "Hellenic Academic and Research Institutions RootCA 2011" @@ -2214,35 +1524,6 @@ LysRJyU3eExRarDzzFhdFPFqSBX/wge2sY0PjlxQRrM9vwGYT7JZVEc+NHt4bVaT LnPqZih4zR0Uv6CPLy64Lo7yFIrM6bV8+2ydDKXhlg== -----END CERTIFICATE----- -# Issuer: O=Trustis Limited OU=Trustis FPS Root CA -# Subject: O=Trustis Limited OU=Trustis FPS Root CA -# Label: "Trustis FPS Root CA" -# Serial: 36053640375399034304724988975563710553 -# MD5 Fingerprint: 30:c9:e7:1e:6b:e6:14:eb:65:b2:16:69:20:31:67:4d -# SHA1 Fingerprint: 3b:c0:38:0b:33:c3:f6:a6:0c:86:15:22:93:d9:df:f5:4b:81:c0:04 -# SHA256 Fingerprint: c1:b4:82:99:ab:a5:20:8f:e9:63:0a:ce:55:ca:68:a0:3e:da:5a:51:9c:88:02:a0:d3:a6:73:be:8f:8e:55:7d ------BEGIN CERTIFICATE----- -MIIDZzCCAk+gAwIBAgIQGx+ttiD5JNM2a/fH8YygWTANBgkqhkiG9w0BAQUFADBF -MQswCQYDVQQGEwJHQjEYMBYGA1UEChMPVHJ1c3RpcyBMaW1pdGVkMRwwGgYDVQQL -ExNUcnVzdGlzIEZQUyBSb290IENBMB4XDTAzMTIyMzEyMTQwNloXDTI0MDEyMTEx -MzY1NFowRTELMAkGA1UEBhMCR0IxGDAWBgNVBAoTD1RydXN0aXMgTGltaXRlZDEc -MBoGA1UECxMTVHJ1c3RpcyBGUFMgUm9vdCBDQTCCASIwDQYJKoZIhvcNAQEBBQAD -ggEPADCCAQoCggEBAMVQe547NdDfxIzNjpvto8A2mfRC6qc+gIMPpqdZh8mQRUN+ -AOqGeSoDvT03mYlmt+WKVoaTnGhLaASMk5MCPjDSNzoiYYkchU59j9WvezX2fihH -iTHcDnlkH5nSW7r+f2C/revnPDgpai/lkQtV/+xvWNUtyd5MZnGPDNcE2gfmHhjj -vSkCqPoc4Vu5g6hBSLwacY3nYuUtsuvffM/bq1rKMfFMIvMFE/eC+XN5DL7XSxzA -0RU8k0Fk0ea+IxciAIleH2ulrG6nS4zto3Lmr2NNL4XSFDWaLk6M6jKYKIahkQlB -OrTh4/L68MkKokHdqeMDx4gVOxzUGpTXn2RZEm0CAwEAAaNTMFEwDwYDVR0TAQH/ -BAUwAwEB/zAfBgNVHSMEGDAWgBS6+nEleYtXQSUhhgtx67JkDoshZzAdBgNVHQ4E -FgQUuvpxJXmLV0ElIYYLceuyZA6LIWcwDQYJKoZIhvcNAQEFBQADggEBAH5Y//01 -GX2cGE+esCu8jowU/yyg2kdbw++BLa8F6nRIW/M+TgfHbcWzk88iNVy2P3UnXwmW -zaD+vkAMXBJV+JOCyinpXj9WV4s4NvdFGkwozZ5BuO1WTISkQMi4sKUraXAEasP4 -1BIy+Q7DsdwyhEQsb8tGD+pmQQ9P8Vilpg0ND2HepZ5dfWWhPBfnqFVO76DH7cZE -f1T1o+CP8HxVIo8ptoGj4W1OLBuAZ+ytIJ8MYmHVl/9D7S3B2l0pKoU/rGXuhg8F -jZBf3+6f9L/uHfuY5H+QK4R4EA5sSVPvFVtlRkpdr7r7OnIdzfYliB6XzCGcKQEN -ZetX2fNXlrtIzYE= ------END CERTIFICATE----- - # Issuer: CN=Buypass Class 2 Root CA O=Buypass AS-983163327 # Subject: CN=Buypass Class 2 Root CA O=Buypass AS-983163327 # Label: "Buypass Class 2 Root CA" @@ -2352,38 +1633,6 @@ e9eiPZaGzPImNC1qkp2aGtAw4l1OBLBfiyB+d8E9lYLRRpo7PHi4b6HQDWSieB4p TpPDpFQUWw== -----END CERTIFICATE----- -# Issuer: CN=EE Certification Centre Root CA O=AS Sertifitseerimiskeskus -# Subject: CN=EE Certification Centre Root CA O=AS Sertifitseerimiskeskus -# Label: "EE Certification Centre Root CA" -# Serial: 112324828676200291871926431888494945866 -# MD5 Fingerprint: 43:5e:88:d4:7d:1a:4a:7e:fd:84:2e:52:eb:01:d4:6f -# SHA1 Fingerprint: c9:a8:b9:e7:55:80:5e:58:e3:53:77:a7:25:eb:af:c3:7b:27:cc:d7 -# SHA256 Fingerprint: 3e:84:ba:43:42:90:85:16:e7:75:73:c0:99:2f:09:79:ca:08:4e:46:85:68:1f:f1:95:cc:ba:8a:22:9b:8a:76 ------BEGIN CERTIFICATE----- -MIIEAzCCAuugAwIBAgIQVID5oHPtPwBMyonY43HmSjANBgkqhkiG9w0BAQUFADB1 -MQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1 -czEoMCYGA1UEAwwfRUUgQ2VydGlmaWNhdGlvbiBDZW50cmUgUm9vdCBDQTEYMBYG -CSqGSIb3DQEJARYJcGtpQHNrLmVlMCIYDzIwMTAxMDMwMTAxMDMwWhgPMjAzMDEy -MTcyMzU5NTlaMHUxCzAJBgNVBAYTAkVFMSIwIAYDVQQKDBlBUyBTZXJ0aWZpdHNl -ZXJpbWlza2Vza3VzMSgwJgYDVQQDDB9FRSBDZXJ0aWZpY2F0aW9uIENlbnRyZSBS -b290IENBMRgwFgYJKoZIhvcNAQkBFglwa2lAc2suZWUwggEiMA0GCSqGSIb3DQEB -AQUAA4IBDwAwggEKAoIBAQDIIMDs4MVLqwd4lfNE7vsLDP90jmG7sWLqI9iroWUy -euuOF0+W2Ap7kaJjbMeMTC55v6kF/GlclY1i+blw7cNRfdCT5mzrMEvhvH2/UpvO -bntl8jixwKIy72KyaOBhU8E2lf/slLo2rpwcpzIP5Xy0xm90/XsY6KxX7QYgSzIw -WFv9zajmofxwvI6Sc9uXp3whrj3B9UiHbCe9nyV0gVWw93X2PaRka9ZP585ArQ/d -MtO8ihJTmMmJ+xAdTX7Nfh9WDSFwhfYggx/2uh8Ej+p3iDXE/+pOoYtNP2MbRMNE -1CV2yreN1x5KZmTNXMWcg+HCCIia7E6j8T4cLNlsHaFLAgMBAAGjgYowgYcwDwYD -VR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFBLyWj7qVhy/ -zQas8fElyalL1BSZMEUGA1UdJQQ+MDwGCCsGAQUFBwMCBggrBgEFBQcDAQYIKwYB -BQUHAwMGCCsGAQUFBwMEBggrBgEFBQcDCAYIKwYBBQUHAwkwDQYJKoZIhvcNAQEF -BQADggEBAHv25MANqhlHt01Xo/6tu7Fq1Q+e2+RjxY6hUFaTlrg4wCQiZrxTFGGV -v9DHKpY5P30osxBAIWrEr7BSdxjhlthWXePdNl4dp1BUoMUq5KqMlIpPnTX/dqQG -E5Gion0ARD9V04I8GtVbvFZMIi5GQ4okQC3zErg7cBqklrkar4dBGmoYDQZPxz5u -uSlNDUmJEYcyW+ZLBMjkXOZ0c5RdFpgTlf7727FE5TpwrDdr5rMzcijJs1eg9gIW -iAYLtqZLICjU3j2LrTcFU3T+bsy8QxdxXvnFzBqpYe73dgzzcvRyrc9yAjYHR8/v -GVCJYMzpJJUPwssd8m92kMfMdcGWxZ0= ------END CERTIFICATE----- - # Issuer: CN=D-TRUST Root Class 3 CA 2 2009 O=D-Trust GmbH # Subject: CN=D-TRUST Root Class 3 CA 2 2009 O=D-Trust GmbH # Label: "D-TRUST Root Class 3 CA 2 2009" @@ -3136,46 +2385,6 @@ KoZIzj0EAwMDaAAwZQIxAOVpEslu28YxuglB4Zf4+/2a4n0Sye18ZNPLBSWLVtmg xwy8p2Fp8fc74SrL+SvzZpA3 -----END CERTIFICATE----- -# Issuer: CN=Staat der Nederlanden Root CA - G3 O=Staat der Nederlanden -# Subject: CN=Staat der Nederlanden Root CA - G3 O=Staat der Nederlanden -# Label: "Staat der Nederlanden Root CA - G3" -# Serial: 10003001 -# MD5 Fingerprint: 0b:46:67:07:db:10:2f:19:8c:35:50:60:d1:0b:f4:37 -# SHA1 Fingerprint: d8:eb:6b:41:51:92:59:e0:f3:e7:85:00:c0:3d:b6:88:97:c9:ee:fc -# SHA256 Fingerprint: 3c:4f:b0:b9:5a:b8:b3:00:32:f4:32:b8:6f:53:5f:e1:72:c1:85:d0:fd:39:86:58:37:cf:36:18:7f:a6:f4:28 ------BEGIN CERTIFICATE----- -MIIFdDCCA1ygAwIBAgIEAJiiOTANBgkqhkiG9w0BAQsFADBaMQswCQYDVQQGEwJO -TDEeMBwGA1UECgwVU3RhYXQgZGVyIE5lZGVybGFuZGVuMSswKQYDVQQDDCJTdGFh -dCBkZXIgTmVkZXJsYW5kZW4gUm9vdCBDQSAtIEczMB4XDTEzMTExNDExMjg0MloX -DTI4MTExMzIzMDAwMFowWjELMAkGA1UEBhMCTkwxHjAcBgNVBAoMFVN0YWF0IGRl -ciBOZWRlcmxhbmRlbjErMCkGA1UEAwwiU3RhYXQgZGVyIE5lZGVybGFuZGVuIFJv -b3QgQ0EgLSBHMzCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAL4yolQP -cPssXFnrbMSkUeiFKrPMSjTysF/zDsccPVMeiAho2G89rcKezIJnByeHaHE6n3WW -IkYFsO2tx1ueKt6c/DrGlaf1F2cY5y9JCAxcz+bMNO14+1Cx3Gsy8KL+tjzk7FqX -xz8ecAgwoNzFs21v0IJyEavSgWhZghe3eJJg+szeP4TrjTgzkApyI/o1zCZxMdFy -KJLZWyNtZrVtB0LrpjPOktvA9mxjeM3KTj215VKb8b475lRgsGYeCasH/lSJEULR -9yS6YHgamPfJEf0WwTUaVHXvQ9Plrk7O53vDxk5hUUurmkVLoR9BvUhTFXFkC4az -5S6+zqQbwSmEorXLCCN2QyIkHxcE1G6cxvx/K2Ya7Irl1s9N9WMJtxU51nus6+N8 -6U78dULI7ViVDAZCopz35HCz33JvWjdAidiFpNfxC95DGdRKWCyMijmev4SH8RY7 -Ngzp07TKbBlBUgmhHbBqv4LvcFEhMtwFdozL92TkA1CvjJFnq8Xy7ljY3r735zHP -bMk7ccHViLVlvMDoFxcHErVc0qsgk7TmgoNwNsXNo42ti+yjwUOH5kPiNL6VizXt -BznaqB16nzaeErAMZRKQFWDZJkBE41ZgpRDUajz9QdwOWke275dhdU/Z/seyHdTt -XUmzqWrLZoQT1Vyg3N9udwbRcXXIV2+vD3dbAgMBAAGjQjBAMA8GA1UdEwEB/wQF -MAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBRUrfrHkleuyjWcLhL75Lpd -INyUVzANBgkqhkiG9w0BAQsFAAOCAgEAMJmdBTLIXg47mAE6iqTnB/d6+Oea31BD -U5cqPco8R5gu4RV78ZLzYdqQJRZlwJ9UXQ4DO1t3ApyEtg2YXzTdO2PCwyiBwpwp -LiniyMMB8jPqKqrMCQj3ZWfGzd/TtiunvczRDnBfuCPRy5FOCvTIeuXZYzbB1N/8 -Ipf3YF3qKS9Ysr1YvY2WTxB1v0h7PVGHoTx0IsL8B3+A3MSs/mrBcDCw6Y5p4ixp -gZQJut3+TcCDjJRYwEYgr5wfAvg1VUkvRtTA8KCWAg8zxXHzniN9lLf9OtMJgwYh -/WA9rjLA0u6NpvDntIJ8CsxwyXmA+P5M9zWEGYox+wrZ13+b8KKaa8MFSu1BYBQw -0aoRQm7TIwIEC8Zl3d1Sd9qBa7Ko+gE4uZbqKmxnl4mUnrzhVNXkanjvSr0rmj1A -fsbAddJu+2gw7OyLnflJNZoaLNmzlTnVHpL3prllL+U9bTpITAjc5CgSKL59NVzq -4BZ+Extq1z7XnvwtdbLBFNUjA9tbbws+eC8N3jONFrdI54OagQ97wUNNVQQXOEpR -1VmiiXTTn74eS9fGbbeIJG9gkaSChVtWQbzQRKtqE77RLFi3EjNYsjdj3BP1lB0/ -QFH1T/U67cjF68IeHRaVesd+QnGTbksVtzDfqu1XhUisHWrdOWnk4Xl4vs4Fv6EM -94B7IWcnMFk= ------END CERTIFICATE----- - # Issuer: CN=Staat der Nederlanden EV Root CA O=Staat der Nederlanden # Subject: CN=Staat der Nederlanden EV Root CA O=Staat der Nederlanden # Label: "Staat der Nederlanden EV Root CA" @@ -3749,47 +2958,6 @@ CkcO8DdZEv8tmZQoTipPNU0zWgIxAOp1AE47xDqUEpHJWEadIRNyp4iciuRMStuW 1KyLa2tJElMzrdfkviT8tQp21KW8EA== -----END CERTIFICATE----- -# Issuer: CN=LuxTrust Global Root 2 O=LuxTrust S.A. -# Subject: CN=LuxTrust Global Root 2 O=LuxTrust S.A. -# Label: "LuxTrust Global Root 2" -# Serial: 59914338225734147123941058376788110305822489521 -# MD5 Fingerprint: b2:e1:09:00:61:af:f7:f1:91:6f:c4:ad:8d:5e:3b:7c -# SHA1 Fingerprint: 1e:0e:56:19:0a:d1:8b:25:98:b2:04:44:ff:66:8a:04:17:99:5f:3f -# SHA256 Fingerprint: 54:45:5f:71:29:c2:0b:14:47:c4:18:f9:97:16:8f:24:c5:8f:c5:02:3b:f5:da:5b:e2:eb:6e:1d:d8:90:2e:d5 ------BEGIN CERTIFICATE----- -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 ------END CERTIFICATE----- - # Issuer: CN=TUBITAK Kamu SM SSL Kok Sertifikasi - Surum 1 O=Turkiye Bilimsel ve Teknolojik Arastirma Kurumu - TUBITAK OU=Kamu Sertifikasyon Merkezi - Kamu SM # Subject: CN=TUBITAK Kamu SM SSL Kok Sertifikasi - Surum 1 O=Turkiye Bilimsel ve Teknolojik Arastirma Kurumu - TUBITAK OU=Kamu Sertifikasyon Merkezi - Kamu SM # Label: "TUBITAK Kamu SM SSL Kok Sertifikasi - Surum 1" @@ -4556,3 +3724,639 @@ L5/ndtFhKvshuzHQqp9HpLIiyhY6UFfEW0NnxWViA0kB60PZ2Pierc+xYw5F9KBa LJstxabArahH9CdMOA0uG0k7UvToiIMrVCjU8jVStDKDYmlkDJGcn5fqdBb9HxEG mpv0 -----END CERTIFICATE----- + +# Issuer: CN=Entrust Root Certification Authority - G4 O=Entrust, Inc. OU=See www.entrust.net/legal-terms/(c) 2015 Entrust, Inc. - for authorized use only +# Subject: CN=Entrust Root Certification Authority - G4 O=Entrust, Inc. OU=See www.entrust.net/legal-terms/(c) 2015 Entrust, Inc. - for authorized use only +# Label: "Entrust Root Certification Authority - G4" +# Serial: 289383649854506086828220374796556676440 +# MD5 Fingerprint: 89:53:f1:83:23:b7:7c:8e:05:f1:8c:71:38:4e:1f:88 +# SHA1 Fingerprint: 14:88:4e:86:26:37:b0:26:af:59:62:5c:40:77:ec:35:29:ba:96:01 +# SHA256 Fingerprint: db:35:17:d1:f6:73:2a:2d:5a:b9:7c:53:3e:c7:07:79:ee:32:70:a6:2f:b4:ac:42:38:37:24:60:e6:f0:1e:88 +-----BEGIN CERTIFICATE----- +MIIGSzCCBDOgAwIBAgIRANm1Q3+vqTkPAAAAAFVlrVgwDQYJKoZIhvcNAQELBQAw +gb4xCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1FbnRydXN0LCBJbmMuMSgwJgYDVQQL +Ex9TZWUgd3d3LmVudHJ1c3QubmV0L2xlZ2FsLXRlcm1zMTkwNwYDVQQLEzAoYykg +MjAxNSBFbnRydXN0LCBJbmMuIC0gZm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxMjAw +BgNVBAMTKUVudHJ1c3QgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAtIEc0 +MB4XDTE1MDUyNzExMTExNloXDTM3MTIyNzExNDExNlowgb4xCzAJBgNVBAYTAlVT +MRYwFAYDVQQKEw1FbnRydXN0LCBJbmMuMSgwJgYDVQQLEx9TZWUgd3d3LmVudHJ1 +c3QubmV0L2xlZ2FsLXRlcm1zMTkwNwYDVQQLEzAoYykgMjAxNSBFbnRydXN0LCBJ +bmMuIC0gZm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxMjAwBgNVBAMTKUVudHJ1c3Qg +Um9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAtIEc0MIICIjANBgkqhkiG9w0B +AQEFAAOCAg8AMIICCgKCAgEAsewsQu7i0TD/pZJH4i3DumSXbcr3DbVZwbPLqGgZ +2K+EbTBwXX7zLtJTmeH+H17ZSK9dE43b/2MzTdMAArzE+NEGCJR5WIoV3imz/f3E +T+iq4qA7ec2/a0My3dl0ELn39GjUu9CH1apLiipvKgS1sqbHoHrmSKvS0VnM1n4j +5pds8ELl3FFLFUHtSUrJ3hCX1nbB76W1NhSXNdh4IjVS70O92yfbYVaCNNzLiGAM +C1rlLAHGVK/XqsEQe9IFWrhAnoanw5CGAlZSCXqc0ieCU0plUmr1POeo8pyvi73T +DtTUXm6Hnmo9RR3RXRv06QqsYJn7ibT/mCzPfB3pAqoEmh643IhuJbNsZvc8kPNX +wbMv9W3y+8qh+CmdRouzavbmZwe+LGcKKh9asj5XxNMhIWNlUpEbsZmOeX7m640A +2Vqq6nPopIICR5b+W45UYaPrL0swsIsjdXJ8ITzI9vF01Bx7owVV7rtNOzK+mndm +nqxpkCIHH2E6lr7lmk/MBTwoWdPBDFSoWWG9yHJM6Nyfh3+9nEg2XpWjDrk4JFX8 +dWbrAuMINClKxuMrLzOg2qOGpRKX/YAr2hRC45K9PvJdXmd0LhyIRyk0X+IyqJwl +N4y6mACXi0mWHv0liqzc2thddG5msP9E36EYxr5ILzeUePiVSj9/E15dWf10hkNj +c0kCAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYD +VR0OBBYEFJ84xFYjwznooHFs6FRM5Og6sb9nMA0GCSqGSIb3DQEBCwUAA4ICAQAS +5UKme4sPDORGpbZgQIeMJX6tuGguW8ZAdjwD+MlZ9POrYs4QjbRaZIxowLByQzTS +Gwv2LFPSypBLhmb8qoMi9IsabyZIrHZ3CL/FmFz0Jomee8O5ZDIBf9PD3Vht7LGr +hFV0d4QEJ1JrhkzO3bll/9bGXp+aEJlLdWr+aumXIOTkdnrG0CSqkM0gkLpHZPt/ +B7NTeLUKYvJzQ85BK4FqLoUWlFPUa19yIqtRLULVAJyZv967lDtX/Zr1hstWO1uI +AeV8KEsD+UmDfLJ/fOPtjqF/YFOOVZ1QNBIPt5d7bIdKROf1beyAN/BYGW5KaHbw +H5Lk6rWS02FREAutp9lfx1/cH6NcjKF+m7ee01ZvZl4HliDtC3T7Zk6LERXpgUl+ +b7DUUH8i119lAg2m9IUe2K4GS0qn0jFmwvjO5QimpAKWRGhXxNUzzxkvFMSUHHuk +2fCfDrGA4tGeEWSpiBE6doLlYsKA2KSD7ZPvfC+QsDJMlhVoSFLUmQjAJOgc47Ol +IQ6SwJAfzyBfyjs4x7dtOvPmRLgOMWuIjnDrnBdSqEGULoe256YSxXXfW8AKbnuk +5F6G+TaU33fD6Q3AOfF5u0aOq0NZJ7cguyPpVkAh7DE9ZapD8j3fcEThuk0mEDuY +n/PIjhs4ViFqUZPTkcpG2om3PVODLAgfi49T3f+sHw== +-----END CERTIFICATE----- + +# Issuer: CN=Microsoft ECC Root Certificate Authority 2017 O=Microsoft Corporation +# Subject: CN=Microsoft ECC Root Certificate Authority 2017 O=Microsoft Corporation +# Label: "Microsoft ECC Root Certificate Authority 2017" +# Serial: 136839042543790627607696632466672567020 +# MD5 Fingerprint: dd:a1:03:e6:4a:93:10:d1:bf:f0:19:42:cb:fe:ed:67 +# SHA1 Fingerprint: 99:9a:64:c3:7f:f4:7d:9f:ab:95:f1:47:69:89:14:60:ee:c4:c3:c5 +# SHA256 Fingerprint: 35:8d:f3:9d:76:4a:f9:e1:b7:66:e9:c9:72:df:35:2e:e1:5c:fa:c2:27:af:6a:d1:d7:0e:8e:4a:6e:dc:ba:02 +-----BEGIN CERTIFICATE----- +MIICWTCCAd+gAwIBAgIQZvI9r4fei7FK6gxXMQHC7DAKBggqhkjOPQQDAzBlMQsw +CQYDVQQGEwJVUzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTYwNAYD +VQQDEy1NaWNyb3NvZnQgRUNDIFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIw +MTcwHhcNMTkxMjE4MjMwNjQ1WhcNNDIwNzE4MjMxNjA0WjBlMQswCQYDVQQGEwJV +UzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTYwNAYDVQQDEy1NaWNy +b3NvZnQgRUNDIFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIwMTcwdjAQBgcq +hkjOPQIBBgUrgQQAIgNiAATUvD0CQnVBEyPNgASGAlEvaqiBYgtlzPbKnR5vSmZR +ogPZnZH6thaxjG7efM3beaYvzrvOcS/lpaso7GMEZpn4+vKTEAXhgShC48Zo9OYb +hGBKia/teQ87zvH2RPUBeMCjVDBSMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8E +BTADAQH/MB0GA1UdDgQWBBTIy5lycFIM+Oa+sgRXKSrPQhDtNTAQBgkrBgEEAYI3 +FQEEAwIBADAKBggqhkjOPQQDAwNoADBlAjBY8k3qDPlfXu5gKcs68tvWMoQZP3zV +L8KxzJOuULsJMsbG7X7JNpQS5GiFBqIb0C8CMQCZ6Ra0DvpWSNSkMBaReNtUjGUB +iudQZsIxtzm6uBoiB078a1QWIP8rtedMDE2mT3M= +-----END CERTIFICATE----- + +# Issuer: CN=Microsoft RSA Root Certificate Authority 2017 O=Microsoft Corporation +# Subject: CN=Microsoft RSA Root Certificate Authority 2017 O=Microsoft Corporation +# Label: "Microsoft RSA Root Certificate Authority 2017" +# Serial: 40975477897264996090493496164228220339 +# MD5 Fingerprint: 10:ff:00:ff:cf:c9:f8:c7:7a:c0:ee:35:8e:c9:0f:47 +# SHA1 Fingerprint: 73:a5:e6:4a:3b:ff:83:16:ff:0e:dc:cc:61:8a:90:6e:4e:ae:4d:74 +# SHA256 Fingerprint: c7:41:f7:0f:4b:2a:8d:88:bf:2e:71:c1:41:22:ef:53:ef:10:eb:a0:cf:a5:e6:4c:fa:20:f4:18:85:30:73:e0 +-----BEGIN CERTIFICATE----- +MIIFqDCCA5CgAwIBAgIQHtOXCV/YtLNHcB6qvn9FszANBgkqhkiG9w0BAQwFADBl +MQswCQYDVQQGEwJVUzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTYw +NAYDVQQDEy1NaWNyb3NvZnQgUlNBIFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5 +IDIwMTcwHhcNMTkxMjE4MjI1MTIyWhcNNDIwNzE4MjMwMDIzWjBlMQswCQYDVQQG +EwJVUzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTYwNAYDVQQDEy1N +aWNyb3NvZnQgUlNBIFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIwMTcwggIi +MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDKW76UM4wplZEWCpW9R2LBifOZ +Nt9GkMml7Xhqb0eRaPgnZ1AzHaGm++DlQ6OEAlcBXZxIQIJTELy/xztokLaCLeX0 +ZdDMbRnMlfl7rEqUrQ7eS0MdhweSE5CAg2Q1OQT85elss7YfUJQ4ZVBcF0a5toW1 +HLUX6NZFndiyJrDKxHBKrmCk3bPZ7Pw71VdyvD/IybLeS2v4I2wDwAW9lcfNcztm +gGTjGqwu+UcF8ga2m3P1eDNbx6H7JyqhtJqRjJHTOoI+dkC0zVJhUXAoP8XFWvLJ +jEm7FFtNyP9nTUwSlq31/niol4fX/V4ggNyhSyL71Imtus5Hl0dVe49FyGcohJUc +aDDv70ngNXtk55iwlNpNhTs+VcQor1fznhPbRiefHqJeRIOkpcrVE7NLP8TjwuaG +YaRSMLl6IE9vDzhTyzMMEyuP1pq9KsgtsRx9S1HKR9FIJ3Jdh+vVReZIZZ2vUpC6 +W6IYZVcSn2i51BVrlMRpIpj0M+Dt+VGOQVDJNE92kKz8OMHY4Xu54+OU4UZpyw4K +UGsTuqwPN1q3ErWQgR5WrlcihtnJ0tHXUeOrO8ZV/R4O03QK0dqq6mm4lyiPSMQH ++FJDOvTKVTUssKZqwJz58oHhEmrARdlns87/I6KJClTUFLkqqNfs+avNJVgyeY+Q +W5g5xAgGwax/Dj0ApQIDAQABo1QwUjAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/ +BAUwAwEB/zAdBgNVHQ4EFgQUCctZf4aycI8awznjwNnpv7tNsiMwEAYJKwYBBAGC +NxUBBAMCAQAwDQYJKoZIhvcNAQEMBQADggIBAKyvPl3CEZaJjqPnktaXFbgToqZC +LgLNFgVZJ8og6Lq46BrsTaiXVq5lQ7GPAJtSzVXNUzltYkyLDVt8LkS/gxCP81OC +gMNPOsduET/m4xaRhPtthH80dK2Jp86519efhGSSvpWhrQlTM93uCupKUY5vVau6 +tZRGrox/2KJQJWVggEbbMwSubLWYdFQl3JPk+ONVFT24bcMKpBLBaYVu32TxU5nh +SnUgnZUP5NbcA/FZGOhHibJXWpS2qdgXKxdJ5XbLwVaZOjex/2kskZGT4d9Mozd2 +TaGf+G0eHdP67Pv0RR0Tbc/3WeUiJ3IrhvNXuzDtJE3cfVa7o7P4NHmJweDyAmH3 +pvwPuxwXC65B2Xy9J6P9LjrRk5Sxcx0ki69bIImtt2dmefU6xqaWM/5TkshGsRGR +xpl/j8nWZjEgQRCHLQzWwa80mMpkg/sTV9HB8Dx6jKXB/ZUhoHHBk2dxEuqPiApp +GWSZI1b7rCoucL5mxAyE7+WL85MB+GqQk2dLsmijtWKP6T+MejteD+eMuMZ87zf9 +dOLITzNy4ZQ5bb0Sr74MTnB8G2+NszKTc0QWbej09+CVgI+WXTik9KveCjCHk9hN +AHFiRSdLOkKEW39lt2c0Ui2cFmuqqNh7o0JMcccMyj6D5KbvtwEwXlGjefVwaaZB +RA+GsCyRxj3qrg+E +-----END CERTIFICATE----- + +# Issuer: CN=e-Szigno Root CA 2017 O=Microsec Ltd. +# Subject: CN=e-Szigno Root CA 2017 O=Microsec Ltd. +# Label: "e-Szigno Root CA 2017" +# Serial: 411379200276854331539784714 +# MD5 Fingerprint: de:1f:f6:9e:84:ae:a7:b4:21:ce:1e:58:7d:d1:84:98 +# SHA1 Fingerprint: 89:d4:83:03:4f:9e:9a:48:80:5f:72:37:d4:a9:a6:ef:cb:7c:1f:d1 +# SHA256 Fingerprint: be:b0:0b:30:83:9b:9b:c3:2c:32:e4:44:79:05:95:06:41:f2:64:21:b1:5e:d0:89:19:8b:51:8a:e2:ea:1b:99 +-----BEGIN CERTIFICATE----- +MIICQDCCAeWgAwIBAgIMAVRI7yH9l1kN9QQKMAoGCCqGSM49BAMCMHExCzAJBgNV +BAYTAkhVMREwDwYDVQQHDAhCdWRhcGVzdDEWMBQGA1UECgwNTWljcm9zZWMgTHRk +LjEXMBUGA1UEYQwOVkFUSFUtMjM1ODQ0OTcxHjAcBgNVBAMMFWUtU3ppZ25vIFJv +b3QgQ0EgMjAxNzAeFw0xNzA4MjIxMjA3MDZaFw00MjA4MjIxMjA3MDZaMHExCzAJ +BgNVBAYTAkhVMREwDwYDVQQHDAhCdWRhcGVzdDEWMBQGA1UECgwNTWljcm9zZWMg +THRkLjEXMBUGA1UEYQwOVkFUSFUtMjM1ODQ0OTcxHjAcBgNVBAMMFWUtU3ppZ25v +IFJvb3QgQ0EgMjAxNzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABJbcPYrYsHtv +xie+RJCxs1YVe45DJH0ahFnuY2iyxl6H0BVIHqiQrb1TotreOpCmYF9oMrWGQd+H +Wyx7xf58etqjYzBhMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0G +A1UdDgQWBBSHERUI0arBeAyxr87GyZDvvzAEwDAfBgNVHSMEGDAWgBSHERUI0arB +eAyxr87GyZDvvzAEwDAKBggqhkjOPQQDAgNJADBGAiEAtVfd14pVCzbhhkT61Nlo +jbjcI4qKDdQvfepz7L9NbKgCIQDLpbQS+ue16M9+k/zzNY9vTlp8tLxOsvxyqltZ ++efcMQ== +-----END CERTIFICATE----- + +# Issuer: O=CERTSIGN SA OU=certSIGN ROOT CA G2 +# Subject: O=CERTSIGN SA OU=certSIGN ROOT CA G2 +# Label: "certSIGN Root CA G2" +# Serial: 313609486401300475190 +# MD5 Fingerprint: 8c:f1:75:8a:c6:19:cf:94:b7:f7:65:20:87:c3:97:c7 +# SHA1 Fingerprint: 26:f9:93:b4:ed:3d:28:27:b0:b9:4b:a7:e9:15:1d:a3:8d:92:e5:32 +# SHA256 Fingerprint: 65:7c:fe:2f:a7:3f:aa:38:46:25:71:f3:32:a2:36:3a:46:fc:e7:02:09:51:71:07:02:cd:fb:b6:ee:da:33:05 +-----BEGIN CERTIFICATE----- +MIIFRzCCAy+gAwIBAgIJEQA0tk7GNi02MA0GCSqGSIb3DQEBCwUAMEExCzAJBgNV +BAYTAlJPMRQwEgYDVQQKEwtDRVJUU0lHTiBTQTEcMBoGA1UECxMTY2VydFNJR04g +Uk9PVCBDQSBHMjAeFw0xNzAyMDYwOTI3MzVaFw00MjAyMDYwOTI3MzVaMEExCzAJ +BgNVBAYTAlJPMRQwEgYDVQQKEwtDRVJUU0lHTiBTQTEcMBoGA1UECxMTY2VydFNJ +R04gUk9PVCBDQSBHMjCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMDF +dRmRfUR0dIf+DjuW3NgBFszuY5HnC2/OOwppGnzC46+CjobXXo9X69MhWf05N0Iw +vlDqtg+piNguLWkh59E3GE59kdUWX2tbAMI5Qw02hVK5U2UPHULlj88F0+7cDBrZ +uIt4ImfkabBoxTzkbFpG583H+u/E7Eu9aqSs/cwoUe+StCmrqzWaTOTECMYmzPhp +n+Sc8CnTXPnGFiWeI8MgwT0PPzhAsP6CRDiqWhqKa2NYOLQV07YRaXseVO6MGiKs +cpc/I1mbySKEwQdPzH/iV8oScLumZfNpdWO9lfsbl83kqK/20U6o2YpxJM02PbyW +xPFsqa7lzw1uKA2wDrXKUXt4FMMgL3/7FFXhEZn91QqhngLjYl/rNUssuHLoPj1P +rCy7Lobio3aP5ZMqz6WryFyNSwb/EkaseMsUBzXgqd+L6a8VTxaJW732jcZZroiF +DsGJ6x9nxUWO/203Nit4ZoORUSs9/1F3dmKh7Gc+PoGD4FapUB8fepmrY7+EF3fx +DTvf95xhszWYijqy7DwaNz9+j5LP2RIUZNoQAhVB/0/E6xyjyfqZ90bp4RjZsbgy +LcsUDFDYg2WD7rlcz8sFWkz6GZdr1l0T08JcVLwyc6B49fFtHsufpaafItzRUZ6C +eWRgKRM+o/1Pcmqr4tTluCRVLERLiohEnMqE0yo7AgMBAAGjQjBAMA8GA1UdEwEB +/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBSCIS1mxteg4BXrzkwJ +d8RgnlRuAzANBgkqhkiG9w0BAQsFAAOCAgEAYN4auOfyYILVAzOBywaK8SJJ6ejq +kX/GM15oGQOGO0MBzwdw5AgeZYWR5hEit/UCI46uuR59H35s5r0l1ZUa8gWmr4UC +b6741jH/JclKyMeKqdmfS0mbEVeZkkMR3rYzpMzXjWR91M08KCy0mpbqTfXERMQl +qiCA2ClV9+BB/AYm/7k29UMUA2Z44RGx2iBfRgB4ACGlHgAoYXhvqAEBj500mv/0 +OJD7uNGzcgbJceaBxXntC6Z58hMLnPddDnskk7RI24Zf3lCGeOdA5jGokHZwYa+c +NywRtYK3qq4kNFtyDGkNzVmf9nGvnAvRCjj5BiKDUyUM/FHE5r7iOZULJK2v0ZXk +ltd0ZGtxTgI8qoXzIKNDOXZbbFD+mpwUHmUUihW9o4JFWklWatKcsWMy5WHgUyIO +pwpJ6st+H6jiYoD2EEVSmAYY3qXNL3+q1Ok+CHLsIwMCPKaq2LxndD0UF/tUSxfj +03k9bWtJySgOLnRQvwzZRjoQhsmnP+mg7H/rpXdYaXHmgwo38oZJar55CJD2AhZk +PuXaTH4MNMn5X7azKFGnpyuqSfqNZSlO42sTp5SjLVFteAxEy9/eCG/Oo2Sr05WE +1LlSVHJ7liXMvGnjSG4N0MedJ5qq+BOS3R7fY581qRY27Iy4g/Q9iY/NtBde17MX +QRBdJ3NghVdJIgc= +-----END CERTIFICATE----- + +# Issuer: CN=Trustwave Global Certification Authority O=Trustwave Holdings, Inc. +# Subject: CN=Trustwave Global Certification Authority O=Trustwave Holdings, Inc. +# Label: "Trustwave Global Certification Authority" +# Serial: 1846098327275375458322922162 +# MD5 Fingerprint: f8:1c:18:2d:2f:ba:5f:6d:a1:6c:bc:c7:ab:91:c7:0e +# SHA1 Fingerprint: 2f:8f:36:4f:e1:58:97:44:21:59:87:a5:2a:9a:d0:69:95:26:7f:b5 +# SHA256 Fingerprint: 97:55:20:15:f5:dd:fc:3c:87:88:c0:06:94:45:55:40:88:94:45:00:84:f1:00:86:70:86:bc:1a:2b:b5:8d:c8 +-----BEGIN CERTIFICATE----- +MIIF2jCCA8KgAwIBAgIMBfcOhtpJ80Y1LrqyMA0GCSqGSIb3DQEBCwUAMIGIMQsw +CQYDVQQGEwJVUzERMA8GA1UECAwISWxsaW5vaXMxEDAOBgNVBAcMB0NoaWNhZ28x +ITAfBgNVBAoMGFRydXN0d2F2ZSBIb2xkaW5ncywgSW5jLjExMC8GA1UEAwwoVHJ1 +c3R3YXZlIEdsb2JhbCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0xNzA4MjMx +OTM0MTJaFw00MjA4MjMxOTM0MTJaMIGIMQswCQYDVQQGEwJVUzERMA8GA1UECAwI +SWxsaW5vaXMxEDAOBgNVBAcMB0NoaWNhZ28xITAfBgNVBAoMGFRydXN0d2F2ZSBI +b2xkaW5ncywgSW5jLjExMC8GA1UEAwwoVHJ1c3R3YXZlIEdsb2JhbCBDZXJ0aWZp +Y2F0aW9uIEF1dGhvcml0eTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIB +ALldUShLPDeS0YLOvR29zd24q88KPuFd5dyqCblXAj7mY2Hf8g+CY66j96xz0Xzn +swuvCAAJWX/NKSqIk4cXGIDtiLK0thAfLdZfVaITXdHG6wZWiYj+rDKd/VzDBcdu +7oaJuogDnXIhhpCujwOl3J+IKMujkkkP7NAP4m1ET4BqstTnoApTAbqOl5F2brz8 +1Ws25kCI1nsvXwXoLG0R8+eyvpJETNKXpP7ScoFDB5zpET71ixpZfR9oWN0EACyW +80OzfpgZdNmcc9kYvkHHNHnZ9GLCQ7mzJ7Aiy/k9UscwR7PJPrhq4ufogXBeQotP +JqX+OsIgbrv4Fo7NDKm0G2x2EOFYeUY+VM6AqFcJNykbmROPDMjWLBz7BegIlT1l +RtzuzWniTY+HKE40Cz7PFNm73bZQmq131BnW2hqIyE4bJ3XYsgjxroMwuREOzYfw +hI0Vcnyh78zyiGG69Gm7DIwLdVcEuE4qFC49DxweMqZiNu5m4iK4BUBjECLzMx10 +coos9TkpoNPnG4CELcU9402x/RpvumUHO1jsQkUm+9jaJXLE9gCxInm943xZYkqc +BW89zubWR2OZxiRvchLIrH+QtAuRcOi35hYQcRfO3gZPSEF9NUqjifLJS3tBEW1n +twiYTOURGa5CgNz7kAXU+FDKvuStx8KU1xad5hePrzb7AgMBAAGjQjBAMA8GA1Ud +EwEB/wQFMAMBAf8wHQYDVR0OBBYEFJngGWcNYtt2s9o9uFvo/ULSMQ6HMA4GA1Ud +DwEB/wQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAgEAmHNw4rDT7TnsTGDZqRKGFx6W +0OhUKDtkLSGm+J1WE2pIPU/HPinbbViDVD2HfSMF1OQc3Og4ZYbFdada2zUFvXfe +uyk3QAUHw5RSn8pk3fEbK9xGChACMf1KaA0HZJDmHvUqoai7PF35owgLEQzxPy0Q +lG/+4jSHg9bP5Rs1bdID4bANqKCqRieCNqcVtgimQlRXtpla4gt5kNdXElE1GYhB +aCXUNxeEFfsBctyV3lImIJgm4nb1J2/6ADtKYdkNy1GTKv0WBpanI5ojSP5RvbbE +sLFUzt5sQa0WZ37b/TjNuThOssFgy50X31ieemKyJo90lZvkWx3SD92YHJtZuSPT +MaCm/zjdzyBP6VhWOmfD0faZmZ26NraAL4hHT4a/RDqA5Dccprrql5gR0IRiR2Qe +qu5AvzSxnI9O4fKSTx+O856X3vOmeWqJcU9LJxdI/uz0UA9PSX3MReO9ekDFQdxh +VicGaeVyQYHTtgGJoC86cnn+OjC/QezHYj6RS8fZMXZC+fc8Y+wmjHMMfRod6qh8 +h6jCJ3zhM0EPz8/8AKAigJ5Kp28AsEFFtyLKaEjFQqKu3R3y4G5OBVixwJAWKqQ9 +EEC+j2Jjg6mcgn0tAumDMHzLJ8n9HmYAsC7TIS+OMxZsmO0QqAfWzJPP29FpHOTK +yeC2nOnOcXHebD8WpHk= +-----END CERTIFICATE----- + +# Issuer: CN=Trustwave Global ECC P256 Certification Authority O=Trustwave Holdings, Inc. +# Subject: CN=Trustwave Global ECC P256 Certification Authority O=Trustwave Holdings, Inc. +# Label: "Trustwave Global ECC P256 Certification Authority" +# Serial: 4151900041497450638097112925 +# MD5 Fingerprint: 5b:44:e3:8d:5d:36:86:26:e8:0d:05:d2:59:a7:83:54 +# SHA1 Fingerprint: b4:90:82:dd:45:0c:be:8b:5b:b1:66:d3:e2:a4:08:26:cd:ed:42:cf +# SHA256 Fingerprint: 94:5b:bc:82:5e:a5:54:f4:89:d1:fd:51:a7:3d:df:2e:a6:24:ac:70:19:a0:52:05:22:5c:22:a7:8c:cf:a8:b4 +-----BEGIN CERTIFICATE----- +MIICYDCCAgegAwIBAgIMDWpfCD8oXD5Rld9dMAoGCCqGSM49BAMCMIGRMQswCQYD +VQQGEwJVUzERMA8GA1UECBMISWxsaW5vaXMxEDAOBgNVBAcTB0NoaWNhZ28xITAf +BgNVBAoTGFRydXN0d2F2ZSBIb2xkaW5ncywgSW5jLjE6MDgGA1UEAxMxVHJ1c3R3 +YXZlIEdsb2JhbCBFQ0MgUDI1NiBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0x +NzA4MjMxOTM1MTBaFw00MjA4MjMxOTM1MTBaMIGRMQswCQYDVQQGEwJVUzERMA8G +A1UECBMISWxsaW5vaXMxEDAOBgNVBAcTB0NoaWNhZ28xITAfBgNVBAoTGFRydXN0 +d2F2ZSBIb2xkaW5ncywgSW5jLjE6MDgGA1UEAxMxVHJ1c3R3YXZlIEdsb2JhbCBF +Q0MgUDI1NiBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTBZMBMGByqGSM49AgEGCCqG +SM49AwEHA0IABH77bOYj43MyCMpg5lOcunSNGLB4kFKA3TjASh3RqMyTpJcGOMoN +FWLGjgEqZZ2q3zSRLoHB5DOSMcT9CTqmP62jQzBBMA8GA1UdEwEB/wQFMAMBAf8w +DwYDVR0PAQH/BAUDAwcGADAdBgNVHQ4EFgQUo0EGrJBt0UrrdaVKEJmzsaGLSvcw +CgYIKoZIzj0EAwIDRwAwRAIgB+ZU2g6gWrKuEZ+Hxbb/ad4lvvigtwjzRM4q3wgh +DDcCIC0mA6AFvWvR9lz4ZcyGbbOcNEhjhAnFjXca4syc4XR7 +-----END CERTIFICATE----- + +# Issuer: CN=Trustwave Global ECC P384 Certification Authority O=Trustwave Holdings, Inc. +# Subject: CN=Trustwave Global ECC P384 Certification Authority O=Trustwave Holdings, Inc. +# Label: "Trustwave Global ECC P384 Certification Authority" +# Serial: 2704997926503831671788816187 +# MD5 Fingerprint: ea:cf:60:c4:3b:b9:15:29:40:a1:97:ed:78:27:93:d6 +# SHA1 Fingerprint: e7:f3:a3:c8:cf:6f:c3:04:2e:6d:0e:67:32:c5:9e:68:95:0d:5e:d2 +# SHA256 Fingerprint: 55:90:38:59:c8:c0:c3:eb:b8:75:9e:ce:4e:25:57:22:5f:f5:75:8b:bd:38:eb:d4:82:76:60:1e:1b:d5:80:97 +-----BEGIN CERTIFICATE----- +MIICnTCCAiSgAwIBAgIMCL2Fl2yZJ6SAaEc7MAoGCCqGSM49BAMDMIGRMQswCQYD +VQQGEwJVUzERMA8GA1UECBMISWxsaW5vaXMxEDAOBgNVBAcTB0NoaWNhZ28xITAf +BgNVBAoTGFRydXN0d2F2ZSBIb2xkaW5ncywgSW5jLjE6MDgGA1UEAxMxVHJ1c3R3 +YXZlIEdsb2JhbCBFQ0MgUDM4NCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0x +NzA4MjMxOTM2NDNaFw00MjA4MjMxOTM2NDNaMIGRMQswCQYDVQQGEwJVUzERMA8G +A1UECBMISWxsaW5vaXMxEDAOBgNVBAcTB0NoaWNhZ28xITAfBgNVBAoTGFRydXN0 +d2F2ZSBIb2xkaW5ncywgSW5jLjE6MDgGA1UEAxMxVHJ1c3R3YXZlIEdsb2JhbCBF +Q0MgUDM4NCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTB2MBAGByqGSM49AgEGBSuB +BAAiA2IABGvaDXU1CDFHBa5FmVXxERMuSvgQMSOjfoPTfygIOiYaOs+Xgh+AtycJ +j9GOMMQKmw6sWASr9zZ9lCOkmwqKi6vr/TklZvFe/oyujUF5nQlgziip04pt89ZF +1PKYhDhloKNDMEEwDwYDVR0TAQH/BAUwAwEB/zAPBgNVHQ8BAf8EBQMDBwYAMB0G +A1UdDgQWBBRVqYSJ0sEyvRjLbKYHTsjnnb6CkDAKBggqhkjOPQQDAwNnADBkAjA3 +AZKXRRJ+oPM+rRk6ct30UJMDEr5E0k9BpIycnR+j9sKS50gU/k6bpZFXrsY3crsC +MGclCrEMXu6pY5Jv5ZAL/mYiykf9ijH3g/56vxC+GCsej/YpHpRZ744hN8tRmKVu +Sw== +-----END CERTIFICATE----- + +# Issuer: CN=NAVER Global Root Certification Authority O=NAVER BUSINESS PLATFORM Corp. +# Subject: CN=NAVER Global Root Certification Authority O=NAVER BUSINESS PLATFORM Corp. +# Label: "NAVER Global Root Certification Authority" +# Serial: 9013692873798656336226253319739695165984492813 +# MD5 Fingerprint: c8:7e:41:f6:25:3b:f5:09:b3:17:e8:46:3d:bf:d0:9b +# SHA1 Fingerprint: 8f:6b:f2:a9:27:4a:da:14:a0:c4:f4:8e:61:27:f9:c0:1e:78:5d:d1 +# SHA256 Fingerprint: 88:f4:38:dc:f8:ff:d1:fa:8f:42:91:15:ff:e5:f8:2a:e1:e0:6e:0c:70:c3:75:fa:ad:71:7b:34:a4:9e:72:65 +-----BEGIN CERTIFICATE----- +MIIFojCCA4qgAwIBAgIUAZQwHqIL3fXFMyqxQ0Rx+NZQTQ0wDQYJKoZIhvcNAQEM +BQAwaTELMAkGA1UEBhMCS1IxJjAkBgNVBAoMHU5BVkVSIEJVU0lORVNTIFBMQVRG +T1JNIENvcnAuMTIwMAYDVQQDDClOQVZFUiBHbG9iYWwgUm9vdCBDZXJ0aWZpY2F0 +aW9uIEF1dGhvcml0eTAeFw0xNzA4MTgwODU4NDJaFw0zNzA4MTgyMzU5NTlaMGkx +CzAJBgNVBAYTAktSMSYwJAYDVQQKDB1OQVZFUiBCVVNJTkVTUyBQTEFURk9STSBD +b3JwLjEyMDAGA1UEAwwpTkFWRVIgR2xvYmFsIFJvb3QgQ2VydGlmaWNhdGlvbiBB +dXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC21PGTXLVA +iQqrDZBbUGOukJR0F0Vy1ntlWilLp1agS7gvQnXp2XskWjFlqxcX0TM62RHcQDaH +38dq6SZeWYp34+hInDEW+j6RscrJo+KfziFTowI2MMtSAuXaMl3Dxeb57hHHi8lE +HoSTGEq0n+USZGnQJoViAbbJAh2+g1G7XNr4rRVqmfeSVPc0W+m/6imBEtRTkZaz +kVrd/pBzKPswRrXKCAfHcXLJZtM0l/aM9BhK4dA9WkW2aacp+yPOiNgSnABIqKYP +szuSjXEOdMWLyEz59JuOuDxp7W87UC9Y7cSw0BwbagzivESq2M0UXZR4Yb8Obtoq +vC8MC3GmsxY/nOb5zJ9TNeIDoKAYv7vxvvTWjIcNQvcGufFt7QSUqP620wbGQGHf +nZ3zVHbOUzoBppJB7ASjjw2i1QnK1sua8e9DXcCrpUHPXFNwcMmIpi3Ua2FzUCaG +YQ5fG8Ir4ozVu53BA0K6lNpfqbDKzE0K70dpAy8i+/Eozr9dUGWokG2zdLAIx6yo +0es+nPxdGoMuK8u180SdOqcXYZaicdNwlhVNt0xz7hlcxVs+Qf6sdWA7G2POAN3a +CJBitOUt7kinaxeZVL6HSuOpXgRM6xBtVNbv8ejyYhbLgGvtPe31HzClrkvJE+2K +AQHJuFFYwGY6sWZLxNUxAmLpdIQM201GLQIDAQABo0IwQDAdBgNVHQ4EFgQU0p+I +36HNLL3s9TsBAZMzJ7LrYEswDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMB +Af8wDQYJKoZIhvcNAQEMBQADggIBADLKgLOdPVQG3dLSLvCkASELZ0jKbY7gyKoN +qo0hV4/GPnrK21HUUrPUloSlWGB/5QuOH/XcChWB5Tu2tyIvCZwTFrFsDDUIbatj +cu3cvuzHV+YwIHHW1xDBE1UBjCpD5EHxzzp6U5LOogMFDTjfArsQLtk70pt6wKGm ++LUx5vR1yblTmXVHIloUFcd4G7ad6Qz4G3bxhYTeodoS76TiEJd6eN4MUZeoIUCL +hr0N8F5OSza7OyAfikJW4Qsav3vQIkMsRIz75Sq0bBwcupTgE34h5prCy8VCZLQe +lHsIJchxzIdFV4XTnyliIoNRlwAYl3dqmJLJfGBs32x9SuRwTMKeuB330DTHD8z7 +p/8Dvq1wkNoL3chtl1+afwkyQf3NosxabUzyqkn+Zvjp2DXrDige7kgvOtB5CTh8 +piKCk5XQA76+AqAF3SAi428diDRgxuYKuQl1C/AH6GmWNcf7I4GOODm4RStDeKLR +LBT/DShycpWbXgnbiUSYqqFJu3FS8r/2/yehNq+4tneI3TqkbZs0kNwUXTC/t+sX +5Ie3cdCh13cV1ELX8vMxmV2b3RZtP+oGI/hGoiLtk/bdmuYqh7GYVPEi92tF4+KO +dh2ajcQGjTa3FPOdVGm3jjzVpG2Tgbet9r1ke8LJaDmgkpzNNIaRkPpkUZ3+/uul +9XXeifdy +-----END CERTIFICATE----- + +# Issuer: CN=AC RAIZ FNMT-RCM SERVIDORES SEGUROS O=FNMT-RCM OU=Ceres +# Subject: CN=AC RAIZ FNMT-RCM SERVIDORES SEGUROS O=FNMT-RCM OU=Ceres +# Label: "AC RAIZ FNMT-RCM SERVIDORES SEGUROS" +# Serial: 131542671362353147877283741781055151509 +# MD5 Fingerprint: 19:36:9c:52:03:2f:d2:d1:bb:23:cc:dd:1e:12:55:bb +# SHA1 Fingerprint: 62:ff:d9:9e:c0:65:0d:03:ce:75:93:d2:ed:3f:2d:32:c9:e3:e5:4a +# SHA256 Fingerprint: 55:41:53:b1:3d:2c:f9:dd:b7:53:bf:be:1a:4e:0a:e0:8d:0a:a4:18:70:58:fe:60:a2:b8:62:b2:e4:b8:7b:cb +-----BEGIN CERTIFICATE----- +MIICbjCCAfOgAwIBAgIQYvYybOXE42hcG2LdnC6dlTAKBggqhkjOPQQDAzB4MQsw +CQYDVQQGEwJFUzERMA8GA1UECgwIRk5NVC1SQ00xDjAMBgNVBAsMBUNlcmVzMRgw +FgYDVQRhDA9WQVRFUy1RMjgyNjAwNEoxLDAqBgNVBAMMI0FDIFJBSVogRk5NVC1S +Q00gU0VSVklET1JFUyBTRUdVUk9TMB4XDTE4MTIyMDA5MzczM1oXDTQzMTIyMDA5 +MzczM1oweDELMAkGA1UEBhMCRVMxETAPBgNVBAoMCEZOTVQtUkNNMQ4wDAYDVQQL +DAVDZXJlczEYMBYGA1UEYQwPVkFURVMtUTI4MjYwMDRKMSwwKgYDVQQDDCNBQyBS +QUlaIEZOTVQtUkNNIFNFUlZJRE9SRVMgU0VHVVJPUzB2MBAGByqGSM49AgEGBSuB +BAAiA2IABPa6V1PIyqvfNkpSIeSX0oNnnvBlUdBeh8dHsVnyV0ebAAKTRBdp20LH +sbI6GA60XYyzZl2hNPk2LEnb80b8s0RpRBNm/dfF/a82Tc4DTQdxz69qBdKiQ1oK +Um8BA06Oi6NCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYD +VR0OBBYEFAG5L++/EYZg8k/QQW6rcx/n0m5JMAoGCCqGSM49BAMDA2kAMGYCMQCu +SuMrQMN0EfKVrRYj3k4MGuZdpSRea0R7/DjiT8ucRRcRTBQnJlU5dUoDzBOQn5IC +MQD6SmxgiHPz7riYYqnOK8LZiqZwMR2vsJRM60/G49HzYqc8/5MuB1xJAWdpEgJy +v+c= +-----END CERTIFICATE----- + +# Issuer: CN=GlobalSign Root R46 O=GlobalSign nv-sa +# Subject: CN=GlobalSign Root R46 O=GlobalSign nv-sa +# Label: "GlobalSign Root R46" +# Serial: 1552617688466950547958867513931858518042577 +# MD5 Fingerprint: c4:14:30:e4:fa:66:43:94:2a:6a:1b:24:5f:19:d0:ef +# SHA1 Fingerprint: 53:a2:b0:4b:ca:6b:d6:45:e6:39:8a:8e:c4:0d:d2:bf:77:c3:a2:90 +# SHA256 Fingerprint: 4f:a3:12:6d:8d:3a:11:d1:c4:85:5a:4f:80:7c:ba:d6:cf:91:9d:3a:5a:88:b0:3b:ea:2c:63:72:d9:3c:40:c9 +-----BEGIN CERTIFICATE----- +MIIFWjCCA0KgAwIBAgISEdK7udcjGJ5AXwqdLdDfJWfRMA0GCSqGSIb3DQEBDAUA +MEYxCzAJBgNVBAYTAkJFMRkwFwYDVQQKExBHbG9iYWxTaWduIG52LXNhMRwwGgYD +VQQDExNHbG9iYWxTaWduIFJvb3QgUjQ2MB4XDTE5MDMyMDAwMDAwMFoXDTQ2MDMy +MDAwMDAwMFowRjELMAkGA1UEBhMCQkUxGTAXBgNVBAoTEEdsb2JhbFNpZ24gbnYt +c2ExHDAaBgNVBAMTE0dsb2JhbFNpZ24gUm9vdCBSNDYwggIiMA0GCSqGSIb3DQEB +AQUAA4ICDwAwggIKAoICAQCsrHQy6LNl5brtQyYdpokNRbopiLKkHWPd08EsCVeJ +OaFV6Wc0dwxu5FUdUiXSE2te4R2pt32JMl8Nnp8semNgQB+msLZ4j5lUlghYruQG +vGIFAha/r6gjA7aUD7xubMLL1aa7DOn2wQL7Id5m3RerdELv8HQvJfTqa1VbkNud +316HCkD7rRlr+/fKYIje2sGP1q7Vf9Q8g+7XFkyDRTNrJ9CG0Bwta/OrffGFqfUo +0q3v84RLHIf8E6M6cqJaESvWJ3En7YEtbWaBkoe0G1h6zD8K+kZPTXhc+CtI4wSE +y132tGqzZfxCnlEmIyDLPRT5ge1lFgBPGmSXZgjPjHvjK8Cd+RTyG/FWaha/LIWF +zXg4mutCagI0GIMXTpRW+LaCtfOW3T3zvn8gdz57GSNrLNRyc0NXfeD412lPFzYE ++cCQYDdF3uYM2HSNrpyibXRdQr4G9dlkbgIQrImwTDsHTUB+JMWKmIJ5jqSngiCN +I/onccnfxkF0oE32kRbcRoxfKWMxWXEM2G/CtjJ9++ZdU6Z+Ffy7dXxd7Pj2Fxzs +x2sZy/N78CsHpdlseVR2bJ0cpm4O6XkMqCNqo98bMDGfsVR7/mrLZqrcZdCinkqa +ByFrgY/bxFn63iLABJzjqls2k+g9vXqhnQt2sQvHnf3PmKgGwvgqo6GDoLclcqUC +4wIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNV +HQ4EFgQUA1yrc4GHqMywptWU4jaWSf8FmSwwDQYJKoZIhvcNAQEMBQADggIBAHx4 +7PYCLLtbfpIrXTncvtgdokIzTfnvpCo7RGkerNlFo048p9gkUbJUHJNOxO97k4Vg +JuoJSOD1u8fpaNK7ajFxzHmuEajwmf3lH7wvqMxX63bEIaZHU1VNaL8FpO7XJqti +2kM3S+LGteWygxk6x9PbTZ4IevPuzz5i+6zoYMzRx6Fcg0XERczzF2sUyQQCPtIk +pnnpHs6i58FZFZ8d4kuaPp92CC1r2LpXFNqD6v6MVenQTqnMdzGxRBF6XLE+0xRF +FRhiJBPSy03OXIPBNvIQtQ6IbbjhVp+J3pZmOUdkLG5NrmJ7v2B0GbhWrJKsFjLt +rWhV/pi60zTe9Mlhww6G9kuEYO4Ne7UyWHmRVSyBQ7N0H3qqJZ4d16GLuc1CLgSk +ZoNNiTW2bKg2SnkheCLQQrzRQDGQob4Ez8pn7fXwgNNgyYMqIgXQBztSvwyeqiv5 +u+YfjyW6hY0XHgL+XVAEV8/+LbzvXMAaq7afJMbfc2hIkCwU9D9SGuTSyxTDYWnP +4vkYxboznxSjBF25cfe1lNj2M8FawTSLfJvdkzrnE6JwYZ+vj+vYxXX4M2bUdGc6 +N3ec592kD3ZDZopD8p/7DEJ4Y9HiD2971KE9dJeFt0g5QdYg/NA6s/rob8SKunE3 +vouXsXgxT7PntgMTzlSdriVZzH81Xwj3QEUxeCp6 +-----END CERTIFICATE----- + +# Issuer: CN=GlobalSign Root E46 O=GlobalSign nv-sa +# Subject: CN=GlobalSign Root E46 O=GlobalSign nv-sa +# Label: "GlobalSign Root E46" +# Serial: 1552617690338932563915843282459653771421763 +# MD5 Fingerprint: b5:b8:66:ed:de:08:83:e3:c9:e2:01:34:06:ac:51:6f +# SHA1 Fingerprint: 39:b4:6c:d5:fe:80:06:eb:e2:2f:4a:bb:08:33:a0:af:db:b9:dd:84 +# SHA256 Fingerprint: cb:b9:c4:4d:84:b8:04:3e:10:50:ea:31:a6:9f:51:49:55:d7:bf:d2:e2:c6:b4:93:01:01:9a:d6:1d:9f:50:58 +-----BEGIN CERTIFICATE----- +MIICCzCCAZGgAwIBAgISEdK7ujNu1LzmJGjFDYQdmOhDMAoGCCqGSM49BAMDMEYx +CzAJBgNVBAYTAkJFMRkwFwYDVQQKExBHbG9iYWxTaWduIG52LXNhMRwwGgYDVQQD +ExNHbG9iYWxTaWduIFJvb3QgRTQ2MB4XDTE5MDMyMDAwMDAwMFoXDTQ2MDMyMDAw +MDAwMFowRjELMAkGA1UEBhMCQkUxGTAXBgNVBAoTEEdsb2JhbFNpZ24gbnYtc2Ex +HDAaBgNVBAMTE0dsb2JhbFNpZ24gUm9vdCBFNDYwdjAQBgcqhkjOPQIBBgUrgQQA +IgNiAAScDrHPt+ieUnd1NPqlRqetMhkytAepJ8qUuwzSChDH2omwlwxwEwkBjtjq +R+q+soArzfwoDdusvKSGN+1wCAB16pMLey5SnCNoIwZD7JIvU4Tb+0cUB+hflGdd +yXqBPCCjQjBAMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1Ud +DgQWBBQxCpCPtsad0kRLgLWi5h+xEk8blTAKBggqhkjOPQQDAwNoADBlAjEA31SQ +7Zvvi5QCkxeCmb6zniz2C5GMn0oUsfZkvLtoURMMA/cVi4RguYv/Uo7njLwcAjA8 ++RHUjE7AwWHCFUyqqx0LMV87HOIAl0Qx5v5zli/altP+CAezNIm8BZ/3Hobui3A= +-----END CERTIFICATE----- + +# Issuer: CN=GLOBALTRUST 2020 O=e-commerce monitoring GmbH +# Subject: CN=GLOBALTRUST 2020 O=e-commerce monitoring GmbH +# Label: "GLOBALTRUST 2020" +# Serial: 109160994242082918454945253 +# MD5 Fingerprint: 8a:c7:6f:cb:6d:e3:cc:a2:f1:7c:83:fa:0e:78:d7:e8 +# SHA1 Fingerprint: d0:67:c1:13:51:01:0c:aa:d0:c7:6a:65:37:31:16:26:4f:53:71:a2 +# SHA256 Fingerprint: 9a:29:6a:51:82:d1:d4:51:a2:e3:7f:43:9b:74:da:af:a2:67:52:33:29:f9:0f:9a:0d:20:07:c3:34:e2:3c:9a +-----BEGIN CERTIFICATE----- +MIIFgjCCA2qgAwIBAgILWku9WvtPilv6ZeUwDQYJKoZIhvcNAQELBQAwTTELMAkG +A1UEBhMCQVQxIzAhBgNVBAoTGmUtY29tbWVyY2UgbW9uaXRvcmluZyBHbWJIMRkw +FwYDVQQDExBHTE9CQUxUUlVTVCAyMDIwMB4XDTIwMDIxMDAwMDAwMFoXDTQwMDYx +MDAwMDAwMFowTTELMAkGA1UEBhMCQVQxIzAhBgNVBAoTGmUtY29tbWVyY2UgbW9u +aXRvcmluZyBHbWJIMRkwFwYDVQQDExBHTE9CQUxUUlVTVCAyMDIwMIICIjANBgkq +hkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAri5WrRsc7/aVj6B3GyvTY4+ETUWiD59b +RatZe1E0+eyLinjF3WuvvcTfk0Uev5E4C64OFudBc/jbu9G4UeDLgztzOG53ig9Z +YybNpyrOVPu44sB8R85gfD+yc/LAGbaKkoc1DZAoouQVBGM+uq/ufF7MpotQsjj3 +QWPKzv9pj2gOlTblzLmMCcpL3TGQlsjMH/1WljTbjhzqLL6FLmPdqqmV0/0plRPw +yJiT2S0WR5ARg6I6IqIoV6Lr/sCMKKCmfecqQjuCgGOlYx8ZzHyyZqjC0203b+J+ +BlHZRYQfEs4kUmSFC0iAToexIiIwquuuvuAC4EDosEKAA1GqtH6qRNdDYfOiaxaJ +SaSjpCuKAsR49GiKweR6NrFvG5Ybd0mN1MkGco/PU+PcF4UgStyYJ9ORJitHHmkH +r96i5OTUawuzXnzUJIBHKWk7buis/UDr2O1xcSvy6Fgd60GXIsUf1DnQJ4+H4xj0 +4KlGDfV0OoIu0G4skaMxXDtG6nsEEFZegB31pWXogvziB4xiRfUg3kZwhqG8k9Me +dKZssCz3AwyIDMvUclOGvGBG85hqwvG/Q/lwIHfKN0F5VVJjjVsSn8VoxIidrPIw +q7ejMZdnrY8XD2zHc+0klGvIg5rQmjdJBKuxFshsSUktq6HQjJLyQUp5ISXbY9e2 +nKd+Qmn7OmMCAwEAAaNjMGEwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC +AQYwHQYDVR0OBBYEFNwuH9FhN3nkq9XVsxJxaD1qaJwiMB8GA1UdIwQYMBaAFNwu +H9FhN3nkq9XVsxJxaD1qaJwiMA0GCSqGSIb3DQEBCwUAA4ICAQCR8EICaEDuw2jA +VC/f7GLDw56KoDEoqoOOpFaWEhCGVrqXctJUMHytGdUdaG/7FELYjQ7ztdGl4wJC +XtzoRlgHNQIw4Lx0SsFDKv/bGtCwr2zD/cuz9X9tAy5ZVp0tLTWMstZDFyySCstd +6IwPS3BD0IL/qMy/pJTAvoe9iuOTe8aPmxadJ2W8esVCgmxcB9CpwYhgROmYhRZf ++I/KARDOJcP5YBugxZfD0yyIMaK9MOzQ0MAS8cE54+X1+NZK3TTN+2/BT+MAi1bi +kvcoskJ3ciNnxz8RFbLEAwW+uxF7Cr+obuf/WEPPm2eggAe2HcqtbepBEX4tdJP7 +wry+UUTF72glJ4DjyKDUEuzZpTcdN3y0kcra1LGWge9oXHYQSa9+pTeAsRxSvTOB +TI/53WXZFM2KJVj04sWDpQmQ1GwUY7VA3+vA/MRYfg0UFodUJ25W5HCEuGwyEn6C +MUO+1918oa2u1qsgEu8KwxCMSZY13At1XrFP1U80DhEgB3VDRemjEdqso5nCtnkn +4rnvyOL2NSl6dPrFf4IFYqYK6miyeUcGbvJXqBUzxvd4Sj1Ce2t+/vdG6tHrju+I +aFvowdlxfv1k7/9nR4hYJS8+hge9+6jlgqispdNpQ80xiEmEU5LAsTkbOYMBMMTy +qfrQA71yN2BWHzZ8vTmR9W0Nv3vXkg== +-----END CERTIFICATE----- + +# Issuer: CN=ANF Secure Server Root CA O=ANF Autoridad de Certificacion OU=ANF CA Raiz +# Subject: CN=ANF Secure Server Root CA O=ANF Autoridad de Certificacion OU=ANF CA Raiz +# Label: "ANF Secure Server Root CA" +# Serial: 996390341000653745 +# MD5 Fingerprint: 26:a6:44:5a:d9:af:4e:2f:b2:1d:b6:65:b0:4e:e8:96 +# SHA1 Fingerprint: 5b:6e:68:d0:cc:15:b6:a0:5f:1e:c1:5f:ae:02:fc:6b:2f:5d:6f:74 +# SHA256 Fingerprint: fb:8f:ec:75:91:69:b9:10:6b:1e:51:16:44:c6:18:c5:13:04:37:3f:6c:06:43:08:8d:8b:ef:fd:1b:99:75:99 +-----BEGIN CERTIFICATE----- +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 +-----END CERTIFICATE----- + +# Issuer: CN=Certum EC-384 CA O=Asseco Data Systems S.A. OU=Certum Certification Authority +# Subject: CN=Certum EC-384 CA O=Asseco Data Systems S.A. OU=Certum Certification Authority +# Label: "Certum EC-384 CA" +# Serial: 160250656287871593594747141429395092468 +# MD5 Fingerprint: b6:65:b3:96:60:97:12:a1:ec:4e:e1:3d:a3:c6:c9:f1 +# SHA1 Fingerprint: f3:3e:78:3c:ac:df:f4:a2:cc:ac:67:55:69:56:d7:e5:16:3c:e1:ed +# SHA256 Fingerprint: 6b:32:80:85:62:53:18:aa:50:d1:73:c9:8d:8b:da:09:d5:7e:27:41:3d:11:4c:f7:87:a0:f5:d0:6c:03:0c:f6 +-----BEGIN CERTIFICATE----- +MIICZTCCAeugAwIBAgIQeI8nXIESUiClBNAt3bpz9DAKBggqhkjOPQQDAzB0MQsw +CQYDVQQGEwJQTDEhMB8GA1UEChMYQXNzZWNvIERhdGEgU3lzdGVtcyBTLkEuMScw +JQYDVQQLEx5DZXJ0dW0gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkxGTAXBgNVBAMT +EENlcnR1bSBFQy0zODQgQ0EwHhcNMTgwMzI2MDcyNDU0WhcNNDMwMzI2MDcyNDU0 +WjB0MQswCQYDVQQGEwJQTDEhMB8GA1UEChMYQXNzZWNvIERhdGEgU3lzdGVtcyBT +LkEuMScwJQYDVQQLEx5DZXJ0dW0gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkxGTAX +BgNVBAMTEENlcnR1bSBFQy0zODQgQ0EwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAATE +KI6rGFtqvm5kN2PkzeyrOvfMobgOgknXhimfoZTy42B4mIF4Bk3y7JoOV2CDn7Tm +Fy8as10CW4kjPMIRBSqniBMY81CE1700LCeJVf/OTOffph8oxPBUw7l8t1Ot68Kj +QjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFI0GZnQkdjrzife81r1HfS+8 +EF9LMA4GA1UdDwEB/wQEAwIBBjAKBggqhkjOPQQDAwNoADBlAjADVS2m5hjEfO/J +UG7BJw+ch69u1RsIGL2SKcHvlJF40jocVYli5RsJHrpka/F2tNQCMQC0QoSZ/6vn +nvuRlydd3LBbMHHOXjgaatkl5+r3YZJW+OraNsKHZZYuciUvf9/DE8k= +-----END CERTIFICATE----- + +# Issuer: CN=Certum Trusted Root CA O=Asseco Data Systems S.A. OU=Certum Certification Authority +# Subject: CN=Certum Trusted Root CA O=Asseco Data Systems S.A. OU=Certum Certification Authority +# Label: "Certum Trusted Root CA" +# Serial: 40870380103424195783807378461123655149 +# MD5 Fingerprint: 51:e1:c2:e7:fe:4c:84:af:59:0e:2f:f4:54:6f:ea:29 +# SHA1 Fingerprint: c8:83:44:c0:18:ae:9f:cc:f1:87:b7:8f:22:d1:c5:d7:45:84:ba:e5 +# SHA256 Fingerprint: fe:76:96:57:38:55:77:3e:37:a9:5e:7a:d4:d9:cc:96:c3:01:57:c1:5d:31:76:5b:a9:b1:57:04:e1:ae:78:fd +-----BEGIN CERTIFICATE----- +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 +-----END CERTIFICATE----- + +# Issuer: CN=TunTrust Root CA O=Agence Nationale de Certification Electronique +# Subject: CN=TunTrust Root CA O=Agence Nationale de Certification Electronique +# Label: "TunTrust Root CA" +# Serial: 108534058042236574382096126452369648152337120275 +# MD5 Fingerprint: 85:13:b9:90:5b:36:5c:b6:5e:b8:5a:f8:e0:31:57:b4 +# SHA1 Fingerprint: cf:e9:70:84:0f:e0:73:0f:9d:f6:0c:7f:2c:4b:ee:20:46:34:9c:bb +# SHA256 Fingerprint: 2e:44:10:2a:b5:8c:b8:54:19:45:1c:8e:19:d9:ac:f3:66:2c:af:bc:61:4b:6a:53:96:0a:30:f7:d0:e2:eb:41 +-----BEGIN CERTIFICATE----- +MIIFszCCA5ugAwIBAgIUEwLV4kBMkkaGFmddtLu7sms+/BMwDQYJKoZIhvcNAQEL +BQAwYTELMAkGA1UEBhMCVE4xNzA1BgNVBAoMLkFnZW5jZSBOYXRpb25hbGUgZGUg +Q2VydGlmaWNhdGlvbiBFbGVjdHJvbmlxdWUxGTAXBgNVBAMMEFR1blRydXN0IFJv +b3QgQ0EwHhcNMTkwNDI2MDg1NzU2WhcNNDQwNDI2MDg1NzU2WjBhMQswCQYDVQQG +EwJUTjE3MDUGA1UECgwuQWdlbmNlIE5hdGlvbmFsZSBkZSBDZXJ0aWZpY2F0aW9u +IEVsZWN0cm9uaXF1ZTEZMBcGA1UEAwwQVHVuVHJ1c3QgUm9vdCBDQTCCAiIwDQYJ +KoZIhvcNAQEBBQADggIPADCCAgoCggIBAMPN0/y9BFPdDCA61YguBUtB9YOCfvdZ +n56eY+hz2vYGqU8ftPkLHzmMmiDQfgbU7DTZhrx1W4eI8NLZ1KMKsmwb60ksPqxd +2JQDoOw05TDENX37Jk0bbjBU2PWARZw5rZzJJQRNmpA+TkBuimvNKWfGzC3gdOgF +VwpIUPp6Q9p+7FuaDmJ2/uqdHYVy7BG7NegfJ7/Boce7SBbdVtfMTqDhuazb1YMZ +GoXRlJfXyqNlC/M4+QKu3fZnz8k/9YosRxqZbwUN/dAdgjH8KcwAWJeRTIAAHDOF +li/LQcKLEITDCSSJH7UP2dl3RxiSlGBcx5kDPP73lad9UKGAwqmDrViWVSHbhlnU +r8a83YFuB9tgYv7sEG7aaAH0gxupPqJbI9dkxt/con3YS7qC0lH4Zr8GRuR5KiY2 +eY8fTpkdso8MDhz/yV3A/ZAQprE38806JG60hZC/gLkMjNWb1sjxVj8agIl6qeIb +MlEsPvLfe/ZdeikZjuXIvTZxi11Mwh0/rViizz1wTaZQmCXcI/m4WEEIcb9PuISg +jwBUFfyRbVinljvrS5YnzWuioYasDXxU5mZMZl+QviGaAkYt5IPCgLnPSz7ofzwB +7I9ezX/SKEIBlYrilz0QIX32nRzFNKHsLA4KUiwSVXAkPcvCFDVDXSdOvsC9qnyW +5/yeYa1E0wCXAgMBAAGjYzBhMB0GA1UdDgQWBBQGmpsfU33x9aTI04Y+oXNZtPdE +ITAPBgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFAaamx9TffH1pMjThj6hc1m0 +90QhMA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAgEAqgVutt0Vyb+z +xiD2BkewhpMl0425yAA/l/VSJ4hxyXT968pk21vvHl26v9Hr7lxpuhbI87mP0zYu +QEkHDVneixCwSQXi/5E/S7fdAo74gShczNxtr18UnH1YeA32gAm56Q6XKRm4t+v4 +FstVEuTGfbvE7Pi1HE4+Z7/FXxttbUcoqgRYYdZ2vyJ/0Adqp2RT8JeNnYA/u8EH +22Wv5psymsNUk8QcCMNE+3tjEUPRahphanltkE8pjkcFwRJpadbGNjHh/PqAulxP +xOu3Mqz4dWEX1xAZufHSCe96Qp1bWgvUxpVOKs7/B9dPfhgGiPEZtdmYu65xxBzn +dFlY7wyJz4sfdZMaBBSSSFCp61cpABbjNhzI+L/wM9VBD8TMPN3pM0MBkRArHtG5 +Xc0yGYuPjCB31yLEQtyEFpslbei0VXF/sHyz03FJuc9SpAQ/3D2gu68zngowYI7b +nV2UqL1g52KAdoGDDIzMMEZJ4gzSqK/rYXHv5yJiqfdcZGyfFoxnNidF9Ql7v/YQ +CvGwjVRDjAS6oz/v4jXH+XTgbzRB0L9zZVcg+ZtnemZoJE6AZb0QmQZZ8mWvuMZH +u/2QeItBcy6vVR/cO5JyboTT0GFMDcx2V+IthSIVNg3rAZ3r2OvEhJn7wAzMMujj +d9qDRIueVSjAi1jTkD5OGwDxFa2DK5o= +-----END CERTIFICATE----- + +# Issuer: CN=HARICA TLS RSA Root CA 2021 O=Hellenic Academic and Research Institutions CA +# Subject: CN=HARICA TLS RSA Root CA 2021 O=Hellenic Academic and Research Institutions CA +# Label: "HARICA TLS RSA Root CA 2021" +# Serial: 76817823531813593706434026085292783742 +# MD5 Fingerprint: 65:47:9b:58:86:dd:2c:f0:fc:a2:84:1f:1e:96:c4:91 +# SHA1 Fingerprint: 02:2d:05:82:fa:88:ce:14:0c:06:79:de:7f:14:10:e9:45:d7:a5:6d +# SHA256 Fingerprint: d9:5d:0e:8e:da:79:52:5b:f9:be:b1:1b:14:d2:10:0d:32:94:98:5f:0c:62:d9:fa:bd:9c:d9:99:ec:cb:7b:1d +-----BEGIN CERTIFICATE----- +MIIFpDCCA4ygAwIBAgIQOcqTHO9D88aOk8f0ZIk4fjANBgkqhkiG9w0BAQsFADBs +MQswCQYDVQQGEwJHUjE3MDUGA1UECgwuSGVsbGVuaWMgQWNhZGVtaWMgYW5kIFJl +c2VhcmNoIEluc3RpdHV0aW9ucyBDQTEkMCIGA1UEAwwbSEFSSUNBIFRMUyBSU0Eg +Um9vdCBDQSAyMDIxMB4XDTIxMDIxOTEwNTUzOFoXDTQ1MDIxMzEwNTUzN1owbDEL +MAkGA1UEBhMCR1IxNzA1BgNVBAoMLkhlbGxlbmljIEFjYWRlbWljIGFuZCBSZXNl +YXJjaCBJbnN0aXR1dGlvbnMgQ0ExJDAiBgNVBAMMG0hBUklDQSBUTFMgUlNBIFJv +b3QgQ0EgMjAyMTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAIvC569l +mwVnlskNJLnQDmT8zuIkGCyEf3dRywQRNrhe7Wlxp57kJQmXZ8FHws+RFjZiPTgE +4VGC/6zStGndLuwRo0Xua2s7TL+MjaQenRG56Tj5eg4MmOIjHdFOY9TnuEFE+2uv +a9of08WRiFukiZLRgeaMOVig1mlDqa2YUlhu2wr7a89o+uOkXjpFc5gH6l8Cct4M +pbOfrqkdtx2z/IpZ525yZa31MJQjB/OCFks1mJxTuy/K5FrZx40d/JiZ+yykgmvw +Kh+OC19xXFyuQnspiYHLA6OZyoieC0AJQTPb5lh6/a6ZcMBaD9YThnEvdmn8kN3b +LW7R8pv1GmuebxWMevBLKKAiOIAkbDakO/IwkfN4E8/BPzWr8R0RI7VDIp4BkrcY +AuUR0YLbFQDMYTfBKnya4dC6s1BG7oKsnTH4+yPiAwBIcKMJJnkVU2DzOFytOOqB +AGMUuTNe3QvboEUHGjMJ+E20pwKmafTCWQWIZYVWrkvL4N48fS0ayOn7H6NhStYq +E613TBoYm5EPWNgGVMWX+Ko/IIqmhaZ39qb8HOLubpQzKoNQhArlT4b4UEV4AIHr +W2jjJo3Me1xR9BQsQL4aYB16cmEdH2MtiKrOokWQCPxrvrNQKlr9qEgYRtaQQJKQ +CoReaDH46+0N0x3GfZkYVVYnZS6NRcUk7M7jAgMBAAGjQjBAMA8GA1UdEwEB/wQF +MAMBAf8wHQYDVR0OBBYEFApII6ZgpJIKM+qTW8VX6iVNvRLuMA4GA1UdDwEB/wQE +AwIBhjANBgkqhkiG9w0BAQsFAAOCAgEAPpBIqm5iFSVmewzVjIuJndftTgfvnNAU +X15QvWiWkKQUEapobQk1OUAJ2vQJLDSle1mESSmXdMgHHkdt8s4cUCbjnj1AUz/3 +f5Z2EMVGpdAgS1D0NTsY9FVqQRtHBmg8uwkIYtlfVUKqrFOFrJVWNlar5AWMxaja +H6NpvVMPxP/cyuN+8kyIhkdGGvMA9YCRotxDQpSbIPDRzbLrLFPCU3hKTwSUQZqP +JzLB5UkZv/HywouoCjkxKLR9YjYsTewfM7Z+d21+UPCfDtcRj88YxeMn/ibvBZ3P +zzfF0HvaO7AWhAw6k9a+F9sPPg4ZeAnHqQJyIkv3N3a6dcSFA1pj1bF1BcK5vZSt +jBWZp5N99sXzqnTPBIWUmAD04vnKJGW/4GKvyMX6ssmeVkjaef2WdhW+o45WxLM0 +/L5H9MG0qPzVMIho7suuyWPEdr6sOBjhXlzPrjoiUevRi7PzKzMHVIf6tLITe7pT +BGIBnfHAT+7hOtSLIBD6Alfm78ELt5BGnBkpjNxvoEppaZS3JGWg/6w/zgH7IS79 +aPib8qXPMThcFarmlwDB31qlpzmq6YR/PFGoOtmUW4y/Twhx5duoXNTSpv4Ao8YW +xw/ogM4cKGR0GQjTQuPOAF1/sdwTsOEFy9EgqoZ0njnnkf3/W9b3raYvAwtt41dU +63ZTGI0RmLo= +-----END CERTIFICATE----- + +# Issuer: CN=HARICA TLS ECC Root CA 2021 O=Hellenic Academic and Research Institutions CA +# Subject: CN=HARICA TLS ECC Root CA 2021 O=Hellenic Academic and Research Institutions CA +# Label: "HARICA TLS ECC Root CA 2021" +# Serial: 137515985548005187474074462014555733966 +# MD5 Fingerprint: ae:f7:4c:e5:66:35:d1:b7:9b:8c:22:93:74:d3:4b:b0 +# SHA1 Fingerprint: bc:b0:c1:9d:e9:98:92:70:19:38:57:e9:8d:a7:b4:5d:6e:ee:01:48 +# SHA256 Fingerprint: 3f:99:cc:47:4a:cf:ce:4d:fe:d5:87:94:66:5e:47:8d:15:47:73:9f:2e:78:0f:1b:b4:ca:9b:13:30:97:d4:01 +-----BEGIN CERTIFICATE----- +MIICVDCCAdugAwIBAgIQZ3SdjXfYO2rbIvT/WeK/zjAKBggqhkjOPQQDAzBsMQsw +CQYDVQQGEwJHUjE3MDUGA1UECgwuSGVsbGVuaWMgQWNhZGVtaWMgYW5kIFJlc2Vh +cmNoIEluc3RpdHV0aW9ucyBDQTEkMCIGA1UEAwwbSEFSSUNBIFRMUyBFQ0MgUm9v +dCBDQSAyMDIxMB4XDTIxMDIxOTExMDExMFoXDTQ1MDIxMzExMDEwOVowbDELMAkG +A1UEBhMCR1IxNzA1BgNVBAoMLkhlbGxlbmljIEFjYWRlbWljIGFuZCBSZXNlYXJj +aCBJbnN0aXR1dGlvbnMgQ0ExJDAiBgNVBAMMG0hBUklDQSBUTFMgRUNDIFJvb3Qg +Q0EgMjAyMTB2MBAGByqGSM49AgEGBSuBBAAiA2IABDgI/rGgltJ6rK9JOtDA4MM7 +KKrxcm1lAEeIhPyaJmuqS7psBAqIXhfyVYf8MLA04jRYVxqEU+kw2anylnTDUR9Y +STHMmE5gEYd103KUkE+bECUqqHgtvpBBWJAVcqeht6NCMEAwDwYDVR0TAQH/BAUw +AwEB/zAdBgNVHQ4EFgQUyRtTgRL+BNUW0aq8mm+3oJUZbsowDgYDVR0PAQH/BAQD +AgGGMAoGCCqGSM49BAMDA2cAMGQCMBHervjcToiwqfAircJRQO9gcS3ujwLEXQNw +SaSS6sUUiHCm0w2wqsosQJz76YJumgIwK0eaB8bRwoF8yguWGEEbo/QwCZ61IygN +nxS2PFOiTAZpffpskcYqSUXm7LcT4Tps +-----END CERTIFICATE----- diff --git a/libs/certifi/core.py b/libs/certifi/core.py index 7271acf40..5d2b8cd32 100644 --- a/libs/certifi/core.py +++ b/libs/certifi/core.py @@ -4,12 +4,57 @@ certifi.py ~~~~~~~~~~ -This module returns the installation location of cacert.pem. +This module returns the installation location of cacert.pem or its contents. """ import os +try: + from importlib.resources import path as get_path, read_text -def where(): - f = os.path.dirname(__file__) + _CACERT_CTX = None + _CACERT_PATH = None - return os.path.join(f, 'cacert.pem') + def where(): + # This is slightly terrible, but we want to delay extracting the file + # in cases where we're inside of a zipimport situation until someone + # actually calls where(), but we don't want to re-extract the file + # on every call of where(), so we'll do it once then store it in a + # global variable. + global _CACERT_CTX + global _CACERT_PATH + if _CACERT_PATH is None: + # This is slightly janky, the importlib.resources API wants you to + # manage the cleanup of this file, so it doesn't actually return a + # path, it returns a context manager that will give you the path + # when you enter it and will do any cleanup when you leave it. In + # the common case of not needing a temporary file, it will just + # return the file system location and the __exit__() is a no-op. + # + # We also have to hold onto the actual context manager, because + # it will do the cleanup whenever it gets garbage collected, so + # we will also store that at the global level as well. + _CACERT_CTX = get_path("certifi", "cacert.pem") + _CACERT_PATH = str(_CACERT_CTX.__enter__()) + + return _CACERT_PATH + + +except ImportError: + # This fallback will work for Python versions prior to 3.7 that lack the + # importlib.resources module but relies on the existing `where` function + # so won't address issues with environments like PyOxidizer that don't set + # __file__ on modules. + def read_text(_module, _path, encoding="ascii"): + with open(where(), "r", encoding=encoding) as data: + return data.read() + + # If we don't have importlib.resources, then we will just do the old logic + # of assuming we're on the filesystem and munge the path directly. + def where(): + f = os.path.dirname(__file__) + + return os.path.join(f, "cacert.pem") + + +def contents(): + return read_text("certifi", "cacert.pem", encoding="ascii") diff --git a/libs/chardet/__init__.py b/libs/chardet/__init__.py index 0f9f820ef..80ad2546d 100644 --- a/libs/chardet/__init__.py +++ b/libs/chardet/__init__.py @@ -16,11 +16,14 @@ ######################### END LICENSE BLOCK ######################### -from .compat import PY2, PY3 from .universaldetector import UniversalDetector +from .enums import InputState from .version import __version__, VERSION +__all__ = ['UniversalDetector', 'detect', 'detect_all', '__version__', 'VERSION'] + + def detect(byte_str): """ Detect the encoding of the given byte string. @@ -31,9 +34,50 @@ def detect(byte_str): if not isinstance(byte_str, bytearray): if not isinstance(byte_str, bytes): raise TypeError('Expected object of type bytes or bytearray, got: ' - '{0}'.format(type(byte_str))) + '{}'.format(type(byte_str))) else: byte_str = bytearray(byte_str) detector = UniversalDetector() detector.feed(byte_str) return detector.close() + + +def detect_all(byte_str): + """ + Detect all the possible encodings of the given byte string. + + :param byte_str: The byte sequence to examine. + :type byte_str: ``bytes`` or ``bytearray`` + """ + if not isinstance(byte_str, bytearray): + if not isinstance(byte_str, bytes): + raise TypeError('Expected object of type bytes or bytearray, got: ' + '{}'.format(type(byte_str))) + else: + byte_str = bytearray(byte_str) + + detector = UniversalDetector() + detector.feed(byte_str) + detector.close() + + if detector._input_state == InputState.HIGH_BYTE: + results = [] + for prober in detector._charset_probers: + if prober.get_confidence() > detector.MINIMUM_THRESHOLD: + charset_name = prober.charset_name + lower_charset_name = prober.charset_name.lower() + # Use Windows encoding name instead of ISO-8859 if we saw any + # extra Windows-specific bytes + if lower_charset_name.startswith('iso-8859'): + if detector._has_win_bytes: + charset_name = detector.ISO_WIN_MAP.get(lower_charset_name, + charset_name) + results.append({ + 'encoding': charset_name, + 'confidence': prober.get_confidence(), + 'language': prober.language, + }) + if len(results) > 0: + return sorted(results, key=lambda result: -result['confidence']) + + return [detector.result] diff --git a/libs/chardet/charsetgroupprober.py b/libs/chardet/charsetgroupprober.py index 8b3738efd..5812cef0b 100644 --- a/libs/chardet/charsetgroupprober.py +++ b/libs/chardet/charsetgroupprober.py @@ -73,6 +73,7 @@ class CharSetGroupProber(CharSetProber): continue if state == ProbingState.FOUND_IT: self._best_guess_prober = prober + self._state = ProbingState.FOUND_IT return self.state elif state == ProbingState.NOT_ME: prober.active = False diff --git a/libs/chardet/cli/chardetect.py b/libs/chardet/cli/chardetect.py index f0a4cc5d7..e1d8cd69a 100644 --- a/libs/chardet/cli/chardetect.py +++ b/libs/chardet/cli/chardetect.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python """ Script which takes one or more file paths and reports on their detected encodings @@ -45,10 +44,10 @@ def description_of(lines, name='stdin'): if PY2: name = name.decode(sys.getfilesystemencoding(), 'ignore') if result['encoding']: - return '{0}: {1} with confidence {2}'.format(name, result['encoding'], + return '{}: {} with confidence {}'.format(name, result['encoding'], result['confidence']) else: - return '{0}: no result'.format(name) + return '{}: no result'.format(name) def main(argv=None): @@ -69,7 +68,7 @@ def main(argv=None): type=argparse.FileType('rb'), nargs='*', default=[sys.stdin if PY2 else sys.stdin.buffer]) parser.add_argument('--version', action='version', - version='%(prog)s {0}'.format(__version__)) + version='%(prog)s {}'.format(__version__)) args = parser.parse_args(argv) for f in args.input: diff --git a/libs/chardet/compat.py b/libs/chardet/compat.py index ddd74687c..8941572b3 100644 --- a/libs/chardet/compat.py +++ b/libs/chardet/compat.py @@ -25,10 +25,12 @@ import sys if sys.version_info < (3, 0): PY2 = True PY3 = False - base_str = (str, unicode) + string_types = (str, unicode) text_type = unicode + iteritems = dict.iteritems else: PY2 = False PY3 = True - base_str = (bytes, str) + string_types = (bytes, str) text_type = str + iteritems = dict.items diff --git a/libs/chardet/langbulgarianmodel.py b/libs/chardet/langbulgarianmodel.py index 2aa4fb2e2..561bfd905 100644 --- a/libs/chardet/langbulgarianmodel.py +++ b/libs/chardet/langbulgarianmodel.py @@ -1,228 +1,4650 @@ -######################## BEGIN LICENSE BLOCK ######################## -# The Original Code is Mozilla Communicator client code. -# -# The Initial Developer of the Original Code is -# Netscape Communications Corporation. -# Portions created by the Initial Developer are Copyright (C) 1998 -# the Initial Developer. All Rights Reserved. -# -# Contributor(s): -# Mark Pilgrim - port to Python -# -# This library is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 2.1 of the License, or (at your option) any later version. -# -# This library is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with this library; if not, write to the Free Software -# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA -# 02110-1301 USA -######################### END LICENSE BLOCK ######################### +#!/usr/bin/env python +# -*- coding: utf-8 -*- -# 255: Control characters that usually does not exist in any text +from chardet.sbcharsetprober import SingleByteCharSetModel + + +# 3: Positive +# 2: Likely +# 1: Unlikely +# 0: Negative + +BULGARIAN_LANG_MODEL = { + 63: { # 'e' + 63: 1, # 'e' + 45: 0, # '\xad' + 31: 0, # 'Ð' + 32: 0, # 'Б' + 35: 0, # 'Ð’' + 43: 0, # 'Г' + 37: 0, # 'Д' + 44: 0, # 'Е' + 55: 0, # 'Ж' + 47: 0, # 'З' + 40: 0, # 'И' + 59: 0, # 'Й' + 33: 0, # 'К' + 46: 0, # 'Л' + 38: 0, # 'Ðœ' + 36: 0, # 'Ð' + 41: 0, # 'О' + 30: 0, # 'П' + 39: 0, # 'Р' + 28: 0, # 'С' + 34: 0, # 'Т' + 51: 0, # 'У' + 48: 0, # 'Ф' + 49: 0, # 'Ð¥' + 53: 0, # 'Ц' + 50: 0, # 'Ч' + 54: 0, # 'Ш' + 57: 0, # 'Щ' + 61: 0, # 'Ъ' + 60: 0, # 'Ю' + 56: 0, # 'Я' + 1: 0, # 'а' + 18: 1, # 'б' + 9: 1, # 'в' + 20: 1, # 'г' + 11: 1, # 'д' + 3: 1, # 'е' + 23: 1, # 'ж' + 15: 1, # 'з' + 2: 0, # 'и' + 26: 1, # 'й' + 12: 1, # 'к' + 10: 1, # 'л' + 14: 1, # 'м' + 6: 1, # 'н' + 4: 1, # 'о' + 13: 1, # 'п' + 7: 1, # 'Ñ€' + 8: 1, # 'Ñ' + 5: 1, # 'Ñ‚' + 19: 0, # 'у' + 29: 1, # 'Ñ„' + 25: 1, # 'Ñ…' + 22: 0, # 'ц' + 21: 1, # 'ч' + 27: 1, # 'ш' + 24: 1, # 'щ' + 17: 0, # 'ÑŠ' + 52: 0, # 'ÑŒ' + 42: 0, # 'ÑŽ' + 16: 1, # 'Ñ' + 58: 0, # 'Ñ”' + 62: 0, # 'â„–' + }, + 45: { # '\xad' + 63: 0, # 'e' + 45: 0, # '\xad' + 31: 0, # 'Ð' + 32: 1, # 'Б' + 35: 1, # 'Ð’' + 43: 0, # 'Г' + 37: 1, # 'Д' + 44: 0, # 'Е' + 55: 0, # 'Ж' + 47: 0, # 'З' + 40: 1, # 'И' + 59: 0, # 'Й' + 33: 1, # 'К' + 46: 0, # 'Л' + 38: 1, # 'Ðœ' + 36: 0, # 'Ð' + 41: 1, # 'О' + 30: 1, # 'П' + 39: 1, # 'Р' + 28: 1, # 'С' + 34: 0, # 'Т' + 51: 0, # 'У' + 48: 0, # 'Ф' + 49: 1, # 'Ð¥' + 53: 0, # 'Ц' + 50: 0, # 'Ч' + 54: 0, # 'Ш' + 57: 0, # 'Щ' + 61: 0, # 'Ъ' + 60: 0, # 'Ю' + 56: 0, # 'Я' + 1: 0, # 'а' + 18: 0, # 'б' + 9: 0, # 'в' + 20: 0, # 'г' + 11: 0, # 'д' + 3: 0, # 'е' + 23: 0, # 'ж' + 15: 0, # 'з' + 2: 0, # 'и' + 26: 0, # 'й' + 12: 0, # 'к' + 10: 0, # 'л' + 14: 0, # 'м' + 6: 0, # 'н' + 4: 0, # 'о' + 13: 0, # 'п' + 7: 0, # 'Ñ€' + 8: 0, # 'Ñ' + 5: 0, # 'Ñ‚' + 19: 0, # 'у' + 29: 0, # 'Ñ„' + 25: 0, # 'Ñ…' + 22: 0, # 'ц' + 21: 0, # 'ч' + 27: 0, # 'ш' + 24: 0, # 'щ' + 17: 0, # 'ÑŠ' + 52: 0, # 'ÑŒ' + 42: 0, # 'ÑŽ' + 16: 0, # 'Ñ' + 58: 0, # 'Ñ”' + 62: 0, # 'â„–' + }, + 31: { # 'Ð' + 63: 0, # 'e' + 45: 1, # '\xad' + 31: 1, # 'Ð' + 32: 1, # 'Б' + 35: 2, # 'Ð’' + 43: 1, # 'Г' + 37: 2, # 'Д' + 44: 2, # 'Е' + 55: 1, # 'Ж' + 47: 2, # 'З' + 40: 1, # 'И' + 59: 1, # 'Й' + 33: 1, # 'К' + 46: 2, # 'Л' + 38: 1, # 'Ðœ' + 36: 2, # 'Ð' + 41: 1, # 'О' + 30: 2, # 'П' + 39: 2, # 'Р' + 28: 2, # 'С' + 34: 2, # 'Т' + 51: 1, # 'У' + 48: 2, # 'Ф' + 49: 1, # 'Ð¥' + 53: 1, # 'Ц' + 50: 1, # 'Ч' + 54: 1, # 'Ш' + 57: 2, # 'Щ' + 61: 0, # 'Ъ' + 60: 0, # 'Ю' + 56: 1, # 'Я' + 1: 1, # 'а' + 18: 2, # 'б' + 9: 2, # 'в' + 20: 2, # 'г' + 11: 2, # 'д' + 3: 1, # 'е' + 23: 1, # 'ж' + 15: 2, # 'з' + 2: 0, # 'и' + 26: 2, # 'й' + 12: 2, # 'к' + 10: 3, # 'л' + 14: 2, # 'м' + 6: 3, # 'н' + 4: 0, # 'о' + 13: 2, # 'п' + 7: 2, # 'Ñ€' + 8: 2, # 'Ñ' + 5: 2, # 'Ñ‚' + 19: 1, # 'у' + 29: 2, # 'Ñ„' + 25: 1, # 'Ñ…' + 22: 1, # 'ц' + 21: 1, # 'ч' + 27: 1, # 'ш' + 24: 0, # 'щ' + 17: 0, # 'ÑŠ' + 52: 0, # 'ÑŒ' + 42: 0, # 'ÑŽ' + 16: 1, # 'Ñ' + 58: 0, # 'Ñ”' + 62: 0, # 'â„–' + }, + 32: { # 'Б' + 63: 0, # 'e' + 45: 0, # '\xad' + 31: 2, # 'Ð' + 32: 2, # 'Б' + 35: 1, # 'Ð’' + 43: 1, # 'Г' + 37: 2, # 'Д' + 44: 1, # 'Е' + 55: 1, # 'Ж' + 47: 2, # 'З' + 40: 1, # 'И' + 59: 0, # 'Й' + 33: 1, # 'К' + 46: 1, # 'Л' + 38: 1, # 'Ðœ' + 36: 2, # 'Ð' + 41: 2, # 'О' + 30: 1, # 'П' + 39: 1, # 'Р' + 28: 2, # 'С' + 34: 2, # 'Т' + 51: 1, # 'У' + 48: 2, # 'Ф' + 49: 1, # 'Ð¥' + 53: 1, # 'Ц' + 50: 1, # 'Ч' + 54: 0, # 'Ш' + 57: 1, # 'Щ' + 61: 2, # 'Ъ' + 60: 1, # 'Ю' + 56: 1, # 'Я' + 1: 3, # 'а' + 18: 0, # 'б' + 9: 0, # 'в' + 20: 0, # 'г' + 11: 1, # 'д' + 3: 3, # 'е' + 23: 0, # 'ж' + 15: 0, # 'з' + 2: 2, # 'и' + 26: 0, # 'й' + 12: 0, # 'к' + 10: 2, # 'л' + 14: 0, # 'м' + 6: 0, # 'н' + 4: 3, # 'о' + 13: 0, # 'п' + 7: 2, # 'Ñ€' + 8: 1, # 'Ñ' + 5: 0, # 'Ñ‚' + 19: 2, # 'у' + 29: 0, # 'Ñ„' + 25: 1, # 'Ñ…' + 22: 0, # 'ц' + 21: 0, # 'ч' + 27: 0, # 'ш' + 24: 0, # 'щ' + 17: 3, # 'ÑŠ' + 52: 1, # 'ÑŒ' + 42: 1, # 'ÑŽ' + 16: 2, # 'Ñ' + 58: 0, # 'Ñ”' + 62: 0, # 'â„–' + }, + 35: { # 'Ð’' + 63: 0, # 'e' + 45: 0, # '\xad' + 31: 2, # 'Ð' + 32: 1, # 'Б' + 35: 1, # 'Ð’' + 43: 0, # 'Г' + 37: 1, # 'Д' + 44: 2, # 'Е' + 55: 0, # 'Ж' + 47: 0, # 'З' + 40: 2, # 'И' + 59: 0, # 'Й' + 33: 1, # 'К' + 46: 1, # 'Л' + 38: 1, # 'Ðœ' + 36: 1, # 'Ð' + 41: 1, # 'О' + 30: 1, # 'П' + 39: 2, # 'Р' + 28: 2, # 'С' + 34: 1, # 'Т' + 51: 1, # 'У' + 48: 2, # 'Ф' + 49: 0, # 'Ð¥' + 53: 1, # 'Ц' + 50: 0, # 'Ч' + 54: 0, # 'Ш' + 57: 0, # 'Щ' + 61: 1, # 'Ъ' + 60: 1, # 'Ю' + 56: 2, # 'Я' + 1: 3, # 'а' + 18: 1, # 'б' + 9: 0, # 'в' + 20: 0, # 'г' + 11: 1, # 'д' + 3: 3, # 'е' + 23: 1, # 'ж' + 15: 2, # 'з' + 2: 3, # 'и' + 26: 0, # 'й' + 12: 1, # 'к' + 10: 2, # 'л' + 14: 1, # 'м' + 6: 2, # 'н' + 4: 2, # 'о' + 13: 1, # 'п' + 7: 2, # 'Ñ€' + 8: 2, # 'Ñ' + 5: 2, # 'Ñ‚' + 19: 1, # 'у' + 29: 0, # 'Ñ„' + 25: 1, # 'Ñ…' + 22: 0, # 'ц' + 21: 2, # 'ч' + 27: 0, # 'ш' + 24: 0, # 'щ' + 17: 2, # 'ÑŠ' + 52: 1, # 'ÑŒ' + 42: 1, # 'ÑŽ' + 16: 1, # 'Ñ' + 58: 0, # 'Ñ”' + 62: 0, # 'â„–' + }, + 43: { # 'Г' + 63: 0, # 'e' + 45: 0, # '\xad' + 31: 2, # 'Ð' + 32: 1, # 'Б' + 35: 0, # 'Ð’' + 43: 0, # 'Г' + 37: 1, # 'Д' + 44: 2, # 'Е' + 55: 0, # 'Ж' + 47: 1, # 'З' + 40: 1, # 'И' + 59: 0, # 'Й' + 33: 1, # 'К' + 46: 1, # 'Л' + 38: 0, # 'Ðœ' + 36: 1, # 'Ð' + 41: 1, # 'О' + 30: 0, # 'П' + 39: 1, # 'Р' + 28: 1, # 'С' + 34: 0, # 'Т' + 51: 1, # 'У' + 48: 1, # 'Ф' + 49: 0, # 'Ð¥' + 53: 0, # 'Ц' + 50: 0, # 'Ч' + 54: 0, # 'Ш' + 57: 1, # 'Щ' + 61: 1, # 'Ъ' + 60: 0, # 'Ю' + 56: 0, # 'Я' + 1: 2, # 'а' + 18: 1, # 'б' + 9: 1, # 'в' + 20: 0, # 'г' + 11: 1, # 'д' + 3: 3, # 'е' + 23: 1, # 'ж' + 15: 0, # 'з' + 2: 2, # 'и' + 26: 0, # 'й' + 12: 1, # 'к' + 10: 2, # 'л' + 14: 1, # 'м' + 6: 1, # 'н' + 4: 2, # 'о' + 13: 0, # 'п' + 7: 2, # 'Ñ€' + 8: 0, # 'Ñ' + 5: 0, # 'Ñ‚' + 19: 2, # 'у' + 29: 0, # 'Ñ„' + 25: 0, # 'Ñ…' + 22: 0, # 'ц' + 21: 0, # 'ч' + 27: 0, # 'ш' + 24: 1, # 'щ' + 17: 2, # 'ÑŠ' + 52: 1, # 'ÑŒ' + 42: 1, # 'ÑŽ' + 16: 1, # 'Ñ' + 58: 0, # 'Ñ”' + 62: 0, # 'â„–' + }, + 37: { # 'Д' + 63: 0, # 'e' + 45: 0, # '\xad' + 31: 2, # 'Ð' + 32: 1, # 'Б' + 35: 2, # 'Ð’' + 43: 1, # 'Г' + 37: 2, # 'Д' + 44: 2, # 'Е' + 55: 2, # 'Ж' + 47: 1, # 'З' + 40: 2, # 'И' + 59: 0, # 'Й' + 33: 1, # 'К' + 46: 1, # 'Л' + 38: 1, # 'Ðœ' + 36: 1, # 'Ð' + 41: 2, # 'О' + 30: 2, # 'П' + 39: 1, # 'Р' + 28: 2, # 'С' + 34: 1, # 'Т' + 51: 1, # 'У' + 48: 1, # 'Ф' + 49: 0, # 'Ð¥' + 53: 1, # 'Ц' + 50: 1, # 'Ч' + 54: 0, # 'Ш' + 57: 0, # 'Щ' + 61: 1, # 'Ъ' + 60: 1, # 'Ю' + 56: 1, # 'Я' + 1: 3, # 'а' + 18: 0, # 'б' + 9: 2, # 'в' + 20: 0, # 'г' + 11: 0, # 'д' + 3: 3, # 'е' + 23: 3, # 'ж' + 15: 1, # 'з' + 2: 3, # 'и' + 26: 0, # 'й' + 12: 0, # 'к' + 10: 1, # 'л' + 14: 1, # 'м' + 6: 2, # 'н' + 4: 3, # 'о' + 13: 0, # 'п' + 7: 2, # 'Ñ€' + 8: 0, # 'Ñ' + 5: 0, # 'Ñ‚' + 19: 2, # 'у' + 29: 0, # 'Ñ„' + 25: 0, # 'Ñ…' + 22: 0, # 'ц' + 21: 0, # 'ч' + 27: 0, # 'ш' + 24: 0, # 'щ' + 17: 2, # 'ÑŠ' + 52: 1, # 'ÑŒ' + 42: 2, # 'ÑŽ' + 16: 1, # 'Ñ' + 58: 0, # 'Ñ”' + 62: 0, # 'â„–' + }, + 44: { # 'Е' + 63: 0, # 'e' + 45: 0, # '\xad' + 31: 1, # 'Ð' + 32: 1, # 'Б' + 35: 2, # 'Ð’' + 43: 1, # 'Г' + 37: 1, # 'Д' + 44: 1, # 'Е' + 55: 1, # 'Ж' + 47: 1, # 'З' + 40: 1, # 'И' + 59: 1, # 'Й' + 33: 2, # 'К' + 46: 2, # 'Л' + 38: 1, # 'Ðœ' + 36: 2, # 'Ð' + 41: 2, # 'О' + 30: 1, # 'П' + 39: 2, # 'Р' + 28: 2, # 'С' + 34: 2, # 'Т' + 51: 1, # 'У' + 48: 2, # 'Ф' + 49: 1, # 'Ð¥' + 53: 2, # 'Ц' + 50: 1, # 'Ч' + 54: 1, # 'Ш' + 57: 1, # 'Щ' + 61: 0, # 'Ъ' + 60: 0, # 'Ю' + 56: 1, # 'Я' + 1: 0, # 'а' + 18: 1, # 'б' + 9: 2, # 'в' + 20: 1, # 'г' + 11: 2, # 'д' + 3: 0, # 'е' + 23: 1, # 'ж' + 15: 1, # 'з' + 2: 0, # 'и' + 26: 1, # 'й' + 12: 2, # 'к' + 10: 2, # 'л' + 14: 2, # 'м' + 6: 2, # 'н' + 4: 0, # 'о' + 13: 1, # 'п' + 7: 2, # 'Ñ€' + 8: 2, # 'Ñ' + 5: 1, # 'Ñ‚' + 19: 1, # 'у' + 29: 1, # 'Ñ„' + 25: 1, # 'Ñ…' + 22: 0, # 'ц' + 21: 1, # 'ч' + 27: 1, # 'ш' + 24: 1, # 'щ' + 17: 1, # 'ÑŠ' + 52: 0, # 'ÑŒ' + 42: 1, # 'ÑŽ' + 16: 1, # 'Ñ' + 58: 0, # 'Ñ”' + 62: 0, # 'â„–' + }, + 55: { # 'Ж' + 63: 0, # 'e' + 45: 0, # '\xad' + 31: 1, # 'Ð' + 32: 0, # 'Б' + 35: 1, # 'Ð’' + 43: 0, # 'Г' + 37: 1, # 'Д' + 44: 1, # 'Е' + 55: 0, # 'Ж' + 47: 0, # 'З' + 40: 1, # 'И' + 59: 0, # 'Й' + 33: 1, # 'К' + 46: 0, # 'Л' + 38: 0, # 'Ðœ' + 36: 1, # 'Ð' + 41: 1, # 'О' + 30: 0, # 'П' + 39: 0, # 'Р' + 28: 0, # 'С' + 34: 0, # 'Т' + 51: 1, # 'У' + 48: 0, # 'Ф' + 49: 0, # 'Ð¥' + 53: 0, # 'Ц' + 50: 0, # 'Ч' + 54: 0, # 'Ш' + 57: 0, # 'Щ' + 61: 0, # 'Ъ' + 60: 0, # 'Ю' + 56: 0, # 'Я' + 1: 2, # 'а' + 18: 0, # 'б' + 9: 0, # 'в' + 20: 0, # 'г' + 11: 1, # 'д' + 3: 2, # 'е' + 23: 0, # 'ж' + 15: 0, # 'з' + 2: 2, # 'и' + 26: 0, # 'й' + 12: 0, # 'к' + 10: 0, # 'л' + 14: 0, # 'м' + 6: 0, # 'н' + 4: 2, # 'о' + 13: 1, # 'п' + 7: 1, # 'Ñ€' + 8: 0, # 'Ñ' + 5: 0, # 'Ñ‚' + 19: 1, # 'у' + 29: 0, # 'Ñ„' + 25: 0, # 'Ñ…' + 22: 0, # 'ц' + 21: 0, # 'ч' + 27: 0, # 'ш' + 24: 0, # 'щ' + 17: 1, # 'ÑŠ' + 52: 1, # 'ÑŒ' + 42: 1, # 'ÑŽ' + 16: 0, # 'Ñ' + 58: 0, # 'Ñ”' + 62: 0, # 'â„–' + }, + 47: { # 'З' + 63: 0, # 'e' + 45: 0, # '\xad' + 31: 2, # 'Ð' + 32: 1, # 'Б' + 35: 1, # 'Ð’' + 43: 1, # 'Г' + 37: 1, # 'Д' + 44: 1, # 'Е' + 55: 0, # 'Ж' + 47: 1, # 'З' + 40: 1, # 'И' + 59: 0, # 'Й' + 33: 1, # 'К' + 46: 1, # 'Л' + 38: 1, # 'Ðœ' + 36: 2, # 'Ð' + 41: 1, # 'О' + 30: 1, # 'П' + 39: 1, # 'Р' + 28: 1, # 'С' + 34: 1, # 'Т' + 51: 1, # 'У' + 48: 0, # 'Ф' + 49: 1, # 'Ð¥' + 53: 1, # 'Ц' + 50: 0, # 'Ч' + 54: 0, # 'Ш' + 57: 0, # 'Щ' + 61: 1, # 'Ъ' + 60: 0, # 'Ю' + 56: 1, # 'Я' + 1: 3, # 'а' + 18: 1, # 'б' + 9: 2, # 'в' + 20: 1, # 'г' + 11: 2, # 'д' + 3: 2, # 'е' + 23: 0, # 'ж' + 15: 0, # 'з' + 2: 1, # 'и' + 26: 0, # 'й' + 12: 0, # 'к' + 10: 2, # 'л' + 14: 1, # 'м' + 6: 1, # 'н' + 4: 1, # 'о' + 13: 0, # 'п' + 7: 1, # 'Ñ€' + 8: 0, # 'Ñ' + 5: 0, # 'Ñ‚' + 19: 1, # 'у' + 29: 0, # 'Ñ„' + 25: 0, # 'Ñ…' + 22: 0, # 'ц' + 21: 0, # 'ч' + 27: 0, # 'ш' + 24: 0, # 'щ' + 17: 1, # 'ÑŠ' + 52: 0, # 'ÑŒ' + 42: 1, # 'ÑŽ' + 16: 0, # 'Ñ' + 58: 0, # 'Ñ”' + 62: 0, # 'â„–' + }, + 40: { # 'И' + 63: 0, # 'e' + 45: 1, # '\xad' + 31: 1, # 'Ð' + 32: 1, # 'Б' + 35: 1, # 'Ð’' + 43: 1, # 'Г' + 37: 1, # 'Д' + 44: 2, # 'Е' + 55: 1, # 'Ж' + 47: 2, # 'З' + 40: 1, # 'И' + 59: 1, # 'Й' + 33: 2, # 'К' + 46: 2, # 'Л' + 38: 2, # 'Ðœ' + 36: 2, # 'Ð' + 41: 1, # 'О' + 30: 1, # 'П' + 39: 2, # 'Р' + 28: 2, # 'С' + 34: 2, # 'Т' + 51: 0, # 'У' + 48: 1, # 'Ф' + 49: 1, # 'Ð¥' + 53: 1, # 'Ц' + 50: 1, # 'Ч' + 54: 1, # 'Ш' + 57: 1, # 'Щ' + 61: 0, # 'Ъ' + 60: 0, # 'Ю' + 56: 2, # 'Я' + 1: 1, # 'а' + 18: 1, # 'б' + 9: 3, # 'в' + 20: 2, # 'г' + 11: 1, # 'д' + 3: 1, # 'е' + 23: 0, # 'ж' + 15: 3, # 'з' + 2: 0, # 'и' + 26: 1, # 'й' + 12: 1, # 'к' + 10: 2, # 'л' + 14: 2, # 'м' + 6: 2, # 'н' + 4: 0, # 'о' + 13: 1, # 'п' + 7: 2, # 'Ñ€' + 8: 2, # 'Ñ' + 5: 2, # 'Ñ‚' + 19: 0, # 'у' + 29: 1, # 'Ñ„' + 25: 1, # 'Ñ…' + 22: 1, # 'ц' + 21: 1, # 'ч' + 27: 1, # 'ш' + 24: 1, # 'щ' + 17: 0, # 'ÑŠ' + 52: 0, # 'ÑŒ' + 42: 0, # 'ÑŽ' + 16: 0, # 'Ñ' + 58: 0, # 'Ñ”' + 62: 0, # 'â„–' + }, + 59: { # 'Й' + 63: 0, # 'e' + 45: 0, # '\xad' + 31: 0, # 'Ð' + 32: 0, # 'Б' + 35: 0, # 'Ð’' + 43: 0, # 'Г' + 37: 1, # 'Д' + 44: 1, # 'Е' + 55: 0, # 'Ж' + 47: 0, # 'З' + 40: 0, # 'И' + 59: 0, # 'Й' + 33: 1, # 'К' + 46: 1, # 'Л' + 38: 1, # 'Ðœ' + 36: 1, # 'Ð' + 41: 1, # 'О' + 30: 0, # 'П' + 39: 0, # 'Р' + 28: 1, # 'С' + 34: 1, # 'Т' + 51: 0, # 'У' + 48: 0, # 'Ф' + 49: 0, # 'Ð¥' + 53: 0, # 'Ц' + 50: 1, # 'Ч' + 54: 0, # 'Ш' + 57: 0, # 'Щ' + 61: 0, # 'Ъ' + 60: 0, # 'Ю' + 56: 1, # 'Я' + 1: 0, # 'а' + 18: 0, # 'б' + 9: 0, # 'в' + 20: 0, # 'г' + 11: 0, # 'д' + 3: 1, # 'е' + 23: 0, # 'ж' + 15: 0, # 'з' + 2: 0, # 'и' + 26: 0, # 'й' + 12: 0, # 'к' + 10: 0, # 'л' + 14: 0, # 'м' + 6: 0, # 'н' + 4: 2, # 'о' + 13: 0, # 'п' + 7: 0, # 'Ñ€' + 8: 0, # 'Ñ' + 5: 0, # 'Ñ‚' + 19: 0, # 'у' + 29: 0, # 'Ñ„' + 25: 0, # 'Ñ…' + 22: 0, # 'ц' + 21: 0, # 'ч' + 27: 0, # 'ш' + 24: 0, # 'щ' + 17: 1, # 'ÑŠ' + 52: 0, # 'ÑŒ' + 42: 0, # 'ÑŽ' + 16: 0, # 'Ñ' + 58: 0, # 'Ñ”' + 62: 0, # 'â„–' + }, + 33: { # 'К' + 63: 0, # 'e' + 45: 1, # '\xad' + 31: 2, # 'Ð' + 32: 1, # 'Б' + 35: 1, # 'Ð’' + 43: 1, # 'Г' + 37: 1, # 'Д' + 44: 1, # 'Е' + 55: 0, # 'Ж' + 47: 1, # 'З' + 40: 2, # 'И' + 59: 0, # 'Й' + 33: 1, # 'К' + 46: 1, # 'Л' + 38: 0, # 'Ðœ' + 36: 2, # 'Ð' + 41: 2, # 'О' + 30: 2, # 'П' + 39: 1, # 'Р' + 28: 2, # 'С' + 34: 1, # 'Т' + 51: 1, # 'У' + 48: 1, # 'Ф' + 49: 1, # 'Ð¥' + 53: 1, # 'Ц' + 50: 0, # 'Ч' + 54: 0, # 'Ш' + 57: 0, # 'Щ' + 61: 1, # 'Ъ' + 60: 1, # 'Ю' + 56: 0, # 'Я' + 1: 3, # 'а' + 18: 0, # 'б' + 9: 1, # 'в' + 20: 0, # 'г' + 11: 0, # 'д' + 3: 2, # 'е' + 23: 1, # 'ж' + 15: 0, # 'з' + 2: 2, # 'и' + 26: 0, # 'й' + 12: 0, # 'к' + 10: 2, # 'л' + 14: 1, # 'м' + 6: 2, # 'н' + 4: 3, # 'о' + 13: 0, # 'п' + 7: 3, # 'Ñ€' + 8: 1, # 'Ñ' + 5: 0, # 'Ñ‚' + 19: 2, # 'у' + 29: 0, # 'Ñ„' + 25: 1, # 'Ñ…' + 22: 0, # 'ц' + 21: 0, # 'ч' + 27: 1, # 'ш' + 24: 0, # 'щ' + 17: 2, # 'ÑŠ' + 52: 1, # 'ÑŒ' + 42: 2, # 'ÑŽ' + 16: 0, # 'Ñ' + 58: 0, # 'Ñ”' + 62: 0, # 'â„–' + }, + 46: { # 'Л' + 63: 1, # 'e' + 45: 0, # '\xad' + 31: 2, # 'Ð' + 32: 1, # 'Б' + 35: 1, # 'Ð’' + 43: 2, # 'Г' + 37: 1, # 'Д' + 44: 2, # 'Е' + 55: 0, # 'Ж' + 47: 1, # 'З' + 40: 2, # 'И' + 59: 0, # 'Й' + 33: 1, # 'К' + 46: 1, # 'Л' + 38: 0, # 'Ðœ' + 36: 1, # 'Ð' + 41: 2, # 'О' + 30: 1, # 'П' + 39: 0, # 'Р' + 28: 1, # 'С' + 34: 1, # 'Т' + 51: 1, # 'У' + 48: 0, # 'Ф' + 49: 1, # 'Ð¥' + 53: 1, # 'Ц' + 50: 1, # 'Ч' + 54: 0, # 'Ш' + 57: 0, # 'Щ' + 61: 1, # 'Ъ' + 60: 1, # 'Ю' + 56: 1, # 'Я' + 1: 2, # 'а' + 18: 0, # 'б' + 9: 1, # 'в' + 20: 0, # 'г' + 11: 0, # 'д' + 3: 3, # 'е' + 23: 0, # 'ж' + 15: 0, # 'з' + 2: 2, # 'и' + 26: 0, # 'й' + 12: 0, # 'к' + 10: 0, # 'л' + 14: 0, # 'м' + 6: 0, # 'н' + 4: 2, # 'о' + 13: 0, # 'п' + 7: 0, # 'Ñ€' + 8: 0, # 'Ñ' + 5: 0, # 'Ñ‚' + 19: 2, # 'у' + 29: 0, # 'Ñ„' + 25: 0, # 'Ñ…' + 22: 0, # 'ц' + 21: 0, # 'ч' + 27: 0, # 'ш' + 24: 0, # 'щ' + 17: 1, # 'ÑŠ' + 52: 1, # 'ÑŒ' + 42: 2, # 'ÑŽ' + 16: 1, # 'Ñ' + 58: 0, # 'Ñ”' + 62: 0, # 'â„–' + }, + 38: { # 'Ðœ' + 63: 0, # 'e' + 45: 0, # '\xad' + 31: 2, # 'Ð' + 32: 1, # 'Б' + 35: 2, # 'Ð’' + 43: 0, # 'Г' + 37: 1, # 'Д' + 44: 1, # 'Е' + 55: 0, # 'Ж' + 47: 1, # 'З' + 40: 2, # 'И' + 59: 0, # 'Й' + 33: 1, # 'К' + 46: 1, # 'Л' + 38: 1, # 'Ðœ' + 36: 1, # 'Ð' + 41: 2, # 'О' + 30: 1, # 'П' + 39: 1, # 'Р' + 28: 2, # 'С' + 34: 1, # 'Т' + 51: 1, # 'У' + 48: 1, # 'Ф' + 49: 0, # 'Ð¥' + 53: 1, # 'Ц' + 50: 0, # 'Ч' + 54: 0, # 'Ш' + 57: 0, # 'Щ' + 61: 1, # 'Ъ' + 60: 0, # 'Ю' + 56: 1, # 'Я' + 1: 3, # 'а' + 18: 0, # 'б' + 9: 0, # 'в' + 20: 0, # 'г' + 11: 0, # 'д' + 3: 3, # 'е' + 23: 0, # 'ж' + 15: 0, # 'з' + 2: 3, # 'и' + 26: 0, # 'й' + 12: 0, # 'к' + 10: 2, # 'л' + 14: 0, # 'м' + 6: 2, # 'н' + 4: 3, # 'о' + 13: 0, # 'п' + 7: 1, # 'Ñ€' + 8: 0, # 'Ñ' + 5: 0, # 'Ñ‚' + 19: 2, # 'у' + 29: 0, # 'Ñ„' + 25: 0, # 'Ñ…' + 22: 0, # 'ц' + 21: 0, # 'ч' + 27: 0, # 'ш' + 24: 0, # 'щ' + 17: 2, # 'ÑŠ' + 52: 1, # 'ÑŒ' + 42: 2, # 'ÑŽ' + 16: 1, # 'Ñ' + 58: 0, # 'Ñ”' + 62: 0, # 'â„–' + }, + 36: { # 'Ð' + 63: 0, # 'e' + 45: 0, # '\xad' + 31: 2, # 'Ð' + 32: 2, # 'Б' + 35: 1, # 'Ð’' + 43: 1, # 'Г' + 37: 2, # 'Д' + 44: 2, # 'Е' + 55: 1, # 'Ж' + 47: 1, # 'З' + 40: 2, # 'И' + 59: 1, # 'Й' + 33: 2, # 'К' + 46: 1, # 'Л' + 38: 1, # 'Ðœ' + 36: 1, # 'Ð' + 41: 2, # 'О' + 30: 1, # 'П' + 39: 1, # 'Р' + 28: 2, # 'С' + 34: 2, # 'Т' + 51: 1, # 'У' + 48: 1, # 'Ф' + 49: 1, # 'Ð¥' + 53: 1, # 'Ц' + 50: 1, # 'Ч' + 54: 1, # 'Ш' + 57: 0, # 'Щ' + 61: 1, # 'Ъ' + 60: 1, # 'Ю' + 56: 1, # 'Я' + 1: 3, # 'а' + 18: 0, # 'б' + 9: 0, # 'в' + 20: 1, # 'г' + 11: 0, # 'д' + 3: 3, # 'е' + 23: 0, # 'ж' + 15: 0, # 'з' + 2: 3, # 'и' + 26: 0, # 'й' + 12: 0, # 'к' + 10: 0, # 'л' + 14: 0, # 'м' + 6: 0, # 'н' + 4: 3, # 'о' + 13: 0, # 'п' + 7: 0, # 'Ñ€' + 8: 0, # 'Ñ' + 5: 1, # 'Ñ‚' + 19: 1, # 'у' + 29: 0, # 'Ñ„' + 25: 0, # 'Ñ…' + 22: 0, # 'ц' + 21: 0, # 'ч' + 27: 1, # 'ш' + 24: 0, # 'щ' + 17: 0, # 'ÑŠ' + 52: 0, # 'ÑŒ' + 42: 2, # 'ÑŽ' + 16: 2, # 'Ñ' + 58: 0, # 'Ñ”' + 62: 0, # 'â„–' + }, + 41: { # 'О' + 63: 0, # 'e' + 45: 0, # '\xad' + 31: 1, # 'Ð' + 32: 1, # 'Б' + 35: 2, # 'Ð’' + 43: 1, # 'Г' + 37: 2, # 'Д' + 44: 1, # 'Е' + 55: 1, # 'Ж' + 47: 1, # 'З' + 40: 1, # 'И' + 59: 1, # 'Й' + 33: 2, # 'К' + 46: 2, # 'Л' + 38: 2, # 'Ðœ' + 36: 2, # 'Ð' + 41: 2, # 'О' + 30: 1, # 'П' + 39: 2, # 'Р' + 28: 2, # 'С' + 34: 2, # 'Т' + 51: 1, # 'У' + 48: 1, # 'Ф' + 49: 1, # 'Ð¥' + 53: 0, # 'Ц' + 50: 1, # 'Ч' + 54: 1, # 'Ш' + 57: 1, # 'Щ' + 61: 0, # 'Ъ' + 60: 0, # 'Ю' + 56: 1, # 'Я' + 1: 1, # 'а' + 18: 2, # 'б' + 9: 2, # 'в' + 20: 2, # 'г' + 11: 1, # 'д' + 3: 1, # 'е' + 23: 1, # 'ж' + 15: 1, # 'з' + 2: 0, # 'и' + 26: 1, # 'й' + 12: 2, # 'к' + 10: 2, # 'л' + 14: 1, # 'м' + 6: 1, # 'н' + 4: 0, # 'о' + 13: 2, # 'п' + 7: 2, # 'Ñ€' + 8: 2, # 'Ñ' + 5: 3, # 'Ñ‚' + 19: 1, # 'у' + 29: 1, # 'Ñ„' + 25: 1, # 'Ñ…' + 22: 1, # 'ц' + 21: 2, # 'ч' + 27: 0, # 'ш' + 24: 2, # 'щ' + 17: 0, # 'ÑŠ' + 52: 0, # 'ÑŒ' + 42: 0, # 'ÑŽ' + 16: 1, # 'Ñ' + 58: 0, # 'Ñ”' + 62: 0, # 'â„–' + }, + 30: { # 'П' + 63: 0, # 'e' + 45: 1, # '\xad' + 31: 2, # 'Ð' + 32: 1, # 'Б' + 35: 1, # 'Ð’' + 43: 1, # 'Г' + 37: 1, # 'Д' + 44: 1, # 'Е' + 55: 0, # 'Ж' + 47: 1, # 'З' + 40: 2, # 'И' + 59: 0, # 'Й' + 33: 1, # 'К' + 46: 1, # 'Л' + 38: 1, # 'Ðœ' + 36: 1, # 'Ð' + 41: 2, # 'О' + 30: 2, # 'П' + 39: 2, # 'Р' + 28: 2, # 'С' + 34: 1, # 'Т' + 51: 2, # 'У' + 48: 1, # 'Ф' + 49: 0, # 'Ð¥' + 53: 1, # 'Ц' + 50: 1, # 'Ч' + 54: 1, # 'Ш' + 57: 0, # 'Щ' + 61: 1, # 'Ъ' + 60: 1, # 'Ю' + 56: 0, # 'Я' + 1: 3, # 'а' + 18: 0, # 'б' + 9: 0, # 'в' + 20: 0, # 'г' + 11: 2, # 'д' + 3: 3, # 'е' + 23: 0, # 'ж' + 15: 0, # 'з' + 2: 2, # 'и' + 26: 0, # 'й' + 12: 1, # 'к' + 10: 3, # 'л' + 14: 0, # 'м' + 6: 1, # 'н' + 4: 3, # 'о' + 13: 0, # 'п' + 7: 3, # 'Ñ€' + 8: 1, # 'Ñ' + 5: 1, # 'Ñ‚' + 19: 2, # 'у' + 29: 1, # 'Ñ„' + 25: 1, # 'Ñ…' + 22: 0, # 'ц' + 21: 1, # 'ч' + 27: 1, # 'ш' + 24: 0, # 'щ' + 17: 2, # 'ÑŠ' + 52: 1, # 'ÑŒ' + 42: 1, # 'ÑŽ' + 16: 1, # 'Ñ' + 58: 0, # 'Ñ”' + 62: 0, # 'â„–' + }, + 39: { # 'Р' + 63: 0, # 'e' + 45: 1, # '\xad' + 31: 2, # 'Ð' + 32: 1, # 'Б' + 35: 1, # 'Ð’' + 43: 2, # 'Г' + 37: 2, # 'Д' + 44: 2, # 'Е' + 55: 0, # 'Ж' + 47: 1, # 'З' + 40: 2, # 'И' + 59: 0, # 'Й' + 33: 1, # 'К' + 46: 0, # 'Л' + 38: 1, # 'Ðœ' + 36: 1, # 'Ð' + 41: 2, # 'О' + 30: 2, # 'П' + 39: 1, # 'Р' + 28: 1, # 'С' + 34: 1, # 'Т' + 51: 1, # 'У' + 48: 1, # 'Ф' + 49: 1, # 'Ð¥' + 53: 1, # 'Ц' + 50: 1, # 'Ч' + 54: 0, # 'Ш' + 57: 0, # 'Щ' + 61: 1, # 'Ъ' + 60: 1, # 'Ю' + 56: 1, # 'Я' + 1: 3, # 'а' + 18: 0, # 'б' + 9: 0, # 'в' + 20: 0, # 'г' + 11: 0, # 'д' + 3: 2, # 'е' + 23: 0, # 'ж' + 15: 0, # 'з' + 2: 2, # 'и' + 26: 0, # 'й' + 12: 0, # 'к' + 10: 0, # 'л' + 14: 0, # 'м' + 6: 1, # 'н' + 4: 3, # 'о' + 13: 0, # 'п' + 7: 0, # 'Ñ€' + 8: 1, # 'Ñ' + 5: 0, # 'Ñ‚' + 19: 3, # 'у' + 29: 0, # 'Ñ„' + 25: 0, # 'Ñ…' + 22: 0, # 'ц' + 21: 0, # 'ч' + 27: 0, # 'ш' + 24: 0, # 'щ' + 17: 1, # 'ÑŠ' + 52: 0, # 'ÑŒ' + 42: 1, # 'ÑŽ' + 16: 1, # 'Ñ' + 58: 0, # 'Ñ”' + 62: 0, # 'â„–' + }, + 28: { # 'С' + 63: 1, # 'e' + 45: 0, # '\xad' + 31: 3, # 'Ð' + 32: 2, # 'Б' + 35: 2, # 'Ð’' + 43: 1, # 'Г' + 37: 2, # 'Д' + 44: 2, # 'Е' + 55: 1, # 'Ж' + 47: 1, # 'З' + 40: 2, # 'И' + 59: 0, # 'Й' + 33: 2, # 'К' + 46: 1, # 'Л' + 38: 1, # 'Ðœ' + 36: 1, # 'Ð' + 41: 2, # 'О' + 30: 2, # 'П' + 39: 1, # 'Р' + 28: 2, # 'С' + 34: 2, # 'Т' + 51: 1, # 'У' + 48: 1, # 'Ф' + 49: 0, # 'Ð¥' + 53: 0, # 'Ц' + 50: 0, # 'Ч' + 54: 0, # 'Ш' + 57: 0, # 'Щ' + 61: 1, # 'Ъ' + 60: 1, # 'Ю' + 56: 1, # 'Я' + 1: 3, # 'а' + 18: 1, # 'б' + 9: 2, # 'в' + 20: 1, # 'г' + 11: 1, # 'д' + 3: 3, # 'е' + 23: 0, # 'ж' + 15: 0, # 'з' + 2: 3, # 'и' + 26: 0, # 'й' + 12: 2, # 'к' + 10: 3, # 'л' + 14: 2, # 'м' + 6: 1, # 'н' + 4: 3, # 'о' + 13: 3, # 'п' + 7: 2, # 'Ñ€' + 8: 0, # 'Ñ' + 5: 3, # 'Ñ‚' + 19: 2, # 'у' + 29: 2, # 'Ñ„' + 25: 1, # 'Ñ…' + 22: 1, # 'ц' + 21: 1, # 'ч' + 27: 0, # 'ш' + 24: 0, # 'щ' + 17: 3, # 'ÑŠ' + 52: 1, # 'ÑŒ' + 42: 1, # 'ÑŽ' + 16: 1, # 'Ñ' + 58: 0, # 'Ñ”' + 62: 0, # 'â„–' + }, + 34: { # 'Т' + 63: 0, # 'e' + 45: 0, # '\xad' + 31: 2, # 'Ð' + 32: 2, # 'Б' + 35: 1, # 'Ð’' + 43: 0, # 'Г' + 37: 1, # 'Д' + 44: 2, # 'Е' + 55: 0, # 'Ж' + 47: 0, # 'З' + 40: 2, # 'И' + 59: 0, # 'Й' + 33: 2, # 'К' + 46: 1, # 'Л' + 38: 1, # 'Ðœ' + 36: 1, # 'Ð' + 41: 2, # 'О' + 30: 1, # 'П' + 39: 2, # 'Р' + 28: 2, # 'С' + 34: 1, # 'Т' + 51: 1, # 'У' + 48: 1, # 'Ф' + 49: 0, # 'Ð¥' + 53: 1, # 'Ц' + 50: 0, # 'Ч' + 54: 0, # 'Ш' + 57: 0, # 'Щ' + 61: 1, # 'Ъ' + 60: 0, # 'Ю' + 56: 1, # 'Я' + 1: 3, # 'а' + 18: 1, # 'б' + 9: 1, # 'в' + 20: 0, # 'г' + 11: 0, # 'д' + 3: 3, # 'е' + 23: 0, # 'ж' + 15: 0, # 'з' + 2: 2, # 'и' + 26: 0, # 'й' + 12: 1, # 'к' + 10: 1, # 'л' + 14: 0, # 'м' + 6: 0, # 'н' + 4: 3, # 'о' + 13: 0, # 'п' + 7: 3, # 'Ñ€' + 8: 0, # 'Ñ' + 5: 0, # 'Ñ‚' + 19: 2, # 'у' + 29: 0, # 'Ñ„' + 25: 0, # 'Ñ…' + 22: 0, # 'ц' + 21: 0, # 'ч' + 27: 0, # 'ш' + 24: 0, # 'щ' + 17: 2, # 'ÑŠ' + 52: 0, # 'ÑŒ' + 42: 1, # 'ÑŽ' + 16: 2, # 'Ñ' + 58: 0, # 'Ñ”' + 62: 0, # 'â„–' + }, + 51: { # 'У' + 63: 0, # 'e' + 45: 1, # '\xad' + 31: 1, # 'Ð' + 32: 1, # 'Б' + 35: 1, # 'Ð’' + 43: 1, # 'Г' + 37: 1, # 'Д' + 44: 2, # 'Е' + 55: 1, # 'Ж' + 47: 1, # 'З' + 40: 1, # 'И' + 59: 0, # 'Й' + 33: 1, # 'К' + 46: 1, # 'Л' + 38: 1, # 'Ðœ' + 36: 1, # 'Ð' + 41: 0, # 'О' + 30: 1, # 'П' + 39: 1, # 'Р' + 28: 1, # 'С' + 34: 2, # 'Т' + 51: 0, # 'У' + 48: 1, # 'Ф' + 49: 1, # 'Ð¥' + 53: 1, # 'Ц' + 50: 1, # 'Ч' + 54: 1, # 'Ш' + 57: 0, # 'Щ' + 61: 0, # 'Ъ' + 60: 0, # 'Ю' + 56: 0, # 'Я' + 1: 1, # 'а' + 18: 1, # 'б' + 9: 2, # 'в' + 20: 1, # 'г' + 11: 1, # 'д' + 3: 2, # 'е' + 23: 1, # 'ж' + 15: 1, # 'з' + 2: 2, # 'и' + 26: 1, # 'й' + 12: 2, # 'к' + 10: 1, # 'л' + 14: 1, # 'м' + 6: 2, # 'н' + 4: 2, # 'о' + 13: 1, # 'п' + 7: 1, # 'Ñ€' + 8: 2, # 'Ñ' + 5: 1, # 'Ñ‚' + 19: 1, # 'у' + 29: 0, # 'Ñ„' + 25: 1, # 'Ñ…' + 22: 0, # 'ц' + 21: 2, # 'ч' + 27: 1, # 'ш' + 24: 0, # 'щ' + 17: 1, # 'ÑŠ' + 52: 0, # 'ÑŒ' + 42: 0, # 'ÑŽ' + 16: 0, # 'Ñ' + 58: 0, # 'Ñ”' + 62: 0, # 'â„–' + }, + 48: { # 'Ф' + 63: 0, # 'e' + 45: 0, # '\xad' + 31: 2, # 'Ð' + 32: 1, # 'Б' + 35: 1, # 'Ð’' + 43: 0, # 'Г' + 37: 0, # 'Д' + 44: 1, # 'Е' + 55: 0, # 'Ж' + 47: 0, # 'З' + 40: 2, # 'И' + 59: 0, # 'Й' + 33: 1, # 'К' + 46: 1, # 'Л' + 38: 0, # 'Ðœ' + 36: 1, # 'Ð' + 41: 1, # 'О' + 30: 2, # 'П' + 39: 1, # 'Р' + 28: 2, # 'С' + 34: 1, # 'Т' + 51: 1, # 'У' + 48: 0, # 'Ф' + 49: 0, # 'Ð¥' + 53: 0, # 'Ц' + 50: 0, # 'Ч' + 54: 0, # 'Ш' + 57: 0, # 'Щ' + 61: 0, # 'Ъ' + 60: 0, # 'Ю' + 56: 0, # 'Я' + 1: 2, # 'а' + 18: 0, # 'б' + 9: 0, # 'в' + 20: 0, # 'г' + 11: 0, # 'д' + 3: 2, # 'е' + 23: 0, # 'ж' + 15: 0, # 'з' + 2: 2, # 'и' + 26: 0, # 'й' + 12: 0, # 'к' + 10: 2, # 'л' + 14: 0, # 'м' + 6: 0, # 'н' + 4: 2, # 'о' + 13: 0, # 'п' + 7: 2, # 'Ñ€' + 8: 0, # 'Ñ' + 5: 0, # 'Ñ‚' + 19: 1, # 'у' + 29: 0, # 'Ñ„' + 25: 0, # 'Ñ…' + 22: 0, # 'ц' + 21: 0, # 'ч' + 27: 0, # 'ш' + 24: 0, # 'щ' + 17: 1, # 'ÑŠ' + 52: 1, # 'ÑŒ' + 42: 1, # 'ÑŽ' + 16: 0, # 'Ñ' + 58: 0, # 'Ñ”' + 62: 0, # 'â„–' + }, + 49: { # 'Ð¥' + 63: 0, # 'e' + 45: 0, # '\xad' + 31: 1, # 'Ð' + 32: 0, # 'Б' + 35: 1, # 'Ð’' + 43: 1, # 'Г' + 37: 1, # 'Д' + 44: 1, # 'Е' + 55: 0, # 'Ж' + 47: 0, # 'З' + 40: 1, # 'И' + 59: 0, # 'Й' + 33: 0, # 'К' + 46: 1, # 'Л' + 38: 1, # 'Ðœ' + 36: 1, # 'Ð' + 41: 1, # 'О' + 30: 1, # 'П' + 39: 1, # 'Р' + 28: 0, # 'С' + 34: 0, # 'Т' + 51: 0, # 'У' + 48: 0, # 'Ф' + 49: 1, # 'Ð¥' + 53: 0, # 'Ц' + 50: 0, # 'Ч' + 54: 0, # 'Ш' + 57: 0, # 'Щ' + 61: 0, # 'Ъ' + 60: 0, # 'Ю' + 56: 0, # 'Я' + 1: 2, # 'а' + 18: 0, # 'б' + 9: 1, # 'в' + 20: 0, # 'г' + 11: 0, # 'д' + 3: 2, # 'е' + 23: 0, # 'ж' + 15: 0, # 'з' + 2: 2, # 'и' + 26: 0, # 'й' + 12: 0, # 'к' + 10: 1, # 'л' + 14: 1, # 'м' + 6: 0, # 'н' + 4: 2, # 'о' + 13: 0, # 'п' + 7: 2, # 'Ñ€' + 8: 0, # 'Ñ' + 5: 0, # 'Ñ‚' + 19: 2, # 'у' + 29: 0, # 'Ñ„' + 25: 0, # 'Ñ…' + 22: 0, # 'ц' + 21: 0, # 'ч' + 27: 0, # 'ш' + 24: 0, # 'щ' + 17: 2, # 'ÑŠ' + 52: 1, # 'ÑŒ' + 42: 1, # 'ÑŽ' + 16: 0, # 'Ñ' + 58: 0, # 'Ñ”' + 62: 0, # 'â„–' + }, + 53: { # 'Ц' + 63: 0, # 'e' + 45: 0, # '\xad' + 31: 1, # 'Ð' + 32: 0, # 'Б' + 35: 1, # 'Ð’' + 43: 0, # 'Г' + 37: 0, # 'Д' + 44: 1, # 'Е' + 55: 0, # 'Ж' + 47: 0, # 'З' + 40: 2, # 'И' + 59: 0, # 'Й' + 33: 2, # 'К' + 46: 1, # 'Л' + 38: 1, # 'Ðœ' + 36: 0, # 'Ð' + 41: 0, # 'О' + 30: 0, # 'П' + 39: 1, # 'Р' + 28: 2, # 'С' + 34: 0, # 'Т' + 51: 1, # 'У' + 48: 0, # 'Ф' + 49: 0, # 'Ð¥' + 53: 0, # 'Ц' + 50: 0, # 'Ч' + 54: 0, # 'Ш' + 57: 0, # 'Щ' + 61: 0, # 'Ъ' + 60: 0, # 'Ю' + 56: 0, # 'Я' + 1: 2, # 'а' + 18: 0, # 'б' + 9: 2, # 'в' + 20: 0, # 'г' + 11: 0, # 'д' + 3: 2, # 'е' + 23: 0, # 'ж' + 15: 1, # 'з' + 2: 2, # 'и' + 26: 0, # 'й' + 12: 0, # 'к' + 10: 0, # 'л' + 14: 0, # 'м' + 6: 0, # 'н' + 4: 1, # 'о' + 13: 0, # 'п' + 7: 1, # 'Ñ€' + 8: 0, # 'Ñ' + 5: 0, # 'Ñ‚' + 19: 1, # 'у' + 29: 0, # 'Ñ„' + 25: 0, # 'Ñ…' + 22: 0, # 'ц' + 21: 0, # 'ч' + 27: 0, # 'ш' + 24: 0, # 'щ' + 17: 1, # 'ÑŠ' + 52: 0, # 'ÑŒ' + 42: 1, # 'ÑŽ' + 16: 1, # 'Ñ' + 58: 0, # 'Ñ”' + 62: 0, # 'â„–' + }, + 50: { # 'Ч' + 63: 0, # 'e' + 45: 0, # '\xad' + 31: 2, # 'Ð' + 32: 1, # 'Б' + 35: 0, # 'Ð’' + 43: 0, # 'Г' + 37: 0, # 'Д' + 44: 1, # 'Е' + 55: 0, # 'Ж' + 47: 1, # 'З' + 40: 1, # 'И' + 59: 0, # 'Й' + 33: 1, # 'К' + 46: 1, # 'Л' + 38: 0, # 'Ðœ' + 36: 1, # 'Ð' + 41: 1, # 'О' + 30: 0, # 'П' + 39: 0, # 'Р' + 28: 0, # 'С' + 34: 0, # 'Т' + 51: 1, # 'У' + 48: 0, # 'Ф' + 49: 0, # 'Ð¥' + 53: 0, # 'Ц' + 50: 0, # 'Ч' + 54: 0, # 'Ш' + 57: 0, # 'Щ' + 61: 0, # 'Ъ' + 60: 0, # 'Ю' + 56: 0, # 'Я' + 1: 2, # 'а' + 18: 0, # 'б' + 9: 0, # 'в' + 20: 0, # 'г' + 11: 0, # 'д' + 3: 3, # 'е' + 23: 1, # 'ж' + 15: 0, # 'з' + 2: 2, # 'и' + 26: 0, # 'й' + 12: 0, # 'к' + 10: 1, # 'л' + 14: 0, # 'м' + 6: 0, # 'н' + 4: 2, # 'о' + 13: 0, # 'п' + 7: 1, # 'Ñ€' + 8: 0, # 'Ñ' + 5: 0, # 'Ñ‚' + 19: 2, # 'у' + 29: 0, # 'Ñ„' + 25: 0, # 'Ñ…' + 22: 0, # 'ц' + 21: 0, # 'ч' + 27: 0, # 'ш' + 24: 0, # 'щ' + 17: 1, # 'ÑŠ' + 52: 1, # 'ÑŒ' + 42: 0, # 'ÑŽ' + 16: 0, # 'Ñ' + 58: 0, # 'Ñ”' + 62: 0, # 'â„–' + }, + 54: { # 'Ш' + 63: 0, # 'e' + 45: 0, # '\xad' + 31: 1, # 'Ð' + 32: 0, # 'Б' + 35: 0, # 'Ð’' + 43: 0, # 'Г' + 37: 0, # 'Д' + 44: 1, # 'Е' + 55: 0, # 'Ж' + 47: 1, # 'З' + 40: 1, # 'И' + 59: 0, # 'Й' + 33: 1, # 'К' + 46: 0, # 'Л' + 38: 0, # 'Ðœ' + 36: 1, # 'Ð' + 41: 1, # 'О' + 30: 0, # 'П' + 39: 0, # 'Р' + 28: 0, # 'С' + 34: 0, # 'Т' + 51: 1, # 'У' + 48: 0, # 'Ф' + 49: 0, # 'Ð¥' + 53: 0, # 'Ц' + 50: 0, # 'Ч' + 54: 0, # 'Ш' + 57: 0, # 'Щ' + 61: 0, # 'Ъ' + 60: 0, # 'Ю' + 56: 0, # 'Я' + 1: 2, # 'а' + 18: 0, # 'б' + 9: 2, # 'в' + 20: 0, # 'г' + 11: 0, # 'д' + 3: 2, # 'е' + 23: 0, # 'ж' + 15: 0, # 'з' + 2: 2, # 'и' + 26: 0, # 'й' + 12: 1, # 'к' + 10: 1, # 'л' + 14: 1, # 'м' + 6: 1, # 'н' + 4: 2, # 'о' + 13: 1, # 'п' + 7: 1, # 'Ñ€' + 8: 0, # 'Ñ' + 5: 0, # 'Ñ‚' + 19: 2, # 'у' + 29: 0, # 'Ñ„' + 25: 0, # 'Ñ…' + 22: 0, # 'ц' + 21: 1, # 'ч' + 27: 0, # 'ш' + 24: 0, # 'щ' + 17: 1, # 'ÑŠ' + 52: 1, # 'ÑŒ' + 42: 0, # 'ÑŽ' + 16: 0, # 'Ñ' + 58: 0, # 'Ñ”' + 62: 0, # 'â„–' + }, + 57: { # 'Щ' + 63: 0, # 'e' + 45: 0, # '\xad' + 31: 1, # 'Ð' + 32: 0, # 'Б' + 35: 0, # 'Ð’' + 43: 0, # 'Г' + 37: 0, # 'Д' + 44: 1, # 'Е' + 55: 0, # 'Ж' + 47: 0, # 'З' + 40: 1, # 'И' + 59: 0, # 'Й' + 33: 0, # 'К' + 46: 0, # 'Л' + 38: 0, # 'Ðœ' + 36: 0, # 'Ð' + 41: 1, # 'О' + 30: 0, # 'П' + 39: 0, # 'Р' + 28: 0, # 'С' + 34: 0, # 'Т' + 51: 0, # 'У' + 48: 0, # 'Ф' + 49: 0, # 'Ð¥' + 53: 0, # 'Ц' + 50: 0, # 'Ч' + 54: 0, # 'Ш' + 57: 0, # 'Щ' + 61: 0, # 'Ъ' + 60: 0, # 'Ю' + 56: 0, # 'Я' + 1: 2, # 'а' + 18: 0, # 'б' + 9: 0, # 'в' + 20: 0, # 'г' + 11: 0, # 'д' + 3: 2, # 'е' + 23: 0, # 'ж' + 15: 0, # 'з' + 2: 1, # 'и' + 26: 0, # 'й' + 12: 0, # 'к' + 10: 0, # 'л' + 14: 0, # 'м' + 6: 0, # 'н' + 4: 1, # 'о' + 13: 0, # 'п' + 7: 1, # 'Ñ€' + 8: 0, # 'Ñ' + 5: 0, # 'Ñ‚' + 19: 1, # 'у' + 29: 0, # 'Ñ„' + 25: 0, # 'Ñ…' + 22: 0, # 'ц' + 21: 0, # 'ч' + 27: 0, # 'ш' + 24: 0, # 'щ' + 17: 1, # 'ÑŠ' + 52: 0, # 'ÑŒ' + 42: 0, # 'ÑŽ' + 16: 1, # 'Ñ' + 58: 0, # 'Ñ”' + 62: 0, # 'â„–' + }, + 61: { # 'Ъ' + 63: 0, # 'e' + 45: 0, # '\xad' + 31: 0, # 'Ð' + 32: 1, # 'Б' + 35: 1, # 'Ð’' + 43: 0, # 'Г' + 37: 1, # 'Д' + 44: 0, # 'Е' + 55: 1, # 'Ж' + 47: 1, # 'З' + 40: 0, # 'И' + 59: 0, # 'Й' + 33: 1, # 'К' + 46: 2, # 'Л' + 38: 1, # 'Ðœ' + 36: 1, # 'Ð' + 41: 0, # 'О' + 30: 1, # 'П' + 39: 2, # 'Р' + 28: 1, # 'С' + 34: 1, # 'Т' + 51: 0, # 'У' + 48: 0, # 'Ф' + 49: 1, # 'Ð¥' + 53: 1, # 'Ц' + 50: 1, # 'Ч' + 54: 1, # 'Ш' + 57: 1, # 'Щ' + 61: 0, # 'Ъ' + 60: 0, # 'Ю' + 56: 0, # 'Я' + 1: 0, # 'а' + 18: 0, # 'б' + 9: 0, # 'в' + 20: 0, # 'г' + 11: 0, # 'д' + 3: 0, # 'е' + 23: 0, # 'ж' + 15: 0, # 'з' + 2: 0, # 'и' + 26: 0, # 'й' + 12: 0, # 'к' + 10: 1, # 'л' + 14: 0, # 'м' + 6: 1, # 'н' + 4: 0, # 'о' + 13: 0, # 'п' + 7: 1, # 'Ñ€' + 8: 0, # 'Ñ' + 5: 0, # 'Ñ‚' + 19: 0, # 'у' + 29: 0, # 'Ñ„' + 25: 0, # 'Ñ…' + 22: 0, # 'ц' + 21: 0, # 'ч' + 27: 0, # 'ш' + 24: 0, # 'щ' + 17: 0, # 'ÑŠ' + 52: 0, # 'ÑŒ' + 42: 0, # 'ÑŽ' + 16: 0, # 'Ñ' + 58: 0, # 'Ñ”' + 62: 0, # 'â„–' + }, + 60: { # 'Ю' + 63: 0, # 'e' + 45: 0, # '\xad' + 31: 1, # 'Ð' + 32: 1, # 'Б' + 35: 0, # 'Ð’' + 43: 1, # 'Г' + 37: 1, # 'Д' + 44: 0, # 'Е' + 55: 1, # 'Ж' + 47: 0, # 'З' + 40: 0, # 'И' + 59: 0, # 'Й' + 33: 1, # 'К' + 46: 1, # 'Л' + 38: 0, # 'Ðœ' + 36: 1, # 'Ð' + 41: 0, # 'О' + 30: 0, # 'П' + 39: 1, # 'Р' + 28: 1, # 'С' + 34: 0, # 'Т' + 51: 0, # 'У' + 48: 0, # 'Ф' + 49: 0, # 'Ð¥' + 53: 0, # 'Ц' + 50: 0, # 'Ч' + 54: 0, # 'Ш' + 57: 0, # 'Щ' + 61: 0, # 'Ъ' + 60: 0, # 'Ю' + 56: 0, # 'Я' + 1: 0, # 'а' + 18: 1, # 'б' + 9: 1, # 'в' + 20: 2, # 'г' + 11: 1, # 'д' + 3: 0, # 'е' + 23: 2, # 'ж' + 15: 1, # 'з' + 2: 1, # 'и' + 26: 0, # 'й' + 12: 1, # 'к' + 10: 1, # 'л' + 14: 1, # 'м' + 6: 1, # 'н' + 4: 0, # 'о' + 13: 1, # 'п' + 7: 1, # 'Ñ€' + 8: 1, # 'Ñ' + 5: 1, # 'Ñ‚' + 19: 0, # 'у' + 29: 0, # 'Ñ„' + 25: 1, # 'Ñ…' + 22: 0, # 'ц' + 21: 0, # 'ч' + 27: 0, # 'ш' + 24: 0, # 'щ' + 17: 0, # 'ÑŠ' + 52: 0, # 'ÑŒ' + 42: 0, # 'ÑŽ' + 16: 0, # 'Ñ' + 58: 0, # 'Ñ”' + 62: 0, # 'â„–' + }, + 56: { # 'Я' + 63: 0, # 'e' + 45: 0, # '\xad' + 31: 0, # 'Ð' + 32: 1, # 'Б' + 35: 1, # 'Ð’' + 43: 1, # 'Г' + 37: 1, # 'Д' + 44: 0, # 'Е' + 55: 0, # 'Ж' + 47: 0, # 'З' + 40: 0, # 'И' + 59: 0, # 'Й' + 33: 1, # 'К' + 46: 1, # 'Л' + 38: 1, # 'Ðœ' + 36: 1, # 'Ð' + 41: 0, # 'О' + 30: 0, # 'П' + 39: 0, # 'Р' + 28: 1, # 'С' + 34: 2, # 'Т' + 51: 0, # 'У' + 48: 0, # 'Ф' + 49: 0, # 'Ð¥' + 53: 0, # 'Ц' + 50: 0, # 'Ч' + 54: 0, # 'Ш' + 57: 0, # 'Щ' + 61: 0, # 'Ъ' + 60: 0, # 'Ю' + 56: 0, # 'Я' + 1: 0, # 'а' + 18: 1, # 'б' + 9: 1, # 'в' + 20: 1, # 'г' + 11: 1, # 'д' + 3: 0, # 'е' + 23: 0, # 'ж' + 15: 1, # 'з' + 2: 1, # 'и' + 26: 1, # 'й' + 12: 1, # 'к' + 10: 1, # 'л' + 14: 2, # 'м' + 6: 2, # 'н' + 4: 0, # 'о' + 13: 2, # 'п' + 7: 1, # 'Ñ€' + 8: 1, # 'Ñ' + 5: 1, # 'Ñ‚' + 19: 0, # 'у' + 29: 0, # 'Ñ„' + 25: 1, # 'Ñ…' + 22: 0, # 'ц' + 21: 0, # 'ч' + 27: 1, # 'ш' + 24: 0, # 'щ' + 17: 0, # 'ÑŠ' + 52: 0, # 'ÑŒ' + 42: 1, # 'ÑŽ' + 16: 0, # 'Ñ' + 58: 0, # 'Ñ”' + 62: 0, # 'â„–' + }, + 1: { # 'а' + 63: 1, # 'e' + 45: 1, # '\xad' + 31: 1, # 'Ð' + 32: 0, # 'Б' + 35: 0, # 'Ð’' + 43: 0, # 'Г' + 37: 0, # 'Д' + 44: 1, # 'Е' + 55: 0, # 'Ж' + 47: 0, # 'З' + 40: 0, # 'И' + 59: 0, # 'Й' + 33: 0, # 'К' + 46: 0, # 'Л' + 38: 0, # 'Ðœ' + 36: 0, # 'Ð' + 41: 0, # 'О' + 30: 0, # 'П' + 39: 0, # 'Р' + 28: 0, # 'С' + 34: 0, # 'Т' + 51: 0, # 'У' + 48: 0, # 'Ф' + 49: 0, # 'Ð¥' + 53: 0, # 'Ц' + 50: 0, # 'Ч' + 54: 0, # 'Ш' + 57: 0, # 'Щ' + 61: 0, # 'Ъ' + 60: 0, # 'Ю' + 56: 0, # 'Я' + 1: 1, # 'а' + 18: 3, # 'б' + 9: 3, # 'в' + 20: 3, # 'г' + 11: 3, # 'д' + 3: 3, # 'е' + 23: 3, # 'ж' + 15: 3, # 'з' + 2: 3, # 'и' + 26: 3, # 'й' + 12: 3, # 'к' + 10: 3, # 'л' + 14: 3, # 'м' + 6: 3, # 'н' + 4: 2, # 'о' + 13: 3, # 'п' + 7: 3, # 'Ñ€' + 8: 3, # 'Ñ' + 5: 3, # 'Ñ‚' + 19: 3, # 'у' + 29: 3, # 'Ñ„' + 25: 3, # 'Ñ…' + 22: 3, # 'ц' + 21: 3, # 'ч' + 27: 3, # 'ш' + 24: 3, # 'щ' + 17: 0, # 'ÑŠ' + 52: 0, # 'ÑŒ' + 42: 1, # 'ÑŽ' + 16: 3, # 'Ñ' + 58: 0, # 'Ñ”' + 62: 0, # 'â„–' + }, + 18: { # 'б' + 63: 1, # 'e' + 45: 0, # '\xad' + 31: 0, # 'Ð' + 32: 0, # 'Б' + 35: 0, # 'Ð’' + 43: 0, # 'Г' + 37: 0, # 'Д' + 44: 0, # 'Е' + 55: 0, # 'Ж' + 47: 0, # 'З' + 40: 0, # 'И' + 59: 0, # 'Й' + 33: 0, # 'К' + 46: 0, # 'Л' + 38: 0, # 'Ðœ' + 36: 0, # 'Ð' + 41: 0, # 'О' + 30: 0, # 'П' + 39: 0, # 'Р' + 28: 0, # 'С' + 34: 0, # 'Т' + 51: 0, # 'У' + 48: 0, # 'Ф' + 49: 0, # 'Ð¥' + 53: 0, # 'Ц' + 50: 0, # 'Ч' + 54: 0, # 'Ш' + 57: 0, # 'Щ' + 61: 0, # 'Ъ' + 60: 0, # 'Ю' + 56: 0, # 'Я' + 1: 3, # 'а' + 18: 0, # 'б' + 9: 3, # 'в' + 20: 1, # 'г' + 11: 2, # 'д' + 3: 3, # 'е' + 23: 1, # 'ж' + 15: 1, # 'з' + 2: 3, # 'и' + 26: 0, # 'й' + 12: 1, # 'к' + 10: 3, # 'л' + 14: 2, # 'м' + 6: 3, # 'н' + 4: 3, # 'о' + 13: 1, # 'п' + 7: 3, # 'Ñ€' + 8: 3, # 'Ñ' + 5: 0, # 'Ñ‚' + 19: 3, # 'у' + 29: 0, # 'Ñ„' + 25: 2, # 'Ñ…' + 22: 1, # 'ц' + 21: 1, # 'ч' + 27: 1, # 'ш' + 24: 3, # 'щ' + 17: 3, # 'ÑŠ' + 52: 1, # 'ÑŒ' + 42: 2, # 'ÑŽ' + 16: 3, # 'Ñ' + 58: 0, # 'Ñ”' + 62: 0, # 'â„–' + }, + 9: { # 'в' + 63: 1, # 'e' + 45: 1, # '\xad' + 31: 0, # 'Ð' + 32: 1, # 'Б' + 35: 0, # 'Ð’' + 43: 0, # 'Г' + 37: 0, # 'Д' + 44: 0, # 'Е' + 55: 0, # 'Ж' + 47: 0, # 'З' + 40: 0, # 'И' + 59: 0, # 'Й' + 33: 0, # 'К' + 46: 0, # 'Л' + 38: 0, # 'Ðœ' + 36: 0, # 'Ð' + 41: 0, # 'О' + 30: 0, # 'П' + 39: 0, # 'Р' + 28: 0, # 'С' + 34: 0, # 'Т' + 51: 0, # 'У' + 48: 1, # 'Ф' + 49: 0, # 'Ð¥' + 53: 0, # 'Ц' + 50: 0, # 'Ч' + 54: 0, # 'Ш' + 57: 0, # 'Щ' + 61: 0, # 'Ъ' + 60: 0, # 'Ю' + 56: 0, # 'Я' + 1: 3, # 'а' + 18: 1, # 'б' + 9: 0, # 'в' + 20: 2, # 'г' + 11: 3, # 'д' + 3: 3, # 'е' + 23: 1, # 'ж' + 15: 3, # 'з' + 2: 3, # 'и' + 26: 0, # 'й' + 12: 3, # 'к' + 10: 3, # 'л' + 14: 2, # 'м' + 6: 3, # 'н' + 4: 3, # 'о' + 13: 2, # 'п' + 7: 3, # 'Ñ€' + 8: 3, # 'Ñ' + 5: 3, # 'Ñ‚' + 19: 2, # 'у' + 29: 0, # 'Ñ„' + 25: 2, # 'Ñ…' + 22: 2, # 'ц' + 21: 3, # 'ч' + 27: 2, # 'ш' + 24: 1, # 'щ' + 17: 3, # 'ÑŠ' + 52: 1, # 'ÑŒ' + 42: 2, # 'ÑŽ' + 16: 3, # 'Ñ' + 58: 0, # 'Ñ”' + 62: 0, # 'â„–' + }, + 20: { # 'г' + 63: 0, # 'e' + 45: 0, # '\xad' + 31: 0, # 'Ð' + 32: 0, # 'Б' + 35: 0, # 'Ð’' + 43: 0, # 'Г' + 37: 0, # 'Д' + 44: 0, # 'Е' + 55: 0, # 'Ж' + 47: 0, # 'З' + 40: 0, # 'И' + 59: 0, # 'Й' + 33: 0, # 'К' + 46: 0, # 'Л' + 38: 0, # 'Ðœ' + 36: 0, # 'Ð' + 41: 0, # 'О' + 30: 0, # 'П' + 39: 0, # 'Р' + 28: 0, # 'С' + 34: 0, # 'Т' + 51: 0, # 'У' + 48: 0, # 'Ф' + 49: 0, # 'Ð¥' + 53: 0, # 'Ц' + 50: 0, # 'Ч' + 54: 0, # 'Ш' + 57: 0, # 'Щ' + 61: 0, # 'Ъ' + 60: 0, # 'Ю' + 56: 0, # 'Я' + 1: 3, # 'а' + 18: 1, # 'б' + 9: 2, # 'в' + 20: 1, # 'г' + 11: 2, # 'д' + 3: 3, # 'е' + 23: 0, # 'ж' + 15: 1, # 'з' + 2: 3, # 'и' + 26: 0, # 'й' + 12: 1, # 'к' + 10: 3, # 'л' + 14: 1, # 'м' + 6: 3, # 'н' + 4: 3, # 'о' + 13: 1, # 'п' + 7: 3, # 'Ñ€' + 8: 2, # 'Ñ' + 5: 2, # 'Ñ‚' + 19: 3, # 'у' + 29: 1, # 'Ñ„' + 25: 1, # 'Ñ…' + 22: 0, # 'ц' + 21: 1, # 'ч' + 27: 0, # 'ш' + 24: 0, # 'щ' + 17: 3, # 'ÑŠ' + 52: 1, # 'ÑŒ' + 42: 1, # 'ÑŽ' + 16: 1, # 'Ñ' + 58: 0, # 'Ñ”' + 62: 0, # 'â„–' + }, + 11: { # 'д' + 63: 1, # 'e' + 45: 0, # '\xad' + 31: 0, # 'Ð' + 32: 0, # 'Б' + 35: 0, # 'Ð’' + 43: 0, # 'Г' + 37: 0, # 'Д' + 44: 0, # 'Е' + 55: 0, # 'Ж' + 47: 0, # 'З' + 40: 0, # 'И' + 59: 0, # 'Й' + 33: 0, # 'К' + 46: 0, # 'Л' + 38: 0, # 'Ðœ' + 36: 0, # 'Ð' + 41: 0, # 'О' + 30: 0, # 'П' + 39: 0, # 'Р' + 28: 0, # 'С' + 34: 0, # 'Т' + 51: 0, # 'У' + 48: 0, # 'Ф' + 49: 0, # 'Ð¥' + 53: 0, # 'Ц' + 50: 0, # 'Ч' + 54: 0, # 'Ш' + 57: 0, # 'Щ' + 61: 0, # 'Ъ' + 60: 0, # 'Ю' + 56: 0, # 'Я' + 1: 3, # 'а' + 18: 2, # 'б' + 9: 3, # 'в' + 20: 2, # 'г' + 11: 2, # 'д' + 3: 3, # 'е' + 23: 3, # 'ж' + 15: 2, # 'з' + 2: 3, # 'и' + 26: 0, # 'й' + 12: 3, # 'к' + 10: 3, # 'л' + 14: 3, # 'м' + 6: 3, # 'н' + 4: 3, # 'о' + 13: 3, # 'п' + 7: 3, # 'Ñ€' + 8: 3, # 'Ñ' + 5: 1, # 'Ñ‚' + 19: 3, # 'у' + 29: 1, # 'Ñ„' + 25: 2, # 'Ñ…' + 22: 2, # 'ц' + 21: 2, # 'ч' + 27: 1, # 'ш' + 24: 1, # 'щ' + 17: 3, # 'ÑŠ' + 52: 1, # 'ÑŒ' + 42: 1, # 'ÑŽ' + 16: 3, # 'Ñ' + 58: 0, # 'Ñ”' + 62: 0, # 'â„–' + }, + 3: { # 'е' + 63: 0, # 'e' + 45: 1, # '\xad' + 31: 0, # 'Ð' + 32: 0, # 'Б' + 35: 0, # 'Ð’' + 43: 0, # 'Г' + 37: 0, # 'Д' + 44: 0, # 'Е' + 55: 0, # 'Ж' + 47: 0, # 'З' + 40: 0, # 'И' + 59: 0, # 'Й' + 33: 0, # 'К' + 46: 0, # 'Л' + 38: 0, # 'Ðœ' + 36: 0, # 'Ð' + 41: 0, # 'О' + 30: 0, # 'П' + 39: 0, # 'Р' + 28: 0, # 'С' + 34: 0, # 'Т' + 51: 0, # 'У' + 48: 0, # 'Ф' + 49: 0, # 'Ð¥' + 53: 0, # 'Ц' + 50: 0, # 'Ч' + 54: 0, # 'Ш' + 57: 0, # 'Щ' + 61: 0, # 'Ъ' + 60: 0, # 'Ю' + 56: 0, # 'Я' + 1: 2, # 'а' + 18: 3, # 'б' + 9: 3, # 'в' + 20: 3, # 'г' + 11: 3, # 'д' + 3: 2, # 'е' + 23: 3, # 'ж' + 15: 3, # 'з' + 2: 2, # 'и' + 26: 3, # 'й' + 12: 3, # 'к' + 10: 3, # 'л' + 14: 3, # 'м' + 6: 3, # 'н' + 4: 3, # 'о' + 13: 3, # 'п' + 7: 3, # 'Ñ€' + 8: 3, # 'Ñ' + 5: 3, # 'Ñ‚' + 19: 2, # 'у' + 29: 3, # 'Ñ„' + 25: 3, # 'Ñ…' + 22: 3, # 'ц' + 21: 3, # 'ч' + 27: 3, # 'ш' + 24: 3, # 'щ' + 17: 1, # 'ÑŠ' + 52: 0, # 'ÑŒ' + 42: 1, # 'ÑŽ' + 16: 3, # 'Ñ' + 58: 0, # 'Ñ”' + 62: 0, # 'â„–' + }, + 23: { # 'ж' + 63: 0, # 'e' + 45: 0, # '\xad' + 31: 0, # 'Ð' + 32: 0, # 'Б' + 35: 0, # 'Ð’' + 43: 0, # 'Г' + 37: 0, # 'Д' + 44: 0, # 'Е' + 55: 0, # 'Ж' + 47: 0, # 'З' + 40: 0, # 'И' + 59: 0, # 'Й' + 33: 0, # 'К' + 46: 0, # 'Л' + 38: 0, # 'Ðœ' + 36: 0, # 'Ð' + 41: 0, # 'О' + 30: 0, # 'П' + 39: 0, # 'Р' + 28: 0, # 'С' + 34: 0, # 'Т' + 51: 0, # 'У' + 48: 0, # 'Ф' + 49: 0, # 'Ð¥' + 53: 0, # 'Ц' + 50: 0, # 'Ч' + 54: 0, # 'Ш' + 57: 0, # 'Щ' + 61: 0, # 'Ъ' + 60: 0, # 'Ю' + 56: 0, # 'Я' + 1: 3, # 'а' + 18: 3, # 'б' + 9: 2, # 'в' + 20: 1, # 'г' + 11: 3, # 'д' + 3: 3, # 'е' + 23: 0, # 'ж' + 15: 0, # 'з' + 2: 3, # 'и' + 26: 0, # 'й' + 12: 2, # 'к' + 10: 1, # 'л' + 14: 1, # 'м' + 6: 3, # 'н' + 4: 2, # 'о' + 13: 1, # 'п' + 7: 1, # 'Ñ€' + 8: 1, # 'Ñ' + 5: 1, # 'Ñ‚' + 19: 2, # 'у' + 29: 0, # 'Ñ„' + 25: 0, # 'Ñ…' + 22: 1, # 'ц' + 21: 1, # 'ч' + 27: 0, # 'ш' + 24: 0, # 'щ' + 17: 2, # 'ÑŠ' + 52: 0, # 'ÑŒ' + 42: 0, # 'ÑŽ' + 16: 1, # 'Ñ' + 58: 0, # 'Ñ”' + 62: 0, # 'â„–' + }, + 15: { # 'з' + 63: 1, # 'e' + 45: 0, # '\xad' + 31: 0, # 'Ð' + 32: 0, # 'Б' + 35: 0, # 'Ð’' + 43: 0, # 'Г' + 37: 0, # 'Д' + 44: 0, # 'Е' + 55: 0, # 'Ж' + 47: 0, # 'З' + 40: 0, # 'И' + 59: 0, # 'Й' + 33: 0, # 'К' + 46: 0, # 'Л' + 38: 0, # 'Ðœ' + 36: 0, # 'Ð' + 41: 0, # 'О' + 30: 0, # 'П' + 39: 0, # 'Р' + 28: 0, # 'С' + 34: 0, # 'Т' + 51: 0, # 'У' + 48: 0, # 'Ф' + 49: 0, # 'Ð¥' + 53: 0, # 'Ц' + 50: 0, # 'Ч' + 54: 0, # 'Ш' + 57: 0, # 'Щ' + 61: 0, # 'Ъ' + 60: 0, # 'Ю' + 56: 0, # 'Я' + 1: 3, # 'а' + 18: 3, # 'б' + 9: 3, # 'в' + 20: 3, # 'г' + 11: 3, # 'д' + 3: 3, # 'е' + 23: 1, # 'ж' + 15: 1, # 'з' + 2: 3, # 'и' + 26: 0, # 'й' + 12: 3, # 'к' + 10: 3, # 'л' + 14: 3, # 'м' + 6: 3, # 'н' + 4: 3, # 'о' + 13: 3, # 'п' + 7: 3, # 'Ñ€' + 8: 3, # 'Ñ' + 5: 3, # 'Ñ‚' + 19: 3, # 'у' + 29: 1, # 'Ñ„' + 25: 2, # 'Ñ…' + 22: 2, # 'ц' + 21: 2, # 'ч' + 27: 2, # 'ш' + 24: 1, # 'щ' + 17: 2, # 'ÑŠ' + 52: 1, # 'ÑŒ' + 42: 1, # 'ÑŽ' + 16: 2, # 'Ñ' + 58: 0, # 'Ñ”' + 62: 0, # 'â„–' + }, + 2: { # 'и' + 63: 1, # 'e' + 45: 1, # '\xad' + 31: 0, # 'Ð' + 32: 0, # 'Б' + 35: 0, # 'Ð’' + 43: 1, # 'Г' + 37: 0, # 'Д' + 44: 0, # 'Е' + 55: 0, # 'Ж' + 47: 0, # 'З' + 40: 0, # 'И' + 59: 0, # 'Й' + 33: 1, # 'К' + 46: 0, # 'Л' + 38: 0, # 'Ðœ' + 36: 0, # 'Ð' + 41: 0, # 'О' + 30: 1, # 'П' + 39: 0, # 'Р' + 28: 0, # 'С' + 34: 0, # 'Т' + 51: 0, # 'У' + 48: 1, # 'Ф' + 49: 0, # 'Ð¥' + 53: 0, # 'Ц' + 50: 0, # 'Ч' + 54: 0, # 'Ш' + 57: 0, # 'Щ' + 61: 0, # 'Ъ' + 60: 0, # 'Ю' + 56: 0, # 'Я' + 1: 3, # 'а' + 18: 3, # 'б' + 9: 3, # 'в' + 20: 3, # 'г' + 11: 3, # 'д' + 3: 3, # 'е' + 23: 3, # 'ж' + 15: 3, # 'з' + 2: 3, # 'и' + 26: 3, # 'й' + 12: 3, # 'к' + 10: 3, # 'л' + 14: 3, # 'м' + 6: 3, # 'н' + 4: 3, # 'о' + 13: 3, # 'п' + 7: 3, # 'Ñ€' + 8: 3, # 'Ñ' + 5: 3, # 'Ñ‚' + 19: 2, # 'у' + 29: 3, # 'Ñ„' + 25: 3, # 'Ñ…' + 22: 3, # 'ц' + 21: 3, # 'ч' + 27: 3, # 'ш' + 24: 3, # 'щ' + 17: 2, # 'ÑŠ' + 52: 0, # 'ÑŒ' + 42: 1, # 'ÑŽ' + 16: 3, # 'Ñ' + 58: 0, # 'Ñ”' + 62: 0, # 'â„–' + }, + 26: { # 'й' + 63: 0, # 'e' + 45: 0, # '\xad' + 31: 0, # 'Ð' + 32: 0, # 'Б' + 35: 0, # 'Ð’' + 43: 0, # 'Г' + 37: 0, # 'Д' + 44: 0, # 'Е' + 55: 0, # 'Ж' + 47: 0, # 'З' + 40: 0, # 'И' + 59: 0, # 'Й' + 33: 0, # 'К' + 46: 0, # 'Л' + 38: 0, # 'Ðœ' + 36: 0, # 'Ð' + 41: 0, # 'О' + 30: 0, # 'П' + 39: 0, # 'Р' + 28: 0, # 'С' + 34: 0, # 'Т' + 51: 0, # 'У' + 48: 0, # 'Ф' + 49: 0, # 'Ð¥' + 53: 0, # 'Ц' + 50: 0, # 'Ч' + 54: 0, # 'Ш' + 57: 0, # 'Щ' + 61: 0, # 'Ъ' + 60: 0, # 'Ю' + 56: 0, # 'Я' + 1: 1, # 'а' + 18: 2, # 'б' + 9: 2, # 'в' + 20: 1, # 'г' + 11: 2, # 'д' + 3: 2, # 'е' + 23: 0, # 'ж' + 15: 2, # 'з' + 2: 1, # 'и' + 26: 0, # 'й' + 12: 3, # 'к' + 10: 2, # 'л' + 14: 2, # 'м' + 6: 3, # 'н' + 4: 2, # 'о' + 13: 1, # 'п' + 7: 2, # 'Ñ€' + 8: 3, # 'Ñ' + 5: 3, # 'Ñ‚' + 19: 1, # 'у' + 29: 2, # 'Ñ„' + 25: 1, # 'Ñ…' + 22: 2, # 'ц' + 21: 2, # 'ч' + 27: 1, # 'ш' + 24: 1, # 'щ' + 17: 1, # 'ÑŠ' + 52: 0, # 'ÑŒ' + 42: 0, # 'ÑŽ' + 16: 1, # 'Ñ' + 58: 0, # 'Ñ”' + 62: 0, # 'â„–' + }, + 12: { # 'к' + 63: 1, # 'e' + 45: 0, # '\xad' + 31: 0, # 'Ð' + 32: 0, # 'Б' + 35: 1, # 'Ð’' + 43: 0, # 'Г' + 37: 0, # 'Д' + 44: 0, # 'Е' + 55: 0, # 'Ж' + 47: 0, # 'З' + 40: 1, # 'И' + 59: 0, # 'Й' + 33: 0, # 'К' + 46: 0, # 'Л' + 38: 0, # 'Ðœ' + 36: 0, # 'Ð' + 41: 0, # 'О' + 30: 0, # 'П' + 39: 0, # 'Р' + 28: 0, # 'С' + 34: 0, # 'Т' + 51: 0, # 'У' + 48: 0, # 'Ф' + 49: 0, # 'Ð¥' + 53: 0, # 'Ц' + 50: 0, # 'Ч' + 54: 0, # 'Ш' + 57: 0, # 'Щ' + 61: 0, # 'Ъ' + 60: 0, # 'Ю' + 56: 0, # 'Я' + 1: 3, # 'а' + 18: 1, # 'б' + 9: 3, # 'в' + 20: 2, # 'г' + 11: 1, # 'д' + 3: 3, # 'е' + 23: 0, # 'ж' + 15: 2, # 'з' + 2: 3, # 'и' + 26: 0, # 'й' + 12: 1, # 'к' + 10: 3, # 'л' + 14: 2, # 'м' + 6: 3, # 'н' + 4: 3, # 'о' + 13: 1, # 'п' + 7: 3, # 'Ñ€' + 8: 3, # 'Ñ' + 5: 3, # 'Ñ‚' + 19: 3, # 'у' + 29: 1, # 'Ñ„' + 25: 1, # 'Ñ…' + 22: 3, # 'ц' + 21: 2, # 'ч' + 27: 1, # 'ш' + 24: 0, # 'щ' + 17: 3, # 'ÑŠ' + 52: 1, # 'ÑŒ' + 42: 2, # 'ÑŽ' + 16: 1, # 'Ñ' + 58: 0, # 'Ñ”' + 62: 0, # 'â„–' + }, + 10: { # 'л' + 63: 1, # 'e' + 45: 1, # '\xad' + 31: 0, # 'Ð' + 32: 0, # 'Б' + 35: 0, # 'Ð’' + 43: 0, # 'Г' + 37: 0, # 'Д' + 44: 0, # 'Е' + 55: 0, # 'Ж' + 47: 0, # 'З' + 40: 0, # 'И' + 59: 0, # 'Й' + 33: 0, # 'К' + 46: 0, # 'Л' + 38: 0, # 'Ðœ' + 36: 0, # 'Ð' + 41: 0, # 'О' + 30: 0, # 'П' + 39: 0, # 'Р' + 28: 1, # 'С' + 34: 0, # 'Т' + 51: 0, # 'У' + 48: 0, # 'Ф' + 49: 0, # 'Ð¥' + 53: 0, # 'Ц' + 50: 0, # 'Ч' + 54: 0, # 'Ш' + 57: 0, # 'Щ' + 61: 0, # 'Ъ' + 60: 0, # 'Ю' + 56: 0, # 'Я' + 1: 3, # 'а' + 18: 3, # 'б' + 9: 3, # 'в' + 20: 3, # 'г' + 11: 2, # 'д' + 3: 3, # 'е' + 23: 3, # 'ж' + 15: 2, # 'з' + 2: 3, # 'и' + 26: 0, # 'й' + 12: 3, # 'к' + 10: 1, # 'л' + 14: 2, # 'м' + 6: 3, # 'н' + 4: 3, # 'о' + 13: 2, # 'п' + 7: 2, # 'Ñ€' + 8: 3, # 'Ñ' + 5: 3, # 'Ñ‚' + 19: 3, # 'у' + 29: 2, # 'Ñ„' + 25: 2, # 'Ñ…' + 22: 2, # 'ц' + 21: 2, # 'ч' + 27: 2, # 'ш' + 24: 1, # 'щ' + 17: 3, # 'ÑŠ' + 52: 2, # 'ÑŒ' + 42: 3, # 'ÑŽ' + 16: 3, # 'Ñ' + 58: 0, # 'Ñ”' + 62: 0, # 'â„–' + }, + 14: { # 'м' + 63: 1, # 'e' + 45: 0, # '\xad' + 31: 1, # 'Ð' + 32: 0, # 'Б' + 35: 0, # 'Ð’' + 43: 0, # 'Г' + 37: 0, # 'Д' + 44: 0, # 'Е' + 55: 0, # 'Ж' + 47: 0, # 'З' + 40: 0, # 'И' + 59: 0, # 'Й' + 33: 0, # 'К' + 46: 0, # 'Л' + 38: 0, # 'Ðœ' + 36: 0, # 'Ð' + 41: 0, # 'О' + 30: 0, # 'П' + 39: 0, # 'Р' + 28: 0, # 'С' + 34: 0, # 'Т' + 51: 0, # 'У' + 48: 0, # 'Ф' + 49: 0, # 'Ð¥' + 53: 0, # 'Ц' + 50: 0, # 'Ч' + 54: 0, # 'Ш' + 57: 0, # 'Щ' + 61: 0, # 'Ъ' + 60: 0, # 'Ю' + 56: 0, # 'Я' + 1: 3, # 'а' + 18: 3, # 'б' + 9: 3, # 'в' + 20: 1, # 'г' + 11: 1, # 'д' + 3: 3, # 'е' + 23: 1, # 'ж' + 15: 1, # 'з' + 2: 3, # 'и' + 26: 0, # 'й' + 12: 2, # 'к' + 10: 3, # 'л' + 14: 1, # 'м' + 6: 3, # 'н' + 4: 3, # 'о' + 13: 3, # 'п' + 7: 2, # 'Ñ€' + 8: 2, # 'Ñ' + 5: 1, # 'Ñ‚' + 19: 3, # 'у' + 29: 2, # 'Ñ„' + 25: 1, # 'Ñ…' + 22: 2, # 'ц' + 21: 2, # 'ч' + 27: 2, # 'ш' + 24: 1, # 'щ' + 17: 3, # 'ÑŠ' + 52: 1, # 'ÑŒ' + 42: 2, # 'ÑŽ' + 16: 3, # 'Ñ' + 58: 0, # 'Ñ”' + 62: 0, # 'â„–' + }, + 6: { # 'н' + 63: 1, # 'e' + 45: 0, # '\xad' + 31: 0, # 'Ð' + 32: 0, # 'Б' + 35: 0, # 'Ð’' + 43: 0, # 'Г' + 37: 0, # 'Д' + 44: 0, # 'Е' + 55: 0, # 'Ж' + 47: 0, # 'З' + 40: 0, # 'И' + 59: 0, # 'Й' + 33: 0, # 'К' + 46: 0, # 'Л' + 38: 0, # 'Ðœ' + 36: 0, # 'Ð' + 41: 0, # 'О' + 30: 0, # 'П' + 39: 1, # 'Р' + 28: 0, # 'С' + 34: 0, # 'Т' + 51: 0, # 'У' + 48: 0, # 'Ф' + 49: 0, # 'Ð¥' + 53: 0, # 'Ц' + 50: 0, # 'Ч' + 54: 0, # 'Ш' + 57: 0, # 'Щ' + 61: 0, # 'Ъ' + 60: 0, # 'Ю' + 56: 0, # 'Я' + 1: 3, # 'а' + 18: 2, # 'б' + 9: 2, # 'в' + 20: 3, # 'г' + 11: 3, # 'д' + 3: 3, # 'е' + 23: 2, # 'ж' + 15: 2, # 'з' + 2: 3, # 'и' + 26: 0, # 'й' + 12: 3, # 'к' + 10: 2, # 'л' + 14: 1, # 'м' + 6: 3, # 'н' + 4: 3, # 'о' + 13: 1, # 'п' + 7: 2, # 'Ñ€' + 8: 3, # 'Ñ' + 5: 3, # 'Ñ‚' + 19: 3, # 'у' + 29: 3, # 'Ñ„' + 25: 2, # 'Ñ…' + 22: 3, # 'ц' + 21: 3, # 'ч' + 27: 2, # 'ш' + 24: 1, # 'щ' + 17: 3, # 'ÑŠ' + 52: 2, # 'ÑŒ' + 42: 2, # 'ÑŽ' + 16: 3, # 'Ñ' + 58: 0, # 'Ñ”' + 62: 0, # 'â„–' + }, + 4: { # 'о' + 63: 0, # 'e' + 45: 1, # '\xad' + 31: 0, # 'Ð' + 32: 0, # 'Б' + 35: 0, # 'Ð’' + 43: 0, # 'Г' + 37: 0, # 'Д' + 44: 0, # 'Е' + 55: 0, # 'Ж' + 47: 0, # 'З' + 40: 0, # 'И' + 59: 0, # 'Й' + 33: 0, # 'К' + 46: 0, # 'Л' + 38: 0, # 'Ðœ' + 36: 0, # 'Ð' + 41: 0, # 'О' + 30: 0, # 'П' + 39: 0, # 'Р' + 28: 0, # 'С' + 34: 0, # 'Т' + 51: 0, # 'У' + 48: 0, # 'Ф' + 49: 0, # 'Ð¥' + 53: 0, # 'Ц' + 50: 0, # 'Ч' + 54: 0, # 'Ш' + 57: 0, # 'Щ' + 61: 0, # 'Ъ' + 60: 0, # 'Ю' + 56: 0, # 'Я' + 1: 2, # 'а' + 18: 3, # 'б' + 9: 3, # 'в' + 20: 3, # 'г' + 11: 3, # 'д' + 3: 3, # 'е' + 23: 3, # 'ж' + 15: 3, # 'з' + 2: 3, # 'и' + 26: 3, # 'й' + 12: 3, # 'к' + 10: 3, # 'л' + 14: 3, # 'м' + 6: 3, # 'н' + 4: 2, # 'о' + 13: 3, # 'п' + 7: 3, # 'Ñ€' + 8: 3, # 'Ñ' + 5: 3, # 'Ñ‚' + 19: 2, # 'у' + 29: 3, # 'Ñ„' + 25: 3, # 'Ñ…' + 22: 3, # 'ц' + 21: 3, # 'ч' + 27: 3, # 'ш' + 24: 3, # 'щ' + 17: 1, # 'ÑŠ' + 52: 0, # 'ÑŒ' + 42: 1, # 'ÑŽ' + 16: 3, # 'Ñ' + 58: 0, # 'Ñ”' + 62: 0, # 'â„–' + }, + 13: { # 'п' + 63: 1, # 'e' + 45: 0, # '\xad' + 31: 0, # 'Ð' + 32: 0, # 'Б' + 35: 0, # 'Ð’' + 43: 0, # 'Г' + 37: 0, # 'Д' + 44: 0, # 'Е' + 55: 0, # 'Ж' + 47: 0, # 'З' + 40: 0, # 'И' + 59: 0, # 'Й' + 33: 0, # 'К' + 46: 0, # 'Л' + 38: 0, # 'Ðœ' + 36: 0, # 'Ð' + 41: 0, # 'О' + 30: 0, # 'П' + 39: 0, # 'Р' + 28: 0, # 'С' + 34: 0, # 'Т' + 51: 0, # 'У' + 48: 0, # 'Ф' + 49: 0, # 'Ð¥' + 53: 0, # 'Ц' + 50: 0, # 'Ч' + 54: 0, # 'Ш' + 57: 0, # 'Щ' + 61: 0, # 'Ъ' + 60: 0, # 'Ю' + 56: 0, # 'Я' + 1: 3, # 'а' + 18: 1, # 'б' + 9: 2, # 'в' + 20: 1, # 'г' + 11: 1, # 'д' + 3: 3, # 'е' + 23: 0, # 'ж' + 15: 1, # 'з' + 2: 3, # 'и' + 26: 1, # 'й' + 12: 2, # 'к' + 10: 3, # 'л' + 14: 1, # 'м' + 6: 2, # 'н' + 4: 3, # 'о' + 13: 1, # 'п' + 7: 3, # 'Ñ€' + 8: 2, # 'Ñ' + 5: 2, # 'Ñ‚' + 19: 3, # 'у' + 29: 1, # 'Ñ„' + 25: 1, # 'Ñ…' + 22: 2, # 'ц' + 21: 2, # 'ч' + 27: 1, # 'ш' + 24: 1, # 'щ' + 17: 3, # 'ÑŠ' + 52: 1, # 'ÑŒ' + 42: 2, # 'ÑŽ' + 16: 2, # 'Ñ' + 58: 0, # 'Ñ”' + 62: 0, # 'â„–' + }, + 7: { # 'Ñ€' + 63: 1, # 'e' + 45: 0, # '\xad' + 31: 0, # 'Ð' + 32: 0, # 'Б' + 35: 0, # 'Ð’' + 43: 0, # 'Г' + 37: 0, # 'Д' + 44: 0, # 'Е' + 55: 0, # 'Ж' + 47: 0, # 'З' + 40: 0, # 'И' + 59: 0, # 'Й' + 33: 0, # 'К' + 46: 0, # 'Л' + 38: 0, # 'Ðœ' + 36: 0, # 'Ð' + 41: 0, # 'О' + 30: 0, # 'П' + 39: 0, # 'Р' + 28: 0, # 'С' + 34: 0, # 'Т' + 51: 0, # 'У' + 48: 0, # 'Ф' + 49: 0, # 'Ð¥' + 53: 0, # 'Ц' + 50: 0, # 'Ч' + 54: 0, # 'Ш' + 57: 0, # 'Щ' + 61: 0, # 'Ъ' + 60: 0, # 'Ю' + 56: 0, # 'Я' + 1: 3, # 'а' + 18: 3, # 'б' + 9: 3, # 'в' + 20: 3, # 'г' + 11: 3, # 'д' + 3: 3, # 'е' + 23: 3, # 'ж' + 15: 2, # 'з' + 2: 3, # 'и' + 26: 0, # 'й' + 12: 3, # 'к' + 10: 3, # 'л' + 14: 3, # 'м' + 6: 3, # 'н' + 4: 3, # 'о' + 13: 2, # 'п' + 7: 1, # 'Ñ€' + 8: 3, # 'Ñ' + 5: 3, # 'Ñ‚' + 19: 3, # 'у' + 29: 2, # 'Ñ„' + 25: 3, # 'Ñ…' + 22: 3, # 'ц' + 21: 2, # 'ч' + 27: 3, # 'ш' + 24: 1, # 'щ' + 17: 3, # 'ÑŠ' + 52: 1, # 'ÑŒ' + 42: 2, # 'ÑŽ' + 16: 3, # 'Ñ' + 58: 0, # 'Ñ”' + 62: 0, # 'â„–' + }, + 8: { # 'Ñ' + 63: 1, # 'e' + 45: 0, # '\xad' + 31: 0, # 'Ð' + 32: 0, # 'Б' + 35: 0, # 'Ð’' + 43: 0, # 'Г' + 37: 0, # 'Д' + 44: 0, # 'Е' + 55: 0, # 'Ж' + 47: 0, # 'З' + 40: 0, # 'И' + 59: 0, # 'Й' + 33: 0, # 'К' + 46: 0, # 'Л' + 38: 0, # 'Ðœ' + 36: 0, # 'Ð' + 41: 0, # 'О' + 30: 0, # 'П' + 39: 0, # 'Р' + 28: 0, # 'С' + 34: 0, # 'Т' + 51: 0, # 'У' + 48: 0, # 'Ф' + 49: 0, # 'Ð¥' + 53: 0, # 'Ц' + 50: 0, # 'Ч' + 54: 0, # 'Ш' + 57: 0, # 'Щ' + 61: 0, # 'Ъ' + 60: 0, # 'Ю' + 56: 0, # 'Я' + 1: 3, # 'а' + 18: 2, # 'б' + 9: 3, # 'в' + 20: 2, # 'г' + 11: 2, # 'д' + 3: 3, # 'е' + 23: 0, # 'ж' + 15: 1, # 'з' + 2: 3, # 'и' + 26: 0, # 'й' + 12: 3, # 'к' + 10: 3, # 'л' + 14: 3, # 'м' + 6: 3, # 'н' + 4: 3, # 'о' + 13: 3, # 'п' + 7: 3, # 'Ñ€' + 8: 1, # 'Ñ' + 5: 3, # 'Ñ‚' + 19: 3, # 'у' + 29: 2, # 'Ñ„' + 25: 2, # 'Ñ…' + 22: 2, # 'ц' + 21: 2, # 'ч' + 27: 2, # 'ш' + 24: 0, # 'щ' + 17: 3, # 'ÑŠ' + 52: 2, # 'ÑŒ' + 42: 2, # 'ÑŽ' + 16: 3, # 'Ñ' + 58: 0, # 'Ñ”' + 62: 0, # 'â„–' + }, + 5: { # 'Ñ‚' + 63: 1, # 'e' + 45: 0, # '\xad' + 31: 0, # 'Ð' + 32: 0, # 'Б' + 35: 0, # 'Ð’' + 43: 0, # 'Г' + 37: 0, # 'Д' + 44: 0, # 'Е' + 55: 0, # 'Ж' + 47: 0, # 'З' + 40: 0, # 'И' + 59: 0, # 'Й' + 33: 0, # 'К' + 46: 0, # 'Л' + 38: 0, # 'Ðœ' + 36: 0, # 'Ð' + 41: 0, # 'О' + 30: 0, # 'П' + 39: 0, # 'Р' + 28: 0, # 'С' + 34: 0, # 'Т' + 51: 0, # 'У' + 48: 0, # 'Ф' + 49: 0, # 'Ð¥' + 53: 0, # 'Ц' + 50: 0, # 'Ч' + 54: 0, # 'Ш' + 57: 0, # 'Щ' + 61: 0, # 'Ъ' + 60: 0, # 'Ю' + 56: 0, # 'Я' + 1: 3, # 'а' + 18: 3, # 'б' + 9: 3, # 'в' + 20: 2, # 'г' + 11: 2, # 'д' + 3: 3, # 'е' + 23: 1, # 'ж' + 15: 1, # 'з' + 2: 3, # 'и' + 26: 0, # 'й' + 12: 3, # 'к' + 10: 3, # 'л' + 14: 2, # 'м' + 6: 3, # 'н' + 4: 3, # 'о' + 13: 2, # 'п' + 7: 3, # 'Ñ€' + 8: 3, # 'Ñ' + 5: 3, # 'Ñ‚' + 19: 3, # 'у' + 29: 1, # 'Ñ„' + 25: 2, # 'Ñ…' + 22: 2, # 'ц' + 21: 2, # 'ч' + 27: 1, # 'ш' + 24: 1, # 'щ' + 17: 3, # 'ÑŠ' + 52: 2, # 'ÑŒ' + 42: 2, # 'ÑŽ' + 16: 3, # 'Ñ' + 58: 0, # 'Ñ”' + 62: 0, # 'â„–' + }, + 19: { # 'у' + 63: 0, # 'e' + 45: 0, # '\xad' + 31: 0, # 'Ð' + 32: 0, # 'Б' + 35: 0, # 'Ð’' + 43: 0, # 'Г' + 37: 0, # 'Д' + 44: 0, # 'Е' + 55: 0, # 'Ж' + 47: 0, # 'З' + 40: 0, # 'И' + 59: 0, # 'Й' + 33: 0, # 'К' + 46: 0, # 'Л' + 38: 0, # 'Ðœ' + 36: 0, # 'Ð' + 41: 0, # 'О' + 30: 0, # 'П' + 39: 0, # 'Р' + 28: 0, # 'С' + 34: 0, # 'Т' + 51: 0, # 'У' + 48: 0, # 'Ф' + 49: 0, # 'Ð¥' + 53: 0, # 'Ц' + 50: 0, # 'Ч' + 54: 0, # 'Ш' + 57: 0, # 'Щ' + 61: 0, # 'Ъ' + 60: 0, # 'Ю' + 56: 0, # 'Я' + 1: 3, # 'а' + 18: 3, # 'б' + 9: 3, # 'в' + 20: 3, # 'г' + 11: 3, # 'д' + 3: 2, # 'е' + 23: 3, # 'ж' + 15: 3, # 'з' + 2: 2, # 'и' + 26: 2, # 'й' + 12: 3, # 'к' + 10: 3, # 'л' + 14: 3, # 'м' + 6: 3, # 'н' + 4: 2, # 'о' + 13: 3, # 'п' + 7: 3, # 'Ñ€' + 8: 3, # 'Ñ' + 5: 3, # 'Ñ‚' + 19: 1, # 'у' + 29: 2, # 'Ñ„' + 25: 2, # 'Ñ…' + 22: 2, # 'ц' + 21: 3, # 'ч' + 27: 3, # 'ш' + 24: 2, # 'щ' + 17: 1, # 'ÑŠ' + 52: 0, # 'ÑŒ' + 42: 1, # 'ÑŽ' + 16: 1, # 'Ñ' + 58: 0, # 'Ñ”' + 62: 0, # 'â„–' + }, + 29: { # 'Ñ„' + 63: 1, # 'e' + 45: 0, # '\xad' + 31: 0, # 'Ð' + 32: 0, # 'Б' + 35: 0, # 'Ð’' + 43: 0, # 'Г' + 37: 0, # 'Д' + 44: 0, # 'Е' + 55: 0, # 'Ж' + 47: 0, # 'З' + 40: 0, # 'И' + 59: 0, # 'Й' + 33: 0, # 'К' + 46: 0, # 'Л' + 38: 0, # 'Ðœ' + 36: 0, # 'Ð' + 41: 0, # 'О' + 30: 0, # 'П' + 39: 0, # 'Р' + 28: 0, # 'С' + 34: 0, # 'Т' + 51: 0, # 'У' + 48: 0, # 'Ф' + 49: 0, # 'Ð¥' + 53: 0, # 'Ц' + 50: 0, # 'Ч' + 54: 0, # 'Ш' + 57: 0, # 'Щ' + 61: 0, # 'Ъ' + 60: 0, # 'Ю' + 56: 0, # 'Я' + 1: 3, # 'а' + 18: 1, # 'б' + 9: 1, # 'в' + 20: 1, # 'г' + 11: 0, # 'д' + 3: 3, # 'е' + 23: 0, # 'ж' + 15: 0, # 'з' + 2: 3, # 'и' + 26: 0, # 'й' + 12: 2, # 'к' + 10: 2, # 'л' + 14: 1, # 'м' + 6: 1, # 'н' + 4: 3, # 'о' + 13: 0, # 'п' + 7: 2, # 'Ñ€' + 8: 2, # 'Ñ' + 5: 2, # 'Ñ‚' + 19: 2, # 'у' + 29: 0, # 'Ñ„' + 25: 1, # 'Ñ…' + 22: 0, # 'ц' + 21: 1, # 'ч' + 27: 1, # 'ш' + 24: 0, # 'щ' + 17: 2, # 'ÑŠ' + 52: 2, # 'ÑŒ' + 42: 1, # 'ÑŽ' + 16: 1, # 'Ñ' + 58: 0, # 'Ñ”' + 62: 0, # 'â„–' + }, + 25: { # 'Ñ…' + 63: 0, # 'e' + 45: 0, # '\xad' + 31: 0, # 'Ð' + 32: 0, # 'Б' + 35: 0, # 'Ð’' + 43: 0, # 'Г' + 37: 0, # 'Д' + 44: 0, # 'Е' + 55: 0, # 'Ж' + 47: 0, # 'З' + 40: 0, # 'И' + 59: 0, # 'Й' + 33: 0, # 'К' + 46: 0, # 'Л' + 38: 0, # 'Ðœ' + 36: 0, # 'Ð' + 41: 0, # 'О' + 30: 0, # 'П' + 39: 0, # 'Р' + 28: 0, # 'С' + 34: 0, # 'Т' + 51: 0, # 'У' + 48: 0, # 'Ф' + 49: 0, # 'Ð¥' + 53: 0, # 'Ц' + 50: 0, # 'Ч' + 54: 0, # 'Ш' + 57: 0, # 'Щ' + 61: 0, # 'Ъ' + 60: 0, # 'Ю' + 56: 0, # 'Я' + 1: 3, # 'а' + 18: 1, # 'б' + 9: 3, # 'в' + 20: 0, # 'г' + 11: 1, # 'д' + 3: 2, # 'е' + 23: 0, # 'ж' + 15: 1, # 'з' + 2: 3, # 'и' + 26: 0, # 'й' + 12: 1, # 'к' + 10: 2, # 'л' + 14: 2, # 'м' + 6: 3, # 'н' + 4: 3, # 'о' + 13: 1, # 'п' + 7: 3, # 'Ñ€' + 8: 1, # 'Ñ' + 5: 2, # 'Ñ‚' + 19: 3, # 'у' + 29: 0, # 'Ñ„' + 25: 1, # 'Ñ…' + 22: 0, # 'ц' + 21: 1, # 'ч' + 27: 0, # 'ш' + 24: 0, # 'щ' + 17: 2, # 'ÑŠ' + 52: 0, # 'ÑŒ' + 42: 1, # 'ÑŽ' + 16: 1, # 'Ñ' + 58: 0, # 'Ñ”' + 62: 0, # 'â„–' + }, + 22: { # 'ц' + 63: 1, # 'e' + 45: 0, # '\xad' + 31: 0, # 'Ð' + 32: 0, # 'Б' + 35: 0, # 'Ð’' + 43: 0, # 'Г' + 37: 0, # 'Д' + 44: 0, # 'Е' + 55: 0, # 'Ж' + 47: 0, # 'З' + 40: 0, # 'И' + 59: 0, # 'Й' + 33: 0, # 'К' + 46: 0, # 'Л' + 38: 0, # 'Ðœ' + 36: 0, # 'Ð' + 41: 0, # 'О' + 30: 0, # 'П' + 39: 0, # 'Р' + 28: 0, # 'С' + 34: 0, # 'Т' + 51: 0, # 'У' + 48: 0, # 'Ф' + 49: 0, # 'Ð¥' + 53: 0, # 'Ц' + 50: 0, # 'Ч' + 54: 0, # 'Ш' + 57: 0, # 'Щ' + 61: 0, # 'Ъ' + 60: 0, # 'Ю' + 56: 0, # 'Я' + 1: 3, # 'а' + 18: 1, # 'б' + 9: 2, # 'в' + 20: 1, # 'г' + 11: 1, # 'д' + 3: 3, # 'е' + 23: 0, # 'ж' + 15: 1, # 'з' + 2: 3, # 'и' + 26: 0, # 'й' + 12: 2, # 'к' + 10: 1, # 'л' + 14: 1, # 'м' + 6: 1, # 'н' + 4: 2, # 'о' + 13: 1, # 'п' + 7: 1, # 'Ñ€' + 8: 1, # 'Ñ' + 5: 1, # 'Ñ‚' + 19: 2, # 'у' + 29: 1, # 'Ñ„' + 25: 1, # 'Ñ…' + 22: 1, # 'ц' + 21: 1, # 'ч' + 27: 1, # 'ш' + 24: 1, # 'щ' + 17: 2, # 'ÑŠ' + 52: 1, # 'ÑŒ' + 42: 0, # 'ÑŽ' + 16: 2, # 'Ñ' + 58: 0, # 'Ñ”' + 62: 0, # 'â„–' + }, + 21: { # 'ч' + 63: 1, # 'e' + 45: 0, # '\xad' + 31: 0, # 'Ð' + 32: 0, # 'Б' + 35: 0, # 'Ð’' + 43: 0, # 'Г' + 37: 0, # 'Д' + 44: 0, # 'Е' + 55: 0, # 'Ж' + 47: 0, # 'З' + 40: 0, # 'И' + 59: 0, # 'Й' + 33: 0, # 'К' + 46: 0, # 'Л' + 38: 0, # 'Ðœ' + 36: 0, # 'Ð' + 41: 0, # 'О' + 30: 0, # 'П' + 39: 0, # 'Р' + 28: 0, # 'С' + 34: 0, # 'Т' + 51: 0, # 'У' + 48: 0, # 'Ф' + 49: 0, # 'Ð¥' + 53: 0, # 'Ц' + 50: 0, # 'Ч' + 54: 0, # 'Ш' + 57: 0, # 'Щ' + 61: 0, # 'Ъ' + 60: 0, # 'Ю' + 56: 0, # 'Я' + 1: 3, # 'а' + 18: 1, # 'б' + 9: 3, # 'в' + 20: 1, # 'г' + 11: 0, # 'д' + 3: 3, # 'е' + 23: 1, # 'ж' + 15: 0, # 'з' + 2: 3, # 'и' + 26: 0, # 'й' + 12: 3, # 'к' + 10: 2, # 'л' + 14: 2, # 'м' + 6: 3, # 'н' + 4: 3, # 'о' + 13: 0, # 'п' + 7: 2, # 'Ñ€' + 8: 0, # 'Ñ' + 5: 2, # 'Ñ‚' + 19: 3, # 'у' + 29: 0, # 'Ñ„' + 25: 0, # 'Ñ…' + 22: 0, # 'ц' + 21: 0, # 'ч' + 27: 1, # 'ш' + 24: 0, # 'щ' + 17: 2, # 'ÑŠ' + 52: 0, # 'ÑŒ' + 42: 1, # 'ÑŽ' + 16: 0, # 'Ñ' + 58: 0, # 'Ñ”' + 62: 0, # 'â„–' + }, + 27: { # 'ш' + 63: 1, # 'e' + 45: 0, # '\xad' + 31: 0, # 'Ð' + 32: 0, # 'Б' + 35: 0, # 'Ð’' + 43: 0, # 'Г' + 37: 0, # 'Д' + 44: 0, # 'Е' + 55: 0, # 'Ж' + 47: 0, # 'З' + 40: 0, # 'И' + 59: 0, # 'Й' + 33: 0, # 'К' + 46: 0, # 'Л' + 38: 0, # 'Ðœ' + 36: 0, # 'Ð' + 41: 0, # 'О' + 30: 0, # 'П' + 39: 0, # 'Р' + 28: 0, # 'С' + 34: 0, # 'Т' + 51: 0, # 'У' + 48: 0, # 'Ф' + 49: 0, # 'Ð¥' + 53: 0, # 'Ц' + 50: 0, # 'Ч' + 54: 0, # 'Ш' + 57: 0, # 'Щ' + 61: 0, # 'Ъ' + 60: 0, # 'Ю' + 56: 0, # 'Я' + 1: 3, # 'а' + 18: 0, # 'б' + 9: 2, # 'в' + 20: 0, # 'г' + 11: 1, # 'д' + 3: 3, # 'е' + 23: 0, # 'ж' + 15: 0, # 'з' + 2: 3, # 'и' + 26: 0, # 'й' + 12: 3, # 'к' + 10: 2, # 'л' + 14: 1, # 'м' + 6: 3, # 'н' + 4: 2, # 'о' + 13: 2, # 'п' + 7: 1, # 'Ñ€' + 8: 0, # 'Ñ' + 5: 1, # 'Ñ‚' + 19: 2, # 'у' + 29: 1, # 'Ñ„' + 25: 0, # 'Ñ…' + 22: 0, # 'ц' + 21: 1, # 'ч' + 27: 0, # 'ш' + 24: 0, # 'щ' + 17: 2, # 'ÑŠ' + 52: 1, # 'ÑŒ' + 42: 1, # 'ÑŽ' + 16: 0, # 'Ñ' + 58: 0, # 'Ñ”' + 62: 0, # 'â„–' + }, + 24: { # 'щ' + 63: 1, # 'e' + 45: 0, # '\xad' + 31: 0, # 'Ð' + 32: 0, # 'Б' + 35: 0, # 'Ð’' + 43: 0, # 'Г' + 37: 0, # 'Д' + 44: 0, # 'Е' + 55: 0, # 'Ж' + 47: 0, # 'З' + 40: 0, # 'И' + 59: 0, # 'Й' + 33: 0, # 'К' + 46: 0, # 'Л' + 38: 0, # 'Ðœ' + 36: 0, # 'Ð' + 41: 0, # 'О' + 30: 0, # 'П' + 39: 0, # 'Р' + 28: 0, # 'С' + 34: 0, # 'Т' + 51: 0, # 'У' + 48: 0, # 'Ф' + 49: 0, # 'Ð¥' + 53: 0, # 'Ц' + 50: 0, # 'Ч' + 54: 0, # 'Ш' + 57: 0, # 'Щ' + 61: 0, # 'Ъ' + 60: 0, # 'Ю' + 56: 0, # 'Я' + 1: 3, # 'а' + 18: 0, # 'б' + 9: 1, # 'в' + 20: 0, # 'г' + 11: 0, # 'д' + 3: 3, # 'е' + 23: 0, # 'ж' + 15: 0, # 'з' + 2: 3, # 'и' + 26: 0, # 'й' + 12: 1, # 'к' + 10: 0, # 'л' + 14: 0, # 'м' + 6: 2, # 'н' + 4: 3, # 'о' + 13: 0, # 'п' + 7: 1, # 'Ñ€' + 8: 0, # 'Ñ' + 5: 2, # 'Ñ‚' + 19: 3, # 'у' + 29: 0, # 'Ñ„' + 25: 0, # 'Ñ…' + 22: 1, # 'ц' + 21: 0, # 'ч' + 27: 0, # 'ш' + 24: 0, # 'щ' + 17: 1, # 'ÑŠ' + 52: 0, # 'ÑŒ' + 42: 0, # 'ÑŽ' + 16: 2, # 'Ñ' + 58: 0, # 'Ñ”' + 62: 0, # 'â„–' + }, + 17: { # 'ÑŠ' + 63: 0, # 'e' + 45: 0, # '\xad' + 31: 0, # 'Ð' + 32: 0, # 'Б' + 35: 0, # 'Ð’' + 43: 0, # 'Г' + 37: 0, # 'Д' + 44: 0, # 'Е' + 55: 0, # 'Ж' + 47: 0, # 'З' + 40: 0, # 'И' + 59: 0, # 'Й' + 33: 0, # 'К' + 46: 0, # 'Л' + 38: 0, # 'Ðœ' + 36: 0, # 'Ð' + 41: 0, # 'О' + 30: 0, # 'П' + 39: 0, # 'Р' + 28: 0, # 'С' + 34: 0, # 'Т' + 51: 0, # 'У' + 48: 0, # 'Ф' + 49: 0, # 'Ð¥' + 53: 0, # 'Ц' + 50: 0, # 'Ч' + 54: 0, # 'Ш' + 57: 0, # 'Щ' + 61: 0, # 'Ъ' + 60: 0, # 'Ю' + 56: 0, # 'Я' + 1: 1, # 'а' + 18: 3, # 'б' + 9: 3, # 'в' + 20: 3, # 'г' + 11: 3, # 'д' + 3: 2, # 'е' + 23: 3, # 'ж' + 15: 3, # 'з' + 2: 1, # 'и' + 26: 2, # 'й' + 12: 3, # 'к' + 10: 3, # 'л' + 14: 3, # 'м' + 6: 3, # 'н' + 4: 3, # 'о' + 13: 3, # 'п' + 7: 3, # 'Ñ€' + 8: 3, # 'Ñ' + 5: 3, # 'Ñ‚' + 19: 1, # 'у' + 29: 1, # 'Ñ„' + 25: 2, # 'Ñ…' + 22: 2, # 'ц' + 21: 3, # 'ч' + 27: 2, # 'ш' + 24: 3, # 'щ' + 17: 0, # 'ÑŠ' + 52: 0, # 'ÑŒ' + 42: 2, # 'ÑŽ' + 16: 0, # 'Ñ' + 58: 0, # 'Ñ”' + 62: 0, # 'â„–' + }, + 52: { # 'ÑŒ' + 63: 0, # 'e' + 45: 0, # '\xad' + 31: 0, # 'Ð' + 32: 0, # 'Б' + 35: 0, # 'Ð’' + 43: 0, # 'Г' + 37: 0, # 'Д' + 44: 0, # 'Е' + 55: 0, # 'Ж' + 47: 0, # 'З' + 40: 0, # 'И' + 59: 0, # 'Й' + 33: 0, # 'К' + 46: 0, # 'Л' + 38: 0, # 'Ðœ' + 36: 0, # 'Ð' + 41: 0, # 'О' + 30: 0, # 'П' + 39: 0, # 'Р' + 28: 0, # 'С' + 34: 0, # 'Т' + 51: 0, # 'У' + 48: 0, # 'Ф' + 49: 0, # 'Ð¥' + 53: 0, # 'Ц' + 50: 0, # 'Ч' + 54: 0, # 'Ш' + 57: 0, # 'Щ' + 61: 0, # 'Ъ' + 60: 0, # 'Ю' + 56: 0, # 'Я' + 1: 0, # 'а' + 18: 0, # 'б' + 9: 0, # 'в' + 20: 0, # 'г' + 11: 0, # 'д' + 3: 1, # 'е' + 23: 0, # 'ж' + 15: 0, # 'з' + 2: 0, # 'и' + 26: 0, # 'й' + 12: 1, # 'к' + 10: 0, # 'л' + 14: 0, # 'м' + 6: 1, # 'н' + 4: 3, # 'о' + 13: 0, # 'п' + 7: 0, # 'Ñ€' + 8: 0, # 'Ñ' + 5: 1, # 'Ñ‚' + 19: 0, # 'у' + 29: 0, # 'Ñ„' + 25: 0, # 'Ñ…' + 22: 1, # 'ц' + 21: 0, # 'ч' + 27: 0, # 'ш' + 24: 0, # 'щ' + 17: 0, # 'ÑŠ' + 52: 0, # 'ÑŒ' + 42: 1, # 'ÑŽ' + 16: 0, # 'Ñ' + 58: 0, # 'Ñ”' + 62: 0, # 'â„–' + }, + 42: { # 'ÑŽ' + 63: 0, # 'e' + 45: 0, # '\xad' + 31: 0, # 'Ð' + 32: 0, # 'Б' + 35: 0, # 'Ð’' + 43: 0, # 'Г' + 37: 0, # 'Д' + 44: 0, # 'Е' + 55: 0, # 'Ж' + 47: 0, # 'З' + 40: 0, # 'И' + 59: 0, # 'Й' + 33: 0, # 'К' + 46: 0, # 'Л' + 38: 0, # 'Ðœ' + 36: 0, # 'Ð' + 41: 0, # 'О' + 30: 0, # 'П' + 39: 0, # 'Р' + 28: 0, # 'С' + 34: 0, # 'Т' + 51: 0, # 'У' + 48: 0, # 'Ф' + 49: 0, # 'Ð¥' + 53: 0, # 'Ц' + 50: 0, # 'Ч' + 54: 0, # 'Ш' + 57: 0, # 'Щ' + 61: 0, # 'Ъ' + 60: 0, # 'Ю' + 56: 0, # 'Я' + 1: 1, # 'а' + 18: 2, # 'б' + 9: 1, # 'в' + 20: 2, # 'г' + 11: 2, # 'д' + 3: 1, # 'е' + 23: 2, # 'ж' + 15: 2, # 'з' + 2: 1, # 'и' + 26: 1, # 'й' + 12: 2, # 'к' + 10: 2, # 'л' + 14: 2, # 'м' + 6: 2, # 'н' + 4: 1, # 'о' + 13: 1, # 'п' + 7: 2, # 'Ñ€' + 8: 2, # 'Ñ' + 5: 2, # 'Ñ‚' + 19: 1, # 'у' + 29: 1, # 'Ñ„' + 25: 1, # 'Ñ…' + 22: 2, # 'ц' + 21: 3, # 'ч' + 27: 1, # 'ш' + 24: 1, # 'щ' + 17: 1, # 'ÑŠ' + 52: 0, # 'ÑŒ' + 42: 0, # 'ÑŽ' + 16: 1, # 'Ñ' + 58: 0, # 'Ñ”' + 62: 0, # 'â„–' + }, + 16: { # 'Ñ' + 63: 0, # 'e' + 45: 1, # '\xad' + 31: 0, # 'Ð' + 32: 0, # 'Б' + 35: 0, # 'Ð’' + 43: 0, # 'Г' + 37: 0, # 'Д' + 44: 0, # 'Е' + 55: 0, # 'Ж' + 47: 0, # 'З' + 40: 0, # 'И' + 59: 0, # 'Й' + 33: 0, # 'К' + 46: 0, # 'Л' + 38: 0, # 'Ðœ' + 36: 0, # 'Ð' + 41: 0, # 'О' + 30: 0, # 'П' + 39: 0, # 'Р' + 28: 0, # 'С' + 34: 0, # 'Т' + 51: 0, # 'У' + 48: 0, # 'Ф' + 49: 0, # 'Ð¥' + 53: 0, # 'Ц' + 50: 0, # 'Ч' + 54: 0, # 'Ш' + 57: 0, # 'Щ' + 61: 0, # 'Ъ' + 60: 0, # 'Ю' + 56: 0, # 'Я' + 1: 0, # 'а' + 18: 3, # 'б' + 9: 3, # 'в' + 20: 2, # 'г' + 11: 3, # 'д' + 3: 2, # 'е' + 23: 1, # 'ж' + 15: 2, # 'з' + 2: 1, # 'и' + 26: 2, # 'й' + 12: 3, # 'к' + 10: 3, # 'л' + 14: 3, # 'м' + 6: 3, # 'н' + 4: 1, # 'о' + 13: 2, # 'п' + 7: 2, # 'Ñ€' + 8: 3, # 'Ñ' + 5: 3, # 'Ñ‚' + 19: 1, # 'у' + 29: 1, # 'Ñ„' + 25: 3, # 'Ñ…' + 22: 2, # 'ц' + 21: 1, # 'ч' + 27: 1, # 'ш' + 24: 2, # 'щ' + 17: 0, # 'ÑŠ' + 52: 0, # 'ÑŒ' + 42: 0, # 'ÑŽ' + 16: 1, # 'Ñ' + 58: 0, # 'Ñ”' + 62: 0, # 'â„–' + }, + 58: { # 'Ñ”' + 63: 0, # 'e' + 45: 0, # '\xad' + 31: 0, # 'Ð' + 32: 0, # 'Б' + 35: 0, # 'Ð’' + 43: 0, # 'Г' + 37: 0, # 'Д' + 44: 0, # 'Е' + 55: 0, # 'Ж' + 47: 0, # 'З' + 40: 0, # 'И' + 59: 0, # 'Й' + 33: 0, # 'К' + 46: 0, # 'Л' + 38: 0, # 'Ðœ' + 36: 0, # 'Ð' + 41: 0, # 'О' + 30: 0, # 'П' + 39: 0, # 'Р' + 28: 0, # 'С' + 34: 0, # 'Т' + 51: 0, # 'У' + 48: 0, # 'Ф' + 49: 0, # 'Ð¥' + 53: 0, # 'Ц' + 50: 0, # 'Ч' + 54: 0, # 'Ш' + 57: 0, # 'Щ' + 61: 0, # 'Ъ' + 60: 0, # 'Ю' + 56: 0, # 'Я' + 1: 0, # 'а' + 18: 0, # 'б' + 9: 0, # 'в' + 20: 0, # 'г' + 11: 0, # 'д' + 3: 0, # 'е' + 23: 0, # 'ж' + 15: 0, # 'з' + 2: 0, # 'и' + 26: 0, # 'й' + 12: 0, # 'к' + 10: 0, # 'л' + 14: 0, # 'м' + 6: 0, # 'н' + 4: 0, # 'о' + 13: 0, # 'п' + 7: 0, # 'Ñ€' + 8: 0, # 'Ñ' + 5: 0, # 'Ñ‚' + 19: 0, # 'у' + 29: 0, # 'Ñ„' + 25: 0, # 'Ñ…' + 22: 0, # 'ц' + 21: 0, # 'ч' + 27: 0, # 'ш' + 24: 0, # 'щ' + 17: 0, # 'ÑŠ' + 52: 0, # 'ÑŒ' + 42: 0, # 'ÑŽ' + 16: 0, # 'Ñ' + 58: 0, # 'Ñ”' + 62: 0, # 'â„–' + }, + 62: { # 'â„–' + 63: 0, # 'e' + 45: 0, # '\xad' + 31: 0, # 'Ð' + 32: 0, # 'Б' + 35: 0, # 'Ð’' + 43: 0, # 'Г' + 37: 0, # 'Д' + 44: 0, # 'Е' + 55: 0, # 'Ж' + 47: 0, # 'З' + 40: 0, # 'И' + 59: 0, # 'Й' + 33: 0, # 'К' + 46: 0, # 'Л' + 38: 0, # 'Ðœ' + 36: 0, # 'Ð' + 41: 0, # 'О' + 30: 0, # 'П' + 39: 0, # 'Р' + 28: 0, # 'С' + 34: 0, # 'Т' + 51: 0, # 'У' + 48: 0, # 'Ф' + 49: 0, # 'Ð¥' + 53: 0, # 'Ц' + 50: 0, # 'Ч' + 54: 0, # 'Ш' + 57: 0, # 'Щ' + 61: 0, # 'Ъ' + 60: 0, # 'Ю' + 56: 0, # 'Я' + 1: 0, # 'а' + 18: 0, # 'б' + 9: 0, # 'в' + 20: 0, # 'г' + 11: 0, # 'д' + 3: 0, # 'е' + 23: 0, # 'ж' + 15: 0, # 'з' + 2: 0, # 'и' + 26: 0, # 'й' + 12: 0, # 'к' + 10: 0, # 'л' + 14: 0, # 'м' + 6: 0, # 'н' + 4: 0, # 'о' + 13: 0, # 'п' + 7: 0, # 'Ñ€' + 8: 0, # 'Ñ' + 5: 0, # 'Ñ‚' + 19: 0, # 'у' + 29: 0, # 'Ñ„' + 25: 0, # 'Ñ…' + 22: 0, # 'ц' + 21: 0, # 'ч' + 27: 0, # 'ш' + 24: 0, # 'щ' + 17: 0, # 'ÑŠ' + 52: 0, # 'ÑŒ' + 42: 0, # 'ÑŽ' + 16: 0, # 'Ñ' + 58: 0, # 'Ñ”' + 62: 0, # 'â„–' + }, +} + +# 255: Undefined characters that did not exist in training text # 254: Carriage/Return # 253: symbol (punctuation) that does not belong to word # 252: 0 - 9 +# 251: Control characters -# Character Mapping Table: -# this table is modified base on win1251BulgarianCharToOrderMap, so -# only number <64 is sure valid - -Latin5_BulgarianCharToOrderMap = ( -255,255,255,255,255,255,255,255,255,255,254,255,255,254,255,255, # 00 -255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, # 10 -253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253, # 20 -252,252,252,252,252,252,252,252,252,252,253,253,253,253,253,253, # 30 -253, 77, 90, 99,100, 72,109,107,101, 79,185, 81,102, 76, 94, 82, # 40 -110,186,108, 91, 74,119, 84, 96,111,187,115,253,253,253,253,253, # 50 -253, 65, 69, 70, 66, 63, 68,112,103, 92,194,104, 95, 86, 87, 71, # 60 -116,195, 85, 93, 97,113,196,197,198,199,200,253,253,253,253,253, # 70 -194,195,196,197,198,199,200,201,202,203,204,205,206,207,208,209, # 80 -210,211,212,213,214,215,216,217,218,219,220,221,222,223,224,225, # 90 - 81,226,227,228,229,230,105,231,232,233,234,235,236, 45,237,238, # a0 - 31, 32, 35, 43, 37, 44, 55, 47, 40, 59, 33, 46, 38, 36, 41, 30, # b0 - 39, 28, 34, 51, 48, 49, 53, 50, 54, 57, 61,239, 67,240, 60, 56, # c0 - 1, 18, 9, 20, 11, 3, 23, 15, 2, 26, 12, 10, 14, 6, 4, 13, # d0 - 7, 8, 5, 19, 29, 25, 22, 21, 27, 24, 17, 75, 52,241, 42, 16, # e0 - 62,242,243,244, 58,245, 98,246,247,248,249,250,251, 91,252,253, # f0 -) - -win1251BulgarianCharToOrderMap = ( -255,255,255,255,255,255,255,255,255,255,254,255,255,254,255,255, # 00 -255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, # 10 -253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253, # 20 -252,252,252,252,252,252,252,252,252,252,253,253,253,253,253,253, # 30 -253, 77, 90, 99,100, 72,109,107,101, 79,185, 81,102, 76, 94, 82, # 40 -110,186,108, 91, 74,119, 84, 96,111,187,115,253,253,253,253,253, # 50 -253, 65, 69, 70, 66, 63, 68,112,103, 92,194,104, 95, 86, 87, 71, # 60 -116,195, 85, 93, 97,113,196,197,198,199,200,253,253,253,253,253, # 70 -206,207,208,209,210,211,212,213,120,214,215,216,217,218,219,220, # 80 -221, 78, 64, 83,121, 98,117,105,222,223,224,225,226,227,228,229, # 90 - 88,230,231,232,233,122, 89,106,234,235,236,237,238, 45,239,240, # a0 - 73, 80,118,114,241,242,243,244,245, 62, 58,246,247,248,249,250, # b0 - 31, 32, 35, 43, 37, 44, 55, 47, 40, 59, 33, 46, 38, 36, 41, 30, # c0 - 39, 28, 34, 51, 48, 49, 53, 50, 54, 57, 61,251, 67,252, 60, 56, # d0 - 1, 18, 9, 20, 11, 3, 23, 15, 2, 26, 12, 10, 14, 6, 4, 13, # e0 - 7, 8, 5, 19, 29, 25, 22, 21, 27, 24, 17, 75, 52,253, 42, 16, # f0 -) - -# Model Table: -# total sequences: 100% -# first 512 sequences: 96.9392% -# first 1024 sequences:3.0618% -# rest sequences: 0.2992% -# negative sequences: 0.0020% -BulgarianLangModel = ( -0,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,2,3,3,3,3,3,3,3,3,2,3,3,3,3,3, -3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,0,3,3,3,2,2,3,2,2,1,2,2, -3,1,3,3,2,3,3,3,3,3,3,3,3,3,3,3,3,0,3,3,3,3,3,3,3,3,3,3,0,3,0,1, -0,0,0,0,0,0,0,0,0,0,1,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1, -3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,2,3,2,3,3,3,3,3,3,3,3,0,3,1,0, -0,1,0,0,0,0,0,0,0,0,1,1,0,1,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1, -3,2,2,2,3,3,3,3,3,3,3,3,3,3,3,3,3,1,3,2,3,3,3,3,3,3,3,3,0,3,0,0, -0,0,0,0,0,0,0,0,0,0,1,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -3,2,3,3,2,3,3,3,3,3,3,3,3,3,3,3,3,1,3,2,3,3,3,3,3,3,3,3,0,3,0,0, -0,0,0,0,0,0,0,0,0,0,1,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -3,3,3,3,3,3,3,3,3,3,3,2,3,2,2,1,3,3,3,3,2,2,2,1,1,2,0,1,0,1,0,0, -0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,1, -3,3,3,3,3,3,3,2,3,2,2,3,3,1,1,2,3,3,2,3,3,3,3,2,1,2,0,2,0,3,0,0, -0,0,0,0,0,0,0,1,0,0,2,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,1, -3,3,3,3,3,3,3,1,3,3,3,3,3,2,3,2,3,3,3,3,3,2,3,3,1,3,0,3,0,2,0,0, -0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,1, -3,3,3,3,3,3,3,3,1,3,3,2,3,3,3,1,3,3,2,3,2,2,2,0,0,2,0,2,0,2,0,0, -0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,1, -3,3,3,3,3,3,3,3,3,0,3,3,3,2,2,3,3,3,1,2,2,3,2,1,1,2,0,2,0,0,0,0, -1,0,0,0,0,0,0,0,0,0,2,0,0,1,0,0,1,0,0,0,1,0,0,0,0,0,0,0,0,0,0,1, -3,3,3,3,3,3,3,2,3,3,1,2,3,2,2,2,3,3,3,3,3,2,2,3,1,2,0,2,1,2,0,0, -0,0,0,0,0,0,0,0,0,0,3,0,0,1,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,1, -3,3,3,3,3,1,3,3,3,3,3,2,3,3,3,2,3,3,2,3,2,2,2,3,1,2,0,1,0,1,0,0, -0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,1, -3,3,3,3,3,3,3,3,3,3,3,1,1,1,2,2,1,3,1,3,2,2,3,0,0,1,0,1,0,1,0,0, -0,0,0,1,0,0,0,0,1,0,2,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,1, -3,3,3,3,3,2,2,3,2,2,3,1,2,1,1,1,2,3,1,3,1,2,2,0,1,1,1,1,0,1,0,0, -0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,1, -3,3,3,3,3,1,3,2,2,3,3,1,2,3,1,1,3,3,3,3,1,2,2,1,1,1,0,2,0,2,0,1, -0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,1, -3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,1,2,2,3,3,3,2,2,1,1,2,0,2,0,1,0,0, -0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,1, -3,0,1,2,1,3,3,2,3,3,3,3,3,2,3,2,1,0,3,1,2,1,2,1,2,3,2,1,0,1,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -1,1,1,2,3,3,3,3,3,3,3,3,3,3,3,3,0,0,3,1,3,3,2,3,3,2,2,2,0,1,0,0, -0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -2,3,3,3,3,0,3,3,3,3,3,2,1,1,2,1,3,3,0,3,1,1,1,1,3,2,0,1,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,1, -3,3,2,2,2,3,3,3,3,3,3,3,3,3,3,3,1,1,3,1,3,3,2,3,2,2,2,3,0,2,0,0, -0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -3,3,3,3,3,2,3,3,2,2,3,2,1,1,1,1,1,3,1,3,1,1,0,0,0,1,0,0,0,1,0,0, -0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0, -3,3,3,3,3,2,3,2,0,3,2,0,3,0,2,0,0,2,1,3,1,0,0,1,0,0,0,1,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1, -3,3,3,3,2,1,1,1,1,2,1,1,2,1,1,1,2,2,1,2,1,1,1,0,1,1,0,1,0,1,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,1, -3,3,3,3,2,1,3,1,1,2,1,3,2,1,1,0,1,2,3,2,1,1,1,0,0,0,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -2,3,3,3,3,2,2,1,0,1,0,0,1,0,0,0,2,1,0,3,0,0,1,0,0,0,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1, -3,3,3,2,3,2,3,3,1,3,2,1,1,1,2,1,1,2,1,3,0,1,0,0,0,1,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -3,1,1,2,2,3,3,2,3,2,2,2,3,1,2,2,1,1,2,1,1,2,2,0,1,1,0,1,0,2,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -3,3,3,3,2,1,3,1,0,2,2,1,3,2,1,0,0,2,0,2,0,1,0,0,0,0,0,0,0,1,0,0, -0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,1, -3,3,3,3,3,3,1,2,0,2,3,1,2,3,2,0,1,3,1,2,1,1,1,0,0,1,0,0,2,2,2,3, -2,2,2,2,1,2,1,1,2,2,1,1,2,0,1,1,1,0,0,1,1,0,0,1,1,0,0,0,1,1,0,1, -3,3,3,3,3,2,1,2,2,1,2,0,2,0,1,0,1,2,1,2,1,1,0,0,0,1,0,1,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,1, -3,3,2,3,3,1,1,3,1,0,3,2,1,0,0,0,1,2,0,2,0,1,0,0,0,1,0,1,2,1,2,2, -1,1,1,1,1,1,1,2,2,2,1,1,1,1,1,1,1,0,1,2,1,1,1,0,0,0,0,0,1,1,0,0, -3,1,0,1,0,2,3,2,2,2,3,2,2,2,2,2,1,0,2,1,2,1,1,1,0,1,2,1,2,2,2,1, -1,1,2,2,2,2,1,2,1,1,0,1,2,1,2,2,2,1,1,1,0,1,1,1,1,2,0,1,0,0,0,0, -2,3,2,3,3,0,0,2,1,0,2,1,0,0,0,0,2,3,0,2,0,0,0,0,0,1,0,0,2,0,1,2, -2,1,2,1,2,2,1,1,1,2,1,1,1,0,1,2,2,1,1,1,1,1,0,1,1,1,0,0,1,2,0,0, -3,3,2,2,3,0,2,3,1,1,2,0,0,0,1,0,0,2,0,2,0,0,0,1,0,1,0,1,2,0,2,2, -1,1,1,1,2,1,0,1,2,2,2,1,1,1,1,1,1,1,0,1,1,1,0,0,0,0,0,0,1,1,0,0, -2,3,2,3,3,0,0,3,0,1,1,0,1,0,0,0,2,2,1,2,0,0,0,0,0,0,0,0,2,0,1,2, -2,2,1,1,1,1,1,2,2,2,1,0,2,0,1,0,1,0,0,1,0,1,0,0,1,0,0,0,0,1,0,0, -3,3,3,3,2,2,2,2,2,0,2,1,1,1,1,2,1,2,1,1,0,2,0,1,0,1,0,0,2,0,1,2, -1,1,1,1,1,1,1,2,2,1,1,0,2,0,1,0,2,0,0,1,1,1,0,0,2,0,0,0,1,1,0,0, -2,3,3,3,3,1,0,0,0,0,0,0,0,0,0,0,2,0,0,1,1,0,0,0,0,0,0,1,2,0,1,2, -2,2,2,1,1,2,1,1,2,2,2,1,2,0,1,1,1,1,1,1,0,1,1,1,1,0,0,1,1,1,0,0, -2,3,3,3,3,0,2,2,0,2,1,0,0,0,1,1,1,2,0,2,0,0,0,3,0,0,0,0,2,0,2,2, -1,1,1,2,1,2,1,1,2,2,2,1,2,0,1,1,1,0,1,1,1,1,0,2,1,0,0,0,1,1,0,0, -2,3,3,3,3,0,2,1,0,0,2,0,0,0,0,0,1,2,0,2,0,0,0,0,0,0,0,0,2,0,1,2, -1,1,1,2,1,1,1,1,2,2,2,0,1,0,1,1,1,0,0,1,1,1,0,0,1,0,0,0,0,1,0,0, -3,3,2,2,3,0,1,0,1,0,0,0,0,0,0,0,1,1,0,3,0,0,0,0,0,0,0,0,1,0,2,2, -1,1,1,1,1,2,1,1,2,2,1,2,2,1,0,1,1,1,1,1,0,1,0,0,1,0,0,0,1,1,0,0, -3,1,0,1,0,2,2,2,2,3,2,1,1,1,2,3,0,0,1,0,2,1,1,0,1,1,1,1,2,1,1,1, -1,2,2,1,2,1,2,2,1,1,0,1,2,1,2,2,1,1,1,0,0,1,1,1,2,1,0,1,0,0,0,0, -2,1,0,1,0,3,1,2,2,2,2,1,2,2,1,1,1,0,2,1,2,2,1,1,2,1,1,0,2,1,1,1, -1,2,2,2,2,2,2,2,1,2,0,1,1,0,2,1,1,1,1,1,0,0,1,1,1,1,0,1,0,0,0,0, -2,1,1,1,1,2,2,2,2,1,2,2,2,1,2,2,1,1,2,1,2,3,2,2,1,1,1,1,0,1,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -2,2,2,3,2,0,1,2,0,1,2,1,1,0,1,0,1,2,1,2,0,0,0,1,1,0,0,0,1,0,0,2, -1,1,0,0,1,1,0,1,1,1,1,0,2,0,1,1,1,0,0,1,1,0,0,0,0,1,0,0,0,1,0,0, -2,0,0,0,0,1,2,2,2,2,2,2,2,1,2,1,1,1,1,1,1,1,0,1,1,1,1,1,2,1,1,1, -1,2,2,2,2,1,1,2,1,2,1,1,1,0,2,1,2,1,1,1,0,2,1,1,1,1,0,1,0,0,0,0, -3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,1,0, -1,1,0,1,0,1,1,1,1,1,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -2,2,2,3,2,0,0,0,0,1,0,0,0,0,0,0,1,1,0,2,0,0,0,0,0,0,0,0,1,0,1,2, -1,1,1,1,1,1,0,0,2,2,2,2,2,0,1,1,0,1,1,1,1,1,0,0,1,0,0,0,1,1,0,1, -2,3,1,2,1,0,1,1,0,2,2,2,0,0,1,0,0,1,1,1,1,0,0,0,0,0,0,0,1,0,1,2, -1,1,1,1,2,1,1,1,1,1,1,1,1,0,1,1,0,1,0,1,0,1,0,0,1,0,0,0,0,1,0,0, -2,2,2,2,2,0,0,2,0,0,2,0,0,0,0,0,0,1,0,1,0,0,0,0,0,0,0,0,2,0,2,2, -1,1,1,1,1,0,0,1,2,1,1,0,1,0,1,0,0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0, -1,2,2,2,2,0,0,2,0,1,1,0,0,0,1,0,0,2,0,2,0,0,0,0,0,0,0,0,0,0,1,1, -0,0,0,1,1,1,1,1,1,1,1,1,1,0,1,0,0,1,0,0,1,0,0,0,0,0,0,0,0,0,0,0, -1,2,2,3,2,0,0,1,0,0,1,0,0,0,0,0,0,1,0,2,0,0,0,1,0,0,0,0,0,0,0,2, -1,1,0,0,1,0,0,0,1,1,0,0,1,0,1,1,0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0, -2,1,2,2,2,1,2,1,2,2,1,1,2,1,1,1,0,1,1,1,1,2,0,1,0,1,1,1,1,0,1,1, -1,1,2,1,1,1,1,1,1,0,0,1,2,1,1,1,1,1,1,0,0,1,1,1,0,0,0,0,0,0,0,0, -1,0,0,1,3,1,1,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -2,2,2,2,1,0,0,1,0,2,0,0,0,0,0,1,1,1,0,1,0,0,0,0,0,0,0,0,2,0,0,1, -0,2,0,1,0,0,1,1,2,0,1,0,1,0,1,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0, -1,2,2,2,2,0,1,1,0,2,1,0,1,1,1,0,0,1,0,2,0,1,0,0,0,0,0,0,0,0,0,1, -0,1,0,0,1,0,0,0,1,1,0,0,1,0,0,1,0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0, -2,2,2,2,2,0,0,1,0,0,0,1,0,1,0,0,0,1,0,1,0,0,0,0,0,0,0,0,0,0,0,1, -0,1,0,1,1,1,0,0,1,1,1,0,1,0,0,0,0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0, -2,0,1,0,0,1,2,1,1,1,1,1,1,2,2,1,0,0,1,0,1,0,0,0,0,1,1,1,1,0,0,0, -1,1,2,1,1,1,1,0,0,0,1,1,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -2,2,1,2,1,0,0,1,0,0,0,0,0,0,0,0,1,1,0,1,0,0,0,0,0,0,0,0,0,0,0,1, -0,0,0,0,0,0,0,0,1,1,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -1,0,0,1,2,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,1,0,0,0, -0,1,1,0,1,1,1,0,0,1,0,0,1,0,1,0,0,0,1,0,0,0,0,0,1,0,0,0,0,0,0,0, -1,0,1,0,0,1,1,1,1,1,1,1,1,1,1,1,0,0,1,0,2,0,0,2,0,1,0,0,1,0,0,1, -1,1,0,0,1,1,0,1,0,0,0,1,0,0,1,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0, -0,0,0,0,0,0,1,1,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,1,0, -1,1,1,1,1,1,1,2,0,0,0,0,0,0,2,1,0,1,1,0,0,1,1,1,0,1,0,0,0,0,0,0, -2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -1,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,1,0,1,1,0,1,1,1,1,1,0,1,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1, -) - -Latin5BulgarianModel = { - 'char_to_order_map': Latin5_BulgarianCharToOrderMap, - 'precedence_matrix': BulgarianLangModel, - 'typical_positive_ratio': 0.969392, - 'keep_english_letter': False, - 'charset_name': "ISO-8859-5", - 'language': 'Bulgairan', +# Character Mapping Table(s): +ISO_8859_5_BULGARIAN_CHAR_TO_ORDER = { + 0: 255, # '\x00' + 1: 255, # '\x01' + 2: 255, # '\x02' + 3: 255, # '\x03' + 4: 255, # '\x04' + 5: 255, # '\x05' + 6: 255, # '\x06' + 7: 255, # '\x07' + 8: 255, # '\x08' + 9: 255, # '\t' + 10: 254, # '\n' + 11: 255, # '\x0b' + 12: 255, # '\x0c' + 13: 254, # '\r' + 14: 255, # '\x0e' + 15: 255, # '\x0f' + 16: 255, # '\x10' + 17: 255, # '\x11' + 18: 255, # '\x12' + 19: 255, # '\x13' + 20: 255, # '\x14' + 21: 255, # '\x15' + 22: 255, # '\x16' + 23: 255, # '\x17' + 24: 255, # '\x18' + 25: 255, # '\x19' + 26: 255, # '\x1a' + 27: 255, # '\x1b' + 28: 255, # '\x1c' + 29: 255, # '\x1d' + 30: 255, # '\x1e' + 31: 255, # '\x1f' + 32: 253, # ' ' + 33: 253, # '!' + 34: 253, # '"' + 35: 253, # '#' + 36: 253, # '$' + 37: 253, # '%' + 38: 253, # '&' + 39: 253, # "'" + 40: 253, # '(' + 41: 253, # ')' + 42: 253, # '*' + 43: 253, # '+' + 44: 253, # ',' + 45: 253, # '-' + 46: 253, # '.' + 47: 253, # '/' + 48: 252, # '0' + 49: 252, # '1' + 50: 252, # '2' + 51: 252, # '3' + 52: 252, # '4' + 53: 252, # '5' + 54: 252, # '6' + 55: 252, # '7' + 56: 252, # '8' + 57: 252, # '9' + 58: 253, # ':' + 59: 253, # ';' + 60: 253, # '<' + 61: 253, # '=' + 62: 253, # '>' + 63: 253, # '?' + 64: 253, # '@' + 65: 77, # 'A' + 66: 90, # 'B' + 67: 99, # 'C' + 68: 100, # 'D' + 69: 72, # 'E' + 70: 109, # 'F' + 71: 107, # 'G' + 72: 101, # 'H' + 73: 79, # 'I' + 74: 185, # 'J' + 75: 81, # 'K' + 76: 102, # 'L' + 77: 76, # 'M' + 78: 94, # 'N' + 79: 82, # 'O' + 80: 110, # 'P' + 81: 186, # 'Q' + 82: 108, # 'R' + 83: 91, # 'S' + 84: 74, # 'T' + 85: 119, # 'U' + 86: 84, # 'V' + 87: 96, # 'W' + 88: 111, # 'X' + 89: 187, # 'Y' + 90: 115, # 'Z' + 91: 253, # '[' + 92: 253, # '\\' + 93: 253, # ']' + 94: 253, # '^' + 95: 253, # '_' + 96: 253, # '`' + 97: 65, # 'a' + 98: 69, # 'b' + 99: 70, # 'c' + 100: 66, # 'd' + 101: 63, # 'e' + 102: 68, # 'f' + 103: 112, # 'g' + 104: 103, # 'h' + 105: 92, # 'i' + 106: 194, # 'j' + 107: 104, # 'k' + 108: 95, # 'l' + 109: 86, # 'm' + 110: 87, # 'n' + 111: 71, # 'o' + 112: 116, # 'p' + 113: 195, # 'q' + 114: 85, # 'r' + 115: 93, # 's' + 116: 97, # 't' + 117: 113, # 'u' + 118: 196, # 'v' + 119: 197, # 'w' + 120: 198, # 'x' + 121: 199, # 'y' + 122: 200, # 'z' + 123: 253, # '{' + 124: 253, # '|' + 125: 253, # '}' + 126: 253, # '~' + 127: 253, # '\x7f' + 128: 194, # '\x80' + 129: 195, # '\x81' + 130: 196, # '\x82' + 131: 197, # '\x83' + 132: 198, # '\x84' + 133: 199, # '\x85' + 134: 200, # '\x86' + 135: 201, # '\x87' + 136: 202, # '\x88' + 137: 203, # '\x89' + 138: 204, # '\x8a' + 139: 205, # '\x8b' + 140: 206, # '\x8c' + 141: 207, # '\x8d' + 142: 208, # '\x8e' + 143: 209, # '\x8f' + 144: 210, # '\x90' + 145: 211, # '\x91' + 146: 212, # '\x92' + 147: 213, # '\x93' + 148: 214, # '\x94' + 149: 215, # '\x95' + 150: 216, # '\x96' + 151: 217, # '\x97' + 152: 218, # '\x98' + 153: 219, # '\x99' + 154: 220, # '\x9a' + 155: 221, # '\x9b' + 156: 222, # '\x9c' + 157: 223, # '\x9d' + 158: 224, # '\x9e' + 159: 225, # '\x9f' + 160: 81, # '\xa0' + 161: 226, # 'Ð' + 162: 227, # 'Ђ' + 163: 228, # 'Ѓ' + 164: 229, # 'Є' + 165: 230, # 'Ð…' + 166: 105, # 'І' + 167: 231, # 'Ї' + 168: 232, # 'Ј' + 169: 233, # 'Љ' + 170: 234, # 'Њ' + 171: 235, # 'Ћ' + 172: 236, # 'ÐŒ' + 173: 45, # '\xad' + 174: 237, # 'ÐŽ' + 175: 238, # 'Ð' + 176: 31, # 'Ð' + 177: 32, # 'Б' + 178: 35, # 'Ð’' + 179: 43, # 'Г' + 180: 37, # 'Д' + 181: 44, # 'Е' + 182: 55, # 'Ж' + 183: 47, # 'З' + 184: 40, # 'И' + 185: 59, # 'Й' + 186: 33, # 'К' + 187: 46, # 'Л' + 188: 38, # 'Ðœ' + 189: 36, # 'Ð' + 190: 41, # 'О' + 191: 30, # 'П' + 192: 39, # 'Р' + 193: 28, # 'С' + 194: 34, # 'Т' + 195: 51, # 'У' + 196: 48, # 'Ф' + 197: 49, # 'Ð¥' + 198: 53, # 'Ц' + 199: 50, # 'Ч' + 200: 54, # 'Ш' + 201: 57, # 'Щ' + 202: 61, # 'Ъ' + 203: 239, # 'Ы' + 204: 67, # 'Ь' + 205: 240, # 'Э' + 206: 60, # 'Ю' + 207: 56, # 'Я' + 208: 1, # 'а' + 209: 18, # 'б' + 210: 9, # 'в' + 211: 20, # 'г' + 212: 11, # 'д' + 213: 3, # 'е' + 214: 23, # 'ж' + 215: 15, # 'з' + 216: 2, # 'и' + 217: 26, # 'й' + 218: 12, # 'к' + 219: 10, # 'л' + 220: 14, # 'м' + 221: 6, # 'н' + 222: 4, # 'о' + 223: 13, # 'п' + 224: 7, # 'Ñ€' + 225: 8, # 'Ñ' + 226: 5, # 'Ñ‚' + 227: 19, # 'у' + 228: 29, # 'Ñ„' + 229: 25, # 'Ñ…' + 230: 22, # 'ц' + 231: 21, # 'ч' + 232: 27, # 'ш' + 233: 24, # 'щ' + 234: 17, # 'ÑŠ' + 235: 75, # 'Ñ‹' + 236: 52, # 'ÑŒ' + 237: 241, # 'Ñ' + 238: 42, # 'ÑŽ' + 239: 16, # 'Ñ' + 240: 62, # 'â„–' + 241: 242, # 'Ñ‘' + 242: 243, # 'Ñ’' + 243: 244, # 'Ñ“' + 244: 58, # 'Ñ”' + 245: 245, # 'Ñ•' + 246: 98, # 'Ñ–' + 247: 246, # 'Ñ—' + 248: 247, # 'ј' + 249: 248, # 'Ñ™' + 250: 249, # 'Ñš' + 251: 250, # 'Ñ›' + 252: 251, # 'Ñœ' + 253: 91, # '§' + 254: 252, # 'Ñž' + 255: 253, # 'ÑŸ' } -Win1251BulgarianModel = { - 'char_to_order_map': win1251BulgarianCharToOrderMap, - 'precedence_matrix': BulgarianLangModel, - 'typical_positive_ratio': 0.969392, - 'keep_english_letter': False, - 'charset_name': "windows-1251", - 'language': 'Bulgarian', +ISO_8859_5_BULGARIAN_MODEL = SingleByteCharSetModel(charset_name='ISO-8859-5', + language='Bulgarian', + char_to_order_map=ISO_8859_5_BULGARIAN_CHAR_TO_ORDER, + language_model=BULGARIAN_LANG_MODEL, + typical_positive_ratio=0.969392, + keep_ascii_letters=False, + alphabet='ÐБВГДЕЖЗИЙКЛМÐОПРСТУФХЦЧШЩЪЬЮЯабвгдежзийклмнопрÑтуфхцчшщъьюÑ') + +WINDOWS_1251_BULGARIAN_CHAR_TO_ORDER = { + 0: 255, # '\x00' + 1: 255, # '\x01' + 2: 255, # '\x02' + 3: 255, # '\x03' + 4: 255, # '\x04' + 5: 255, # '\x05' + 6: 255, # '\x06' + 7: 255, # '\x07' + 8: 255, # '\x08' + 9: 255, # '\t' + 10: 254, # '\n' + 11: 255, # '\x0b' + 12: 255, # '\x0c' + 13: 254, # '\r' + 14: 255, # '\x0e' + 15: 255, # '\x0f' + 16: 255, # '\x10' + 17: 255, # '\x11' + 18: 255, # '\x12' + 19: 255, # '\x13' + 20: 255, # '\x14' + 21: 255, # '\x15' + 22: 255, # '\x16' + 23: 255, # '\x17' + 24: 255, # '\x18' + 25: 255, # '\x19' + 26: 255, # '\x1a' + 27: 255, # '\x1b' + 28: 255, # '\x1c' + 29: 255, # '\x1d' + 30: 255, # '\x1e' + 31: 255, # '\x1f' + 32: 253, # ' ' + 33: 253, # '!' + 34: 253, # '"' + 35: 253, # '#' + 36: 253, # '$' + 37: 253, # '%' + 38: 253, # '&' + 39: 253, # "'" + 40: 253, # '(' + 41: 253, # ')' + 42: 253, # '*' + 43: 253, # '+' + 44: 253, # ',' + 45: 253, # '-' + 46: 253, # '.' + 47: 253, # '/' + 48: 252, # '0' + 49: 252, # '1' + 50: 252, # '2' + 51: 252, # '3' + 52: 252, # '4' + 53: 252, # '5' + 54: 252, # '6' + 55: 252, # '7' + 56: 252, # '8' + 57: 252, # '9' + 58: 253, # ':' + 59: 253, # ';' + 60: 253, # '<' + 61: 253, # '=' + 62: 253, # '>' + 63: 253, # '?' + 64: 253, # '@' + 65: 77, # 'A' + 66: 90, # 'B' + 67: 99, # 'C' + 68: 100, # 'D' + 69: 72, # 'E' + 70: 109, # 'F' + 71: 107, # 'G' + 72: 101, # 'H' + 73: 79, # 'I' + 74: 185, # 'J' + 75: 81, # 'K' + 76: 102, # 'L' + 77: 76, # 'M' + 78: 94, # 'N' + 79: 82, # 'O' + 80: 110, # 'P' + 81: 186, # 'Q' + 82: 108, # 'R' + 83: 91, # 'S' + 84: 74, # 'T' + 85: 119, # 'U' + 86: 84, # 'V' + 87: 96, # 'W' + 88: 111, # 'X' + 89: 187, # 'Y' + 90: 115, # 'Z' + 91: 253, # '[' + 92: 253, # '\\' + 93: 253, # ']' + 94: 253, # '^' + 95: 253, # '_' + 96: 253, # '`' + 97: 65, # 'a' + 98: 69, # 'b' + 99: 70, # 'c' + 100: 66, # 'd' + 101: 63, # 'e' + 102: 68, # 'f' + 103: 112, # 'g' + 104: 103, # 'h' + 105: 92, # 'i' + 106: 194, # 'j' + 107: 104, # 'k' + 108: 95, # 'l' + 109: 86, # 'm' + 110: 87, # 'n' + 111: 71, # 'o' + 112: 116, # 'p' + 113: 195, # 'q' + 114: 85, # 'r' + 115: 93, # 's' + 116: 97, # 't' + 117: 113, # 'u' + 118: 196, # 'v' + 119: 197, # 'w' + 120: 198, # 'x' + 121: 199, # 'y' + 122: 200, # 'z' + 123: 253, # '{' + 124: 253, # '|' + 125: 253, # '}' + 126: 253, # '~' + 127: 253, # '\x7f' + 128: 206, # 'Ђ' + 129: 207, # 'Ѓ' + 130: 208, # '‚' + 131: 209, # 'Ñ“' + 132: 210, # '„' + 133: 211, # '…' + 134: 212, # '†' + 135: 213, # '‡' + 136: 120, # '€' + 137: 214, # '‰' + 138: 215, # 'Љ' + 139: 216, # '‹' + 140: 217, # 'Њ' + 141: 218, # 'ÐŒ' + 142: 219, # 'Ћ' + 143: 220, # 'Ð' + 144: 221, # 'Ñ’' + 145: 78, # '‘' + 146: 64, # '’' + 147: 83, # '“' + 148: 121, # 'â€' + 149: 98, # '•' + 150: 117, # '–' + 151: 105, # '—' + 152: 222, # None + 153: 223, # 'â„¢' + 154: 224, # 'Ñ™' + 155: 225, # '›' + 156: 226, # 'Ñš' + 157: 227, # 'Ñœ' + 158: 228, # 'Ñ›' + 159: 229, # 'ÑŸ' + 160: 88, # '\xa0' + 161: 230, # 'ÐŽ' + 162: 231, # 'Ñž' + 163: 232, # 'Ј' + 164: 233, # '¤' + 165: 122, # 'Ò' + 166: 89, # '¦' + 167: 106, # '§' + 168: 234, # 'Ð' + 169: 235, # '©' + 170: 236, # 'Є' + 171: 237, # '«' + 172: 238, # '¬' + 173: 45, # '\xad' + 174: 239, # '®' + 175: 240, # 'Ї' + 176: 73, # '°' + 177: 80, # '±' + 178: 118, # 'І' + 179: 114, # 'Ñ–' + 180: 241, # 'Ò‘' + 181: 242, # 'µ' + 182: 243, # '¶' + 183: 244, # '·' + 184: 245, # 'Ñ‘' + 185: 62, # 'â„–' + 186: 58, # 'Ñ”' + 187: 246, # '»' + 188: 247, # 'ј' + 189: 248, # 'Ð…' + 190: 249, # 'Ñ•' + 191: 250, # 'Ñ—' + 192: 31, # 'Ð' + 193: 32, # 'Б' + 194: 35, # 'Ð’' + 195: 43, # 'Г' + 196: 37, # 'Д' + 197: 44, # 'Е' + 198: 55, # 'Ж' + 199: 47, # 'З' + 200: 40, # 'И' + 201: 59, # 'Й' + 202: 33, # 'К' + 203: 46, # 'Л' + 204: 38, # 'Ðœ' + 205: 36, # 'Ð' + 206: 41, # 'О' + 207: 30, # 'П' + 208: 39, # 'Р' + 209: 28, # 'С' + 210: 34, # 'Т' + 211: 51, # 'У' + 212: 48, # 'Ф' + 213: 49, # 'Ð¥' + 214: 53, # 'Ц' + 215: 50, # 'Ч' + 216: 54, # 'Ш' + 217: 57, # 'Щ' + 218: 61, # 'Ъ' + 219: 251, # 'Ы' + 220: 67, # 'Ь' + 221: 252, # 'Э' + 222: 60, # 'Ю' + 223: 56, # 'Я' + 224: 1, # 'а' + 225: 18, # 'б' + 226: 9, # 'в' + 227: 20, # 'г' + 228: 11, # 'д' + 229: 3, # 'е' + 230: 23, # 'ж' + 231: 15, # 'з' + 232: 2, # 'и' + 233: 26, # 'й' + 234: 12, # 'к' + 235: 10, # 'л' + 236: 14, # 'м' + 237: 6, # 'н' + 238: 4, # 'о' + 239: 13, # 'п' + 240: 7, # 'Ñ€' + 241: 8, # 'Ñ' + 242: 5, # 'Ñ‚' + 243: 19, # 'у' + 244: 29, # 'Ñ„' + 245: 25, # 'Ñ…' + 246: 22, # 'ц' + 247: 21, # 'ч' + 248: 27, # 'ш' + 249: 24, # 'щ' + 250: 17, # 'ÑŠ' + 251: 75, # 'Ñ‹' + 252: 52, # 'ÑŒ' + 253: 253, # 'Ñ' + 254: 42, # 'ÑŽ' + 255: 16, # 'Ñ' } + +WINDOWS_1251_BULGARIAN_MODEL = SingleByteCharSetModel(charset_name='windows-1251', + language='Bulgarian', + char_to_order_map=WINDOWS_1251_BULGARIAN_CHAR_TO_ORDER, + language_model=BULGARIAN_LANG_MODEL, + typical_positive_ratio=0.969392, + keep_ascii_letters=False, + alphabet='ÐБВГДЕЖЗИЙКЛМÐОПРСТУФХЦЧШЩЪЬЮЯабвгдежзийклмнопрÑтуфхцчшщъьюÑ') + diff --git a/libs/chardet/langcyrillicmodel.py b/libs/chardet/langcyrillicmodel.py deleted file mode 100644 index e5f9a1fd1..000000000 --- a/libs/chardet/langcyrillicmodel.py +++ /dev/null @@ -1,333 +0,0 @@ -######################## BEGIN LICENSE BLOCK ######################## -# The Original Code is Mozilla Communicator client code. -# -# The Initial Developer of the Original Code is -# Netscape Communications Corporation. -# Portions created by the Initial Developer are Copyright (C) 1998 -# the Initial Developer. All Rights Reserved. -# -# Contributor(s): -# Mark Pilgrim - port to Python -# -# This library is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 2.1 of the License, or (at your option) any later version. -# -# This library is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with this library; if not, write to the Free Software -# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA -# 02110-1301 USA -######################### END LICENSE BLOCK ######################### - -# KOI8-R language model -# Character Mapping Table: -KOI8R_char_to_order_map = ( -255,255,255,255,255,255,255,255,255,255,254,255,255,254,255,255, # 00 -255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, # 10 -253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253, # 20 -252,252,252,252,252,252,252,252,252,252,253,253,253,253,253,253, # 30 -253,142,143,144,145,146,147,148,149,150,151,152, 74,153, 75,154, # 40 -155,156,157,158,159,160,161,162,163,164,165,253,253,253,253,253, # 50 -253, 71,172, 66,173, 65,174, 76,175, 64,176,177, 77, 72,178, 69, # 60 - 67,179, 78, 73,180,181, 79,182,183,184,185,253,253,253,253,253, # 70 -191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,206, # 80 -207,208,209,210,211,212,213,214,215,216,217,218,219,220,221,222, # 90 -223,224,225, 68,226,227,228,229,230,231,232,233,234,235,236,237, # a0 -238,239,240,241,242,243,244,245,246,247,248,249,250,251,252,253, # b0 - 27, 3, 21, 28, 13, 2, 39, 19, 26, 4, 23, 11, 8, 12, 5, 1, # c0 - 15, 16, 9, 7, 6, 14, 24, 10, 17, 18, 20, 25, 30, 29, 22, 54, # d0 - 59, 37, 44, 58, 41, 48, 53, 46, 55, 42, 60, 36, 49, 38, 31, 34, # e0 - 35, 43, 45, 32, 40, 52, 56, 33, 61, 62, 51, 57, 47, 63, 50, 70, # f0 -) - -win1251_char_to_order_map = ( -255,255,255,255,255,255,255,255,255,255,254,255,255,254,255,255, # 00 -255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, # 10 -253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253, # 20 -252,252,252,252,252,252,252,252,252,252,253,253,253,253,253,253, # 30 -253,142,143,144,145,146,147,148,149,150,151,152, 74,153, 75,154, # 40 -155,156,157,158,159,160,161,162,163,164,165,253,253,253,253,253, # 50 -253, 71,172, 66,173, 65,174, 76,175, 64,176,177, 77, 72,178, 69, # 60 - 67,179, 78, 73,180,181, 79,182,183,184,185,253,253,253,253,253, # 70 -191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,206, -207,208,209,210,211,212,213,214,215,216,217,218,219,220,221,222, -223,224,225,226,227,228,229,230,231,232,233,234,235,236,237,238, -239,240,241,242,243,244,245,246, 68,247,248,249,250,251,252,253, - 37, 44, 33, 46, 41, 48, 56, 51, 42, 60, 36, 49, 38, 31, 34, 35, - 45, 32, 40, 52, 53, 55, 58, 50, 57, 63, 70, 62, 61, 47, 59, 43, - 3, 21, 10, 19, 13, 2, 24, 20, 4, 23, 11, 8, 12, 5, 1, 15, - 9, 7, 6, 14, 39, 26, 28, 22, 25, 29, 54, 18, 17, 30, 27, 16, -) - -latin5_char_to_order_map = ( -255,255,255,255,255,255,255,255,255,255,254,255,255,254,255,255, # 00 -255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, # 10 -253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253, # 20 -252,252,252,252,252,252,252,252,252,252,253,253,253,253,253,253, # 30 -253,142,143,144,145,146,147,148,149,150,151,152, 74,153, 75,154, # 40 -155,156,157,158,159,160,161,162,163,164,165,253,253,253,253,253, # 50 -253, 71,172, 66,173, 65,174, 76,175, 64,176,177, 77, 72,178, 69, # 60 - 67,179, 78, 73,180,181, 79,182,183,184,185,253,253,253,253,253, # 70 -191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,206, -207,208,209,210,211,212,213,214,215,216,217,218,219,220,221,222, -223,224,225,226,227,228,229,230,231,232,233,234,235,236,237,238, - 37, 44, 33, 46, 41, 48, 56, 51, 42, 60, 36, 49, 38, 31, 34, 35, - 45, 32, 40, 52, 53, 55, 58, 50, 57, 63, 70, 62, 61, 47, 59, 43, - 3, 21, 10, 19, 13, 2, 24, 20, 4, 23, 11, 8, 12, 5, 1, 15, - 9, 7, 6, 14, 39, 26, 28, 22, 25, 29, 54, 18, 17, 30, 27, 16, -239, 68,240,241,242,243,244,245,246,247,248,249,250,251,252,255, -) - -macCyrillic_char_to_order_map = ( -255,255,255,255,255,255,255,255,255,255,254,255,255,254,255,255, # 00 -255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, # 10 -253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253, # 20 -252,252,252,252,252,252,252,252,252,252,253,253,253,253,253,253, # 30 -253,142,143,144,145,146,147,148,149,150,151,152, 74,153, 75,154, # 40 -155,156,157,158,159,160,161,162,163,164,165,253,253,253,253,253, # 50 -253, 71,172, 66,173, 65,174, 76,175, 64,176,177, 77, 72,178, 69, # 60 - 67,179, 78, 73,180,181, 79,182,183,184,185,253,253,253,253,253, # 70 - 37, 44, 33, 46, 41, 48, 56, 51, 42, 60, 36, 49, 38, 31, 34, 35, - 45, 32, 40, 52, 53, 55, 58, 50, 57, 63, 70, 62, 61, 47, 59, 43, -191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,206, -207,208,209,210,211,212,213,214,215,216,217,218,219,220,221,222, -223,224,225,226,227,228,229,230,231,232,233,234,235,236,237,238, -239,240,241,242,243,244,245,246,247,248,249,250,251,252, 68, 16, - 3, 21, 10, 19, 13, 2, 24, 20, 4, 23, 11, 8, 12, 5, 1, 15, - 9, 7, 6, 14, 39, 26, 28, 22, 25, 29, 54, 18, 17, 30, 27,255, -) - -IBM855_char_to_order_map = ( -255,255,255,255,255,255,255,255,255,255,254,255,255,254,255,255, # 00 -255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, # 10 -253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253, # 20 -252,252,252,252,252,252,252,252,252,252,253,253,253,253,253,253, # 30 -253,142,143,144,145,146,147,148,149,150,151,152, 74,153, 75,154, # 40 -155,156,157,158,159,160,161,162,163,164,165,253,253,253,253,253, # 50 -253, 71,172, 66,173, 65,174, 76,175, 64,176,177, 77, 72,178, 69, # 60 - 67,179, 78, 73,180,181, 79,182,183,184,185,253,253,253,253,253, # 70 -191,192,193,194, 68,195,196,197,198,199,200,201,202,203,204,205, -206,207,208,209,210,211,212,213,214,215,216,217, 27, 59, 54, 70, - 3, 37, 21, 44, 28, 58, 13, 41, 2, 48, 39, 53, 19, 46,218,219, -220,221,222,223,224, 26, 55, 4, 42,225,226,227,228, 23, 60,229, -230,231,232,233,234,235, 11, 36,236,237,238,239,240,241,242,243, - 8, 49, 12, 38, 5, 31, 1, 34, 15,244,245,246,247, 35, 16,248, - 43, 9, 45, 7, 32, 6, 40, 14, 52, 24, 56, 10, 33, 17, 61,249, -250, 18, 62, 20, 51, 25, 57, 30, 47, 29, 63, 22, 50,251,252,255, -) - -IBM866_char_to_order_map = ( -255,255,255,255,255,255,255,255,255,255,254,255,255,254,255,255, # 00 -255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, # 10 -253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253, # 20 -252,252,252,252,252,252,252,252,252,252,253,253,253,253,253,253, # 30 -253,142,143,144,145,146,147,148,149,150,151,152, 74,153, 75,154, # 40 -155,156,157,158,159,160,161,162,163,164,165,253,253,253,253,253, # 50 -253, 71,172, 66,173, 65,174, 76,175, 64,176,177, 77, 72,178, 69, # 60 - 67,179, 78, 73,180,181, 79,182,183,184,185,253,253,253,253,253, # 70 - 37, 44, 33, 46, 41, 48, 56, 51, 42, 60, 36, 49, 38, 31, 34, 35, - 45, 32, 40, 52, 53, 55, 58, 50, 57, 63, 70, 62, 61, 47, 59, 43, - 3, 21, 10, 19, 13, 2, 24, 20, 4, 23, 11, 8, 12, 5, 1, 15, -191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,206, -207,208,209,210,211,212,213,214,215,216,217,218,219,220,221,222, -223,224,225,226,227,228,229,230,231,232,233,234,235,236,237,238, - 9, 7, 6, 14, 39, 26, 28, 22, 25, 29, 54, 18, 17, 30, 27, 16, -239, 68,240,241,242,243,244,245,246,247,248,249,250,251,252,255, -) - -# Model Table: -# total sequences: 100% -# first 512 sequences: 97.6601% -# first 1024 sequences: 2.3389% -# rest sequences: 0.1237% -# negative sequences: 0.0009% -RussianLangModel = ( -0,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,1,1,3,3,3,3,1,3,3,3,2,3,2,3,3, -3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,0,3,2,2,2,2,2,0,0,2, -3,3,3,2,3,3,3,3,3,3,3,3,3,3,2,3,3,0,0,3,3,3,3,3,3,3,3,3,2,3,2,0, -0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -3,3,3,2,2,3,3,3,3,3,3,3,3,3,2,3,3,0,0,3,3,3,3,3,3,3,3,2,3,3,1,0, -0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -3,2,3,2,3,3,3,3,3,3,3,3,3,3,3,3,3,0,0,3,3,3,3,3,3,3,3,3,3,3,2,1, -0,0,0,0,0,0,0,2,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -3,3,3,3,3,3,3,3,3,3,3,3,3,3,2,3,3,0,0,3,3,3,3,3,3,3,3,3,3,3,2,1, -0,0,0,0,0,1,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -3,3,3,3,3,3,3,3,2,2,2,3,1,3,3,1,3,3,3,3,2,2,3,0,2,2,2,3,3,2,1,0, -0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0, -3,3,3,3,3,3,2,3,3,3,3,3,2,2,3,2,3,3,3,2,1,2,2,0,1,2,2,2,2,2,2,0, -0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0, -3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,2,2,2,3,0,2,2,3,3,2,1,2,0, -0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,1,0,0,2,0,0,0,0,0,0,0,0,0, -3,3,3,3,3,3,2,3,3,1,2,3,2,2,3,2,3,3,3,3,2,2,3,0,3,2,2,3,1,1,1,0, -0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -3,3,3,3,3,3,3,3,2,2,3,3,3,3,3,2,3,3,3,3,2,2,2,0,3,3,3,2,2,2,2,0, -0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -3,3,3,3,3,3,3,3,3,3,2,3,2,3,3,3,3,3,3,2,3,2,2,0,1,3,2,1,2,2,1,0, -0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0, -3,3,3,3,3,3,3,3,3,3,3,2,1,1,3,0,1,1,1,1,2,1,1,0,2,2,2,1,2,0,1,0, -0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -3,3,3,3,3,3,2,3,3,2,2,2,2,1,3,2,3,2,3,2,1,2,2,0,1,1,2,1,2,1,2,0, -0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -3,3,3,3,3,3,3,3,3,3,3,3,2,2,3,2,3,3,3,2,2,2,2,0,2,2,2,2,3,1,1,0, -0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0, -3,2,3,2,2,3,3,3,3,3,3,3,3,3,1,3,2,0,0,3,3,3,3,2,3,3,3,3,2,3,2,0, -0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -2,3,3,3,3,3,2,2,3,3,0,2,1,0,3,2,3,2,3,0,0,1,2,0,0,1,0,1,2,1,1,0, -0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -3,0,3,0,2,3,3,3,3,2,3,3,3,3,1,2,2,0,0,2,3,2,2,2,3,2,3,2,2,3,0,0, -0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -3,2,3,0,2,3,2,3,0,1,2,3,3,2,0,2,3,0,0,2,3,2,2,0,1,3,1,3,2,2,1,0, -0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -3,1,3,0,2,3,3,3,3,3,3,3,3,2,1,3,2,0,0,2,2,3,3,3,2,3,3,0,2,2,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -3,3,3,3,3,3,2,2,3,3,2,2,2,3,3,0,0,1,1,1,1,1,2,0,0,1,1,1,1,0,1,0, -0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -3,3,3,3,3,3,2,2,3,3,3,3,3,3,3,0,3,2,3,3,2,3,2,0,2,1,0,1,1,0,1,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0, -3,3,3,3,3,3,2,3,3,3,2,2,2,2,3,1,3,2,3,1,1,2,1,0,2,2,2,2,1,3,1,0, -0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0, -2,2,3,3,3,3,3,1,2,2,1,3,1,0,3,0,0,3,0,0,0,1,1,0,1,2,1,0,0,0,0,0, -0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -3,2,2,1,1,3,3,3,2,2,1,2,2,3,1,1,2,0,0,2,2,1,3,0,0,2,1,1,2,1,1,0, -0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -3,2,3,3,3,3,1,2,2,2,1,2,1,3,3,1,1,2,1,2,1,2,2,0,2,0,0,1,1,0,1,0, -0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -2,3,3,3,3,3,2,1,3,2,2,3,2,0,3,2,0,3,0,1,0,1,1,0,0,1,1,1,1,0,1,0, -0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -3,3,2,3,3,3,2,2,2,3,3,1,2,1,2,1,0,1,0,1,1,0,1,0,0,2,1,1,1,0,1,0, -0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0, -3,1,1,2,1,2,3,3,2,2,1,2,2,3,0,2,1,0,0,2,2,3,2,1,2,2,2,2,2,3,1,0, -0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -3,3,3,3,3,1,1,0,1,1,2,2,1,1,3,0,0,1,3,1,1,1,0,0,0,1,0,1,1,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -2,1,3,3,3,2,0,0,0,2,1,0,1,0,2,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -2,0,1,0,0,2,3,2,2,2,1,2,2,2,1,2,1,0,0,1,1,1,0,2,0,1,1,1,0,0,1,1, -1,0,0,0,0,0,1,2,0,0,0,0,0,1,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0, -2,3,3,3,3,0,0,0,0,1,0,0,0,0,3,0,1,2,1,0,0,0,0,0,0,0,1,1,0,0,1,1, -1,0,1,0,1,2,0,0,1,1,2,1,0,1,1,1,1,0,1,1,1,1,0,1,0,0,1,0,0,1,1,0, -2,2,3,2,2,2,3,1,2,2,2,2,2,2,2,2,1,1,1,1,1,1,1,0,1,0,1,1,1,0,2,1, -1,1,1,1,1,1,1,1,2,1,1,1,1,1,1,1,1,1,1,0,1,0,1,1,0,1,1,1,0,1,1,0, -3,3,3,2,2,2,2,3,2,2,1,1,2,2,2,2,1,1,3,1,2,1,2,0,0,1,1,0,1,0,2,1, -1,1,1,1,1,2,1,0,1,1,1,1,0,1,0,0,1,1,0,0,1,0,1,0,0,1,0,0,0,1,1,0, -2,0,0,1,0,3,2,2,2,2,1,2,1,2,1,2,0,0,0,2,1,2,2,1,1,2,2,0,1,1,0,2, -1,1,1,1,1,0,1,1,1,2,1,1,1,2,1,0,1,2,1,1,1,1,0,1,1,1,0,0,1,0,0,1, -1,3,2,2,2,1,1,1,2,3,0,0,0,0,2,0,2,2,1,0,0,0,0,0,0,1,0,0,0,0,1,1, -1,0,1,1,0,1,0,1,1,0,1,1,0,2,0,0,1,1,0,0,1,0,0,0,0,0,0,0,0,1,1,0, -2,3,2,3,2,1,2,2,2,2,1,0,0,0,2,0,0,1,1,0,0,0,0,0,0,0,1,1,0,0,2,1, -1,1,2,1,0,2,0,0,1,0,1,0,0,1,0,0,1,1,0,1,1,0,0,0,0,0,1,0,0,0,0,0, -3,0,0,1,0,2,2,2,3,2,2,2,2,2,2,2,0,0,0,2,1,2,1,1,1,2,2,0,0,0,1,2, -1,1,1,1,1,0,1,2,1,1,1,1,1,1,1,0,1,1,1,1,1,1,0,1,1,1,1,1,1,0,0,1, -2,3,2,3,3,2,0,1,1,1,0,0,1,0,2,0,1,1,3,1,0,0,0,0,0,0,0,1,0,0,2,1, -1,1,1,1,1,1,1,0,1,0,1,1,1,1,0,1,1,1,0,0,1,1,0,1,0,0,0,0,0,0,1,0, -2,3,3,3,3,1,2,2,2,2,0,1,1,0,2,1,1,1,2,1,0,1,1,0,0,1,0,1,0,0,2,0, -0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -2,3,3,3,2,0,0,1,1,2,2,1,0,0,2,0,1,1,3,0,0,1,0,0,0,0,0,1,0,1,2,1, -1,1,2,0,1,1,1,0,1,0,1,1,0,1,0,1,1,1,1,0,1,0,0,0,0,0,0,1,0,1,1,0, -1,3,2,3,2,1,0,0,2,2,2,0,1,0,2,0,1,1,1,0,1,0,0,0,3,0,1,1,0,0,2,1, -1,1,1,0,1,1,0,0,0,0,1,1,0,1,0,0,2,1,1,0,1,0,0,0,1,0,1,0,0,1,1,0, -3,1,2,1,1,2,2,2,2,2,2,1,2,2,1,1,0,0,0,2,2,2,0,0,0,1,2,1,0,1,0,1, -2,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,2,1,1,1,0,1,0,1,1,0,1,1,1,0,0,1, -3,0,0,0,0,2,0,1,1,1,1,1,1,1,0,1,0,0,0,1,1,1,0,1,0,1,1,0,0,1,0,1, -1,1,0,0,1,0,0,0,1,0,1,1,0,0,1,0,1,0,1,0,0,0,0,1,0,0,0,1,0,0,0,1, -1,3,3,2,2,0,0,0,2,2,0,0,0,1,2,0,1,1,2,0,0,0,0,0,0,0,0,1,0,0,2,1, -0,1,1,0,0,1,1,0,0,0,1,1,0,1,1,0,1,1,0,0,1,0,0,0,0,0,0,0,0,0,1,0, -2,3,2,3,2,0,0,0,0,1,1,0,0,0,2,0,2,0,2,0,0,0,0,0,1,0,0,1,0,0,1,1, -1,1,2,0,1,2,1,0,1,1,2,1,1,1,1,1,2,1,1,0,1,0,0,1,1,1,1,1,0,1,1,0, -1,3,2,2,2,1,0,0,2,2,1,0,1,2,2,0,0,1,0,0,0,0,0,0,0,0,0,1,0,0,1,1, -0,0,1,1,0,1,1,0,0,1,1,0,1,1,0,0,1,1,0,0,1,0,0,0,0,0,0,0,0,0,0,0, -1,0,0,1,0,2,3,1,2,2,2,2,2,2,1,1,0,0,0,1,0,1,0,2,1,1,1,0,0,0,0,1, -1,1,0,1,1,0,1,1,1,1,0,0,0,1,0,0,0,1,0,0,0,0,0,0,0,0,0,0,1,0,0,0, -2,0,2,0,0,1,0,3,2,1,2,1,2,2,0,1,0,0,0,2,1,0,0,2,1,1,1,1,0,2,0,2, -2,1,1,1,1,1,1,1,1,1,1,1,1,2,1,0,1,1,1,1,0,0,0,1,1,1,1,0,1,0,0,1, -1,2,2,2,2,1,0,0,1,0,0,0,0,0,2,0,1,1,1,1,0,0,0,0,1,0,1,2,0,0,2,0, -1,0,1,1,1,2,1,0,1,0,1,1,0,0,1,0,1,1,1,0,1,0,0,0,1,0,0,1,0,1,1,0, -2,1,2,2,2,0,3,0,1,1,0,0,0,0,2,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,1, -0,0,0,1,1,1,0,0,1,0,1,0,0,0,0,0,1,0,0,0,1,0,0,0,0,0,0,0,0,1,0,0, -1,2,2,3,2,2,0,0,1,1,2,0,1,2,1,0,1,0,1,0,0,1,0,0,0,0,0,0,0,0,0,1, -0,1,1,0,0,1,1,0,0,1,1,0,0,1,1,0,1,1,0,0,1,0,0,0,0,0,0,0,0,1,1,0, -2,2,1,1,2,1,2,2,2,2,2,1,2,2,0,1,0,0,0,1,2,2,2,1,2,1,1,1,1,1,2,1, -1,1,1,1,1,1,1,1,1,1,0,0,1,1,1,0,1,1,1,0,0,0,0,1,1,1,0,1,1,0,0,1, -1,2,2,2,2,0,1,0,2,2,0,0,0,0,2,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,2,0, -0,0,1,0,0,1,0,0,0,0,1,0,1,1,0,0,1,1,0,0,1,0,0,0,0,0,0,0,0,0,0,0, -0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -1,2,2,2,2,0,0,0,2,2,2,0,1,0,1,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,1,1, -0,1,1,0,0,1,1,0,0,0,1,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -1,2,2,2,2,0,0,0,0,1,0,0,1,1,2,0,0,0,0,1,0,1,0,0,1,0,0,2,0,0,0,1, -0,0,1,0,0,1,0,0,0,1,1,0,0,0,0,0,1,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0, -1,2,2,2,1,1,2,0,2,1,1,1,1,0,2,2,0,0,0,0,0,0,0,0,0,1,1,0,0,0,1,1, -0,0,1,0,1,1,0,0,0,0,1,0,0,0,0,0,1,1,0,0,1,0,0,0,0,0,0,0,0,0,0,0, -1,0,2,1,2,0,0,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,0,0,0,0,0,1,0,0,0,0, -0,0,1,0,1,1,0,0,0,0,1,0,0,0,0,0,1,0,0,0,1,0,0,0,0,0,0,0,0,0,1,0, -1,0,0,0,0,2,0,1,2,1,0,1,1,1,0,1,0,0,0,1,0,1,0,0,1,0,1,0,0,0,0,1, -0,0,0,0,0,1,0,0,1,1,0,0,1,1,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,1, -2,2,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1, -1,0,0,0,1,0,0,0,1,1,0,0,0,0,0,0,0,1,0,0,0,0,0,1,0,0,1,0,0,0,0,0, -2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1, -1,1,1,0,1,0,1,0,0,1,1,1,1,0,0,0,1,0,0,0,0,1,0,0,0,1,0,1,0,0,0,0, -1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1, -1,1,0,1,1,0,1,0,1,0,0,0,0,1,1,0,1,1,0,0,0,0,0,1,0,1,1,0,1,0,0,0, -0,1,1,1,1,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,0,0,0,1,0,0,0,0,1,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0, -) - -Koi8rModel = { - 'char_to_order_map': KOI8R_char_to_order_map, - 'precedence_matrix': RussianLangModel, - 'typical_positive_ratio': 0.976601, - 'keep_english_letter': False, - 'charset_name': "KOI8-R", - 'language': 'Russian', -} - -Win1251CyrillicModel = { - 'char_to_order_map': win1251_char_to_order_map, - 'precedence_matrix': RussianLangModel, - 'typical_positive_ratio': 0.976601, - 'keep_english_letter': False, - 'charset_name': "windows-1251", - 'language': 'Russian', -} - -Latin5CyrillicModel = { - 'char_to_order_map': latin5_char_to_order_map, - 'precedence_matrix': RussianLangModel, - 'typical_positive_ratio': 0.976601, - 'keep_english_letter': False, - 'charset_name': "ISO-8859-5", - 'language': 'Russian', -} - -MacCyrillicModel = { - 'char_to_order_map': macCyrillic_char_to_order_map, - 'precedence_matrix': RussianLangModel, - 'typical_positive_ratio': 0.976601, - 'keep_english_letter': False, - 'charset_name': "MacCyrillic", - 'language': 'Russian', -} - -Ibm866Model = { - 'char_to_order_map': IBM866_char_to_order_map, - 'precedence_matrix': RussianLangModel, - 'typical_positive_ratio': 0.976601, - 'keep_english_letter': False, - 'charset_name': "IBM866", - 'language': 'Russian', -} - -Ibm855Model = { - 'char_to_order_map': IBM855_char_to_order_map, - 'precedence_matrix': RussianLangModel, - 'typical_positive_ratio': 0.976601, - 'keep_english_letter': False, - 'charset_name': "IBM855", - 'language': 'Russian', -} diff --git a/libs/chardet/langgreekmodel.py b/libs/chardet/langgreekmodel.py index 533222166..02b94de65 100644 --- a/libs/chardet/langgreekmodel.py +++ b/libs/chardet/langgreekmodel.py @@ -1,225 +1,4398 @@ -######################## BEGIN LICENSE BLOCK ######################## -# The Original Code is Mozilla Communicator client code. -# -# The Initial Developer of the Original Code is -# Netscape Communications Corporation. -# Portions created by the Initial Developer are Copyright (C) 1998 -# the Initial Developer. All Rights Reserved. -# -# Contributor(s): -# Mark Pilgrim - port to Python -# -# This library is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 2.1 of the License, or (at your option) any later version. -# -# This library is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with this library; if not, write to the Free Software -# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA -# 02110-1301 USA -######################### END LICENSE BLOCK ######################### +#!/usr/bin/env python +# -*- coding: utf-8 -*- -# 255: Control characters that usually does not exist in any text +from chardet.sbcharsetprober import SingleByteCharSetModel + + +# 3: Positive +# 2: Likely +# 1: Unlikely +# 0: Negative + +GREEK_LANG_MODEL = { + 60: { # 'e' + 60: 2, # 'e' + 55: 1, # 'o' + 58: 2, # 't' + 36: 1, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'ÎŒ' + 31: 0, # 'Α' + 51: 0, # 'Î’' + 43: 0, # 'Γ' + 41: 0, # 'Δ' + 34: 0, # 'Ε' + 40: 0, # 'Η' + 52: 0, # 'Θ' + 47: 0, # 'Ι' + 44: 0, # 'Κ' + 53: 0, # 'Λ' + 38: 0, # 'Îœ' + 49: 0, # 'Î' + 59: 0, # 'Ξ' + 39: 0, # 'Ο' + 35: 0, # 'Π' + 48: 0, # 'Ρ' + 37: 0, # 'Σ' + 33: 0, # 'Τ' + 45: 0, # 'Î¥' + 56: 0, # 'Φ' + 50: 1, # 'Χ' + 57: 0, # 'Ω' + 17: 0, # 'ά' + 18: 0, # 'έ' + 22: 0, # 'ή' + 15: 0, # 'ί' + 1: 0, # 'α' + 29: 0, # 'β' + 20: 0, # 'γ' + 21: 0, # 'δ' + 3: 0, # 'ε' + 32: 0, # 'ζ' + 13: 0, # 'η' + 25: 0, # 'θ' + 5: 0, # 'ι' + 11: 0, # 'κ' + 16: 0, # 'λ' + 10: 0, # 'μ' + 6: 0, # 'ν' + 30: 0, # 'ξ' + 4: 0, # 'ο' + 9: 0, # 'Ï€' + 8: 0, # 'Ï' + 14: 0, # 'Ï‚' + 7: 0, # 'σ' + 2: 0, # 'Ï„' + 12: 0, # 'Ï…' + 28: 0, # 'φ' + 23: 0, # 'χ' + 42: 0, # 'ψ' + 24: 0, # 'ω' + 19: 0, # 'ÏŒ' + 26: 0, # 'Ï' + 27: 0, # 'ÏŽ' + }, + 55: { # 'o' + 60: 0, # 'e' + 55: 2, # 'o' + 58: 2, # 't' + 36: 1, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'ÎŒ' + 31: 0, # 'Α' + 51: 0, # 'Î’' + 43: 0, # 'Γ' + 41: 0, # 'Δ' + 34: 0, # 'Ε' + 40: 0, # 'Η' + 52: 0, # 'Θ' + 47: 0, # 'Ι' + 44: 0, # 'Κ' + 53: 0, # 'Λ' + 38: 0, # 'Îœ' + 49: 0, # 'Î' + 59: 0, # 'Ξ' + 39: 0, # 'Ο' + 35: 0, # 'Π' + 48: 0, # 'Ρ' + 37: 0, # 'Σ' + 33: 0, # 'Τ' + 45: 0, # 'Î¥' + 56: 0, # 'Φ' + 50: 0, # 'Χ' + 57: 0, # 'Ω' + 17: 0, # 'ά' + 18: 0, # 'έ' + 22: 0, # 'ή' + 15: 0, # 'ί' + 1: 0, # 'α' + 29: 0, # 'β' + 20: 0, # 'γ' + 21: 0, # 'δ' + 3: 0, # 'ε' + 32: 0, # 'ζ' + 13: 0, # 'η' + 25: 0, # 'θ' + 5: 0, # 'ι' + 11: 0, # 'κ' + 16: 0, # 'λ' + 10: 0, # 'μ' + 6: 1, # 'ν' + 30: 0, # 'ξ' + 4: 0, # 'ο' + 9: 0, # 'Ï€' + 8: 0, # 'Ï' + 14: 0, # 'Ï‚' + 7: 0, # 'σ' + 2: 0, # 'Ï„' + 12: 1, # 'Ï…' + 28: 0, # 'φ' + 23: 0, # 'χ' + 42: 0, # 'ψ' + 24: 0, # 'ω' + 19: 0, # 'ÏŒ' + 26: 0, # 'Ï' + 27: 0, # 'ÏŽ' + }, + 58: { # 't' + 60: 2, # 'e' + 55: 1, # 'o' + 58: 1, # 't' + 36: 0, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'ÎŒ' + 31: 0, # 'Α' + 51: 0, # 'Î’' + 43: 0, # 'Γ' + 41: 0, # 'Δ' + 34: 0, # 'Ε' + 40: 0, # 'Η' + 52: 0, # 'Θ' + 47: 0, # 'Ι' + 44: 0, # 'Κ' + 53: 0, # 'Λ' + 38: 0, # 'Îœ' + 49: 0, # 'Î' + 59: 0, # 'Ξ' + 39: 0, # 'Ο' + 35: 0, # 'Π' + 48: 0, # 'Ρ' + 37: 0, # 'Σ' + 33: 0, # 'Τ' + 45: 0, # 'Î¥' + 56: 0, # 'Φ' + 50: 0, # 'Χ' + 57: 0, # 'Ω' + 17: 2, # 'ά' + 18: 0, # 'έ' + 22: 0, # 'ή' + 15: 0, # 'ί' + 1: 0, # 'α' + 29: 0, # 'β' + 20: 0, # 'γ' + 21: 0, # 'δ' + 3: 0, # 'ε' + 32: 0, # 'ζ' + 13: 0, # 'η' + 25: 0, # 'θ' + 5: 0, # 'ι' + 11: 0, # 'κ' + 16: 0, # 'λ' + 10: 0, # 'μ' + 6: 0, # 'ν' + 30: 0, # 'ξ' + 4: 1, # 'ο' + 9: 0, # 'Ï€' + 8: 0, # 'Ï' + 14: 0, # 'Ï‚' + 7: 0, # 'σ' + 2: 0, # 'Ï„' + 12: 0, # 'Ï…' + 28: 0, # 'φ' + 23: 0, # 'χ' + 42: 0, # 'ψ' + 24: 0, # 'ω' + 19: 0, # 'ÏŒ' + 26: 0, # 'Ï' + 27: 0, # 'ÏŽ' + }, + 36: { # '·' + 60: 0, # 'e' + 55: 0, # 'o' + 58: 0, # 't' + 36: 0, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'ÎŒ' + 31: 0, # 'Α' + 51: 0, # 'Î’' + 43: 0, # 'Γ' + 41: 0, # 'Δ' + 34: 0, # 'Ε' + 40: 0, # 'Η' + 52: 0, # 'Θ' + 47: 0, # 'Ι' + 44: 0, # 'Κ' + 53: 0, # 'Λ' + 38: 0, # 'Îœ' + 49: 0, # 'Î' + 59: 0, # 'Ξ' + 39: 0, # 'Ο' + 35: 0, # 'Π' + 48: 0, # 'Ρ' + 37: 0, # 'Σ' + 33: 0, # 'Τ' + 45: 0, # 'Î¥' + 56: 0, # 'Φ' + 50: 0, # 'Χ' + 57: 0, # 'Ω' + 17: 0, # 'ά' + 18: 0, # 'έ' + 22: 0, # 'ή' + 15: 0, # 'ί' + 1: 0, # 'α' + 29: 0, # 'β' + 20: 0, # 'γ' + 21: 0, # 'δ' + 3: 0, # 'ε' + 32: 0, # 'ζ' + 13: 0, # 'η' + 25: 0, # 'θ' + 5: 0, # 'ι' + 11: 0, # 'κ' + 16: 0, # 'λ' + 10: 0, # 'μ' + 6: 0, # 'ν' + 30: 0, # 'ξ' + 4: 0, # 'ο' + 9: 0, # 'Ï€' + 8: 0, # 'Ï' + 14: 0, # 'Ï‚' + 7: 0, # 'σ' + 2: 0, # 'Ï„' + 12: 0, # 'Ï…' + 28: 0, # 'φ' + 23: 0, # 'χ' + 42: 0, # 'ψ' + 24: 0, # 'ω' + 19: 0, # 'ÏŒ' + 26: 0, # 'Ï' + 27: 0, # 'ÏŽ' + }, + 61: { # 'Ά' + 60: 0, # 'e' + 55: 0, # 'o' + 58: 0, # 't' + 36: 0, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'ÎŒ' + 31: 0, # 'Α' + 51: 0, # 'Î’' + 43: 0, # 'Γ' + 41: 0, # 'Δ' + 34: 0, # 'Ε' + 40: 0, # 'Η' + 52: 0, # 'Θ' + 47: 0, # 'Ι' + 44: 0, # 'Κ' + 53: 0, # 'Λ' + 38: 0, # 'Îœ' + 49: 0, # 'Î' + 59: 0, # 'Ξ' + 39: 0, # 'Ο' + 35: 0, # 'Π' + 48: 0, # 'Ρ' + 37: 0, # 'Σ' + 33: 0, # 'Τ' + 45: 0, # 'Î¥' + 56: 0, # 'Φ' + 50: 0, # 'Χ' + 57: 0, # 'Ω' + 17: 0, # 'ά' + 18: 0, # 'έ' + 22: 0, # 'ή' + 15: 0, # 'ί' + 1: 0, # 'α' + 29: 0, # 'β' + 20: 1, # 'γ' + 21: 2, # 'δ' + 3: 0, # 'ε' + 32: 0, # 'ζ' + 13: 0, # 'η' + 25: 0, # 'θ' + 5: 0, # 'ι' + 11: 0, # 'κ' + 16: 2, # 'λ' + 10: 0, # 'μ' + 6: 0, # 'ν' + 30: 0, # 'ξ' + 4: 0, # 'ο' + 9: 1, # 'Ï€' + 8: 2, # 'Ï' + 14: 0, # 'Ï‚' + 7: 0, # 'σ' + 2: 0, # 'Ï„' + 12: 0, # 'Ï…' + 28: 0, # 'φ' + 23: 0, # 'χ' + 42: 0, # 'ψ' + 24: 0, # 'ω' + 19: 0, # 'ÏŒ' + 26: 0, # 'Ï' + 27: 0, # 'ÏŽ' + }, + 46: { # 'Έ' + 60: 0, # 'e' + 55: 0, # 'o' + 58: 0, # 't' + 36: 0, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'ÎŒ' + 31: 0, # 'Α' + 51: 0, # 'Î’' + 43: 0, # 'Γ' + 41: 0, # 'Δ' + 34: 0, # 'Ε' + 40: 0, # 'Η' + 52: 0, # 'Θ' + 47: 0, # 'Ι' + 44: 0, # 'Κ' + 53: 0, # 'Λ' + 38: 0, # 'Îœ' + 49: 0, # 'Î' + 59: 0, # 'Ξ' + 39: 0, # 'Ο' + 35: 0, # 'Π' + 48: 0, # 'Ρ' + 37: 0, # 'Σ' + 33: 0, # 'Τ' + 45: 0, # 'Î¥' + 56: 0, # 'Φ' + 50: 0, # 'Χ' + 57: 0, # 'Ω' + 17: 0, # 'ά' + 18: 0, # 'έ' + 22: 0, # 'ή' + 15: 0, # 'ί' + 1: 0, # 'α' + 29: 2, # 'β' + 20: 2, # 'γ' + 21: 0, # 'δ' + 3: 0, # 'ε' + 32: 0, # 'ζ' + 13: 0, # 'η' + 25: 0, # 'θ' + 5: 0, # 'ι' + 11: 2, # 'κ' + 16: 2, # 'λ' + 10: 0, # 'μ' + 6: 3, # 'ν' + 30: 2, # 'ξ' + 4: 0, # 'ο' + 9: 2, # 'Ï€' + 8: 2, # 'Ï' + 14: 0, # 'Ï‚' + 7: 1, # 'σ' + 2: 2, # 'Ï„' + 12: 0, # 'Ï…' + 28: 2, # 'φ' + 23: 3, # 'χ' + 42: 0, # 'ψ' + 24: 0, # 'ω' + 19: 0, # 'ÏŒ' + 26: 0, # 'Ï' + 27: 0, # 'ÏŽ' + }, + 54: { # 'ÎŒ' + 60: 0, # 'e' + 55: 0, # 'o' + 58: 0, # 't' + 36: 0, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'ÎŒ' + 31: 0, # 'Α' + 51: 0, # 'Î’' + 43: 0, # 'Γ' + 41: 0, # 'Δ' + 34: 0, # 'Ε' + 40: 0, # 'Η' + 52: 0, # 'Θ' + 47: 0, # 'Ι' + 44: 0, # 'Κ' + 53: 0, # 'Λ' + 38: 0, # 'Îœ' + 49: 0, # 'Î' + 59: 0, # 'Ξ' + 39: 0, # 'Ο' + 35: 0, # 'Π' + 48: 0, # 'Ρ' + 37: 0, # 'Σ' + 33: 0, # 'Τ' + 45: 0, # 'Î¥' + 56: 0, # 'Φ' + 50: 0, # 'Χ' + 57: 0, # 'Ω' + 17: 0, # 'ά' + 18: 0, # 'έ' + 22: 0, # 'ή' + 15: 0, # 'ί' + 1: 0, # 'α' + 29: 0, # 'β' + 20: 0, # 'γ' + 21: 0, # 'δ' + 3: 0, # 'ε' + 32: 0, # 'ζ' + 13: 0, # 'η' + 25: 0, # 'θ' + 5: 0, # 'ι' + 11: 0, # 'κ' + 16: 2, # 'λ' + 10: 2, # 'μ' + 6: 2, # 'ν' + 30: 0, # 'ξ' + 4: 0, # 'ο' + 9: 2, # 'Ï€' + 8: 0, # 'Ï' + 14: 0, # 'Ï‚' + 7: 2, # 'σ' + 2: 3, # 'Ï„' + 12: 0, # 'Ï…' + 28: 0, # 'φ' + 23: 2, # 'χ' + 42: 0, # 'ψ' + 24: 0, # 'ω' + 19: 0, # 'ÏŒ' + 26: 0, # 'Ï' + 27: 0, # 'ÏŽ' + }, + 31: { # 'Α' + 60: 0, # 'e' + 55: 0, # 'o' + 58: 0, # 't' + 36: 0, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'ÎŒ' + 31: 0, # 'Α' + 51: 2, # 'Î’' + 43: 2, # 'Γ' + 41: 1, # 'Δ' + 34: 0, # 'Ε' + 40: 0, # 'Η' + 52: 2, # 'Θ' + 47: 2, # 'Ι' + 44: 2, # 'Κ' + 53: 2, # 'Λ' + 38: 2, # 'Îœ' + 49: 2, # 'Î' + 59: 1, # 'Ξ' + 39: 0, # 'Ο' + 35: 2, # 'Π' + 48: 2, # 'Ρ' + 37: 2, # 'Σ' + 33: 2, # 'Τ' + 45: 2, # 'Î¥' + 56: 2, # 'Φ' + 50: 0, # 'Χ' + 57: 0, # 'Ω' + 17: 0, # 'ά' + 18: 0, # 'έ' + 22: 0, # 'ή' + 15: 0, # 'ί' + 1: 0, # 'α' + 29: 0, # 'β' + 20: 2, # 'γ' + 21: 0, # 'δ' + 3: 0, # 'ε' + 32: 0, # 'ζ' + 13: 0, # 'η' + 25: 1, # 'θ' + 5: 0, # 'ι' + 11: 2, # 'κ' + 16: 3, # 'λ' + 10: 2, # 'μ' + 6: 3, # 'ν' + 30: 2, # 'ξ' + 4: 0, # 'ο' + 9: 3, # 'Ï€' + 8: 3, # 'Ï' + 14: 2, # 'Ï‚' + 7: 2, # 'σ' + 2: 0, # 'Ï„' + 12: 3, # 'Ï…' + 28: 2, # 'φ' + 23: 0, # 'χ' + 42: 0, # 'ψ' + 24: 0, # 'ω' + 19: 0, # 'ÏŒ' + 26: 2, # 'Ï' + 27: 0, # 'ÏŽ' + }, + 51: { # 'Î’' + 60: 0, # 'e' + 55: 0, # 'o' + 58: 0, # 't' + 36: 0, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'ÎŒ' + 31: 2, # 'Α' + 51: 0, # 'Î’' + 43: 0, # 'Γ' + 41: 0, # 'Δ' + 34: 1, # 'Ε' + 40: 1, # 'Η' + 52: 0, # 'Θ' + 47: 1, # 'Ι' + 44: 0, # 'Κ' + 53: 1, # 'Λ' + 38: 0, # 'Îœ' + 49: 0, # 'Î' + 59: 0, # 'Ξ' + 39: 2, # 'Ο' + 35: 0, # 'Π' + 48: 0, # 'Ρ' + 37: 0, # 'Σ' + 33: 0, # 'Τ' + 45: 0, # 'Î¥' + 56: 0, # 'Φ' + 50: 0, # 'Χ' + 57: 0, # 'Ω' + 17: 2, # 'ά' + 18: 2, # 'έ' + 22: 2, # 'ή' + 15: 0, # 'ί' + 1: 2, # 'α' + 29: 0, # 'β' + 20: 0, # 'γ' + 21: 0, # 'δ' + 3: 2, # 'ε' + 32: 0, # 'ζ' + 13: 0, # 'η' + 25: 0, # 'θ' + 5: 2, # 'ι' + 11: 0, # 'κ' + 16: 2, # 'λ' + 10: 0, # 'μ' + 6: 0, # 'ν' + 30: 0, # 'ξ' + 4: 2, # 'ο' + 9: 0, # 'Ï€' + 8: 2, # 'Ï' + 14: 0, # 'Ï‚' + 7: 0, # 'σ' + 2: 0, # 'Ï„' + 12: 0, # 'Ï…' + 28: 0, # 'φ' + 23: 0, # 'χ' + 42: 0, # 'ψ' + 24: 0, # 'ω' + 19: 0, # 'ÏŒ' + 26: 0, # 'Ï' + 27: 0, # 'ÏŽ' + }, + 43: { # 'Γ' + 60: 0, # 'e' + 55: 0, # 'o' + 58: 0, # 't' + 36: 0, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'ÎŒ' + 31: 1, # 'Α' + 51: 0, # 'Î’' + 43: 2, # 'Γ' + 41: 0, # 'Δ' + 34: 2, # 'Ε' + 40: 1, # 'Η' + 52: 0, # 'Θ' + 47: 2, # 'Ι' + 44: 1, # 'Κ' + 53: 1, # 'Λ' + 38: 0, # 'Îœ' + 49: 0, # 'Î' + 59: 0, # 'Ξ' + 39: 1, # 'Ο' + 35: 0, # 'Π' + 48: 2, # 'Ρ' + 37: 0, # 'Σ' + 33: 0, # 'Τ' + 45: 2, # 'Î¥' + 56: 0, # 'Φ' + 50: 1, # 'Χ' + 57: 2, # 'Ω' + 17: 0, # 'ά' + 18: 0, # 'έ' + 22: 0, # 'ή' + 15: 2, # 'ί' + 1: 2, # 'α' + 29: 0, # 'β' + 20: 0, # 'γ' + 21: 0, # 'δ' + 3: 2, # 'ε' + 32: 0, # 'ζ' + 13: 0, # 'η' + 25: 0, # 'θ' + 5: 3, # 'ι' + 11: 0, # 'κ' + 16: 2, # 'λ' + 10: 0, # 'μ' + 6: 2, # 'ν' + 30: 0, # 'ξ' + 4: 0, # 'ο' + 9: 0, # 'Ï€' + 8: 2, # 'Ï' + 14: 0, # 'Ï‚' + 7: 0, # 'σ' + 2: 0, # 'Ï„' + 12: 0, # 'Ï…' + 28: 0, # 'φ' + 23: 0, # 'χ' + 42: 0, # 'ψ' + 24: 0, # 'ω' + 19: 0, # 'ÏŒ' + 26: 0, # 'Ï' + 27: 0, # 'ÏŽ' + }, + 41: { # 'Δ' + 60: 0, # 'e' + 55: 0, # 'o' + 58: 0, # 't' + 36: 0, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'ÎŒ' + 31: 0, # 'Α' + 51: 0, # 'Î’' + 43: 0, # 'Γ' + 41: 0, # 'Δ' + 34: 2, # 'Ε' + 40: 2, # 'Η' + 52: 0, # 'Θ' + 47: 2, # 'Ι' + 44: 0, # 'Κ' + 53: 0, # 'Λ' + 38: 0, # 'Îœ' + 49: 0, # 'Î' + 59: 0, # 'Ξ' + 39: 2, # 'Ο' + 35: 0, # 'Π' + 48: 0, # 'Ρ' + 37: 0, # 'Σ' + 33: 0, # 'Τ' + 45: 0, # 'Î¥' + 56: 0, # 'Φ' + 50: 0, # 'Χ' + 57: 2, # 'Ω' + 17: 0, # 'ά' + 18: 0, # 'έ' + 22: 2, # 'ή' + 15: 2, # 'ί' + 1: 0, # 'α' + 29: 0, # 'β' + 20: 0, # 'γ' + 21: 0, # 'δ' + 3: 3, # 'ε' + 32: 0, # 'ζ' + 13: 2, # 'η' + 25: 0, # 'θ' + 5: 3, # 'ι' + 11: 0, # 'κ' + 16: 0, # 'λ' + 10: 0, # 'μ' + 6: 0, # 'ν' + 30: 0, # 'ξ' + 4: 2, # 'ο' + 9: 0, # 'Ï€' + 8: 2, # 'Ï' + 14: 0, # 'Ï‚' + 7: 0, # 'σ' + 2: 0, # 'Ï„' + 12: 2, # 'Ï…' + 28: 0, # 'φ' + 23: 0, # 'χ' + 42: 0, # 'ψ' + 24: 2, # 'ω' + 19: 1, # 'ÏŒ' + 26: 2, # 'Ï' + 27: 2, # 'ÏŽ' + }, + 34: { # 'Ε' + 60: 0, # 'e' + 55: 0, # 'o' + 58: 0, # 't' + 36: 0, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'ÎŒ' + 31: 2, # 'Α' + 51: 0, # 'Î’' + 43: 2, # 'Γ' + 41: 2, # 'Δ' + 34: 0, # 'Ε' + 40: 0, # 'Η' + 52: 0, # 'Θ' + 47: 2, # 'Ι' + 44: 2, # 'Κ' + 53: 2, # 'Λ' + 38: 2, # 'Îœ' + 49: 2, # 'Î' + 59: 1, # 'Ξ' + 39: 0, # 'Ο' + 35: 2, # 'Π' + 48: 2, # 'Ρ' + 37: 2, # 'Σ' + 33: 2, # 'Τ' + 45: 2, # 'Î¥' + 56: 0, # 'Φ' + 50: 2, # 'Χ' + 57: 2, # 'Ω' + 17: 3, # 'ά' + 18: 0, # 'έ' + 22: 0, # 'ή' + 15: 3, # 'ί' + 1: 0, # 'α' + 29: 0, # 'β' + 20: 3, # 'γ' + 21: 2, # 'δ' + 3: 1, # 'ε' + 32: 0, # 'ζ' + 13: 0, # 'η' + 25: 1, # 'θ' + 5: 2, # 'ι' + 11: 3, # 'κ' + 16: 3, # 'λ' + 10: 2, # 'μ' + 6: 3, # 'ν' + 30: 2, # 'ξ' + 4: 0, # 'ο' + 9: 3, # 'Ï€' + 8: 2, # 'Ï' + 14: 0, # 'Ï‚' + 7: 2, # 'σ' + 2: 2, # 'Ï„' + 12: 2, # 'Ï…' + 28: 2, # 'φ' + 23: 0, # 'χ' + 42: 0, # 'ψ' + 24: 0, # 'ω' + 19: 0, # 'ÏŒ' + 26: 1, # 'Ï' + 27: 0, # 'ÏŽ' + }, + 40: { # 'Η' + 60: 0, # 'e' + 55: 0, # 'o' + 58: 0, # 't' + 36: 0, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'ÎŒ' + 31: 0, # 'Α' + 51: 0, # 'Î’' + 43: 1, # 'Γ' + 41: 0, # 'Δ' + 34: 0, # 'Ε' + 40: 0, # 'Η' + 52: 2, # 'Θ' + 47: 0, # 'Ι' + 44: 2, # 'Κ' + 53: 0, # 'Λ' + 38: 2, # 'Îœ' + 49: 2, # 'Î' + 59: 0, # 'Ξ' + 39: 0, # 'Ο' + 35: 2, # 'Π' + 48: 2, # 'Ρ' + 37: 2, # 'Σ' + 33: 2, # 'Τ' + 45: 1, # 'Î¥' + 56: 0, # 'Φ' + 50: 0, # 'Χ' + 57: 0, # 'Ω' + 17: 0, # 'ά' + 18: 0, # 'έ' + 22: 0, # 'ή' + 15: 0, # 'ί' + 1: 0, # 'α' + 29: 0, # 'β' + 20: 0, # 'γ' + 21: 0, # 'δ' + 3: 0, # 'ε' + 32: 0, # 'ζ' + 13: 0, # 'η' + 25: 0, # 'θ' + 5: 0, # 'ι' + 11: 0, # 'κ' + 16: 2, # 'λ' + 10: 0, # 'μ' + 6: 1, # 'ν' + 30: 0, # 'ξ' + 4: 0, # 'ο' + 9: 0, # 'Ï€' + 8: 0, # 'Ï' + 14: 0, # 'Ï‚' + 7: 0, # 'σ' + 2: 0, # 'Ï„' + 12: 0, # 'Ï…' + 28: 0, # 'φ' + 23: 1, # 'χ' + 42: 0, # 'ψ' + 24: 0, # 'ω' + 19: 0, # 'ÏŒ' + 26: 0, # 'Ï' + 27: 0, # 'ÏŽ' + }, + 52: { # 'Θ' + 60: 0, # 'e' + 55: 0, # 'o' + 58: 0, # 't' + 36: 0, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'ÎŒ' + 31: 2, # 'Α' + 51: 0, # 'Î’' + 43: 0, # 'Γ' + 41: 0, # 'Δ' + 34: 2, # 'Ε' + 40: 2, # 'Η' + 52: 0, # 'Θ' + 47: 0, # 'Ι' + 44: 0, # 'Κ' + 53: 0, # 'Λ' + 38: 0, # 'Îœ' + 49: 0, # 'Î' + 59: 0, # 'Ξ' + 39: 2, # 'Ο' + 35: 0, # 'Π' + 48: 1, # 'Ρ' + 37: 0, # 'Σ' + 33: 0, # 'Τ' + 45: 1, # 'Î¥' + 56: 0, # 'Φ' + 50: 0, # 'Χ' + 57: 0, # 'Ω' + 17: 0, # 'ά' + 18: 2, # 'έ' + 22: 0, # 'ή' + 15: 0, # 'ί' + 1: 3, # 'α' + 29: 0, # 'β' + 20: 0, # 'γ' + 21: 0, # 'δ' + 3: 2, # 'ε' + 32: 0, # 'ζ' + 13: 0, # 'η' + 25: 0, # 'θ' + 5: 0, # 'ι' + 11: 0, # 'κ' + 16: 0, # 'λ' + 10: 0, # 'μ' + 6: 0, # 'ν' + 30: 0, # 'ξ' + 4: 0, # 'ο' + 9: 0, # 'Ï€' + 8: 0, # 'Ï' + 14: 0, # 'Ï‚' + 7: 0, # 'σ' + 2: 0, # 'Ï„' + 12: 2, # 'Ï…' + 28: 0, # 'φ' + 23: 0, # 'χ' + 42: 0, # 'ψ' + 24: 0, # 'ω' + 19: 0, # 'ÏŒ' + 26: 2, # 'Ï' + 27: 0, # 'ÏŽ' + }, + 47: { # 'Ι' + 60: 0, # 'e' + 55: 0, # 'o' + 58: 0, # 't' + 36: 0, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'ÎŒ' + 31: 2, # 'Α' + 51: 1, # 'Î’' + 43: 1, # 'Γ' + 41: 2, # 'Δ' + 34: 2, # 'Ε' + 40: 2, # 'Η' + 52: 0, # 'Θ' + 47: 0, # 'Ι' + 44: 2, # 'Κ' + 53: 2, # 'Λ' + 38: 2, # 'Îœ' + 49: 2, # 'Î' + 59: 0, # 'Ξ' + 39: 2, # 'Ο' + 35: 0, # 'Π' + 48: 2, # 'Ρ' + 37: 2, # 'Σ' + 33: 2, # 'Τ' + 45: 0, # 'Î¥' + 56: 2, # 'Φ' + 50: 0, # 'Χ' + 57: 2, # 'Ω' + 17: 0, # 'ά' + 18: 0, # 'έ' + 22: 0, # 'ή' + 15: 0, # 'ί' + 1: 2, # 'α' + 29: 0, # 'β' + 20: 0, # 'γ' + 21: 2, # 'δ' + 3: 0, # 'ε' + 32: 0, # 'ζ' + 13: 0, # 'η' + 25: 0, # 'θ' + 5: 0, # 'ι' + 11: 0, # 'κ' + 16: 0, # 'λ' + 10: 0, # 'μ' + 6: 1, # 'ν' + 30: 0, # 'ξ' + 4: 2, # 'ο' + 9: 0, # 'Ï€' + 8: 0, # 'Ï' + 14: 0, # 'Ï‚' + 7: 2, # 'σ' + 2: 1, # 'Ï„' + 12: 0, # 'Ï…' + 28: 0, # 'φ' + 23: 0, # 'χ' + 42: 0, # 'ψ' + 24: 1, # 'ω' + 19: 0, # 'ÏŒ' + 26: 0, # 'Ï' + 27: 0, # 'ÏŽ' + }, + 44: { # 'Κ' + 60: 0, # 'e' + 55: 0, # 'o' + 58: 0, # 't' + 36: 0, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'ÎŒ' + 31: 2, # 'Α' + 51: 0, # 'Î’' + 43: 0, # 'Γ' + 41: 1, # 'Δ' + 34: 2, # 'Ε' + 40: 2, # 'Η' + 52: 0, # 'Θ' + 47: 0, # 'Ι' + 44: 0, # 'Κ' + 53: 0, # 'Λ' + 38: 1, # 'Îœ' + 49: 0, # 'Î' + 59: 0, # 'Ξ' + 39: 2, # 'Ο' + 35: 0, # 'Π' + 48: 2, # 'Ρ' + 37: 0, # 'Σ' + 33: 1, # 'Τ' + 45: 2, # 'Î¥' + 56: 0, # 'Φ' + 50: 0, # 'Χ' + 57: 1, # 'Ω' + 17: 3, # 'ά' + 18: 0, # 'έ' + 22: 0, # 'ή' + 15: 0, # 'ί' + 1: 3, # 'α' + 29: 0, # 'β' + 20: 0, # 'γ' + 21: 0, # 'δ' + 3: 2, # 'ε' + 32: 0, # 'ζ' + 13: 0, # 'η' + 25: 0, # 'θ' + 5: 2, # 'ι' + 11: 0, # 'κ' + 16: 2, # 'λ' + 10: 0, # 'μ' + 6: 0, # 'ν' + 30: 0, # 'ξ' + 4: 2, # 'ο' + 9: 0, # 'Ï€' + 8: 2, # 'Ï' + 14: 0, # 'Ï‚' + 7: 0, # 'σ' + 2: 0, # 'Ï„' + 12: 2, # 'Ï…' + 28: 0, # 'φ' + 23: 0, # 'χ' + 42: 0, # 'ψ' + 24: 0, # 'ω' + 19: 2, # 'ÏŒ' + 26: 2, # 'Ï' + 27: 2, # 'ÏŽ' + }, + 53: { # 'Λ' + 60: 0, # 'e' + 55: 0, # 'o' + 58: 0, # 't' + 36: 0, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'ÎŒ' + 31: 2, # 'Α' + 51: 0, # 'Î’' + 43: 0, # 'Γ' + 41: 0, # 'Δ' + 34: 2, # 'Ε' + 40: 2, # 'Η' + 52: 0, # 'Θ' + 47: 2, # 'Ι' + 44: 0, # 'Κ' + 53: 2, # 'Λ' + 38: 0, # 'Îœ' + 49: 0, # 'Î' + 59: 0, # 'Ξ' + 39: 2, # 'Ο' + 35: 0, # 'Π' + 48: 0, # 'Ρ' + 37: 2, # 'Σ' + 33: 0, # 'Τ' + 45: 2, # 'Î¥' + 56: 0, # 'Φ' + 50: 0, # 'Χ' + 57: 2, # 'Ω' + 17: 2, # 'ά' + 18: 2, # 'έ' + 22: 0, # 'ή' + 15: 2, # 'ί' + 1: 2, # 'α' + 29: 0, # 'β' + 20: 0, # 'γ' + 21: 0, # 'δ' + 3: 2, # 'ε' + 32: 0, # 'ζ' + 13: 0, # 'η' + 25: 0, # 'θ' + 5: 1, # 'ι' + 11: 0, # 'κ' + 16: 0, # 'λ' + 10: 0, # 'μ' + 6: 0, # 'ν' + 30: 0, # 'ξ' + 4: 2, # 'ο' + 9: 0, # 'Ï€' + 8: 0, # 'Ï' + 14: 0, # 'Ï‚' + 7: 0, # 'σ' + 2: 0, # 'Ï„' + 12: 2, # 'Ï…' + 28: 0, # 'φ' + 23: 0, # 'χ' + 42: 0, # 'ψ' + 24: 0, # 'ω' + 19: 2, # 'ÏŒ' + 26: 2, # 'Ï' + 27: 0, # 'ÏŽ' + }, + 38: { # 'Îœ' + 60: 0, # 'e' + 55: 0, # 'o' + 58: 0, # 't' + 36: 0, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'ÎŒ' + 31: 2, # 'Α' + 51: 2, # 'Î’' + 43: 0, # 'Γ' + 41: 0, # 'Δ' + 34: 2, # 'Ε' + 40: 2, # 'Η' + 52: 0, # 'Θ' + 47: 2, # 'Ι' + 44: 0, # 'Κ' + 53: 0, # 'Λ' + 38: 2, # 'Îœ' + 49: 0, # 'Î' + 59: 0, # 'Ξ' + 39: 2, # 'Ο' + 35: 2, # 'Π' + 48: 0, # 'Ρ' + 37: 0, # 'Σ' + 33: 0, # 'Τ' + 45: 0, # 'Î¥' + 56: 0, # 'Φ' + 50: 0, # 'Χ' + 57: 0, # 'Ω' + 17: 2, # 'ά' + 18: 2, # 'έ' + 22: 2, # 'ή' + 15: 2, # 'ί' + 1: 2, # 'α' + 29: 0, # 'β' + 20: 0, # 'γ' + 21: 0, # 'δ' + 3: 3, # 'ε' + 32: 0, # 'ζ' + 13: 2, # 'η' + 25: 0, # 'θ' + 5: 3, # 'ι' + 11: 0, # 'κ' + 16: 0, # 'λ' + 10: 0, # 'μ' + 6: 0, # 'ν' + 30: 0, # 'ξ' + 4: 2, # 'ο' + 9: 3, # 'Ï€' + 8: 0, # 'Ï' + 14: 0, # 'Ï‚' + 7: 0, # 'σ' + 2: 0, # 'Ï„' + 12: 2, # 'Ï…' + 28: 0, # 'φ' + 23: 0, # 'χ' + 42: 0, # 'ψ' + 24: 0, # 'ω' + 19: 2, # 'ÏŒ' + 26: 0, # 'Ï' + 27: 0, # 'ÏŽ' + }, + 49: { # 'Î' + 60: 2, # 'e' + 55: 0, # 'o' + 58: 0, # 't' + 36: 0, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'ÎŒ' + 31: 2, # 'Α' + 51: 0, # 'Î’' + 43: 0, # 'Γ' + 41: 0, # 'Δ' + 34: 2, # 'Ε' + 40: 2, # 'Η' + 52: 0, # 'Θ' + 47: 2, # 'Ι' + 44: 0, # 'Κ' + 53: 0, # 'Λ' + 38: 0, # 'Îœ' + 49: 0, # 'Î' + 59: 0, # 'Ξ' + 39: 2, # 'Ο' + 35: 0, # 'Π' + 48: 0, # 'Ρ' + 37: 0, # 'Σ' + 33: 2, # 'Τ' + 45: 0, # 'Î¥' + 56: 0, # 'Φ' + 50: 0, # 'Χ' + 57: 2, # 'Ω' + 17: 0, # 'ά' + 18: 2, # 'έ' + 22: 0, # 'ή' + 15: 2, # 'ί' + 1: 2, # 'α' + 29: 0, # 'β' + 20: 0, # 'γ' + 21: 0, # 'δ' + 3: 1, # 'ε' + 32: 0, # 'ζ' + 13: 0, # 'η' + 25: 0, # 'θ' + 5: 0, # 'ι' + 11: 0, # 'κ' + 16: 0, # 'λ' + 10: 0, # 'μ' + 6: 0, # 'ν' + 30: 0, # 'ξ' + 4: 2, # 'ο' + 9: 0, # 'Ï€' + 8: 0, # 'Ï' + 14: 0, # 'Ï‚' + 7: 0, # 'σ' + 2: 0, # 'Ï„' + 12: 0, # 'Ï…' + 28: 0, # 'φ' + 23: 0, # 'χ' + 42: 0, # 'ψ' + 24: 1, # 'ω' + 19: 2, # 'ÏŒ' + 26: 0, # 'Ï' + 27: 0, # 'ÏŽ' + }, + 59: { # 'Ξ' + 60: 0, # 'e' + 55: 0, # 'o' + 58: 0, # 't' + 36: 0, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'ÎŒ' + 31: 0, # 'Α' + 51: 0, # 'Î’' + 43: 0, # 'Γ' + 41: 0, # 'Δ' + 34: 1, # 'Ε' + 40: 1, # 'Η' + 52: 0, # 'Θ' + 47: 0, # 'Ι' + 44: 0, # 'Κ' + 53: 0, # 'Λ' + 38: 0, # 'Îœ' + 49: 0, # 'Î' + 59: 0, # 'Ξ' + 39: 1, # 'Ο' + 35: 0, # 'Π' + 48: 0, # 'Ρ' + 37: 0, # 'Σ' + 33: 0, # 'Τ' + 45: 0, # 'Î¥' + 56: 0, # 'Φ' + 50: 0, # 'Χ' + 57: 0, # 'Ω' + 17: 0, # 'ά' + 18: 2, # 'έ' + 22: 0, # 'ή' + 15: 0, # 'ί' + 1: 2, # 'α' + 29: 0, # 'β' + 20: 0, # 'γ' + 21: 0, # 'δ' + 3: 2, # 'ε' + 32: 0, # 'ζ' + 13: 0, # 'η' + 25: 0, # 'θ' + 5: 0, # 'ι' + 11: 0, # 'κ' + 16: 0, # 'λ' + 10: 0, # 'μ' + 6: 0, # 'ν' + 30: 0, # 'ξ' + 4: 0, # 'ο' + 9: 0, # 'Ï€' + 8: 0, # 'Ï' + 14: 0, # 'Ï‚' + 7: 0, # 'σ' + 2: 0, # 'Ï„' + 12: 0, # 'Ï…' + 28: 0, # 'φ' + 23: 0, # 'χ' + 42: 0, # 'ψ' + 24: 0, # 'ω' + 19: 0, # 'ÏŒ' + 26: 0, # 'Ï' + 27: 0, # 'ÏŽ' + }, + 39: { # 'Ο' + 60: 0, # 'e' + 55: 0, # 'o' + 58: 0, # 't' + 36: 0, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'ÎŒ' + 31: 0, # 'Α' + 51: 1, # 'Î’' + 43: 2, # 'Γ' + 41: 2, # 'Δ' + 34: 2, # 'Ε' + 40: 1, # 'Η' + 52: 2, # 'Θ' + 47: 2, # 'Ι' + 44: 2, # 'Κ' + 53: 2, # 'Λ' + 38: 2, # 'Îœ' + 49: 2, # 'Î' + 59: 0, # 'Ξ' + 39: 0, # 'Ο' + 35: 2, # 'Π' + 48: 2, # 'Ρ' + 37: 2, # 'Σ' + 33: 2, # 'Τ' + 45: 2, # 'Î¥' + 56: 2, # 'Φ' + 50: 2, # 'Χ' + 57: 0, # 'Ω' + 17: 0, # 'ά' + 18: 0, # 'έ' + 22: 0, # 'ή' + 15: 0, # 'ί' + 1: 0, # 'α' + 29: 0, # 'β' + 20: 0, # 'γ' + 21: 2, # 'δ' + 3: 0, # 'ε' + 32: 0, # 'ζ' + 13: 0, # 'η' + 25: 0, # 'θ' + 5: 3, # 'ι' + 11: 2, # 'κ' + 16: 2, # 'λ' + 10: 2, # 'μ' + 6: 2, # 'ν' + 30: 0, # 'ξ' + 4: 0, # 'ο' + 9: 2, # 'Ï€' + 8: 2, # 'Ï' + 14: 0, # 'Ï‚' + 7: 0, # 'σ' + 2: 2, # 'Ï„' + 12: 2, # 'Ï…' + 28: 1, # 'φ' + 23: 1, # 'χ' + 42: 0, # 'ψ' + 24: 0, # 'ω' + 19: 0, # 'ÏŒ' + 26: 2, # 'Ï' + 27: 0, # 'ÏŽ' + }, + 35: { # 'Π' + 60: 0, # 'e' + 55: 0, # 'o' + 58: 0, # 't' + 36: 0, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'ÎŒ' + 31: 2, # 'Α' + 51: 0, # 'Î’' + 43: 0, # 'Γ' + 41: 0, # 'Δ' + 34: 2, # 'Ε' + 40: 0, # 'Η' + 52: 0, # 'Θ' + 47: 2, # 'Ι' + 44: 0, # 'Κ' + 53: 2, # 'Λ' + 38: 1, # 'Îœ' + 49: 0, # 'Î' + 59: 0, # 'Ξ' + 39: 2, # 'Ο' + 35: 0, # 'Π' + 48: 2, # 'Ρ' + 37: 0, # 'Σ' + 33: 1, # 'Τ' + 45: 0, # 'Î¥' + 56: 0, # 'Φ' + 50: 1, # 'Χ' + 57: 2, # 'Ω' + 17: 2, # 'ά' + 18: 1, # 'έ' + 22: 1, # 'ή' + 15: 2, # 'ί' + 1: 3, # 'α' + 29: 0, # 'β' + 20: 0, # 'γ' + 21: 0, # 'δ' + 3: 3, # 'ε' + 32: 0, # 'ζ' + 13: 2, # 'η' + 25: 0, # 'θ' + 5: 2, # 'ι' + 11: 0, # 'κ' + 16: 2, # 'λ' + 10: 0, # 'μ' + 6: 2, # 'ν' + 30: 0, # 'ξ' + 4: 3, # 'ο' + 9: 0, # 'Ï€' + 8: 3, # 'Ï' + 14: 0, # 'Ï‚' + 7: 0, # 'σ' + 2: 0, # 'Ï„' + 12: 2, # 'Ï…' + 28: 0, # 'φ' + 23: 2, # 'χ' + 42: 0, # 'ψ' + 24: 2, # 'ω' + 19: 2, # 'ÏŒ' + 26: 0, # 'Ï' + 27: 3, # 'ÏŽ' + }, + 48: { # 'Ρ' + 60: 0, # 'e' + 55: 0, # 'o' + 58: 0, # 't' + 36: 0, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'ÎŒ' + 31: 2, # 'Α' + 51: 0, # 'Î’' + 43: 1, # 'Γ' + 41: 1, # 'Δ' + 34: 2, # 'Ε' + 40: 2, # 'Η' + 52: 0, # 'Θ' + 47: 2, # 'Ι' + 44: 0, # 'Κ' + 53: 0, # 'Λ' + 38: 0, # 'Îœ' + 49: 2, # 'Î' + 59: 0, # 'Ξ' + 39: 2, # 'Ο' + 35: 0, # 'Π' + 48: 2, # 'Ρ' + 37: 0, # 'Σ' + 33: 1, # 'Τ' + 45: 1, # 'Î¥' + 56: 0, # 'Φ' + 50: 1, # 'Χ' + 57: 1, # 'Ω' + 17: 0, # 'ά' + 18: 0, # 'έ' + 22: 0, # 'ή' + 15: 2, # 'ί' + 1: 0, # 'α' + 29: 0, # 'β' + 20: 0, # 'γ' + 21: 0, # 'δ' + 3: 0, # 'ε' + 32: 0, # 'ζ' + 13: 0, # 'η' + 25: 0, # 'θ' + 5: 0, # 'ι' + 11: 0, # 'κ' + 16: 0, # 'λ' + 10: 0, # 'μ' + 6: 0, # 'ν' + 30: 0, # 'ξ' + 4: 1, # 'ο' + 9: 0, # 'Ï€' + 8: 0, # 'Ï' + 14: 0, # 'Ï‚' + 7: 0, # 'σ' + 2: 0, # 'Ï„' + 12: 3, # 'Ï…' + 28: 0, # 'φ' + 23: 0, # 'χ' + 42: 0, # 'ψ' + 24: 2, # 'ω' + 19: 0, # 'ÏŒ' + 26: 2, # 'Ï' + 27: 0, # 'ÏŽ' + }, + 37: { # 'Σ' + 60: 0, # 'e' + 55: 0, # 'o' + 58: 0, # 't' + 36: 0, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'ÎŒ' + 31: 2, # 'Α' + 51: 0, # 'Î’' + 43: 0, # 'Γ' + 41: 1, # 'Δ' + 34: 2, # 'Ε' + 40: 2, # 'Η' + 52: 0, # 'Θ' + 47: 2, # 'Ι' + 44: 2, # 'Κ' + 53: 0, # 'Λ' + 38: 2, # 'Îœ' + 49: 0, # 'Î' + 59: 0, # 'Ξ' + 39: 2, # 'Ο' + 35: 0, # 'Π' + 48: 0, # 'Ρ' + 37: 2, # 'Σ' + 33: 2, # 'Τ' + 45: 2, # 'Î¥' + 56: 0, # 'Φ' + 50: 2, # 'Χ' + 57: 2, # 'Ω' + 17: 0, # 'ά' + 18: 0, # 'έ' + 22: 2, # 'ή' + 15: 2, # 'ί' + 1: 2, # 'α' + 29: 2, # 'β' + 20: 0, # 'γ' + 21: 0, # 'δ' + 3: 3, # 'ε' + 32: 0, # 'ζ' + 13: 3, # 'η' + 25: 0, # 'θ' + 5: 2, # 'ι' + 11: 2, # 'κ' + 16: 0, # 'λ' + 10: 0, # 'μ' + 6: 0, # 'ν' + 30: 0, # 'ξ' + 4: 2, # 'ο' + 9: 2, # 'Ï€' + 8: 0, # 'Ï' + 14: 0, # 'Ï‚' + 7: 0, # 'σ' + 2: 3, # 'Ï„' + 12: 3, # 'Ï…' + 28: 0, # 'φ' + 23: 2, # 'χ' + 42: 0, # 'ψ' + 24: 2, # 'ω' + 19: 0, # 'ÏŒ' + 26: 2, # 'Ï' + 27: 2, # 'ÏŽ' + }, + 33: { # 'Τ' + 60: 0, # 'e' + 55: 1, # 'o' + 58: 0, # 't' + 36: 0, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'ÎŒ' + 31: 2, # 'Α' + 51: 0, # 'Î’' + 43: 0, # 'Γ' + 41: 0, # 'Δ' + 34: 2, # 'Ε' + 40: 2, # 'Η' + 52: 0, # 'Θ' + 47: 2, # 'Ι' + 44: 2, # 'Κ' + 53: 0, # 'Λ' + 38: 0, # 'Îœ' + 49: 0, # 'Î' + 59: 0, # 'Ξ' + 39: 2, # 'Ο' + 35: 0, # 'Π' + 48: 2, # 'Ρ' + 37: 0, # 'Σ' + 33: 1, # 'Τ' + 45: 1, # 'Î¥' + 56: 0, # 'Φ' + 50: 0, # 'Χ' + 57: 2, # 'Ω' + 17: 2, # 'ά' + 18: 2, # 'έ' + 22: 0, # 'ή' + 15: 2, # 'ί' + 1: 3, # 'α' + 29: 0, # 'β' + 20: 0, # 'γ' + 21: 0, # 'δ' + 3: 2, # 'ε' + 32: 0, # 'ζ' + 13: 2, # 'η' + 25: 0, # 'θ' + 5: 2, # 'ι' + 11: 0, # 'κ' + 16: 0, # 'λ' + 10: 2, # 'μ' + 6: 0, # 'ν' + 30: 0, # 'ξ' + 4: 3, # 'ο' + 9: 0, # 'Ï€' + 8: 2, # 'Ï' + 14: 0, # 'Ï‚' + 7: 2, # 'σ' + 2: 0, # 'Ï„' + 12: 2, # 'Ï…' + 28: 0, # 'φ' + 23: 0, # 'χ' + 42: 0, # 'ψ' + 24: 0, # 'ω' + 19: 2, # 'ÏŒ' + 26: 2, # 'Ï' + 27: 3, # 'ÏŽ' + }, + 45: { # 'Î¥' + 60: 0, # 'e' + 55: 0, # 'o' + 58: 0, # 't' + 36: 0, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'ÎŒ' + 31: 0, # 'Α' + 51: 0, # 'Î’' + 43: 2, # 'Γ' + 41: 0, # 'Δ' + 34: 1, # 'Ε' + 40: 2, # 'Η' + 52: 2, # 'Θ' + 47: 0, # 'Ι' + 44: 0, # 'Κ' + 53: 1, # 'Λ' + 38: 2, # 'Îœ' + 49: 2, # 'Î' + 59: 0, # 'Ξ' + 39: 0, # 'Ο' + 35: 2, # 'Π' + 48: 1, # 'Ρ' + 37: 2, # 'Σ' + 33: 2, # 'Τ' + 45: 0, # 'Î¥' + 56: 0, # 'Φ' + 50: 1, # 'Χ' + 57: 0, # 'Ω' + 17: 0, # 'ά' + 18: 0, # 'έ' + 22: 0, # 'ή' + 15: 0, # 'ί' + 1: 0, # 'α' + 29: 0, # 'β' + 20: 0, # 'γ' + 21: 0, # 'δ' + 3: 0, # 'ε' + 32: 0, # 'ζ' + 13: 0, # 'η' + 25: 0, # 'θ' + 5: 0, # 'ι' + 11: 0, # 'κ' + 16: 2, # 'λ' + 10: 0, # 'μ' + 6: 0, # 'ν' + 30: 0, # 'ξ' + 4: 0, # 'ο' + 9: 3, # 'Ï€' + 8: 0, # 'Ï' + 14: 0, # 'Ï‚' + 7: 0, # 'σ' + 2: 0, # 'Ï„' + 12: 0, # 'Ï…' + 28: 0, # 'φ' + 23: 0, # 'χ' + 42: 0, # 'ψ' + 24: 0, # 'ω' + 19: 0, # 'ÏŒ' + 26: 0, # 'Ï' + 27: 0, # 'ÏŽ' + }, + 56: { # 'Φ' + 60: 0, # 'e' + 55: 0, # 'o' + 58: 0, # 't' + 36: 0, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'ÎŒ' + 31: 1, # 'Α' + 51: 0, # 'Î’' + 43: 0, # 'Γ' + 41: 0, # 'Δ' + 34: 0, # 'Ε' + 40: 1, # 'Η' + 52: 0, # 'Θ' + 47: 2, # 'Ι' + 44: 0, # 'Κ' + 53: 0, # 'Λ' + 38: 0, # 'Îœ' + 49: 0, # 'Î' + 59: 0, # 'Ξ' + 39: 2, # 'Ο' + 35: 0, # 'Π' + 48: 0, # 'Ρ' + 37: 0, # 'Σ' + 33: 0, # 'Τ' + 45: 0, # 'Î¥' + 56: 0, # 'Φ' + 50: 0, # 'Χ' + 57: 0, # 'Ω' + 17: 0, # 'ά' + 18: 0, # 'έ' + 22: 0, # 'ή' + 15: 0, # 'ί' + 1: 2, # 'α' + 29: 0, # 'β' + 20: 0, # 'γ' + 21: 0, # 'δ' + 3: 2, # 'ε' + 32: 0, # 'ζ' + 13: 0, # 'η' + 25: 0, # 'θ' + 5: 2, # 'ι' + 11: 0, # 'κ' + 16: 0, # 'λ' + 10: 0, # 'μ' + 6: 0, # 'ν' + 30: 0, # 'ξ' + 4: 2, # 'ο' + 9: 0, # 'Ï€' + 8: 0, # 'Ï' + 14: 0, # 'Ï‚' + 7: 0, # 'σ' + 2: 2, # 'Ï„' + 12: 2, # 'Ï…' + 28: 0, # 'φ' + 23: 0, # 'χ' + 42: 0, # 'ψ' + 24: 0, # 'ω' + 19: 0, # 'ÏŒ' + 26: 1, # 'Ï' + 27: 1, # 'ÏŽ' + }, + 50: { # 'Χ' + 60: 0, # 'e' + 55: 0, # 'o' + 58: 0, # 't' + 36: 0, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'ÎŒ' + 31: 1, # 'Α' + 51: 0, # 'Î’' + 43: 0, # 'Γ' + 41: 0, # 'Δ' + 34: 2, # 'Ε' + 40: 2, # 'Η' + 52: 0, # 'Θ' + 47: 2, # 'Ι' + 44: 0, # 'Κ' + 53: 0, # 'Λ' + 38: 0, # 'Îœ' + 49: 1, # 'Î' + 59: 0, # 'Ξ' + 39: 1, # 'Ο' + 35: 0, # 'Π' + 48: 2, # 'Ρ' + 37: 0, # 'Σ' + 33: 0, # 'Τ' + 45: 0, # 'Î¥' + 56: 0, # 'Φ' + 50: 1, # 'Χ' + 57: 1, # 'Ω' + 17: 2, # 'ά' + 18: 0, # 'έ' + 22: 0, # 'ή' + 15: 0, # 'ί' + 1: 2, # 'α' + 29: 0, # 'β' + 20: 0, # 'γ' + 21: 0, # 'δ' + 3: 2, # 'ε' + 32: 0, # 'ζ' + 13: 0, # 'η' + 25: 0, # 'θ' + 5: 0, # 'ι' + 11: 0, # 'κ' + 16: 0, # 'λ' + 10: 0, # 'μ' + 6: 0, # 'ν' + 30: 0, # 'ξ' + 4: 2, # 'ο' + 9: 0, # 'Ï€' + 8: 3, # 'Ï' + 14: 0, # 'Ï‚' + 7: 0, # 'σ' + 2: 2, # 'Ï„' + 12: 0, # 'Ï…' + 28: 0, # 'φ' + 23: 0, # 'χ' + 42: 0, # 'ψ' + 24: 2, # 'ω' + 19: 0, # 'ÏŒ' + 26: 0, # 'Ï' + 27: 0, # 'ÏŽ' + }, + 57: { # 'Ω' + 60: 0, # 'e' + 55: 0, # 'o' + 58: 0, # 't' + 36: 0, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'ÎŒ' + 31: 0, # 'Α' + 51: 0, # 'Î’' + 43: 1, # 'Γ' + 41: 0, # 'Δ' + 34: 0, # 'Ε' + 40: 0, # 'Η' + 52: 0, # 'Θ' + 47: 0, # 'Ι' + 44: 0, # 'Κ' + 53: 1, # 'Λ' + 38: 0, # 'Îœ' + 49: 2, # 'Î' + 59: 0, # 'Ξ' + 39: 0, # 'Ο' + 35: 0, # 'Π' + 48: 2, # 'Ρ' + 37: 2, # 'Σ' + 33: 2, # 'Τ' + 45: 0, # 'Î¥' + 56: 0, # 'Φ' + 50: 0, # 'Χ' + 57: 0, # 'Ω' + 17: 0, # 'ά' + 18: 0, # 'έ' + 22: 0, # 'ή' + 15: 0, # 'ί' + 1: 0, # 'α' + 29: 0, # 'β' + 20: 0, # 'γ' + 21: 0, # 'δ' + 3: 0, # 'ε' + 32: 0, # 'ζ' + 13: 0, # 'η' + 25: 0, # 'θ' + 5: 0, # 'ι' + 11: 0, # 'κ' + 16: 0, # 'λ' + 10: 0, # 'μ' + 6: 0, # 'ν' + 30: 0, # 'ξ' + 4: 0, # 'ο' + 9: 0, # 'Ï€' + 8: 2, # 'Ï' + 14: 2, # 'Ï‚' + 7: 2, # 'σ' + 2: 0, # 'Ï„' + 12: 0, # 'Ï…' + 28: 0, # 'φ' + 23: 1, # 'χ' + 42: 0, # 'ψ' + 24: 0, # 'ω' + 19: 0, # 'ÏŒ' + 26: 0, # 'Ï' + 27: 0, # 'ÏŽ' + }, + 17: { # 'ά' + 60: 0, # 'e' + 55: 0, # 'o' + 58: 0, # 't' + 36: 2, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'ÎŒ' + 31: 0, # 'Α' + 51: 0, # 'Î’' + 43: 0, # 'Γ' + 41: 0, # 'Δ' + 34: 0, # 'Ε' + 40: 0, # 'Η' + 52: 0, # 'Θ' + 47: 0, # 'Ι' + 44: 0, # 'Κ' + 53: 0, # 'Λ' + 38: 0, # 'Îœ' + 49: 0, # 'Î' + 59: 0, # 'Ξ' + 39: 0, # 'Ο' + 35: 0, # 'Π' + 48: 0, # 'Ρ' + 37: 0, # 'Σ' + 33: 0, # 'Τ' + 45: 0, # 'Î¥' + 56: 0, # 'Φ' + 50: 0, # 'Χ' + 57: 0, # 'Ω' + 17: 0, # 'ά' + 18: 0, # 'έ' + 22: 0, # 'ή' + 15: 0, # 'ί' + 1: 0, # 'α' + 29: 3, # 'β' + 20: 3, # 'γ' + 21: 3, # 'δ' + 3: 3, # 'ε' + 32: 3, # 'ζ' + 13: 0, # 'η' + 25: 3, # 'θ' + 5: 2, # 'ι' + 11: 3, # 'κ' + 16: 3, # 'λ' + 10: 3, # 'μ' + 6: 3, # 'ν' + 30: 3, # 'ξ' + 4: 0, # 'ο' + 9: 3, # 'Ï€' + 8: 3, # 'Ï' + 14: 3, # 'Ï‚' + 7: 3, # 'σ' + 2: 3, # 'Ï„' + 12: 0, # 'Ï…' + 28: 3, # 'φ' + 23: 3, # 'χ' + 42: 3, # 'ψ' + 24: 2, # 'ω' + 19: 0, # 'ÏŒ' + 26: 0, # 'Ï' + 27: 0, # 'ÏŽ' + }, + 18: { # 'έ' + 60: 0, # 'e' + 55: 0, # 'o' + 58: 0, # 't' + 36: 0, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'ÎŒ' + 31: 0, # 'Α' + 51: 0, # 'Î’' + 43: 0, # 'Γ' + 41: 0, # 'Δ' + 34: 0, # 'Ε' + 40: 0, # 'Η' + 52: 0, # 'Θ' + 47: 0, # 'Ι' + 44: 0, # 'Κ' + 53: 0, # 'Λ' + 38: 0, # 'Îœ' + 49: 0, # 'Î' + 59: 0, # 'Ξ' + 39: 0, # 'Ο' + 35: 0, # 'Π' + 48: 0, # 'Ρ' + 37: 0, # 'Σ' + 33: 0, # 'Τ' + 45: 0, # 'Î¥' + 56: 0, # 'Φ' + 50: 0, # 'Χ' + 57: 0, # 'Ω' + 17: 0, # 'ά' + 18: 0, # 'έ' + 22: 0, # 'ή' + 15: 0, # 'ί' + 1: 3, # 'α' + 29: 2, # 'β' + 20: 3, # 'γ' + 21: 2, # 'δ' + 3: 3, # 'ε' + 32: 2, # 'ζ' + 13: 0, # 'η' + 25: 3, # 'θ' + 5: 0, # 'ι' + 11: 3, # 'κ' + 16: 3, # 'λ' + 10: 3, # 'μ' + 6: 3, # 'ν' + 30: 3, # 'ξ' + 4: 3, # 'ο' + 9: 3, # 'Ï€' + 8: 3, # 'Ï' + 14: 3, # 'Ï‚' + 7: 3, # 'σ' + 2: 3, # 'Ï„' + 12: 0, # 'Ï…' + 28: 3, # 'φ' + 23: 3, # 'χ' + 42: 3, # 'ψ' + 24: 2, # 'ω' + 19: 0, # 'ÏŒ' + 26: 0, # 'Ï' + 27: 0, # 'ÏŽ' + }, + 22: { # 'ή' + 60: 0, # 'e' + 55: 0, # 'o' + 58: 0, # 't' + 36: 1, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'ÎŒ' + 31: 0, # 'Α' + 51: 0, # 'Î’' + 43: 0, # 'Γ' + 41: 0, # 'Δ' + 34: 0, # 'Ε' + 40: 0, # 'Η' + 52: 0, # 'Θ' + 47: 0, # 'Ι' + 44: 0, # 'Κ' + 53: 0, # 'Λ' + 38: 0, # 'Îœ' + 49: 0, # 'Î' + 59: 0, # 'Ξ' + 39: 0, # 'Ο' + 35: 0, # 'Π' + 48: 0, # 'Ρ' + 37: 0, # 'Σ' + 33: 0, # 'Τ' + 45: 0, # 'Î¥' + 56: 0, # 'Φ' + 50: 0, # 'Χ' + 57: 0, # 'Ω' + 17: 0, # 'ά' + 18: 0, # 'έ' + 22: 0, # 'ή' + 15: 0, # 'ί' + 1: 0, # 'α' + 29: 0, # 'β' + 20: 3, # 'γ' + 21: 3, # 'δ' + 3: 0, # 'ε' + 32: 0, # 'ζ' + 13: 0, # 'η' + 25: 3, # 'θ' + 5: 0, # 'ι' + 11: 3, # 'κ' + 16: 2, # 'λ' + 10: 3, # 'μ' + 6: 3, # 'ν' + 30: 2, # 'ξ' + 4: 0, # 'ο' + 9: 3, # 'Ï€' + 8: 3, # 'Ï' + 14: 3, # 'Ï‚' + 7: 3, # 'σ' + 2: 3, # 'Ï„' + 12: 0, # 'Ï…' + 28: 2, # 'φ' + 23: 3, # 'χ' + 42: 2, # 'ψ' + 24: 0, # 'ω' + 19: 0, # 'ÏŒ' + 26: 0, # 'Ï' + 27: 0, # 'ÏŽ' + }, + 15: { # 'ί' + 60: 0, # 'e' + 55: 0, # 'o' + 58: 0, # 't' + 36: 0, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'ÎŒ' + 31: 0, # 'Α' + 51: 0, # 'Î’' + 43: 0, # 'Γ' + 41: 0, # 'Δ' + 34: 0, # 'Ε' + 40: 0, # 'Η' + 52: 0, # 'Θ' + 47: 0, # 'Ι' + 44: 0, # 'Κ' + 53: 0, # 'Λ' + 38: 0, # 'Îœ' + 49: 0, # 'Î' + 59: 0, # 'Ξ' + 39: 0, # 'Ο' + 35: 0, # 'Π' + 48: 0, # 'Ρ' + 37: 0, # 'Σ' + 33: 0, # 'Τ' + 45: 0, # 'Î¥' + 56: 0, # 'Φ' + 50: 0, # 'Χ' + 57: 0, # 'Ω' + 17: 0, # 'ά' + 18: 0, # 'έ' + 22: 0, # 'ή' + 15: 0, # 'ί' + 1: 3, # 'α' + 29: 2, # 'β' + 20: 3, # 'γ' + 21: 3, # 'δ' + 3: 3, # 'ε' + 32: 3, # 'ζ' + 13: 3, # 'η' + 25: 3, # 'θ' + 5: 0, # 'ι' + 11: 3, # 'κ' + 16: 3, # 'λ' + 10: 3, # 'μ' + 6: 3, # 'ν' + 30: 3, # 'ξ' + 4: 3, # 'ο' + 9: 3, # 'Ï€' + 8: 3, # 'Ï' + 14: 3, # 'Ï‚' + 7: 3, # 'σ' + 2: 3, # 'Ï„' + 12: 0, # 'Ï…' + 28: 1, # 'φ' + 23: 3, # 'χ' + 42: 2, # 'ψ' + 24: 3, # 'ω' + 19: 0, # 'ÏŒ' + 26: 0, # 'Ï' + 27: 0, # 'ÏŽ' + }, + 1: { # 'α' + 60: 0, # 'e' + 55: 0, # 'o' + 58: 0, # 't' + 36: 2, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'ÎŒ' + 31: 0, # 'Α' + 51: 0, # 'Î’' + 43: 0, # 'Γ' + 41: 0, # 'Δ' + 34: 0, # 'Ε' + 40: 0, # 'Η' + 52: 0, # 'Θ' + 47: 0, # 'Ι' + 44: 0, # 'Κ' + 53: 0, # 'Λ' + 38: 0, # 'Îœ' + 49: 0, # 'Î' + 59: 0, # 'Ξ' + 39: 0, # 'Ο' + 35: 0, # 'Π' + 48: 0, # 'Ρ' + 37: 0, # 'Σ' + 33: 0, # 'Τ' + 45: 0, # 'Î¥' + 56: 0, # 'Φ' + 50: 0, # 'Χ' + 57: 0, # 'Ω' + 17: 0, # 'ά' + 18: 2, # 'έ' + 22: 0, # 'ή' + 15: 3, # 'ί' + 1: 0, # 'α' + 29: 3, # 'β' + 20: 3, # 'γ' + 21: 3, # 'δ' + 3: 2, # 'ε' + 32: 3, # 'ζ' + 13: 1, # 'η' + 25: 3, # 'θ' + 5: 3, # 'ι' + 11: 3, # 'κ' + 16: 3, # 'λ' + 10: 3, # 'μ' + 6: 3, # 'ν' + 30: 3, # 'ξ' + 4: 2, # 'ο' + 9: 3, # 'Ï€' + 8: 3, # 'Ï' + 14: 3, # 'Ï‚' + 7: 3, # 'σ' + 2: 3, # 'Ï„' + 12: 3, # 'Ï…' + 28: 3, # 'φ' + 23: 3, # 'χ' + 42: 2, # 'ψ' + 24: 0, # 'ω' + 19: 2, # 'ÏŒ' + 26: 2, # 'Ï' + 27: 0, # 'ÏŽ' + }, + 29: { # 'β' + 60: 0, # 'e' + 55: 0, # 'o' + 58: 0, # 't' + 36: 0, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'ÎŒ' + 31: 0, # 'Α' + 51: 0, # 'Î’' + 43: 0, # 'Γ' + 41: 0, # 'Δ' + 34: 0, # 'Ε' + 40: 0, # 'Η' + 52: 0, # 'Θ' + 47: 0, # 'Ι' + 44: 0, # 'Κ' + 53: 0, # 'Λ' + 38: 0, # 'Îœ' + 49: 0, # 'Î' + 59: 0, # 'Ξ' + 39: 0, # 'Ο' + 35: 0, # 'Π' + 48: 0, # 'Ρ' + 37: 0, # 'Σ' + 33: 0, # 'Τ' + 45: 0, # 'Î¥' + 56: 0, # 'Φ' + 50: 0, # 'Χ' + 57: 0, # 'Ω' + 17: 3, # 'ά' + 18: 2, # 'έ' + 22: 3, # 'ή' + 15: 2, # 'ί' + 1: 3, # 'α' + 29: 0, # 'β' + 20: 2, # 'γ' + 21: 2, # 'δ' + 3: 3, # 'ε' + 32: 0, # 'ζ' + 13: 2, # 'η' + 25: 0, # 'θ' + 5: 3, # 'ι' + 11: 0, # 'κ' + 16: 3, # 'λ' + 10: 0, # 'μ' + 6: 0, # 'ν' + 30: 0, # 'ξ' + 4: 3, # 'ο' + 9: 0, # 'Ï€' + 8: 3, # 'Ï' + 14: 0, # 'Ï‚' + 7: 0, # 'σ' + 2: 0, # 'Ï„' + 12: 0, # 'Ï…' + 28: 0, # 'φ' + 23: 0, # 'χ' + 42: 0, # 'ψ' + 24: 2, # 'ω' + 19: 2, # 'ÏŒ' + 26: 2, # 'Ï' + 27: 2, # 'ÏŽ' + }, + 20: { # 'γ' + 60: 0, # 'e' + 55: 0, # 'o' + 58: 0, # 't' + 36: 0, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'ÎŒ' + 31: 0, # 'Α' + 51: 0, # 'Î’' + 43: 0, # 'Γ' + 41: 0, # 'Δ' + 34: 0, # 'Ε' + 40: 0, # 'Η' + 52: 0, # 'Θ' + 47: 0, # 'Ι' + 44: 0, # 'Κ' + 53: 0, # 'Λ' + 38: 0, # 'Îœ' + 49: 0, # 'Î' + 59: 0, # 'Ξ' + 39: 0, # 'Ο' + 35: 0, # 'Π' + 48: 0, # 'Ρ' + 37: 0, # 'Σ' + 33: 0, # 'Τ' + 45: 0, # 'Î¥' + 56: 0, # 'Φ' + 50: 0, # 'Χ' + 57: 0, # 'Ω' + 17: 3, # 'ά' + 18: 3, # 'έ' + 22: 3, # 'ή' + 15: 3, # 'ί' + 1: 3, # 'α' + 29: 0, # 'β' + 20: 3, # 'γ' + 21: 0, # 'δ' + 3: 3, # 'ε' + 32: 0, # 'ζ' + 13: 3, # 'η' + 25: 0, # 'θ' + 5: 3, # 'ι' + 11: 3, # 'κ' + 16: 3, # 'λ' + 10: 3, # 'μ' + 6: 3, # 'ν' + 30: 3, # 'ξ' + 4: 3, # 'ο' + 9: 0, # 'Ï€' + 8: 3, # 'Ï' + 14: 0, # 'Ï‚' + 7: 0, # 'σ' + 2: 0, # 'Ï„' + 12: 2, # 'Ï…' + 28: 0, # 'φ' + 23: 3, # 'χ' + 42: 0, # 'ψ' + 24: 3, # 'ω' + 19: 3, # 'ÏŒ' + 26: 2, # 'Ï' + 27: 3, # 'ÏŽ' + }, + 21: { # 'δ' + 60: 0, # 'e' + 55: 0, # 'o' + 58: 0, # 't' + 36: 0, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'ÎŒ' + 31: 0, # 'Α' + 51: 0, # 'Î’' + 43: 0, # 'Γ' + 41: 0, # 'Δ' + 34: 0, # 'Ε' + 40: 0, # 'Η' + 52: 0, # 'Θ' + 47: 0, # 'Ι' + 44: 0, # 'Κ' + 53: 0, # 'Λ' + 38: 0, # 'Îœ' + 49: 0, # 'Î' + 59: 0, # 'Ξ' + 39: 0, # 'Ο' + 35: 0, # 'Π' + 48: 0, # 'Ρ' + 37: 0, # 'Σ' + 33: 0, # 'Τ' + 45: 0, # 'Î¥' + 56: 0, # 'Φ' + 50: 0, # 'Χ' + 57: 0, # 'Ω' + 17: 2, # 'ά' + 18: 3, # 'έ' + 22: 3, # 'ή' + 15: 3, # 'ί' + 1: 3, # 'α' + 29: 0, # 'β' + 20: 0, # 'γ' + 21: 0, # 'δ' + 3: 3, # 'ε' + 32: 0, # 'ζ' + 13: 3, # 'η' + 25: 0, # 'θ' + 5: 3, # 'ι' + 11: 0, # 'κ' + 16: 0, # 'λ' + 10: 0, # 'μ' + 6: 0, # 'ν' + 30: 0, # 'ξ' + 4: 3, # 'ο' + 9: 0, # 'Ï€' + 8: 3, # 'Ï' + 14: 0, # 'Ï‚' + 7: 0, # 'σ' + 2: 0, # 'Ï„' + 12: 3, # 'Ï…' + 28: 0, # 'φ' + 23: 0, # 'χ' + 42: 0, # 'ψ' + 24: 3, # 'ω' + 19: 3, # 'ÏŒ' + 26: 3, # 'Ï' + 27: 3, # 'ÏŽ' + }, + 3: { # 'ε' + 60: 0, # 'e' + 55: 0, # 'o' + 58: 0, # 't' + 36: 2, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'ÎŒ' + 31: 0, # 'Α' + 51: 0, # 'Î’' + 43: 0, # 'Γ' + 41: 0, # 'Δ' + 34: 0, # 'Ε' + 40: 0, # 'Η' + 52: 0, # 'Θ' + 47: 0, # 'Ι' + 44: 0, # 'Κ' + 53: 0, # 'Λ' + 38: 0, # 'Îœ' + 49: 0, # 'Î' + 59: 0, # 'Ξ' + 39: 0, # 'Ο' + 35: 0, # 'Π' + 48: 0, # 'Ρ' + 37: 0, # 'Σ' + 33: 0, # 'Τ' + 45: 0, # 'Î¥' + 56: 0, # 'Φ' + 50: 0, # 'Χ' + 57: 0, # 'Ω' + 17: 3, # 'ά' + 18: 0, # 'έ' + 22: 0, # 'ή' + 15: 3, # 'ί' + 1: 2, # 'α' + 29: 3, # 'β' + 20: 3, # 'γ' + 21: 3, # 'δ' + 3: 2, # 'ε' + 32: 2, # 'ζ' + 13: 0, # 'η' + 25: 3, # 'θ' + 5: 3, # 'ι' + 11: 3, # 'κ' + 16: 3, # 'λ' + 10: 3, # 'μ' + 6: 3, # 'ν' + 30: 3, # 'ξ' + 4: 2, # 'ο' + 9: 3, # 'Ï€' + 8: 3, # 'Ï' + 14: 3, # 'Ï‚' + 7: 3, # 'σ' + 2: 3, # 'Ï„' + 12: 3, # 'Ï…' + 28: 3, # 'φ' + 23: 3, # 'χ' + 42: 2, # 'ψ' + 24: 3, # 'ω' + 19: 2, # 'ÏŒ' + 26: 3, # 'Ï' + 27: 2, # 'ÏŽ' + }, + 32: { # 'ζ' + 60: 0, # 'e' + 55: 0, # 'o' + 58: 0, # 't' + 36: 0, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'ÎŒ' + 31: 0, # 'Α' + 51: 0, # 'Î’' + 43: 0, # 'Γ' + 41: 0, # 'Δ' + 34: 0, # 'Ε' + 40: 0, # 'Η' + 52: 0, # 'Θ' + 47: 0, # 'Ι' + 44: 0, # 'Κ' + 53: 0, # 'Λ' + 38: 0, # 'Îœ' + 49: 0, # 'Î' + 59: 0, # 'Ξ' + 39: 0, # 'Ο' + 35: 0, # 'Π' + 48: 0, # 'Ρ' + 37: 0, # 'Σ' + 33: 0, # 'Τ' + 45: 0, # 'Î¥' + 56: 0, # 'Φ' + 50: 0, # 'Χ' + 57: 0, # 'Ω' + 17: 2, # 'ά' + 18: 2, # 'έ' + 22: 2, # 'ή' + 15: 2, # 'ί' + 1: 2, # 'α' + 29: 0, # 'β' + 20: 0, # 'γ' + 21: 0, # 'δ' + 3: 3, # 'ε' + 32: 0, # 'ζ' + 13: 3, # 'η' + 25: 0, # 'θ' + 5: 2, # 'ι' + 11: 0, # 'κ' + 16: 0, # 'λ' + 10: 0, # 'μ' + 6: 0, # 'ν' + 30: 0, # 'ξ' + 4: 3, # 'ο' + 9: 0, # 'Ï€' + 8: 0, # 'Ï' + 14: 0, # 'Ï‚' + 7: 0, # 'σ' + 2: 0, # 'Ï„' + 12: 1, # 'Ï…' + 28: 0, # 'φ' + 23: 0, # 'χ' + 42: 0, # 'ψ' + 24: 3, # 'ω' + 19: 2, # 'ÏŒ' + 26: 0, # 'Ï' + 27: 2, # 'ÏŽ' + }, + 13: { # 'η' + 60: 0, # 'e' + 55: 0, # 'o' + 58: 0, # 't' + 36: 2, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'ÎŒ' + 31: 0, # 'Α' + 51: 0, # 'Î’' + 43: 0, # 'Γ' + 41: 0, # 'Δ' + 34: 0, # 'Ε' + 40: 0, # 'Η' + 52: 0, # 'Θ' + 47: 0, # 'Ι' + 44: 0, # 'Κ' + 53: 0, # 'Λ' + 38: 0, # 'Îœ' + 49: 0, # 'Î' + 59: 0, # 'Ξ' + 39: 0, # 'Ο' + 35: 0, # 'Π' + 48: 0, # 'Ρ' + 37: 0, # 'Σ' + 33: 0, # 'Τ' + 45: 0, # 'Î¥' + 56: 0, # 'Φ' + 50: 0, # 'Χ' + 57: 0, # 'Ω' + 17: 0, # 'ά' + 18: 0, # 'έ' + 22: 0, # 'ή' + 15: 0, # 'ί' + 1: 0, # 'α' + 29: 0, # 'β' + 20: 3, # 'γ' + 21: 2, # 'δ' + 3: 0, # 'ε' + 32: 0, # 'ζ' + 13: 0, # 'η' + 25: 3, # 'θ' + 5: 0, # 'ι' + 11: 3, # 'κ' + 16: 3, # 'λ' + 10: 3, # 'μ' + 6: 3, # 'ν' + 30: 2, # 'ξ' + 4: 0, # 'ο' + 9: 2, # 'Ï€' + 8: 3, # 'Ï' + 14: 3, # 'Ï‚' + 7: 3, # 'σ' + 2: 3, # 'Ï„' + 12: 0, # 'Ï…' + 28: 2, # 'φ' + 23: 3, # 'χ' + 42: 2, # 'ψ' + 24: 0, # 'ω' + 19: 0, # 'ÏŒ' + 26: 0, # 'Ï' + 27: 0, # 'ÏŽ' + }, + 25: { # 'θ' + 60: 0, # 'e' + 55: 0, # 'o' + 58: 0, # 't' + 36: 0, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'ÎŒ' + 31: 0, # 'Α' + 51: 0, # 'Î’' + 43: 0, # 'Γ' + 41: 0, # 'Δ' + 34: 0, # 'Ε' + 40: 0, # 'Η' + 52: 0, # 'Θ' + 47: 0, # 'Ι' + 44: 0, # 'Κ' + 53: 0, # 'Λ' + 38: 0, # 'Îœ' + 49: 0, # 'Î' + 59: 0, # 'Ξ' + 39: 0, # 'Ο' + 35: 0, # 'Π' + 48: 0, # 'Ρ' + 37: 0, # 'Σ' + 33: 0, # 'Τ' + 45: 0, # 'Î¥' + 56: 0, # 'Φ' + 50: 0, # 'Χ' + 57: 0, # 'Ω' + 17: 2, # 'ά' + 18: 3, # 'έ' + 22: 3, # 'ή' + 15: 2, # 'ί' + 1: 3, # 'α' + 29: 0, # 'β' + 20: 0, # 'γ' + 21: 0, # 'δ' + 3: 3, # 'ε' + 32: 0, # 'ζ' + 13: 3, # 'η' + 25: 0, # 'θ' + 5: 3, # 'ι' + 11: 0, # 'κ' + 16: 1, # 'λ' + 10: 3, # 'μ' + 6: 2, # 'ν' + 30: 0, # 'ξ' + 4: 3, # 'ο' + 9: 0, # 'Ï€' + 8: 3, # 'Ï' + 14: 0, # 'Ï‚' + 7: 0, # 'σ' + 2: 0, # 'Ï„' + 12: 3, # 'Ï…' + 28: 0, # 'φ' + 23: 0, # 'χ' + 42: 0, # 'ψ' + 24: 3, # 'ω' + 19: 3, # 'ÏŒ' + 26: 3, # 'Ï' + 27: 3, # 'ÏŽ' + }, + 5: { # 'ι' + 60: 0, # 'e' + 55: 1, # 'o' + 58: 0, # 't' + 36: 2, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'ÎŒ' + 31: 0, # 'Α' + 51: 0, # 'Î’' + 43: 0, # 'Γ' + 41: 0, # 'Δ' + 34: 1, # 'Ε' + 40: 0, # 'Η' + 52: 0, # 'Θ' + 47: 0, # 'Ι' + 44: 0, # 'Κ' + 53: 0, # 'Λ' + 38: 0, # 'Îœ' + 49: 0, # 'Î' + 59: 0, # 'Ξ' + 39: 0, # 'Ο' + 35: 0, # 'Π' + 48: 0, # 'Ρ' + 37: 0, # 'Σ' + 33: 0, # 'Τ' + 45: 0, # 'Î¥' + 56: 0, # 'Φ' + 50: 0, # 'Χ' + 57: 0, # 'Ω' + 17: 3, # 'ά' + 18: 3, # 'έ' + 22: 3, # 'ή' + 15: 0, # 'ί' + 1: 3, # 'α' + 29: 3, # 'β' + 20: 3, # 'γ' + 21: 3, # 'δ' + 3: 3, # 'ε' + 32: 2, # 'ζ' + 13: 3, # 'η' + 25: 3, # 'θ' + 5: 0, # 'ι' + 11: 3, # 'κ' + 16: 3, # 'λ' + 10: 3, # 'μ' + 6: 3, # 'ν' + 30: 3, # 'ξ' + 4: 3, # 'ο' + 9: 3, # 'Ï€' + 8: 3, # 'Ï' + 14: 3, # 'Ï‚' + 7: 3, # 'σ' + 2: 3, # 'Ï„' + 12: 0, # 'Ï…' + 28: 2, # 'φ' + 23: 3, # 'χ' + 42: 2, # 'ψ' + 24: 3, # 'ω' + 19: 3, # 'ÏŒ' + 26: 0, # 'Ï' + 27: 3, # 'ÏŽ' + }, + 11: { # 'κ' + 60: 0, # 'e' + 55: 0, # 'o' + 58: 0, # 't' + 36: 0, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'ÎŒ' + 31: 0, # 'Α' + 51: 0, # 'Î’' + 43: 0, # 'Γ' + 41: 0, # 'Δ' + 34: 0, # 'Ε' + 40: 0, # 'Η' + 52: 0, # 'Θ' + 47: 0, # 'Ι' + 44: 0, # 'Κ' + 53: 0, # 'Λ' + 38: 0, # 'Îœ' + 49: 0, # 'Î' + 59: 0, # 'Ξ' + 39: 0, # 'Ο' + 35: 0, # 'Π' + 48: 0, # 'Ρ' + 37: 0, # 'Σ' + 33: 0, # 'Τ' + 45: 0, # 'Î¥' + 56: 0, # 'Φ' + 50: 0, # 'Χ' + 57: 0, # 'Ω' + 17: 3, # 'ά' + 18: 3, # 'έ' + 22: 3, # 'ή' + 15: 3, # 'ί' + 1: 3, # 'α' + 29: 0, # 'β' + 20: 0, # 'γ' + 21: 3, # 'δ' + 3: 3, # 'ε' + 32: 0, # 'ζ' + 13: 3, # 'η' + 25: 2, # 'θ' + 5: 3, # 'ι' + 11: 3, # 'κ' + 16: 3, # 'λ' + 10: 3, # 'μ' + 6: 2, # 'ν' + 30: 0, # 'ξ' + 4: 3, # 'ο' + 9: 2, # 'Ï€' + 8: 3, # 'Ï' + 14: 0, # 'Ï‚' + 7: 0, # 'σ' + 2: 3, # 'Ï„' + 12: 3, # 'Ï…' + 28: 2, # 'φ' + 23: 2, # 'χ' + 42: 0, # 'ψ' + 24: 3, # 'ω' + 19: 3, # 'ÏŒ' + 26: 3, # 'Ï' + 27: 3, # 'ÏŽ' + }, + 16: { # 'λ' + 60: 0, # 'e' + 55: 0, # 'o' + 58: 0, # 't' + 36: 0, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'ÎŒ' + 31: 0, # 'Α' + 51: 0, # 'Î’' + 43: 0, # 'Γ' + 41: 0, # 'Δ' + 34: 0, # 'Ε' + 40: 0, # 'Η' + 52: 0, # 'Θ' + 47: 0, # 'Ι' + 44: 0, # 'Κ' + 53: 0, # 'Λ' + 38: 0, # 'Îœ' + 49: 0, # 'Î' + 59: 0, # 'Ξ' + 39: 0, # 'Ο' + 35: 0, # 'Π' + 48: 0, # 'Ρ' + 37: 0, # 'Σ' + 33: 0, # 'Τ' + 45: 0, # 'Î¥' + 56: 0, # 'Φ' + 50: 0, # 'Χ' + 57: 0, # 'Ω' + 17: 3, # 'ά' + 18: 3, # 'έ' + 22: 3, # 'ή' + 15: 3, # 'ί' + 1: 3, # 'α' + 29: 1, # 'β' + 20: 2, # 'γ' + 21: 1, # 'δ' + 3: 3, # 'ε' + 32: 0, # 'ζ' + 13: 3, # 'η' + 25: 2, # 'θ' + 5: 3, # 'ι' + 11: 2, # 'κ' + 16: 3, # 'λ' + 10: 2, # 'μ' + 6: 2, # 'ν' + 30: 0, # 'ξ' + 4: 3, # 'ο' + 9: 3, # 'Ï€' + 8: 0, # 'Ï' + 14: 0, # 'Ï‚' + 7: 0, # 'σ' + 2: 3, # 'Ï„' + 12: 3, # 'Ï…' + 28: 2, # 'φ' + 23: 0, # 'χ' + 42: 0, # 'ψ' + 24: 3, # 'ω' + 19: 3, # 'ÏŒ' + 26: 3, # 'Ï' + 27: 3, # 'ÏŽ' + }, + 10: { # 'μ' + 60: 0, # 'e' + 55: 0, # 'o' + 58: 0, # 't' + 36: 0, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'ÎŒ' + 31: 0, # 'Α' + 51: 0, # 'Î’' + 43: 0, # 'Γ' + 41: 0, # 'Δ' + 34: 1, # 'Ε' + 40: 0, # 'Η' + 52: 0, # 'Θ' + 47: 0, # 'Ι' + 44: 0, # 'Κ' + 53: 0, # 'Λ' + 38: 0, # 'Îœ' + 49: 0, # 'Î' + 59: 0, # 'Ξ' + 39: 0, # 'Ο' + 35: 0, # 'Π' + 48: 0, # 'Ρ' + 37: 0, # 'Σ' + 33: 0, # 'Τ' + 45: 0, # 'Î¥' + 56: 0, # 'Φ' + 50: 0, # 'Χ' + 57: 0, # 'Ω' + 17: 3, # 'ά' + 18: 3, # 'έ' + 22: 3, # 'ή' + 15: 3, # 'ί' + 1: 3, # 'α' + 29: 3, # 'β' + 20: 0, # 'γ' + 21: 0, # 'δ' + 3: 3, # 'ε' + 32: 0, # 'ζ' + 13: 3, # 'η' + 25: 0, # 'θ' + 5: 3, # 'ι' + 11: 0, # 'κ' + 16: 0, # 'λ' + 10: 3, # 'μ' + 6: 3, # 'ν' + 30: 0, # 'ξ' + 4: 3, # 'ο' + 9: 3, # 'Ï€' + 8: 0, # 'Ï' + 14: 0, # 'Ï‚' + 7: 0, # 'σ' + 2: 0, # 'Ï„' + 12: 2, # 'Ï…' + 28: 3, # 'φ' + 23: 0, # 'χ' + 42: 2, # 'ψ' + 24: 3, # 'ω' + 19: 3, # 'ÏŒ' + 26: 2, # 'Ï' + 27: 2, # 'ÏŽ' + }, + 6: { # 'ν' + 60: 0, # 'e' + 55: 0, # 'o' + 58: 0, # 't' + 36: 2, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'ÎŒ' + 31: 0, # 'Α' + 51: 0, # 'Î’' + 43: 0, # 'Γ' + 41: 0, # 'Δ' + 34: 0, # 'Ε' + 40: 0, # 'Η' + 52: 0, # 'Θ' + 47: 0, # 'Ι' + 44: 0, # 'Κ' + 53: 0, # 'Λ' + 38: 0, # 'Îœ' + 49: 0, # 'Î' + 59: 0, # 'Ξ' + 39: 0, # 'Ο' + 35: 0, # 'Π' + 48: 0, # 'Ρ' + 37: 0, # 'Σ' + 33: 0, # 'Τ' + 45: 0, # 'Î¥' + 56: 0, # 'Φ' + 50: 0, # 'Χ' + 57: 0, # 'Ω' + 17: 3, # 'ά' + 18: 3, # 'έ' + 22: 3, # 'ή' + 15: 3, # 'ί' + 1: 3, # 'α' + 29: 0, # 'β' + 20: 0, # 'γ' + 21: 3, # 'δ' + 3: 3, # 'ε' + 32: 2, # 'ζ' + 13: 3, # 'η' + 25: 3, # 'θ' + 5: 3, # 'ι' + 11: 0, # 'κ' + 16: 1, # 'λ' + 10: 0, # 'μ' + 6: 2, # 'ν' + 30: 0, # 'ξ' + 4: 3, # 'ο' + 9: 0, # 'Ï€' + 8: 0, # 'Ï' + 14: 0, # 'Ï‚' + 7: 3, # 'σ' + 2: 3, # 'Ï„' + 12: 3, # 'Ï…' + 28: 0, # 'φ' + 23: 0, # 'χ' + 42: 0, # 'ψ' + 24: 3, # 'ω' + 19: 3, # 'ÏŒ' + 26: 3, # 'Ï' + 27: 3, # 'ÏŽ' + }, + 30: { # 'ξ' + 60: 0, # 'e' + 55: 0, # 'o' + 58: 0, # 't' + 36: 0, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'ÎŒ' + 31: 0, # 'Α' + 51: 0, # 'Î’' + 43: 0, # 'Γ' + 41: 0, # 'Δ' + 34: 0, # 'Ε' + 40: 0, # 'Η' + 52: 0, # 'Θ' + 47: 0, # 'Ι' + 44: 0, # 'Κ' + 53: 0, # 'Λ' + 38: 0, # 'Îœ' + 49: 0, # 'Î' + 59: 0, # 'Ξ' + 39: 0, # 'Ο' + 35: 0, # 'Π' + 48: 0, # 'Ρ' + 37: 0, # 'Σ' + 33: 0, # 'Τ' + 45: 0, # 'Î¥' + 56: 0, # 'Φ' + 50: 0, # 'Χ' + 57: 0, # 'Ω' + 17: 2, # 'ά' + 18: 3, # 'έ' + 22: 3, # 'ή' + 15: 2, # 'ί' + 1: 3, # 'α' + 29: 0, # 'β' + 20: 0, # 'γ' + 21: 0, # 'δ' + 3: 3, # 'ε' + 32: 0, # 'ζ' + 13: 3, # 'η' + 25: 0, # 'θ' + 5: 2, # 'ι' + 11: 0, # 'κ' + 16: 0, # 'λ' + 10: 0, # 'μ' + 6: 0, # 'ν' + 30: 0, # 'ξ' + 4: 3, # 'ο' + 9: 0, # 'Ï€' + 8: 0, # 'Ï' + 14: 0, # 'Ï‚' + 7: 0, # 'σ' + 2: 3, # 'Ï„' + 12: 2, # 'Ï…' + 28: 0, # 'φ' + 23: 0, # 'χ' + 42: 0, # 'ψ' + 24: 3, # 'ω' + 19: 2, # 'ÏŒ' + 26: 3, # 'Ï' + 27: 1, # 'ÏŽ' + }, + 4: { # 'ο' + 60: 0, # 'e' + 55: 0, # 'o' + 58: 0, # 't' + 36: 2, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'ÎŒ' + 31: 0, # 'Α' + 51: 0, # 'Î’' + 43: 0, # 'Γ' + 41: 0, # 'Δ' + 34: 0, # 'Ε' + 40: 0, # 'Η' + 52: 0, # 'Θ' + 47: 0, # 'Ι' + 44: 0, # 'Κ' + 53: 0, # 'Λ' + 38: 0, # 'Îœ' + 49: 0, # 'Î' + 59: 0, # 'Ξ' + 39: 0, # 'Ο' + 35: 0, # 'Π' + 48: 0, # 'Ρ' + 37: 0, # 'Σ' + 33: 0, # 'Τ' + 45: 0, # 'Î¥' + 56: 0, # 'Φ' + 50: 0, # 'Χ' + 57: 0, # 'Ω' + 17: 0, # 'ά' + 18: 2, # 'έ' + 22: 3, # 'ή' + 15: 3, # 'ί' + 1: 2, # 'α' + 29: 3, # 'β' + 20: 3, # 'γ' + 21: 3, # 'δ' + 3: 3, # 'ε' + 32: 0, # 'ζ' + 13: 3, # 'η' + 25: 3, # 'θ' + 5: 3, # 'ι' + 11: 3, # 'κ' + 16: 3, # 'λ' + 10: 3, # 'μ' + 6: 3, # 'ν' + 30: 2, # 'ξ' + 4: 2, # 'ο' + 9: 3, # 'Ï€' + 8: 3, # 'Ï' + 14: 3, # 'Ï‚' + 7: 3, # 'σ' + 2: 3, # 'Ï„' + 12: 3, # 'Ï…' + 28: 3, # 'φ' + 23: 3, # 'χ' + 42: 2, # 'ψ' + 24: 2, # 'ω' + 19: 1, # 'ÏŒ' + 26: 3, # 'Ï' + 27: 2, # 'ÏŽ' + }, + 9: { # 'Ï€' + 60: 0, # 'e' + 55: 0, # 'o' + 58: 0, # 't' + 36: 0, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'ÎŒ' + 31: 0, # 'Α' + 51: 0, # 'Î’' + 43: 0, # 'Γ' + 41: 0, # 'Δ' + 34: 0, # 'Ε' + 40: 0, # 'Η' + 52: 0, # 'Θ' + 47: 0, # 'Ι' + 44: 0, # 'Κ' + 53: 0, # 'Λ' + 38: 0, # 'Îœ' + 49: 0, # 'Î' + 59: 0, # 'Ξ' + 39: 0, # 'Ο' + 35: 0, # 'Π' + 48: 0, # 'Ρ' + 37: 0, # 'Σ' + 33: 0, # 'Τ' + 45: 0, # 'Î¥' + 56: 0, # 'Φ' + 50: 0, # 'Χ' + 57: 0, # 'Ω' + 17: 3, # 'ά' + 18: 3, # 'έ' + 22: 3, # 'ή' + 15: 3, # 'ί' + 1: 3, # 'α' + 29: 0, # 'β' + 20: 0, # 'γ' + 21: 0, # 'δ' + 3: 3, # 'ε' + 32: 0, # 'ζ' + 13: 3, # 'η' + 25: 0, # 'θ' + 5: 3, # 'ι' + 11: 0, # 'κ' + 16: 3, # 'λ' + 10: 0, # 'μ' + 6: 2, # 'ν' + 30: 0, # 'ξ' + 4: 3, # 'ο' + 9: 0, # 'Ï€' + 8: 3, # 'Ï' + 14: 2, # 'Ï‚' + 7: 0, # 'σ' + 2: 3, # 'Ï„' + 12: 3, # 'Ï…' + 28: 0, # 'φ' + 23: 2, # 'χ' + 42: 0, # 'ψ' + 24: 3, # 'ω' + 19: 3, # 'ÏŒ' + 26: 2, # 'Ï' + 27: 3, # 'ÏŽ' + }, + 8: { # 'Ï' + 60: 0, # 'e' + 55: 0, # 'o' + 58: 0, # 't' + 36: 0, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'ÎŒ' + 31: 0, # 'Α' + 51: 0, # 'Î’' + 43: 0, # 'Γ' + 41: 0, # 'Δ' + 34: 0, # 'Ε' + 40: 0, # 'Η' + 52: 0, # 'Θ' + 47: 0, # 'Ι' + 44: 0, # 'Κ' + 53: 0, # 'Λ' + 38: 0, # 'Îœ' + 49: 0, # 'Î' + 59: 0, # 'Ξ' + 39: 0, # 'Ο' + 35: 0, # 'Π' + 48: 0, # 'Ρ' + 37: 0, # 'Σ' + 33: 0, # 'Τ' + 45: 0, # 'Î¥' + 56: 0, # 'Φ' + 50: 0, # 'Χ' + 57: 0, # 'Ω' + 17: 3, # 'ά' + 18: 3, # 'έ' + 22: 3, # 'ή' + 15: 3, # 'ί' + 1: 3, # 'α' + 29: 2, # 'β' + 20: 3, # 'γ' + 21: 2, # 'δ' + 3: 3, # 'ε' + 32: 0, # 'ζ' + 13: 3, # 'η' + 25: 3, # 'θ' + 5: 3, # 'ι' + 11: 3, # 'κ' + 16: 1, # 'λ' + 10: 3, # 'μ' + 6: 3, # 'ν' + 30: 2, # 'ξ' + 4: 3, # 'ο' + 9: 2, # 'Ï€' + 8: 2, # 'Ï' + 14: 0, # 'Ï‚' + 7: 2, # 'σ' + 2: 3, # 'Ï„' + 12: 3, # 'Ï…' + 28: 3, # 'φ' + 23: 3, # 'χ' + 42: 0, # 'ψ' + 24: 3, # 'ω' + 19: 3, # 'ÏŒ' + 26: 3, # 'Ï' + 27: 3, # 'ÏŽ' + }, + 14: { # 'Ï‚' + 60: 0, # 'e' + 55: 0, # 'o' + 58: 0, # 't' + 36: 2, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'ÎŒ' + 31: 0, # 'Α' + 51: 0, # 'Î’' + 43: 0, # 'Γ' + 41: 0, # 'Δ' + 34: 0, # 'Ε' + 40: 0, # 'Η' + 52: 0, # 'Θ' + 47: 0, # 'Ι' + 44: 0, # 'Κ' + 53: 0, # 'Λ' + 38: 0, # 'Îœ' + 49: 0, # 'Î' + 59: 0, # 'Ξ' + 39: 0, # 'Ο' + 35: 0, # 'Π' + 48: 0, # 'Ρ' + 37: 0, # 'Σ' + 33: 0, # 'Τ' + 45: 0, # 'Î¥' + 56: 0, # 'Φ' + 50: 0, # 'Χ' + 57: 0, # 'Ω' + 17: 0, # 'ά' + 18: 0, # 'έ' + 22: 0, # 'ή' + 15: 0, # 'ί' + 1: 0, # 'α' + 29: 0, # 'β' + 20: 0, # 'γ' + 21: 0, # 'δ' + 3: 0, # 'ε' + 32: 0, # 'ζ' + 13: 0, # 'η' + 25: 0, # 'θ' + 5: 0, # 'ι' + 11: 0, # 'κ' + 16: 0, # 'λ' + 10: 0, # 'μ' + 6: 0, # 'ν' + 30: 0, # 'ξ' + 4: 0, # 'ο' + 9: 0, # 'Ï€' + 8: 0, # 'Ï' + 14: 0, # 'Ï‚' + 7: 0, # 'σ' + 2: 0, # 'Ï„' + 12: 0, # 'Ï…' + 28: 0, # 'φ' + 23: 0, # 'χ' + 42: 0, # 'ψ' + 24: 0, # 'ω' + 19: 0, # 'ÏŒ' + 26: 0, # 'Ï' + 27: 0, # 'ÏŽ' + }, + 7: { # 'σ' + 60: 0, # 'e' + 55: 0, # 'o' + 58: 0, # 't' + 36: 0, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'ÎŒ' + 31: 0, # 'Α' + 51: 0, # 'Î’' + 43: 0, # 'Γ' + 41: 0, # 'Δ' + 34: 0, # 'Ε' + 40: 0, # 'Η' + 52: 0, # 'Θ' + 47: 0, # 'Ι' + 44: 0, # 'Κ' + 53: 0, # 'Λ' + 38: 0, # 'Îœ' + 49: 0, # 'Î' + 59: 0, # 'Ξ' + 39: 0, # 'Ο' + 35: 0, # 'Π' + 48: 0, # 'Ρ' + 37: 0, # 'Σ' + 33: 0, # 'Τ' + 45: 0, # 'Î¥' + 56: 0, # 'Φ' + 50: 0, # 'Χ' + 57: 0, # 'Ω' + 17: 2, # 'ά' + 18: 2, # 'έ' + 22: 3, # 'ή' + 15: 3, # 'ί' + 1: 3, # 'α' + 29: 3, # 'β' + 20: 0, # 'γ' + 21: 2, # 'δ' + 3: 3, # 'ε' + 32: 0, # 'ζ' + 13: 3, # 'η' + 25: 3, # 'θ' + 5: 3, # 'ι' + 11: 3, # 'κ' + 16: 2, # 'λ' + 10: 3, # 'μ' + 6: 0, # 'ν' + 30: 0, # 'ξ' + 4: 3, # 'ο' + 9: 3, # 'Ï€' + 8: 0, # 'Ï' + 14: 0, # 'Ï‚' + 7: 3, # 'σ' + 2: 3, # 'Ï„' + 12: 3, # 'Ï…' + 28: 3, # 'φ' + 23: 3, # 'χ' + 42: 0, # 'ψ' + 24: 3, # 'ω' + 19: 3, # 'ÏŒ' + 26: 3, # 'Ï' + 27: 2, # 'ÏŽ' + }, + 2: { # 'Ï„' + 60: 0, # 'e' + 55: 2, # 'o' + 58: 0, # 't' + 36: 0, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'ÎŒ' + 31: 0, # 'Α' + 51: 0, # 'Î’' + 43: 0, # 'Γ' + 41: 0, # 'Δ' + 34: 0, # 'Ε' + 40: 0, # 'Η' + 52: 0, # 'Θ' + 47: 0, # 'Ι' + 44: 0, # 'Κ' + 53: 0, # 'Λ' + 38: 0, # 'Îœ' + 49: 0, # 'Î' + 59: 0, # 'Ξ' + 39: 0, # 'Ο' + 35: 0, # 'Π' + 48: 0, # 'Ρ' + 37: 0, # 'Σ' + 33: 0, # 'Τ' + 45: 0, # 'Î¥' + 56: 0, # 'Φ' + 50: 0, # 'Χ' + 57: 0, # 'Ω' + 17: 3, # 'ά' + 18: 3, # 'έ' + 22: 3, # 'ή' + 15: 3, # 'ί' + 1: 3, # 'α' + 29: 0, # 'β' + 20: 0, # 'γ' + 21: 0, # 'δ' + 3: 3, # 'ε' + 32: 2, # 'ζ' + 13: 3, # 'η' + 25: 0, # 'θ' + 5: 3, # 'ι' + 11: 2, # 'κ' + 16: 2, # 'λ' + 10: 3, # 'μ' + 6: 0, # 'ν' + 30: 0, # 'ξ' + 4: 3, # 'ο' + 9: 0, # 'Ï€' + 8: 3, # 'Ï' + 14: 0, # 'Ï‚' + 7: 3, # 'σ' + 2: 3, # 'Ï„' + 12: 3, # 'Ï…' + 28: 2, # 'φ' + 23: 0, # 'χ' + 42: 0, # 'ψ' + 24: 3, # 'ω' + 19: 3, # 'ÏŒ' + 26: 3, # 'Ï' + 27: 3, # 'ÏŽ' + }, + 12: { # 'Ï…' + 60: 0, # 'e' + 55: 0, # 'o' + 58: 0, # 't' + 36: 0, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'ÎŒ' + 31: 0, # 'Α' + 51: 0, # 'Î’' + 43: 0, # 'Γ' + 41: 0, # 'Δ' + 34: 0, # 'Ε' + 40: 0, # 'Η' + 52: 0, # 'Θ' + 47: 0, # 'Ι' + 44: 0, # 'Κ' + 53: 0, # 'Λ' + 38: 0, # 'Îœ' + 49: 0, # 'Î' + 59: 0, # 'Ξ' + 39: 0, # 'Ο' + 35: 0, # 'Π' + 48: 0, # 'Ρ' + 37: 0, # 'Σ' + 33: 0, # 'Τ' + 45: 0, # 'Î¥' + 56: 0, # 'Φ' + 50: 0, # 'Χ' + 57: 0, # 'Ω' + 17: 2, # 'ά' + 18: 2, # 'έ' + 22: 3, # 'ή' + 15: 2, # 'ί' + 1: 3, # 'α' + 29: 2, # 'β' + 20: 3, # 'γ' + 21: 2, # 'δ' + 3: 2, # 'ε' + 32: 2, # 'ζ' + 13: 2, # 'η' + 25: 3, # 'θ' + 5: 2, # 'ι' + 11: 3, # 'κ' + 16: 3, # 'λ' + 10: 3, # 'μ' + 6: 3, # 'ν' + 30: 3, # 'ξ' + 4: 3, # 'ο' + 9: 3, # 'Ï€' + 8: 3, # 'Ï' + 14: 3, # 'Ï‚' + 7: 3, # 'σ' + 2: 3, # 'Ï„' + 12: 0, # 'Ï…' + 28: 2, # 'φ' + 23: 3, # 'χ' + 42: 2, # 'ψ' + 24: 2, # 'ω' + 19: 2, # 'ÏŒ' + 26: 0, # 'Ï' + 27: 2, # 'ÏŽ' + }, + 28: { # 'φ' + 60: 0, # 'e' + 55: 0, # 'o' + 58: 0, # 't' + 36: 0, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'ÎŒ' + 31: 0, # 'Α' + 51: 0, # 'Î’' + 43: 0, # 'Γ' + 41: 0, # 'Δ' + 34: 0, # 'Ε' + 40: 0, # 'Η' + 52: 0, # 'Θ' + 47: 0, # 'Ι' + 44: 0, # 'Κ' + 53: 0, # 'Λ' + 38: 0, # 'Îœ' + 49: 0, # 'Î' + 59: 0, # 'Ξ' + 39: 0, # 'Ο' + 35: 0, # 'Π' + 48: 0, # 'Ρ' + 37: 0, # 'Σ' + 33: 0, # 'Τ' + 45: 0, # 'Î¥' + 56: 0, # 'Φ' + 50: 0, # 'Χ' + 57: 0, # 'Ω' + 17: 3, # 'ά' + 18: 3, # 'έ' + 22: 3, # 'ή' + 15: 3, # 'ί' + 1: 3, # 'α' + 29: 0, # 'β' + 20: 0, # 'γ' + 21: 0, # 'δ' + 3: 3, # 'ε' + 32: 0, # 'ζ' + 13: 2, # 'η' + 25: 2, # 'θ' + 5: 3, # 'ι' + 11: 0, # 'κ' + 16: 2, # 'λ' + 10: 0, # 'μ' + 6: 1, # 'ν' + 30: 0, # 'ξ' + 4: 3, # 'ο' + 9: 0, # 'Ï€' + 8: 3, # 'Ï' + 14: 0, # 'Ï‚' + 7: 0, # 'σ' + 2: 3, # 'Ï„' + 12: 3, # 'Ï…' + 28: 1, # 'φ' + 23: 0, # 'χ' + 42: 0, # 'ψ' + 24: 3, # 'ω' + 19: 3, # 'ÏŒ' + 26: 2, # 'Ï' + 27: 2, # 'ÏŽ' + }, + 23: { # 'χ' + 60: 0, # 'e' + 55: 0, # 'o' + 58: 0, # 't' + 36: 0, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'ÎŒ' + 31: 0, # 'Α' + 51: 0, # 'Î’' + 43: 0, # 'Γ' + 41: 0, # 'Δ' + 34: 0, # 'Ε' + 40: 0, # 'Η' + 52: 0, # 'Θ' + 47: 0, # 'Ι' + 44: 0, # 'Κ' + 53: 0, # 'Λ' + 38: 0, # 'Îœ' + 49: 0, # 'Î' + 59: 0, # 'Ξ' + 39: 0, # 'Ο' + 35: 0, # 'Π' + 48: 0, # 'Ρ' + 37: 0, # 'Σ' + 33: 0, # 'Τ' + 45: 0, # 'Î¥' + 56: 0, # 'Φ' + 50: 0, # 'Χ' + 57: 0, # 'Ω' + 17: 3, # 'ά' + 18: 2, # 'έ' + 22: 3, # 'ή' + 15: 3, # 'ί' + 1: 3, # 'α' + 29: 0, # 'β' + 20: 0, # 'γ' + 21: 0, # 'δ' + 3: 3, # 'ε' + 32: 0, # 'ζ' + 13: 2, # 'η' + 25: 2, # 'θ' + 5: 3, # 'ι' + 11: 0, # 'κ' + 16: 2, # 'λ' + 10: 2, # 'μ' + 6: 3, # 'ν' + 30: 0, # 'ξ' + 4: 3, # 'ο' + 9: 0, # 'Ï€' + 8: 3, # 'Ï' + 14: 0, # 'Ï‚' + 7: 0, # 'σ' + 2: 3, # 'Ï„' + 12: 3, # 'Ï…' + 28: 0, # 'φ' + 23: 2, # 'χ' + 42: 0, # 'ψ' + 24: 3, # 'ω' + 19: 3, # 'ÏŒ' + 26: 3, # 'Ï' + 27: 3, # 'ÏŽ' + }, + 42: { # 'ψ' + 60: 0, # 'e' + 55: 0, # 'o' + 58: 0, # 't' + 36: 0, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'ÎŒ' + 31: 0, # 'Α' + 51: 0, # 'Î’' + 43: 0, # 'Γ' + 41: 0, # 'Δ' + 34: 0, # 'Ε' + 40: 0, # 'Η' + 52: 0, # 'Θ' + 47: 0, # 'Ι' + 44: 0, # 'Κ' + 53: 0, # 'Λ' + 38: 0, # 'Îœ' + 49: 0, # 'Î' + 59: 0, # 'Ξ' + 39: 0, # 'Ο' + 35: 0, # 'Π' + 48: 0, # 'Ρ' + 37: 0, # 'Σ' + 33: 0, # 'Τ' + 45: 0, # 'Î¥' + 56: 0, # 'Φ' + 50: 0, # 'Χ' + 57: 0, # 'Ω' + 17: 2, # 'ά' + 18: 2, # 'έ' + 22: 1, # 'ή' + 15: 2, # 'ί' + 1: 2, # 'α' + 29: 0, # 'β' + 20: 0, # 'γ' + 21: 0, # 'δ' + 3: 3, # 'ε' + 32: 0, # 'ζ' + 13: 3, # 'η' + 25: 0, # 'θ' + 5: 2, # 'ι' + 11: 0, # 'κ' + 16: 0, # 'λ' + 10: 0, # 'μ' + 6: 0, # 'ν' + 30: 0, # 'ξ' + 4: 2, # 'ο' + 9: 0, # 'Ï€' + 8: 0, # 'Ï' + 14: 0, # 'Ï‚' + 7: 0, # 'σ' + 2: 2, # 'Ï„' + 12: 1, # 'Ï…' + 28: 0, # 'φ' + 23: 0, # 'χ' + 42: 0, # 'ψ' + 24: 2, # 'ω' + 19: 0, # 'ÏŒ' + 26: 0, # 'Ï' + 27: 0, # 'ÏŽ' + }, + 24: { # 'ω' + 60: 0, # 'e' + 55: 0, # 'o' + 58: 0, # 't' + 36: 0, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'ÎŒ' + 31: 0, # 'Α' + 51: 0, # 'Î’' + 43: 0, # 'Γ' + 41: 0, # 'Δ' + 34: 0, # 'Ε' + 40: 0, # 'Η' + 52: 0, # 'Θ' + 47: 0, # 'Ι' + 44: 0, # 'Κ' + 53: 0, # 'Λ' + 38: 0, # 'Îœ' + 49: 0, # 'Î' + 59: 0, # 'Ξ' + 39: 0, # 'Ο' + 35: 0, # 'Π' + 48: 0, # 'Ρ' + 37: 0, # 'Σ' + 33: 0, # 'Τ' + 45: 0, # 'Î¥' + 56: 0, # 'Φ' + 50: 0, # 'Χ' + 57: 0, # 'Ω' + 17: 1, # 'ά' + 18: 0, # 'έ' + 22: 2, # 'ή' + 15: 0, # 'ί' + 1: 0, # 'α' + 29: 2, # 'β' + 20: 3, # 'γ' + 21: 2, # 'δ' + 3: 0, # 'ε' + 32: 0, # 'ζ' + 13: 0, # 'η' + 25: 3, # 'θ' + 5: 2, # 'ι' + 11: 0, # 'κ' + 16: 2, # 'λ' + 10: 3, # 'μ' + 6: 3, # 'ν' + 30: 0, # 'ξ' + 4: 0, # 'ο' + 9: 3, # 'Ï€' + 8: 3, # 'Ï' + 14: 3, # 'Ï‚' + 7: 3, # 'σ' + 2: 3, # 'Ï„' + 12: 0, # 'Ï…' + 28: 2, # 'φ' + 23: 2, # 'χ' + 42: 0, # 'ψ' + 24: 0, # 'ω' + 19: 0, # 'ÏŒ' + 26: 0, # 'Ï' + 27: 0, # 'ÏŽ' + }, + 19: { # 'ÏŒ' + 60: 0, # 'e' + 55: 0, # 'o' + 58: 0, # 't' + 36: 0, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'ÎŒ' + 31: 0, # 'Α' + 51: 0, # 'Î’' + 43: 0, # 'Γ' + 41: 0, # 'Δ' + 34: 0, # 'Ε' + 40: 0, # 'Η' + 52: 0, # 'Θ' + 47: 0, # 'Ι' + 44: 0, # 'Κ' + 53: 0, # 'Λ' + 38: 0, # 'Îœ' + 49: 0, # 'Î' + 59: 0, # 'Ξ' + 39: 0, # 'Ο' + 35: 0, # 'Π' + 48: 0, # 'Ρ' + 37: 0, # 'Σ' + 33: 0, # 'Τ' + 45: 0, # 'Î¥' + 56: 0, # 'Φ' + 50: 0, # 'Χ' + 57: 0, # 'Ω' + 17: 0, # 'ά' + 18: 0, # 'έ' + 22: 0, # 'ή' + 15: 0, # 'ί' + 1: 0, # 'α' + 29: 3, # 'β' + 20: 3, # 'γ' + 21: 3, # 'δ' + 3: 1, # 'ε' + 32: 2, # 'ζ' + 13: 2, # 'η' + 25: 2, # 'θ' + 5: 2, # 'ι' + 11: 3, # 'κ' + 16: 3, # 'λ' + 10: 3, # 'μ' + 6: 3, # 'ν' + 30: 1, # 'ξ' + 4: 2, # 'ο' + 9: 3, # 'Ï€' + 8: 3, # 'Ï' + 14: 3, # 'Ï‚' + 7: 3, # 'σ' + 2: 3, # 'Ï„' + 12: 0, # 'Ï…' + 28: 2, # 'φ' + 23: 3, # 'χ' + 42: 2, # 'ψ' + 24: 0, # 'ω' + 19: 0, # 'ÏŒ' + 26: 0, # 'Ï' + 27: 0, # 'ÏŽ' + }, + 26: { # 'Ï' + 60: 0, # 'e' + 55: 0, # 'o' + 58: 0, # 't' + 36: 0, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'ÎŒ' + 31: 0, # 'Α' + 51: 0, # 'Î’' + 43: 0, # 'Γ' + 41: 0, # 'Δ' + 34: 0, # 'Ε' + 40: 0, # 'Η' + 52: 0, # 'Θ' + 47: 0, # 'Ι' + 44: 0, # 'Κ' + 53: 0, # 'Λ' + 38: 0, # 'Îœ' + 49: 0, # 'Î' + 59: 0, # 'Ξ' + 39: 0, # 'Ο' + 35: 0, # 'Π' + 48: 0, # 'Ρ' + 37: 0, # 'Σ' + 33: 0, # 'Τ' + 45: 0, # 'Î¥' + 56: 0, # 'Φ' + 50: 0, # 'Χ' + 57: 0, # 'Ω' + 17: 0, # 'ά' + 18: 0, # 'έ' + 22: 0, # 'ή' + 15: 0, # 'ί' + 1: 2, # 'α' + 29: 2, # 'β' + 20: 2, # 'γ' + 21: 1, # 'δ' + 3: 3, # 'ε' + 32: 0, # 'ζ' + 13: 2, # 'η' + 25: 3, # 'θ' + 5: 0, # 'ι' + 11: 3, # 'κ' + 16: 3, # 'λ' + 10: 3, # 'μ' + 6: 3, # 'ν' + 30: 2, # 'ξ' + 4: 3, # 'ο' + 9: 3, # 'Ï€' + 8: 3, # 'Ï' + 14: 3, # 'Ï‚' + 7: 3, # 'σ' + 2: 3, # 'Ï„' + 12: 0, # 'Ï…' + 28: 2, # 'φ' + 23: 2, # 'χ' + 42: 2, # 'ψ' + 24: 2, # 'ω' + 19: 0, # 'ÏŒ' + 26: 0, # 'Ï' + 27: 0, # 'ÏŽ' + }, + 27: { # 'ÏŽ' + 60: 0, # 'e' + 55: 0, # 'o' + 58: 0, # 't' + 36: 0, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'ÎŒ' + 31: 0, # 'Α' + 51: 0, # 'Î’' + 43: 0, # 'Γ' + 41: 0, # 'Δ' + 34: 0, # 'Ε' + 40: 0, # 'Η' + 52: 0, # 'Θ' + 47: 0, # 'Ι' + 44: 0, # 'Κ' + 53: 0, # 'Λ' + 38: 0, # 'Îœ' + 49: 0, # 'Î' + 59: 0, # 'Ξ' + 39: 0, # 'Ο' + 35: 0, # 'Π' + 48: 0, # 'Ρ' + 37: 0, # 'Σ' + 33: 0, # 'Τ' + 45: 0, # 'Î¥' + 56: 0, # 'Φ' + 50: 0, # 'Χ' + 57: 0, # 'Ω' + 17: 0, # 'ά' + 18: 0, # 'έ' + 22: 0, # 'ή' + 15: 0, # 'ί' + 1: 0, # 'α' + 29: 1, # 'β' + 20: 0, # 'γ' + 21: 3, # 'δ' + 3: 0, # 'ε' + 32: 0, # 'ζ' + 13: 1, # 'η' + 25: 2, # 'θ' + 5: 2, # 'ι' + 11: 0, # 'κ' + 16: 2, # 'λ' + 10: 3, # 'μ' + 6: 3, # 'ν' + 30: 1, # 'ξ' + 4: 0, # 'ο' + 9: 2, # 'Ï€' + 8: 3, # 'Ï' + 14: 3, # 'Ï‚' + 7: 3, # 'σ' + 2: 3, # 'Ï„' + 12: 0, # 'Ï…' + 28: 1, # 'φ' + 23: 1, # 'χ' + 42: 0, # 'ψ' + 24: 0, # 'ω' + 19: 0, # 'ÏŒ' + 26: 0, # 'Ï' + 27: 0, # 'ÏŽ' + }, +} + +# 255: Undefined characters that did not exist in training text # 254: Carriage/Return # 253: symbol (punctuation) that does not belong to word # 252: 0 - 9 +# 251: Control characters -# Character Mapping Table: -Latin7_char_to_order_map = ( -255,255,255,255,255,255,255,255,255,255,254,255,255,254,255,255, # 00 -255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, # 10 -253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253, # 20 -252,252,252,252,252,252,252,252,252,252,253,253,253,253,253,253, # 30 -253, 82,100,104, 94, 98,101,116,102,111,187,117, 92, 88,113, 85, # 40 - 79,118,105, 83, 67,114,119, 95, 99,109,188,253,253,253,253,253, # 50 -253, 72, 70, 80, 81, 60, 96, 93, 89, 68,120, 97, 77, 86, 69, 55, # 60 - 78,115, 65, 66, 58, 76,106,103, 87,107,112,253,253,253,253,253, # 70 -255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, # 80 -255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, # 90 -253,233, 90,253,253,253,253,253,253,253,253,253,253, 74,253,253, # a0 -253,253,253,253,247,248, 61, 36, 46, 71, 73,253, 54,253,108,123, # b0 -110, 31, 51, 43, 41, 34, 91, 40, 52, 47, 44, 53, 38, 49, 59, 39, # c0 - 35, 48,250, 37, 33, 45, 56, 50, 84, 57,120,121, 17, 18, 22, 15, # d0 -124, 1, 29, 20, 21, 3, 32, 13, 25, 5, 11, 16, 10, 6, 30, 4, # e0 - 9, 8, 14, 7, 2, 12, 28, 23, 42, 24, 64, 75, 19, 26, 27,253, # f0 -) - -win1253_char_to_order_map = ( -255,255,255,255,255,255,255,255,255,255,254,255,255,254,255,255, # 00 -255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, # 10 -253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253, # 20 -252,252,252,252,252,252,252,252,252,252,253,253,253,253,253,253, # 30 -253, 82,100,104, 94, 98,101,116,102,111,187,117, 92, 88,113, 85, # 40 - 79,118,105, 83, 67,114,119, 95, 99,109,188,253,253,253,253,253, # 50 -253, 72, 70, 80, 81, 60, 96, 93, 89, 68,120, 97, 77, 86, 69, 55, # 60 - 78,115, 65, 66, 58, 76,106,103, 87,107,112,253,253,253,253,253, # 70 -255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, # 80 -255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, # 90 -253,233, 61,253,253,253,253,253,253,253,253,253,253, 74,253,253, # a0 -253,253,253,253,247,253,253, 36, 46, 71, 73,253, 54,253,108,123, # b0 -110, 31, 51, 43, 41, 34, 91, 40, 52, 47, 44, 53, 38, 49, 59, 39, # c0 - 35, 48,250, 37, 33, 45, 56, 50, 84, 57,120,121, 17, 18, 22, 15, # d0 -124, 1, 29, 20, 21, 3, 32, 13, 25, 5, 11, 16, 10, 6, 30, 4, # e0 - 9, 8, 14, 7, 2, 12, 28, 23, 42, 24, 64, 75, 19, 26, 27,253, # f0 -) - -# Model Table: -# total sequences: 100% -# first 512 sequences: 98.2851% -# first 1024 sequences:1.7001% -# rest sequences: 0.0359% -# negative sequences: 0.0148% -GreekLangModel = ( -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,3,2,2,3,3,3,3,3,3,3,3,1,3,3,3,0,2,2,3,3,0,3,0,3,2,0,3,3,3,0, -3,0,0,0,2,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,3,3,3,3,3,0,3,3,0,3,2,3,3,0,3,2,3,3,3,0,0,3,0,3,0,3,3,2,0,0,0, -2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0, -0,2,3,2,2,3,3,3,3,3,3,3,3,0,3,3,3,3,0,2,3,3,0,3,3,3,3,2,3,3,3,0, -2,0,0,0,2,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,2,3,3,2,3,3,3,3,3,3,3,3,3,3,3,3,0,2,1,3,3,3,3,2,3,3,2,3,3,2,0, -0,0,0,0,2,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,3,3,3,3,0,3,3,3,3,3,3,0,3,3,0,3,3,3,3,3,3,3,3,3,3,0,3,2,3,3,0, -2,0,1,0,2,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0, -0,3,3,3,3,3,2,3,0,0,0,0,3,3,0,3,1,3,3,3,0,3,3,0,3,3,3,3,0,0,0,0, -2,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,3,3,3,3,3,0,3,0,3,3,3,3,3,0,3,2,2,2,3,0,2,3,3,3,3,3,2,3,3,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,3,3,3,3,3,3,2,2,2,3,3,3,3,0,3,1,3,3,3,3,2,3,3,3,3,3,3,3,2,2,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,3,3,3,3,3,2,0,3,0,0,0,3,3,2,3,3,3,3,3,0,0,3,2,3,0,2,3,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,3,0,3,3,3,3,0,0,3,3,0,2,3,0,3,0,3,3,3,0,0,3,0,3,0,2,2,3,3,0,0, -0,0,1,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,3,3,3,3,3,2,0,3,2,3,3,3,3,0,3,3,3,3,3,0,3,3,2,3,2,3,3,2,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,3,3,2,3,2,3,3,3,3,3,3,0,2,3,2,3,2,2,2,3,2,3,3,2,3,0,2,2,2,3,0, -2,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,3,0,0,0,3,3,3,2,3,3,0,0,3,0,3,0,0,0,3,2,0,3,0,3,0,0,2,0,2,0, -0,0,0,0,2,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,3,3,3,3,0,3,3,3,3,3,3,0,3,3,0,3,0,0,0,3,3,0,3,3,3,0,0,1,2,3,0, -3,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,3,3,3,3,3,2,0,0,3,2,2,3,3,0,3,3,3,3,3,2,1,3,0,3,2,3,3,2,1,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,3,3,0,2,3,3,3,3,3,3,0,0,3,0,3,0,0,0,3,3,0,3,2,3,0,0,3,3,3,0, -3,0,0,0,2,0,0,0,0,0,3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,3,3,3,3,0,3,3,3,3,3,3,0,0,3,0,3,0,0,0,3,2,0,3,2,3,0,0,3,2,3,0, -2,0,0,0,0,0,0,0,0,0,3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,3,1,2,2,3,3,3,3,3,3,0,2,3,0,3,0,0,0,3,3,0,3,0,2,0,0,2,3,1,0, -2,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,3,0,3,3,3,3,0,3,0,3,3,2,3,0,3,3,3,3,3,3,0,3,3,3,0,2,3,0,0,3,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,3,0,3,3,3,0,0,3,0,0,0,3,3,0,3,0,2,3,3,0,0,3,0,3,0,3,3,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,3,0,0,0,3,3,3,3,3,3,0,0,3,0,2,0,0,0,3,3,0,3,0,3,0,0,2,0,2,0, -0,0,0,0,1,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,3,3,3,3,3,3,0,3,0,2,0,3,2,0,3,2,3,2,3,0,0,3,2,3,2,3,3,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,3,0,0,2,3,3,3,3,3,0,0,0,3,0,2,1,0,0,3,2,2,2,0,3,0,0,2,2,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,3,0,3,3,3,2,0,3,0,3,0,3,3,0,2,1,2,3,3,0,0,3,0,3,0,3,3,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,2,3,3,3,0,3,3,3,3,3,3,0,2,3,0,3,0,0,0,2,1,0,2,2,3,0,0,2,2,2,0, -0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,3,0,0,2,3,3,3,2,3,0,0,1,3,0,2,0,0,0,0,3,0,1,0,2,0,0,1,1,1,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,3,3,3,3,3,1,0,3,0,0,0,3,2,0,3,2,3,3,3,0,0,3,0,3,2,2,2,1,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,3,0,3,3,3,0,0,3,0,0,0,0,2,0,2,3,3,2,2,2,2,3,0,2,0,2,2,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,3,3,3,3,2,0,0,0,0,0,0,2,3,0,2,0,2,3,2,0,0,3,0,3,0,3,1,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,0,0,0,0,3,2,3,3,2,2,3,0,2,0,3,0,0,0,2,0,0,0,0,1,2,0,2,0,2,0, -0,2,0,2,0,2,2,0,0,1,0,2,2,2,0,2,2,2,0,2,2,2,0,0,2,0,0,1,0,0,0,0, -0,2,0,3,3,2,0,0,0,0,0,0,1,3,0,2,0,2,2,2,0,0,2,0,3,0,0,2,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,3,0,2,3,2,0,2,2,0,2,0,2,2,0,2,0,2,2,2,0,0,0,0,0,0,2,3,0,0,0,2, -0,1,2,0,0,0,0,2,2,0,0,0,2,1,0,2,2,0,0,0,0,0,0,1,0,2,0,0,0,0,0,0, -0,0,2,1,0,2,3,2,2,3,2,3,2,0,0,3,3,3,0,0,3,2,0,0,0,1,1,0,2,0,2,2, -0,2,0,2,0,2,2,0,0,2,0,2,2,2,0,2,2,2,2,0,0,2,0,0,0,2,0,1,0,0,0,0, -0,3,0,3,3,2,2,0,3,0,0,0,2,2,0,2,2,2,1,2,0,0,1,2,2,0,0,3,0,0,0,2, -0,1,2,0,0,0,1,2,0,0,0,0,0,0,0,2,2,0,1,0,0,2,0,0,0,2,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,2,3,3,2,2,0,0,0,2,0,2,3,3,0,2,0,0,0,0,0,0,2,2,2,0,2,2,0,2,0,2, -0,2,2,0,0,2,2,2,2,1,0,0,2,2,0,2,0,0,2,0,0,0,0,0,0,2,0,0,0,0,0,0, -0,2,0,3,2,3,0,0,0,3,0,0,2,2,0,2,0,2,2,2,0,0,2,0,0,0,0,0,0,0,0,2, -0,0,2,2,0,0,2,2,2,0,0,0,0,0,0,2,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,2,0,0,3,2,0,2,2,2,2,2,0,0,0,2,0,0,0,0,2,0,1,0,0,2,0,1,0,0,0, -0,2,2,2,0,2,2,0,1,2,0,2,2,2,0,2,2,2,2,1,2,2,0,0,2,0,0,0,0,0,0,0, -0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0, -0,2,0,2,0,2,2,0,0,0,0,1,2,1,0,0,2,2,0,0,2,0,0,0,0,0,0,0,0,0,0,0, -0,0,0,3,2,3,0,0,2,0,0,0,2,2,0,2,0,0,0,1,0,0,2,0,2,0,2,2,0,0,0,0, -0,0,2,0,0,0,0,2,2,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0, -0,2,2,3,2,2,0,0,0,0,0,0,1,3,0,2,0,2,2,0,0,0,1,0,2,0,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,2,0,2,0,3,2,0,2,0,0,0,0,0,0,2,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1, -0,0,2,0,0,0,0,1,1,0,0,2,1,2,0,2,2,0,1,0,0,1,0,0,0,2,0,0,0,0,0,0, -0,3,0,2,2,2,0,0,2,0,0,0,2,0,0,0,2,3,0,2,0,0,0,0,0,0,2,2,0,0,0,2, -0,1,2,0,0,0,1,2,2,1,0,0,0,2,0,0,2,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,0,3,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,2,1,2,0,2,2,0,2,0,0,2,0,0,0,0,1,2,1,0,2,1,0,0,0,0,0,0,0,0,0,0, -0,0,2,0,0,0,3,1,2,2,0,2,0,0,0,0,2,0,0,0,2,0,0,3,0,0,0,0,2,2,2,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,2,1,0,2,0,1,2,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,1,0,0,0,0,0,0,2, -0,2,2,0,0,2,2,2,2,2,0,1,2,0,0,0,2,2,0,1,0,2,0,0,2,2,0,0,0,0,0,0, -0,0,0,0,1,0,0,0,0,0,0,0,3,0,0,2,0,0,0,0,0,0,0,0,2,0,2,0,0,0,0,2, -0,1,2,0,0,0,0,2,2,1,0,1,0,1,0,2,2,2,1,0,0,0,0,0,0,1,0,0,0,0,0,0, -0,2,0,1,2,0,0,0,0,0,0,0,0,0,0,2,0,0,2,2,0,0,0,0,1,0,0,0,0,0,0,2, -0,2,2,0,0,0,0,2,2,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,2,0,0,2,0,0,0, -0,2,2,2,2,0,0,0,3,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,2,0,0,0,0,0,0,1, -0,0,2,0,0,0,0,1,2,0,0,0,0,0,0,2,2,1,1,0,0,0,0,0,0,1,0,0,0,0,0,0, -0,2,0,2,2,2,0,0,2,0,0,0,0,0,0,0,2,2,2,0,0,0,2,0,0,0,0,0,0,0,0,2, -0,0,1,0,0,0,0,2,1,0,0,0,0,0,0,1,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0, -0,3,0,2,0,0,0,0,0,0,0,0,2,0,0,0,0,0,2,0,0,0,0,0,0,0,2,0,0,0,0,2, -0,0,2,0,0,0,0,2,2,0,0,0,0,1,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,2,0,2,2,1,0,0,0,0,0,0,2,0,0,2,0,2,2,2,0,0,0,0,0,0,2,0,0,0,0,2, -0,0,2,0,0,2,0,2,2,0,0,0,0,2,0,2,0,0,0,0,0,2,0,0,0,2,0,0,0,0,0,0, -0,0,3,0,0,0,2,2,0,2,2,0,0,0,0,0,2,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,0,0,0,0,1,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,2,0,0,0,0,0, -0,2,2,2,2,2,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,0,0,0,1, -0,0,0,0,0,0,0,2,1,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,0,0,0,0,0,2,2,0,0,0,0,0,2,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0, -0,2,0,0,0,2,0,0,0,0,0,1,0,0,0,0,2,2,0,0,0,1,0,0,0,0,0,0,0,0,0,0, -0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,1,0,2,0,0,0, -0,2,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,1,0,0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,1,0,0,2,0,2,0,0,0, -0,0,0,0,0,0,0,0,2,1,0,0,0,0,0,0,2,0,0,0,1,2,0,0,0,0,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -) - -Latin7GreekModel = { - 'char_to_order_map': Latin7_char_to_order_map, - 'precedence_matrix': GreekLangModel, - 'typical_positive_ratio': 0.982851, - 'keep_english_letter': False, - 'charset_name': "ISO-8859-7", - 'language': 'Greek', +# Character Mapping Table(s): +WINDOWS_1253_GREEK_CHAR_TO_ORDER = { + 0: 255, # '\x00' + 1: 255, # '\x01' + 2: 255, # '\x02' + 3: 255, # '\x03' + 4: 255, # '\x04' + 5: 255, # '\x05' + 6: 255, # '\x06' + 7: 255, # '\x07' + 8: 255, # '\x08' + 9: 255, # '\t' + 10: 254, # '\n' + 11: 255, # '\x0b' + 12: 255, # '\x0c' + 13: 254, # '\r' + 14: 255, # '\x0e' + 15: 255, # '\x0f' + 16: 255, # '\x10' + 17: 255, # '\x11' + 18: 255, # '\x12' + 19: 255, # '\x13' + 20: 255, # '\x14' + 21: 255, # '\x15' + 22: 255, # '\x16' + 23: 255, # '\x17' + 24: 255, # '\x18' + 25: 255, # '\x19' + 26: 255, # '\x1a' + 27: 255, # '\x1b' + 28: 255, # '\x1c' + 29: 255, # '\x1d' + 30: 255, # '\x1e' + 31: 255, # '\x1f' + 32: 253, # ' ' + 33: 253, # '!' + 34: 253, # '"' + 35: 253, # '#' + 36: 253, # '$' + 37: 253, # '%' + 38: 253, # '&' + 39: 253, # "'" + 40: 253, # '(' + 41: 253, # ')' + 42: 253, # '*' + 43: 253, # '+' + 44: 253, # ',' + 45: 253, # '-' + 46: 253, # '.' + 47: 253, # '/' + 48: 252, # '0' + 49: 252, # '1' + 50: 252, # '2' + 51: 252, # '3' + 52: 252, # '4' + 53: 252, # '5' + 54: 252, # '6' + 55: 252, # '7' + 56: 252, # '8' + 57: 252, # '9' + 58: 253, # ':' + 59: 253, # ';' + 60: 253, # '<' + 61: 253, # '=' + 62: 253, # '>' + 63: 253, # '?' + 64: 253, # '@' + 65: 82, # 'A' + 66: 100, # 'B' + 67: 104, # 'C' + 68: 94, # 'D' + 69: 98, # 'E' + 70: 101, # 'F' + 71: 116, # 'G' + 72: 102, # 'H' + 73: 111, # 'I' + 74: 187, # 'J' + 75: 117, # 'K' + 76: 92, # 'L' + 77: 88, # 'M' + 78: 113, # 'N' + 79: 85, # 'O' + 80: 79, # 'P' + 81: 118, # 'Q' + 82: 105, # 'R' + 83: 83, # 'S' + 84: 67, # 'T' + 85: 114, # 'U' + 86: 119, # 'V' + 87: 95, # 'W' + 88: 99, # 'X' + 89: 109, # 'Y' + 90: 188, # 'Z' + 91: 253, # '[' + 92: 253, # '\\' + 93: 253, # ']' + 94: 253, # '^' + 95: 253, # '_' + 96: 253, # '`' + 97: 72, # 'a' + 98: 70, # 'b' + 99: 80, # 'c' + 100: 81, # 'd' + 101: 60, # 'e' + 102: 96, # 'f' + 103: 93, # 'g' + 104: 89, # 'h' + 105: 68, # 'i' + 106: 120, # 'j' + 107: 97, # 'k' + 108: 77, # 'l' + 109: 86, # 'm' + 110: 69, # 'n' + 111: 55, # 'o' + 112: 78, # 'p' + 113: 115, # 'q' + 114: 65, # 'r' + 115: 66, # 's' + 116: 58, # 't' + 117: 76, # 'u' + 118: 106, # 'v' + 119: 103, # 'w' + 120: 87, # 'x' + 121: 107, # 'y' + 122: 112, # 'z' + 123: 253, # '{' + 124: 253, # '|' + 125: 253, # '}' + 126: 253, # '~' + 127: 253, # '\x7f' + 128: 255, # '€' + 129: 255, # None + 130: 255, # '‚' + 131: 255, # 'Æ’' + 132: 255, # '„' + 133: 255, # '…' + 134: 255, # '†' + 135: 255, # '‡' + 136: 255, # None + 137: 255, # '‰' + 138: 255, # None + 139: 255, # '‹' + 140: 255, # None + 141: 255, # None + 142: 255, # None + 143: 255, # None + 144: 255, # None + 145: 255, # '‘' + 146: 255, # '’' + 147: 255, # '“' + 148: 255, # 'â€' + 149: 255, # '•' + 150: 255, # '–' + 151: 255, # '—' + 152: 255, # None + 153: 255, # 'â„¢' + 154: 255, # None + 155: 255, # '›' + 156: 255, # None + 157: 255, # None + 158: 255, # None + 159: 255, # None + 160: 253, # '\xa0' + 161: 233, # 'Î…' + 162: 61, # 'Ά' + 163: 253, # '£' + 164: 253, # '¤' + 165: 253, # 'Â¥' + 166: 253, # '¦' + 167: 253, # '§' + 168: 253, # '¨' + 169: 253, # '©' + 170: 253, # None + 171: 253, # '«' + 172: 253, # '¬' + 173: 74, # '\xad' + 174: 253, # '®' + 175: 253, # '―' + 176: 253, # '°' + 177: 253, # '±' + 178: 253, # '²' + 179: 253, # '³' + 180: 247, # '΄' + 181: 253, # 'µ' + 182: 253, # '¶' + 183: 36, # '·' + 184: 46, # 'Έ' + 185: 71, # 'Ή' + 186: 73, # 'Ί' + 187: 253, # '»' + 188: 54, # 'ÎŒ' + 189: 253, # '½' + 190: 108, # 'ÎŽ' + 191: 123, # 'Î' + 192: 110, # 'Î' + 193: 31, # 'Α' + 194: 51, # 'Î’' + 195: 43, # 'Γ' + 196: 41, # 'Δ' + 197: 34, # 'Ε' + 198: 91, # 'Ζ' + 199: 40, # 'Η' + 200: 52, # 'Θ' + 201: 47, # 'Ι' + 202: 44, # 'Κ' + 203: 53, # 'Λ' + 204: 38, # 'Îœ' + 205: 49, # 'Î' + 206: 59, # 'Ξ' + 207: 39, # 'Ο' + 208: 35, # 'Π' + 209: 48, # 'Ρ' + 210: 250, # None + 211: 37, # 'Σ' + 212: 33, # 'Τ' + 213: 45, # 'Î¥' + 214: 56, # 'Φ' + 215: 50, # 'Χ' + 216: 84, # 'Ψ' + 217: 57, # 'Ω' + 218: 120, # 'Ϊ' + 219: 121, # 'Ϋ' + 220: 17, # 'ά' + 221: 18, # 'έ' + 222: 22, # 'ή' + 223: 15, # 'ί' + 224: 124, # 'ΰ' + 225: 1, # 'α' + 226: 29, # 'β' + 227: 20, # 'γ' + 228: 21, # 'δ' + 229: 3, # 'ε' + 230: 32, # 'ζ' + 231: 13, # 'η' + 232: 25, # 'θ' + 233: 5, # 'ι' + 234: 11, # 'κ' + 235: 16, # 'λ' + 236: 10, # 'μ' + 237: 6, # 'ν' + 238: 30, # 'ξ' + 239: 4, # 'ο' + 240: 9, # 'Ï€' + 241: 8, # 'Ï' + 242: 14, # 'Ï‚' + 243: 7, # 'σ' + 244: 2, # 'Ï„' + 245: 12, # 'Ï…' + 246: 28, # 'φ' + 247: 23, # 'χ' + 248: 42, # 'ψ' + 249: 24, # 'ω' + 250: 64, # 'ÏŠ' + 251: 75, # 'Ï‹' + 252: 19, # 'ÏŒ' + 253: 26, # 'Ï' + 254: 27, # 'ÏŽ' + 255: 253, # None } -Win1253GreekModel = { - 'char_to_order_map': win1253_char_to_order_map, - 'precedence_matrix': GreekLangModel, - 'typical_positive_ratio': 0.982851, - 'keep_english_letter': False, - 'charset_name': "windows-1253", - 'language': 'Greek', +WINDOWS_1253_GREEK_MODEL = SingleByteCharSetModel(charset_name='windows-1253', + language='Greek', + char_to_order_map=WINDOWS_1253_GREEK_CHAR_TO_ORDER, + language_model=GREEK_LANG_MODEL, + typical_positive_ratio=0.982851, + keep_ascii_letters=False, + alphabet='ΆΈΉΊΌΎÎΑΒΓΔΕΖΗΘΙΚΛΜÎΞΟΠΡΣΤΥΦΧΨΩάέήίαβγδεζηθικλμνξοπÏςστυφχψωόÏÏŽ') + +ISO_8859_7_GREEK_CHAR_TO_ORDER = { + 0: 255, # '\x00' + 1: 255, # '\x01' + 2: 255, # '\x02' + 3: 255, # '\x03' + 4: 255, # '\x04' + 5: 255, # '\x05' + 6: 255, # '\x06' + 7: 255, # '\x07' + 8: 255, # '\x08' + 9: 255, # '\t' + 10: 254, # '\n' + 11: 255, # '\x0b' + 12: 255, # '\x0c' + 13: 254, # '\r' + 14: 255, # '\x0e' + 15: 255, # '\x0f' + 16: 255, # '\x10' + 17: 255, # '\x11' + 18: 255, # '\x12' + 19: 255, # '\x13' + 20: 255, # '\x14' + 21: 255, # '\x15' + 22: 255, # '\x16' + 23: 255, # '\x17' + 24: 255, # '\x18' + 25: 255, # '\x19' + 26: 255, # '\x1a' + 27: 255, # '\x1b' + 28: 255, # '\x1c' + 29: 255, # '\x1d' + 30: 255, # '\x1e' + 31: 255, # '\x1f' + 32: 253, # ' ' + 33: 253, # '!' + 34: 253, # '"' + 35: 253, # '#' + 36: 253, # '$' + 37: 253, # '%' + 38: 253, # '&' + 39: 253, # "'" + 40: 253, # '(' + 41: 253, # ')' + 42: 253, # '*' + 43: 253, # '+' + 44: 253, # ',' + 45: 253, # '-' + 46: 253, # '.' + 47: 253, # '/' + 48: 252, # '0' + 49: 252, # '1' + 50: 252, # '2' + 51: 252, # '3' + 52: 252, # '4' + 53: 252, # '5' + 54: 252, # '6' + 55: 252, # '7' + 56: 252, # '8' + 57: 252, # '9' + 58: 253, # ':' + 59: 253, # ';' + 60: 253, # '<' + 61: 253, # '=' + 62: 253, # '>' + 63: 253, # '?' + 64: 253, # '@' + 65: 82, # 'A' + 66: 100, # 'B' + 67: 104, # 'C' + 68: 94, # 'D' + 69: 98, # 'E' + 70: 101, # 'F' + 71: 116, # 'G' + 72: 102, # 'H' + 73: 111, # 'I' + 74: 187, # 'J' + 75: 117, # 'K' + 76: 92, # 'L' + 77: 88, # 'M' + 78: 113, # 'N' + 79: 85, # 'O' + 80: 79, # 'P' + 81: 118, # 'Q' + 82: 105, # 'R' + 83: 83, # 'S' + 84: 67, # 'T' + 85: 114, # 'U' + 86: 119, # 'V' + 87: 95, # 'W' + 88: 99, # 'X' + 89: 109, # 'Y' + 90: 188, # 'Z' + 91: 253, # '[' + 92: 253, # '\\' + 93: 253, # ']' + 94: 253, # '^' + 95: 253, # '_' + 96: 253, # '`' + 97: 72, # 'a' + 98: 70, # 'b' + 99: 80, # 'c' + 100: 81, # 'd' + 101: 60, # 'e' + 102: 96, # 'f' + 103: 93, # 'g' + 104: 89, # 'h' + 105: 68, # 'i' + 106: 120, # 'j' + 107: 97, # 'k' + 108: 77, # 'l' + 109: 86, # 'm' + 110: 69, # 'n' + 111: 55, # 'o' + 112: 78, # 'p' + 113: 115, # 'q' + 114: 65, # 'r' + 115: 66, # 's' + 116: 58, # 't' + 117: 76, # 'u' + 118: 106, # 'v' + 119: 103, # 'w' + 120: 87, # 'x' + 121: 107, # 'y' + 122: 112, # 'z' + 123: 253, # '{' + 124: 253, # '|' + 125: 253, # '}' + 126: 253, # '~' + 127: 253, # '\x7f' + 128: 255, # '\x80' + 129: 255, # '\x81' + 130: 255, # '\x82' + 131: 255, # '\x83' + 132: 255, # '\x84' + 133: 255, # '\x85' + 134: 255, # '\x86' + 135: 255, # '\x87' + 136: 255, # '\x88' + 137: 255, # '\x89' + 138: 255, # '\x8a' + 139: 255, # '\x8b' + 140: 255, # '\x8c' + 141: 255, # '\x8d' + 142: 255, # '\x8e' + 143: 255, # '\x8f' + 144: 255, # '\x90' + 145: 255, # '\x91' + 146: 255, # '\x92' + 147: 255, # '\x93' + 148: 255, # '\x94' + 149: 255, # '\x95' + 150: 255, # '\x96' + 151: 255, # '\x97' + 152: 255, # '\x98' + 153: 255, # '\x99' + 154: 255, # '\x9a' + 155: 255, # '\x9b' + 156: 255, # '\x9c' + 157: 255, # '\x9d' + 158: 255, # '\x9e' + 159: 255, # '\x9f' + 160: 253, # '\xa0' + 161: 233, # '‘' + 162: 90, # '’' + 163: 253, # '£' + 164: 253, # '€' + 165: 253, # '₯' + 166: 253, # '¦' + 167: 253, # '§' + 168: 253, # '¨' + 169: 253, # '©' + 170: 253, # 'ͺ' + 171: 253, # '«' + 172: 253, # '¬' + 173: 74, # '\xad' + 174: 253, # None + 175: 253, # '―' + 176: 253, # '°' + 177: 253, # '±' + 178: 253, # '²' + 179: 253, # '³' + 180: 247, # '΄' + 181: 248, # 'Î…' + 182: 61, # 'Ά' + 183: 36, # '·' + 184: 46, # 'Έ' + 185: 71, # 'Ή' + 186: 73, # 'Ί' + 187: 253, # '»' + 188: 54, # 'ÎŒ' + 189: 253, # '½' + 190: 108, # 'ÎŽ' + 191: 123, # 'Î' + 192: 110, # 'Î' + 193: 31, # 'Α' + 194: 51, # 'Î’' + 195: 43, # 'Γ' + 196: 41, # 'Δ' + 197: 34, # 'Ε' + 198: 91, # 'Ζ' + 199: 40, # 'Η' + 200: 52, # 'Θ' + 201: 47, # 'Ι' + 202: 44, # 'Κ' + 203: 53, # 'Λ' + 204: 38, # 'Îœ' + 205: 49, # 'Î' + 206: 59, # 'Ξ' + 207: 39, # 'Ο' + 208: 35, # 'Π' + 209: 48, # 'Ρ' + 210: 250, # None + 211: 37, # 'Σ' + 212: 33, # 'Τ' + 213: 45, # 'Î¥' + 214: 56, # 'Φ' + 215: 50, # 'Χ' + 216: 84, # 'Ψ' + 217: 57, # 'Ω' + 218: 120, # 'Ϊ' + 219: 121, # 'Ϋ' + 220: 17, # 'ά' + 221: 18, # 'έ' + 222: 22, # 'ή' + 223: 15, # 'ί' + 224: 124, # 'ΰ' + 225: 1, # 'α' + 226: 29, # 'β' + 227: 20, # 'γ' + 228: 21, # 'δ' + 229: 3, # 'ε' + 230: 32, # 'ζ' + 231: 13, # 'η' + 232: 25, # 'θ' + 233: 5, # 'ι' + 234: 11, # 'κ' + 235: 16, # 'λ' + 236: 10, # 'μ' + 237: 6, # 'ν' + 238: 30, # 'ξ' + 239: 4, # 'ο' + 240: 9, # 'Ï€' + 241: 8, # 'Ï' + 242: 14, # 'Ï‚' + 243: 7, # 'σ' + 244: 2, # 'Ï„' + 245: 12, # 'Ï…' + 246: 28, # 'φ' + 247: 23, # 'χ' + 248: 42, # 'ψ' + 249: 24, # 'ω' + 250: 64, # 'ÏŠ' + 251: 75, # 'Ï‹' + 252: 19, # 'ÏŒ' + 253: 26, # 'Ï' + 254: 27, # 'ÏŽ' + 255: 253, # None } + +ISO_8859_7_GREEK_MODEL = SingleByteCharSetModel(charset_name='ISO-8859-7', + language='Greek', + char_to_order_map=ISO_8859_7_GREEK_CHAR_TO_ORDER, + language_model=GREEK_LANG_MODEL, + typical_positive_ratio=0.982851, + keep_ascii_letters=False, + alphabet='ΆΈΉΊΌΎÎΑΒΓΔΕΖΗΘΙΚΛΜÎΞΟΠΡΣΤΥΦΧΨΩάέήίαβγδεζηθικλμνξοπÏςστυφχψωόÏÏŽ') + diff --git a/libs/chardet/langhebrewmodel.py b/libs/chardet/langhebrewmodel.py index 58f4c875e..40fd674c4 100644 --- a/libs/chardet/langhebrewmodel.py +++ b/libs/chardet/langhebrewmodel.py @@ -1,200 +1,4383 @@ -######################## BEGIN LICENSE BLOCK ######################## -# The Original Code is Mozilla Universal charset detector code. -# -# The Initial Developer of the Original Code is -# Simon Montagu -# Portions created by the Initial Developer are Copyright (C) 2005 -# the Initial Developer. All Rights Reserved. -# -# Contributor(s): -# Mark Pilgrim - port to Python -# Shy Shalom - original C code -# Shoshannah Forbes - original C code (?) -# -# This library is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 2.1 of the License, or (at your option) any later version. -# -# This library is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with this library; if not, write to the Free Software -# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA -# 02110-1301 USA -######################### END LICENSE BLOCK ######################### +#!/usr/bin/env python +# -*- coding: utf-8 -*- -# 255: Control characters that usually does not exist in any text +from chardet.sbcharsetprober import SingleByteCharSetModel + + +# 3: Positive +# 2: Likely +# 1: Unlikely +# 0: Negative + +HEBREW_LANG_MODEL = { + 50: { # 'a' + 50: 0, # 'a' + 60: 1, # 'c' + 61: 1, # 'd' + 42: 1, # 'e' + 53: 1, # 'i' + 56: 2, # 'l' + 54: 2, # 'n' + 49: 0, # 'o' + 51: 2, # 'r' + 43: 1, # 's' + 44: 2, # 't' + 63: 1, # 'u' + 34: 0, # '\xa0' + 55: 0, # '´' + 48: 0, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 0, # 'Ö°' + 59: 0, # 'Ö±' + 41: 0, # 'Ö²' + 33: 0, # 'Ö´' + 37: 0, # 'Öµ' + 36: 0, # 'Ö¶' + 31: 0, # 'Ö·' + 29: 0, # 'Ö¸' + 35: 0, # 'Ö¹' + 62: 0, # 'Ö»' + 28: 0, # 'Ö¼' + 38: 0, # '×' + 45: 0, # 'ׂ' + 9: 0, # '×' + 8: 0, # 'ב' + 20: 0, # '×’' + 16: 0, # 'ד' + 3: 1, # '×”' + 2: 0, # 'ו' + 24: 0, # '×–' + 14: 0, # '×—' + 22: 0, # 'ט' + 1: 0, # '×™' + 25: 0, # 'ך' + 15: 0, # '×›' + 4: 0, # 'ל' + 11: 0, # '×' + 6: 1, # 'מ' + 23: 0, # 'ן' + 12: 0, # '× ' + 19: 0, # 'ס' + 13: 0, # '×¢' + 26: 0, # '×£' + 18: 0, # 'פ' + 27: 0, # '×¥' + 21: 0, # 'צ' + 17: 1, # 'ק' + 7: 0, # 'ר' + 10: 1, # 'ש' + 5: 0, # 'ת' + 32: 0, # '–' + 52: 1, # '’' + 47: 0, # '“' + 46: 1, # 'â€' + 58: 0, # '†' + 40: 1, # '…' + }, + 60: { # 'c' + 50: 1, # 'a' + 60: 1, # 'c' + 61: 0, # 'd' + 42: 1, # 'e' + 53: 1, # 'i' + 56: 1, # 'l' + 54: 0, # 'n' + 49: 1, # 'o' + 51: 1, # 'r' + 43: 1, # 's' + 44: 2, # 't' + 63: 1, # 'u' + 34: 0, # '\xa0' + 55: 0, # '´' + 48: 0, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 0, # 'Ö°' + 59: 0, # 'Ö±' + 41: 0, # 'Ö²' + 33: 0, # 'Ö´' + 37: 0, # 'Öµ' + 36: 0, # 'Ö¶' + 31: 0, # 'Ö·' + 29: 0, # 'Ö¸' + 35: 0, # 'Ö¹' + 62: 0, # 'Ö»' + 28: 0, # 'Ö¼' + 38: 0, # '×' + 45: 0, # 'ׂ' + 9: 1, # '×' + 8: 0, # 'ב' + 20: 0, # '×’' + 16: 0, # 'ד' + 3: 1, # '×”' + 2: 0, # 'ו' + 24: 0, # '×–' + 14: 0, # '×—' + 22: 0, # 'ט' + 1: 0, # '×™' + 25: 0, # 'ך' + 15: 0, # '×›' + 4: 0, # 'ל' + 11: 0, # '×' + 6: 1, # 'מ' + 23: 0, # 'ן' + 12: 1, # '× ' + 19: 0, # 'ס' + 13: 0, # '×¢' + 26: 0, # '×£' + 18: 0, # 'פ' + 27: 0, # '×¥' + 21: 0, # 'צ' + 17: 0, # 'ק' + 7: 0, # 'ר' + 10: 0, # 'ש' + 5: 0, # 'ת' + 32: 0, # '–' + 52: 0, # '’' + 47: 0, # '“' + 46: 1, # 'â€' + 58: 0, # '†' + 40: 1, # '…' + }, + 61: { # 'd' + 50: 1, # 'a' + 60: 0, # 'c' + 61: 1, # 'd' + 42: 1, # 'e' + 53: 1, # 'i' + 56: 1, # 'l' + 54: 1, # 'n' + 49: 2, # 'o' + 51: 1, # 'r' + 43: 1, # 's' + 44: 0, # 't' + 63: 1, # 'u' + 34: 0, # '\xa0' + 55: 0, # '´' + 48: 0, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 0, # 'Ö°' + 59: 0, # 'Ö±' + 41: 0, # 'Ö²' + 33: 0, # 'Ö´' + 37: 0, # 'Öµ' + 36: 0, # 'Ö¶' + 31: 0, # 'Ö·' + 29: 0, # 'Ö¸' + 35: 0, # 'Ö¹' + 62: 0, # 'Ö»' + 28: 0, # 'Ö¼' + 38: 0, # '×' + 45: 0, # 'ׂ' + 9: 0, # '×' + 8: 0, # 'ב' + 20: 0, # '×’' + 16: 0, # 'ד' + 3: 1, # '×”' + 2: 0, # 'ו' + 24: 0, # '×–' + 14: 0, # '×—' + 22: 0, # 'ט' + 1: 0, # '×™' + 25: 0, # 'ך' + 15: 0, # '×›' + 4: 0, # 'ל' + 11: 0, # '×' + 6: 0, # 'מ' + 23: 0, # 'ן' + 12: 0, # '× ' + 19: 0, # 'ס' + 13: 0, # '×¢' + 26: 0, # '×£' + 18: 0, # 'פ' + 27: 0, # '×¥' + 21: 0, # 'צ' + 17: 0, # 'ק' + 7: 0, # 'ר' + 10: 0, # 'ש' + 5: 0, # 'ת' + 32: 1, # '–' + 52: 1, # '’' + 47: 0, # '“' + 46: 1, # 'â€' + 58: 0, # '†' + 40: 1, # '…' + }, + 42: { # 'e' + 50: 1, # 'a' + 60: 1, # 'c' + 61: 2, # 'd' + 42: 1, # 'e' + 53: 1, # 'i' + 56: 2, # 'l' + 54: 2, # 'n' + 49: 1, # 'o' + 51: 2, # 'r' + 43: 2, # 's' + 44: 2, # 't' + 63: 1, # 'u' + 34: 1, # '\xa0' + 55: 0, # '´' + 48: 0, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 0, # 'Ö°' + 59: 0, # 'Ö±' + 41: 0, # 'Ö²' + 33: 0, # 'Ö´' + 37: 0, # 'Öµ' + 36: 0, # 'Ö¶' + 31: 0, # 'Ö·' + 29: 0, # 'Ö¸' + 35: 0, # 'Ö¹' + 62: 0, # 'Ö»' + 28: 0, # 'Ö¼' + 38: 0, # '×' + 45: 0, # 'ׂ' + 9: 0, # '×' + 8: 0, # 'ב' + 20: 0, # '×’' + 16: 0, # 'ד' + 3: 0, # '×”' + 2: 0, # 'ו' + 24: 0, # '×–' + 14: 0, # '×—' + 22: 0, # 'ט' + 1: 0, # '×™' + 25: 0, # 'ך' + 15: 0, # '×›' + 4: 0, # 'ל' + 11: 0, # '×' + 6: 0, # 'מ' + 23: 0, # 'ן' + 12: 0, # '× ' + 19: 0, # 'ס' + 13: 0, # '×¢' + 26: 0, # '×£' + 18: 1, # 'פ' + 27: 0, # '×¥' + 21: 0, # 'צ' + 17: 0, # 'ק' + 7: 0, # 'ר' + 10: 0, # 'ש' + 5: 0, # 'ת' + 32: 1, # '–' + 52: 2, # '’' + 47: 0, # '“' + 46: 1, # 'â€' + 58: 0, # '†' + 40: 1, # '…' + }, + 53: { # 'i' + 50: 1, # 'a' + 60: 2, # 'c' + 61: 1, # 'd' + 42: 1, # 'e' + 53: 0, # 'i' + 56: 1, # 'l' + 54: 2, # 'n' + 49: 2, # 'o' + 51: 1, # 'r' + 43: 2, # 's' + 44: 2, # 't' + 63: 1, # 'u' + 34: 0, # '\xa0' + 55: 1, # '´' + 48: 0, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 0, # 'Ö°' + 59: 0, # 'Ö±' + 41: 0, # 'Ö²' + 33: 0, # 'Ö´' + 37: 0, # 'Öµ' + 36: 0, # 'Ö¶' + 31: 0, # 'Ö·' + 29: 0, # 'Ö¸' + 35: 0, # 'Ö¹' + 62: 0, # 'Ö»' + 28: 0, # 'Ö¼' + 38: 0, # '×' + 45: 0, # 'ׂ' + 9: 0, # '×' + 8: 0, # 'ב' + 20: 0, # '×’' + 16: 0, # 'ד' + 3: 0, # '×”' + 2: 0, # 'ו' + 24: 0, # '×–' + 14: 0, # '×—' + 22: 0, # 'ט' + 1: 0, # '×™' + 25: 0, # 'ך' + 15: 0, # '×›' + 4: 0, # 'ל' + 11: 0, # '×' + 6: 0, # 'מ' + 23: 0, # 'ן' + 12: 0, # '× ' + 19: 0, # 'ס' + 13: 0, # '×¢' + 26: 0, # '×£' + 18: 0, # 'פ' + 27: 0, # '×¥' + 21: 0, # 'צ' + 17: 0, # 'ק' + 7: 0, # 'ר' + 10: 0, # 'ש' + 5: 0, # 'ת' + 32: 0, # '–' + 52: 1, # '’' + 47: 0, # '“' + 46: 0, # 'â€' + 58: 0, # '†' + 40: 0, # '…' + }, + 56: { # 'l' + 50: 1, # 'a' + 60: 1, # 'c' + 61: 1, # 'd' + 42: 2, # 'e' + 53: 2, # 'i' + 56: 2, # 'l' + 54: 1, # 'n' + 49: 1, # 'o' + 51: 0, # 'r' + 43: 1, # 's' + 44: 1, # 't' + 63: 1, # 'u' + 34: 0, # '\xa0' + 55: 0, # '´' + 48: 0, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 0, # 'Ö°' + 59: 0, # 'Ö±' + 41: 0, # 'Ö²' + 33: 0, # 'Ö´' + 37: 0, # 'Öµ' + 36: 0, # 'Ö¶' + 31: 0, # 'Ö·' + 29: 0, # 'Ö¸' + 35: 0, # 'Ö¹' + 62: 0, # 'Ö»' + 28: 0, # 'Ö¼' + 38: 0, # '×' + 45: 0, # 'ׂ' + 9: 0, # '×' + 8: 0, # 'ב' + 20: 0, # '×’' + 16: 0, # 'ד' + 3: 0, # '×”' + 2: 0, # 'ו' + 24: 0, # '×–' + 14: 0, # '×—' + 22: 0, # 'ט' + 1: 0, # '×™' + 25: 0, # 'ך' + 15: 0, # '×›' + 4: 0, # 'ל' + 11: 0, # '×' + 6: 0, # 'מ' + 23: 0, # 'ן' + 12: 0, # '× ' + 19: 0, # 'ס' + 13: 0, # '×¢' + 26: 0, # '×£' + 18: 0, # 'פ' + 27: 0, # '×¥' + 21: 0, # 'צ' + 17: 0, # 'ק' + 7: 0, # 'ר' + 10: 0, # 'ש' + 5: 0, # 'ת' + 32: 0, # '–' + 52: 1, # '’' + 47: 0, # '“' + 46: 1, # 'â€' + 58: 0, # '†' + 40: 1, # '…' + }, + 54: { # 'n' + 50: 1, # 'a' + 60: 1, # 'c' + 61: 1, # 'd' + 42: 1, # 'e' + 53: 1, # 'i' + 56: 1, # 'l' + 54: 1, # 'n' + 49: 1, # 'o' + 51: 0, # 'r' + 43: 1, # 's' + 44: 2, # 't' + 63: 1, # 'u' + 34: 0, # '\xa0' + 55: 0, # '´' + 48: 0, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 0, # 'Ö°' + 59: 0, # 'Ö±' + 41: 0, # 'Ö²' + 33: 0, # 'Ö´' + 37: 0, # 'Öµ' + 36: 0, # 'Ö¶' + 31: 0, # 'Ö·' + 29: 0, # 'Ö¸' + 35: 0, # 'Ö¹' + 62: 0, # 'Ö»' + 28: 0, # 'Ö¼' + 38: 0, # '×' + 45: 0, # 'ׂ' + 9: 0, # '×' + 8: 0, # 'ב' + 20: 0, # '×’' + 16: 0, # 'ד' + 3: 1, # '×”' + 2: 0, # 'ו' + 24: 0, # '×–' + 14: 0, # '×—' + 22: 0, # 'ט' + 1: 0, # '×™' + 25: 0, # 'ך' + 15: 0, # '×›' + 4: 0, # 'ל' + 11: 0, # '×' + 6: 0, # 'מ' + 23: 0, # 'ן' + 12: 0, # '× ' + 19: 0, # 'ס' + 13: 0, # '×¢' + 26: 0, # '×£' + 18: 0, # 'פ' + 27: 0, # '×¥' + 21: 0, # 'צ' + 17: 0, # 'ק' + 7: 0, # 'ר' + 10: 0, # 'ש' + 5: 0, # 'ת' + 32: 0, # '–' + 52: 2, # '’' + 47: 0, # '“' + 46: 1, # 'â€' + 58: 0, # '†' + 40: 1, # '…' + }, + 49: { # 'o' + 50: 1, # 'a' + 60: 1, # 'c' + 61: 1, # 'd' + 42: 1, # 'e' + 53: 1, # 'i' + 56: 1, # 'l' + 54: 2, # 'n' + 49: 1, # 'o' + 51: 2, # 'r' + 43: 1, # 's' + 44: 1, # 't' + 63: 1, # 'u' + 34: 0, # '\xa0' + 55: 0, # '´' + 48: 0, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 0, # 'Ö°' + 59: 0, # 'Ö±' + 41: 0, # 'Ö²' + 33: 0, # 'Ö´' + 37: 0, # 'Öµ' + 36: 0, # 'Ö¶' + 31: 0, # 'Ö·' + 29: 0, # 'Ö¸' + 35: 0, # 'Ö¹' + 62: 0, # 'Ö»' + 28: 0, # 'Ö¼' + 38: 0, # '×' + 45: 0, # 'ׂ' + 9: 0, # '×' + 8: 0, # 'ב' + 20: 0, # '×’' + 16: 0, # 'ד' + 3: 0, # '×”' + 2: 0, # 'ו' + 24: 0, # '×–' + 14: 0, # '×—' + 22: 0, # 'ט' + 1: 0, # '×™' + 25: 0, # 'ך' + 15: 0, # '×›' + 4: 0, # 'ל' + 11: 0, # '×' + 6: 0, # 'מ' + 23: 0, # 'ן' + 12: 0, # '× ' + 19: 0, # 'ס' + 13: 0, # '×¢' + 26: 0, # '×£' + 18: 0, # 'פ' + 27: 0, # '×¥' + 21: 0, # 'צ' + 17: 0, # 'ק' + 7: 0, # 'ר' + 10: 0, # 'ש' + 5: 0, # 'ת' + 32: 0, # '–' + 52: 1, # '’' + 47: 0, # '“' + 46: 1, # 'â€' + 58: 0, # '†' + 40: 1, # '…' + }, + 51: { # 'r' + 50: 2, # 'a' + 60: 1, # 'c' + 61: 1, # 'd' + 42: 2, # 'e' + 53: 1, # 'i' + 56: 1, # 'l' + 54: 1, # 'n' + 49: 2, # 'o' + 51: 1, # 'r' + 43: 1, # 's' + 44: 1, # 't' + 63: 1, # 'u' + 34: 0, # '\xa0' + 55: 0, # '´' + 48: 0, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 0, # 'Ö°' + 59: 0, # 'Ö±' + 41: 0, # 'Ö²' + 33: 0, # 'Ö´' + 37: 0, # 'Öµ' + 36: 0, # 'Ö¶' + 31: 0, # 'Ö·' + 29: 0, # 'Ö¸' + 35: 0, # 'Ö¹' + 62: 0, # 'Ö»' + 28: 0, # 'Ö¼' + 38: 0, # '×' + 45: 0, # 'ׂ' + 9: 0, # '×' + 8: 0, # 'ב' + 20: 0, # '×’' + 16: 0, # 'ד' + 3: 0, # '×”' + 2: 0, # 'ו' + 24: 0, # '×–' + 14: 0, # '×—' + 22: 0, # 'ט' + 1: 0, # '×™' + 25: 0, # 'ך' + 15: 0, # '×›' + 4: 0, # 'ל' + 11: 0, # '×' + 6: 0, # 'מ' + 23: 0, # 'ן' + 12: 0, # '× ' + 19: 0, # 'ס' + 13: 0, # '×¢' + 26: 0, # '×£' + 18: 0, # 'פ' + 27: 0, # '×¥' + 21: 0, # 'צ' + 17: 0, # 'ק' + 7: 0, # 'ר' + 10: 0, # 'ש' + 5: 0, # 'ת' + 32: 0, # '–' + 52: 2, # '’' + 47: 0, # '“' + 46: 1, # 'â€' + 58: 0, # '†' + 40: 1, # '…' + }, + 43: { # 's' + 50: 1, # 'a' + 60: 1, # 'c' + 61: 0, # 'd' + 42: 2, # 'e' + 53: 1, # 'i' + 56: 1, # 'l' + 54: 1, # 'n' + 49: 1, # 'o' + 51: 1, # 'r' + 43: 1, # 's' + 44: 2, # 't' + 63: 1, # 'u' + 34: 0, # '\xa0' + 55: 0, # '´' + 48: 0, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 0, # 'Ö°' + 59: 0, # 'Ö±' + 41: 0, # 'Ö²' + 33: 0, # 'Ö´' + 37: 0, # 'Öµ' + 36: 0, # 'Ö¶' + 31: 0, # 'Ö·' + 29: 0, # 'Ö¸' + 35: 0, # 'Ö¹' + 62: 0, # 'Ö»' + 28: 0, # 'Ö¼' + 38: 0, # '×' + 45: 0, # 'ׂ' + 9: 0, # '×' + 8: 0, # 'ב' + 20: 0, # '×’' + 16: 0, # 'ד' + 3: 0, # '×”' + 2: 0, # 'ו' + 24: 0, # '×–' + 14: 0, # '×—' + 22: 0, # 'ט' + 1: 0, # '×™' + 25: 0, # 'ך' + 15: 0, # '×›' + 4: 0, # 'ל' + 11: 0, # '×' + 6: 0, # 'מ' + 23: 0, # 'ן' + 12: 0, # '× ' + 19: 0, # 'ס' + 13: 0, # '×¢' + 26: 0, # '×£' + 18: 0, # 'פ' + 27: 0, # '×¥' + 21: 0, # 'צ' + 17: 0, # 'ק' + 7: 0, # 'ר' + 10: 0, # 'ש' + 5: 0, # 'ת' + 32: 0, # '–' + 52: 1, # '’' + 47: 0, # '“' + 46: 2, # 'â€' + 58: 0, # '†' + 40: 2, # '…' + }, + 44: { # 't' + 50: 1, # 'a' + 60: 1, # 'c' + 61: 0, # 'd' + 42: 2, # 'e' + 53: 2, # 'i' + 56: 1, # 'l' + 54: 0, # 'n' + 49: 1, # 'o' + 51: 1, # 'r' + 43: 1, # 's' + 44: 1, # 't' + 63: 1, # 'u' + 34: 1, # '\xa0' + 55: 0, # '´' + 48: 0, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 0, # 'Ö°' + 59: 0, # 'Ö±' + 41: 0, # 'Ö²' + 33: 0, # 'Ö´' + 37: 0, # 'Öµ' + 36: 0, # 'Ö¶' + 31: 0, # 'Ö·' + 29: 0, # 'Ö¸' + 35: 0, # 'Ö¹' + 62: 0, # 'Ö»' + 28: 0, # 'Ö¼' + 38: 0, # '×' + 45: 0, # 'ׂ' + 9: 0, # '×' + 8: 0, # 'ב' + 20: 0, # '×’' + 16: 0, # 'ד' + 3: 0, # '×”' + 2: 0, # 'ו' + 24: 0, # '×–' + 14: 0, # '×—' + 22: 0, # 'ט' + 1: 0, # '×™' + 25: 0, # 'ך' + 15: 0, # '×›' + 4: 0, # 'ל' + 11: 0, # '×' + 6: 0, # 'מ' + 23: 0, # 'ן' + 12: 0, # '× ' + 19: 0, # 'ס' + 13: 0, # '×¢' + 26: 0, # '×£' + 18: 0, # 'פ' + 27: 0, # '×¥' + 21: 0, # 'צ' + 17: 0, # 'ק' + 7: 0, # 'ר' + 10: 0, # 'ש' + 5: 0, # 'ת' + 32: 0, # '–' + 52: 2, # '’' + 47: 0, # '“' + 46: 1, # 'â€' + 58: 0, # '†' + 40: 1, # '…' + }, + 63: { # 'u' + 50: 1, # 'a' + 60: 1, # 'c' + 61: 1, # 'd' + 42: 1, # 'e' + 53: 1, # 'i' + 56: 1, # 'l' + 54: 1, # 'n' + 49: 0, # 'o' + 51: 1, # 'r' + 43: 2, # 's' + 44: 1, # 't' + 63: 0, # 'u' + 34: 0, # '\xa0' + 55: 0, # '´' + 48: 0, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 0, # 'Ö°' + 59: 0, # 'Ö±' + 41: 0, # 'Ö²' + 33: 0, # 'Ö´' + 37: 0, # 'Öµ' + 36: 0, # 'Ö¶' + 31: 0, # 'Ö·' + 29: 0, # 'Ö¸' + 35: 0, # 'Ö¹' + 62: 0, # 'Ö»' + 28: 0, # 'Ö¼' + 38: 0, # '×' + 45: 0, # 'ׂ' + 9: 0, # '×' + 8: 0, # 'ב' + 20: 0, # '×’' + 16: 0, # 'ד' + 3: 0, # '×”' + 2: 0, # 'ו' + 24: 0, # '×–' + 14: 0, # '×—' + 22: 0, # 'ט' + 1: 0, # '×™' + 25: 0, # 'ך' + 15: 0, # '×›' + 4: 0, # 'ל' + 11: 0, # '×' + 6: 0, # 'מ' + 23: 0, # 'ן' + 12: 0, # '× ' + 19: 0, # 'ס' + 13: 0, # '×¢' + 26: 0, # '×£' + 18: 0, # 'פ' + 27: 0, # '×¥' + 21: 0, # 'צ' + 17: 0, # 'ק' + 7: 0, # 'ר' + 10: 0, # 'ש' + 5: 0, # 'ת' + 32: 0, # '–' + 52: 1, # '’' + 47: 0, # '“' + 46: 0, # 'â€' + 58: 0, # '†' + 40: 0, # '…' + }, + 34: { # '\xa0' + 50: 1, # 'a' + 60: 0, # 'c' + 61: 1, # 'd' + 42: 0, # 'e' + 53: 1, # 'i' + 56: 0, # 'l' + 54: 1, # 'n' + 49: 1, # 'o' + 51: 0, # 'r' + 43: 1, # 's' + 44: 1, # 't' + 63: 0, # 'u' + 34: 2, # '\xa0' + 55: 0, # '´' + 48: 0, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 0, # 'Ö°' + 59: 0, # 'Ö±' + 41: 0, # 'Ö²' + 33: 0, # 'Ö´' + 37: 0, # 'Öµ' + 36: 0, # 'Ö¶' + 31: 0, # 'Ö·' + 29: 0, # 'Ö¸' + 35: 0, # 'Ö¹' + 62: 0, # 'Ö»' + 28: 0, # 'Ö¼' + 38: 0, # '×' + 45: 0, # 'ׂ' + 9: 2, # '×' + 8: 1, # 'ב' + 20: 1, # '×’' + 16: 1, # 'ד' + 3: 1, # '×”' + 2: 1, # 'ו' + 24: 1, # '×–' + 14: 1, # '×—' + 22: 1, # 'ט' + 1: 2, # '×™' + 25: 0, # 'ך' + 15: 1, # '×›' + 4: 1, # 'ל' + 11: 0, # '×' + 6: 2, # 'מ' + 23: 0, # 'ן' + 12: 1, # '× ' + 19: 1, # 'ס' + 13: 1, # '×¢' + 26: 0, # '×£' + 18: 1, # 'פ' + 27: 0, # '×¥' + 21: 1, # 'צ' + 17: 1, # 'ק' + 7: 1, # 'ר' + 10: 1, # 'ש' + 5: 1, # 'ת' + 32: 0, # '–' + 52: 0, # '’' + 47: 0, # '“' + 46: 0, # 'â€' + 58: 0, # '†' + 40: 0, # '…' + }, + 55: { # '´' + 50: 0, # 'a' + 60: 0, # 'c' + 61: 0, # 'd' + 42: 0, # 'e' + 53: 0, # 'i' + 56: 0, # 'l' + 54: 0, # 'n' + 49: 0, # 'o' + 51: 0, # 'r' + 43: 1, # 's' + 44: 0, # 't' + 63: 0, # 'u' + 34: 0, # '\xa0' + 55: 0, # '´' + 48: 0, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 0, # 'Ö°' + 59: 0, # 'Ö±' + 41: 0, # 'Ö²' + 33: 0, # 'Ö´' + 37: 0, # 'Öµ' + 36: 0, # 'Ö¶' + 31: 0, # 'Ö·' + 29: 0, # 'Ö¸' + 35: 0, # 'Ö¹' + 62: 0, # 'Ö»' + 28: 0, # 'Ö¼' + 38: 0, # '×' + 45: 0, # 'ׂ' + 9: 1, # '×' + 8: 0, # 'ב' + 20: 0, # '×’' + 16: 0, # 'ד' + 3: 1, # '×”' + 2: 1, # 'ו' + 24: 0, # '×–' + 14: 0, # '×—' + 22: 0, # 'ט' + 1: 2, # '×™' + 25: 0, # 'ך' + 15: 0, # '×›' + 4: 1, # 'ל' + 11: 0, # '×' + 6: 1, # 'מ' + 23: 1, # 'ן' + 12: 1, # '× ' + 19: 1, # 'ס' + 13: 0, # '×¢' + 26: 0, # '×£' + 18: 0, # 'פ' + 27: 0, # '×¥' + 21: 0, # 'צ' + 17: 0, # 'ק' + 7: 1, # 'ר' + 10: 1, # 'ש' + 5: 0, # 'ת' + 32: 0, # '–' + 52: 0, # '’' + 47: 0, # '“' + 46: 0, # 'â€' + 58: 0, # '†' + 40: 0, # '…' + }, + 48: { # '¼' + 50: 0, # 'a' + 60: 0, # 'c' + 61: 0, # 'd' + 42: 0, # 'e' + 53: 0, # 'i' + 56: 0, # 'l' + 54: 0, # 'n' + 49: 0, # 'o' + 51: 0, # 'r' + 43: 0, # 's' + 44: 0, # 't' + 63: 0, # 'u' + 34: 0, # '\xa0' + 55: 0, # '´' + 48: 0, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 0, # 'Ö°' + 59: 0, # 'Ö±' + 41: 0, # 'Ö²' + 33: 0, # 'Ö´' + 37: 0, # 'Öµ' + 36: 0, # 'Ö¶' + 31: 0, # 'Ö·' + 29: 0, # 'Ö¸' + 35: 0, # 'Ö¹' + 62: 0, # 'Ö»' + 28: 0, # 'Ö¼' + 38: 0, # '×' + 45: 0, # 'ׂ' + 9: 1, # '×' + 8: 0, # 'ב' + 20: 0, # '×’' + 16: 0, # 'ד' + 3: 0, # '×”' + 2: 1, # 'ו' + 24: 0, # '×–' + 14: 0, # '×—' + 22: 0, # 'ט' + 1: 0, # '×™' + 25: 0, # 'ך' + 15: 1, # '×›' + 4: 1, # 'ל' + 11: 0, # '×' + 6: 1, # 'מ' + 23: 0, # 'ן' + 12: 0, # '× ' + 19: 0, # 'ס' + 13: 0, # '×¢' + 26: 0, # '×£' + 18: 0, # 'פ' + 27: 0, # '×¥' + 21: 0, # 'צ' + 17: 0, # 'ק' + 7: 0, # 'ר' + 10: 0, # 'ש' + 5: 0, # 'ת' + 32: 0, # '–' + 52: 0, # '’' + 47: 0, # '“' + 46: 0, # 'â€' + 58: 0, # '†' + 40: 0, # '…' + }, + 39: { # '½' + 50: 0, # 'a' + 60: 0, # 'c' + 61: 0, # 'd' + 42: 0, # 'e' + 53: 0, # 'i' + 56: 0, # 'l' + 54: 0, # 'n' + 49: 0, # 'o' + 51: 0, # 'r' + 43: 0, # 's' + 44: 0, # 't' + 63: 0, # 'u' + 34: 0, # '\xa0' + 55: 0, # '´' + 48: 0, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 0, # 'Ö°' + 59: 0, # 'Ö±' + 41: 0, # 'Ö²' + 33: 0, # 'Ö´' + 37: 0, # 'Öµ' + 36: 0, # 'Ö¶' + 31: 0, # 'Ö·' + 29: 0, # 'Ö¸' + 35: 0, # 'Ö¹' + 62: 0, # 'Ö»' + 28: 0, # 'Ö¼' + 38: 0, # '×' + 45: 0, # 'ׂ' + 9: 0, # '×' + 8: 0, # 'ב' + 20: 0, # '×’' + 16: 0, # 'ד' + 3: 0, # '×”' + 2: 0, # 'ו' + 24: 0, # '×–' + 14: 0, # '×—' + 22: 0, # 'ט' + 1: 0, # '×™' + 25: 0, # 'ך' + 15: 1, # '×›' + 4: 1, # 'ל' + 11: 0, # '×' + 6: 0, # 'מ' + 23: 0, # 'ן' + 12: 0, # '× ' + 19: 0, # 'ס' + 13: 0, # '×¢' + 26: 0, # '×£' + 18: 0, # 'פ' + 27: 0, # '×¥' + 21: 1, # 'צ' + 17: 1, # 'ק' + 7: 0, # 'ר' + 10: 0, # 'ש' + 5: 0, # 'ת' + 32: 0, # '–' + 52: 0, # '’' + 47: 0, # '“' + 46: 0, # 'â€' + 58: 0, # '†' + 40: 0, # '…' + }, + 57: { # '¾' + 50: 0, # 'a' + 60: 0, # 'c' + 61: 0, # 'd' + 42: 0, # 'e' + 53: 0, # 'i' + 56: 0, # 'l' + 54: 0, # 'n' + 49: 0, # 'o' + 51: 0, # 'r' + 43: 0, # 's' + 44: 0, # 't' + 63: 0, # 'u' + 34: 0, # '\xa0' + 55: 0, # '´' + 48: 0, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 0, # 'Ö°' + 59: 0, # 'Ö±' + 41: 0, # 'Ö²' + 33: 0, # 'Ö´' + 37: 0, # 'Öµ' + 36: 0, # 'Ö¶' + 31: 0, # 'Ö·' + 29: 0, # 'Ö¸' + 35: 0, # 'Ö¹' + 62: 0, # 'Ö»' + 28: 0, # 'Ö¼' + 38: 0, # '×' + 45: 0, # 'ׂ' + 9: 0, # '×' + 8: 0, # 'ב' + 20: 0, # '×’' + 16: 0, # 'ד' + 3: 0, # '×”' + 2: 0, # 'ו' + 24: 0, # '×–' + 14: 0, # '×—' + 22: 0, # 'ט' + 1: 0, # '×™' + 25: 0, # 'ך' + 15: 0, # '×›' + 4: 0, # 'ל' + 11: 0, # '×' + 6: 0, # 'מ' + 23: 0, # 'ן' + 12: 0, # '× ' + 19: 0, # 'ס' + 13: 0, # '×¢' + 26: 0, # '×£' + 18: 0, # 'פ' + 27: 0, # '×¥' + 21: 0, # 'צ' + 17: 0, # 'ק' + 7: 0, # 'ר' + 10: 0, # 'ש' + 5: 0, # 'ת' + 32: 0, # '–' + 52: 0, # '’' + 47: 0, # '“' + 46: 0, # 'â€' + 58: 0, # '†' + 40: 0, # '…' + }, + 30: { # 'Ö°' + 50: 0, # 'a' + 60: 0, # 'c' + 61: 0, # 'd' + 42: 0, # 'e' + 53: 0, # 'i' + 56: 0, # 'l' + 54: 0, # 'n' + 49: 0, # 'o' + 51: 0, # 'r' + 43: 0, # 's' + 44: 0, # 't' + 63: 0, # 'u' + 34: 0, # '\xa0' + 55: 0, # '´' + 48: 0, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 0, # 'Ö°' + 59: 0, # 'Ö±' + 41: 0, # 'Ö²' + 33: 0, # 'Ö´' + 37: 0, # 'Öµ' + 36: 1, # 'Ö¶' + 31: 0, # 'Ö·' + 29: 0, # 'Ö¸' + 35: 1, # 'Ö¹' + 62: 0, # 'Ö»' + 28: 0, # 'Ö¼' + 38: 0, # '×' + 45: 0, # 'ׂ' + 9: 2, # '×' + 8: 2, # 'ב' + 20: 2, # '×’' + 16: 2, # 'ד' + 3: 2, # '×”' + 2: 2, # 'ו' + 24: 2, # '×–' + 14: 2, # '×—' + 22: 2, # 'ט' + 1: 2, # '×™' + 25: 2, # 'ך' + 15: 2, # '×›' + 4: 2, # 'ל' + 11: 1, # '×' + 6: 2, # 'מ' + 23: 0, # 'ן' + 12: 2, # '× ' + 19: 2, # 'ס' + 13: 2, # '×¢' + 26: 0, # '×£' + 18: 2, # 'פ' + 27: 0, # '×¥' + 21: 2, # 'צ' + 17: 2, # 'ק' + 7: 2, # 'ר' + 10: 2, # 'ש' + 5: 2, # 'ת' + 32: 0, # '–' + 52: 0, # '’' + 47: 0, # '“' + 46: 0, # 'â€' + 58: 0, # '†' + 40: 0, # '…' + }, + 59: { # 'Ö±' + 50: 0, # 'a' + 60: 0, # 'c' + 61: 0, # 'd' + 42: 0, # 'e' + 53: 0, # 'i' + 56: 0, # 'l' + 54: 0, # 'n' + 49: 0, # 'o' + 51: 0, # 'r' + 43: 0, # 's' + 44: 0, # 't' + 63: 0, # 'u' + 34: 0, # '\xa0' + 55: 0, # '´' + 48: 0, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 1, # 'Ö°' + 59: 0, # 'Ö±' + 41: 0, # 'Ö²' + 33: 0, # 'Ö´' + 37: 0, # 'Öµ' + 36: 0, # 'Ö¶' + 31: 0, # 'Ö·' + 29: 0, # 'Ö¸' + 35: 0, # 'Ö¹' + 62: 0, # 'Ö»' + 28: 0, # 'Ö¼' + 38: 0, # '×' + 45: 0, # 'ׂ' + 9: 0, # '×' + 8: 1, # 'ב' + 20: 1, # '×’' + 16: 0, # 'ד' + 3: 0, # '×”' + 2: 0, # 'ו' + 24: 1, # '×–' + 14: 0, # '×—' + 22: 0, # 'ט' + 1: 1, # '×™' + 25: 0, # 'ך' + 15: 1, # '×›' + 4: 2, # 'ל' + 11: 0, # '×' + 6: 2, # 'מ' + 23: 0, # 'ן' + 12: 1, # '× ' + 19: 0, # 'ס' + 13: 0, # '×¢' + 26: 0, # '×£' + 18: 0, # 'פ' + 27: 0, # '×¥' + 21: 0, # 'צ' + 17: 0, # 'ק' + 7: 1, # 'ר' + 10: 1, # 'ש' + 5: 0, # 'ת' + 32: 0, # '–' + 52: 0, # '’' + 47: 0, # '“' + 46: 0, # 'â€' + 58: 0, # '†' + 40: 0, # '…' + }, + 41: { # 'Ö²' + 50: 0, # 'a' + 60: 0, # 'c' + 61: 0, # 'd' + 42: 0, # 'e' + 53: 0, # 'i' + 56: 0, # 'l' + 54: 0, # 'n' + 49: 0, # 'o' + 51: 0, # 'r' + 43: 0, # 's' + 44: 0, # 't' + 63: 0, # 'u' + 34: 0, # '\xa0' + 55: 0, # '´' + 48: 0, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 0, # 'Ö°' + 59: 0, # 'Ö±' + 41: 0, # 'Ö²' + 33: 0, # 'Ö´' + 37: 0, # 'Öµ' + 36: 0, # 'Ö¶' + 31: 0, # 'Ö·' + 29: 0, # 'Ö¸' + 35: 0, # 'Ö¹' + 62: 0, # 'Ö»' + 28: 0, # 'Ö¼' + 38: 0, # '×' + 45: 0, # 'ׂ' + 9: 0, # '×' + 8: 2, # 'ב' + 20: 1, # '×’' + 16: 2, # 'ד' + 3: 1, # '×”' + 2: 1, # 'ו' + 24: 1, # '×–' + 14: 1, # '×—' + 22: 1, # 'ט' + 1: 1, # '×™' + 25: 1, # 'ך' + 15: 1, # '×›' + 4: 2, # 'ל' + 11: 0, # '×' + 6: 2, # 'מ' + 23: 0, # 'ן' + 12: 2, # '× ' + 19: 1, # 'ס' + 13: 0, # '×¢' + 26: 0, # '×£' + 18: 1, # 'פ' + 27: 0, # '×¥' + 21: 2, # 'צ' + 17: 1, # 'ק' + 7: 2, # 'ר' + 10: 2, # 'ש' + 5: 1, # 'ת' + 32: 0, # '–' + 52: 0, # '’' + 47: 0, # '“' + 46: 0, # 'â€' + 58: 0, # '†' + 40: 0, # '…' + }, + 33: { # 'Ö´' + 50: 0, # 'a' + 60: 0, # 'c' + 61: 0, # 'd' + 42: 0, # 'e' + 53: 0, # 'i' + 56: 0, # 'l' + 54: 0, # 'n' + 49: 0, # 'o' + 51: 0, # 'r' + 43: 0, # 's' + 44: 0, # 't' + 63: 0, # 'u' + 34: 0, # '\xa0' + 55: 0, # '´' + 48: 0, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 1, # 'Ö°' + 59: 0, # 'Ö±' + 41: 0, # 'Ö²' + 33: 1, # 'Ö´' + 37: 0, # 'Öµ' + 36: 1, # 'Ö¶' + 31: 0, # 'Ö·' + 29: 1, # 'Ö¸' + 35: 0, # 'Ö¹' + 62: 0, # 'Ö»' + 28: 1, # 'Ö¼' + 38: 0, # '×' + 45: 0, # 'ׂ' + 9: 1, # '×' + 8: 2, # 'ב' + 20: 2, # '×’' + 16: 2, # 'ד' + 3: 1, # '×”' + 2: 1, # 'ו' + 24: 2, # '×–' + 14: 1, # '×—' + 22: 1, # 'ט' + 1: 3, # '×™' + 25: 1, # 'ך' + 15: 2, # '×›' + 4: 2, # 'ל' + 11: 2, # '×' + 6: 2, # 'מ' + 23: 2, # 'ן' + 12: 2, # '× ' + 19: 2, # 'ס' + 13: 1, # '×¢' + 26: 0, # '×£' + 18: 2, # 'פ' + 27: 1, # '×¥' + 21: 2, # 'צ' + 17: 2, # 'ק' + 7: 2, # 'ר' + 10: 2, # 'ש' + 5: 2, # 'ת' + 32: 0, # '–' + 52: 0, # '’' + 47: 0, # '“' + 46: 0, # 'â€' + 58: 0, # '†' + 40: 0, # '…' + }, + 37: { # 'Öµ' + 50: 0, # 'a' + 60: 0, # 'c' + 61: 0, # 'd' + 42: 0, # 'e' + 53: 0, # 'i' + 56: 0, # 'l' + 54: 0, # 'n' + 49: 0, # 'o' + 51: 0, # 'r' + 43: 0, # 's' + 44: 0, # 't' + 63: 0, # 'u' + 34: 0, # '\xa0' + 55: 0, # '´' + 48: 0, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 0, # 'Ö°' + 59: 0, # 'Ö±' + 41: 0, # 'Ö²' + 33: 0, # 'Ö´' + 37: 0, # 'Öµ' + 36: 1, # 'Ö¶' + 31: 1, # 'Ö·' + 29: 1, # 'Ö¸' + 35: 0, # 'Ö¹' + 62: 0, # 'Ö»' + 28: 0, # 'Ö¼' + 38: 0, # '×' + 45: 0, # 'ׂ' + 9: 2, # '×' + 8: 2, # 'ב' + 20: 1, # '×’' + 16: 2, # 'ד' + 3: 2, # '×”' + 2: 1, # 'ו' + 24: 1, # '×–' + 14: 2, # '×—' + 22: 1, # 'ט' + 1: 3, # '×™' + 25: 2, # 'ך' + 15: 1, # '×›' + 4: 2, # 'ל' + 11: 2, # '×' + 6: 1, # 'מ' + 23: 2, # 'ן' + 12: 2, # '× ' + 19: 1, # 'ס' + 13: 2, # '×¢' + 26: 1, # '×£' + 18: 1, # 'פ' + 27: 1, # '×¥' + 21: 1, # 'צ' + 17: 1, # 'ק' + 7: 2, # 'ר' + 10: 2, # 'ש' + 5: 2, # 'ת' + 32: 0, # '–' + 52: 0, # '’' + 47: 0, # '“' + 46: 0, # 'â€' + 58: 0, # '†' + 40: 0, # '…' + }, + 36: { # 'Ö¶' + 50: 0, # 'a' + 60: 0, # 'c' + 61: 0, # 'd' + 42: 0, # 'e' + 53: 0, # 'i' + 56: 0, # 'l' + 54: 0, # 'n' + 49: 0, # 'o' + 51: 0, # 'r' + 43: 0, # 's' + 44: 0, # 't' + 63: 0, # 'u' + 34: 0, # '\xa0' + 55: 0, # '´' + 48: 0, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 0, # 'Ö°' + 59: 0, # 'Ö±' + 41: 0, # 'Ö²' + 33: 0, # 'Ö´' + 37: 0, # 'Öµ' + 36: 1, # 'Ö¶' + 31: 1, # 'Ö·' + 29: 1, # 'Ö¸' + 35: 0, # 'Ö¹' + 62: 0, # 'Ö»' + 28: 0, # 'Ö¼' + 38: 0, # '×' + 45: 0, # 'ׂ' + 9: 2, # '×' + 8: 2, # 'ב' + 20: 1, # '×’' + 16: 2, # 'ד' + 3: 2, # '×”' + 2: 1, # 'ו' + 24: 1, # '×–' + 14: 2, # '×—' + 22: 1, # 'ט' + 1: 2, # '×™' + 25: 2, # 'ך' + 15: 1, # '×›' + 4: 2, # 'ל' + 11: 2, # '×' + 6: 2, # 'מ' + 23: 2, # 'ן' + 12: 2, # '× ' + 19: 2, # 'ס' + 13: 1, # '×¢' + 26: 1, # '×£' + 18: 1, # 'פ' + 27: 2, # '×¥' + 21: 1, # 'צ' + 17: 1, # 'ק' + 7: 2, # 'ר' + 10: 2, # 'ש' + 5: 2, # 'ת' + 32: 0, # '–' + 52: 0, # '’' + 47: 0, # '“' + 46: 0, # 'â€' + 58: 0, # '†' + 40: 0, # '…' + }, + 31: { # 'Ö·' + 50: 0, # 'a' + 60: 0, # 'c' + 61: 0, # 'd' + 42: 0, # 'e' + 53: 0, # 'i' + 56: 0, # 'l' + 54: 0, # 'n' + 49: 0, # 'o' + 51: 0, # 'r' + 43: 0, # 's' + 44: 0, # 't' + 63: 0, # 'u' + 34: 0, # '\xa0' + 55: 0, # '´' + 48: 0, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 1, # 'Ö°' + 59: 0, # 'Ö±' + 41: 0, # 'Ö²' + 33: 0, # 'Ö´' + 37: 0, # 'Öµ' + 36: 1, # 'Ö¶' + 31: 0, # 'Ö·' + 29: 2, # 'Ö¸' + 35: 0, # 'Ö¹' + 62: 0, # 'Ö»' + 28: 0, # 'Ö¼' + 38: 0, # '×' + 45: 0, # 'ׂ' + 9: 2, # '×' + 8: 2, # 'ב' + 20: 2, # '×’' + 16: 2, # 'ד' + 3: 2, # '×”' + 2: 1, # 'ו' + 24: 2, # '×–' + 14: 2, # '×—' + 22: 2, # 'ט' + 1: 3, # '×™' + 25: 1, # 'ך' + 15: 2, # '×›' + 4: 2, # 'ל' + 11: 2, # '×' + 6: 2, # 'מ' + 23: 2, # 'ן' + 12: 2, # '× ' + 19: 2, # 'ס' + 13: 2, # '×¢' + 26: 2, # '×£' + 18: 2, # 'פ' + 27: 1, # '×¥' + 21: 2, # 'צ' + 17: 2, # 'ק' + 7: 2, # 'ר' + 10: 2, # 'ש' + 5: 2, # 'ת' + 32: 0, # '–' + 52: 0, # '’' + 47: 0, # '“' + 46: 0, # 'â€' + 58: 0, # '†' + 40: 0, # '…' + }, + 29: { # 'Ö¸' + 50: 0, # 'a' + 60: 0, # 'c' + 61: 0, # 'd' + 42: 0, # 'e' + 53: 0, # 'i' + 56: 0, # 'l' + 54: 0, # 'n' + 49: 0, # 'o' + 51: 0, # 'r' + 43: 0, # 's' + 44: 0, # 't' + 63: 0, # 'u' + 34: 0, # '\xa0' + 55: 0, # '´' + 48: 0, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 0, # 'Ö°' + 59: 0, # 'Ö±' + 41: 0, # 'Ö²' + 33: 0, # 'Ö´' + 37: 0, # 'Öµ' + 36: 0, # 'Ö¶' + 31: 1, # 'Ö·' + 29: 2, # 'Ö¸' + 35: 0, # 'Ö¹' + 62: 0, # 'Ö»' + 28: 1, # 'Ö¼' + 38: 0, # '×' + 45: 0, # 'ׂ' + 9: 2, # '×' + 8: 2, # 'ב' + 20: 2, # '×’' + 16: 2, # 'ד' + 3: 3, # '×”' + 2: 2, # 'ו' + 24: 2, # '×–' + 14: 2, # '×—' + 22: 1, # 'ט' + 1: 2, # '×™' + 25: 2, # 'ך' + 15: 2, # '×›' + 4: 2, # 'ל' + 11: 2, # '×' + 6: 2, # 'מ' + 23: 2, # 'ן' + 12: 2, # '× ' + 19: 1, # 'ס' + 13: 2, # '×¢' + 26: 1, # '×£' + 18: 2, # 'פ' + 27: 1, # '×¥' + 21: 2, # 'צ' + 17: 2, # 'ק' + 7: 2, # 'ר' + 10: 2, # 'ש' + 5: 2, # 'ת' + 32: 0, # '–' + 52: 0, # '’' + 47: 0, # '“' + 46: 0, # 'â€' + 58: 0, # '†' + 40: 0, # '…' + }, + 35: { # 'Ö¹' + 50: 0, # 'a' + 60: 0, # 'c' + 61: 0, # 'd' + 42: 0, # 'e' + 53: 0, # 'i' + 56: 0, # 'l' + 54: 0, # 'n' + 49: 0, # 'o' + 51: 0, # 'r' + 43: 0, # 's' + 44: 0, # 't' + 63: 0, # 'u' + 34: 0, # '\xa0' + 55: 0, # '´' + 48: 0, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 0, # 'Ö°' + 59: 0, # 'Ö±' + 41: 0, # 'Ö²' + 33: 0, # 'Ö´' + 37: 0, # 'Öµ' + 36: 0, # 'Ö¶' + 31: 0, # 'Ö·' + 29: 0, # 'Ö¸' + 35: 1, # 'Ö¹' + 62: 0, # 'Ö»' + 28: 0, # 'Ö¼' + 38: 0, # '×' + 45: 0, # 'ׂ' + 9: 2, # '×' + 8: 2, # 'ב' + 20: 1, # '×’' + 16: 2, # 'ד' + 3: 2, # '×”' + 2: 1, # 'ו' + 24: 1, # '×–' + 14: 1, # '×—' + 22: 1, # 'ט' + 1: 1, # '×™' + 25: 1, # 'ך' + 15: 2, # '×›' + 4: 2, # 'ל' + 11: 2, # '×' + 6: 2, # 'מ' + 23: 2, # 'ן' + 12: 2, # '× ' + 19: 2, # 'ס' + 13: 2, # '×¢' + 26: 1, # '×£' + 18: 2, # 'פ' + 27: 1, # '×¥' + 21: 2, # 'צ' + 17: 2, # 'ק' + 7: 2, # 'ר' + 10: 2, # 'ש' + 5: 2, # 'ת' + 32: 0, # '–' + 52: 0, # '’' + 47: 0, # '“' + 46: 0, # 'â€' + 58: 0, # '†' + 40: 0, # '…' + }, + 62: { # 'Ö»' + 50: 0, # 'a' + 60: 0, # 'c' + 61: 0, # 'd' + 42: 0, # 'e' + 53: 0, # 'i' + 56: 0, # 'l' + 54: 0, # 'n' + 49: 0, # 'o' + 51: 0, # 'r' + 43: 0, # 's' + 44: 0, # 't' + 63: 0, # 'u' + 34: 0, # '\xa0' + 55: 0, # '´' + 48: 0, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 0, # 'Ö°' + 59: 0, # 'Ö±' + 41: 0, # 'Ö²' + 33: 0, # 'Ö´' + 37: 0, # 'Öµ' + 36: 0, # 'Ö¶' + 31: 0, # 'Ö·' + 29: 0, # 'Ö¸' + 35: 0, # 'Ö¹' + 62: 0, # 'Ö»' + 28: 0, # 'Ö¼' + 38: 0, # '×' + 45: 0, # 'ׂ' + 9: 0, # '×' + 8: 1, # 'ב' + 20: 1, # '×’' + 16: 1, # 'ד' + 3: 1, # '×”' + 2: 1, # 'ו' + 24: 1, # '×–' + 14: 1, # '×—' + 22: 0, # 'ט' + 1: 1, # '×™' + 25: 0, # 'ך' + 15: 1, # '×›' + 4: 2, # 'ל' + 11: 1, # '×' + 6: 1, # 'מ' + 23: 1, # 'ן' + 12: 1, # '× ' + 19: 1, # 'ס' + 13: 1, # '×¢' + 26: 0, # '×£' + 18: 1, # 'פ' + 27: 0, # '×¥' + 21: 1, # 'צ' + 17: 1, # 'ק' + 7: 1, # 'ר' + 10: 1, # 'ש' + 5: 1, # 'ת' + 32: 0, # '–' + 52: 0, # '’' + 47: 0, # '“' + 46: 0, # 'â€' + 58: 0, # '†' + 40: 0, # '…' + }, + 28: { # 'Ö¼' + 50: 0, # 'a' + 60: 0, # 'c' + 61: 0, # 'd' + 42: 0, # 'e' + 53: 0, # 'i' + 56: 0, # 'l' + 54: 0, # 'n' + 49: 0, # 'o' + 51: 0, # 'r' + 43: 0, # 's' + 44: 0, # 't' + 63: 0, # 'u' + 34: 0, # '\xa0' + 55: 0, # '´' + 48: 0, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 3, # 'Ö°' + 59: 0, # 'Ö±' + 41: 1, # 'Ö²' + 33: 3, # 'Ö´' + 37: 2, # 'Öµ' + 36: 2, # 'Ö¶' + 31: 3, # 'Ö·' + 29: 3, # 'Ö¸' + 35: 2, # 'Ö¹' + 62: 1, # 'Ö»' + 28: 0, # 'Ö¼' + 38: 2, # '×' + 45: 1, # 'ׂ' + 9: 2, # '×' + 8: 2, # 'ב' + 20: 1, # '×’' + 16: 2, # 'ד' + 3: 1, # '×”' + 2: 2, # 'ו' + 24: 1, # '×–' + 14: 1, # '×—' + 22: 1, # 'ט' + 1: 2, # '×™' + 25: 2, # 'ך' + 15: 2, # '×›' + 4: 2, # 'ל' + 11: 1, # '×' + 6: 2, # 'מ' + 23: 1, # 'ן' + 12: 2, # '× ' + 19: 1, # 'ס' + 13: 2, # '×¢' + 26: 1, # '×£' + 18: 1, # 'פ' + 27: 1, # '×¥' + 21: 1, # 'צ' + 17: 1, # 'ק' + 7: 2, # 'ר' + 10: 2, # 'ש' + 5: 2, # 'ת' + 32: 0, # '–' + 52: 0, # '’' + 47: 0, # '“' + 46: 0, # 'â€' + 58: 0, # '†' + 40: 0, # '…' + }, + 38: { # '×' + 50: 0, # 'a' + 60: 0, # 'c' + 61: 0, # 'd' + 42: 0, # 'e' + 53: 0, # 'i' + 56: 0, # 'l' + 54: 0, # 'n' + 49: 0, # 'o' + 51: 0, # 'r' + 43: 0, # 's' + 44: 0, # 't' + 63: 0, # 'u' + 34: 0, # '\xa0' + 55: 0, # '´' + 48: 0, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 2, # 'Ö°' + 59: 0, # 'Ö±' + 41: 0, # 'Ö²' + 33: 2, # 'Ö´' + 37: 2, # 'Öµ' + 36: 2, # 'Ö¶' + 31: 2, # 'Ö·' + 29: 2, # 'Ö¸' + 35: 1, # 'Ö¹' + 62: 1, # 'Ö»' + 28: 0, # 'Ö¼' + 38: 0, # '×' + 45: 0, # 'ׂ' + 9: 0, # '×' + 8: 0, # 'ב' + 20: 0, # '×’' + 16: 0, # 'ד' + 3: 0, # '×”' + 2: 2, # 'ו' + 24: 0, # '×–' + 14: 0, # '×—' + 22: 0, # 'ט' + 1: 1, # '×™' + 25: 0, # 'ך' + 15: 0, # '×›' + 4: 0, # 'ל' + 11: 0, # '×' + 6: 0, # 'מ' + 23: 0, # 'ן' + 12: 0, # '× ' + 19: 0, # 'ס' + 13: 1, # '×¢' + 26: 0, # '×£' + 18: 0, # 'פ' + 27: 0, # '×¥' + 21: 0, # 'צ' + 17: 0, # 'ק' + 7: 0, # 'ר' + 10: 0, # 'ש' + 5: 0, # 'ת' + 32: 0, # '–' + 52: 0, # '’' + 47: 0, # '“' + 46: 0, # 'â€' + 58: 0, # '†' + 40: 0, # '…' + }, + 45: { # 'ׂ' + 50: 0, # 'a' + 60: 0, # 'c' + 61: 0, # 'd' + 42: 0, # 'e' + 53: 0, # 'i' + 56: 0, # 'l' + 54: 0, # 'n' + 49: 0, # 'o' + 51: 0, # 'r' + 43: 0, # 's' + 44: 0, # 't' + 63: 0, # 'u' + 34: 0, # '\xa0' + 55: 0, # '´' + 48: 0, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 2, # 'Ö°' + 59: 0, # 'Ö±' + 41: 0, # 'Ö²' + 33: 2, # 'Ö´' + 37: 1, # 'Öµ' + 36: 2, # 'Ö¶' + 31: 1, # 'Ö·' + 29: 2, # 'Ö¸' + 35: 1, # 'Ö¹' + 62: 0, # 'Ö»' + 28: 0, # 'Ö¼' + 38: 0, # '×' + 45: 0, # 'ׂ' + 9: 1, # '×' + 8: 0, # 'ב' + 20: 1, # '×’' + 16: 0, # 'ד' + 3: 1, # '×”' + 2: 2, # 'ו' + 24: 0, # '×–' + 14: 1, # '×—' + 22: 0, # 'ט' + 1: 1, # '×™' + 25: 0, # 'ך' + 15: 0, # '×›' + 4: 0, # 'ל' + 11: 1, # '×' + 6: 1, # 'מ' + 23: 0, # 'ן' + 12: 1, # '× ' + 19: 0, # 'ס' + 13: 1, # '×¢' + 26: 0, # '×£' + 18: 1, # 'פ' + 27: 0, # '×¥' + 21: 0, # 'צ' + 17: 0, # 'ק' + 7: 1, # 'ר' + 10: 0, # 'ש' + 5: 1, # 'ת' + 32: 0, # '–' + 52: 0, # '’' + 47: 0, # '“' + 46: 0, # 'â€' + 58: 0, # '†' + 40: 0, # '…' + }, + 9: { # '×' + 50: 0, # 'a' + 60: 0, # 'c' + 61: 0, # 'd' + 42: 0, # 'e' + 53: 0, # 'i' + 56: 0, # 'l' + 54: 0, # 'n' + 49: 0, # 'o' + 51: 0, # 'r' + 43: 0, # 's' + 44: 0, # 't' + 63: 0, # 'u' + 34: 1, # '\xa0' + 55: 1, # '´' + 48: 1, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 0, # 'Ö°' + 59: 2, # 'Ö±' + 41: 2, # 'Ö²' + 33: 2, # 'Ö´' + 37: 2, # 'Öµ' + 36: 2, # 'Ö¶' + 31: 2, # 'Ö·' + 29: 2, # 'Ö¸' + 35: 2, # 'Ö¹' + 62: 1, # 'Ö»' + 28: 0, # 'Ö¼' + 38: 0, # '×' + 45: 0, # 'ׂ' + 9: 2, # '×' + 8: 3, # 'ב' + 20: 3, # '×’' + 16: 3, # 'ד' + 3: 3, # '×”' + 2: 3, # 'ו' + 24: 3, # '×–' + 14: 3, # '×—' + 22: 3, # 'ט' + 1: 3, # '×™' + 25: 3, # 'ך' + 15: 3, # '×›' + 4: 3, # 'ל' + 11: 3, # '×' + 6: 3, # 'מ' + 23: 3, # 'ן' + 12: 3, # '× ' + 19: 3, # 'ס' + 13: 2, # '×¢' + 26: 3, # '×£' + 18: 3, # 'פ' + 27: 1, # '×¥' + 21: 3, # 'צ' + 17: 3, # 'ק' + 7: 3, # 'ר' + 10: 3, # 'ש' + 5: 3, # 'ת' + 32: 0, # '–' + 52: 0, # '’' + 47: 0, # '“' + 46: 1, # 'â€' + 58: 0, # '†' + 40: 1, # '…' + }, + 8: { # 'ב' + 50: 0, # 'a' + 60: 0, # 'c' + 61: 1, # 'd' + 42: 0, # 'e' + 53: 0, # 'i' + 56: 0, # 'l' + 54: 0, # 'n' + 49: 0, # 'o' + 51: 0, # 'r' + 43: 0, # 's' + 44: 0, # 't' + 63: 0, # 'u' + 34: 1, # '\xa0' + 55: 1, # '´' + 48: 0, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 2, # 'Ö°' + 59: 0, # 'Ö±' + 41: 0, # 'Ö²' + 33: 2, # 'Ö´' + 37: 2, # 'Öµ' + 36: 2, # 'Ö¶' + 31: 2, # 'Ö·' + 29: 2, # 'Ö¸' + 35: 2, # 'Ö¹' + 62: 1, # 'Ö»' + 28: 3, # 'Ö¼' + 38: 0, # '×' + 45: 0, # 'ׂ' + 9: 3, # '×' + 8: 3, # 'ב' + 20: 3, # '×’' + 16: 3, # 'ד' + 3: 3, # '×”' + 2: 3, # 'ו' + 24: 3, # '×–' + 14: 3, # '×—' + 22: 3, # 'ט' + 1: 3, # '×™' + 25: 2, # 'ך' + 15: 3, # '×›' + 4: 3, # 'ל' + 11: 2, # '×' + 6: 3, # 'מ' + 23: 3, # 'ן' + 12: 3, # '× ' + 19: 3, # 'ס' + 13: 3, # '×¢' + 26: 1, # '×£' + 18: 3, # 'פ' + 27: 2, # '×¥' + 21: 3, # 'צ' + 17: 3, # 'ק' + 7: 3, # 'ר' + 10: 3, # 'ש' + 5: 3, # 'ת' + 32: 1, # '–' + 52: 0, # '’' + 47: 0, # '“' + 46: 1, # 'â€' + 58: 0, # '†' + 40: 1, # '…' + }, + 20: { # '×’' + 50: 0, # 'a' + 60: 0, # 'c' + 61: 0, # 'd' + 42: 0, # 'e' + 53: 0, # 'i' + 56: 0, # 'l' + 54: 0, # 'n' + 49: 0, # 'o' + 51: 0, # 'r' + 43: 0, # 's' + 44: 0, # 't' + 63: 0, # 'u' + 34: 1, # '\xa0' + 55: 2, # '´' + 48: 0, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 2, # 'Ö°' + 59: 0, # 'Ö±' + 41: 0, # 'Ö²' + 33: 1, # 'Ö´' + 37: 1, # 'Öµ' + 36: 1, # 'Ö¶' + 31: 2, # 'Ö·' + 29: 2, # 'Ö¸' + 35: 1, # 'Ö¹' + 62: 0, # 'Ö»' + 28: 2, # 'Ö¼' + 38: 0, # '×' + 45: 0, # 'ׂ' + 9: 2, # '×' + 8: 3, # 'ב' + 20: 2, # '×’' + 16: 3, # 'ד' + 3: 3, # '×”' + 2: 3, # 'ו' + 24: 3, # '×–' + 14: 2, # '×—' + 22: 2, # 'ט' + 1: 3, # '×™' + 25: 1, # 'ך' + 15: 1, # '×›' + 4: 3, # 'ל' + 11: 3, # '×' + 6: 3, # 'מ' + 23: 3, # 'ן' + 12: 3, # '× ' + 19: 2, # 'ס' + 13: 3, # '×¢' + 26: 2, # '×£' + 18: 2, # 'פ' + 27: 1, # '×¥' + 21: 1, # 'צ' + 17: 1, # 'ק' + 7: 3, # 'ר' + 10: 3, # 'ש' + 5: 3, # 'ת' + 32: 0, # '–' + 52: 1, # '’' + 47: 0, # '“' + 46: 1, # 'â€' + 58: 0, # '†' + 40: 0, # '…' + }, + 16: { # 'ד' + 50: 0, # 'a' + 60: 0, # 'c' + 61: 0, # 'd' + 42: 0, # 'e' + 53: 0, # 'i' + 56: 0, # 'l' + 54: 0, # 'n' + 49: 0, # 'o' + 51: 0, # 'r' + 43: 0, # 's' + 44: 0, # 't' + 63: 0, # 'u' + 34: 0, # '\xa0' + 55: 0, # '´' + 48: 0, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 2, # 'Ö°' + 59: 0, # 'Ö±' + 41: 0, # 'Ö²' + 33: 2, # 'Ö´' + 37: 2, # 'Öµ' + 36: 2, # 'Ö¶' + 31: 2, # 'Ö·' + 29: 2, # 'Ö¸' + 35: 2, # 'Ö¹' + 62: 1, # 'Ö»' + 28: 2, # 'Ö¼' + 38: 0, # '×' + 45: 0, # 'ׂ' + 9: 3, # '×' + 8: 3, # 'ב' + 20: 3, # '×’' + 16: 3, # 'ד' + 3: 3, # '×”' + 2: 3, # 'ו' + 24: 1, # '×–' + 14: 2, # '×—' + 22: 2, # 'ט' + 1: 3, # '×™' + 25: 2, # 'ך' + 15: 2, # '×›' + 4: 3, # 'ל' + 11: 3, # '×' + 6: 3, # 'מ' + 23: 2, # 'ן' + 12: 3, # '× ' + 19: 2, # 'ס' + 13: 3, # '×¢' + 26: 2, # '×£' + 18: 3, # 'פ' + 27: 0, # '×¥' + 21: 2, # 'צ' + 17: 3, # 'ק' + 7: 3, # 'ר' + 10: 3, # 'ש' + 5: 3, # 'ת' + 32: 0, # '–' + 52: 0, # '’' + 47: 0, # '“' + 46: 1, # 'â€' + 58: 0, # '†' + 40: 1, # '…' + }, + 3: { # '×”' + 50: 0, # 'a' + 60: 0, # 'c' + 61: 1, # 'd' + 42: 0, # 'e' + 53: 0, # 'i' + 56: 0, # 'l' + 54: 0, # 'n' + 49: 0, # 'o' + 51: 0, # 'r' + 43: 0, # 's' + 44: 0, # 't' + 63: 0, # 'u' + 34: 1, # '\xa0' + 55: 0, # '´' + 48: 1, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 1, # 'Ö°' + 59: 1, # 'Ö±' + 41: 2, # 'Ö²' + 33: 2, # 'Ö´' + 37: 2, # 'Öµ' + 36: 2, # 'Ö¶' + 31: 3, # 'Ö·' + 29: 2, # 'Ö¸' + 35: 1, # 'Ö¹' + 62: 1, # 'Ö»' + 28: 2, # 'Ö¼' + 38: 0, # '×' + 45: 0, # 'ׂ' + 9: 3, # '×' + 8: 3, # 'ב' + 20: 3, # '×’' + 16: 3, # 'ד' + 3: 3, # '×”' + 2: 3, # 'ו' + 24: 3, # '×–' + 14: 3, # '×—' + 22: 3, # 'ט' + 1: 3, # '×™' + 25: 1, # 'ך' + 15: 3, # '×›' + 4: 3, # 'ל' + 11: 3, # '×' + 6: 3, # 'מ' + 23: 3, # 'ן' + 12: 3, # '× ' + 19: 3, # 'ס' + 13: 3, # '×¢' + 26: 0, # '×£' + 18: 3, # 'פ' + 27: 1, # '×¥' + 21: 3, # 'צ' + 17: 3, # 'ק' + 7: 3, # 'ר' + 10: 3, # 'ש' + 5: 3, # 'ת' + 32: 1, # '–' + 52: 1, # '’' + 47: 0, # '“' + 46: 1, # 'â€' + 58: 0, # '†' + 40: 2, # '…' + }, + 2: { # 'ו' + 50: 0, # 'a' + 60: 0, # 'c' + 61: 0, # 'd' + 42: 0, # 'e' + 53: 0, # 'i' + 56: 0, # 'l' + 54: 0, # 'n' + 49: 0, # 'o' + 51: 0, # 'r' + 43: 0, # 's' + 44: 1, # 't' + 63: 0, # 'u' + 34: 1, # '\xa0' + 55: 1, # '´' + 48: 1, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 2, # 'Ö°' + 59: 0, # 'Ö±' + 41: 0, # 'Ö²' + 33: 2, # 'Ö´' + 37: 1, # 'Öµ' + 36: 1, # 'Ö¶' + 31: 2, # 'Ö·' + 29: 2, # 'Ö¸' + 35: 3, # 'Ö¹' + 62: 0, # 'Ö»' + 28: 3, # 'Ö¼' + 38: 0, # '×' + 45: 0, # 'ׂ' + 9: 3, # '×' + 8: 3, # 'ב' + 20: 3, # '×’' + 16: 3, # 'ד' + 3: 3, # '×”' + 2: 3, # 'ו' + 24: 3, # '×–' + 14: 3, # '×—' + 22: 3, # 'ט' + 1: 3, # '×™' + 25: 3, # 'ך' + 15: 3, # '×›' + 4: 3, # 'ל' + 11: 3, # '×' + 6: 3, # 'מ' + 23: 3, # 'ן' + 12: 3, # '× ' + 19: 3, # 'ס' + 13: 3, # '×¢' + 26: 3, # '×£' + 18: 3, # 'פ' + 27: 3, # '×¥' + 21: 3, # 'צ' + 17: 3, # 'ק' + 7: 3, # 'ר' + 10: 3, # 'ש' + 5: 3, # 'ת' + 32: 1, # '–' + 52: 0, # '’' + 47: 0, # '“' + 46: 1, # 'â€' + 58: 0, # '†' + 40: 2, # '…' + }, + 24: { # '×–' + 50: 0, # 'a' + 60: 0, # 'c' + 61: 0, # 'd' + 42: 0, # 'e' + 53: 0, # 'i' + 56: 0, # 'l' + 54: 0, # 'n' + 49: 0, # 'o' + 51: 0, # 'r' + 43: 0, # 's' + 44: 0, # 't' + 63: 0, # 'u' + 34: 0, # '\xa0' + 55: 1, # '´' + 48: 0, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 2, # 'Ö°' + 59: 0, # 'Ö±' + 41: 1, # 'Ö²' + 33: 1, # 'Ö´' + 37: 2, # 'Öµ' + 36: 2, # 'Ö¶' + 31: 2, # 'Ö·' + 29: 2, # 'Ö¸' + 35: 1, # 'Ö¹' + 62: 1, # 'Ö»' + 28: 2, # 'Ö¼' + 38: 0, # '×' + 45: 0, # 'ׂ' + 9: 3, # '×' + 8: 2, # 'ב' + 20: 2, # '×’' + 16: 2, # 'ד' + 3: 3, # '×”' + 2: 3, # 'ו' + 24: 2, # '×–' + 14: 2, # '×—' + 22: 1, # 'ט' + 1: 3, # '×™' + 25: 1, # 'ך' + 15: 3, # '×›' + 4: 3, # 'ל' + 11: 2, # '×' + 6: 3, # 'מ' + 23: 2, # 'ן' + 12: 2, # '× ' + 19: 1, # 'ס' + 13: 2, # '×¢' + 26: 1, # '×£' + 18: 1, # 'פ' + 27: 0, # '×¥' + 21: 2, # 'צ' + 17: 3, # 'ק' + 7: 3, # 'ר' + 10: 1, # 'ש' + 5: 2, # 'ת' + 32: 0, # '–' + 52: 0, # '’' + 47: 0, # '“' + 46: 0, # 'â€' + 58: 0, # '†' + 40: 1, # '…' + }, + 14: { # '×—' + 50: 0, # 'a' + 60: 0, # 'c' + 61: 0, # 'd' + 42: 0, # 'e' + 53: 0, # 'i' + 56: 0, # 'l' + 54: 0, # 'n' + 49: 0, # 'o' + 51: 0, # 'r' + 43: 0, # 's' + 44: 0, # 't' + 63: 0, # 'u' + 34: 1, # '\xa0' + 55: 1, # '´' + 48: 0, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 2, # 'Ö°' + 59: 1, # 'Ö±' + 41: 2, # 'Ö²' + 33: 2, # 'Ö´' + 37: 2, # 'Öµ' + 36: 2, # 'Ö¶' + 31: 2, # 'Ö·' + 29: 2, # 'Ö¸' + 35: 2, # 'Ö¹' + 62: 1, # 'Ö»' + 28: 0, # 'Ö¼' + 38: 0, # '×' + 45: 0, # 'ׂ' + 9: 2, # '×' + 8: 3, # 'ב' + 20: 2, # '×’' + 16: 3, # 'ד' + 3: 3, # '×”' + 2: 3, # 'ו' + 24: 3, # '×–' + 14: 2, # '×—' + 22: 2, # 'ט' + 1: 3, # '×™' + 25: 1, # 'ך' + 15: 2, # '×›' + 4: 3, # 'ל' + 11: 3, # '×' + 6: 3, # 'מ' + 23: 2, # 'ן' + 12: 3, # '× ' + 19: 3, # 'ס' + 13: 1, # '×¢' + 26: 2, # '×£' + 18: 2, # 'פ' + 27: 2, # '×¥' + 21: 3, # 'צ' + 17: 3, # 'ק' + 7: 3, # 'ר' + 10: 3, # 'ש' + 5: 3, # 'ת' + 32: 0, # '–' + 52: 1, # '’' + 47: 0, # '“' + 46: 1, # 'â€' + 58: 0, # '†' + 40: 1, # '…' + }, + 22: { # 'ט' + 50: 0, # 'a' + 60: 0, # 'c' + 61: 0, # 'd' + 42: 0, # 'e' + 53: 0, # 'i' + 56: 0, # 'l' + 54: 0, # 'n' + 49: 0, # 'o' + 51: 0, # 'r' + 43: 0, # 's' + 44: 0, # 't' + 63: 0, # 'u' + 34: 1, # '\xa0' + 55: 0, # '´' + 48: 0, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 2, # 'Ö°' + 59: 0, # 'Ö±' + 41: 0, # 'Ö²' + 33: 2, # 'Ö´' + 37: 1, # 'Öµ' + 36: 1, # 'Ö¶' + 31: 2, # 'Ö·' + 29: 1, # 'Ö¸' + 35: 1, # 'Ö¹' + 62: 1, # 'Ö»' + 28: 1, # 'Ö¼' + 38: 0, # '×' + 45: 0, # 'ׂ' + 9: 3, # '×' + 8: 3, # 'ב' + 20: 3, # '×’' + 16: 1, # 'ד' + 3: 3, # '×”' + 2: 3, # 'ו' + 24: 2, # '×–' + 14: 3, # '×—' + 22: 2, # 'ט' + 1: 3, # '×™' + 25: 1, # 'ך' + 15: 2, # '×›' + 4: 3, # 'ל' + 11: 2, # '×' + 6: 2, # 'מ' + 23: 2, # 'ן' + 12: 3, # '× ' + 19: 2, # 'ס' + 13: 3, # '×¢' + 26: 2, # '×£' + 18: 3, # 'פ' + 27: 1, # '×¥' + 21: 2, # 'צ' + 17: 2, # 'ק' + 7: 3, # 'ר' + 10: 2, # 'ש' + 5: 3, # 'ת' + 32: 0, # '–' + 52: 0, # '’' + 47: 0, # '“' + 46: 0, # 'â€' + 58: 0, # '†' + 40: 1, # '…' + }, + 1: { # '×™' + 50: 0, # 'a' + 60: 0, # 'c' + 61: 0, # 'd' + 42: 0, # 'e' + 53: 0, # 'i' + 56: 0, # 'l' + 54: 0, # 'n' + 49: 0, # 'o' + 51: 0, # 'r' + 43: 0, # 's' + 44: 0, # 't' + 63: 0, # 'u' + 34: 1, # '\xa0' + 55: 1, # '´' + 48: 1, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 2, # 'Ö°' + 59: 0, # 'Ö±' + 41: 0, # 'Ö²' + 33: 2, # 'Ö´' + 37: 2, # 'Öµ' + 36: 1, # 'Ö¶' + 31: 2, # 'Ö·' + 29: 2, # 'Ö¸' + 35: 2, # 'Ö¹' + 62: 1, # 'Ö»' + 28: 2, # 'Ö¼' + 38: 0, # '×' + 45: 0, # 'ׂ' + 9: 3, # '×' + 8: 3, # 'ב' + 20: 3, # '×’' + 16: 3, # 'ד' + 3: 3, # '×”' + 2: 3, # 'ו' + 24: 3, # '×–' + 14: 3, # '×—' + 22: 3, # 'ט' + 1: 3, # '×™' + 25: 3, # 'ך' + 15: 3, # '×›' + 4: 3, # 'ל' + 11: 3, # '×' + 6: 3, # 'מ' + 23: 3, # 'ן' + 12: 3, # '× ' + 19: 3, # 'ס' + 13: 3, # '×¢' + 26: 3, # '×£' + 18: 3, # 'פ' + 27: 3, # '×¥' + 21: 3, # 'צ' + 17: 3, # 'ק' + 7: 3, # 'ר' + 10: 3, # 'ש' + 5: 3, # 'ת' + 32: 1, # '–' + 52: 0, # '’' + 47: 0, # '“' + 46: 1, # 'â€' + 58: 0, # '†' + 40: 2, # '…' + }, + 25: { # 'ך' + 50: 0, # 'a' + 60: 0, # 'c' + 61: 0, # 'd' + 42: 0, # 'e' + 53: 0, # 'i' + 56: 0, # 'l' + 54: 0, # 'n' + 49: 0, # 'o' + 51: 0, # 'r' + 43: 0, # 's' + 44: 0, # 't' + 63: 0, # 'u' + 34: 0, # '\xa0' + 55: 0, # '´' + 48: 0, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 2, # 'Ö°' + 59: 0, # 'Ö±' + 41: 0, # 'Ö²' + 33: 0, # 'Ö´' + 37: 0, # 'Öµ' + 36: 0, # 'Ö¶' + 31: 0, # 'Ö·' + 29: 2, # 'Ö¸' + 35: 0, # 'Ö¹' + 62: 0, # 'Ö»' + 28: 1, # 'Ö¼' + 38: 0, # '×' + 45: 0, # 'ׂ' + 9: 1, # '×' + 8: 0, # 'ב' + 20: 0, # '×’' + 16: 0, # 'ד' + 3: 1, # '×”' + 2: 0, # 'ו' + 24: 0, # '×–' + 14: 1, # '×—' + 22: 0, # 'ט' + 1: 0, # '×™' + 25: 0, # 'ך' + 15: 0, # '×›' + 4: 1, # 'ל' + 11: 0, # '×' + 6: 1, # 'מ' + 23: 0, # 'ן' + 12: 0, # '× ' + 19: 0, # 'ס' + 13: 0, # '×¢' + 26: 0, # '×£' + 18: 0, # 'פ' + 27: 0, # '×¥' + 21: 0, # 'צ' + 17: 0, # 'ק' + 7: 0, # 'ר' + 10: 1, # 'ש' + 5: 0, # 'ת' + 32: 0, # '–' + 52: 0, # '’' + 47: 0, # '“' + 46: 0, # 'â€' + 58: 0, # '†' + 40: 1, # '…' + }, + 15: { # '×›' + 50: 0, # 'a' + 60: 0, # 'c' + 61: 0, # 'd' + 42: 0, # 'e' + 53: 0, # 'i' + 56: 0, # 'l' + 54: 0, # 'n' + 49: 0, # 'o' + 51: 0, # 'r' + 43: 0, # 's' + 44: 0, # 't' + 63: 0, # 'u' + 34: 0, # '\xa0' + 55: 0, # '´' + 48: 0, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 2, # 'Ö°' + 59: 0, # 'Ö±' + 41: 0, # 'Ö²' + 33: 2, # 'Ö´' + 37: 2, # 'Öµ' + 36: 2, # 'Ö¶' + 31: 2, # 'Ö·' + 29: 2, # 'Ö¸' + 35: 1, # 'Ö¹' + 62: 1, # 'Ö»' + 28: 3, # 'Ö¼' + 38: 0, # '×' + 45: 0, # 'ׂ' + 9: 3, # '×' + 8: 3, # 'ב' + 20: 2, # '×’' + 16: 3, # 'ד' + 3: 3, # '×”' + 2: 3, # 'ו' + 24: 3, # '×–' + 14: 3, # '×—' + 22: 2, # 'ט' + 1: 3, # '×™' + 25: 3, # 'ך' + 15: 3, # '×›' + 4: 3, # 'ל' + 11: 3, # '×' + 6: 3, # 'מ' + 23: 3, # 'ן' + 12: 3, # '× ' + 19: 3, # 'ס' + 13: 2, # '×¢' + 26: 3, # '×£' + 18: 3, # 'פ' + 27: 1, # '×¥' + 21: 2, # 'צ' + 17: 2, # 'ק' + 7: 3, # 'ר' + 10: 3, # 'ש' + 5: 3, # 'ת' + 32: 0, # '–' + 52: 0, # '’' + 47: 0, # '“' + 46: 0, # 'â€' + 58: 0, # '†' + 40: 0, # '…' + }, + 4: { # 'ל' + 50: 0, # 'a' + 60: 0, # 'c' + 61: 0, # 'd' + 42: 0, # 'e' + 53: 0, # 'i' + 56: 0, # 'l' + 54: 0, # 'n' + 49: 0, # 'o' + 51: 0, # 'r' + 43: 0, # 's' + 44: 0, # 't' + 63: 0, # 'u' + 34: 1, # '\xa0' + 55: 1, # '´' + 48: 0, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 3, # 'Ö°' + 59: 0, # 'Ö±' + 41: 0, # 'Ö²' + 33: 2, # 'Ö´' + 37: 2, # 'Öµ' + 36: 2, # 'Ö¶' + 31: 2, # 'Ö·' + 29: 2, # 'Ö¸' + 35: 2, # 'Ö¹' + 62: 1, # 'Ö»' + 28: 2, # 'Ö¼' + 38: 0, # '×' + 45: 0, # 'ׂ' + 9: 3, # '×' + 8: 3, # 'ב' + 20: 3, # '×’' + 16: 3, # 'ד' + 3: 3, # '×”' + 2: 3, # 'ו' + 24: 3, # '×–' + 14: 3, # '×—' + 22: 3, # 'ט' + 1: 3, # '×™' + 25: 3, # 'ך' + 15: 3, # '×›' + 4: 3, # 'ל' + 11: 3, # '×' + 6: 3, # 'מ' + 23: 2, # 'ן' + 12: 3, # '× ' + 19: 3, # 'ס' + 13: 3, # '×¢' + 26: 2, # '×£' + 18: 3, # 'פ' + 27: 2, # '×¥' + 21: 3, # 'צ' + 17: 3, # 'ק' + 7: 3, # 'ר' + 10: 3, # 'ש' + 5: 3, # 'ת' + 32: 1, # '–' + 52: 0, # '’' + 47: 0, # '“' + 46: 1, # 'â€' + 58: 0, # '†' + 40: 1, # '…' + }, + 11: { # '×' + 50: 0, # 'a' + 60: 0, # 'c' + 61: 0, # 'd' + 42: 0, # 'e' + 53: 0, # 'i' + 56: 0, # 'l' + 54: 0, # 'n' + 49: 0, # 'o' + 51: 0, # 'r' + 43: 0, # 's' + 44: 0, # 't' + 63: 0, # 'u' + 34: 1, # '\xa0' + 55: 0, # '´' + 48: 0, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 0, # 'Ö°' + 59: 0, # 'Ö±' + 41: 0, # 'Ö²' + 33: 0, # 'Ö´' + 37: 0, # 'Öµ' + 36: 0, # 'Ö¶' + 31: 0, # 'Ö·' + 29: 0, # 'Ö¸' + 35: 0, # 'Ö¹' + 62: 0, # 'Ö»' + 28: 0, # 'Ö¼' + 38: 0, # '×' + 45: 0, # 'ׂ' + 9: 1, # '×' + 8: 1, # 'ב' + 20: 1, # '×’' + 16: 0, # 'ד' + 3: 1, # '×”' + 2: 1, # 'ו' + 24: 1, # '×–' + 14: 1, # '×—' + 22: 0, # 'ט' + 1: 1, # '×™' + 25: 0, # 'ך' + 15: 1, # '×›' + 4: 1, # 'ל' + 11: 1, # '×' + 6: 1, # 'מ' + 23: 0, # 'ן' + 12: 1, # '× ' + 19: 0, # 'ס' + 13: 1, # '×¢' + 26: 0, # '×£' + 18: 1, # 'פ' + 27: 1, # '×¥' + 21: 1, # 'צ' + 17: 1, # 'ק' + 7: 1, # 'ר' + 10: 1, # 'ש' + 5: 1, # 'ת' + 32: 0, # '–' + 52: 0, # '’' + 47: 0, # '“' + 46: 1, # 'â€' + 58: 0, # '†' + 40: 2, # '…' + }, + 6: { # 'מ' + 50: 0, # 'a' + 60: 0, # 'c' + 61: 0, # 'd' + 42: 0, # 'e' + 53: 0, # 'i' + 56: 0, # 'l' + 54: 0, # 'n' + 49: 0, # 'o' + 51: 0, # 'r' + 43: 0, # 's' + 44: 0, # 't' + 63: 0, # 'u' + 34: 0, # '\xa0' + 55: 1, # '´' + 48: 0, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 2, # 'Ö°' + 59: 0, # 'Ö±' + 41: 0, # 'Ö²' + 33: 2, # 'Ö´' + 37: 2, # 'Öµ' + 36: 2, # 'Ö¶' + 31: 2, # 'Ö·' + 29: 2, # 'Ö¸' + 35: 2, # 'Ö¹' + 62: 1, # 'Ö»' + 28: 2, # 'Ö¼' + 38: 0, # '×' + 45: 0, # 'ׂ' + 9: 3, # '×' + 8: 3, # 'ב' + 20: 3, # '×’' + 16: 3, # 'ד' + 3: 3, # '×”' + 2: 3, # 'ו' + 24: 3, # '×–' + 14: 3, # '×—' + 22: 3, # 'ט' + 1: 3, # '×™' + 25: 2, # 'ך' + 15: 3, # '×›' + 4: 3, # 'ל' + 11: 3, # '×' + 6: 3, # 'מ' + 23: 3, # 'ן' + 12: 3, # '× ' + 19: 3, # 'ס' + 13: 3, # '×¢' + 26: 0, # '×£' + 18: 3, # 'פ' + 27: 2, # '×¥' + 21: 3, # 'צ' + 17: 3, # 'ק' + 7: 3, # 'ר' + 10: 3, # 'ש' + 5: 3, # 'ת' + 32: 0, # '–' + 52: 0, # '’' + 47: 0, # '“' + 46: 0, # 'â€' + 58: 0, # '†' + 40: 1, # '…' + }, + 23: { # 'ן' + 50: 0, # 'a' + 60: 0, # 'c' + 61: 0, # 'd' + 42: 0, # 'e' + 53: 0, # 'i' + 56: 0, # 'l' + 54: 0, # 'n' + 49: 0, # 'o' + 51: 0, # 'r' + 43: 0, # 's' + 44: 0, # 't' + 63: 0, # 'u' + 34: 1, # '\xa0' + 55: 0, # '´' + 48: 1, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 0, # 'Ö°' + 59: 0, # 'Ö±' + 41: 0, # 'Ö²' + 33: 0, # 'Ö´' + 37: 0, # 'Öµ' + 36: 0, # 'Ö¶' + 31: 0, # 'Ö·' + 29: 0, # 'Ö¸' + 35: 0, # 'Ö¹' + 62: 0, # 'Ö»' + 28: 0, # 'Ö¼' + 38: 0, # '×' + 45: 0, # 'ׂ' + 9: 1, # '×' + 8: 1, # 'ב' + 20: 1, # '×’' + 16: 1, # 'ד' + 3: 1, # '×”' + 2: 1, # 'ו' + 24: 0, # '×–' + 14: 1, # '×—' + 22: 1, # 'ט' + 1: 1, # '×™' + 25: 0, # 'ך' + 15: 1, # '×›' + 4: 1, # 'ל' + 11: 1, # '×' + 6: 1, # 'מ' + 23: 0, # 'ן' + 12: 1, # '× ' + 19: 1, # 'ס' + 13: 1, # '×¢' + 26: 1, # '×£' + 18: 1, # 'פ' + 27: 0, # '×¥' + 21: 0, # 'צ' + 17: 1, # 'ק' + 7: 1, # 'ר' + 10: 1, # 'ש' + 5: 1, # 'ת' + 32: 1, # '–' + 52: 0, # '’' + 47: 0, # '“' + 46: 1, # 'â€' + 58: 0, # '†' + 40: 2, # '…' + }, + 12: { # '× ' + 50: 0, # 'a' + 60: 0, # 'c' + 61: 0, # 'd' + 42: 0, # 'e' + 53: 0, # 'i' + 56: 0, # 'l' + 54: 0, # 'n' + 49: 0, # 'o' + 51: 0, # 'r' + 43: 0, # 's' + 44: 0, # 't' + 63: 0, # 'u' + 34: 0, # '\xa0' + 55: 0, # '´' + 48: 0, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 2, # 'Ö°' + 59: 0, # 'Ö±' + 41: 0, # 'Ö²' + 33: 2, # 'Ö´' + 37: 2, # 'Öµ' + 36: 2, # 'Ö¶' + 31: 2, # 'Ö·' + 29: 2, # 'Ö¸' + 35: 1, # 'Ö¹' + 62: 1, # 'Ö»' + 28: 2, # 'Ö¼' + 38: 0, # '×' + 45: 0, # 'ׂ' + 9: 3, # '×' + 8: 3, # 'ב' + 20: 3, # '×’' + 16: 3, # 'ד' + 3: 3, # '×”' + 2: 3, # 'ו' + 24: 3, # '×–' + 14: 3, # '×—' + 22: 3, # 'ט' + 1: 3, # '×™' + 25: 2, # 'ך' + 15: 3, # '×›' + 4: 3, # 'ל' + 11: 3, # '×' + 6: 3, # 'מ' + 23: 3, # 'ן' + 12: 3, # '× ' + 19: 3, # 'ס' + 13: 3, # '×¢' + 26: 2, # '×£' + 18: 3, # 'פ' + 27: 2, # '×¥' + 21: 3, # 'צ' + 17: 3, # 'ק' + 7: 3, # 'ר' + 10: 3, # 'ש' + 5: 3, # 'ת' + 32: 0, # '–' + 52: 0, # '’' + 47: 0, # '“' + 46: 0, # 'â€' + 58: 0, # '†' + 40: 0, # '…' + }, + 19: { # 'ס' + 50: 0, # 'a' + 60: 0, # 'c' + 61: 0, # 'd' + 42: 0, # 'e' + 53: 0, # 'i' + 56: 0, # 'l' + 54: 0, # 'n' + 49: 0, # 'o' + 51: 0, # 'r' + 43: 0, # 's' + 44: 0, # 't' + 63: 0, # 'u' + 34: 1, # '\xa0' + 55: 1, # '´' + 48: 0, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 2, # 'Ö°' + 59: 0, # 'Ö±' + 41: 0, # 'Ö²' + 33: 2, # 'Ö´' + 37: 1, # 'Öµ' + 36: 2, # 'Ö¶' + 31: 2, # 'Ö·' + 29: 1, # 'Ö¸' + 35: 1, # 'Ö¹' + 62: 2, # 'Ö»' + 28: 2, # 'Ö¼' + 38: 0, # '×' + 45: 0, # 'ׂ' + 9: 2, # '×' + 8: 3, # 'ב' + 20: 3, # '×’' + 16: 3, # 'ד' + 3: 3, # '×”' + 2: 3, # 'ו' + 24: 1, # '×–' + 14: 3, # '×—' + 22: 3, # 'ט' + 1: 3, # '×™' + 25: 2, # 'ך' + 15: 3, # '×›' + 4: 3, # 'ל' + 11: 2, # '×' + 6: 3, # 'מ' + 23: 2, # 'ן' + 12: 3, # '× ' + 19: 2, # 'ס' + 13: 3, # '×¢' + 26: 3, # '×£' + 18: 3, # 'פ' + 27: 0, # '×¥' + 21: 2, # 'צ' + 17: 3, # 'ק' + 7: 3, # 'ר' + 10: 1, # 'ש' + 5: 3, # 'ת' + 32: 0, # '–' + 52: 0, # '’' + 47: 0, # '“' + 46: 1, # 'â€' + 58: 0, # '†' + 40: 1, # '…' + }, + 13: { # '×¢' + 50: 0, # 'a' + 60: 0, # 'c' + 61: 0, # 'd' + 42: 0, # 'e' + 53: 0, # 'i' + 56: 0, # 'l' + 54: 0, # 'n' + 49: 0, # 'o' + 51: 0, # 'r' + 43: 0, # 's' + 44: 0, # 't' + 63: 0, # 'u' + 34: 0, # '\xa0' + 55: 0, # '´' + 48: 1, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 1, # 'Ö°' + 59: 1, # 'Ö±' + 41: 2, # 'Ö²' + 33: 2, # 'Ö´' + 37: 2, # 'Öµ' + 36: 2, # 'Ö¶' + 31: 2, # 'Ö·' + 29: 2, # 'Ö¸' + 35: 2, # 'Ö¹' + 62: 1, # 'Ö»' + 28: 0, # 'Ö¼' + 38: 0, # '×' + 45: 0, # 'ׂ' + 9: 2, # '×' + 8: 3, # 'ב' + 20: 3, # '×’' + 16: 3, # 'ד' + 3: 3, # '×”' + 2: 3, # 'ו' + 24: 3, # '×–' + 14: 1, # '×—' + 22: 3, # 'ט' + 1: 3, # '×™' + 25: 2, # 'ך' + 15: 2, # '×›' + 4: 3, # 'ל' + 11: 3, # '×' + 6: 3, # 'מ' + 23: 2, # 'ן' + 12: 3, # '× ' + 19: 3, # 'ס' + 13: 2, # '×¢' + 26: 1, # '×£' + 18: 2, # 'פ' + 27: 2, # '×¥' + 21: 3, # 'צ' + 17: 3, # 'ק' + 7: 3, # 'ר' + 10: 3, # 'ש' + 5: 3, # 'ת' + 32: 0, # '–' + 52: 0, # '’' + 47: 0, # '“' + 46: 1, # 'â€' + 58: 0, # '†' + 40: 1, # '…' + }, + 26: { # '×£' + 50: 0, # 'a' + 60: 0, # 'c' + 61: 0, # 'd' + 42: 0, # 'e' + 53: 0, # 'i' + 56: 0, # 'l' + 54: 0, # 'n' + 49: 0, # 'o' + 51: 0, # 'r' + 43: 0, # 's' + 44: 0, # 't' + 63: 0, # 'u' + 34: 0, # '\xa0' + 55: 0, # '´' + 48: 0, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 0, # 'Ö°' + 59: 0, # 'Ö±' + 41: 0, # 'Ö²' + 33: 0, # 'Ö´' + 37: 0, # 'Öµ' + 36: 0, # 'Ö¶' + 31: 0, # 'Ö·' + 29: 0, # 'Ö¸' + 35: 0, # 'Ö¹' + 62: 0, # 'Ö»' + 28: 0, # 'Ö¼' + 38: 0, # '×' + 45: 0, # 'ׂ' + 9: 1, # '×' + 8: 0, # 'ב' + 20: 0, # '×’' + 16: 0, # 'ד' + 3: 0, # '×”' + 2: 1, # 'ו' + 24: 0, # '×–' + 14: 1, # '×—' + 22: 0, # 'ט' + 1: 0, # '×™' + 25: 0, # 'ך' + 15: 1, # '×›' + 4: 1, # 'ל' + 11: 0, # '×' + 6: 1, # 'מ' + 23: 0, # 'ן' + 12: 0, # '× ' + 19: 1, # 'ס' + 13: 0, # '×¢' + 26: 1, # '×£' + 18: 1, # 'פ' + 27: 0, # '×¥' + 21: 0, # 'צ' + 17: 1, # 'ק' + 7: 1, # 'ר' + 10: 1, # 'ש' + 5: 0, # 'ת' + 32: 0, # '–' + 52: 0, # '’' + 47: 0, # '“' + 46: 0, # 'â€' + 58: 0, # '†' + 40: 1, # '…' + }, + 18: { # 'פ' + 50: 0, # 'a' + 60: 0, # 'c' + 61: 0, # 'd' + 42: 0, # 'e' + 53: 0, # 'i' + 56: 0, # 'l' + 54: 0, # 'n' + 49: 0, # 'o' + 51: 0, # 'r' + 43: 0, # 's' + 44: 0, # 't' + 63: 0, # 'u' + 34: 0, # '\xa0' + 55: 1, # '´' + 48: 0, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 2, # 'Ö°' + 59: 0, # 'Ö±' + 41: 0, # 'Ö²' + 33: 2, # 'Ö´' + 37: 1, # 'Öµ' + 36: 2, # 'Ö¶' + 31: 1, # 'Ö·' + 29: 2, # 'Ö¸' + 35: 1, # 'Ö¹' + 62: 1, # 'Ö»' + 28: 2, # 'Ö¼' + 38: 0, # '×' + 45: 0, # 'ׂ' + 9: 3, # '×' + 8: 2, # 'ב' + 20: 3, # '×’' + 16: 2, # 'ד' + 3: 3, # '×”' + 2: 3, # 'ו' + 24: 2, # '×–' + 14: 3, # '×—' + 22: 3, # 'ט' + 1: 3, # '×™' + 25: 2, # 'ך' + 15: 3, # '×›' + 4: 3, # 'ל' + 11: 2, # '×' + 6: 2, # 'מ' + 23: 3, # 'ן' + 12: 3, # '× ' + 19: 3, # 'ס' + 13: 3, # '×¢' + 26: 2, # '×£' + 18: 2, # 'פ' + 27: 2, # '×¥' + 21: 3, # 'צ' + 17: 3, # 'ק' + 7: 3, # 'ר' + 10: 3, # 'ש' + 5: 3, # 'ת' + 32: 0, # '–' + 52: 0, # '’' + 47: 0, # '“' + 46: 1, # 'â€' + 58: 0, # '†' + 40: 0, # '…' + }, + 27: { # '×¥' + 50: 0, # 'a' + 60: 0, # 'c' + 61: 0, # 'd' + 42: 0, # 'e' + 53: 0, # 'i' + 56: 0, # 'l' + 54: 0, # 'n' + 49: 0, # 'o' + 51: 0, # 'r' + 43: 0, # 's' + 44: 0, # 't' + 63: 0, # 'u' + 34: 0, # '\xa0' + 55: 1, # '´' + 48: 0, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 0, # 'Ö°' + 59: 0, # 'Ö±' + 41: 0, # 'Ö²' + 33: 0, # 'Ö´' + 37: 0, # 'Öµ' + 36: 0, # 'Ö¶' + 31: 0, # 'Ö·' + 29: 0, # 'Ö¸' + 35: 0, # 'Ö¹' + 62: 0, # 'Ö»' + 28: 0, # 'Ö¼' + 38: 0, # '×' + 45: 0, # 'ׂ' + 9: 1, # '×' + 8: 0, # 'ב' + 20: 0, # '×’' + 16: 0, # 'ד' + 3: 0, # '×”' + 2: 0, # 'ו' + 24: 0, # '×–' + 14: 0, # '×—' + 22: 0, # 'ט' + 1: 0, # '×™' + 25: 0, # 'ך' + 15: 0, # '×›' + 4: 1, # 'ל' + 11: 0, # '×' + 6: 0, # 'מ' + 23: 0, # 'ן' + 12: 0, # '× ' + 19: 1, # 'ס' + 13: 0, # '×¢' + 26: 0, # '×£' + 18: 0, # 'פ' + 27: 0, # '×¥' + 21: 0, # 'צ' + 17: 0, # 'ק' + 7: 1, # 'ר' + 10: 0, # 'ש' + 5: 1, # 'ת' + 32: 0, # '–' + 52: 0, # '’' + 47: 0, # '“' + 46: 0, # 'â€' + 58: 0, # '†' + 40: 1, # '…' + }, + 21: { # 'צ' + 50: 0, # 'a' + 60: 0, # 'c' + 61: 0, # 'd' + 42: 0, # 'e' + 53: 0, # 'i' + 56: 0, # 'l' + 54: 0, # 'n' + 49: 0, # 'o' + 51: 0, # 'r' + 43: 0, # 's' + 44: 0, # 't' + 63: 0, # 'u' + 34: 0, # '\xa0' + 55: 1, # '´' + 48: 0, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 2, # 'Ö°' + 59: 0, # 'Ö±' + 41: 0, # 'Ö²' + 33: 2, # 'Ö´' + 37: 2, # 'Öµ' + 36: 1, # 'Ö¶' + 31: 2, # 'Ö·' + 29: 2, # 'Ö¸' + 35: 1, # 'Ö¹' + 62: 1, # 'Ö»' + 28: 2, # 'Ö¼' + 38: 0, # '×' + 45: 0, # 'ׂ' + 9: 3, # '×' + 8: 3, # 'ב' + 20: 2, # '×’' + 16: 3, # 'ד' + 3: 3, # '×”' + 2: 3, # 'ו' + 24: 1, # '×–' + 14: 3, # '×—' + 22: 2, # 'ט' + 1: 3, # '×™' + 25: 1, # 'ך' + 15: 1, # '×›' + 4: 3, # 'ל' + 11: 2, # '×' + 6: 3, # 'מ' + 23: 2, # 'ן' + 12: 3, # '× ' + 19: 1, # 'ס' + 13: 3, # '×¢' + 26: 2, # '×£' + 18: 3, # 'פ' + 27: 2, # '×¥' + 21: 2, # 'צ' + 17: 3, # 'ק' + 7: 3, # 'ר' + 10: 0, # 'ש' + 5: 3, # 'ת' + 32: 0, # '–' + 52: 0, # '’' + 47: 0, # '“' + 46: 0, # 'â€' + 58: 0, # '†' + 40: 0, # '…' + }, + 17: { # 'ק' + 50: 0, # 'a' + 60: 0, # 'c' + 61: 0, # 'd' + 42: 0, # 'e' + 53: 0, # 'i' + 56: 0, # 'l' + 54: 0, # 'n' + 49: 0, # 'o' + 51: 0, # 'r' + 43: 0, # 's' + 44: 0, # 't' + 63: 0, # 'u' + 34: 1, # '\xa0' + 55: 1, # '´' + 48: 0, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 2, # 'Ö°' + 59: 0, # 'Ö±' + 41: 0, # 'Ö²' + 33: 2, # 'Ö´' + 37: 2, # 'Öµ' + 36: 1, # 'Ö¶' + 31: 2, # 'Ö·' + 29: 2, # 'Ö¸' + 35: 2, # 'Ö¹' + 62: 1, # 'Ö»' + 28: 2, # 'Ö¼' + 38: 0, # '×' + 45: 0, # 'ׂ' + 9: 3, # '×' + 8: 3, # 'ב' + 20: 2, # '×’' + 16: 3, # 'ד' + 3: 3, # '×”' + 2: 3, # 'ו' + 24: 2, # '×–' + 14: 3, # '×—' + 22: 3, # 'ט' + 1: 3, # '×™' + 25: 1, # 'ך' + 15: 1, # '×›' + 4: 3, # 'ל' + 11: 2, # '×' + 6: 3, # 'מ' + 23: 2, # 'ן' + 12: 3, # '× ' + 19: 3, # 'ס' + 13: 3, # '×¢' + 26: 2, # '×£' + 18: 3, # 'פ' + 27: 2, # '×¥' + 21: 3, # 'צ' + 17: 2, # 'ק' + 7: 3, # 'ר' + 10: 3, # 'ש' + 5: 3, # 'ת' + 32: 0, # '–' + 52: 1, # '’' + 47: 0, # '“' + 46: 1, # 'â€' + 58: 0, # '†' + 40: 1, # '…' + }, + 7: { # 'ר' + 50: 0, # 'a' + 60: 0, # 'c' + 61: 0, # 'd' + 42: 0, # 'e' + 53: 0, # 'i' + 56: 0, # 'l' + 54: 0, # 'n' + 49: 0, # 'o' + 51: 0, # 'r' + 43: 0, # 's' + 44: 0, # 't' + 63: 0, # 'u' + 34: 1, # '\xa0' + 55: 2, # '´' + 48: 1, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 2, # 'Ö°' + 59: 0, # 'Ö±' + 41: 1, # 'Ö²' + 33: 2, # 'Ö´' + 37: 2, # 'Öµ' + 36: 2, # 'Ö¶' + 31: 2, # 'Ö·' + 29: 2, # 'Ö¸' + 35: 2, # 'Ö¹' + 62: 1, # 'Ö»' + 28: 0, # 'Ö¼' + 38: 0, # '×' + 45: 0, # 'ׂ' + 9: 3, # '×' + 8: 3, # 'ב' + 20: 3, # '×’' + 16: 3, # 'ד' + 3: 3, # '×”' + 2: 3, # 'ו' + 24: 3, # '×–' + 14: 3, # '×—' + 22: 3, # 'ט' + 1: 3, # '×™' + 25: 3, # 'ך' + 15: 3, # '×›' + 4: 3, # 'ל' + 11: 3, # '×' + 6: 3, # 'מ' + 23: 3, # 'ן' + 12: 3, # '× ' + 19: 3, # 'ס' + 13: 3, # '×¢' + 26: 2, # '×£' + 18: 3, # 'פ' + 27: 3, # '×¥' + 21: 3, # 'צ' + 17: 3, # 'ק' + 7: 3, # 'ר' + 10: 3, # 'ש' + 5: 3, # 'ת' + 32: 0, # '–' + 52: 0, # '’' + 47: 0, # '“' + 46: 1, # 'â€' + 58: 0, # '†' + 40: 2, # '…' + }, + 10: { # 'ש' + 50: 0, # 'a' + 60: 0, # 'c' + 61: 0, # 'd' + 42: 0, # 'e' + 53: 0, # 'i' + 56: 0, # 'l' + 54: 0, # 'n' + 49: 0, # 'o' + 51: 0, # 'r' + 43: 0, # 's' + 44: 0, # 't' + 63: 0, # 'u' + 34: 1, # '\xa0' + 55: 0, # '´' + 48: 0, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 1, # 'Ö°' + 59: 0, # 'Ö±' + 41: 0, # 'Ö²' + 33: 1, # 'Ö´' + 37: 1, # 'Öµ' + 36: 1, # 'Ö¶' + 31: 1, # 'Ö·' + 29: 1, # 'Ö¸' + 35: 1, # 'Ö¹' + 62: 1, # 'Ö»' + 28: 2, # 'Ö¼' + 38: 3, # '×' + 45: 2, # 'ׂ' + 9: 3, # '×' + 8: 3, # 'ב' + 20: 3, # '×’' + 16: 3, # 'ד' + 3: 3, # '×”' + 2: 3, # 'ו' + 24: 2, # '×–' + 14: 3, # '×—' + 22: 3, # 'ט' + 1: 3, # '×™' + 25: 3, # 'ך' + 15: 3, # '×›' + 4: 3, # 'ל' + 11: 3, # '×' + 6: 3, # 'מ' + 23: 2, # 'ן' + 12: 3, # '× ' + 19: 2, # 'ס' + 13: 3, # '×¢' + 26: 2, # '×£' + 18: 3, # 'פ' + 27: 1, # '×¥' + 21: 2, # 'צ' + 17: 3, # 'ק' + 7: 3, # 'ר' + 10: 3, # 'ש' + 5: 3, # 'ת' + 32: 0, # '–' + 52: 0, # '’' + 47: 0, # '“' + 46: 1, # 'â€' + 58: 0, # '†' + 40: 1, # '…' + }, + 5: { # 'ת' + 50: 0, # 'a' + 60: 0, # 'c' + 61: 0, # 'd' + 42: 0, # 'e' + 53: 0, # 'i' + 56: 0, # 'l' + 54: 0, # 'n' + 49: 0, # 'o' + 51: 0, # 'r' + 43: 0, # 's' + 44: 0, # 't' + 63: 0, # 'u' + 34: 1, # '\xa0' + 55: 0, # '´' + 48: 1, # '¼' + 39: 1, # '½' + 57: 0, # '¾' + 30: 2, # 'Ö°' + 59: 0, # 'Ö±' + 41: 0, # 'Ö²' + 33: 2, # 'Ö´' + 37: 2, # 'Öµ' + 36: 2, # 'Ö¶' + 31: 2, # 'Ö·' + 29: 2, # 'Ö¸' + 35: 1, # 'Ö¹' + 62: 1, # 'Ö»' + 28: 2, # 'Ö¼' + 38: 0, # '×' + 45: 0, # 'ׂ' + 9: 3, # '×' + 8: 3, # 'ב' + 20: 3, # '×’' + 16: 2, # 'ד' + 3: 3, # '×”' + 2: 3, # 'ו' + 24: 2, # '×–' + 14: 3, # '×—' + 22: 2, # 'ט' + 1: 3, # '×™' + 25: 2, # 'ך' + 15: 3, # '×›' + 4: 3, # 'ל' + 11: 3, # '×' + 6: 3, # 'מ' + 23: 3, # 'ן' + 12: 3, # '× ' + 19: 2, # 'ס' + 13: 3, # '×¢' + 26: 2, # '×£' + 18: 3, # 'פ' + 27: 1, # '×¥' + 21: 2, # 'צ' + 17: 3, # 'ק' + 7: 3, # 'ר' + 10: 3, # 'ש' + 5: 3, # 'ת' + 32: 1, # '–' + 52: 1, # '’' + 47: 0, # '“' + 46: 0, # 'â€' + 58: 0, # '†' + 40: 2, # '…' + }, + 32: { # '–' + 50: 0, # 'a' + 60: 0, # 'c' + 61: 0, # 'd' + 42: 0, # 'e' + 53: 0, # 'i' + 56: 0, # 'l' + 54: 1, # 'n' + 49: 0, # 'o' + 51: 0, # 'r' + 43: 0, # 's' + 44: 0, # 't' + 63: 0, # 'u' + 34: 0, # '\xa0' + 55: 0, # '´' + 48: 0, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 0, # 'Ö°' + 59: 0, # 'Ö±' + 41: 0, # 'Ö²' + 33: 0, # 'Ö´' + 37: 0, # 'Öµ' + 36: 0, # 'Ö¶' + 31: 0, # 'Ö·' + 29: 0, # 'Ö¸' + 35: 0, # 'Ö¹' + 62: 0, # 'Ö»' + 28: 0, # 'Ö¼' + 38: 0, # '×' + 45: 0, # 'ׂ' + 9: 1, # '×' + 8: 1, # 'ב' + 20: 1, # '×’' + 16: 1, # 'ד' + 3: 1, # '×”' + 2: 1, # 'ו' + 24: 0, # '×–' + 14: 1, # '×—' + 22: 0, # 'ט' + 1: 1, # '×™' + 25: 0, # 'ך' + 15: 1, # '×›' + 4: 1, # 'ל' + 11: 0, # '×' + 6: 1, # 'מ' + 23: 0, # 'ן' + 12: 0, # '× ' + 19: 1, # 'ס' + 13: 1, # '×¢' + 26: 0, # '×£' + 18: 1, # 'פ' + 27: 0, # '×¥' + 21: 1, # 'צ' + 17: 0, # 'ק' + 7: 1, # 'ר' + 10: 1, # 'ש' + 5: 1, # 'ת' + 32: 0, # '–' + 52: 0, # '’' + 47: 0, # '“' + 46: 0, # 'â€' + 58: 0, # '†' + 40: 0, # '…' + }, + 52: { # '’' + 50: 1, # 'a' + 60: 0, # 'c' + 61: 1, # 'd' + 42: 1, # 'e' + 53: 1, # 'i' + 56: 1, # 'l' + 54: 0, # 'n' + 49: 0, # 'o' + 51: 1, # 'r' + 43: 2, # 's' + 44: 2, # 't' + 63: 1, # 'u' + 34: 0, # '\xa0' + 55: 0, # '´' + 48: 0, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 0, # 'Ö°' + 59: 0, # 'Ö±' + 41: 0, # 'Ö²' + 33: 0, # 'Ö´' + 37: 0, # 'Öµ' + 36: 0, # 'Ö¶' + 31: 0, # 'Ö·' + 29: 0, # 'Ö¸' + 35: 0, # 'Ö¹' + 62: 0, # 'Ö»' + 28: 0, # 'Ö¼' + 38: 0, # '×' + 45: 0, # 'ׂ' + 9: 0, # '×' + 8: 0, # 'ב' + 20: 0, # '×’' + 16: 0, # 'ד' + 3: 0, # '×”' + 2: 1, # 'ו' + 24: 0, # '×–' + 14: 0, # '×—' + 22: 0, # 'ט' + 1: 0, # '×™' + 25: 0, # 'ך' + 15: 0, # '×›' + 4: 0, # 'ל' + 11: 0, # '×' + 6: 1, # 'מ' + 23: 0, # 'ן' + 12: 0, # '× ' + 19: 0, # 'ס' + 13: 0, # '×¢' + 26: 0, # '×£' + 18: 0, # 'פ' + 27: 0, # '×¥' + 21: 0, # 'צ' + 17: 0, # 'ק' + 7: 0, # 'ר' + 10: 0, # 'ש' + 5: 1, # 'ת' + 32: 0, # '–' + 52: 0, # '’' + 47: 0, # '“' + 46: 0, # 'â€' + 58: 0, # '†' + 40: 0, # '…' + }, + 47: { # '“' + 50: 1, # 'a' + 60: 1, # 'c' + 61: 1, # 'd' + 42: 1, # 'e' + 53: 1, # 'i' + 56: 1, # 'l' + 54: 1, # 'n' + 49: 1, # 'o' + 51: 1, # 'r' + 43: 1, # 's' + 44: 1, # 't' + 63: 1, # 'u' + 34: 0, # '\xa0' + 55: 0, # '´' + 48: 0, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 0, # 'Ö°' + 59: 0, # 'Ö±' + 41: 0, # 'Ö²' + 33: 0, # 'Ö´' + 37: 0, # 'Öµ' + 36: 0, # 'Ö¶' + 31: 0, # 'Ö·' + 29: 0, # 'Ö¸' + 35: 0, # 'Ö¹' + 62: 0, # 'Ö»' + 28: 0, # 'Ö¼' + 38: 0, # '×' + 45: 0, # 'ׂ' + 9: 2, # '×' + 8: 1, # 'ב' + 20: 1, # '×’' + 16: 1, # 'ד' + 3: 1, # '×”' + 2: 1, # 'ו' + 24: 1, # '×–' + 14: 1, # '×—' + 22: 1, # 'ט' + 1: 1, # '×™' + 25: 0, # 'ך' + 15: 1, # '×›' + 4: 1, # 'ל' + 11: 0, # '×' + 6: 1, # 'מ' + 23: 0, # 'ן' + 12: 1, # '× ' + 19: 1, # 'ס' + 13: 1, # '×¢' + 26: 0, # '×£' + 18: 1, # 'פ' + 27: 0, # '×¥' + 21: 1, # 'צ' + 17: 1, # 'ק' + 7: 1, # 'ר' + 10: 1, # 'ש' + 5: 1, # 'ת' + 32: 0, # '–' + 52: 0, # '’' + 47: 0, # '“' + 46: 0, # 'â€' + 58: 0, # '†' + 40: 0, # '…' + }, + 46: { # 'â€' + 50: 0, # 'a' + 60: 0, # 'c' + 61: 0, # 'd' + 42: 0, # 'e' + 53: 0, # 'i' + 56: 0, # 'l' + 54: 0, # 'n' + 49: 0, # 'o' + 51: 0, # 'r' + 43: 0, # 's' + 44: 1, # 't' + 63: 0, # 'u' + 34: 0, # '\xa0' + 55: 0, # '´' + 48: 0, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 0, # 'Ö°' + 59: 0, # 'Ö±' + 41: 0, # 'Ö²' + 33: 0, # 'Ö´' + 37: 0, # 'Öµ' + 36: 0, # 'Ö¶' + 31: 0, # 'Ö·' + 29: 0, # 'Ö¸' + 35: 0, # 'Ö¹' + 62: 0, # 'Ö»' + 28: 0, # 'Ö¼' + 38: 0, # '×' + 45: 0, # 'ׂ' + 9: 1, # '×' + 8: 1, # 'ב' + 20: 1, # '×’' + 16: 0, # 'ד' + 3: 0, # '×”' + 2: 0, # 'ו' + 24: 0, # '×–' + 14: 0, # '×—' + 22: 0, # 'ט' + 1: 1, # '×™' + 25: 0, # 'ך' + 15: 1, # '×›' + 4: 1, # 'ל' + 11: 0, # '×' + 6: 1, # 'מ' + 23: 0, # 'ן' + 12: 0, # '× ' + 19: 0, # 'ס' + 13: 0, # '×¢' + 26: 0, # '×£' + 18: 0, # 'פ' + 27: 0, # '×¥' + 21: 1, # 'צ' + 17: 0, # 'ק' + 7: 1, # 'ר' + 10: 0, # 'ש' + 5: 0, # 'ת' + 32: 0, # '–' + 52: 0, # '’' + 47: 0, # '“' + 46: 0, # 'â€' + 58: 0, # '†' + 40: 0, # '…' + }, + 58: { # '†' + 50: 0, # 'a' + 60: 0, # 'c' + 61: 0, # 'd' + 42: 0, # 'e' + 53: 0, # 'i' + 56: 0, # 'l' + 54: 0, # 'n' + 49: 0, # 'o' + 51: 0, # 'r' + 43: 0, # 's' + 44: 0, # 't' + 63: 0, # 'u' + 34: 0, # '\xa0' + 55: 0, # '´' + 48: 0, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 0, # 'Ö°' + 59: 0, # 'Ö±' + 41: 0, # 'Ö²' + 33: 0, # 'Ö´' + 37: 0, # 'Öµ' + 36: 0, # 'Ö¶' + 31: 0, # 'Ö·' + 29: 0, # 'Ö¸' + 35: 0, # 'Ö¹' + 62: 0, # 'Ö»' + 28: 0, # 'Ö¼' + 38: 0, # '×' + 45: 0, # 'ׂ' + 9: 0, # '×' + 8: 0, # 'ב' + 20: 0, # '×’' + 16: 0, # 'ד' + 3: 0, # '×”' + 2: 0, # 'ו' + 24: 0, # '×–' + 14: 0, # '×—' + 22: 0, # 'ט' + 1: 0, # '×™' + 25: 0, # 'ך' + 15: 0, # '×›' + 4: 0, # 'ל' + 11: 0, # '×' + 6: 0, # 'מ' + 23: 0, # 'ן' + 12: 0, # '× ' + 19: 0, # 'ס' + 13: 0, # '×¢' + 26: 0, # '×£' + 18: 0, # 'פ' + 27: 0, # '×¥' + 21: 0, # 'צ' + 17: 0, # 'ק' + 7: 0, # 'ר' + 10: 0, # 'ש' + 5: 0, # 'ת' + 32: 0, # '–' + 52: 0, # '’' + 47: 0, # '“' + 46: 0, # 'â€' + 58: 2, # '†' + 40: 0, # '…' + }, + 40: { # '…' + 50: 1, # 'a' + 60: 1, # 'c' + 61: 1, # 'd' + 42: 1, # 'e' + 53: 1, # 'i' + 56: 0, # 'l' + 54: 1, # 'n' + 49: 0, # 'o' + 51: 1, # 'r' + 43: 1, # 's' + 44: 1, # 't' + 63: 0, # 'u' + 34: 0, # '\xa0' + 55: 0, # '´' + 48: 0, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 0, # 'Ö°' + 59: 0, # 'Ö±' + 41: 0, # 'Ö²' + 33: 0, # 'Ö´' + 37: 0, # 'Öµ' + 36: 0, # 'Ö¶' + 31: 0, # 'Ö·' + 29: 0, # 'Ö¸' + 35: 0, # 'Ö¹' + 62: 0, # 'Ö»' + 28: 0, # 'Ö¼' + 38: 0, # '×' + 45: 0, # 'ׂ' + 9: 1, # '×' + 8: 0, # 'ב' + 20: 0, # '×’' + 16: 0, # 'ד' + 3: 1, # '×”' + 2: 1, # 'ו' + 24: 1, # '×–' + 14: 0, # '×—' + 22: 0, # 'ט' + 1: 1, # '×™' + 25: 0, # 'ך' + 15: 1, # '×›' + 4: 1, # 'ל' + 11: 0, # '×' + 6: 1, # 'מ' + 23: 0, # 'ן' + 12: 1, # '× ' + 19: 0, # 'ס' + 13: 0, # '×¢' + 26: 0, # '×£' + 18: 1, # 'פ' + 27: 0, # '×¥' + 21: 0, # 'צ' + 17: 0, # 'ק' + 7: 1, # 'ר' + 10: 1, # 'ש' + 5: 1, # 'ת' + 32: 0, # '–' + 52: 0, # '’' + 47: 0, # '“' + 46: 1, # 'â€' + 58: 0, # '†' + 40: 2, # '…' + }, +} + +# 255: Undefined characters that did not exist in training text # 254: Carriage/Return # 253: symbol (punctuation) that does not belong to word # 252: 0 - 9 +# 251: Control characters -# Windows-1255 language model -# Character Mapping Table: -WIN1255_CHAR_TO_ORDER_MAP = ( -255,255,255,255,255,255,255,255,255,255,254,255,255,254,255,255, # 00 -255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, # 10 -253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253, # 20 -252,252,252,252,252,252,252,252,252,252,253,253,253,253,253,253, # 30 -253, 69, 91, 79, 80, 92, 89, 97, 90, 68,111,112, 82, 73, 95, 85, # 40 - 78,121, 86, 71, 67,102,107, 84,114,103,115,253,253,253,253,253, # 50 -253, 50, 74, 60, 61, 42, 76, 70, 64, 53,105, 93, 56, 65, 54, 49, # 60 - 66,110, 51, 43, 44, 63, 81, 77, 98, 75,108,253,253,253,253,253, # 70 -124,202,203,204,205, 40, 58,206,207,208,209,210,211,212,213,214, -215, 83, 52, 47, 46, 72, 32, 94,216,113,217,109,218,219,220,221, - 34,116,222,118,100,223,224,117,119,104,125,225,226, 87, 99,227, -106,122,123,228, 55,229,230,101,231,232,120,233, 48, 39, 57,234, - 30, 59, 41, 88, 33, 37, 36, 31, 29, 35,235, 62, 28,236,126,237, -238, 38, 45,239,240,241,242,243,127,244,245,246,247,248,249,250, - 9, 8, 20, 16, 3, 2, 24, 14, 22, 1, 25, 15, 4, 11, 6, 23, - 12, 19, 13, 26, 18, 27, 21, 17, 7, 10, 5,251,252,128, 96,253, -) - -# Model Table: -# total sequences: 100% -# first 512 sequences: 98.4004% -# first 1024 sequences: 1.5981% -# rest sequences: 0.087% -# negative sequences: 0.0015% -HEBREW_LANG_MODEL = ( -0,3,3,3,3,3,3,3,3,3,3,2,3,3,3,3,3,3,3,3,3,3,3,2,3,2,1,2,0,1,0,0, -3,0,3,1,0,0,1,3,2,0,1,1,2,0,2,2,2,1,1,1,1,2,1,1,1,2,0,0,2,2,0,1, -3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,2,2,2,2, -1,2,1,2,1,2,0,0,2,0,0,0,0,0,1,0,1,0,0,0,0,0,0,1,0,0,0,0,0,0,1,0, -3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,2,2,2, -1,2,1,3,1,1,0,0,2,0,0,0,1,0,1,0,1,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0, -3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,1,0,1,2,2,1,3, -1,2,1,1,2,2,0,0,2,2,0,0,0,0,1,0,1,0,0,0,1,0,0,0,0,0,0,1,0,1,1,0, -3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,2,3,3,2,2,2,2,3,2, -1,2,1,2,2,2,0,0,1,0,0,0,0,0,1,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,1,0, -3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,2,3,3,2,3,2,2,3,2,2,2,1,2,2,2,2, -1,2,1,1,2,2,0,1,2,0,0,0,0,0,0,0,1,0,0,0,1,0,0,0,0,0,0,0,0,0,1,0, -3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,2,0,2,2,2,2,2, -0,2,0,2,2,2,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,1,0, -3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,2,3,0,2,2,2, -0,2,1,2,2,2,0,0,2,1,0,0,0,0,1,0,1,0,0,0,0,0,0,2,0,0,0,0,0,0,1,0, -3,3,3,3,3,3,3,3,3,3,3,2,3,3,3,3,3,3,3,3,3,3,3,3,3,2,1,2,3,2,2,2, -1,2,1,2,2,2,0,0,1,0,0,0,0,0,1,0,0,0,0,0,0,0,0,1,0,0,0,0,0,1,1,0, -3,3,3,3,3,3,3,3,3,2,3,3,3,2,3,3,3,3,3,3,3,3,3,3,3,3,3,1,0,2,0,2, -0,2,1,2,2,2,0,0,1,2,0,0,0,0,1,0,1,0,0,0,0,0,0,1,0,0,0,2,0,0,1,0, -3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,2,3,2,3,2,2,3,2,1,2,1,1,1, -0,1,1,1,1,1,3,0,1,0,0,0,0,2,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0, -3,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,1,1,0,1,1,0,0,1,0,0,1,0,0,0,0, -0,0,1,0,0,0,0,0,2,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,2,2,2,2,2,2,2, -0,2,0,1,2,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0, -3,3,3,3,3,3,3,3,3,2,3,3,3,2,1,2,3,3,2,3,3,3,3,2,3,2,1,2,0,2,1,2, -0,2,0,2,2,2,0,0,1,2,0,0,0,0,1,0,1,0,0,0,0,0,0,0,0,0,0,1,0,0,1,0, -3,3,3,3,3,3,3,3,3,2,3,3,3,1,2,2,3,3,2,3,2,3,2,2,3,1,2,2,0,2,2,2, -0,2,1,2,2,2,0,0,1,2,0,0,0,0,1,0,0,0,0,0,1,0,0,1,0,0,0,1,0,0,1,0, -3,3,3,3,3,3,3,3,3,3,3,3,3,2,3,3,3,2,3,3,2,2,2,3,3,3,3,1,3,2,2,2, -0,2,0,1,2,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0, -3,3,3,3,3,3,3,3,3,3,3,3,3,3,2,2,3,3,3,2,3,2,2,2,1,2,2,0,2,2,2,2, -0,2,0,2,2,2,0,0,1,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0, -3,3,3,3,3,3,3,3,3,3,3,2,3,3,3,1,3,2,3,3,2,3,3,2,2,1,2,2,2,2,2,2, -0,2,1,2,1,2,0,0,1,0,0,0,0,0,1,0,0,0,0,0,1,0,0,1,0,0,0,0,0,0,1,0, -3,3,3,3,3,3,2,3,2,3,3,2,3,3,3,3,2,3,2,3,3,3,3,3,2,2,2,2,2,2,2,1, -0,2,0,1,2,1,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,1,0, -3,3,3,3,3,3,3,3,3,2,1,2,3,3,3,3,3,3,3,2,3,2,3,2,1,2,3,0,2,1,2,2, -0,2,1,1,2,1,0,0,1,0,0,0,0,0,1,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,2,0, -3,3,3,3,3,3,3,3,3,2,3,3,3,3,2,1,3,1,2,2,2,1,2,3,3,1,2,1,2,2,2,2, -0,1,1,1,1,1,0,0,0,0,0,0,0,0,1,0,0,0,0,0,1,0,0,2,0,0,0,0,0,0,0,0, -3,3,3,3,3,3,3,3,3,3,0,2,3,3,3,1,3,3,3,1,2,2,2,2,1,1,2,2,2,2,2,2, -0,2,0,1,1,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,1,0, -3,3,3,3,3,3,2,3,3,3,2,2,3,3,3,2,1,2,3,2,3,2,2,2,2,1,2,1,1,1,2,2, -0,2,1,1,1,1,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0, -3,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,1,0,0,0,1,0,0,0,0,0, -1,0,1,0,0,0,0,0,2,0,0,0,0,0,1,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -3,3,3,3,3,2,3,3,2,3,1,2,2,2,2,3,2,3,1,1,2,2,1,2,2,1,1,0,2,2,2,2, -0,1,0,1,2,2,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,1,0, -3,0,0,1,1,0,1,0,0,1,1,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,1,2,2,0, -0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -3,0,1,0,1,0,1,1,0,1,1,0,0,0,1,1,0,1,1,1,0,0,0,0,0,0,1,0,0,0,0,0, -0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -3,0,0,0,1,1,0,1,0,1,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0, -3,2,2,1,2,2,2,2,2,2,2,1,2,2,1,2,2,1,1,1,1,1,1,1,1,2,1,1,0,3,3,3, -0,3,0,2,2,2,2,0,0,1,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0, -2,2,2,3,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,1,2,2,1,2,2,2,1,1,1,2,0,1, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -2,2,2,2,2,2,2,2,2,2,2,1,2,2,2,2,2,2,2,2,2,2,2,0,2,2,0,0,0,0,0,0, -0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -2,3,1,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,1,2,1,0,2,1,0, -0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -3,1,1,1,1,1,1,1,1,1,1,0,0,1,1,1,1,0,1,1,1,1,0,0,0,0,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0, -0,3,1,1,2,2,2,2,2,1,2,2,2,1,1,2,2,2,2,2,2,2,1,2,2,1,0,1,1,1,1,0, -0,1,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -3,2,1,1,1,1,2,1,1,2,1,0,1,1,1,1,1,1,1,1,1,1,1,0,1,0,0,0,0,0,0,0, -0,0,2,0,0,0,0,0,0,0,0,1,1,0,0,0,0,1,1,0,0,1,1,0,0,0,0,0,0,1,0,0, -2,1,1,2,2,2,2,2,2,2,2,2,2,2,1,2,2,2,2,2,1,2,1,2,1,1,1,1,0,0,0,0, -0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -1,2,1,2,2,2,2,2,2,2,2,2,2,1,2,1,2,1,1,2,1,1,1,2,1,2,1,2,0,1,0,1, -0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,3,1,2,2,2,1,2,2,2,2,2,2,2,2,1,2,1,1,1,1,1,1,2,1,2,1,1,0,1,0,1, -0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -2,1,2,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,2,2, -0,2,0,1,2,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0, -3,0,0,0,1,0,0,0,0,0,0,0,0,0,0,1,0,1,0,0,0,1,0,0,0,0,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -2,1,1,1,1,1,1,1,0,1,1,0,1,0,0,1,0,0,1,0,0,0,0,0,1,0,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,2,0,1,1,1,0,1,0,0,0,1,1,0,1,1,0,0,0,0,0,1,1,0,0, -0,1,1,1,2,1,2,2,2,0,2,0,2,0,1,1,2,1,1,1,1,2,1,0,1,1,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0, -1,0,1,0,0,0,0,0,1,0,1,2,2,0,1,0,0,1,1,2,2,1,2,0,2,0,0,0,1,2,0,1, -2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,2,0,2,1,2,0,2,0,0,1,1,1,1,1,1,0,1,0,0,0,1,0,0,1, -2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,1,0,0,0,0,0,1,0,2,1,1,0,1,0,0,1,1,1,2,2,0,0,1,0,0,0,1,0,0,1, -1,1,2,1,0,1,1,1,0,1,0,1,1,1,1,0,0,0,1,0,1,0,0,0,0,0,0,0,0,2,2,1, -0,2,0,1,2,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -2,1,0,0,1,0,1,1,1,1,0,0,0,0,0,1,0,0,0,0,1,1,0,0,0,0,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -1,1,1,1,1,1,1,1,1,2,1,0,1,1,1,1,1,1,1,1,1,1,1,0,1,0,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,1,1,1,0,0,0,0,1,1,1,0,1,1,0,1,0,0,0,1,1,0,1, -2,0,1,0,1,0,1,0,0,1,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,1,0,1,1,1,0,1,0,0,1,1,2,1,1,2,0,1,0,0,0,1,1,0,1, -1,0,0,1,0,0,1,0,0,0,1,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,1,0,1,1,2,0,1,0,0,0,0,2,1,1,2,0,2,0,0,0,1,1,0,1, -1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,1,0,2,1,1,0,1,0,0,2,2,1,2,1,1,0,1,0,0,0,1,1,0,1, -2,0,1,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,1,2,2,0,0,0,0,0,1,1,0,1,0,0,1,0,0,0,0,1,0,1, -1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,1,2,2,0,0,0,0,2,1,1,1,0,2,1,1,0,0,0,2,1,0,1, -1,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,1,0,1,1,2,0,1,0,0,1,1,0,2,1,1,0,1,0,0,0,1,1,0,1, -2,2,1,1,1,0,1,1,0,1,1,0,1,0,0,0,0,0,0,1,0,0,0,1,0,0,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,1,0,2,1,1,0,1,0,0,1,1,0,1,2,1,0,2,0,0,0,1,1,0,1, -2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0, -0,1,0,0,2,0,2,1,1,0,1,0,1,0,0,1,0,0,0,0,1,0,0,0,1,0,0,0,0,0,1,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,1,0,1,1,2,0,1,0,0,1,1,1,0,1,0,0,1,0,0,0,1,0,0,1, -1,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -1,0,0,0,0,0,0,0,1,0,1,1,0,0,1,0,0,2,1,1,1,1,1,0,1,0,0,0,0,1,0,1, -0,1,1,1,2,1,1,1,1,0,1,1,1,1,1,1,1,1,1,1,1,1,0,1,1,0,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,1,2,1,0,0,0,0,0,1,1,1,1,1,0,1,0,0,0,1,1,0,0, -) - -Win1255HebrewModel = { - 'char_to_order_map': WIN1255_CHAR_TO_ORDER_MAP, - 'precedence_matrix': HEBREW_LANG_MODEL, - 'typical_positive_ratio': 0.984004, - 'keep_english_letter': False, - 'charset_name': "windows-1255", - 'language': 'Hebrew', +# Character Mapping Table(s): +WINDOWS_1255_HEBREW_CHAR_TO_ORDER = { + 0: 255, # '\x00' + 1: 255, # '\x01' + 2: 255, # '\x02' + 3: 255, # '\x03' + 4: 255, # '\x04' + 5: 255, # '\x05' + 6: 255, # '\x06' + 7: 255, # '\x07' + 8: 255, # '\x08' + 9: 255, # '\t' + 10: 254, # '\n' + 11: 255, # '\x0b' + 12: 255, # '\x0c' + 13: 254, # '\r' + 14: 255, # '\x0e' + 15: 255, # '\x0f' + 16: 255, # '\x10' + 17: 255, # '\x11' + 18: 255, # '\x12' + 19: 255, # '\x13' + 20: 255, # '\x14' + 21: 255, # '\x15' + 22: 255, # '\x16' + 23: 255, # '\x17' + 24: 255, # '\x18' + 25: 255, # '\x19' + 26: 255, # '\x1a' + 27: 255, # '\x1b' + 28: 255, # '\x1c' + 29: 255, # '\x1d' + 30: 255, # '\x1e' + 31: 255, # '\x1f' + 32: 253, # ' ' + 33: 253, # '!' + 34: 253, # '"' + 35: 253, # '#' + 36: 253, # '$' + 37: 253, # '%' + 38: 253, # '&' + 39: 253, # "'" + 40: 253, # '(' + 41: 253, # ')' + 42: 253, # '*' + 43: 253, # '+' + 44: 253, # ',' + 45: 253, # '-' + 46: 253, # '.' + 47: 253, # '/' + 48: 252, # '0' + 49: 252, # '1' + 50: 252, # '2' + 51: 252, # '3' + 52: 252, # '4' + 53: 252, # '5' + 54: 252, # '6' + 55: 252, # '7' + 56: 252, # '8' + 57: 252, # '9' + 58: 253, # ':' + 59: 253, # ';' + 60: 253, # '<' + 61: 253, # '=' + 62: 253, # '>' + 63: 253, # '?' + 64: 253, # '@' + 65: 69, # 'A' + 66: 91, # 'B' + 67: 79, # 'C' + 68: 80, # 'D' + 69: 92, # 'E' + 70: 89, # 'F' + 71: 97, # 'G' + 72: 90, # 'H' + 73: 68, # 'I' + 74: 111, # 'J' + 75: 112, # 'K' + 76: 82, # 'L' + 77: 73, # 'M' + 78: 95, # 'N' + 79: 85, # 'O' + 80: 78, # 'P' + 81: 121, # 'Q' + 82: 86, # 'R' + 83: 71, # 'S' + 84: 67, # 'T' + 85: 102, # 'U' + 86: 107, # 'V' + 87: 84, # 'W' + 88: 114, # 'X' + 89: 103, # 'Y' + 90: 115, # 'Z' + 91: 253, # '[' + 92: 253, # '\\' + 93: 253, # ']' + 94: 253, # '^' + 95: 253, # '_' + 96: 253, # '`' + 97: 50, # 'a' + 98: 74, # 'b' + 99: 60, # 'c' + 100: 61, # 'd' + 101: 42, # 'e' + 102: 76, # 'f' + 103: 70, # 'g' + 104: 64, # 'h' + 105: 53, # 'i' + 106: 105, # 'j' + 107: 93, # 'k' + 108: 56, # 'l' + 109: 65, # 'm' + 110: 54, # 'n' + 111: 49, # 'o' + 112: 66, # 'p' + 113: 110, # 'q' + 114: 51, # 'r' + 115: 43, # 's' + 116: 44, # 't' + 117: 63, # 'u' + 118: 81, # 'v' + 119: 77, # 'w' + 120: 98, # 'x' + 121: 75, # 'y' + 122: 108, # 'z' + 123: 253, # '{' + 124: 253, # '|' + 125: 253, # '}' + 126: 253, # '~' + 127: 253, # '\x7f' + 128: 124, # '€' + 129: 202, # None + 130: 203, # '‚' + 131: 204, # 'Æ’' + 132: 205, # '„' + 133: 40, # '…' + 134: 58, # '†' + 135: 206, # '‡' + 136: 207, # 'ˆ' + 137: 208, # '‰' + 138: 209, # None + 139: 210, # '‹' + 140: 211, # None + 141: 212, # None + 142: 213, # None + 143: 214, # None + 144: 215, # None + 145: 83, # '‘' + 146: 52, # '’' + 147: 47, # '“' + 148: 46, # 'â€' + 149: 72, # '•' + 150: 32, # '–' + 151: 94, # '—' + 152: 216, # 'Ëœ' + 153: 113, # 'â„¢' + 154: 217, # None + 155: 109, # '›' + 156: 218, # None + 157: 219, # None + 158: 220, # None + 159: 221, # None + 160: 34, # '\xa0' + 161: 116, # '¡' + 162: 222, # '¢' + 163: 118, # '£' + 164: 100, # '₪' + 165: 223, # 'Â¥' + 166: 224, # '¦' + 167: 117, # '§' + 168: 119, # '¨' + 169: 104, # '©' + 170: 125, # '×' + 171: 225, # '«' + 172: 226, # '¬' + 173: 87, # '\xad' + 174: 99, # '®' + 175: 227, # '¯' + 176: 106, # '°' + 177: 122, # '±' + 178: 123, # '²' + 179: 228, # '³' + 180: 55, # '´' + 181: 229, # 'µ' + 182: 230, # '¶' + 183: 101, # '·' + 184: 231, # '¸' + 185: 232, # '¹' + 186: 120, # '÷' + 187: 233, # '»' + 188: 48, # '¼' + 189: 39, # '½' + 190: 57, # '¾' + 191: 234, # '¿' + 192: 30, # 'Ö°' + 193: 59, # 'Ö±' + 194: 41, # 'Ö²' + 195: 88, # 'Ö³' + 196: 33, # 'Ö´' + 197: 37, # 'Öµ' + 198: 36, # 'Ö¶' + 199: 31, # 'Ö·' + 200: 29, # 'Ö¸' + 201: 35, # 'Ö¹' + 202: 235, # None + 203: 62, # 'Ö»' + 204: 28, # 'Ö¼' + 205: 236, # 'Ö½' + 206: 126, # 'Ö¾' + 207: 237, # 'Ö¿' + 208: 238, # '×€' + 209: 38, # '×' + 210: 45, # 'ׂ' + 211: 239, # '׃' + 212: 240, # '×°' + 213: 241, # '×±' + 214: 242, # 'ײ' + 215: 243, # '׳' + 216: 127, # '×´' + 217: 244, # None + 218: 245, # None + 219: 246, # None + 220: 247, # None + 221: 248, # None + 222: 249, # None + 223: 250, # None + 224: 9, # '×' + 225: 8, # 'ב' + 226: 20, # '×’' + 227: 16, # 'ד' + 228: 3, # '×”' + 229: 2, # 'ו' + 230: 24, # '×–' + 231: 14, # '×—' + 232: 22, # 'ט' + 233: 1, # '×™' + 234: 25, # 'ך' + 235: 15, # '×›' + 236: 4, # 'ל' + 237: 11, # '×' + 238: 6, # 'מ' + 239: 23, # 'ן' + 240: 12, # '× ' + 241: 19, # 'ס' + 242: 13, # '×¢' + 243: 26, # '×£' + 244: 18, # 'פ' + 245: 27, # '×¥' + 246: 21, # 'צ' + 247: 17, # 'ק' + 248: 7, # 'ר' + 249: 10, # 'ש' + 250: 5, # 'ת' + 251: 251, # None + 252: 252, # None + 253: 128, # '\u200e' + 254: 96, # '\u200f' + 255: 253, # None } + +WINDOWS_1255_HEBREW_MODEL = SingleByteCharSetModel(charset_name='windows-1255', + language='Hebrew', + char_to_order_map=WINDOWS_1255_HEBREW_CHAR_TO_ORDER, + language_model=HEBREW_LANG_MODEL, + typical_positive_ratio=0.984004, + keep_ascii_letters=False, + alphabet='×בגדהוזחטיךכל×מןנסעףפץצקרשתװױײ') + diff --git a/libs/chardet/langhungarianmodel.py b/libs/chardet/langhungarianmodel.py index bb7c095e1..24a097f52 100644 --- a/libs/chardet/langhungarianmodel.py +++ b/libs/chardet/langhungarianmodel.py @@ -1,225 +1,4650 @@ -######################## BEGIN LICENSE BLOCK ######################## -# The Original Code is Mozilla Communicator client code. -# -# The Initial Developer of the Original Code is -# Netscape Communications Corporation. -# Portions created by the Initial Developer are Copyright (C) 1998 -# the Initial Developer. All Rights Reserved. -# -# Contributor(s): -# Mark Pilgrim - port to Python -# -# This library is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 2.1 of the License, or (at your option) any later version. -# -# This library is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with this library; if not, write to the Free Software -# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA -# 02110-1301 USA -######################### END LICENSE BLOCK ######################### +#!/usr/bin/env python +# -*- coding: utf-8 -*- -# 255: Control characters that usually does not exist in any text +from chardet.sbcharsetprober import SingleByteCharSetModel + + +# 3: Positive +# 2: Likely +# 1: Unlikely +# 0: Negative + +HUNGARIAN_LANG_MODEL = { + 28: { # 'A' + 28: 0, # 'A' + 40: 1, # 'B' + 54: 1, # 'C' + 45: 2, # 'D' + 32: 1, # 'E' + 50: 1, # 'F' + 49: 2, # 'G' + 38: 1, # 'H' + 39: 2, # 'I' + 53: 1, # 'J' + 36: 2, # 'K' + 41: 2, # 'L' + 34: 1, # 'M' + 35: 2, # 'N' + 47: 1, # 'O' + 46: 2, # 'P' + 43: 2, # 'R' + 33: 2, # 'S' + 37: 2, # 'T' + 57: 1, # 'U' + 48: 1, # 'V' + 55: 1, # 'Y' + 52: 2, # 'Z' + 2: 0, # 'a' + 18: 1, # 'b' + 26: 1, # 'c' + 17: 2, # 'd' + 1: 1, # 'e' + 27: 1, # 'f' + 12: 1, # 'g' + 20: 1, # 'h' + 9: 1, # 'i' + 22: 1, # 'j' + 7: 2, # 'k' + 6: 2, # 'l' + 13: 2, # 'm' + 4: 2, # 'n' + 8: 0, # 'o' + 23: 2, # 'p' + 10: 2, # 'r' + 5: 1, # 's' + 3: 1, # 't' + 21: 1, # 'u' + 19: 1, # 'v' + 62: 1, # 'x' + 16: 0, # 'y' + 11: 3, # 'z' + 51: 1, # 'Ã' + 44: 0, # 'É' + 61: 1, # 'Ã' + 58: 0, # 'Ó' + 59: 0, # 'Ö' + 60: 0, # 'Ú' + 63: 0, # 'Ãœ' + 14: 0, # 'á' + 15: 0, # 'é' + 30: 0, # 'í' + 25: 0, # 'ó' + 24: 0, # 'ö' + 31: 0, # 'ú' + 29: 0, # 'ü' + 42: 0, # 'Å‘' + 56: 0, # 'ű' + }, + 40: { # 'B' + 28: 2, # 'A' + 40: 1, # 'B' + 54: 1, # 'C' + 45: 1, # 'D' + 32: 2, # 'E' + 50: 0, # 'F' + 49: 0, # 'G' + 38: 0, # 'H' + 39: 1, # 'I' + 53: 1, # 'J' + 36: 1, # 'K' + 41: 1, # 'L' + 34: 0, # 'M' + 35: 1, # 'N' + 47: 2, # 'O' + 46: 0, # 'P' + 43: 1, # 'R' + 33: 1, # 'S' + 37: 1, # 'T' + 57: 1, # 'U' + 48: 1, # 'V' + 55: 0, # 'Y' + 52: 0, # 'Z' + 2: 2, # 'a' + 18: 0, # 'b' + 26: 0, # 'c' + 17: 0, # 'd' + 1: 3, # 'e' + 27: 0, # 'f' + 12: 0, # 'g' + 20: 0, # 'h' + 9: 2, # 'i' + 22: 1, # 'j' + 7: 0, # 'k' + 6: 1, # 'l' + 13: 0, # 'm' + 4: 0, # 'n' + 8: 2, # 'o' + 23: 1, # 'p' + 10: 2, # 'r' + 5: 0, # 's' + 3: 0, # 't' + 21: 3, # 'u' + 19: 0, # 'v' + 62: 0, # 'x' + 16: 1, # 'y' + 11: 0, # 'z' + 51: 1, # 'Ã' + 44: 1, # 'É' + 61: 1, # 'Ã' + 58: 1, # 'Ó' + 59: 1, # 'Ö' + 60: 1, # 'Ú' + 63: 1, # 'Ãœ' + 14: 2, # 'á' + 15: 2, # 'é' + 30: 1, # 'í' + 25: 1, # 'ó' + 24: 1, # 'ö' + 31: 1, # 'ú' + 29: 1, # 'ü' + 42: 1, # 'Å‘' + 56: 1, # 'ű' + }, + 54: { # 'C' + 28: 1, # 'A' + 40: 1, # 'B' + 54: 1, # 'C' + 45: 1, # 'D' + 32: 1, # 'E' + 50: 0, # 'F' + 49: 0, # 'G' + 38: 1, # 'H' + 39: 2, # 'I' + 53: 1, # 'J' + 36: 1, # 'K' + 41: 1, # 'L' + 34: 1, # 'M' + 35: 0, # 'N' + 47: 1, # 'O' + 46: 1, # 'P' + 43: 1, # 'R' + 33: 2, # 'S' + 37: 1, # 'T' + 57: 1, # 'U' + 48: 0, # 'V' + 55: 1, # 'Y' + 52: 1, # 'Z' + 2: 2, # 'a' + 18: 0, # 'b' + 26: 0, # 'c' + 17: 0, # 'd' + 1: 1, # 'e' + 27: 0, # 'f' + 12: 0, # 'g' + 20: 1, # 'h' + 9: 1, # 'i' + 22: 0, # 'j' + 7: 0, # 'k' + 6: 1, # 'l' + 13: 0, # 'm' + 4: 0, # 'n' + 8: 2, # 'o' + 23: 0, # 'p' + 10: 1, # 'r' + 5: 3, # 's' + 3: 0, # 't' + 21: 1, # 'u' + 19: 0, # 'v' + 62: 0, # 'x' + 16: 1, # 'y' + 11: 1, # 'z' + 51: 1, # 'Ã' + 44: 1, # 'É' + 61: 1, # 'Ã' + 58: 0, # 'Ó' + 59: 0, # 'Ö' + 60: 0, # 'Ú' + 63: 0, # 'Ãœ' + 14: 1, # 'á' + 15: 1, # 'é' + 30: 1, # 'í' + 25: 1, # 'ó' + 24: 0, # 'ö' + 31: 0, # 'ú' + 29: 0, # 'ü' + 42: 0, # 'Å‘' + 56: 0, # 'ű' + }, + 45: { # 'D' + 28: 2, # 'A' + 40: 1, # 'B' + 54: 0, # 'C' + 45: 1, # 'D' + 32: 2, # 'E' + 50: 1, # 'F' + 49: 1, # 'G' + 38: 1, # 'H' + 39: 2, # 'I' + 53: 1, # 'J' + 36: 1, # 'K' + 41: 0, # 'L' + 34: 1, # 'M' + 35: 1, # 'N' + 47: 2, # 'O' + 46: 0, # 'P' + 43: 1, # 'R' + 33: 1, # 'S' + 37: 1, # 'T' + 57: 1, # 'U' + 48: 1, # 'V' + 55: 1, # 'Y' + 52: 1, # 'Z' + 2: 2, # 'a' + 18: 0, # 'b' + 26: 0, # 'c' + 17: 0, # 'd' + 1: 3, # 'e' + 27: 0, # 'f' + 12: 0, # 'g' + 20: 0, # 'h' + 9: 1, # 'i' + 22: 0, # 'j' + 7: 0, # 'k' + 6: 0, # 'l' + 13: 0, # 'm' + 4: 0, # 'n' + 8: 1, # 'o' + 23: 0, # 'p' + 10: 2, # 'r' + 5: 0, # 's' + 3: 0, # 't' + 21: 2, # 'u' + 19: 0, # 'v' + 62: 0, # 'x' + 16: 1, # 'y' + 11: 1, # 'z' + 51: 1, # 'Ã' + 44: 1, # 'É' + 61: 1, # 'Ã' + 58: 1, # 'Ó' + 59: 1, # 'Ö' + 60: 1, # 'Ú' + 63: 1, # 'Ãœ' + 14: 1, # 'á' + 15: 1, # 'é' + 30: 1, # 'í' + 25: 1, # 'ó' + 24: 1, # 'ö' + 31: 1, # 'ú' + 29: 1, # 'ü' + 42: 1, # 'Å‘' + 56: 0, # 'ű' + }, + 32: { # 'E' + 28: 1, # 'A' + 40: 1, # 'B' + 54: 1, # 'C' + 45: 1, # 'D' + 32: 1, # 'E' + 50: 1, # 'F' + 49: 2, # 'G' + 38: 1, # 'H' + 39: 1, # 'I' + 53: 1, # 'J' + 36: 2, # 'K' + 41: 2, # 'L' + 34: 2, # 'M' + 35: 2, # 'N' + 47: 1, # 'O' + 46: 1, # 'P' + 43: 2, # 'R' + 33: 2, # 'S' + 37: 2, # 'T' + 57: 1, # 'U' + 48: 1, # 'V' + 55: 1, # 'Y' + 52: 1, # 'Z' + 2: 1, # 'a' + 18: 1, # 'b' + 26: 1, # 'c' + 17: 2, # 'd' + 1: 1, # 'e' + 27: 1, # 'f' + 12: 3, # 'g' + 20: 1, # 'h' + 9: 1, # 'i' + 22: 1, # 'j' + 7: 1, # 'k' + 6: 2, # 'l' + 13: 2, # 'm' + 4: 2, # 'n' + 8: 0, # 'o' + 23: 1, # 'p' + 10: 2, # 'r' + 5: 2, # 's' + 3: 1, # 't' + 21: 2, # 'u' + 19: 1, # 'v' + 62: 1, # 'x' + 16: 0, # 'y' + 11: 3, # 'z' + 51: 1, # 'Ã' + 44: 1, # 'É' + 61: 0, # 'Ã' + 58: 1, # 'Ó' + 59: 1, # 'Ö' + 60: 0, # 'Ú' + 63: 1, # 'Ãœ' + 14: 0, # 'á' + 15: 0, # 'é' + 30: 0, # 'í' + 25: 0, # 'ó' + 24: 1, # 'ö' + 31: 0, # 'ú' + 29: 0, # 'ü' + 42: 0, # 'Å‘' + 56: 0, # 'ű' + }, + 50: { # 'F' + 28: 1, # 'A' + 40: 0, # 'B' + 54: 0, # 'C' + 45: 0, # 'D' + 32: 1, # 'E' + 50: 1, # 'F' + 49: 0, # 'G' + 38: 1, # 'H' + 39: 1, # 'I' + 53: 1, # 'J' + 36: 1, # 'K' + 41: 1, # 'L' + 34: 1, # 'M' + 35: 1, # 'N' + 47: 1, # 'O' + 46: 0, # 'P' + 43: 1, # 'R' + 33: 0, # 'S' + 37: 1, # 'T' + 57: 1, # 'U' + 48: 0, # 'V' + 55: 1, # 'Y' + 52: 0, # 'Z' + 2: 2, # 'a' + 18: 0, # 'b' + 26: 0, # 'c' + 17: 0, # 'd' + 1: 2, # 'e' + 27: 1, # 'f' + 12: 0, # 'g' + 20: 0, # 'h' + 9: 2, # 'i' + 22: 1, # 'j' + 7: 0, # 'k' + 6: 1, # 'l' + 13: 0, # 'm' + 4: 0, # 'n' + 8: 2, # 'o' + 23: 0, # 'p' + 10: 2, # 'r' + 5: 0, # 's' + 3: 0, # 't' + 21: 1, # 'u' + 19: 0, # 'v' + 62: 0, # 'x' + 16: 0, # 'y' + 11: 0, # 'z' + 51: 1, # 'Ã' + 44: 1, # 'É' + 61: 0, # 'Ã' + 58: 1, # 'Ó' + 59: 1, # 'Ö' + 60: 0, # 'Ú' + 63: 1, # 'Ãœ' + 14: 1, # 'á' + 15: 1, # 'é' + 30: 0, # 'í' + 25: 0, # 'ó' + 24: 2, # 'ö' + 31: 1, # 'ú' + 29: 1, # 'ü' + 42: 1, # 'Å‘' + 56: 1, # 'ű' + }, + 49: { # 'G' + 28: 2, # 'A' + 40: 1, # 'B' + 54: 1, # 'C' + 45: 1, # 'D' + 32: 2, # 'E' + 50: 1, # 'F' + 49: 1, # 'G' + 38: 1, # 'H' + 39: 1, # 'I' + 53: 1, # 'J' + 36: 1, # 'K' + 41: 1, # 'L' + 34: 1, # 'M' + 35: 1, # 'N' + 47: 1, # 'O' + 46: 1, # 'P' + 43: 1, # 'R' + 33: 1, # 'S' + 37: 1, # 'T' + 57: 1, # 'U' + 48: 1, # 'V' + 55: 2, # 'Y' + 52: 1, # 'Z' + 2: 2, # 'a' + 18: 0, # 'b' + 26: 0, # 'c' + 17: 0, # 'd' + 1: 2, # 'e' + 27: 0, # 'f' + 12: 0, # 'g' + 20: 0, # 'h' + 9: 1, # 'i' + 22: 0, # 'j' + 7: 0, # 'k' + 6: 1, # 'l' + 13: 0, # 'm' + 4: 0, # 'n' + 8: 2, # 'o' + 23: 0, # 'p' + 10: 2, # 'r' + 5: 0, # 's' + 3: 0, # 't' + 21: 1, # 'u' + 19: 0, # 'v' + 62: 0, # 'x' + 16: 2, # 'y' + 11: 0, # 'z' + 51: 1, # 'Ã' + 44: 1, # 'É' + 61: 1, # 'Ã' + 58: 1, # 'Ó' + 59: 1, # 'Ö' + 60: 1, # 'Ú' + 63: 1, # 'Ãœ' + 14: 1, # 'á' + 15: 1, # 'é' + 30: 0, # 'í' + 25: 1, # 'ó' + 24: 1, # 'ö' + 31: 1, # 'ú' + 29: 1, # 'ü' + 42: 1, # 'Å‘' + 56: 0, # 'ű' + }, + 38: { # 'H' + 28: 2, # 'A' + 40: 1, # 'B' + 54: 1, # 'C' + 45: 0, # 'D' + 32: 1, # 'E' + 50: 0, # 'F' + 49: 0, # 'G' + 38: 0, # 'H' + 39: 1, # 'I' + 53: 0, # 'J' + 36: 0, # 'K' + 41: 1, # 'L' + 34: 0, # 'M' + 35: 0, # 'N' + 47: 1, # 'O' + 46: 0, # 'P' + 43: 1, # 'R' + 33: 1, # 'S' + 37: 1, # 'T' + 57: 1, # 'U' + 48: 0, # 'V' + 55: 1, # 'Y' + 52: 0, # 'Z' + 2: 3, # 'a' + 18: 0, # 'b' + 26: 0, # 'c' + 17: 0, # 'd' + 1: 2, # 'e' + 27: 0, # 'f' + 12: 0, # 'g' + 20: 0, # 'h' + 9: 2, # 'i' + 22: 1, # 'j' + 7: 0, # 'k' + 6: 1, # 'l' + 13: 1, # 'm' + 4: 0, # 'n' + 8: 3, # 'o' + 23: 0, # 'p' + 10: 1, # 'r' + 5: 0, # 's' + 3: 0, # 't' + 21: 2, # 'u' + 19: 0, # 'v' + 62: 0, # 'x' + 16: 1, # 'y' + 11: 0, # 'z' + 51: 2, # 'Ã' + 44: 2, # 'É' + 61: 1, # 'Ã' + 58: 1, # 'Ó' + 59: 1, # 'Ö' + 60: 1, # 'Ú' + 63: 1, # 'Ãœ' + 14: 2, # 'á' + 15: 1, # 'é' + 30: 2, # 'í' + 25: 1, # 'ó' + 24: 1, # 'ö' + 31: 1, # 'ú' + 29: 1, # 'ü' + 42: 1, # 'Å‘' + 56: 1, # 'ű' + }, + 39: { # 'I' + 28: 2, # 'A' + 40: 1, # 'B' + 54: 1, # 'C' + 45: 1, # 'D' + 32: 1, # 'E' + 50: 1, # 'F' + 49: 1, # 'G' + 38: 1, # 'H' + 39: 2, # 'I' + 53: 1, # 'J' + 36: 2, # 'K' + 41: 2, # 'L' + 34: 1, # 'M' + 35: 2, # 'N' + 47: 1, # 'O' + 46: 1, # 'P' + 43: 1, # 'R' + 33: 2, # 'S' + 37: 1, # 'T' + 57: 1, # 'U' + 48: 1, # 'V' + 55: 0, # 'Y' + 52: 2, # 'Z' + 2: 0, # 'a' + 18: 1, # 'b' + 26: 1, # 'c' + 17: 2, # 'd' + 1: 0, # 'e' + 27: 1, # 'f' + 12: 2, # 'g' + 20: 1, # 'h' + 9: 0, # 'i' + 22: 1, # 'j' + 7: 1, # 'k' + 6: 2, # 'l' + 13: 2, # 'm' + 4: 1, # 'n' + 8: 0, # 'o' + 23: 1, # 'p' + 10: 2, # 'r' + 5: 2, # 's' + 3: 2, # 't' + 21: 0, # 'u' + 19: 1, # 'v' + 62: 0, # 'x' + 16: 0, # 'y' + 11: 1, # 'z' + 51: 1, # 'Ã' + 44: 1, # 'É' + 61: 0, # 'Ã' + 58: 1, # 'Ó' + 59: 1, # 'Ö' + 60: 1, # 'Ú' + 63: 1, # 'Ãœ' + 14: 0, # 'á' + 15: 0, # 'é' + 30: 0, # 'í' + 25: 0, # 'ó' + 24: 0, # 'ö' + 31: 0, # 'ú' + 29: 0, # 'ü' + 42: 0, # 'Å‘' + 56: 0, # 'ű' + }, + 53: { # 'J' + 28: 2, # 'A' + 40: 0, # 'B' + 54: 1, # 'C' + 45: 1, # 'D' + 32: 2, # 'E' + 50: 0, # 'F' + 49: 0, # 'G' + 38: 1, # 'H' + 39: 1, # 'I' + 53: 1, # 'J' + 36: 1, # 'K' + 41: 1, # 'L' + 34: 1, # 'M' + 35: 1, # 'N' + 47: 1, # 'O' + 46: 0, # 'P' + 43: 0, # 'R' + 33: 1, # 'S' + 37: 1, # 'T' + 57: 1, # 'U' + 48: 0, # 'V' + 55: 0, # 'Y' + 52: 1, # 'Z' + 2: 2, # 'a' + 18: 0, # 'b' + 26: 0, # 'c' + 17: 0, # 'd' + 1: 2, # 'e' + 27: 0, # 'f' + 12: 0, # 'g' + 20: 0, # 'h' + 9: 1, # 'i' + 22: 0, # 'j' + 7: 0, # 'k' + 6: 0, # 'l' + 13: 0, # 'm' + 4: 0, # 'n' + 8: 1, # 'o' + 23: 0, # 'p' + 10: 0, # 'r' + 5: 0, # 's' + 3: 0, # 't' + 21: 2, # 'u' + 19: 0, # 'v' + 62: 0, # 'x' + 16: 0, # 'y' + 11: 0, # 'z' + 51: 1, # 'Ã' + 44: 1, # 'É' + 61: 0, # 'Ã' + 58: 1, # 'Ó' + 59: 1, # 'Ö' + 60: 1, # 'Ú' + 63: 1, # 'Ãœ' + 14: 2, # 'á' + 15: 1, # 'é' + 30: 0, # 'í' + 25: 2, # 'ó' + 24: 2, # 'ö' + 31: 1, # 'ú' + 29: 0, # 'ü' + 42: 1, # 'Å‘' + 56: 0, # 'ű' + }, + 36: { # 'K' + 28: 2, # 'A' + 40: 1, # 'B' + 54: 1, # 'C' + 45: 1, # 'D' + 32: 2, # 'E' + 50: 1, # 'F' + 49: 0, # 'G' + 38: 1, # 'H' + 39: 2, # 'I' + 53: 1, # 'J' + 36: 1, # 'K' + 41: 1, # 'L' + 34: 1, # 'M' + 35: 1, # 'N' + 47: 2, # 'O' + 46: 0, # 'P' + 43: 1, # 'R' + 33: 1, # 'S' + 37: 1, # 'T' + 57: 1, # 'U' + 48: 1, # 'V' + 55: 1, # 'Y' + 52: 0, # 'Z' + 2: 2, # 'a' + 18: 0, # 'b' + 26: 0, # 'c' + 17: 0, # 'd' + 1: 2, # 'e' + 27: 1, # 'f' + 12: 0, # 'g' + 20: 1, # 'h' + 9: 3, # 'i' + 22: 0, # 'j' + 7: 0, # 'k' + 6: 1, # 'l' + 13: 1, # 'm' + 4: 1, # 'n' + 8: 2, # 'o' + 23: 0, # 'p' + 10: 2, # 'r' + 5: 0, # 's' + 3: 0, # 't' + 21: 1, # 'u' + 19: 1, # 'v' + 62: 0, # 'x' + 16: 1, # 'y' + 11: 0, # 'z' + 51: 1, # 'Ã' + 44: 1, # 'É' + 61: 1, # 'Ã' + 58: 1, # 'Ó' + 59: 2, # 'Ö' + 60: 1, # 'Ú' + 63: 1, # 'Ãœ' + 14: 2, # 'á' + 15: 2, # 'é' + 30: 1, # 'í' + 25: 1, # 'ó' + 24: 2, # 'ö' + 31: 1, # 'ú' + 29: 2, # 'ü' + 42: 1, # 'Å‘' + 56: 0, # 'ű' + }, + 41: { # 'L' + 28: 2, # 'A' + 40: 1, # 'B' + 54: 1, # 'C' + 45: 1, # 'D' + 32: 2, # 'E' + 50: 1, # 'F' + 49: 1, # 'G' + 38: 1, # 'H' + 39: 2, # 'I' + 53: 1, # 'J' + 36: 1, # 'K' + 41: 2, # 'L' + 34: 1, # 'M' + 35: 1, # 'N' + 47: 2, # 'O' + 46: 0, # 'P' + 43: 1, # 'R' + 33: 1, # 'S' + 37: 2, # 'T' + 57: 1, # 'U' + 48: 1, # 'V' + 55: 1, # 'Y' + 52: 1, # 'Z' + 2: 2, # 'a' + 18: 0, # 'b' + 26: 0, # 'c' + 17: 0, # 'd' + 1: 3, # 'e' + 27: 0, # 'f' + 12: 0, # 'g' + 20: 0, # 'h' + 9: 2, # 'i' + 22: 1, # 'j' + 7: 0, # 'k' + 6: 1, # 'l' + 13: 0, # 'm' + 4: 0, # 'n' + 8: 2, # 'o' + 23: 0, # 'p' + 10: 0, # 'r' + 5: 0, # 's' + 3: 0, # 't' + 21: 2, # 'u' + 19: 0, # 'v' + 62: 0, # 'x' + 16: 1, # 'y' + 11: 0, # 'z' + 51: 2, # 'Ã' + 44: 1, # 'É' + 61: 1, # 'Ã' + 58: 1, # 'Ó' + 59: 1, # 'Ö' + 60: 1, # 'Ú' + 63: 1, # 'Ãœ' + 14: 2, # 'á' + 15: 1, # 'é' + 30: 1, # 'í' + 25: 1, # 'ó' + 24: 1, # 'ö' + 31: 0, # 'ú' + 29: 1, # 'ü' + 42: 0, # 'Å‘' + 56: 0, # 'ű' + }, + 34: { # 'M' + 28: 2, # 'A' + 40: 1, # 'B' + 54: 0, # 'C' + 45: 0, # 'D' + 32: 2, # 'E' + 50: 1, # 'F' + 49: 0, # 'G' + 38: 1, # 'H' + 39: 2, # 'I' + 53: 1, # 'J' + 36: 1, # 'K' + 41: 1, # 'L' + 34: 1, # 'M' + 35: 1, # 'N' + 47: 1, # 'O' + 46: 1, # 'P' + 43: 1, # 'R' + 33: 1, # 'S' + 37: 1, # 'T' + 57: 1, # 'U' + 48: 1, # 'V' + 55: 1, # 'Y' + 52: 1, # 'Z' + 2: 3, # 'a' + 18: 0, # 'b' + 26: 1, # 'c' + 17: 0, # 'd' + 1: 3, # 'e' + 27: 0, # 'f' + 12: 0, # 'g' + 20: 0, # 'h' + 9: 3, # 'i' + 22: 0, # 'j' + 7: 0, # 'k' + 6: 0, # 'l' + 13: 1, # 'm' + 4: 1, # 'n' + 8: 3, # 'o' + 23: 0, # 'p' + 10: 1, # 'r' + 5: 0, # 's' + 3: 0, # 't' + 21: 2, # 'u' + 19: 0, # 'v' + 62: 0, # 'x' + 16: 1, # 'y' + 11: 0, # 'z' + 51: 2, # 'Ã' + 44: 1, # 'É' + 61: 1, # 'Ã' + 58: 1, # 'Ó' + 59: 1, # 'Ö' + 60: 1, # 'Ú' + 63: 1, # 'Ãœ' + 14: 2, # 'á' + 15: 2, # 'é' + 30: 1, # 'í' + 25: 1, # 'ó' + 24: 1, # 'ö' + 31: 1, # 'ú' + 29: 1, # 'ü' + 42: 0, # 'Å‘' + 56: 1, # 'ű' + }, + 35: { # 'N' + 28: 2, # 'A' + 40: 1, # 'B' + 54: 1, # 'C' + 45: 2, # 'D' + 32: 2, # 'E' + 50: 1, # 'F' + 49: 1, # 'G' + 38: 1, # 'H' + 39: 1, # 'I' + 53: 1, # 'J' + 36: 1, # 'K' + 41: 1, # 'L' + 34: 1, # 'M' + 35: 1, # 'N' + 47: 1, # 'O' + 46: 1, # 'P' + 43: 1, # 'R' + 33: 1, # 'S' + 37: 2, # 'T' + 57: 1, # 'U' + 48: 1, # 'V' + 55: 2, # 'Y' + 52: 1, # 'Z' + 2: 3, # 'a' + 18: 0, # 'b' + 26: 0, # 'c' + 17: 0, # 'd' + 1: 3, # 'e' + 27: 0, # 'f' + 12: 0, # 'g' + 20: 0, # 'h' + 9: 2, # 'i' + 22: 0, # 'j' + 7: 0, # 'k' + 6: 0, # 'l' + 13: 0, # 'm' + 4: 1, # 'n' + 8: 2, # 'o' + 23: 0, # 'p' + 10: 0, # 'r' + 5: 0, # 's' + 3: 0, # 't' + 21: 1, # 'u' + 19: 0, # 'v' + 62: 0, # 'x' + 16: 2, # 'y' + 11: 0, # 'z' + 51: 1, # 'Ã' + 44: 1, # 'É' + 61: 1, # 'Ã' + 58: 1, # 'Ó' + 59: 1, # 'Ö' + 60: 1, # 'Ú' + 63: 1, # 'Ãœ' + 14: 1, # 'á' + 15: 2, # 'é' + 30: 1, # 'í' + 25: 1, # 'ó' + 24: 1, # 'ö' + 31: 0, # 'ú' + 29: 0, # 'ü' + 42: 1, # 'Å‘' + 56: 0, # 'ű' + }, + 47: { # 'O' + 28: 1, # 'A' + 40: 1, # 'B' + 54: 1, # 'C' + 45: 1, # 'D' + 32: 1, # 'E' + 50: 1, # 'F' + 49: 1, # 'G' + 38: 1, # 'H' + 39: 1, # 'I' + 53: 1, # 'J' + 36: 2, # 'K' + 41: 2, # 'L' + 34: 2, # 'M' + 35: 2, # 'N' + 47: 1, # 'O' + 46: 1, # 'P' + 43: 2, # 'R' + 33: 2, # 'S' + 37: 2, # 'T' + 57: 1, # 'U' + 48: 1, # 'V' + 55: 1, # 'Y' + 52: 1, # 'Z' + 2: 0, # 'a' + 18: 1, # 'b' + 26: 1, # 'c' + 17: 1, # 'd' + 1: 1, # 'e' + 27: 1, # 'f' + 12: 1, # 'g' + 20: 1, # 'h' + 9: 1, # 'i' + 22: 1, # 'j' + 7: 2, # 'k' + 6: 2, # 'l' + 13: 1, # 'm' + 4: 1, # 'n' + 8: 1, # 'o' + 23: 1, # 'p' + 10: 2, # 'r' + 5: 1, # 's' + 3: 2, # 't' + 21: 1, # 'u' + 19: 0, # 'v' + 62: 1, # 'x' + 16: 0, # 'y' + 11: 1, # 'z' + 51: 1, # 'Ã' + 44: 1, # 'É' + 61: 0, # 'Ã' + 58: 1, # 'Ó' + 59: 0, # 'Ö' + 60: 0, # 'Ú' + 63: 0, # 'Ãœ' + 14: 0, # 'á' + 15: 0, # 'é' + 30: 0, # 'í' + 25: 0, # 'ó' + 24: 0, # 'ö' + 31: 0, # 'ú' + 29: 0, # 'ü' + 42: 0, # 'Å‘' + 56: 0, # 'ű' + }, + 46: { # 'P' + 28: 1, # 'A' + 40: 1, # 'B' + 54: 1, # 'C' + 45: 1, # 'D' + 32: 1, # 'E' + 50: 1, # 'F' + 49: 1, # 'G' + 38: 1, # 'H' + 39: 1, # 'I' + 53: 1, # 'J' + 36: 1, # 'K' + 41: 1, # 'L' + 34: 0, # 'M' + 35: 1, # 'N' + 47: 1, # 'O' + 46: 1, # 'P' + 43: 2, # 'R' + 33: 1, # 'S' + 37: 1, # 'T' + 57: 1, # 'U' + 48: 1, # 'V' + 55: 0, # 'Y' + 52: 1, # 'Z' + 2: 2, # 'a' + 18: 0, # 'b' + 26: 0, # 'c' + 17: 0, # 'd' + 1: 2, # 'e' + 27: 1, # 'f' + 12: 0, # 'g' + 20: 1, # 'h' + 9: 2, # 'i' + 22: 0, # 'j' + 7: 0, # 'k' + 6: 1, # 'l' + 13: 0, # 'm' + 4: 1, # 'n' + 8: 2, # 'o' + 23: 0, # 'p' + 10: 2, # 'r' + 5: 1, # 's' + 3: 0, # 't' + 21: 1, # 'u' + 19: 0, # 'v' + 62: 0, # 'x' + 16: 1, # 'y' + 11: 0, # 'z' + 51: 2, # 'Ã' + 44: 1, # 'É' + 61: 1, # 'Ã' + 58: 1, # 'Ó' + 59: 1, # 'Ö' + 60: 0, # 'Ú' + 63: 1, # 'Ãœ' + 14: 3, # 'á' + 15: 2, # 'é' + 30: 0, # 'í' + 25: 1, # 'ó' + 24: 1, # 'ö' + 31: 0, # 'ú' + 29: 1, # 'ü' + 42: 1, # 'Å‘' + 56: 0, # 'ű' + }, + 43: { # 'R' + 28: 2, # 'A' + 40: 1, # 'B' + 54: 1, # 'C' + 45: 1, # 'D' + 32: 2, # 'E' + 50: 1, # 'F' + 49: 1, # 'G' + 38: 1, # 'H' + 39: 2, # 'I' + 53: 1, # 'J' + 36: 1, # 'K' + 41: 1, # 'L' + 34: 1, # 'M' + 35: 1, # 'N' + 47: 2, # 'O' + 46: 1, # 'P' + 43: 1, # 'R' + 33: 2, # 'S' + 37: 2, # 'T' + 57: 1, # 'U' + 48: 1, # 'V' + 55: 1, # 'Y' + 52: 1, # 'Z' + 2: 2, # 'a' + 18: 0, # 'b' + 26: 0, # 'c' + 17: 0, # 'd' + 1: 2, # 'e' + 27: 0, # 'f' + 12: 0, # 'g' + 20: 1, # 'h' + 9: 2, # 'i' + 22: 0, # 'j' + 7: 0, # 'k' + 6: 0, # 'l' + 13: 0, # 'm' + 4: 0, # 'n' + 8: 2, # 'o' + 23: 0, # 'p' + 10: 0, # 'r' + 5: 0, # 's' + 3: 0, # 't' + 21: 1, # 'u' + 19: 0, # 'v' + 62: 0, # 'x' + 16: 1, # 'y' + 11: 0, # 'z' + 51: 2, # 'Ã' + 44: 1, # 'É' + 61: 1, # 'Ã' + 58: 2, # 'Ó' + 59: 1, # 'Ö' + 60: 1, # 'Ú' + 63: 1, # 'Ãœ' + 14: 2, # 'á' + 15: 2, # 'é' + 30: 1, # 'í' + 25: 2, # 'ó' + 24: 1, # 'ö' + 31: 1, # 'ú' + 29: 1, # 'ü' + 42: 0, # 'Å‘' + 56: 0, # 'ű' + }, + 33: { # 'S' + 28: 2, # 'A' + 40: 1, # 'B' + 54: 1, # 'C' + 45: 1, # 'D' + 32: 2, # 'E' + 50: 1, # 'F' + 49: 1, # 'G' + 38: 1, # 'H' + 39: 2, # 'I' + 53: 1, # 'J' + 36: 1, # 'K' + 41: 1, # 'L' + 34: 1, # 'M' + 35: 1, # 'N' + 47: 2, # 'O' + 46: 1, # 'P' + 43: 1, # 'R' + 33: 2, # 'S' + 37: 2, # 'T' + 57: 1, # 'U' + 48: 1, # 'V' + 55: 1, # 'Y' + 52: 3, # 'Z' + 2: 2, # 'a' + 18: 0, # 'b' + 26: 1, # 'c' + 17: 0, # 'd' + 1: 2, # 'e' + 27: 0, # 'f' + 12: 0, # 'g' + 20: 1, # 'h' + 9: 2, # 'i' + 22: 0, # 'j' + 7: 1, # 'k' + 6: 1, # 'l' + 13: 1, # 'm' + 4: 0, # 'n' + 8: 2, # 'o' + 23: 1, # 'p' + 10: 0, # 'r' + 5: 0, # 's' + 3: 1, # 't' + 21: 1, # 'u' + 19: 1, # 'v' + 62: 0, # 'x' + 16: 1, # 'y' + 11: 3, # 'z' + 51: 2, # 'Ã' + 44: 1, # 'É' + 61: 1, # 'Ã' + 58: 1, # 'Ó' + 59: 1, # 'Ö' + 60: 1, # 'Ú' + 63: 1, # 'Ãœ' + 14: 2, # 'á' + 15: 1, # 'é' + 30: 1, # 'í' + 25: 1, # 'ó' + 24: 1, # 'ö' + 31: 1, # 'ú' + 29: 1, # 'ü' + 42: 1, # 'Å‘' + 56: 1, # 'ű' + }, + 37: { # 'T' + 28: 2, # 'A' + 40: 1, # 'B' + 54: 1, # 'C' + 45: 1, # 'D' + 32: 2, # 'E' + 50: 1, # 'F' + 49: 1, # 'G' + 38: 1, # 'H' + 39: 2, # 'I' + 53: 1, # 'J' + 36: 1, # 'K' + 41: 1, # 'L' + 34: 1, # 'M' + 35: 1, # 'N' + 47: 2, # 'O' + 46: 1, # 'P' + 43: 2, # 'R' + 33: 1, # 'S' + 37: 2, # 'T' + 57: 1, # 'U' + 48: 1, # 'V' + 55: 1, # 'Y' + 52: 1, # 'Z' + 2: 2, # 'a' + 18: 0, # 'b' + 26: 0, # 'c' + 17: 0, # 'd' + 1: 2, # 'e' + 27: 0, # 'f' + 12: 0, # 'g' + 20: 1, # 'h' + 9: 2, # 'i' + 22: 0, # 'j' + 7: 0, # 'k' + 6: 0, # 'l' + 13: 0, # 'm' + 4: 0, # 'n' + 8: 2, # 'o' + 23: 0, # 'p' + 10: 1, # 'r' + 5: 1, # 's' + 3: 0, # 't' + 21: 2, # 'u' + 19: 0, # 'v' + 62: 0, # 'x' + 16: 1, # 'y' + 11: 1, # 'z' + 51: 2, # 'Ã' + 44: 2, # 'É' + 61: 1, # 'Ã' + 58: 1, # 'Ó' + 59: 1, # 'Ö' + 60: 1, # 'Ú' + 63: 1, # 'Ãœ' + 14: 2, # 'á' + 15: 1, # 'é' + 30: 1, # 'í' + 25: 1, # 'ó' + 24: 2, # 'ö' + 31: 1, # 'ú' + 29: 1, # 'ü' + 42: 1, # 'Å‘' + 56: 1, # 'ű' + }, + 57: { # 'U' + 28: 1, # 'A' + 40: 1, # 'B' + 54: 1, # 'C' + 45: 1, # 'D' + 32: 1, # 'E' + 50: 1, # 'F' + 49: 1, # 'G' + 38: 1, # 'H' + 39: 1, # 'I' + 53: 1, # 'J' + 36: 1, # 'K' + 41: 1, # 'L' + 34: 1, # 'M' + 35: 1, # 'N' + 47: 1, # 'O' + 46: 1, # 'P' + 43: 1, # 'R' + 33: 2, # 'S' + 37: 1, # 'T' + 57: 0, # 'U' + 48: 1, # 'V' + 55: 0, # 'Y' + 52: 1, # 'Z' + 2: 0, # 'a' + 18: 1, # 'b' + 26: 1, # 'c' + 17: 1, # 'd' + 1: 1, # 'e' + 27: 0, # 'f' + 12: 2, # 'g' + 20: 0, # 'h' + 9: 0, # 'i' + 22: 1, # 'j' + 7: 1, # 'k' + 6: 1, # 'l' + 13: 1, # 'm' + 4: 1, # 'n' + 8: 0, # 'o' + 23: 1, # 'p' + 10: 1, # 'r' + 5: 1, # 's' + 3: 1, # 't' + 21: 0, # 'u' + 19: 0, # 'v' + 62: 0, # 'x' + 16: 0, # 'y' + 11: 1, # 'z' + 51: 0, # 'Ã' + 44: 0, # 'É' + 61: 1, # 'Ã' + 58: 0, # 'Ó' + 59: 0, # 'Ö' + 60: 0, # 'Ú' + 63: 0, # 'Ãœ' + 14: 0, # 'á' + 15: 0, # 'é' + 30: 0, # 'í' + 25: 0, # 'ó' + 24: 0, # 'ö' + 31: 0, # 'ú' + 29: 0, # 'ü' + 42: 0, # 'Å‘' + 56: 0, # 'ű' + }, + 48: { # 'V' + 28: 2, # 'A' + 40: 0, # 'B' + 54: 0, # 'C' + 45: 1, # 'D' + 32: 2, # 'E' + 50: 1, # 'F' + 49: 0, # 'G' + 38: 0, # 'H' + 39: 2, # 'I' + 53: 1, # 'J' + 36: 1, # 'K' + 41: 0, # 'L' + 34: 1, # 'M' + 35: 1, # 'N' + 47: 1, # 'O' + 46: 1, # 'P' + 43: 1, # 'R' + 33: 1, # 'S' + 37: 1, # 'T' + 57: 1, # 'U' + 48: 1, # 'V' + 55: 1, # 'Y' + 52: 0, # 'Z' + 2: 3, # 'a' + 18: 0, # 'b' + 26: 0, # 'c' + 17: 0, # 'd' + 1: 2, # 'e' + 27: 0, # 'f' + 12: 0, # 'g' + 20: 0, # 'h' + 9: 2, # 'i' + 22: 0, # 'j' + 7: 0, # 'k' + 6: 1, # 'l' + 13: 0, # 'm' + 4: 0, # 'n' + 8: 2, # 'o' + 23: 0, # 'p' + 10: 0, # 'r' + 5: 0, # 's' + 3: 0, # 't' + 21: 1, # 'u' + 19: 0, # 'v' + 62: 0, # 'x' + 16: 0, # 'y' + 11: 0, # 'z' + 51: 2, # 'Ã' + 44: 2, # 'É' + 61: 1, # 'Ã' + 58: 1, # 'Ó' + 59: 1, # 'Ö' + 60: 0, # 'Ú' + 63: 1, # 'Ãœ' + 14: 2, # 'á' + 15: 2, # 'é' + 30: 1, # 'í' + 25: 0, # 'ó' + 24: 1, # 'ö' + 31: 0, # 'ú' + 29: 0, # 'ü' + 42: 0, # 'Å‘' + 56: 0, # 'ű' + }, + 55: { # 'Y' + 28: 2, # 'A' + 40: 1, # 'B' + 54: 1, # 'C' + 45: 1, # 'D' + 32: 2, # 'E' + 50: 1, # 'F' + 49: 1, # 'G' + 38: 1, # 'H' + 39: 1, # 'I' + 53: 1, # 'J' + 36: 1, # 'K' + 41: 1, # 'L' + 34: 1, # 'M' + 35: 1, # 'N' + 47: 1, # 'O' + 46: 1, # 'P' + 43: 1, # 'R' + 33: 1, # 'S' + 37: 1, # 'T' + 57: 1, # 'U' + 48: 1, # 'V' + 55: 0, # 'Y' + 52: 2, # 'Z' + 2: 1, # 'a' + 18: 0, # 'b' + 26: 0, # 'c' + 17: 1, # 'd' + 1: 1, # 'e' + 27: 0, # 'f' + 12: 0, # 'g' + 20: 0, # 'h' + 9: 0, # 'i' + 22: 0, # 'j' + 7: 0, # 'k' + 6: 0, # 'l' + 13: 0, # 'm' + 4: 0, # 'n' + 8: 1, # 'o' + 23: 1, # 'p' + 10: 0, # 'r' + 5: 0, # 's' + 3: 0, # 't' + 21: 0, # 'u' + 19: 1, # 'v' + 62: 0, # 'x' + 16: 0, # 'y' + 11: 0, # 'z' + 51: 1, # 'Ã' + 44: 1, # 'É' + 61: 1, # 'Ã' + 58: 1, # 'Ó' + 59: 1, # 'Ö' + 60: 1, # 'Ú' + 63: 1, # 'Ãœ' + 14: 0, # 'á' + 15: 0, # 'é' + 30: 0, # 'í' + 25: 0, # 'ó' + 24: 0, # 'ö' + 31: 0, # 'ú' + 29: 0, # 'ü' + 42: 0, # 'Å‘' + 56: 0, # 'ű' + }, + 52: { # 'Z' + 28: 2, # 'A' + 40: 1, # 'B' + 54: 0, # 'C' + 45: 1, # 'D' + 32: 2, # 'E' + 50: 1, # 'F' + 49: 1, # 'G' + 38: 1, # 'H' + 39: 2, # 'I' + 53: 1, # 'J' + 36: 1, # 'K' + 41: 1, # 'L' + 34: 1, # 'M' + 35: 1, # 'N' + 47: 2, # 'O' + 46: 1, # 'P' + 43: 1, # 'R' + 33: 2, # 'S' + 37: 1, # 'T' + 57: 1, # 'U' + 48: 1, # 'V' + 55: 1, # 'Y' + 52: 1, # 'Z' + 2: 1, # 'a' + 18: 0, # 'b' + 26: 0, # 'c' + 17: 0, # 'd' + 1: 1, # 'e' + 27: 0, # 'f' + 12: 0, # 'g' + 20: 0, # 'h' + 9: 1, # 'i' + 22: 0, # 'j' + 7: 0, # 'k' + 6: 0, # 'l' + 13: 0, # 'm' + 4: 1, # 'n' + 8: 1, # 'o' + 23: 0, # 'p' + 10: 1, # 'r' + 5: 2, # 's' + 3: 0, # 't' + 21: 1, # 'u' + 19: 0, # 'v' + 62: 0, # 'x' + 16: 0, # 'y' + 11: 0, # 'z' + 51: 2, # 'Ã' + 44: 1, # 'É' + 61: 1, # 'Ã' + 58: 1, # 'Ó' + 59: 1, # 'Ö' + 60: 1, # 'Ú' + 63: 1, # 'Ãœ' + 14: 1, # 'á' + 15: 1, # 'é' + 30: 0, # 'í' + 25: 0, # 'ó' + 24: 1, # 'ö' + 31: 1, # 'ú' + 29: 1, # 'ü' + 42: 0, # 'Å‘' + 56: 0, # 'ű' + }, + 2: { # 'a' + 28: 0, # 'A' + 40: 0, # 'B' + 54: 0, # 'C' + 45: 0, # 'D' + 32: 0, # 'E' + 50: 0, # 'F' + 49: 0, # 'G' + 38: 0, # 'H' + 39: 0, # 'I' + 53: 0, # 'J' + 36: 0, # 'K' + 41: 0, # 'L' + 34: 0, # 'M' + 35: 0, # 'N' + 47: 0, # 'O' + 46: 0, # 'P' + 43: 0, # 'R' + 33: 0, # 'S' + 37: 0, # 'T' + 57: 0, # 'U' + 48: 0, # 'V' + 55: 0, # 'Y' + 52: 0, # 'Z' + 2: 1, # 'a' + 18: 3, # 'b' + 26: 3, # 'c' + 17: 3, # 'd' + 1: 2, # 'e' + 27: 2, # 'f' + 12: 3, # 'g' + 20: 3, # 'h' + 9: 3, # 'i' + 22: 3, # 'j' + 7: 3, # 'k' + 6: 3, # 'l' + 13: 3, # 'm' + 4: 3, # 'n' + 8: 2, # 'o' + 23: 3, # 'p' + 10: 3, # 'r' + 5: 3, # 's' + 3: 3, # 't' + 21: 3, # 'u' + 19: 3, # 'v' + 62: 1, # 'x' + 16: 2, # 'y' + 11: 3, # 'z' + 51: 0, # 'Ã' + 44: 0, # 'É' + 61: 0, # 'Ã' + 58: 0, # 'Ó' + 59: 0, # 'Ö' + 60: 0, # 'Ú' + 63: 0, # 'Ãœ' + 14: 1, # 'á' + 15: 1, # 'é' + 30: 1, # 'í' + 25: 1, # 'ó' + 24: 1, # 'ö' + 31: 1, # 'ú' + 29: 1, # 'ü' + 42: 0, # 'Å‘' + 56: 0, # 'ű' + }, + 18: { # 'b' + 28: 0, # 'A' + 40: 0, # 'B' + 54: 0, # 'C' + 45: 0, # 'D' + 32: 0, # 'E' + 50: 0, # 'F' + 49: 0, # 'G' + 38: 0, # 'H' + 39: 0, # 'I' + 53: 0, # 'J' + 36: 0, # 'K' + 41: 0, # 'L' + 34: 0, # 'M' + 35: 0, # 'N' + 47: 0, # 'O' + 46: 0, # 'P' + 43: 0, # 'R' + 33: 0, # 'S' + 37: 0, # 'T' + 57: 0, # 'U' + 48: 0, # 'V' + 55: 0, # 'Y' + 52: 0, # 'Z' + 2: 3, # 'a' + 18: 3, # 'b' + 26: 1, # 'c' + 17: 1, # 'd' + 1: 3, # 'e' + 27: 1, # 'f' + 12: 1, # 'g' + 20: 1, # 'h' + 9: 3, # 'i' + 22: 2, # 'j' + 7: 2, # 'k' + 6: 2, # 'l' + 13: 1, # 'm' + 4: 2, # 'n' + 8: 3, # 'o' + 23: 1, # 'p' + 10: 3, # 'r' + 5: 2, # 's' + 3: 1, # 't' + 21: 3, # 'u' + 19: 1, # 'v' + 62: 0, # 'x' + 16: 1, # 'y' + 11: 1, # 'z' + 51: 0, # 'Ã' + 44: 0, # 'É' + 61: 0, # 'Ã' + 58: 0, # 'Ó' + 59: 0, # 'Ö' + 60: 0, # 'Ú' + 63: 0, # 'Ãœ' + 14: 3, # 'á' + 15: 3, # 'é' + 30: 2, # 'í' + 25: 3, # 'ó' + 24: 2, # 'ö' + 31: 2, # 'ú' + 29: 2, # 'ü' + 42: 2, # 'Å‘' + 56: 1, # 'ű' + }, + 26: { # 'c' + 28: 0, # 'A' + 40: 0, # 'B' + 54: 1, # 'C' + 45: 0, # 'D' + 32: 0, # 'E' + 50: 0, # 'F' + 49: 1, # 'G' + 38: 0, # 'H' + 39: 0, # 'I' + 53: 0, # 'J' + 36: 0, # 'K' + 41: 0, # 'L' + 34: 0, # 'M' + 35: 0, # 'N' + 47: 0, # 'O' + 46: 0, # 'P' + 43: 0, # 'R' + 33: 0, # 'S' + 37: 0, # 'T' + 57: 0, # 'U' + 48: 0, # 'V' + 55: 0, # 'Y' + 52: 0, # 'Z' + 2: 2, # 'a' + 18: 1, # 'b' + 26: 2, # 'c' + 17: 1, # 'd' + 1: 3, # 'e' + 27: 1, # 'f' + 12: 1, # 'g' + 20: 3, # 'h' + 9: 3, # 'i' + 22: 1, # 'j' + 7: 2, # 'k' + 6: 1, # 'l' + 13: 1, # 'm' + 4: 1, # 'n' + 8: 3, # 'o' + 23: 1, # 'p' + 10: 2, # 'r' + 5: 3, # 's' + 3: 2, # 't' + 21: 2, # 'u' + 19: 1, # 'v' + 62: 0, # 'x' + 16: 1, # 'y' + 11: 2, # 'z' + 51: 0, # 'Ã' + 44: 0, # 'É' + 61: 0, # 'Ã' + 58: 0, # 'Ó' + 59: 0, # 'Ö' + 60: 0, # 'Ú' + 63: 0, # 'Ãœ' + 14: 2, # 'á' + 15: 2, # 'é' + 30: 2, # 'í' + 25: 1, # 'ó' + 24: 1, # 'ö' + 31: 1, # 'ú' + 29: 1, # 'ü' + 42: 0, # 'Å‘' + 56: 0, # 'ű' + }, + 17: { # 'd' + 28: 0, # 'A' + 40: 0, # 'B' + 54: 0, # 'C' + 45: 0, # 'D' + 32: 0, # 'E' + 50: 0, # 'F' + 49: 0, # 'G' + 38: 0, # 'H' + 39: 0, # 'I' + 53: 0, # 'J' + 36: 0, # 'K' + 41: 0, # 'L' + 34: 0, # 'M' + 35: 0, # 'N' + 47: 0, # 'O' + 46: 0, # 'P' + 43: 0, # 'R' + 33: 0, # 'S' + 37: 0, # 'T' + 57: 0, # 'U' + 48: 0, # 'V' + 55: 0, # 'Y' + 52: 0, # 'Z' + 2: 3, # 'a' + 18: 2, # 'b' + 26: 1, # 'c' + 17: 2, # 'd' + 1: 3, # 'e' + 27: 1, # 'f' + 12: 1, # 'g' + 20: 2, # 'h' + 9: 3, # 'i' + 22: 3, # 'j' + 7: 2, # 'k' + 6: 1, # 'l' + 13: 2, # 'm' + 4: 3, # 'n' + 8: 3, # 'o' + 23: 1, # 'p' + 10: 3, # 'r' + 5: 3, # 's' + 3: 3, # 't' + 21: 3, # 'u' + 19: 3, # 'v' + 62: 0, # 'x' + 16: 2, # 'y' + 11: 2, # 'z' + 51: 0, # 'Ã' + 44: 0, # 'É' + 61: 0, # 'Ã' + 58: 0, # 'Ó' + 59: 0, # 'Ö' + 60: 0, # 'Ú' + 63: 0, # 'Ãœ' + 14: 3, # 'á' + 15: 3, # 'é' + 30: 3, # 'í' + 25: 3, # 'ó' + 24: 3, # 'ö' + 31: 2, # 'ú' + 29: 2, # 'ü' + 42: 2, # 'Å‘' + 56: 1, # 'ű' + }, + 1: { # 'e' + 28: 0, # 'A' + 40: 0, # 'B' + 54: 0, # 'C' + 45: 0, # 'D' + 32: 0, # 'E' + 50: 0, # 'F' + 49: 0, # 'G' + 38: 0, # 'H' + 39: 0, # 'I' + 53: 0, # 'J' + 36: 0, # 'K' + 41: 0, # 'L' + 34: 0, # 'M' + 35: 0, # 'N' + 47: 0, # 'O' + 46: 0, # 'P' + 43: 0, # 'R' + 33: 0, # 'S' + 37: 0, # 'T' + 57: 0, # 'U' + 48: 0, # 'V' + 55: 0, # 'Y' + 52: 0, # 'Z' + 2: 2, # 'a' + 18: 3, # 'b' + 26: 3, # 'c' + 17: 3, # 'd' + 1: 2, # 'e' + 27: 3, # 'f' + 12: 3, # 'g' + 20: 3, # 'h' + 9: 3, # 'i' + 22: 3, # 'j' + 7: 3, # 'k' + 6: 3, # 'l' + 13: 3, # 'm' + 4: 3, # 'n' + 8: 2, # 'o' + 23: 3, # 'p' + 10: 3, # 'r' + 5: 3, # 's' + 3: 3, # 't' + 21: 2, # 'u' + 19: 3, # 'v' + 62: 2, # 'x' + 16: 2, # 'y' + 11: 3, # 'z' + 51: 0, # 'Ã' + 44: 0, # 'É' + 61: 0, # 'Ã' + 58: 0, # 'Ó' + 59: 0, # 'Ö' + 60: 0, # 'Ú' + 63: 0, # 'Ãœ' + 14: 3, # 'á' + 15: 1, # 'é' + 30: 1, # 'í' + 25: 1, # 'ó' + 24: 1, # 'ö' + 31: 1, # 'ú' + 29: 1, # 'ü' + 42: 0, # 'Å‘' + 56: 0, # 'ű' + }, + 27: { # 'f' + 28: 0, # 'A' + 40: 0, # 'B' + 54: 0, # 'C' + 45: 0, # 'D' + 32: 0, # 'E' + 50: 0, # 'F' + 49: 0, # 'G' + 38: 0, # 'H' + 39: 0, # 'I' + 53: 0, # 'J' + 36: 0, # 'K' + 41: 0, # 'L' + 34: 0, # 'M' + 35: 0, # 'N' + 47: 0, # 'O' + 46: 0, # 'P' + 43: 0, # 'R' + 33: 0, # 'S' + 37: 0, # 'T' + 57: 0, # 'U' + 48: 0, # 'V' + 55: 0, # 'Y' + 52: 0, # 'Z' + 2: 3, # 'a' + 18: 1, # 'b' + 26: 1, # 'c' + 17: 1, # 'd' + 1: 3, # 'e' + 27: 2, # 'f' + 12: 1, # 'g' + 20: 1, # 'h' + 9: 3, # 'i' + 22: 2, # 'j' + 7: 1, # 'k' + 6: 1, # 'l' + 13: 1, # 'm' + 4: 1, # 'n' + 8: 3, # 'o' + 23: 0, # 'p' + 10: 3, # 'r' + 5: 1, # 's' + 3: 1, # 't' + 21: 2, # 'u' + 19: 1, # 'v' + 62: 0, # 'x' + 16: 1, # 'y' + 11: 0, # 'z' + 51: 0, # 'Ã' + 44: 0, # 'É' + 61: 0, # 'Ã' + 58: 0, # 'Ó' + 59: 0, # 'Ö' + 60: 0, # 'Ú' + 63: 0, # 'Ãœ' + 14: 3, # 'á' + 15: 3, # 'é' + 30: 1, # 'í' + 25: 1, # 'ó' + 24: 3, # 'ö' + 31: 1, # 'ú' + 29: 2, # 'ü' + 42: 1, # 'Å‘' + 56: 1, # 'ű' + }, + 12: { # 'g' + 28: 0, # 'A' + 40: 0, # 'B' + 54: 0, # 'C' + 45: 0, # 'D' + 32: 0, # 'E' + 50: 0, # 'F' + 49: 0, # 'G' + 38: 0, # 'H' + 39: 0, # 'I' + 53: 0, # 'J' + 36: 0, # 'K' + 41: 0, # 'L' + 34: 0, # 'M' + 35: 0, # 'N' + 47: 0, # 'O' + 46: 0, # 'P' + 43: 0, # 'R' + 33: 0, # 'S' + 37: 0, # 'T' + 57: 0, # 'U' + 48: 0, # 'V' + 55: 0, # 'Y' + 52: 0, # 'Z' + 2: 3, # 'a' + 18: 3, # 'b' + 26: 2, # 'c' + 17: 2, # 'd' + 1: 3, # 'e' + 27: 2, # 'f' + 12: 3, # 'g' + 20: 3, # 'h' + 9: 3, # 'i' + 22: 3, # 'j' + 7: 2, # 'k' + 6: 3, # 'l' + 13: 2, # 'm' + 4: 3, # 'n' + 8: 3, # 'o' + 23: 1, # 'p' + 10: 3, # 'r' + 5: 3, # 's' + 3: 3, # 't' + 21: 3, # 'u' + 19: 3, # 'v' + 62: 0, # 'x' + 16: 3, # 'y' + 11: 2, # 'z' + 51: 0, # 'Ã' + 44: 0, # 'É' + 61: 0, # 'Ã' + 58: 0, # 'Ó' + 59: 0, # 'Ö' + 60: 0, # 'Ú' + 63: 0, # 'Ãœ' + 14: 3, # 'á' + 15: 3, # 'é' + 30: 2, # 'í' + 25: 3, # 'ó' + 24: 2, # 'ö' + 31: 2, # 'ú' + 29: 2, # 'ü' + 42: 2, # 'Å‘' + 56: 1, # 'ű' + }, + 20: { # 'h' + 28: 0, # 'A' + 40: 0, # 'B' + 54: 0, # 'C' + 45: 0, # 'D' + 32: 0, # 'E' + 50: 0, # 'F' + 49: 0, # 'G' + 38: 0, # 'H' + 39: 0, # 'I' + 53: 0, # 'J' + 36: 0, # 'K' + 41: 0, # 'L' + 34: 0, # 'M' + 35: 0, # 'N' + 47: 0, # 'O' + 46: 0, # 'P' + 43: 0, # 'R' + 33: 0, # 'S' + 37: 0, # 'T' + 57: 0, # 'U' + 48: 0, # 'V' + 55: 0, # 'Y' + 52: 0, # 'Z' + 2: 3, # 'a' + 18: 1, # 'b' + 26: 1, # 'c' + 17: 0, # 'd' + 1: 3, # 'e' + 27: 0, # 'f' + 12: 1, # 'g' + 20: 2, # 'h' + 9: 3, # 'i' + 22: 1, # 'j' + 7: 1, # 'k' + 6: 1, # 'l' + 13: 1, # 'm' + 4: 1, # 'n' + 8: 3, # 'o' + 23: 0, # 'p' + 10: 1, # 'r' + 5: 2, # 's' + 3: 1, # 't' + 21: 3, # 'u' + 19: 1, # 'v' + 62: 0, # 'x' + 16: 2, # 'y' + 11: 0, # 'z' + 51: 0, # 'Ã' + 44: 0, # 'É' + 61: 0, # 'Ã' + 58: 0, # 'Ó' + 59: 0, # 'Ö' + 60: 0, # 'Ú' + 63: 0, # 'Ãœ' + 14: 3, # 'á' + 15: 3, # 'é' + 30: 3, # 'í' + 25: 2, # 'ó' + 24: 2, # 'ö' + 31: 2, # 'ú' + 29: 1, # 'ü' + 42: 1, # 'Å‘' + 56: 1, # 'ű' + }, + 9: { # 'i' + 28: 0, # 'A' + 40: 0, # 'B' + 54: 0, # 'C' + 45: 0, # 'D' + 32: 0, # 'E' + 50: 0, # 'F' + 49: 0, # 'G' + 38: 0, # 'H' + 39: 0, # 'I' + 53: 0, # 'J' + 36: 0, # 'K' + 41: 0, # 'L' + 34: 0, # 'M' + 35: 0, # 'N' + 47: 0, # 'O' + 46: 0, # 'P' + 43: 0, # 'R' + 33: 0, # 'S' + 37: 0, # 'T' + 57: 0, # 'U' + 48: 0, # 'V' + 55: 0, # 'Y' + 52: 0, # 'Z' + 2: 3, # 'a' + 18: 3, # 'b' + 26: 3, # 'c' + 17: 3, # 'd' + 1: 3, # 'e' + 27: 3, # 'f' + 12: 3, # 'g' + 20: 3, # 'h' + 9: 2, # 'i' + 22: 2, # 'j' + 7: 3, # 'k' + 6: 3, # 'l' + 13: 3, # 'm' + 4: 3, # 'n' + 8: 2, # 'o' + 23: 2, # 'p' + 10: 3, # 'r' + 5: 3, # 's' + 3: 3, # 't' + 21: 3, # 'u' + 19: 3, # 'v' + 62: 1, # 'x' + 16: 1, # 'y' + 11: 3, # 'z' + 51: 0, # 'Ã' + 44: 0, # 'É' + 61: 0, # 'Ã' + 58: 0, # 'Ó' + 59: 0, # 'Ö' + 60: 0, # 'Ú' + 63: 0, # 'Ãœ' + 14: 3, # 'á' + 15: 2, # 'é' + 30: 1, # 'í' + 25: 3, # 'ó' + 24: 1, # 'ö' + 31: 2, # 'ú' + 29: 1, # 'ü' + 42: 0, # 'Å‘' + 56: 1, # 'ű' + }, + 22: { # 'j' + 28: 0, # 'A' + 40: 0, # 'B' + 54: 0, # 'C' + 45: 0, # 'D' + 32: 0, # 'E' + 50: 0, # 'F' + 49: 0, # 'G' + 38: 0, # 'H' + 39: 0, # 'I' + 53: 0, # 'J' + 36: 0, # 'K' + 41: 0, # 'L' + 34: 0, # 'M' + 35: 0, # 'N' + 47: 0, # 'O' + 46: 0, # 'P' + 43: 0, # 'R' + 33: 0, # 'S' + 37: 0, # 'T' + 57: 0, # 'U' + 48: 0, # 'V' + 55: 0, # 'Y' + 52: 0, # 'Z' + 2: 3, # 'a' + 18: 2, # 'b' + 26: 1, # 'c' + 17: 3, # 'd' + 1: 3, # 'e' + 27: 1, # 'f' + 12: 1, # 'g' + 20: 2, # 'h' + 9: 1, # 'i' + 22: 2, # 'j' + 7: 2, # 'k' + 6: 2, # 'l' + 13: 1, # 'm' + 4: 2, # 'n' + 8: 3, # 'o' + 23: 1, # 'p' + 10: 2, # 'r' + 5: 2, # 's' + 3: 3, # 't' + 21: 3, # 'u' + 19: 1, # 'v' + 62: 0, # 'x' + 16: 0, # 'y' + 11: 2, # 'z' + 51: 0, # 'Ã' + 44: 0, # 'É' + 61: 0, # 'Ã' + 58: 0, # 'Ó' + 59: 0, # 'Ö' + 60: 0, # 'Ú' + 63: 0, # 'Ãœ' + 14: 3, # 'á' + 15: 3, # 'é' + 30: 1, # 'í' + 25: 3, # 'ó' + 24: 3, # 'ö' + 31: 3, # 'ú' + 29: 2, # 'ü' + 42: 1, # 'Å‘' + 56: 1, # 'ű' + }, + 7: { # 'k' + 28: 0, # 'A' + 40: 0, # 'B' + 54: 0, # 'C' + 45: 0, # 'D' + 32: 0, # 'E' + 50: 0, # 'F' + 49: 0, # 'G' + 38: 0, # 'H' + 39: 0, # 'I' + 53: 0, # 'J' + 36: 0, # 'K' + 41: 0, # 'L' + 34: 0, # 'M' + 35: 0, # 'N' + 47: 0, # 'O' + 46: 0, # 'P' + 43: 0, # 'R' + 33: 0, # 'S' + 37: 0, # 'T' + 57: 0, # 'U' + 48: 0, # 'V' + 55: 0, # 'Y' + 52: 0, # 'Z' + 2: 3, # 'a' + 18: 3, # 'b' + 26: 2, # 'c' + 17: 1, # 'd' + 1: 3, # 'e' + 27: 1, # 'f' + 12: 1, # 'g' + 20: 2, # 'h' + 9: 3, # 'i' + 22: 2, # 'j' + 7: 3, # 'k' + 6: 3, # 'l' + 13: 1, # 'm' + 4: 3, # 'n' + 8: 3, # 'o' + 23: 1, # 'p' + 10: 3, # 'r' + 5: 3, # 's' + 3: 3, # 't' + 21: 3, # 'u' + 19: 2, # 'v' + 62: 0, # 'x' + 16: 2, # 'y' + 11: 1, # 'z' + 51: 0, # 'Ã' + 44: 0, # 'É' + 61: 0, # 'Ã' + 58: 0, # 'Ó' + 59: 0, # 'Ö' + 60: 0, # 'Ú' + 63: 0, # 'Ãœ' + 14: 3, # 'á' + 15: 3, # 'é' + 30: 3, # 'í' + 25: 2, # 'ó' + 24: 3, # 'ö' + 31: 1, # 'ú' + 29: 3, # 'ü' + 42: 1, # 'Å‘' + 56: 1, # 'ű' + }, + 6: { # 'l' + 28: 0, # 'A' + 40: 0, # 'B' + 54: 0, # 'C' + 45: 0, # 'D' + 32: 0, # 'E' + 50: 0, # 'F' + 49: 0, # 'G' + 38: 0, # 'H' + 39: 0, # 'I' + 53: 0, # 'J' + 36: 1, # 'K' + 41: 0, # 'L' + 34: 0, # 'M' + 35: 1, # 'N' + 47: 0, # 'O' + 46: 0, # 'P' + 43: 0, # 'R' + 33: 0, # 'S' + 37: 0, # 'T' + 57: 0, # 'U' + 48: 0, # 'V' + 55: 0, # 'Y' + 52: 0, # 'Z' + 2: 3, # 'a' + 18: 2, # 'b' + 26: 3, # 'c' + 17: 3, # 'd' + 1: 3, # 'e' + 27: 3, # 'f' + 12: 3, # 'g' + 20: 3, # 'h' + 9: 3, # 'i' + 22: 3, # 'j' + 7: 3, # 'k' + 6: 3, # 'l' + 13: 3, # 'm' + 4: 3, # 'n' + 8: 3, # 'o' + 23: 2, # 'p' + 10: 2, # 'r' + 5: 3, # 's' + 3: 3, # 't' + 21: 3, # 'u' + 19: 3, # 'v' + 62: 0, # 'x' + 16: 3, # 'y' + 11: 2, # 'z' + 51: 0, # 'Ã' + 44: 0, # 'É' + 61: 0, # 'Ã' + 58: 0, # 'Ó' + 59: 0, # 'Ö' + 60: 0, # 'Ú' + 63: 0, # 'Ãœ' + 14: 3, # 'á' + 15: 3, # 'é' + 30: 3, # 'í' + 25: 3, # 'ó' + 24: 3, # 'ö' + 31: 2, # 'ú' + 29: 2, # 'ü' + 42: 3, # 'Å‘' + 56: 1, # 'ű' + }, + 13: { # 'm' + 28: 0, # 'A' + 40: 0, # 'B' + 54: 0, # 'C' + 45: 0, # 'D' + 32: 0, # 'E' + 50: 0, # 'F' + 49: 0, # 'G' + 38: 0, # 'H' + 39: 0, # 'I' + 53: 0, # 'J' + 36: 0, # 'K' + 41: 0, # 'L' + 34: 0, # 'M' + 35: 0, # 'N' + 47: 0, # 'O' + 46: 0, # 'P' + 43: 0, # 'R' + 33: 0, # 'S' + 37: 0, # 'T' + 57: 0, # 'U' + 48: 0, # 'V' + 55: 0, # 'Y' + 52: 0, # 'Z' + 2: 3, # 'a' + 18: 3, # 'b' + 26: 2, # 'c' + 17: 1, # 'd' + 1: 3, # 'e' + 27: 1, # 'f' + 12: 1, # 'g' + 20: 2, # 'h' + 9: 3, # 'i' + 22: 2, # 'j' + 7: 1, # 'k' + 6: 3, # 'l' + 13: 3, # 'm' + 4: 2, # 'n' + 8: 3, # 'o' + 23: 3, # 'p' + 10: 2, # 'r' + 5: 2, # 's' + 3: 2, # 't' + 21: 3, # 'u' + 19: 1, # 'v' + 62: 0, # 'x' + 16: 1, # 'y' + 11: 2, # 'z' + 51: 0, # 'Ã' + 44: 0, # 'É' + 61: 0, # 'Ã' + 58: 0, # 'Ó' + 59: 0, # 'Ö' + 60: 0, # 'Ú' + 63: 0, # 'Ãœ' + 14: 3, # 'á' + 15: 3, # 'é' + 30: 2, # 'í' + 25: 2, # 'ó' + 24: 2, # 'ö' + 31: 2, # 'ú' + 29: 2, # 'ü' + 42: 1, # 'Å‘' + 56: 2, # 'ű' + }, + 4: { # 'n' + 28: 0, # 'A' + 40: 0, # 'B' + 54: 0, # 'C' + 45: 0, # 'D' + 32: 0, # 'E' + 50: 0, # 'F' + 49: 0, # 'G' + 38: 0, # 'H' + 39: 0, # 'I' + 53: 0, # 'J' + 36: 0, # 'K' + 41: 0, # 'L' + 34: 0, # 'M' + 35: 0, # 'N' + 47: 0, # 'O' + 46: 0, # 'P' + 43: 0, # 'R' + 33: 0, # 'S' + 37: 0, # 'T' + 57: 0, # 'U' + 48: 0, # 'V' + 55: 0, # 'Y' + 52: 0, # 'Z' + 2: 3, # 'a' + 18: 3, # 'b' + 26: 3, # 'c' + 17: 3, # 'd' + 1: 3, # 'e' + 27: 2, # 'f' + 12: 3, # 'g' + 20: 3, # 'h' + 9: 3, # 'i' + 22: 2, # 'j' + 7: 3, # 'k' + 6: 2, # 'l' + 13: 2, # 'm' + 4: 3, # 'n' + 8: 3, # 'o' + 23: 2, # 'p' + 10: 2, # 'r' + 5: 3, # 's' + 3: 3, # 't' + 21: 3, # 'u' + 19: 2, # 'v' + 62: 1, # 'x' + 16: 3, # 'y' + 11: 3, # 'z' + 51: 0, # 'Ã' + 44: 0, # 'É' + 61: 0, # 'Ã' + 58: 0, # 'Ó' + 59: 0, # 'Ö' + 60: 0, # 'Ú' + 63: 0, # 'Ãœ' + 14: 3, # 'á' + 15: 3, # 'é' + 30: 2, # 'í' + 25: 2, # 'ó' + 24: 3, # 'ö' + 31: 2, # 'ú' + 29: 3, # 'ü' + 42: 2, # 'Å‘' + 56: 1, # 'ű' + }, + 8: { # 'o' + 28: 0, # 'A' + 40: 0, # 'B' + 54: 0, # 'C' + 45: 0, # 'D' + 32: 0, # 'E' + 50: 0, # 'F' + 49: 0, # 'G' + 38: 0, # 'H' + 39: 0, # 'I' + 53: 0, # 'J' + 36: 0, # 'K' + 41: 0, # 'L' + 34: 0, # 'M' + 35: 0, # 'N' + 47: 1, # 'O' + 46: 0, # 'P' + 43: 0, # 'R' + 33: 0, # 'S' + 37: 0, # 'T' + 57: 0, # 'U' + 48: 0, # 'V' + 55: 0, # 'Y' + 52: 0, # 'Z' + 2: 2, # 'a' + 18: 3, # 'b' + 26: 3, # 'c' + 17: 3, # 'd' + 1: 2, # 'e' + 27: 2, # 'f' + 12: 3, # 'g' + 20: 3, # 'h' + 9: 2, # 'i' + 22: 2, # 'j' + 7: 3, # 'k' + 6: 3, # 'l' + 13: 3, # 'm' + 4: 3, # 'n' + 8: 1, # 'o' + 23: 3, # 'p' + 10: 3, # 'r' + 5: 3, # 's' + 3: 3, # 't' + 21: 2, # 'u' + 19: 3, # 'v' + 62: 1, # 'x' + 16: 1, # 'y' + 11: 3, # 'z' + 51: 0, # 'Ã' + 44: 0, # 'É' + 61: 0, # 'Ã' + 58: 0, # 'Ó' + 59: 0, # 'Ö' + 60: 0, # 'Ú' + 63: 0, # 'Ãœ' + 14: 1, # 'á' + 15: 2, # 'é' + 30: 1, # 'í' + 25: 1, # 'ó' + 24: 1, # 'ö' + 31: 1, # 'ú' + 29: 1, # 'ü' + 42: 0, # 'Å‘' + 56: 0, # 'ű' + }, + 23: { # 'p' + 28: 0, # 'A' + 40: 0, # 'B' + 54: 0, # 'C' + 45: 0, # 'D' + 32: 0, # 'E' + 50: 0, # 'F' + 49: 0, # 'G' + 38: 0, # 'H' + 39: 0, # 'I' + 53: 0, # 'J' + 36: 0, # 'K' + 41: 0, # 'L' + 34: 0, # 'M' + 35: 0, # 'N' + 47: 0, # 'O' + 46: 0, # 'P' + 43: 0, # 'R' + 33: 0, # 'S' + 37: 0, # 'T' + 57: 0, # 'U' + 48: 0, # 'V' + 55: 0, # 'Y' + 52: 0, # 'Z' + 2: 3, # 'a' + 18: 1, # 'b' + 26: 2, # 'c' + 17: 1, # 'd' + 1: 3, # 'e' + 27: 1, # 'f' + 12: 1, # 'g' + 20: 2, # 'h' + 9: 3, # 'i' + 22: 2, # 'j' + 7: 2, # 'k' + 6: 3, # 'l' + 13: 1, # 'm' + 4: 2, # 'n' + 8: 3, # 'o' + 23: 3, # 'p' + 10: 3, # 'r' + 5: 2, # 's' + 3: 2, # 't' + 21: 3, # 'u' + 19: 2, # 'v' + 62: 0, # 'x' + 16: 1, # 'y' + 11: 2, # 'z' + 51: 0, # 'Ã' + 44: 0, # 'É' + 61: 0, # 'Ã' + 58: 0, # 'Ó' + 59: 0, # 'Ö' + 60: 0, # 'Ú' + 63: 0, # 'Ãœ' + 14: 3, # 'á' + 15: 3, # 'é' + 30: 2, # 'í' + 25: 2, # 'ó' + 24: 2, # 'ö' + 31: 1, # 'ú' + 29: 2, # 'ü' + 42: 1, # 'Å‘' + 56: 1, # 'ű' + }, + 10: { # 'r' + 28: 0, # 'A' + 40: 0, # 'B' + 54: 0, # 'C' + 45: 0, # 'D' + 32: 0, # 'E' + 50: 0, # 'F' + 49: 0, # 'G' + 38: 0, # 'H' + 39: 0, # 'I' + 53: 0, # 'J' + 36: 0, # 'K' + 41: 0, # 'L' + 34: 0, # 'M' + 35: 0, # 'N' + 47: 0, # 'O' + 46: 0, # 'P' + 43: 0, # 'R' + 33: 0, # 'S' + 37: 0, # 'T' + 57: 0, # 'U' + 48: 0, # 'V' + 55: 0, # 'Y' + 52: 0, # 'Z' + 2: 3, # 'a' + 18: 3, # 'b' + 26: 3, # 'c' + 17: 3, # 'd' + 1: 3, # 'e' + 27: 2, # 'f' + 12: 3, # 'g' + 20: 2, # 'h' + 9: 3, # 'i' + 22: 3, # 'j' + 7: 3, # 'k' + 6: 3, # 'l' + 13: 3, # 'm' + 4: 3, # 'n' + 8: 3, # 'o' + 23: 2, # 'p' + 10: 3, # 'r' + 5: 3, # 's' + 3: 3, # 't' + 21: 3, # 'u' + 19: 3, # 'v' + 62: 1, # 'x' + 16: 2, # 'y' + 11: 3, # 'z' + 51: 0, # 'Ã' + 44: 0, # 'É' + 61: 0, # 'Ã' + 58: 0, # 'Ó' + 59: 0, # 'Ö' + 60: 0, # 'Ú' + 63: 0, # 'Ãœ' + 14: 3, # 'á' + 15: 3, # 'é' + 30: 2, # 'í' + 25: 3, # 'ó' + 24: 3, # 'ö' + 31: 3, # 'ú' + 29: 3, # 'ü' + 42: 2, # 'Å‘' + 56: 2, # 'ű' + }, + 5: { # 's' + 28: 0, # 'A' + 40: 0, # 'B' + 54: 0, # 'C' + 45: 0, # 'D' + 32: 0, # 'E' + 50: 0, # 'F' + 49: 0, # 'G' + 38: 0, # 'H' + 39: 0, # 'I' + 53: 0, # 'J' + 36: 0, # 'K' + 41: 0, # 'L' + 34: 0, # 'M' + 35: 0, # 'N' + 47: 0, # 'O' + 46: 0, # 'P' + 43: 0, # 'R' + 33: 0, # 'S' + 37: 0, # 'T' + 57: 0, # 'U' + 48: 0, # 'V' + 55: 0, # 'Y' + 52: 0, # 'Z' + 2: 3, # 'a' + 18: 3, # 'b' + 26: 2, # 'c' + 17: 2, # 'd' + 1: 3, # 'e' + 27: 2, # 'f' + 12: 2, # 'g' + 20: 2, # 'h' + 9: 3, # 'i' + 22: 1, # 'j' + 7: 3, # 'k' + 6: 2, # 'l' + 13: 3, # 'm' + 4: 3, # 'n' + 8: 3, # 'o' + 23: 2, # 'p' + 10: 3, # 'r' + 5: 3, # 's' + 3: 3, # 't' + 21: 3, # 'u' + 19: 2, # 'v' + 62: 0, # 'x' + 16: 1, # 'y' + 11: 3, # 'z' + 51: 0, # 'Ã' + 44: 0, # 'É' + 61: 0, # 'Ã' + 58: 0, # 'Ó' + 59: 0, # 'Ö' + 60: 0, # 'Ú' + 63: 0, # 'Ãœ' + 14: 3, # 'á' + 15: 3, # 'é' + 30: 3, # 'í' + 25: 3, # 'ó' + 24: 3, # 'ö' + 31: 3, # 'ú' + 29: 3, # 'ü' + 42: 2, # 'Å‘' + 56: 1, # 'ű' + }, + 3: { # 't' + 28: 0, # 'A' + 40: 0, # 'B' + 54: 0, # 'C' + 45: 0, # 'D' + 32: 0, # 'E' + 50: 0, # 'F' + 49: 0, # 'G' + 38: 0, # 'H' + 39: 0, # 'I' + 53: 0, # 'J' + 36: 0, # 'K' + 41: 0, # 'L' + 34: 0, # 'M' + 35: 0, # 'N' + 47: 0, # 'O' + 46: 0, # 'P' + 43: 0, # 'R' + 33: 0, # 'S' + 37: 0, # 'T' + 57: 0, # 'U' + 48: 0, # 'V' + 55: 0, # 'Y' + 52: 0, # 'Z' + 2: 3, # 'a' + 18: 3, # 'b' + 26: 2, # 'c' + 17: 1, # 'd' + 1: 3, # 'e' + 27: 2, # 'f' + 12: 1, # 'g' + 20: 3, # 'h' + 9: 3, # 'i' + 22: 3, # 'j' + 7: 3, # 'k' + 6: 3, # 'l' + 13: 2, # 'm' + 4: 3, # 'n' + 8: 3, # 'o' + 23: 1, # 'p' + 10: 3, # 'r' + 5: 3, # 's' + 3: 3, # 't' + 21: 3, # 'u' + 19: 3, # 'v' + 62: 0, # 'x' + 16: 3, # 'y' + 11: 1, # 'z' + 51: 0, # 'Ã' + 44: 0, # 'É' + 61: 0, # 'Ã' + 58: 0, # 'Ó' + 59: 0, # 'Ö' + 60: 0, # 'Ú' + 63: 0, # 'Ãœ' + 14: 3, # 'á' + 15: 3, # 'é' + 30: 2, # 'í' + 25: 3, # 'ó' + 24: 3, # 'ö' + 31: 3, # 'ú' + 29: 3, # 'ü' + 42: 3, # 'Å‘' + 56: 2, # 'ű' + }, + 21: { # 'u' + 28: 0, # 'A' + 40: 0, # 'B' + 54: 0, # 'C' + 45: 0, # 'D' + 32: 0, # 'E' + 50: 0, # 'F' + 49: 0, # 'G' + 38: 0, # 'H' + 39: 0, # 'I' + 53: 0, # 'J' + 36: 0, # 'K' + 41: 0, # 'L' + 34: 0, # 'M' + 35: 0, # 'N' + 47: 0, # 'O' + 46: 0, # 'P' + 43: 0, # 'R' + 33: 0, # 'S' + 37: 0, # 'T' + 57: 0, # 'U' + 48: 0, # 'V' + 55: 0, # 'Y' + 52: 0, # 'Z' + 2: 1, # 'a' + 18: 2, # 'b' + 26: 2, # 'c' + 17: 3, # 'd' + 1: 2, # 'e' + 27: 1, # 'f' + 12: 3, # 'g' + 20: 2, # 'h' + 9: 2, # 'i' + 22: 2, # 'j' + 7: 3, # 'k' + 6: 3, # 'l' + 13: 3, # 'm' + 4: 3, # 'n' + 8: 1, # 'o' + 23: 2, # 'p' + 10: 3, # 'r' + 5: 3, # 's' + 3: 3, # 't' + 21: 1, # 'u' + 19: 3, # 'v' + 62: 1, # 'x' + 16: 1, # 'y' + 11: 2, # 'z' + 51: 0, # 'Ã' + 44: 0, # 'É' + 61: 0, # 'Ã' + 58: 0, # 'Ó' + 59: 0, # 'Ö' + 60: 0, # 'Ú' + 63: 0, # 'Ãœ' + 14: 2, # 'á' + 15: 1, # 'é' + 30: 1, # 'í' + 25: 1, # 'ó' + 24: 0, # 'ö' + 31: 1, # 'ú' + 29: 0, # 'ü' + 42: 0, # 'Å‘' + 56: 0, # 'ű' + }, + 19: { # 'v' + 28: 0, # 'A' + 40: 0, # 'B' + 54: 0, # 'C' + 45: 0, # 'D' + 32: 0, # 'E' + 50: 0, # 'F' + 49: 0, # 'G' + 38: 0, # 'H' + 39: 0, # 'I' + 53: 0, # 'J' + 36: 0, # 'K' + 41: 0, # 'L' + 34: 0, # 'M' + 35: 0, # 'N' + 47: 0, # 'O' + 46: 0, # 'P' + 43: 0, # 'R' + 33: 0, # 'S' + 37: 0, # 'T' + 57: 0, # 'U' + 48: 0, # 'V' + 55: 0, # 'Y' + 52: 0, # 'Z' + 2: 3, # 'a' + 18: 2, # 'b' + 26: 1, # 'c' + 17: 1, # 'd' + 1: 3, # 'e' + 27: 1, # 'f' + 12: 1, # 'g' + 20: 1, # 'h' + 9: 3, # 'i' + 22: 1, # 'j' + 7: 1, # 'k' + 6: 1, # 'l' + 13: 1, # 'm' + 4: 1, # 'n' + 8: 3, # 'o' + 23: 1, # 'p' + 10: 1, # 'r' + 5: 2, # 's' + 3: 2, # 't' + 21: 2, # 'u' + 19: 2, # 'v' + 62: 0, # 'x' + 16: 1, # 'y' + 11: 1, # 'z' + 51: 0, # 'Ã' + 44: 0, # 'É' + 61: 0, # 'Ã' + 58: 0, # 'Ó' + 59: 0, # 'Ö' + 60: 0, # 'Ú' + 63: 0, # 'Ãœ' + 14: 3, # 'á' + 15: 3, # 'é' + 30: 2, # 'í' + 25: 2, # 'ó' + 24: 2, # 'ö' + 31: 1, # 'ú' + 29: 2, # 'ü' + 42: 1, # 'Å‘' + 56: 1, # 'ű' + }, + 62: { # 'x' + 28: 0, # 'A' + 40: 0, # 'B' + 54: 0, # 'C' + 45: 0, # 'D' + 32: 0, # 'E' + 50: 0, # 'F' + 49: 0, # 'G' + 38: 0, # 'H' + 39: 0, # 'I' + 53: 0, # 'J' + 36: 0, # 'K' + 41: 0, # 'L' + 34: 0, # 'M' + 35: 0, # 'N' + 47: 0, # 'O' + 46: 0, # 'P' + 43: 0, # 'R' + 33: 0, # 'S' + 37: 0, # 'T' + 57: 0, # 'U' + 48: 0, # 'V' + 55: 0, # 'Y' + 52: 0, # 'Z' + 2: 1, # 'a' + 18: 1, # 'b' + 26: 1, # 'c' + 17: 0, # 'd' + 1: 1, # 'e' + 27: 1, # 'f' + 12: 0, # 'g' + 20: 0, # 'h' + 9: 1, # 'i' + 22: 0, # 'j' + 7: 1, # 'k' + 6: 1, # 'l' + 13: 1, # 'm' + 4: 1, # 'n' + 8: 1, # 'o' + 23: 1, # 'p' + 10: 1, # 'r' + 5: 1, # 's' + 3: 1, # 't' + 21: 1, # 'u' + 19: 0, # 'v' + 62: 0, # 'x' + 16: 0, # 'y' + 11: 0, # 'z' + 51: 0, # 'Ã' + 44: 0, # 'É' + 61: 0, # 'Ã' + 58: 0, # 'Ó' + 59: 0, # 'Ö' + 60: 0, # 'Ú' + 63: 0, # 'Ãœ' + 14: 1, # 'á' + 15: 1, # 'é' + 30: 1, # 'í' + 25: 1, # 'ó' + 24: 0, # 'ö' + 31: 0, # 'ú' + 29: 0, # 'ü' + 42: 0, # 'Å‘' + 56: 0, # 'ű' + }, + 16: { # 'y' + 28: 0, # 'A' + 40: 0, # 'B' + 54: 0, # 'C' + 45: 0, # 'D' + 32: 0, # 'E' + 50: 0, # 'F' + 49: 0, # 'G' + 38: 0, # 'H' + 39: 0, # 'I' + 53: 0, # 'J' + 36: 0, # 'K' + 41: 0, # 'L' + 34: 0, # 'M' + 35: 0, # 'N' + 47: 0, # 'O' + 46: 0, # 'P' + 43: 0, # 'R' + 33: 0, # 'S' + 37: 0, # 'T' + 57: 0, # 'U' + 48: 0, # 'V' + 55: 0, # 'Y' + 52: 0, # 'Z' + 2: 3, # 'a' + 18: 2, # 'b' + 26: 1, # 'c' + 17: 1, # 'd' + 1: 3, # 'e' + 27: 2, # 'f' + 12: 2, # 'g' + 20: 2, # 'h' + 9: 3, # 'i' + 22: 2, # 'j' + 7: 2, # 'k' + 6: 2, # 'l' + 13: 2, # 'm' + 4: 3, # 'n' + 8: 3, # 'o' + 23: 2, # 'p' + 10: 2, # 'r' + 5: 3, # 's' + 3: 3, # 't' + 21: 3, # 'u' + 19: 3, # 'v' + 62: 0, # 'x' + 16: 0, # 'y' + 11: 2, # 'z' + 51: 0, # 'Ã' + 44: 0, # 'É' + 61: 0, # 'Ã' + 58: 0, # 'Ó' + 59: 0, # 'Ö' + 60: 0, # 'Ú' + 63: 0, # 'Ãœ' + 14: 3, # 'á' + 15: 3, # 'é' + 30: 2, # 'í' + 25: 2, # 'ó' + 24: 3, # 'ö' + 31: 2, # 'ú' + 29: 2, # 'ü' + 42: 1, # 'Å‘' + 56: 2, # 'ű' + }, + 11: { # 'z' + 28: 0, # 'A' + 40: 0, # 'B' + 54: 0, # 'C' + 45: 0, # 'D' + 32: 0, # 'E' + 50: 0, # 'F' + 49: 0, # 'G' + 38: 0, # 'H' + 39: 0, # 'I' + 53: 0, # 'J' + 36: 0, # 'K' + 41: 0, # 'L' + 34: 0, # 'M' + 35: 0, # 'N' + 47: 0, # 'O' + 46: 0, # 'P' + 43: 0, # 'R' + 33: 0, # 'S' + 37: 0, # 'T' + 57: 0, # 'U' + 48: 0, # 'V' + 55: 0, # 'Y' + 52: 0, # 'Z' + 2: 3, # 'a' + 18: 2, # 'b' + 26: 1, # 'c' + 17: 3, # 'd' + 1: 3, # 'e' + 27: 1, # 'f' + 12: 2, # 'g' + 20: 2, # 'h' + 9: 3, # 'i' + 22: 1, # 'j' + 7: 3, # 'k' + 6: 2, # 'l' + 13: 3, # 'm' + 4: 3, # 'n' + 8: 3, # 'o' + 23: 1, # 'p' + 10: 2, # 'r' + 5: 3, # 's' + 3: 3, # 't' + 21: 3, # 'u' + 19: 2, # 'v' + 62: 0, # 'x' + 16: 1, # 'y' + 11: 3, # 'z' + 51: 0, # 'Ã' + 44: 0, # 'É' + 61: 0, # 'Ã' + 58: 0, # 'Ó' + 59: 0, # 'Ö' + 60: 0, # 'Ú' + 63: 0, # 'Ãœ' + 14: 3, # 'á' + 15: 3, # 'é' + 30: 3, # 'í' + 25: 3, # 'ó' + 24: 3, # 'ö' + 31: 2, # 'ú' + 29: 3, # 'ü' + 42: 2, # 'Å‘' + 56: 1, # 'ű' + }, + 51: { # 'Ã' + 28: 0, # 'A' + 40: 1, # 'B' + 54: 1, # 'C' + 45: 1, # 'D' + 32: 0, # 'E' + 50: 1, # 'F' + 49: 2, # 'G' + 38: 1, # 'H' + 39: 1, # 'I' + 53: 1, # 'J' + 36: 1, # 'K' + 41: 2, # 'L' + 34: 1, # 'M' + 35: 2, # 'N' + 47: 0, # 'O' + 46: 1, # 'P' + 43: 2, # 'R' + 33: 2, # 'S' + 37: 1, # 'T' + 57: 0, # 'U' + 48: 1, # 'V' + 55: 0, # 'Y' + 52: 1, # 'Z' + 2: 0, # 'a' + 18: 1, # 'b' + 26: 1, # 'c' + 17: 1, # 'd' + 1: 0, # 'e' + 27: 0, # 'f' + 12: 1, # 'g' + 20: 1, # 'h' + 9: 0, # 'i' + 22: 1, # 'j' + 7: 1, # 'k' + 6: 2, # 'l' + 13: 2, # 'm' + 4: 0, # 'n' + 8: 0, # 'o' + 23: 1, # 'p' + 10: 1, # 'r' + 5: 1, # 's' + 3: 1, # 't' + 21: 0, # 'u' + 19: 0, # 'v' + 62: 0, # 'x' + 16: 0, # 'y' + 11: 1, # 'z' + 51: 0, # 'Ã' + 44: 0, # 'É' + 61: 1, # 'Ã' + 58: 0, # 'Ó' + 59: 0, # 'Ö' + 60: 0, # 'Ú' + 63: 0, # 'Ãœ' + 14: 0, # 'á' + 15: 0, # 'é' + 30: 0, # 'í' + 25: 0, # 'ó' + 24: 0, # 'ö' + 31: 0, # 'ú' + 29: 0, # 'ü' + 42: 0, # 'Å‘' + 56: 0, # 'ű' + }, + 44: { # 'É' + 28: 0, # 'A' + 40: 1, # 'B' + 54: 1, # 'C' + 45: 1, # 'D' + 32: 1, # 'E' + 50: 0, # 'F' + 49: 2, # 'G' + 38: 1, # 'H' + 39: 1, # 'I' + 53: 1, # 'J' + 36: 1, # 'K' + 41: 2, # 'L' + 34: 1, # 'M' + 35: 2, # 'N' + 47: 0, # 'O' + 46: 1, # 'P' + 43: 2, # 'R' + 33: 2, # 'S' + 37: 2, # 'T' + 57: 0, # 'U' + 48: 1, # 'V' + 55: 0, # 'Y' + 52: 1, # 'Z' + 2: 0, # 'a' + 18: 1, # 'b' + 26: 1, # 'c' + 17: 1, # 'd' + 1: 0, # 'e' + 27: 0, # 'f' + 12: 1, # 'g' + 20: 1, # 'h' + 9: 0, # 'i' + 22: 1, # 'j' + 7: 1, # 'k' + 6: 2, # 'l' + 13: 1, # 'm' + 4: 2, # 'n' + 8: 0, # 'o' + 23: 1, # 'p' + 10: 2, # 'r' + 5: 3, # 's' + 3: 1, # 't' + 21: 0, # 'u' + 19: 1, # 'v' + 62: 0, # 'x' + 16: 0, # 'y' + 11: 0, # 'z' + 51: 0, # 'Ã' + 44: 1, # 'É' + 61: 0, # 'Ã' + 58: 0, # 'Ó' + 59: 0, # 'Ö' + 60: 0, # 'Ú' + 63: 0, # 'Ãœ' + 14: 0, # 'á' + 15: 0, # 'é' + 30: 0, # 'í' + 25: 0, # 'ó' + 24: 0, # 'ö' + 31: 0, # 'ú' + 29: 0, # 'ü' + 42: 0, # 'Å‘' + 56: 0, # 'ű' + }, + 61: { # 'Ã' + 28: 0, # 'A' + 40: 1, # 'B' + 54: 1, # 'C' + 45: 1, # 'D' + 32: 0, # 'E' + 50: 1, # 'F' + 49: 1, # 'G' + 38: 0, # 'H' + 39: 0, # 'I' + 53: 1, # 'J' + 36: 0, # 'K' + 41: 1, # 'L' + 34: 1, # 'M' + 35: 1, # 'N' + 47: 0, # 'O' + 46: 1, # 'P' + 43: 1, # 'R' + 33: 1, # 'S' + 37: 1, # 'T' + 57: 0, # 'U' + 48: 1, # 'V' + 55: 0, # 'Y' + 52: 1, # 'Z' + 2: 0, # 'a' + 18: 0, # 'b' + 26: 0, # 'c' + 17: 0, # 'd' + 1: 0, # 'e' + 27: 0, # 'f' + 12: 2, # 'g' + 20: 0, # 'h' + 9: 0, # 'i' + 22: 0, # 'j' + 7: 0, # 'k' + 6: 0, # 'l' + 13: 1, # 'm' + 4: 0, # 'n' + 8: 0, # 'o' + 23: 0, # 'p' + 10: 1, # 'r' + 5: 0, # 's' + 3: 1, # 't' + 21: 0, # 'u' + 19: 0, # 'v' + 62: 0, # 'x' + 16: 0, # 'y' + 11: 1, # 'z' + 51: 0, # 'Ã' + 44: 0, # 'É' + 61: 0, # 'Ã' + 58: 0, # 'Ó' + 59: 0, # 'Ö' + 60: 0, # 'Ú' + 63: 0, # 'Ãœ' + 14: 0, # 'á' + 15: 0, # 'é' + 30: 0, # 'í' + 25: 0, # 'ó' + 24: 0, # 'ö' + 31: 0, # 'ú' + 29: 0, # 'ü' + 42: 0, # 'Å‘' + 56: 0, # 'ű' + }, + 58: { # 'Ó' + 28: 1, # 'A' + 40: 1, # 'B' + 54: 1, # 'C' + 45: 1, # 'D' + 32: 0, # 'E' + 50: 1, # 'F' + 49: 1, # 'G' + 38: 1, # 'H' + 39: 1, # 'I' + 53: 1, # 'J' + 36: 1, # 'K' + 41: 2, # 'L' + 34: 1, # 'M' + 35: 1, # 'N' + 47: 0, # 'O' + 46: 1, # 'P' + 43: 1, # 'R' + 33: 1, # 'S' + 37: 1, # 'T' + 57: 0, # 'U' + 48: 1, # 'V' + 55: 0, # 'Y' + 52: 1, # 'Z' + 2: 0, # 'a' + 18: 1, # 'b' + 26: 1, # 'c' + 17: 1, # 'd' + 1: 0, # 'e' + 27: 0, # 'f' + 12: 0, # 'g' + 20: 2, # 'h' + 9: 0, # 'i' + 22: 0, # 'j' + 7: 1, # 'k' + 6: 1, # 'l' + 13: 0, # 'm' + 4: 1, # 'n' + 8: 0, # 'o' + 23: 1, # 'p' + 10: 1, # 'r' + 5: 1, # 's' + 3: 0, # 't' + 21: 0, # 'u' + 19: 1, # 'v' + 62: 0, # 'x' + 16: 0, # 'y' + 11: 1, # 'z' + 51: 0, # 'Ã' + 44: 1, # 'É' + 61: 0, # 'Ã' + 58: 0, # 'Ó' + 59: 0, # 'Ö' + 60: 0, # 'Ú' + 63: 0, # 'Ãœ' + 14: 0, # 'á' + 15: 0, # 'é' + 30: 0, # 'í' + 25: 0, # 'ó' + 24: 0, # 'ö' + 31: 0, # 'ú' + 29: 0, # 'ü' + 42: 0, # 'Å‘' + 56: 0, # 'ű' + }, + 59: { # 'Ö' + 28: 0, # 'A' + 40: 1, # 'B' + 54: 1, # 'C' + 45: 1, # 'D' + 32: 0, # 'E' + 50: 0, # 'F' + 49: 1, # 'G' + 38: 1, # 'H' + 39: 0, # 'I' + 53: 1, # 'J' + 36: 1, # 'K' + 41: 1, # 'L' + 34: 1, # 'M' + 35: 1, # 'N' + 47: 0, # 'O' + 46: 1, # 'P' + 43: 1, # 'R' + 33: 1, # 'S' + 37: 1, # 'T' + 57: 0, # 'U' + 48: 1, # 'V' + 55: 0, # 'Y' + 52: 1, # 'Z' + 2: 0, # 'a' + 18: 0, # 'b' + 26: 1, # 'c' + 17: 1, # 'd' + 1: 0, # 'e' + 27: 0, # 'f' + 12: 0, # 'g' + 20: 0, # 'h' + 9: 0, # 'i' + 22: 0, # 'j' + 7: 1, # 'k' + 6: 1, # 'l' + 13: 1, # 'm' + 4: 1, # 'n' + 8: 0, # 'o' + 23: 0, # 'p' + 10: 2, # 'r' + 5: 1, # 's' + 3: 1, # 't' + 21: 0, # 'u' + 19: 1, # 'v' + 62: 0, # 'x' + 16: 0, # 'y' + 11: 1, # 'z' + 51: 0, # 'Ã' + 44: 0, # 'É' + 61: 0, # 'Ã' + 58: 0, # 'Ó' + 59: 0, # 'Ö' + 60: 0, # 'Ú' + 63: 0, # 'Ãœ' + 14: 0, # 'á' + 15: 0, # 'é' + 30: 0, # 'í' + 25: 0, # 'ó' + 24: 0, # 'ö' + 31: 0, # 'ú' + 29: 0, # 'ü' + 42: 0, # 'Å‘' + 56: 0, # 'ű' + }, + 60: { # 'Ú' + 28: 0, # 'A' + 40: 1, # 'B' + 54: 1, # 'C' + 45: 1, # 'D' + 32: 0, # 'E' + 50: 1, # 'F' + 49: 1, # 'G' + 38: 0, # 'H' + 39: 0, # 'I' + 53: 1, # 'J' + 36: 1, # 'K' + 41: 1, # 'L' + 34: 1, # 'M' + 35: 1, # 'N' + 47: 0, # 'O' + 46: 0, # 'P' + 43: 1, # 'R' + 33: 1, # 'S' + 37: 1, # 'T' + 57: 0, # 'U' + 48: 1, # 'V' + 55: 0, # 'Y' + 52: 1, # 'Z' + 2: 0, # 'a' + 18: 0, # 'b' + 26: 0, # 'c' + 17: 0, # 'd' + 1: 0, # 'e' + 27: 0, # 'f' + 12: 2, # 'g' + 20: 0, # 'h' + 9: 0, # 'i' + 22: 2, # 'j' + 7: 0, # 'k' + 6: 0, # 'l' + 13: 0, # 'm' + 4: 1, # 'n' + 8: 0, # 'o' + 23: 0, # 'p' + 10: 1, # 'r' + 5: 1, # 's' + 3: 1, # 't' + 21: 0, # 'u' + 19: 0, # 'v' + 62: 0, # 'x' + 16: 0, # 'y' + 11: 0, # 'z' + 51: 0, # 'Ã' + 44: 0, # 'É' + 61: 0, # 'Ã' + 58: 0, # 'Ó' + 59: 0, # 'Ö' + 60: 0, # 'Ú' + 63: 0, # 'Ãœ' + 14: 0, # 'á' + 15: 0, # 'é' + 30: 0, # 'í' + 25: 0, # 'ó' + 24: 0, # 'ö' + 31: 0, # 'ú' + 29: 0, # 'ü' + 42: 0, # 'Å‘' + 56: 0, # 'ű' + }, + 63: { # 'Ãœ' + 28: 0, # 'A' + 40: 1, # 'B' + 54: 0, # 'C' + 45: 1, # 'D' + 32: 0, # 'E' + 50: 0, # 'F' + 49: 1, # 'G' + 38: 1, # 'H' + 39: 0, # 'I' + 53: 1, # 'J' + 36: 1, # 'K' + 41: 1, # 'L' + 34: 1, # 'M' + 35: 1, # 'N' + 47: 0, # 'O' + 46: 0, # 'P' + 43: 1, # 'R' + 33: 1, # 'S' + 37: 1, # 'T' + 57: 0, # 'U' + 48: 1, # 'V' + 55: 0, # 'Y' + 52: 1, # 'Z' + 2: 0, # 'a' + 18: 1, # 'b' + 26: 0, # 'c' + 17: 1, # 'd' + 1: 0, # 'e' + 27: 0, # 'f' + 12: 1, # 'g' + 20: 0, # 'h' + 9: 0, # 'i' + 22: 0, # 'j' + 7: 0, # 'k' + 6: 1, # 'l' + 13: 0, # 'm' + 4: 1, # 'n' + 8: 0, # 'o' + 23: 0, # 'p' + 10: 1, # 'r' + 5: 1, # 's' + 3: 1, # 't' + 21: 0, # 'u' + 19: 1, # 'v' + 62: 0, # 'x' + 16: 0, # 'y' + 11: 1, # 'z' + 51: 0, # 'Ã' + 44: 0, # 'É' + 61: 0, # 'Ã' + 58: 0, # 'Ó' + 59: 0, # 'Ö' + 60: 0, # 'Ú' + 63: 0, # 'Ãœ' + 14: 0, # 'á' + 15: 0, # 'é' + 30: 0, # 'í' + 25: 0, # 'ó' + 24: 0, # 'ö' + 31: 0, # 'ú' + 29: 0, # 'ü' + 42: 0, # 'Å‘' + 56: 0, # 'ű' + }, + 14: { # 'á' + 28: 0, # 'A' + 40: 0, # 'B' + 54: 0, # 'C' + 45: 0, # 'D' + 32: 0, # 'E' + 50: 0, # 'F' + 49: 0, # 'G' + 38: 0, # 'H' + 39: 0, # 'I' + 53: 0, # 'J' + 36: 0, # 'K' + 41: 0, # 'L' + 34: 0, # 'M' + 35: 0, # 'N' + 47: 0, # 'O' + 46: 0, # 'P' + 43: 0, # 'R' + 33: 0, # 'S' + 37: 0, # 'T' + 57: 0, # 'U' + 48: 0, # 'V' + 55: 0, # 'Y' + 52: 0, # 'Z' + 2: 1, # 'a' + 18: 3, # 'b' + 26: 3, # 'c' + 17: 3, # 'd' + 1: 1, # 'e' + 27: 2, # 'f' + 12: 3, # 'g' + 20: 2, # 'h' + 9: 2, # 'i' + 22: 3, # 'j' + 7: 3, # 'k' + 6: 3, # 'l' + 13: 3, # 'm' + 4: 3, # 'n' + 8: 1, # 'o' + 23: 2, # 'p' + 10: 3, # 'r' + 5: 3, # 's' + 3: 3, # 't' + 21: 2, # 'u' + 19: 3, # 'v' + 62: 0, # 'x' + 16: 1, # 'y' + 11: 3, # 'z' + 51: 0, # 'Ã' + 44: 0, # 'É' + 61: 0, # 'Ã' + 58: 0, # 'Ó' + 59: 0, # 'Ö' + 60: 0, # 'Ú' + 63: 0, # 'Ãœ' + 14: 1, # 'á' + 15: 2, # 'é' + 30: 1, # 'í' + 25: 0, # 'ó' + 24: 1, # 'ö' + 31: 0, # 'ú' + 29: 1, # 'ü' + 42: 0, # 'Å‘' + 56: 0, # 'ű' + }, + 15: { # 'é' + 28: 0, # 'A' + 40: 0, # 'B' + 54: 0, # 'C' + 45: 0, # 'D' + 32: 0, # 'E' + 50: 0, # 'F' + 49: 0, # 'G' + 38: 0, # 'H' + 39: 0, # 'I' + 53: 0, # 'J' + 36: 0, # 'K' + 41: 0, # 'L' + 34: 0, # 'M' + 35: 0, # 'N' + 47: 0, # 'O' + 46: 0, # 'P' + 43: 0, # 'R' + 33: 0, # 'S' + 37: 0, # 'T' + 57: 0, # 'U' + 48: 0, # 'V' + 55: 0, # 'Y' + 52: 0, # 'Z' + 2: 1, # 'a' + 18: 3, # 'b' + 26: 2, # 'c' + 17: 3, # 'd' + 1: 1, # 'e' + 27: 1, # 'f' + 12: 3, # 'g' + 20: 3, # 'h' + 9: 2, # 'i' + 22: 2, # 'j' + 7: 3, # 'k' + 6: 3, # 'l' + 13: 3, # 'm' + 4: 3, # 'n' + 8: 1, # 'o' + 23: 3, # 'p' + 10: 3, # 'r' + 5: 3, # 's' + 3: 3, # 't' + 21: 0, # 'u' + 19: 3, # 'v' + 62: 0, # 'x' + 16: 0, # 'y' + 11: 3, # 'z' + 51: 0, # 'Ã' + 44: 0, # 'É' + 61: 0, # 'Ã' + 58: 0, # 'Ó' + 59: 0, # 'Ö' + 60: 0, # 'Ú' + 63: 0, # 'Ãœ' + 14: 1, # 'á' + 15: 1, # 'é' + 30: 0, # 'í' + 25: 0, # 'ó' + 24: 0, # 'ö' + 31: 0, # 'ú' + 29: 1, # 'ü' + 42: 0, # 'Å‘' + 56: 0, # 'ű' + }, + 30: { # 'í' + 28: 0, # 'A' + 40: 0, # 'B' + 54: 0, # 'C' + 45: 0, # 'D' + 32: 0, # 'E' + 50: 0, # 'F' + 49: 0, # 'G' + 38: 0, # 'H' + 39: 0, # 'I' + 53: 0, # 'J' + 36: 0, # 'K' + 41: 0, # 'L' + 34: 0, # 'M' + 35: 0, # 'N' + 47: 0, # 'O' + 46: 0, # 'P' + 43: 0, # 'R' + 33: 0, # 'S' + 37: 0, # 'T' + 57: 0, # 'U' + 48: 0, # 'V' + 55: 0, # 'Y' + 52: 0, # 'Z' + 2: 0, # 'a' + 18: 1, # 'b' + 26: 2, # 'c' + 17: 1, # 'd' + 1: 0, # 'e' + 27: 1, # 'f' + 12: 3, # 'g' + 20: 0, # 'h' + 9: 0, # 'i' + 22: 1, # 'j' + 7: 1, # 'k' + 6: 2, # 'l' + 13: 2, # 'm' + 4: 3, # 'n' + 8: 0, # 'o' + 23: 1, # 'p' + 10: 3, # 'r' + 5: 2, # 's' + 3: 3, # 't' + 21: 0, # 'u' + 19: 3, # 'v' + 62: 0, # 'x' + 16: 0, # 'y' + 11: 2, # 'z' + 51: 0, # 'Ã' + 44: 0, # 'É' + 61: 0, # 'Ã' + 58: 0, # 'Ó' + 59: 0, # 'Ö' + 60: 0, # 'Ú' + 63: 0, # 'Ãœ' + 14: 0, # 'á' + 15: 0, # 'é' + 30: 0, # 'í' + 25: 0, # 'ó' + 24: 0, # 'ö' + 31: 0, # 'ú' + 29: 0, # 'ü' + 42: 0, # 'Å‘' + 56: 0, # 'ű' + }, + 25: { # 'ó' + 28: 0, # 'A' + 40: 0, # 'B' + 54: 0, # 'C' + 45: 0, # 'D' + 32: 0, # 'E' + 50: 0, # 'F' + 49: 0, # 'G' + 38: 0, # 'H' + 39: 0, # 'I' + 53: 0, # 'J' + 36: 0, # 'K' + 41: 0, # 'L' + 34: 0, # 'M' + 35: 0, # 'N' + 47: 0, # 'O' + 46: 0, # 'P' + 43: 0, # 'R' + 33: 0, # 'S' + 37: 0, # 'T' + 57: 0, # 'U' + 48: 0, # 'V' + 55: 0, # 'Y' + 52: 0, # 'Z' + 2: 2, # 'a' + 18: 3, # 'b' + 26: 2, # 'c' + 17: 3, # 'd' + 1: 1, # 'e' + 27: 2, # 'f' + 12: 2, # 'g' + 20: 2, # 'h' + 9: 2, # 'i' + 22: 2, # 'j' + 7: 3, # 'k' + 6: 3, # 'l' + 13: 2, # 'm' + 4: 3, # 'n' + 8: 1, # 'o' + 23: 2, # 'p' + 10: 3, # 'r' + 5: 3, # 's' + 3: 3, # 't' + 21: 1, # 'u' + 19: 2, # 'v' + 62: 0, # 'x' + 16: 0, # 'y' + 11: 3, # 'z' + 51: 0, # 'Ã' + 44: 0, # 'É' + 61: 0, # 'Ã' + 58: 0, # 'Ó' + 59: 0, # 'Ö' + 60: 0, # 'Ú' + 63: 0, # 'Ãœ' + 14: 1, # 'á' + 15: 1, # 'é' + 30: 1, # 'í' + 25: 0, # 'ó' + 24: 1, # 'ö' + 31: 1, # 'ú' + 29: 1, # 'ü' + 42: 0, # 'Å‘' + 56: 0, # 'ű' + }, + 24: { # 'ö' + 28: 0, # 'A' + 40: 0, # 'B' + 54: 0, # 'C' + 45: 0, # 'D' + 32: 0, # 'E' + 50: 0, # 'F' + 49: 0, # 'G' + 38: 0, # 'H' + 39: 0, # 'I' + 53: 0, # 'J' + 36: 0, # 'K' + 41: 0, # 'L' + 34: 0, # 'M' + 35: 0, # 'N' + 47: 0, # 'O' + 46: 0, # 'P' + 43: 0, # 'R' + 33: 0, # 'S' + 37: 0, # 'T' + 57: 0, # 'U' + 48: 0, # 'V' + 55: 0, # 'Y' + 52: 0, # 'Z' + 2: 0, # 'a' + 18: 3, # 'b' + 26: 1, # 'c' + 17: 2, # 'd' + 1: 0, # 'e' + 27: 1, # 'f' + 12: 2, # 'g' + 20: 1, # 'h' + 9: 0, # 'i' + 22: 1, # 'j' + 7: 3, # 'k' + 6: 3, # 'l' + 13: 3, # 'm' + 4: 3, # 'n' + 8: 0, # 'o' + 23: 2, # 'p' + 10: 3, # 'r' + 5: 3, # 's' + 3: 3, # 't' + 21: 0, # 'u' + 19: 3, # 'v' + 62: 0, # 'x' + 16: 0, # 'y' + 11: 3, # 'z' + 51: 0, # 'Ã' + 44: 0, # 'É' + 61: 0, # 'Ã' + 58: 0, # 'Ó' + 59: 0, # 'Ö' + 60: 0, # 'Ú' + 63: 0, # 'Ãœ' + 14: 0, # 'á' + 15: 0, # 'é' + 30: 0, # 'í' + 25: 0, # 'ó' + 24: 0, # 'ö' + 31: 0, # 'ú' + 29: 0, # 'ü' + 42: 0, # 'Å‘' + 56: 0, # 'ű' + }, + 31: { # 'ú' + 28: 0, # 'A' + 40: 0, # 'B' + 54: 0, # 'C' + 45: 0, # 'D' + 32: 0, # 'E' + 50: 0, # 'F' + 49: 0, # 'G' + 38: 0, # 'H' + 39: 0, # 'I' + 53: 0, # 'J' + 36: 0, # 'K' + 41: 0, # 'L' + 34: 0, # 'M' + 35: 0, # 'N' + 47: 0, # 'O' + 46: 0, # 'P' + 43: 0, # 'R' + 33: 0, # 'S' + 37: 0, # 'T' + 57: 0, # 'U' + 48: 0, # 'V' + 55: 0, # 'Y' + 52: 0, # 'Z' + 2: 1, # 'a' + 18: 1, # 'b' + 26: 2, # 'c' + 17: 1, # 'd' + 1: 1, # 'e' + 27: 2, # 'f' + 12: 3, # 'g' + 20: 1, # 'h' + 9: 1, # 'i' + 22: 3, # 'j' + 7: 1, # 'k' + 6: 3, # 'l' + 13: 1, # 'm' + 4: 2, # 'n' + 8: 0, # 'o' + 23: 1, # 'p' + 10: 3, # 'r' + 5: 3, # 's' + 3: 2, # 't' + 21: 1, # 'u' + 19: 1, # 'v' + 62: 0, # 'x' + 16: 0, # 'y' + 11: 2, # 'z' + 51: 0, # 'Ã' + 44: 0, # 'É' + 61: 0, # 'Ã' + 58: 0, # 'Ó' + 59: 0, # 'Ö' + 60: 0, # 'Ú' + 63: 0, # 'Ãœ' + 14: 1, # 'á' + 15: 1, # 'é' + 30: 0, # 'í' + 25: 0, # 'ó' + 24: 0, # 'ö' + 31: 0, # 'ú' + 29: 0, # 'ü' + 42: 0, # 'Å‘' + 56: 0, # 'ű' + }, + 29: { # 'ü' + 28: 0, # 'A' + 40: 0, # 'B' + 54: 0, # 'C' + 45: 0, # 'D' + 32: 0, # 'E' + 50: 0, # 'F' + 49: 0, # 'G' + 38: 0, # 'H' + 39: 0, # 'I' + 53: 0, # 'J' + 36: 0, # 'K' + 41: 0, # 'L' + 34: 0, # 'M' + 35: 0, # 'N' + 47: 0, # 'O' + 46: 0, # 'P' + 43: 0, # 'R' + 33: 0, # 'S' + 37: 0, # 'T' + 57: 0, # 'U' + 48: 0, # 'V' + 55: 0, # 'Y' + 52: 0, # 'Z' + 2: 1, # 'a' + 18: 1, # 'b' + 26: 1, # 'c' + 17: 2, # 'd' + 1: 1, # 'e' + 27: 1, # 'f' + 12: 3, # 'g' + 20: 2, # 'h' + 9: 1, # 'i' + 22: 1, # 'j' + 7: 3, # 'k' + 6: 3, # 'l' + 13: 1, # 'm' + 4: 3, # 'n' + 8: 0, # 'o' + 23: 1, # 'p' + 10: 2, # 'r' + 5: 2, # 's' + 3: 2, # 't' + 21: 0, # 'u' + 19: 2, # 'v' + 62: 0, # 'x' + 16: 0, # 'y' + 11: 2, # 'z' + 51: 0, # 'Ã' + 44: 0, # 'É' + 61: 0, # 'Ã' + 58: 0, # 'Ó' + 59: 0, # 'Ö' + 60: 0, # 'Ú' + 63: 0, # 'Ãœ' + 14: 0, # 'á' + 15: 1, # 'é' + 30: 0, # 'í' + 25: 0, # 'ó' + 24: 0, # 'ö' + 31: 0, # 'ú' + 29: 0, # 'ü' + 42: 0, # 'Å‘' + 56: 0, # 'ű' + }, + 42: { # 'Å‘' + 28: 0, # 'A' + 40: 0, # 'B' + 54: 0, # 'C' + 45: 0, # 'D' + 32: 0, # 'E' + 50: 0, # 'F' + 49: 0, # 'G' + 38: 0, # 'H' + 39: 0, # 'I' + 53: 0, # 'J' + 36: 0, # 'K' + 41: 0, # 'L' + 34: 0, # 'M' + 35: 0, # 'N' + 47: 0, # 'O' + 46: 0, # 'P' + 43: 0, # 'R' + 33: 0, # 'S' + 37: 0, # 'T' + 57: 0, # 'U' + 48: 0, # 'V' + 55: 0, # 'Y' + 52: 0, # 'Z' + 2: 1, # 'a' + 18: 2, # 'b' + 26: 1, # 'c' + 17: 2, # 'd' + 1: 1, # 'e' + 27: 1, # 'f' + 12: 1, # 'g' + 20: 1, # 'h' + 9: 1, # 'i' + 22: 1, # 'j' + 7: 2, # 'k' + 6: 3, # 'l' + 13: 1, # 'm' + 4: 2, # 'n' + 8: 1, # 'o' + 23: 1, # 'p' + 10: 2, # 'r' + 5: 2, # 's' + 3: 2, # 't' + 21: 1, # 'u' + 19: 1, # 'v' + 62: 0, # 'x' + 16: 0, # 'y' + 11: 2, # 'z' + 51: 0, # 'Ã' + 44: 0, # 'É' + 61: 0, # 'Ã' + 58: 0, # 'Ó' + 59: 0, # 'Ö' + 60: 0, # 'Ú' + 63: 0, # 'Ãœ' + 14: 0, # 'á' + 15: 1, # 'é' + 30: 1, # 'í' + 25: 0, # 'ó' + 24: 0, # 'ö' + 31: 0, # 'ú' + 29: 1, # 'ü' + 42: 0, # 'Å‘' + 56: 0, # 'ű' + }, + 56: { # 'ű' + 28: 0, # 'A' + 40: 0, # 'B' + 54: 0, # 'C' + 45: 0, # 'D' + 32: 0, # 'E' + 50: 0, # 'F' + 49: 0, # 'G' + 38: 0, # 'H' + 39: 0, # 'I' + 53: 0, # 'J' + 36: 0, # 'K' + 41: 0, # 'L' + 34: 0, # 'M' + 35: 0, # 'N' + 47: 0, # 'O' + 46: 0, # 'P' + 43: 0, # 'R' + 33: 0, # 'S' + 37: 0, # 'T' + 57: 0, # 'U' + 48: 0, # 'V' + 55: 0, # 'Y' + 52: 0, # 'Z' + 2: 1, # 'a' + 18: 1, # 'b' + 26: 0, # 'c' + 17: 1, # 'd' + 1: 1, # 'e' + 27: 1, # 'f' + 12: 1, # 'g' + 20: 1, # 'h' + 9: 1, # 'i' + 22: 1, # 'j' + 7: 1, # 'k' + 6: 1, # 'l' + 13: 0, # 'm' + 4: 2, # 'n' + 8: 0, # 'o' + 23: 0, # 'p' + 10: 1, # 'r' + 5: 1, # 's' + 3: 1, # 't' + 21: 0, # 'u' + 19: 1, # 'v' + 62: 0, # 'x' + 16: 0, # 'y' + 11: 2, # 'z' + 51: 0, # 'Ã' + 44: 0, # 'É' + 61: 0, # 'Ã' + 58: 0, # 'Ó' + 59: 0, # 'Ö' + 60: 0, # 'Ú' + 63: 0, # 'Ãœ' + 14: 0, # 'á' + 15: 0, # 'é' + 30: 0, # 'í' + 25: 0, # 'ó' + 24: 0, # 'ö' + 31: 0, # 'ú' + 29: 0, # 'ü' + 42: 0, # 'Å‘' + 56: 0, # 'ű' + }, +} + +# 255: Undefined characters that did not exist in training text # 254: Carriage/Return # 253: symbol (punctuation) that does not belong to word # 252: 0 - 9 +# 251: Control characters -# Character Mapping Table: -Latin2_HungarianCharToOrderMap = ( -255,255,255,255,255,255,255,255,255,255,254,255,255,254,255,255, # 00 -255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, # 10 -253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253, # 20 -252,252,252,252,252,252,252,252,252,252,253,253,253,253,253,253, # 30 -253, 28, 40, 54, 45, 32, 50, 49, 38, 39, 53, 36, 41, 34, 35, 47, - 46, 71, 43, 33, 37, 57, 48, 64, 68, 55, 52,253,253,253,253,253, -253, 2, 18, 26, 17, 1, 27, 12, 20, 9, 22, 7, 6, 13, 4, 8, - 23, 67, 10, 5, 3, 21, 19, 65, 62, 16, 11,253,253,253,253,253, -159,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174, -175,176,177,178,179,180,181,182,183,184,185,186,187,188,189,190, -191,192,193,194,195,196,197, 75,198,199,200,201,202,203,204,205, - 79,206,207,208,209,210,211,212,213,214,215,216,217,218,219,220, -221, 51, 81,222, 78,223,224,225,226, 44,227,228,229, 61,230,231, -232,233,234, 58,235, 66, 59,236,237,238, 60, 69, 63,239,240,241, - 82, 14, 74,242, 70, 80,243, 72,244, 15, 83, 77, 84, 30, 76, 85, -245,246,247, 25, 73, 42, 24,248,249,250, 31, 56, 29,251,252,253, -) - -win1250HungarianCharToOrderMap = ( -255,255,255,255,255,255,255,255,255,255,254,255,255,254,255,255, # 00 -255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, # 10 -253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253, # 20 -252,252,252,252,252,252,252,252,252,252,253,253,253,253,253,253, # 30 -253, 28, 40, 54, 45, 32, 50, 49, 38, 39, 53, 36, 41, 34, 35, 47, - 46, 72, 43, 33, 37, 57, 48, 64, 68, 55, 52,253,253,253,253,253, -253, 2, 18, 26, 17, 1, 27, 12, 20, 9, 22, 7, 6, 13, 4, 8, - 23, 67, 10, 5, 3, 21, 19, 65, 62, 16, 11,253,253,253,253,253, -161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176, -177,178,179,180, 78,181, 69,182,183,184,185,186,187,188,189,190, -191,192,193,194,195,196,197, 76,198,199,200,201,202,203,204,205, - 81,206,207,208,209,210,211,212,213,214,215,216,217,218,219,220, -221, 51, 83,222, 80,223,224,225,226, 44,227,228,229, 61,230,231, -232,233,234, 58,235, 66, 59,236,237,238, 60, 70, 63,239,240,241, - 84, 14, 75,242, 71, 82,243, 73,244, 15, 85, 79, 86, 30, 77, 87, -245,246,247, 25, 74, 42, 24,248,249,250, 31, 56, 29,251,252,253, -) - -# Model Table: -# total sequences: 100% -# first 512 sequences: 94.7368% -# first 1024 sequences:5.2623% -# rest sequences: 0.8894% -# negative sequences: 0.0009% -HungarianLangModel = ( -0,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,1,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3, -3,3,3,3,3,3,3,3,3,3,2,3,3,3,3,3,3,3,3,2,2,3,3,1,1,2,2,2,2,2,1,2, -3,2,2,3,3,3,3,3,2,3,3,3,3,3,3,1,2,3,3,3,3,2,3,3,1,1,3,3,0,1,1,1, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0, -3,2,1,3,3,3,3,3,2,3,3,3,3,3,1,1,2,3,3,3,3,3,3,3,1,1,3,2,0,1,1,1, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0, -3,3,3,3,3,3,3,3,3,3,3,1,1,2,3,3,3,1,3,3,3,3,3,1,3,3,2,2,0,3,2,3, -0,0,0,0,0,0,0,0,0,0,3,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0, -3,3,3,3,3,3,2,3,3,3,2,3,3,2,3,3,3,3,3,2,3,3,2,2,3,2,3,2,0,3,2,2, -0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,1,0, -3,3,3,3,3,3,2,3,3,3,3,3,2,3,3,3,1,2,3,2,2,3,1,2,3,3,2,2,0,3,3,3, -0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0, -3,3,3,3,3,3,3,3,3,3,2,2,3,3,3,3,3,3,2,3,3,3,3,2,3,3,3,3,0,2,3,2, -0,0,0,1,1,0,0,0,0,0,3,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0, -3,3,3,3,3,3,3,3,3,3,3,1,1,1,3,3,2,1,3,2,2,3,2,1,3,2,2,1,0,3,3,1, -0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0, -3,2,2,3,3,3,3,3,1,2,3,3,3,3,1,2,1,3,3,3,3,2,2,3,1,1,3,2,0,1,1,1, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0, -3,3,3,3,3,3,3,3,2,2,3,3,3,3,3,2,1,3,3,3,3,3,2,2,1,3,3,3,0,1,1,2, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,1,0, -3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,2,3,3,3,2,3,3,2,3,3,3,2,0,3,2,3, -0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,1,0, -3,3,3,3,3,3,2,3,3,3,2,3,2,3,3,3,1,3,2,2,2,3,1,1,3,3,1,1,0,3,3,2, -0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0, -3,3,3,3,3,3,3,2,3,3,3,2,3,2,3,3,3,2,3,3,3,3,3,1,2,3,2,2,0,2,2,2, -0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0, -3,3,3,2,2,2,3,1,3,3,2,2,1,3,3,3,1,1,3,1,2,3,2,3,2,2,2,1,0,2,2,2, -0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0, -3,1,1,3,3,3,3,3,1,2,3,3,3,3,1,2,1,3,3,3,2,2,3,2,1,0,3,2,0,1,1,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -3,1,1,3,3,3,3,3,1,2,3,3,3,3,1,1,0,3,3,3,3,0,2,3,0,0,2,1,0,1,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -3,3,3,3,3,3,2,2,3,3,2,2,2,2,3,3,0,1,2,3,2,3,2,2,3,2,1,2,0,2,2,2, -0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0, -3,3,3,3,3,3,1,2,3,3,3,2,1,2,3,3,2,2,2,3,2,3,3,1,3,3,1,1,0,2,3,2, -0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0, -3,3,3,1,2,2,2,2,3,3,3,1,1,1,3,3,1,1,3,1,1,3,2,1,2,3,1,1,0,2,2,2, -0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0, -3,3,3,2,1,2,1,1,3,3,1,1,1,1,3,3,1,1,2,2,1,2,1,1,2,2,1,1,0,2,2,1, -0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0, -3,3,3,1,1,2,1,1,3,3,1,0,1,1,3,3,2,0,1,1,2,3,1,0,2,2,1,0,0,1,3,2, -0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0, -3,2,1,3,3,3,3,3,1,2,3,2,3,3,2,1,1,3,2,3,2,1,2,2,0,1,2,1,0,0,1,1, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0, -3,3,3,3,2,2,2,2,3,1,2,2,1,1,3,3,0,3,2,1,2,3,2,1,3,3,1,1,0,2,1,3, -0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0, -3,3,3,2,2,2,3,2,3,3,3,2,1,1,3,3,1,1,1,2,2,3,2,3,2,2,2,1,0,2,2,1, -0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0, -1,0,0,3,3,3,3,3,0,0,3,3,2,3,0,0,0,2,3,3,1,0,1,2,0,0,1,1,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -3,1,2,3,3,3,3,3,1,2,3,3,2,2,1,1,0,3,3,2,2,1,2,2,1,0,2,2,0,1,1,1, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -3,3,2,2,1,3,1,2,3,3,2,2,1,1,2,2,1,1,1,1,3,2,1,1,1,1,2,1,0,1,2,1, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,1,0,0,0,0,0,0,0,0,0, -2,3,3,1,1,1,1,1,3,3,3,0,1,1,3,3,1,1,1,1,1,2,2,0,3,1,1,2,0,2,1,1, -0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0, -3,1,0,1,2,1,2,2,0,1,2,3,1,2,0,0,0,2,1,1,1,1,1,2,0,0,1,1,0,0,0,0, -1,2,1,2,2,2,1,2,1,2,0,2,0,2,2,1,1,2,1,1,2,1,1,1,0,1,0,0,0,1,1,0, -1,1,1,2,3,2,3,3,0,1,2,2,3,1,0,1,0,2,1,2,2,0,1,1,0,0,1,1,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -1,0,0,3,3,2,2,1,0,0,3,2,3,2,0,0,0,1,1,3,0,0,1,1,0,0,2,1,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -3,1,1,2,2,3,3,1,0,1,3,2,3,1,1,1,0,1,1,1,1,1,3,1,0,0,2,2,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -3,1,1,1,2,2,2,1,0,1,2,3,3,2,0,0,0,2,1,1,1,2,1,1,1,0,1,1,1,0,0,0, -1,2,2,2,2,2,1,1,1,2,0,2,1,1,1,1,1,2,1,1,1,1,1,1,0,1,1,1,0,0,1,1, -3,2,2,1,0,0,1,1,2,2,0,3,0,1,2,1,1,0,0,1,1,1,0,1,1,1,1,0,2,1,1,1, -2,2,1,1,1,2,1,2,1,1,1,1,1,1,1,2,1,1,1,2,3,1,1,1,1,1,1,1,1,1,0,1, -2,3,3,0,1,0,0,0,3,3,1,0,0,1,2,2,1,0,0,0,0,2,0,0,1,1,1,0,2,1,1,1, -2,1,1,1,1,1,1,2,1,1,0,1,1,0,1,1,1,0,1,2,1,1,0,1,1,1,1,1,1,1,0,1, -2,3,3,0,1,0,0,0,2,2,0,0,0,0,1,2,2,0,0,0,0,1,0,0,1,1,0,0,2,0,1,0, -2,1,1,1,1,2,1,1,1,1,1,1,1,2,1,1,1,1,1,1,1,1,1,2,0,1,1,1,1,1,0,1, -3,2,2,0,1,0,1,0,2,3,2,0,0,1,2,2,1,0,0,1,1,1,0,0,2,1,0,1,2,2,1,1, -2,1,1,1,1,1,1,2,1,1,1,1,1,1,0,2,1,0,1,1,0,1,1,1,0,1,1,2,1,1,0,1, -2,2,2,0,0,1,0,0,2,2,1,1,0,0,2,1,1,0,0,0,1,2,0,0,2,1,0,0,2,1,1,1, -2,1,1,1,1,2,1,2,1,1,1,2,2,1,1,2,1,1,1,2,1,1,1,1,1,1,1,1,1,1,0,1, -1,2,3,0,0,0,1,0,3,2,1,0,0,1,2,1,1,0,0,0,0,2,1,0,1,1,0,0,2,1,2,1, -1,1,0,0,0,1,0,1,1,1,1,1,2,0,0,1,0,0,0,2,0,0,1,1,1,1,1,1,1,1,0,1, -3,0,0,2,1,2,2,1,0,0,2,1,2,2,0,0,0,2,1,1,1,0,1,1,0,0,1,1,2,0,0,0, -1,2,1,2,2,1,1,2,1,2,0,1,1,1,1,1,1,1,1,1,2,1,1,0,0,1,1,1,1,0,0,1, -1,3,2,0,0,0,1,0,2,2,2,0,0,0,2,2,1,0,0,0,0,3,1,1,1,1,0,0,2,1,1,1, -2,1,0,1,1,1,0,1,1,1,1,1,1,1,0,2,1,0,0,1,0,1,1,0,1,1,1,1,1,1,0,1, -2,3,2,0,0,0,1,0,2,2,0,0,0,0,2,1,1,0,0,0,0,2,1,0,1,1,0,0,2,1,1,0, -2,1,1,1,1,2,1,2,1,2,0,1,1,1,0,2,1,1,1,2,1,1,1,1,0,1,1,1,1,1,0,1, -3,1,1,2,2,2,3,2,1,1,2,2,1,1,0,1,0,2,2,1,1,1,1,1,0,0,1,1,0,1,1,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -2,2,2,0,0,0,0,0,2,2,0,0,0,0,2,2,1,0,0,0,1,1,0,0,1,2,0,0,2,1,1,1, -2,2,1,1,1,2,1,2,1,1,0,1,1,1,1,2,1,1,1,2,1,1,1,1,0,1,2,1,1,1,0,1, -1,0,0,1,2,3,2,1,0,0,2,0,1,1,0,0,0,1,1,1,1,0,1,1,0,0,1,0,0,0,0,0, -1,2,1,2,1,2,1,1,1,2,0,2,1,1,1,0,1,2,0,0,1,1,1,0,0,0,0,0,0,0,0,0, -2,3,2,0,0,0,0,0,1,1,2,1,0,0,1,1,1,0,0,0,0,2,0,0,1,1,0,0,2,1,1,1, -2,1,1,1,1,1,1,2,1,0,1,1,1,1,0,2,1,1,1,1,1,1,0,1,0,1,1,1,1,1,0,1, -1,2,2,0,1,1,1,0,2,2,2,0,0,0,3,2,1,0,0,0,1,1,0,0,1,1,0,1,1,1,0,0, -1,1,0,1,1,1,1,1,1,1,1,2,1,1,1,1,1,1,1,2,1,1,1,0,0,1,1,1,0,1,0,1, -2,1,0,2,1,1,2,2,1,1,2,1,1,1,0,0,0,1,1,0,1,1,1,1,0,0,1,1,1,0,0,0, -1,2,2,2,2,2,1,1,1,2,0,2,1,1,1,1,1,1,1,1,1,1,1,1,0,1,1,0,0,0,1,0, -1,2,3,0,0,0,1,0,2,2,0,0,0,0,2,2,0,0,0,0,0,1,0,0,1,0,0,0,2,0,1,0, -2,1,1,1,1,1,0,2,0,0,0,1,2,1,1,1,1,0,1,2,0,1,0,1,0,1,1,1,0,1,0,1, -2,2,2,0,0,0,1,0,2,1,2,0,0,0,1,1,2,0,0,0,0,1,0,0,1,1,0,0,2,1,0,1, -2,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,2,0,1,1,1,1,1,0,1, -1,2,2,0,0,0,1,0,2,2,2,0,0,0,1,1,0,0,0,0,0,1,1,0,2,0,0,1,1,1,0,1, -1,0,1,1,1,1,1,1,0,1,1,1,1,0,0,1,0,0,1,1,0,1,0,1,1,1,1,1,0,0,0,1, -1,0,0,1,0,1,2,1,0,0,1,1,1,2,0,0,0,1,1,0,1,0,1,1,0,0,1,0,0,0,0,0, -0,2,1,2,1,1,1,1,1,2,0,2,0,1,1,0,1,2,1,0,1,1,1,0,0,0,0,0,0,1,0,0, -2,1,1,0,1,2,0,0,1,1,1,0,0,0,1,1,0,0,0,0,0,1,0,0,1,0,0,0,2,1,0,1, -2,2,1,1,1,1,1,2,1,1,0,1,1,1,1,2,1,1,1,2,1,1,0,1,0,1,1,1,1,1,0,1, -1,2,2,0,0,0,0,0,1,1,0,0,0,0,2,1,0,0,0,0,0,2,0,0,2,2,0,0,2,0,0,1, -2,1,1,1,1,1,1,1,0,1,1,0,1,1,0,1,0,0,0,1,1,1,1,0,0,1,1,1,1,0,0,1, -1,1,2,0,0,3,1,0,2,1,1,1,0,0,1,1,1,0,0,0,1,1,0,0,0,1,0,0,1,0,1,0, -1,2,1,0,1,1,1,2,1,1,0,1,1,1,1,1,0,0,0,1,1,1,1,1,0,1,0,0,0,1,0,0, -2,1,1,0,0,0,0,0,1,0,0,0,0,0,0,0,0,1,0,1,0,0,0,1,0,0,0,0,2,0,0,0, -2,1,1,1,1,1,1,1,1,1,0,1,1,1,1,1,1,1,1,1,2,1,1,0,0,1,1,1,1,1,0,1, -2,1,1,1,2,1,1,1,0,1,1,2,1,0,0,0,0,1,1,1,1,0,1,0,0,0,0,1,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -1,1,0,1,1,1,1,1,0,0,1,1,2,1,0,0,0,1,1,0,0,0,1,1,0,0,1,0,1,0,0,0, -1,2,1,1,1,1,1,1,1,1,0,1,0,1,1,1,1,1,1,0,1,1,1,0,0,0,0,0,0,1,0,0, -2,0,0,0,1,1,1,1,0,0,1,1,0,0,0,0,0,1,1,1,2,0,0,1,0,0,1,0,1,0,0,0, -0,1,1,1,1,1,1,1,1,2,0,1,1,1,1,0,1,1,1,0,1,1,1,0,0,0,0,0,0,0,0,0, -1,0,0,1,1,1,1,1,0,0,2,1,0,1,0,0,0,1,0,1,0,0,0,0,0,0,1,0,0,0,0,0, -0,1,1,1,1,1,1,0,1,1,0,1,0,1,1,0,1,1,0,0,1,1,1,0,0,0,0,0,0,0,0,0, -1,0,0,1,1,1,0,0,0,0,1,0,2,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0, -0,1,1,1,1,1,0,0,1,1,0,1,0,1,0,0,1,1,1,0,1,1,1,0,0,0,0,0,0,0,0,0, -0,0,0,1,0,0,0,0,0,0,1,1,2,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,1,1,1,0,1,0,0,1,1,0,1,0,1,1,0,1,1,1,0,1,1,1,0,0,0,0,0,0,0,0,0, -2,1,1,1,1,1,1,1,1,1,1,0,0,1,1,1,0,0,1,0,0,1,0,1,0,1,1,1,0,0,1,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -1,0,0,1,1,1,1,0,0,0,1,1,1,0,0,0,0,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0, -0,1,1,1,1,1,1,0,1,1,0,1,0,1,0,0,1,1,0,0,1,1,0,0,0,0,0,0,0,0,0,0, -) - -Latin2HungarianModel = { - 'char_to_order_map': Latin2_HungarianCharToOrderMap, - 'precedence_matrix': HungarianLangModel, - 'typical_positive_ratio': 0.947368, - 'keep_english_letter': True, - 'charset_name': "ISO-8859-2", - 'language': 'Hungarian', +# Character Mapping Table(s): +WINDOWS_1250_HUNGARIAN_CHAR_TO_ORDER = { + 0: 255, # '\x00' + 1: 255, # '\x01' + 2: 255, # '\x02' + 3: 255, # '\x03' + 4: 255, # '\x04' + 5: 255, # '\x05' + 6: 255, # '\x06' + 7: 255, # '\x07' + 8: 255, # '\x08' + 9: 255, # '\t' + 10: 254, # '\n' + 11: 255, # '\x0b' + 12: 255, # '\x0c' + 13: 254, # '\r' + 14: 255, # '\x0e' + 15: 255, # '\x0f' + 16: 255, # '\x10' + 17: 255, # '\x11' + 18: 255, # '\x12' + 19: 255, # '\x13' + 20: 255, # '\x14' + 21: 255, # '\x15' + 22: 255, # '\x16' + 23: 255, # '\x17' + 24: 255, # '\x18' + 25: 255, # '\x19' + 26: 255, # '\x1a' + 27: 255, # '\x1b' + 28: 255, # '\x1c' + 29: 255, # '\x1d' + 30: 255, # '\x1e' + 31: 255, # '\x1f' + 32: 253, # ' ' + 33: 253, # '!' + 34: 253, # '"' + 35: 253, # '#' + 36: 253, # '$' + 37: 253, # '%' + 38: 253, # '&' + 39: 253, # "'" + 40: 253, # '(' + 41: 253, # ')' + 42: 253, # '*' + 43: 253, # '+' + 44: 253, # ',' + 45: 253, # '-' + 46: 253, # '.' + 47: 253, # '/' + 48: 252, # '0' + 49: 252, # '1' + 50: 252, # '2' + 51: 252, # '3' + 52: 252, # '4' + 53: 252, # '5' + 54: 252, # '6' + 55: 252, # '7' + 56: 252, # '8' + 57: 252, # '9' + 58: 253, # ':' + 59: 253, # ';' + 60: 253, # '<' + 61: 253, # '=' + 62: 253, # '>' + 63: 253, # '?' + 64: 253, # '@' + 65: 28, # 'A' + 66: 40, # 'B' + 67: 54, # 'C' + 68: 45, # 'D' + 69: 32, # 'E' + 70: 50, # 'F' + 71: 49, # 'G' + 72: 38, # 'H' + 73: 39, # 'I' + 74: 53, # 'J' + 75: 36, # 'K' + 76: 41, # 'L' + 77: 34, # 'M' + 78: 35, # 'N' + 79: 47, # 'O' + 80: 46, # 'P' + 81: 72, # 'Q' + 82: 43, # 'R' + 83: 33, # 'S' + 84: 37, # 'T' + 85: 57, # 'U' + 86: 48, # 'V' + 87: 64, # 'W' + 88: 68, # 'X' + 89: 55, # 'Y' + 90: 52, # 'Z' + 91: 253, # '[' + 92: 253, # '\\' + 93: 253, # ']' + 94: 253, # '^' + 95: 253, # '_' + 96: 253, # '`' + 97: 2, # 'a' + 98: 18, # 'b' + 99: 26, # 'c' + 100: 17, # 'd' + 101: 1, # 'e' + 102: 27, # 'f' + 103: 12, # 'g' + 104: 20, # 'h' + 105: 9, # 'i' + 106: 22, # 'j' + 107: 7, # 'k' + 108: 6, # 'l' + 109: 13, # 'm' + 110: 4, # 'n' + 111: 8, # 'o' + 112: 23, # 'p' + 113: 67, # 'q' + 114: 10, # 'r' + 115: 5, # 's' + 116: 3, # 't' + 117: 21, # 'u' + 118: 19, # 'v' + 119: 65, # 'w' + 120: 62, # 'x' + 121: 16, # 'y' + 122: 11, # 'z' + 123: 253, # '{' + 124: 253, # '|' + 125: 253, # '}' + 126: 253, # '~' + 127: 253, # '\x7f' + 128: 161, # '€' + 129: 162, # None + 130: 163, # '‚' + 131: 164, # None + 132: 165, # '„' + 133: 166, # '…' + 134: 167, # '†' + 135: 168, # '‡' + 136: 169, # None + 137: 170, # '‰' + 138: 171, # 'Å ' + 139: 172, # '‹' + 140: 173, # 'Åš' + 141: 174, # 'Ť' + 142: 175, # 'Ž' + 143: 176, # 'Ź' + 144: 177, # None + 145: 178, # '‘' + 146: 179, # '’' + 147: 180, # '“' + 148: 78, # 'â€' + 149: 181, # '•' + 150: 69, # '–' + 151: 182, # '—' + 152: 183, # None + 153: 184, # 'â„¢' + 154: 185, # 'Å¡' + 155: 186, # '›' + 156: 187, # 'Å›' + 157: 188, # 'Å¥' + 158: 189, # 'ž' + 159: 190, # 'ź' + 160: 191, # '\xa0' + 161: 192, # 'ˇ' + 162: 193, # '˘' + 163: 194, # 'Å' + 164: 195, # '¤' + 165: 196, # 'Ä„' + 166: 197, # '¦' + 167: 76, # '§' + 168: 198, # '¨' + 169: 199, # '©' + 170: 200, # 'Åž' + 171: 201, # '«' + 172: 202, # '¬' + 173: 203, # '\xad' + 174: 204, # '®' + 175: 205, # 'Å»' + 176: 81, # '°' + 177: 206, # '±' + 178: 207, # 'Ë›' + 179: 208, # 'Å‚' + 180: 209, # '´' + 181: 210, # 'µ' + 182: 211, # '¶' + 183: 212, # '·' + 184: 213, # '¸' + 185: 214, # 'Ä…' + 186: 215, # 'ÅŸ' + 187: 216, # '»' + 188: 217, # 'Ľ' + 189: 218, # 'Ë' + 190: 219, # 'ľ' + 191: 220, # 'ż' + 192: 221, # 'Å”' + 193: 51, # 'Ã' + 194: 83, # 'Â' + 195: 222, # 'Ä‚' + 196: 80, # 'Ä' + 197: 223, # 'Ĺ' + 198: 224, # 'Ć' + 199: 225, # 'Ç' + 200: 226, # 'ÄŒ' + 201: 44, # 'É' + 202: 227, # 'Ę' + 203: 228, # 'Ë' + 204: 229, # 'Äš' + 205: 61, # 'Ã' + 206: 230, # 'ÃŽ' + 207: 231, # 'ÄŽ' + 208: 232, # 'Ä' + 209: 233, # 'Ń' + 210: 234, # 'Ň' + 211: 58, # 'Ó' + 212: 235, # 'Ô' + 213: 66, # 'Å' + 214: 59, # 'Ö' + 215: 236, # '×' + 216: 237, # 'Ř' + 217: 238, # 'Å®' + 218: 60, # 'Ú' + 219: 70, # 'Å°' + 220: 63, # 'Ãœ' + 221: 239, # 'Ã' + 222: 240, # 'Å¢' + 223: 241, # 'ß' + 224: 84, # 'Å•' + 225: 14, # 'á' + 226: 75, # 'â' + 227: 242, # 'ă' + 228: 71, # 'ä' + 229: 82, # 'ĺ' + 230: 243, # 'ć' + 231: 73, # 'ç' + 232: 244, # 'Ä' + 233: 15, # 'é' + 234: 85, # 'Ä™' + 235: 79, # 'ë' + 236: 86, # 'Ä›' + 237: 30, # 'í' + 238: 77, # 'î' + 239: 87, # 'Ä' + 240: 245, # 'Ä‘' + 241: 246, # 'Å„' + 242: 247, # 'ň' + 243: 25, # 'ó' + 244: 74, # 'ô' + 245: 42, # 'Å‘' + 246: 24, # 'ö' + 247: 248, # '÷' + 248: 249, # 'Å™' + 249: 250, # 'ů' + 250: 31, # 'ú' + 251: 56, # 'ű' + 252: 29, # 'ü' + 253: 251, # 'ý' + 254: 252, # 'Å£' + 255: 253, # 'Ë™' } -Win1250HungarianModel = { - 'char_to_order_map': win1250HungarianCharToOrderMap, - 'precedence_matrix': HungarianLangModel, - 'typical_positive_ratio': 0.947368, - 'keep_english_letter': True, - 'charset_name': "windows-1250", - 'language': 'Hungarian', +WINDOWS_1250_HUNGARIAN_MODEL = SingleByteCharSetModel(charset_name='windows-1250', + language='Hungarian', + char_to_order_map=WINDOWS_1250_HUNGARIAN_CHAR_TO_ORDER, + language_model=HUNGARIAN_LANG_MODEL, + typical_positive_ratio=0.947368, + keep_ascii_letters=True, + alphabet='ABCDEFGHIJKLMNOPRSTUVZabcdefghijklmnoprstuvzÃÉÃÓÖÚÜáéíóöúüÅőŰű') + +ISO_8859_2_HUNGARIAN_CHAR_TO_ORDER = { + 0: 255, # '\x00' + 1: 255, # '\x01' + 2: 255, # '\x02' + 3: 255, # '\x03' + 4: 255, # '\x04' + 5: 255, # '\x05' + 6: 255, # '\x06' + 7: 255, # '\x07' + 8: 255, # '\x08' + 9: 255, # '\t' + 10: 254, # '\n' + 11: 255, # '\x0b' + 12: 255, # '\x0c' + 13: 254, # '\r' + 14: 255, # '\x0e' + 15: 255, # '\x0f' + 16: 255, # '\x10' + 17: 255, # '\x11' + 18: 255, # '\x12' + 19: 255, # '\x13' + 20: 255, # '\x14' + 21: 255, # '\x15' + 22: 255, # '\x16' + 23: 255, # '\x17' + 24: 255, # '\x18' + 25: 255, # '\x19' + 26: 255, # '\x1a' + 27: 255, # '\x1b' + 28: 255, # '\x1c' + 29: 255, # '\x1d' + 30: 255, # '\x1e' + 31: 255, # '\x1f' + 32: 253, # ' ' + 33: 253, # '!' + 34: 253, # '"' + 35: 253, # '#' + 36: 253, # '$' + 37: 253, # '%' + 38: 253, # '&' + 39: 253, # "'" + 40: 253, # '(' + 41: 253, # ')' + 42: 253, # '*' + 43: 253, # '+' + 44: 253, # ',' + 45: 253, # '-' + 46: 253, # '.' + 47: 253, # '/' + 48: 252, # '0' + 49: 252, # '1' + 50: 252, # '2' + 51: 252, # '3' + 52: 252, # '4' + 53: 252, # '5' + 54: 252, # '6' + 55: 252, # '7' + 56: 252, # '8' + 57: 252, # '9' + 58: 253, # ':' + 59: 253, # ';' + 60: 253, # '<' + 61: 253, # '=' + 62: 253, # '>' + 63: 253, # '?' + 64: 253, # '@' + 65: 28, # 'A' + 66: 40, # 'B' + 67: 54, # 'C' + 68: 45, # 'D' + 69: 32, # 'E' + 70: 50, # 'F' + 71: 49, # 'G' + 72: 38, # 'H' + 73: 39, # 'I' + 74: 53, # 'J' + 75: 36, # 'K' + 76: 41, # 'L' + 77: 34, # 'M' + 78: 35, # 'N' + 79: 47, # 'O' + 80: 46, # 'P' + 81: 71, # 'Q' + 82: 43, # 'R' + 83: 33, # 'S' + 84: 37, # 'T' + 85: 57, # 'U' + 86: 48, # 'V' + 87: 64, # 'W' + 88: 68, # 'X' + 89: 55, # 'Y' + 90: 52, # 'Z' + 91: 253, # '[' + 92: 253, # '\\' + 93: 253, # ']' + 94: 253, # '^' + 95: 253, # '_' + 96: 253, # '`' + 97: 2, # 'a' + 98: 18, # 'b' + 99: 26, # 'c' + 100: 17, # 'd' + 101: 1, # 'e' + 102: 27, # 'f' + 103: 12, # 'g' + 104: 20, # 'h' + 105: 9, # 'i' + 106: 22, # 'j' + 107: 7, # 'k' + 108: 6, # 'l' + 109: 13, # 'm' + 110: 4, # 'n' + 111: 8, # 'o' + 112: 23, # 'p' + 113: 67, # 'q' + 114: 10, # 'r' + 115: 5, # 's' + 116: 3, # 't' + 117: 21, # 'u' + 118: 19, # 'v' + 119: 65, # 'w' + 120: 62, # 'x' + 121: 16, # 'y' + 122: 11, # 'z' + 123: 253, # '{' + 124: 253, # '|' + 125: 253, # '}' + 126: 253, # '~' + 127: 253, # '\x7f' + 128: 159, # '\x80' + 129: 160, # '\x81' + 130: 161, # '\x82' + 131: 162, # '\x83' + 132: 163, # '\x84' + 133: 164, # '\x85' + 134: 165, # '\x86' + 135: 166, # '\x87' + 136: 167, # '\x88' + 137: 168, # '\x89' + 138: 169, # '\x8a' + 139: 170, # '\x8b' + 140: 171, # '\x8c' + 141: 172, # '\x8d' + 142: 173, # '\x8e' + 143: 174, # '\x8f' + 144: 175, # '\x90' + 145: 176, # '\x91' + 146: 177, # '\x92' + 147: 178, # '\x93' + 148: 179, # '\x94' + 149: 180, # '\x95' + 150: 181, # '\x96' + 151: 182, # '\x97' + 152: 183, # '\x98' + 153: 184, # '\x99' + 154: 185, # '\x9a' + 155: 186, # '\x9b' + 156: 187, # '\x9c' + 157: 188, # '\x9d' + 158: 189, # '\x9e' + 159: 190, # '\x9f' + 160: 191, # '\xa0' + 161: 192, # 'Ä„' + 162: 193, # '˘' + 163: 194, # 'Å' + 164: 195, # '¤' + 165: 196, # 'Ľ' + 166: 197, # 'Åš' + 167: 75, # '§' + 168: 198, # '¨' + 169: 199, # 'Å ' + 170: 200, # 'Åž' + 171: 201, # 'Ť' + 172: 202, # 'Ź' + 173: 203, # '\xad' + 174: 204, # 'Ž' + 175: 205, # 'Å»' + 176: 79, # '°' + 177: 206, # 'Ä…' + 178: 207, # 'Ë›' + 179: 208, # 'Å‚' + 180: 209, # '´' + 181: 210, # 'ľ' + 182: 211, # 'Å›' + 183: 212, # 'ˇ' + 184: 213, # '¸' + 185: 214, # 'Å¡' + 186: 215, # 'ÅŸ' + 187: 216, # 'Å¥' + 188: 217, # 'ź' + 189: 218, # 'Ë' + 190: 219, # 'ž' + 191: 220, # 'ż' + 192: 221, # 'Å”' + 193: 51, # 'Ã' + 194: 81, # 'Â' + 195: 222, # 'Ä‚' + 196: 78, # 'Ä' + 197: 223, # 'Ĺ' + 198: 224, # 'Ć' + 199: 225, # 'Ç' + 200: 226, # 'ÄŒ' + 201: 44, # 'É' + 202: 227, # 'Ę' + 203: 228, # 'Ë' + 204: 229, # 'Äš' + 205: 61, # 'Ã' + 206: 230, # 'ÃŽ' + 207: 231, # 'ÄŽ' + 208: 232, # 'Ä' + 209: 233, # 'Ń' + 210: 234, # 'Ň' + 211: 58, # 'Ó' + 212: 235, # 'Ô' + 213: 66, # 'Å' + 214: 59, # 'Ö' + 215: 236, # '×' + 216: 237, # 'Ř' + 217: 238, # 'Å®' + 218: 60, # 'Ú' + 219: 69, # 'Å°' + 220: 63, # 'Ãœ' + 221: 239, # 'Ã' + 222: 240, # 'Å¢' + 223: 241, # 'ß' + 224: 82, # 'Å•' + 225: 14, # 'á' + 226: 74, # 'â' + 227: 242, # 'ă' + 228: 70, # 'ä' + 229: 80, # 'ĺ' + 230: 243, # 'ć' + 231: 72, # 'ç' + 232: 244, # 'Ä' + 233: 15, # 'é' + 234: 83, # 'Ä™' + 235: 77, # 'ë' + 236: 84, # 'Ä›' + 237: 30, # 'í' + 238: 76, # 'î' + 239: 85, # 'Ä' + 240: 245, # 'Ä‘' + 241: 246, # 'Å„' + 242: 247, # 'ň' + 243: 25, # 'ó' + 244: 73, # 'ô' + 245: 42, # 'Å‘' + 246: 24, # 'ö' + 247: 248, # '÷' + 248: 249, # 'Å™' + 249: 250, # 'ů' + 250: 31, # 'ú' + 251: 56, # 'ű' + 252: 29, # 'ü' + 253: 251, # 'ý' + 254: 252, # 'Å£' + 255: 253, # 'Ë™' } + +ISO_8859_2_HUNGARIAN_MODEL = SingleByteCharSetModel(charset_name='ISO-8859-2', + language='Hungarian', + char_to_order_map=ISO_8859_2_HUNGARIAN_CHAR_TO_ORDER, + language_model=HUNGARIAN_LANG_MODEL, + typical_positive_ratio=0.947368, + keep_ascii_letters=True, + alphabet='ABCDEFGHIJKLMNOPRSTUVZabcdefghijklmnoprstuvzÃÉÃÓÖÚÜáéíóöúüÅőŰű') + diff --git a/libs/chardet/langrussianmodel.py b/libs/chardet/langrussianmodel.py new file mode 100644 index 000000000..569689d0f --- /dev/null +++ b/libs/chardet/langrussianmodel.py @@ -0,0 +1,5718 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +from chardet.sbcharsetprober import SingleByteCharSetModel + + +# 3: Positive +# 2: Likely +# 1: Unlikely +# 0: Negative + +RUSSIAN_LANG_MODEL = { + 37: { # 'Ð' + 37: 0, # 'Ð' + 44: 1, # 'Б' + 33: 1, # 'Ð’' + 46: 1, # 'Г' + 41: 1, # 'Д' + 48: 1, # 'Е' + 56: 1, # 'Ж' + 51: 1, # 'З' + 42: 1, # 'И' + 60: 1, # 'Й' + 36: 1, # 'К' + 49: 1, # 'Л' + 38: 1, # 'Ðœ' + 31: 2, # 'Ð' + 34: 1, # 'О' + 35: 1, # 'П' + 45: 1, # 'Р' + 32: 1, # 'С' + 40: 1, # 'Т' + 52: 1, # 'У' + 53: 1, # 'Ф' + 55: 1, # 'Ð¥' + 58: 1, # 'Ц' + 50: 1, # 'Ч' + 57: 1, # 'Ш' + 63: 1, # 'Щ' + 62: 0, # 'Ы' + 61: 0, # 'Ь' + 47: 0, # 'Э' + 59: 1, # 'Ю' + 43: 1, # 'Я' + 3: 1, # 'а' + 21: 2, # 'б' + 10: 2, # 'в' + 19: 2, # 'г' + 13: 2, # 'д' + 2: 0, # 'е' + 24: 1, # 'ж' + 20: 1, # 'з' + 4: 0, # 'и' + 23: 1, # 'й' + 11: 2, # 'к' + 8: 3, # 'л' + 12: 2, # 'м' + 5: 2, # 'н' + 1: 0, # 'о' + 15: 2, # 'п' + 9: 2, # 'Ñ€' + 7: 2, # 'Ñ' + 6: 2, # 'Ñ‚' + 14: 2, # 'у' + 39: 2, # 'Ñ„' + 26: 2, # 'Ñ…' + 28: 0, # 'ц' + 22: 1, # 'ч' + 25: 2, # 'ш' + 29: 0, # 'щ' + 54: 0, # 'ÑŠ' + 18: 0, # 'Ñ‹' + 17: 0, # 'ÑŒ' + 30: 1, # 'Ñ' + 27: 0, # 'ÑŽ' + 16: 0, # 'Ñ' + }, + 44: { # 'Б' + 37: 1, # 'Ð' + 44: 0, # 'Б' + 33: 1, # 'Ð’' + 46: 1, # 'Г' + 41: 0, # 'Д' + 48: 1, # 'Е' + 56: 0, # 'Ж' + 51: 0, # 'З' + 42: 1, # 'И' + 60: 0, # 'Й' + 36: 0, # 'К' + 49: 1, # 'Л' + 38: 1, # 'Ðœ' + 31: 1, # 'Ð' + 34: 1, # 'О' + 35: 0, # 'П' + 45: 1, # 'Р' + 32: 0, # 'С' + 40: 0, # 'Т' + 52: 1, # 'У' + 53: 0, # 'Ф' + 55: 0, # 'Ð¥' + 58: 0, # 'Ц' + 50: 0, # 'Ч' + 57: 0, # 'Ш' + 63: 0, # 'Щ' + 62: 1, # 'Ы' + 61: 0, # 'Ь' + 47: 0, # 'Э' + 59: 0, # 'Ю' + 43: 1, # 'Я' + 3: 2, # 'а' + 21: 0, # 'б' + 10: 0, # 'в' + 19: 0, # 'г' + 13: 1, # 'д' + 2: 3, # 'е' + 24: 0, # 'ж' + 20: 0, # 'з' + 4: 2, # 'и' + 23: 0, # 'й' + 11: 0, # 'к' + 8: 2, # 'л' + 12: 0, # 'м' + 5: 0, # 'н' + 1: 3, # 'о' + 15: 0, # 'п' + 9: 2, # 'Ñ€' + 7: 0, # 'Ñ' + 6: 0, # 'Ñ‚' + 14: 2, # 'у' + 39: 0, # 'Ñ„' + 26: 0, # 'Ñ…' + 28: 0, # 'ц' + 22: 0, # 'ч' + 25: 0, # 'ш' + 29: 0, # 'щ' + 54: 0, # 'ÑŠ' + 18: 2, # 'Ñ‹' + 17: 1, # 'ÑŒ' + 30: 2, # 'Ñ' + 27: 1, # 'ÑŽ' + 16: 1, # 'Ñ' + }, + 33: { # 'Ð’' + 37: 2, # 'Ð' + 44: 0, # 'Б' + 33: 1, # 'Ð’' + 46: 0, # 'Г' + 41: 1, # 'Д' + 48: 1, # 'Е' + 56: 0, # 'Ж' + 51: 0, # 'З' + 42: 1, # 'И' + 60: 0, # 'Й' + 36: 1, # 'К' + 49: 1, # 'Л' + 38: 1, # 'Ðœ' + 31: 1, # 'Ð' + 34: 1, # 'О' + 35: 1, # 'П' + 45: 1, # 'Р' + 32: 1, # 'С' + 40: 1, # 'Т' + 52: 1, # 'У' + 53: 0, # 'Ф' + 55: 0, # 'Ð¥' + 58: 0, # 'Ц' + 50: 0, # 'Ч' + 57: 1, # 'Ш' + 63: 0, # 'Щ' + 62: 1, # 'Ы' + 61: 1, # 'Ь' + 47: 0, # 'Э' + 59: 0, # 'Ю' + 43: 1, # 'Я' + 3: 2, # 'а' + 21: 1, # 'б' + 10: 1, # 'в' + 19: 1, # 'г' + 13: 2, # 'д' + 2: 3, # 'е' + 24: 0, # 'ж' + 20: 2, # 'з' + 4: 2, # 'и' + 23: 0, # 'й' + 11: 1, # 'к' + 8: 2, # 'л' + 12: 2, # 'м' + 5: 2, # 'н' + 1: 3, # 'о' + 15: 2, # 'п' + 9: 2, # 'Ñ€' + 7: 3, # 'Ñ' + 6: 2, # 'Ñ‚' + 14: 2, # 'у' + 39: 0, # 'Ñ„' + 26: 1, # 'Ñ…' + 28: 1, # 'ц' + 22: 2, # 'ч' + 25: 1, # 'ш' + 29: 0, # 'щ' + 54: 1, # 'ÑŠ' + 18: 3, # 'Ñ‹' + 17: 1, # 'ÑŒ' + 30: 2, # 'Ñ' + 27: 0, # 'ÑŽ' + 16: 1, # 'Ñ' + }, + 46: { # 'Г' + 37: 1, # 'Ð' + 44: 1, # 'Б' + 33: 0, # 'Ð’' + 46: 0, # 'Г' + 41: 1, # 'Д' + 48: 1, # 'Е' + 56: 0, # 'Ж' + 51: 0, # 'З' + 42: 1, # 'И' + 60: 0, # 'Й' + 36: 0, # 'К' + 49: 1, # 'Л' + 38: 1, # 'Ðœ' + 31: 1, # 'Ð' + 34: 1, # 'О' + 35: 1, # 'П' + 45: 1, # 'Р' + 32: 0, # 'С' + 40: 0, # 'Т' + 52: 1, # 'У' + 53: 0, # 'Ф' + 55: 0, # 'Ð¥' + 58: 0, # 'Ц' + 50: 0, # 'Ч' + 57: 0, # 'Ш' + 63: 0, # 'Щ' + 62: 0, # 'Ы' + 61: 0, # 'Ь' + 47: 0, # 'Э' + 59: 0, # 'Ю' + 43: 0, # 'Я' + 3: 2, # 'а' + 21: 0, # 'б' + 10: 1, # 'в' + 19: 0, # 'г' + 13: 2, # 'д' + 2: 2, # 'е' + 24: 0, # 'ж' + 20: 0, # 'з' + 4: 2, # 'и' + 23: 0, # 'й' + 11: 0, # 'к' + 8: 2, # 'л' + 12: 1, # 'м' + 5: 1, # 'н' + 1: 3, # 'о' + 15: 0, # 'п' + 9: 2, # 'Ñ€' + 7: 0, # 'Ñ' + 6: 0, # 'Ñ‚' + 14: 2, # 'у' + 39: 0, # 'Ñ„' + 26: 0, # 'Ñ…' + 28: 0, # 'ц' + 22: 0, # 'ч' + 25: 0, # 'ш' + 29: 0, # 'щ' + 54: 0, # 'ÑŠ' + 18: 0, # 'Ñ‹' + 17: 1, # 'ÑŒ' + 30: 1, # 'Ñ' + 27: 1, # 'ÑŽ' + 16: 0, # 'Ñ' + }, + 41: { # 'Д' + 37: 1, # 'Ð' + 44: 0, # 'Б' + 33: 1, # 'Ð’' + 46: 0, # 'Г' + 41: 0, # 'Д' + 48: 2, # 'Е' + 56: 1, # 'Ж' + 51: 0, # 'З' + 42: 1, # 'И' + 60: 0, # 'Й' + 36: 1, # 'К' + 49: 1, # 'Л' + 38: 0, # 'Ðœ' + 31: 1, # 'Ð' + 34: 1, # 'О' + 35: 0, # 'П' + 45: 1, # 'Р' + 32: 1, # 'С' + 40: 0, # 'Т' + 52: 1, # 'У' + 53: 0, # 'Ф' + 55: 0, # 'Ð¥' + 58: 1, # 'Ц' + 50: 1, # 'Ч' + 57: 0, # 'Ш' + 63: 0, # 'Щ' + 62: 1, # 'Ы' + 61: 1, # 'Ь' + 47: 0, # 'Э' + 59: 0, # 'Ю' + 43: 1, # 'Я' + 3: 3, # 'а' + 21: 0, # 'б' + 10: 2, # 'в' + 19: 0, # 'г' + 13: 0, # 'д' + 2: 2, # 'е' + 24: 3, # 'ж' + 20: 1, # 'з' + 4: 2, # 'и' + 23: 0, # 'й' + 11: 0, # 'к' + 8: 2, # 'л' + 12: 1, # 'м' + 5: 1, # 'н' + 1: 3, # 'о' + 15: 0, # 'п' + 9: 2, # 'Ñ€' + 7: 0, # 'Ñ' + 6: 0, # 'Ñ‚' + 14: 2, # 'у' + 39: 0, # 'Ñ„' + 26: 1, # 'Ñ…' + 28: 0, # 'ц' + 22: 0, # 'ч' + 25: 0, # 'ш' + 29: 0, # 'щ' + 54: 0, # 'ÑŠ' + 18: 1, # 'Ñ‹' + 17: 1, # 'ÑŒ' + 30: 2, # 'Ñ' + 27: 1, # 'ÑŽ' + 16: 1, # 'Ñ' + }, + 48: { # 'Е' + 37: 1, # 'Ð' + 44: 1, # 'Б' + 33: 1, # 'Ð’' + 46: 1, # 'Г' + 41: 1, # 'Д' + 48: 1, # 'Е' + 56: 1, # 'Ж' + 51: 1, # 'З' + 42: 1, # 'И' + 60: 1, # 'Й' + 36: 1, # 'К' + 49: 1, # 'Л' + 38: 1, # 'Ðœ' + 31: 2, # 'Ð' + 34: 1, # 'О' + 35: 1, # 'П' + 45: 2, # 'Р' + 32: 2, # 'С' + 40: 1, # 'Т' + 52: 0, # 'У' + 53: 0, # 'Ф' + 55: 1, # 'Ð¥' + 58: 1, # 'Ц' + 50: 1, # 'Ч' + 57: 1, # 'Ш' + 63: 1, # 'Щ' + 62: 0, # 'Ы' + 61: 0, # 'Ь' + 47: 0, # 'Э' + 59: 0, # 'Ю' + 43: 1, # 'Я' + 3: 0, # 'а' + 21: 0, # 'б' + 10: 2, # 'в' + 19: 2, # 'г' + 13: 2, # 'д' + 2: 2, # 'е' + 24: 1, # 'ж' + 20: 1, # 'з' + 4: 0, # 'и' + 23: 2, # 'й' + 11: 1, # 'к' + 8: 2, # 'л' + 12: 2, # 'м' + 5: 1, # 'н' + 1: 0, # 'о' + 15: 1, # 'п' + 9: 1, # 'Ñ€' + 7: 3, # 'Ñ' + 6: 0, # 'Ñ‚' + 14: 0, # 'у' + 39: 1, # 'Ñ„' + 26: 1, # 'Ñ…' + 28: 0, # 'ц' + 22: 0, # 'ч' + 25: 1, # 'ш' + 29: 2, # 'щ' + 54: 0, # 'ÑŠ' + 18: 0, # 'Ñ‹' + 17: 0, # 'ÑŒ' + 30: 0, # 'Ñ' + 27: 1, # 'ÑŽ' + 16: 0, # 'Ñ' + }, + 56: { # 'Ж' + 37: 1, # 'Ð' + 44: 0, # 'Б' + 33: 0, # 'Ð’' + 46: 0, # 'Г' + 41: 1, # 'Д' + 48: 1, # 'Е' + 56: 0, # 'Ж' + 51: 1, # 'З' + 42: 1, # 'И' + 60: 0, # 'Й' + 36: 0, # 'К' + 49: 0, # 'Л' + 38: 0, # 'Ðœ' + 31: 1, # 'Ð' + 34: 1, # 'О' + 35: 0, # 'П' + 45: 0, # 'Р' + 32: 0, # 'С' + 40: 0, # 'Т' + 52: 1, # 'У' + 53: 0, # 'Ф' + 55: 0, # 'Ð¥' + 58: 0, # 'Ц' + 50: 0, # 'Ч' + 57: 0, # 'Ш' + 63: 0, # 'Щ' + 62: 0, # 'Ы' + 61: 0, # 'Ь' + 47: 0, # 'Э' + 59: 0, # 'Ю' + 43: 0, # 'Я' + 3: 2, # 'а' + 21: 1, # 'б' + 10: 0, # 'в' + 19: 1, # 'г' + 13: 1, # 'д' + 2: 2, # 'е' + 24: 1, # 'ж' + 20: 0, # 'з' + 4: 2, # 'и' + 23: 0, # 'й' + 11: 0, # 'к' + 8: 0, # 'л' + 12: 1, # 'м' + 5: 0, # 'н' + 1: 2, # 'о' + 15: 0, # 'п' + 9: 1, # 'Ñ€' + 7: 0, # 'Ñ' + 6: 0, # 'Ñ‚' + 14: 2, # 'у' + 39: 0, # 'Ñ„' + 26: 0, # 'Ñ…' + 28: 0, # 'ц' + 22: 0, # 'ч' + 25: 0, # 'ш' + 29: 0, # 'щ' + 54: 0, # 'ÑŠ' + 18: 0, # 'Ñ‹' + 17: 0, # 'ÑŒ' + 30: 0, # 'Ñ' + 27: 2, # 'ÑŽ' + 16: 0, # 'Ñ' + }, + 51: { # 'З' + 37: 1, # 'Ð' + 44: 0, # 'Б' + 33: 1, # 'Ð’' + 46: 1, # 'Г' + 41: 1, # 'Д' + 48: 1, # 'Е' + 56: 0, # 'Ж' + 51: 0, # 'З' + 42: 1, # 'И' + 60: 0, # 'Й' + 36: 0, # 'К' + 49: 1, # 'Л' + 38: 1, # 'Ðœ' + 31: 1, # 'Ð' + 34: 1, # 'О' + 35: 0, # 'П' + 45: 1, # 'Р' + 32: 0, # 'С' + 40: 0, # 'Т' + 52: 1, # 'У' + 53: 0, # 'Ф' + 55: 0, # 'Ð¥' + 58: 0, # 'Ц' + 50: 0, # 'Ч' + 57: 0, # 'Ш' + 63: 0, # 'Щ' + 62: 1, # 'Ы' + 61: 1, # 'Ь' + 47: 0, # 'Э' + 59: 0, # 'Ю' + 43: 0, # 'Я' + 3: 3, # 'а' + 21: 1, # 'б' + 10: 2, # 'в' + 19: 0, # 'г' + 13: 2, # 'д' + 2: 2, # 'е' + 24: 0, # 'ж' + 20: 0, # 'з' + 4: 2, # 'и' + 23: 0, # 'й' + 11: 0, # 'к' + 8: 1, # 'л' + 12: 1, # 'м' + 5: 2, # 'н' + 1: 2, # 'о' + 15: 0, # 'п' + 9: 1, # 'Ñ€' + 7: 0, # 'Ñ' + 6: 0, # 'Ñ‚' + 14: 1, # 'у' + 39: 0, # 'Ñ„' + 26: 0, # 'Ñ…' + 28: 0, # 'ц' + 22: 0, # 'ч' + 25: 0, # 'ш' + 29: 0, # 'щ' + 54: 0, # 'ÑŠ' + 18: 1, # 'Ñ‹' + 17: 0, # 'ÑŒ' + 30: 0, # 'Ñ' + 27: 0, # 'ÑŽ' + 16: 1, # 'Ñ' + }, + 42: { # 'И' + 37: 1, # 'Ð' + 44: 1, # 'Б' + 33: 1, # 'Ð’' + 46: 1, # 'Г' + 41: 1, # 'Д' + 48: 2, # 'Е' + 56: 1, # 'Ж' + 51: 1, # 'З' + 42: 1, # 'И' + 60: 1, # 'Й' + 36: 1, # 'К' + 49: 1, # 'Л' + 38: 1, # 'Ðœ' + 31: 1, # 'Ð' + 34: 1, # 'О' + 35: 1, # 'П' + 45: 1, # 'Р' + 32: 2, # 'С' + 40: 1, # 'Т' + 52: 0, # 'У' + 53: 1, # 'Ф' + 55: 1, # 'Ð¥' + 58: 1, # 'Ц' + 50: 1, # 'Ч' + 57: 0, # 'Ш' + 63: 1, # 'Щ' + 62: 0, # 'Ы' + 61: 0, # 'Ь' + 47: 0, # 'Э' + 59: 1, # 'Ю' + 43: 1, # 'Я' + 3: 1, # 'а' + 21: 2, # 'б' + 10: 2, # 'в' + 19: 2, # 'г' + 13: 2, # 'д' + 2: 2, # 'е' + 24: 0, # 'ж' + 20: 2, # 'з' + 4: 1, # 'и' + 23: 0, # 'й' + 11: 1, # 'к' + 8: 2, # 'л' + 12: 2, # 'м' + 5: 2, # 'н' + 1: 1, # 'о' + 15: 1, # 'п' + 9: 2, # 'Ñ€' + 7: 2, # 'Ñ' + 6: 2, # 'Ñ‚' + 14: 1, # 'у' + 39: 1, # 'Ñ„' + 26: 2, # 'Ñ…' + 28: 0, # 'ц' + 22: 0, # 'ч' + 25: 1, # 'ш' + 29: 1, # 'щ' + 54: 0, # 'ÑŠ' + 18: 0, # 'Ñ‹' + 17: 0, # 'ÑŒ' + 30: 0, # 'Ñ' + 27: 1, # 'ÑŽ' + 16: 0, # 'Ñ' + }, + 60: { # 'Й' + 37: 0, # 'Ð' + 44: 0, # 'Б' + 33: 0, # 'Ð’' + 46: 0, # 'Г' + 41: 1, # 'Д' + 48: 0, # 'Е' + 56: 0, # 'Ж' + 51: 0, # 'З' + 42: 0, # 'И' + 60: 0, # 'Й' + 36: 1, # 'К' + 49: 1, # 'Л' + 38: 0, # 'Ðœ' + 31: 1, # 'Ð' + 34: 0, # 'О' + 35: 0, # 'П' + 45: 0, # 'Р' + 32: 1, # 'С' + 40: 1, # 'Т' + 52: 0, # 'У' + 53: 0, # 'Ф' + 55: 1, # 'Ð¥' + 58: 1, # 'Ц' + 50: 0, # 'Ч' + 57: 0, # 'Ш' + 63: 0, # 'Щ' + 62: 0, # 'Ы' + 61: 0, # 'Ь' + 47: 0, # 'Э' + 59: 0, # 'Ю' + 43: 0, # 'Я' + 3: 0, # 'а' + 21: 0, # 'б' + 10: 0, # 'в' + 19: 0, # 'г' + 13: 0, # 'д' + 2: 1, # 'е' + 24: 0, # 'ж' + 20: 0, # 'з' + 4: 0, # 'и' + 23: 0, # 'й' + 11: 0, # 'к' + 8: 0, # 'л' + 12: 0, # 'м' + 5: 0, # 'н' + 1: 2, # 'о' + 15: 0, # 'п' + 9: 0, # 'Ñ€' + 7: 0, # 'Ñ' + 6: 0, # 'Ñ‚' + 14: 0, # 'у' + 39: 0, # 'Ñ„' + 26: 0, # 'Ñ…' + 28: 0, # 'ц' + 22: 0, # 'ч' + 25: 0, # 'ш' + 29: 0, # 'щ' + 54: 0, # 'ÑŠ' + 18: 0, # 'Ñ‹' + 17: 0, # 'ÑŒ' + 30: 0, # 'Ñ' + 27: 0, # 'ÑŽ' + 16: 0, # 'Ñ' + }, + 36: { # 'К' + 37: 2, # 'Ð' + 44: 0, # 'Б' + 33: 1, # 'Ð’' + 46: 0, # 'Г' + 41: 0, # 'Д' + 48: 1, # 'Е' + 56: 0, # 'Ж' + 51: 1, # 'З' + 42: 1, # 'И' + 60: 0, # 'Й' + 36: 0, # 'К' + 49: 1, # 'Л' + 38: 0, # 'Ðœ' + 31: 1, # 'Ð' + 34: 2, # 'О' + 35: 1, # 'П' + 45: 1, # 'Р' + 32: 1, # 'С' + 40: 1, # 'Т' + 52: 1, # 'У' + 53: 0, # 'Ф' + 55: 0, # 'Ð¥' + 58: 1, # 'Ц' + 50: 0, # 'Ч' + 57: 0, # 'Ш' + 63: 0, # 'Щ' + 62: 0, # 'Ы' + 61: 0, # 'Ь' + 47: 0, # 'Э' + 59: 0, # 'Ю' + 43: 0, # 'Я' + 3: 3, # 'а' + 21: 0, # 'б' + 10: 1, # 'в' + 19: 0, # 'г' + 13: 0, # 'д' + 2: 2, # 'е' + 24: 0, # 'ж' + 20: 0, # 'з' + 4: 2, # 'и' + 23: 0, # 'й' + 11: 0, # 'к' + 8: 2, # 'л' + 12: 0, # 'м' + 5: 1, # 'н' + 1: 3, # 'о' + 15: 0, # 'п' + 9: 2, # 'Ñ€' + 7: 2, # 'Ñ' + 6: 2, # 'Ñ‚' + 14: 2, # 'у' + 39: 0, # 'Ñ„' + 26: 1, # 'Ñ…' + 28: 0, # 'ц' + 22: 0, # 'ч' + 25: 0, # 'ш' + 29: 0, # 'щ' + 54: 0, # 'ÑŠ' + 18: 1, # 'Ñ‹' + 17: 1, # 'ÑŒ' + 30: 2, # 'Ñ' + 27: 1, # 'ÑŽ' + 16: 0, # 'Ñ' + }, + 49: { # 'Л' + 37: 2, # 'Ð' + 44: 0, # 'Б' + 33: 0, # 'Ð’' + 46: 1, # 'Г' + 41: 0, # 'Д' + 48: 1, # 'Е' + 56: 1, # 'Ж' + 51: 0, # 'З' + 42: 1, # 'И' + 60: 0, # 'Й' + 36: 1, # 'К' + 49: 1, # 'Л' + 38: 1, # 'Ðœ' + 31: 0, # 'Ð' + 34: 1, # 'О' + 35: 1, # 'П' + 45: 0, # 'Р' + 32: 1, # 'С' + 40: 1, # 'Т' + 52: 1, # 'У' + 53: 0, # 'Ф' + 55: 0, # 'Ð¥' + 58: 0, # 'Ц' + 50: 1, # 'Ч' + 57: 0, # 'Ш' + 63: 0, # 'Щ' + 62: 1, # 'Ы' + 61: 1, # 'Ь' + 47: 0, # 'Э' + 59: 1, # 'Ю' + 43: 1, # 'Я' + 3: 2, # 'а' + 21: 0, # 'б' + 10: 0, # 'в' + 19: 1, # 'г' + 13: 0, # 'д' + 2: 2, # 'е' + 24: 1, # 'ж' + 20: 0, # 'з' + 4: 2, # 'и' + 23: 0, # 'й' + 11: 0, # 'к' + 8: 1, # 'л' + 12: 0, # 'м' + 5: 1, # 'н' + 1: 2, # 'о' + 15: 0, # 'п' + 9: 0, # 'Ñ€' + 7: 0, # 'Ñ' + 6: 0, # 'Ñ‚' + 14: 2, # 'у' + 39: 0, # 'Ñ„' + 26: 1, # 'Ñ…' + 28: 0, # 'ц' + 22: 0, # 'ч' + 25: 0, # 'ш' + 29: 0, # 'щ' + 54: 0, # 'ÑŠ' + 18: 1, # 'Ñ‹' + 17: 1, # 'ÑŒ' + 30: 2, # 'Ñ' + 27: 2, # 'ÑŽ' + 16: 1, # 'Ñ' + }, + 38: { # 'Ðœ' + 37: 1, # 'Ð' + 44: 1, # 'Б' + 33: 1, # 'Ð’' + 46: 0, # 'Г' + 41: 0, # 'Д' + 48: 1, # 'Е' + 56: 0, # 'Ж' + 51: 0, # 'З' + 42: 1, # 'И' + 60: 0, # 'Й' + 36: 1, # 'К' + 49: 1, # 'Л' + 38: 1, # 'Ðœ' + 31: 1, # 'Ð' + 34: 1, # 'О' + 35: 1, # 'П' + 45: 1, # 'Р' + 32: 1, # 'С' + 40: 1, # 'Т' + 52: 1, # 'У' + 53: 1, # 'Ф' + 55: 1, # 'Ð¥' + 58: 0, # 'Ц' + 50: 0, # 'Ч' + 57: 0, # 'Ш' + 63: 0, # 'Щ' + 62: 1, # 'Ы' + 61: 0, # 'Ь' + 47: 1, # 'Э' + 59: 0, # 'Ю' + 43: 1, # 'Я' + 3: 3, # 'а' + 21: 0, # 'б' + 10: 0, # 'в' + 19: 1, # 'г' + 13: 0, # 'д' + 2: 2, # 'е' + 24: 0, # 'ж' + 20: 0, # 'з' + 4: 3, # 'и' + 23: 0, # 'й' + 11: 0, # 'к' + 8: 1, # 'л' + 12: 1, # 'м' + 5: 2, # 'н' + 1: 3, # 'о' + 15: 0, # 'п' + 9: 1, # 'Ñ€' + 7: 1, # 'Ñ' + 6: 0, # 'Ñ‚' + 14: 2, # 'у' + 39: 0, # 'Ñ„' + 26: 0, # 'Ñ…' + 28: 0, # 'ц' + 22: 0, # 'ч' + 25: 0, # 'ш' + 29: 0, # 'щ' + 54: 0, # 'ÑŠ' + 18: 3, # 'Ñ‹' + 17: 1, # 'ÑŒ' + 30: 2, # 'Ñ' + 27: 1, # 'ÑŽ' + 16: 1, # 'Ñ' + }, + 31: { # 'Ð' + 37: 2, # 'Ð' + 44: 0, # 'Б' + 33: 0, # 'Ð’' + 46: 1, # 'Г' + 41: 1, # 'Д' + 48: 1, # 'Е' + 56: 0, # 'Ж' + 51: 1, # 'З' + 42: 2, # 'И' + 60: 0, # 'Й' + 36: 1, # 'К' + 49: 0, # 'Л' + 38: 0, # 'Ðœ' + 31: 1, # 'Ð' + 34: 1, # 'О' + 35: 0, # 'П' + 45: 1, # 'Р' + 32: 1, # 'С' + 40: 1, # 'Т' + 52: 1, # 'У' + 53: 1, # 'Ф' + 55: 1, # 'Ð¥' + 58: 1, # 'Ц' + 50: 1, # 'Ч' + 57: 0, # 'Ш' + 63: 0, # 'Щ' + 62: 1, # 'Ы' + 61: 1, # 'Ь' + 47: 1, # 'Э' + 59: 0, # 'Ю' + 43: 1, # 'Я' + 3: 3, # 'а' + 21: 0, # 'б' + 10: 0, # 'в' + 19: 0, # 'г' + 13: 0, # 'д' + 2: 3, # 'е' + 24: 0, # 'ж' + 20: 0, # 'з' + 4: 3, # 'и' + 23: 0, # 'й' + 11: 0, # 'к' + 8: 0, # 'л' + 12: 0, # 'м' + 5: 0, # 'н' + 1: 3, # 'о' + 15: 0, # 'п' + 9: 1, # 'Ñ€' + 7: 0, # 'Ñ' + 6: 0, # 'Ñ‚' + 14: 3, # 'у' + 39: 0, # 'Ñ„' + 26: 1, # 'Ñ…' + 28: 0, # 'ц' + 22: 0, # 'ч' + 25: 0, # 'ш' + 29: 0, # 'щ' + 54: 0, # 'ÑŠ' + 18: 1, # 'Ñ‹' + 17: 2, # 'ÑŒ' + 30: 1, # 'Ñ' + 27: 1, # 'ÑŽ' + 16: 1, # 'Ñ' + }, + 34: { # 'О' + 37: 0, # 'Ð' + 44: 1, # 'Б' + 33: 1, # 'Ð’' + 46: 1, # 'Г' + 41: 2, # 'Д' + 48: 1, # 'Е' + 56: 1, # 'Ж' + 51: 1, # 'З' + 42: 1, # 'И' + 60: 1, # 'Й' + 36: 1, # 'К' + 49: 2, # 'Л' + 38: 1, # 'Ðœ' + 31: 2, # 'Ð' + 34: 1, # 'О' + 35: 1, # 'П' + 45: 2, # 'Р' + 32: 1, # 'С' + 40: 1, # 'Т' + 52: 1, # 'У' + 53: 1, # 'Ф' + 55: 1, # 'Ð¥' + 58: 0, # 'Ц' + 50: 1, # 'Ч' + 57: 1, # 'Ш' + 63: 1, # 'Щ' + 62: 0, # 'Ы' + 61: 0, # 'Ь' + 47: 0, # 'Э' + 59: 0, # 'Ю' + 43: 1, # 'Я' + 3: 1, # 'а' + 21: 2, # 'б' + 10: 1, # 'в' + 19: 2, # 'г' + 13: 2, # 'д' + 2: 0, # 'е' + 24: 1, # 'ж' + 20: 1, # 'з' + 4: 0, # 'и' + 23: 1, # 'й' + 11: 2, # 'к' + 8: 2, # 'л' + 12: 1, # 'м' + 5: 3, # 'н' + 1: 0, # 'о' + 15: 2, # 'п' + 9: 2, # 'Ñ€' + 7: 2, # 'Ñ' + 6: 2, # 'Ñ‚' + 14: 1, # 'у' + 39: 1, # 'Ñ„' + 26: 2, # 'Ñ…' + 28: 1, # 'ц' + 22: 2, # 'ч' + 25: 2, # 'ш' + 29: 1, # 'щ' + 54: 0, # 'ÑŠ' + 18: 0, # 'Ñ‹' + 17: 0, # 'ÑŒ' + 30: 0, # 'Ñ' + 27: 0, # 'ÑŽ' + 16: 0, # 'Ñ' + }, + 35: { # 'П' + 37: 1, # 'Ð' + 44: 0, # 'Б' + 33: 0, # 'Ð’' + 46: 0, # 'Г' + 41: 0, # 'Д' + 48: 1, # 'Е' + 56: 0, # 'Ж' + 51: 0, # 'З' + 42: 1, # 'И' + 60: 0, # 'Й' + 36: 0, # 'К' + 49: 1, # 'Л' + 38: 0, # 'Ðœ' + 31: 1, # 'Ð' + 34: 1, # 'О' + 35: 1, # 'П' + 45: 2, # 'Р' + 32: 1, # 'С' + 40: 1, # 'Т' + 52: 1, # 'У' + 53: 0, # 'Ф' + 55: 0, # 'Ð¥' + 58: 0, # 'Ц' + 50: 0, # 'Ч' + 57: 0, # 'Ш' + 63: 0, # 'Щ' + 62: 1, # 'Ы' + 61: 1, # 'Ь' + 47: 0, # 'Э' + 59: 0, # 'Ю' + 43: 1, # 'Я' + 3: 2, # 'а' + 21: 0, # 'б' + 10: 0, # 'в' + 19: 0, # 'г' + 13: 0, # 'д' + 2: 2, # 'е' + 24: 0, # 'ж' + 20: 0, # 'з' + 4: 2, # 'и' + 23: 0, # 'й' + 11: 0, # 'к' + 8: 2, # 'л' + 12: 0, # 'м' + 5: 1, # 'н' + 1: 3, # 'о' + 15: 0, # 'п' + 9: 3, # 'Ñ€' + 7: 1, # 'Ñ' + 6: 1, # 'Ñ‚' + 14: 2, # 'у' + 39: 1, # 'Ñ„' + 26: 0, # 'Ñ…' + 28: 0, # 'ц' + 22: 0, # 'ч' + 25: 1, # 'ш' + 29: 0, # 'щ' + 54: 0, # 'ÑŠ' + 18: 1, # 'Ñ‹' + 17: 2, # 'ÑŒ' + 30: 1, # 'Ñ' + 27: 0, # 'ÑŽ' + 16: 2, # 'Ñ' + }, + 45: { # 'Р' + 37: 2, # 'Ð' + 44: 1, # 'Б' + 33: 1, # 'Ð’' + 46: 1, # 'Г' + 41: 1, # 'Д' + 48: 2, # 'Е' + 56: 1, # 'Ж' + 51: 0, # 'З' + 42: 2, # 'И' + 60: 0, # 'Й' + 36: 1, # 'К' + 49: 1, # 'Л' + 38: 1, # 'Ðœ' + 31: 1, # 'Ð' + 34: 2, # 'О' + 35: 0, # 'П' + 45: 1, # 'Р' + 32: 1, # 'С' + 40: 1, # 'Т' + 52: 1, # 'У' + 53: 0, # 'Ф' + 55: 1, # 'Ð¥' + 58: 1, # 'Ц' + 50: 1, # 'Ч' + 57: 1, # 'Ш' + 63: 0, # 'Щ' + 62: 1, # 'Ы' + 61: 1, # 'Ь' + 47: 1, # 'Э' + 59: 1, # 'Ю' + 43: 1, # 'Я' + 3: 3, # 'а' + 21: 0, # 'б' + 10: 1, # 'в' + 19: 0, # 'г' + 13: 0, # 'д' + 2: 2, # 'е' + 24: 1, # 'ж' + 20: 0, # 'з' + 4: 2, # 'и' + 23: 0, # 'й' + 11: 0, # 'к' + 8: 0, # 'л' + 12: 0, # 'м' + 5: 0, # 'н' + 1: 3, # 'о' + 15: 0, # 'п' + 9: 1, # 'Ñ€' + 7: 0, # 'Ñ' + 6: 0, # 'Ñ‚' + 14: 2, # 'у' + 39: 0, # 'Ñ„' + 26: 0, # 'Ñ…' + 28: 0, # 'ц' + 22: 0, # 'ч' + 25: 0, # 'ш' + 29: 0, # 'щ' + 54: 0, # 'ÑŠ' + 18: 2, # 'Ñ‹' + 17: 0, # 'ÑŒ' + 30: 1, # 'Ñ' + 27: 1, # 'ÑŽ' + 16: 2, # 'Ñ' + }, + 32: { # 'С' + 37: 1, # 'Ð' + 44: 1, # 'Б' + 33: 1, # 'Ð’' + 46: 1, # 'Г' + 41: 1, # 'Д' + 48: 1, # 'Е' + 56: 0, # 'Ж' + 51: 0, # 'З' + 42: 1, # 'И' + 60: 0, # 'Й' + 36: 1, # 'К' + 49: 1, # 'Л' + 38: 1, # 'Ðœ' + 31: 1, # 'Ð' + 34: 1, # 'О' + 35: 1, # 'П' + 45: 1, # 'Р' + 32: 1, # 'С' + 40: 2, # 'Т' + 52: 1, # 'У' + 53: 0, # 'Ф' + 55: 1, # 'Ð¥' + 58: 1, # 'Ц' + 50: 1, # 'Ч' + 57: 1, # 'Ш' + 63: 0, # 'Щ' + 62: 1, # 'Ы' + 61: 1, # 'Ь' + 47: 1, # 'Э' + 59: 1, # 'Ю' + 43: 1, # 'Я' + 3: 2, # 'а' + 21: 1, # 'б' + 10: 2, # 'в' + 19: 1, # 'г' + 13: 2, # 'д' + 2: 3, # 'е' + 24: 1, # 'ж' + 20: 1, # 'з' + 4: 2, # 'и' + 23: 0, # 'й' + 11: 2, # 'к' + 8: 2, # 'л' + 12: 2, # 'м' + 5: 2, # 'н' + 1: 2, # 'о' + 15: 2, # 'п' + 9: 2, # 'Ñ€' + 7: 1, # 'Ñ' + 6: 3, # 'Ñ‚' + 14: 2, # 'у' + 39: 1, # 'Ñ„' + 26: 1, # 'Ñ…' + 28: 1, # 'ц' + 22: 1, # 'ч' + 25: 0, # 'ш' + 29: 0, # 'щ' + 54: 1, # 'ÑŠ' + 18: 1, # 'Ñ‹' + 17: 1, # 'ÑŒ' + 30: 2, # 'Ñ' + 27: 1, # 'ÑŽ' + 16: 1, # 'Ñ' + }, + 40: { # 'Т' + 37: 1, # 'Ð' + 44: 0, # 'Б' + 33: 1, # 'Ð’' + 46: 0, # 'Г' + 41: 0, # 'Д' + 48: 1, # 'Е' + 56: 0, # 'Ж' + 51: 0, # 'З' + 42: 1, # 'И' + 60: 0, # 'Й' + 36: 1, # 'К' + 49: 1, # 'Л' + 38: 1, # 'Ðœ' + 31: 1, # 'Ð' + 34: 2, # 'О' + 35: 0, # 'П' + 45: 1, # 'Р' + 32: 1, # 'С' + 40: 1, # 'Т' + 52: 1, # 'У' + 53: 0, # 'Ф' + 55: 0, # 'Ð¥' + 58: 0, # 'Ц' + 50: 1, # 'Ч' + 57: 0, # 'Ш' + 63: 0, # 'Щ' + 62: 1, # 'Ы' + 61: 1, # 'Ь' + 47: 1, # 'Э' + 59: 1, # 'Ю' + 43: 1, # 'Я' + 3: 3, # 'а' + 21: 1, # 'б' + 10: 2, # 'в' + 19: 0, # 'г' + 13: 0, # 'д' + 2: 3, # 'е' + 24: 0, # 'ж' + 20: 0, # 'з' + 4: 2, # 'и' + 23: 0, # 'й' + 11: 1, # 'к' + 8: 1, # 'л' + 12: 0, # 'м' + 5: 0, # 'н' + 1: 3, # 'о' + 15: 0, # 'п' + 9: 2, # 'Ñ€' + 7: 1, # 'Ñ' + 6: 0, # 'Ñ‚' + 14: 2, # 'у' + 39: 0, # 'Ñ„' + 26: 0, # 'Ñ…' + 28: 0, # 'ц' + 22: 0, # 'ч' + 25: 0, # 'ш' + 29: 1, # 'щ' + 54: 0, # 'ÑŠ' + 18: 3, # 'Ñ‹' + 17: 1, # 'ÑŒ' + 30: 2, # 'Ñ' + 27: 1, # 'ÑŽ' + 16: 1, # 'Ñ' + }, + 52: { # 'У' + 37: 1, # 'Ð' + 44: 1, # 'Б' + 33: 1, # 'Ð’' + 46: 1, # 'Г' + 41: 1, # 'Д' + 48: 1, # 'Е' + 56: 1, # 'Ж' + 51: 0, # 'З' + 42: 0, # 'И' + 60: 1, # 'Й' + 36: 1, # 'К' + 49: 1, # 'Л' + 38: 1, # 'Ðœ' + 31: 1, # 'Ð' + 34: 1, # 'О' + 35: 1, # 'П' + 45: 1, # 'Р' + 32: 1, # 'С' + 40: 1, # 'Т' + 52: 0, # 'У' + 53: 0, # 'Ф' + 55: 1, # 'Ð¥' + 58: 0, # 'Ц' + 50: 1, # 'Ч' + 57: 1, # 'Ш' + 63: 1, # 'Щ' + 62: 0, # 'Ы' + 61: 0, # 'Ь' + 47: 0, # 'Э' + 59: 1, # 'Ю' + 43: 0, # 'Я' + 3: 1, # 'а' + 21: 2, # 'б' + 10: 2, # 'в' + 19: 1, # 'г' + 13: 2, # 'д' + 2: 1, # 'е' + 24: 2, # 'ж' + 20: 2, # 'з' + 4: 2, # 'и' + 23: 1, # 'й' + 11: 1, # 'к' + 8: 2, # 'л' + 12: 2, # 'м' + 5: 1, # 'н' + 1: 2, # 'о' + 15: 1, # 'п' + 9: 2, # 'Ñ€' + 7: 2, # 'Ñ' + 6: 2, # 'Ñ‚' + 14: 0, # 'у' + 39: 1, # 'Ñ„' + 26: 1, # 'Ñ…' + 28: 1, # 'ц' + 22: 2, # 'ч' + 25: 1, # 'ш' + 29: 1, # 'щ' + 54: 0, # 'ÑŠ' + 18: 0, # 'Ñ‹' + 17: 0, # 'ÑŒ' + 30: 2, # 'Ñ' + 27: 1, # 'ÑŽ' + 16: 0, # 'Ñ' + }, + 53: { # 'Ф' + 37: 1, # 'Ð' + 44: 1, # 'Б' + 33: 0, # 'Ð’' + 46: 0, # 'Г' + 41: 0, # 'Д' + 48: 1, # 'Е' + 56: 0, # 'Ж' + 51: 0, # 'З' + 42: 1, # 'И' + 60: 0, # 'Й' + 36: 0, # 'К' + 49: 1, # 'Л' + 38: 0, # 'Ðœ' + 31: 0, # 'Ð' + 34: 1, # 'О' + 35: 0, # 'П' + 45: 1, # 'Р' + 32: 0, # 'С' + 40: 0, # 'Т' + 52: 1, # 'У' + 53: 0, # 'Ф' + 55: 0, # 'Ð¥' + 58: 0, # 'Ц' + 50: 0, # 'Ч' + 57: 0, # 'Ш' + 63: 0, # 'Щ' + 62: 0, # 'Ы' + 61: 0, # 'Ь' + 47: 0, # 'Э' + 59: 0, # 'Ю' + 43: 0, # 'Я' + 3: 2, # 'а' + 21: 0, # 'б' + 10: 0, # 'в' + 19: 0, # 'г' + 13: 0, # 'д' + 2: 2, # 'е' + 24: 0, # 'ж' + 20: 0, # 'з' + 4: 2, # 'и' + 23: 0, # 'й' + 11: 0, # 'к' + 8: 2, # 'л' + 12: 0, # 'м' + 5: 0, # 'н' + 1: 2, # 'о' + 15: 0, # 'п' + 9: 2, # 'Ñ€' + 7: 0, # 'Ñ' + 6: 1, # 'Ñ‚' + 14: 2, # 'у' + 39: 0, # 'Ñ„' + 26: 0, # 'Ñ…' + 28: 0, # 'ц' + 22: 0, # 'ч' + 25: 0, # 'ш' + 29: 0, # 'щ' + 54: 0, # 'ÑŠ' + 18: 0, # 'Ñ‹' + 17: 1, # 'ÑŒ' + 30: 2, # 'Ñ' + 27: 0, # 'ÑŽ' + 16: 0, # 'Ñ' + }, + 55: { # 'Ð¥' + 37: 1, # 'Ð' + 44: 0, # 'Б' + 33: 1, # 'Ð’' + 46: 0, # 'Г' + 41: 0, # 'Д' + 48: 0, # 'Е' + 56: 0, # 'Ж' + 51: 0, # 'З' + 42: 1, # 'И' + 60: 0, # 'Й' + 36: 0, # 'К' + 49: 1, # 'Л' + 38: 1, # 'Ðœ' + 31: 1, # 'Ð' + 34: 1, # 'О' + 35: 0, # 'П' + 45: 0, # 'Р' + 32: 0, # 'С' + 40: 0, # 'Т' + 52: 0, # 'У' + 53: 0, # 'Ф' + 55: 0, # 'Ð¥' + 58: 0, # 'Ц' + 50: 0, # 'Ч' + 57: 0, # 'Ш' + 63: 0, # 'Щ' + 62: 0, # 'Ы' + 61: 0, # 'Ь' + 47: 0, # 'Э' + 59: 0, # 'Ю' + 43: 0, # 'Я' + 3: 2, # 'а' + 21: 0, # 'б' + 10: 2, # 'в' + 19: 0, # 'г' + 13: 0, # 'д' + 2: 2, # 'е' + 24: 0, # 'ж' + 20: 0, # 'з' + 4: 2, # 'и' + 23: 0, # 'й' + 11: 0, # 'к' + 8: 2, # 'л' + 12: 1, # 'м' + 5: 0, # 'н' + 1: 2, # 'о' + 15: 0, # 'п' + 9: 2, # 'Ñ€' + 7: 0, # 'Ñ' + 6: 0, # 'Ñ‚' + 14: 1, # 'у' + 39: 0, # 'Ñ„' + 26: 0, # 'Ñ…' + 28: 0, # 'ц' + 22: 0, # 'ч' + 25: 0, # 'ш' + 29: 0, # 'щ' + 54: 0, # 'ÑŠ' + 18: 0, # 'Ñ‹' + 17: 1, # 'ÑŒ' + 30: 1, # 'Ñ' + 27: 0, # 'ÑŽ' + 16: 0, # 'Ñ' + }, + 58: { # 'Ц' + 37: 1, # 'Ð' + 44: 0, # 'Б' + 33: 0, # 'Ð’' + 46: 0, # 'Г' + 41: 0, # 'Д' + 48: 1, # 'Е' + 56: 0, # 'Ж' + 51: 0, # 'З' + 42: 1, # 'И' + 60: 0, # 'Й' + 36: 1, # 'К' + 49: 0, # 'Л' + 38: 0, # 'Ðœ' + 31: 0, # 'Ð' + 34: 1, # 'О' + 35: 0, # 'П' + 45: 0, # 'Р' + 32: 0, # 'С' + 40: 0, # 'Т' + 52: 1, # 'У' + 53: 0, # 'Ф' + 55: 0, # 'Ð¥' + 58: 0, # 'Ц' + 50: 0, # 'Ч' + 57: 0, # 'Ш' + 63: 0, # 'Щ' + 62: 1, # 'Ы' + 61: 0, # 'Ь' + 47: 0, # 'Э' + 59: 0, # 'Ю' + 43: 0, # 'Я' + 3: 1, # 'а' + 21: 0, # 'б' + 10: 1, # 'в' + 19: 0, # 'г' + 13: 0, # 'д' + 2: 2, # 'е' + 24: 0, # 'ж' + 20: 0, # 'з' + 4: 2, # 'и' + 23: 0, # 'й' + 11: 0, # 'к' + 8: 0, # 'л' + 12: 0, # 'м' + 5: 0, # 'н' + 1: 0, # 'о' + 15: 0, # 'п' + 9: 0, # 'Ñ€' + 7: 0, # 'Ñ' + 6: 0, # 'Ñ‚' + 14: 1, # 'у' + 39: 0, # 'Ñ„' + 26: 0, # 'Ñ…' + 28: 0, # 'ц' + 22: 0, # 'ч' + 25: 0, # 'ш' + 29: 0, # 'щ' + 54: 0, # 'ÑŠ' + 18: 1, # 'Ñ‹' + 17: 0, # 'ÑŒ' + 30: 0, # 'Ñ' + 27: 1, # 'ÑŽ' + 16: 0, # 'Ñ' + }, + 50: { # 'Ч' + 37: 1, # 'Ð' + 44: 0, # 'Б' + 33: 0, # 'Ð’' + 46: 0, # 'Г' + 41: 0, # 'Д' + 48: 1, # 'Е' + 56: 0, # 'Ж' + 51: 0, # 'З' + 42: 1, # 'И' + 60: 0, # 'Й' + 36: 1, # 'К' + 49: 0, # 'Л' + 38: 0, # 'Ðœ' + 31: 1, # 'Ð' + 34: 0, # 'О' + 35: 1, # 'П' + 45: 0, # 'Р' + 32: 0, # 'С' + 40: 1, # 'Т' + 52: 1, # 'У' + 53: 0, # 'Ф' + 55: 0, # 'Ð¥' + 58: 0, # 'Ц' + 50: 0, # 'Ч' + 57: 0, # 'Ш' + 63: 0, # 'Щ' + 62: 0, # 'Ы' + 61: 1, # 'Ь' + 47: 0, # 'Э' + 59: 0, # 'Ю' + 43: 0, # 'Я' + 3: 2, # 'а' + 21: 0, # 'б' + 10: 0, # 'в' + 19: 0, # 'г' + 13: 0, # 'д' + 2: 2, # 'е' + 24: 0, # 'ж' + 20: 0, # 'з' + 4: 2, # 'и' + 23: 0, # 'й' + 11: 0, # 'к' + 8: 1, # 'л' + 12: 0, # 'м' + 5: 0, # 'н' + 1: 1, # 'о' + 15: 0, # 'п' + 9: 1, # 'Ñ€' + 7: 0, # 'Ñ' + 6: 3, # 'Ñ‚' + 14: 2, # 'у' + 39: 0, # 'Ñ„' + 26: 0, # 'Ñ…' + 28: 0, # 'ц' + 22: 0, # 'ч' + 25: 0, # 'ш' + 29: 0, # 'щ' + 54: 0, # 'ÑŠ' + 18: 0, # 'Ñ‹' + 17: 1, # 'ÑŒ' + 30: 0, # 'Ñ' + 27: 0, # 'ÑŽ' + 16: 0, # 'Ñ' + }, + 57: { # 'Ш' + 37: 1, # 'Ð' + 44: 0, # 'Б' + 33: 0, # 'Ð’' + 46: 0, # 'Г' + 41: 0, # 'Д' + 48: 1, # 'Е' + 56: 0, # 'Ж' + 51: 0, # 'З' + 42: 1, # 'И' + 60: 0, # 'Й' + 36: 1, # 'К' + 49: 1, # 'Л' + 38: 0, # 'Ðœ' + 31: 1, # 'Ð' + 34: 1, # 'О' + 35: 0, # 'П' + 45: 0, # 'Р' + 32: 0, # 'С' + 40: 0, # 'Т' + 52: 1, # 'У' + 53: 0, # 'Ф' + 55: 0, # 'Ð¥' + 58: 0, # 'Ц' + 50: 0, # 'Ч' + 57: 0, # 'Ш' + 63: 0, # 'Щ' + 62: 0, # 'Ы' + 61: 0, # 'Ь' + 47: 0, # 'Э' + 59: 0, # 'Ю' + 43: 0, # 'Я' + 3: 2, # 'а' + 21: 0, # 'б' + 10: 1, # 'в' + 19: 0, # 'г' + 13: 0, # 'д' + 2: 2, # 'е' + 24: 0, # 'ж' + 20: 0, # 'з' + 4: 1, # 'и' + 23: 0, # 'й' + 11: 1, # 'к' + 8: 2, # 'л' + 12: 1, # 'м' + 5: 1, # 'н' + 1: 2, # 'о' + 15: 2, # 'п' + 9: 1, # 'Ñ€' + 7: 0, # 'Ñ' + 6: 2, # 'Ñ‚' + 14: 2, # 'у' + 39: 0, # 'Ñ„' + 26: 1, # 'Ñ…' + 28: 0, # 'ц' + 22: 0, # 'ч' + 25: 1, # 'ш' + 29: 0, # 'щ' + 54: 0, # 'ÑŠ' + 18: 0, # 'Ñ‹' + 17: 0, # 'ÑŒ' + 30: 1, # 'Ñ' + 27: 0, # 'ÑŽ' + 16: 0, # 'Ñ' + }, + 63: { # 'Щ' + 37: 1, # 'Ð' + 44: 0, # 'Б' + 33: 0, # 'Ð’' + 46: 0, # 'Г' + 41: 0, # 'Д' + 48: 1, # 'Е' + 56: 0, # 'Ж' + 51: 0, # 'З' + 42: 1, # 'И' + 60: 0, # 'Й' + 36: 0, # 'К' + 49: 0, # 'Л' + 38: 0, # 'Ðœ' + 31: 0, # 'Ð' + 34: 0, # 'О' + 35: 0, # 'П' + 45: 0, # 'Р' + 32: 0, # 'С' + 40: 0, # 'Т' + 52: 0, # 'У' + 53: 0, # 'Ф' + 55: 0, # 'Ð¥' + 58: 0, # 'Ц' + 50: 0, # 'Ч' + 57: 0, # 'Ш' + 63: 0, # 'Щ' + 62: 0, # 'Ы' + 61: 1, # 'Ь' + 47: 0, # 'Э' + 59: 0, # 'Ю' + 43: 0, # 'Я' + 3: 1, # 'а' + 21: 0, # 'б' + 10: 0, # 'в' + 19: 0, # 'г' + 13: 0, # 'д' + 2: 1, # 'е' + 24: 0, # 'ж' + 20: 0, # 'з' + 4: 1, # 'и' + 23: 0, # 'й' + 11: 0, # 'к' + 8: 0, # 'л' + 12: 0, # 'м' + 5: 0, # 'н' + 1: 1, # 'о' + 15: 0, # 'п' + 9: 0, # 'Ñ€' + 7: 0, # 'Ñ' + 6: 0, # 'Ñ‚' + 14: 1, # 'у' + 39: 0, # 'Ñ„' + 26: 0, # 'Ñ…' + 28: 0, # 'ц' + 22: 0, # 'ч' + 25: 0, # 'ш' + 29: 0, # 'щ' + 54: 0, # 'ÑŠ' + 18: 0, # 'Ñ‹' + 17: 0, # 'ÑŒ' + 30: 0, # 'Ñ' + 27: 0, # 'ÑŽ' + 16: 0, # 'Ñ' + }, + 62: { # 'Ы' + 37: 0, # 'Ð' + 44: 0, # 'Б' + 33: 1, # 'Ð’' + 46: 1, # 'Г' + 41: 0, # 'Д' + 48: 1, # 'Е' + 56: 0, # 'Ж' + 51: 0, # 'З' + 42: 0, # 'И' + 60: 1, # 'Й' + 36: 1, # 'К' + 49: 1, # 'Л' + 38: 1, # 'Ðœ' + 31: 1, # 'Ð' + 34: 0, # 'О' + 35: 1, # 'П' + 45: 1, # 'Р' + 32: 1, # 'С' + 40: 1, # 'Т' + 52: 0, # 'У' + 53: 0, # 'Ф' + 55: 1, # 'Ð¥' + 58: 1, # 'Ц' + 50: 0, # 'Ч' + 57: 1, # 'Ш' + 63: 0, # 'Щ' + 62: 0, # 'Ы' + 61: 0, # 'Ь' + 47: 0, # 'Э' + 59: 0, # 'Ю' + 43: 0, # 'Я' + 3: 0, # 'а' + 21: 0, # 'б' + 10: 0, # 'в' + 19: 0, # 'г' + 13: 0, # 'д' + 2: 0, # 'е' + 24: 0, # 'ж' + 20: 0, # 'з' + 4: 0, # 'и' + 23: 0, # 'й' + 11: 0, # 'к' + 8: 0, # 'л' + 12: 0, # 'м' + 5: 0, # 'н' + 1: 0, # 'о' + 15: 0, # 'п' + 9: 0, # 'Ñ€' + 7: 0, # 'Ñ' + 6: 0, # 'Ñ‚' + 14: 0, # 'у' + 39: 0, # 'Ñ„' + 26: 0, # 'Ñ…' + 28: 0, # 'ц' + 22: 0, # 'ч' + 25: 0, # 'ш' + 29: 0, # 'щ' + 54: 0, # 'ÑŠ' + 18: 0, # 'Ñ‹' + 17: 0, # 'ÑŒ' + 30: 0, # 'Ñ' + 27: 0, # 'ÑŽ' + 16: 0, # 'Ñ' + }, + 61: { # 'Ь' + 37: 0, # 'Ð' + 44: 1, # 'Б' + 33: 1, # 'Ð’' + 46: 0, # 'Г' + 41: 1, # 'Д' + 48: 1, # 'Е' + 56: 0, # 'Ж' + 51: 0, # 'З' + 42: 1, # 'И' + 60: 0, # 'Й' + 36: 1, # 'К' + 49: 0, # 'Л' + 38: 1, # 'Ðœ' + 31: 1, # 'Ð' + 34: 1, # 'О' + 35: 0, # 'П' + 45: 0, # 'Р' + 32: 1, # 'С' + 40: 0, # 'Т' + 52: 0, # 'У' + 53: 1, # 'Ф' + 55: 0, # 'Ð¥' + 58: 0, # 'Ц' + 50: 0, # 'Ч' + 57: 1, # 'Ш' + 63: 0, # 'Щ' + 62: 0, # 'Ы' + 61: 0, # 'Ь' + 47: 0, # 'Э' + 59: 1, # 'Ю' + 43: 1, # 'Я' + 3: 0, # 'а' + 21: 0, # 'б' + 10: 0, # 'в' + 19: 0, # 'г' + 13: 0, # 'д' + 2: 0, # 'е' + 24: 0, # 'ж' + 20: 0, # 'з' + 4: 0, # 'и' + 23: 0, # 'й' + 11: 0, # 'к' + 8: 0, # 'л' + 12: 0, # 'м' + 5: 0, # 'н' + 1: 0, # 'о' + 15: 0, # 'п' + 9: 0, # 'Ñ€' + 7: 0, # 'Ñ' + 6: 0, # 'Ñ‚' + 14: 0, # 'у' + 39: 0, # 'Ñ„' + 26: 0, # 'Ñ…' + 28: 0, # 'ц' + 22: 0, # 'ч' + 25: 0, # 'ш' + 29: 0, # 'щ' + 54: 0, # 'ÑŠ' + 18: 0, # 'Ñ‹' + 17: 0, # 'ÑŒ' + 30: 0, # 'Ñ' + 27: 0, # 'ÑŽ' + 16: 0, # 'Ñ' + }, + 47: { # 'Э' + 37: 0, # 'Ð' + 44: 0, # 'Б' + 33: 1, # 'Ð’' + 46: 0, # 'Г' + 41: 1, # 'Д' + 48: 0, # 'Е' + 56: 0, # 'Ж' + 51: 0, # 'З' + 42: 0, # 'И' + 60: 1, # 'Й' + 36: 1, # 'К' + 49: 1, # 'Л' + 38: 1, # 'Ðœ' + 31: 1, # 'Ð' + 34: 0, # 'О' + 35: 1, # 'П' + 45: 1, # 'Р' + 32: 1, # 'С' + 40: 1, # 'Т' + 52: 0, # 'У' + 53: 0, # 'Ф' + 55: 0, # 'Ð¥' + 58: 0, # 'Ц' + 50: 0, # 'Ч' + 57: 0, # 'Ш' + 63: 0, # 'Щ' + 62: 0, # 'Ы' + 61: 0, # 'Ь' + 47: 0, # 'Э' + 59: 0, # 'Ю' + 43: 0, # 'Я' + 3: 1, # 'а' + 21: 1, # 'б' + 10: 2, # 'в' + 19: 1, # 'г' + 13: 2, # 'д' + 2: 0, # 'е' + 24: 1, # 'ж' + 20: 0, # 'з' + 4: 0, # 'и' + 23: 2, # 'й' + 11: 2, # 'к' + 8: 2, # 'л' + 12: 2, # 'м' + 5: 2, # 'н' + 1: 0, # 'о' + 15: 1, # 'п' + 9: 2, # 'Ñ€' + 7: 1, # 'Ñ' + 6: 3, # 'Ñ‚' + 14: 1, # 'у' + 39: 1, # 'Ñ„' + 26: 1, # 'Ñ…' + 28: 0, # 'ц' + 22: 0, # 'ч' + 25: 1, # 'ш' + 29: 0, # 'щ' + 54: 0, # 'ÑŠ' + 18: 0, # 'Ñ‹' + 17: 0, # 'ÑŒ' + 30: 0, # 'Ñ' + 27: 0, # 'ÑŽ' + 16: 0, # 'Ñ' + }, + 59: { # 'Ю' + 37: 1, # 'Ð' + 44: 1, # 'Б' + 33: 0, # 'Ð’' + 46: 0, # 'Г' + 41: 1, # 'Д' + 48: 0, # 'Е' + 56: 0, # 'Ж' + 51: 0, # 'З' + 42: 0, # 'И' + 60: 0, # 'Й' + 36: 0, # 'К' + 49: 0, # 'Л' + 38: 0, # 'Ðœ' + 31: 1, # 'Ð' + 34: 0, # 'О' + 35: 0, # 'П' + 45: 1, # 'Р' + 32: 0, # 'С' + 40: 1, # 'Т' + 52: 0, # 'У' + 53: 0, # 'Ф' + 55: 0, # 'Ð¥' + 58: 0, # 'Ц' + 50: 1, # 'Ч' + 57: 0, # 'Ш' + 63: 1, # 'Щ' + 62: 0, # 'Ы' + 61: 0, # 'Ь' + 47: 0, # 'Э' + 59: 0, # 'Ю' + 43: 0, # 'Я' + 3: 0, # 'а' + 21: 1, # 'б' + 10: 0, # 'в' + 19: 1, # 'г' + 13: 1, # 'д' + 2: 0, # 'е' + 24: 1, # 'ж' + 20: 0, # 'з' + 4: 0, # 'и' + 23: 0, # 'й' + 11: 1, # 'к' + 8: 2, # 'л' + 12: 1, # 'м' + 5: 2, # 'н' + 1: 0, # 'о' + 15: 1, # 'п' + 9: 1, # 'Ñ€' + 7: 1, # 'Ñ' + 6: 0, # 'Ñ‚' + 14: 0, # 'у' + 39: 0, # 'Ñ„' + 26: 1, # 'Ñ…' + 28: 0, # 'ц' + 22: 0, # 'ч' + 25: 0, # 'ш' + 29: 0, # 'щ' + 54: 0, # 'ÑŠ' + 18: 0, # 'Ñ‹' + 17: 0, # 'ÑŒ' + 30: 0, # 'Ñ' + 27: 0, # 'ÑŽ' + 16: 0, # 'Ñ' + }, + 43: { # 'Я' + 37: 0, # 'Ð' + 44: 0, # 'Б' + 33: 1, # 'Ð’' + 46: 1, # 'Г' + 41: 0, # 'Д' + 48: 1, # 'Е' + 56: 0, # 'Ж' + 51: 0, # 'З' + 42: 1, # 'И' + 60: 0, # 'Й' + 36: 1, # 'К' + 49: 0, # 'Л' + 38: 0, # 'Ðœ' + 31: 1, # 'Ð' + 34: 0, # 'О' + 35: 0, # 'П' + 45: 0, # 'Р' + 32: 1, # 'С' + 40: 1, # 'Т' + 52: 0, # 'У' + 53: 0, # 'Ф' + 55: 1, # 'Ð¥' + 58: 0, # 'Ц' + 50: 1, # 'Ч' + 57: 0, # 'Ш' + 63: 1, # 'Щ' + 62: 0, # 'Ы' + 61: 0, # 'Ь' + 47: 0, # 'Э' + 59: 1, # 'Ю' + 43: 1, # 'Я' + 3: 0, # 'а' + 21: 1, # 'б' + 10: 1, # 'в' + 19: 1, # 'г' + 13: 1, # 'д' + 2: 0, # 'е' + 24: 0, # 'ж' + 20: 1, # 'з' + 4: 0, # 'и' + 23: 1, # 'й' + 11: 1, # 'к' + 8: 1, # 'л' + 12: 1, # 'м' + 5: 2, # 'н' + 1: 0, # 'о' + 15: 1, # 'п' + 9: 1, # 'Ñ€' + 7: 1, # 'Ñ' + 6: 0, # 'Ñ‚' + 14: 0, # 'у' + 39: 0, # 'Ñ„' + 26: 1, # 'Ñ…' + 28: 0, # 'ц' + 22: 0, # 'ч' + 25: 1, # 'ш' + 29: 1, # 'щ' + 54: 0, # 'ÑŠ' + 18: 0, # 'Ñ‹' + 17: 0, # 'ÑŒ' + 30: 0, # 'Ñ' + 27: 0, # 'ÑŽ' + 16: 0, # 'Ñ' + }, + 3: { # 'а' + 37: 0, # 'Ð' + 44: 0, # 'Б' + 33: 0, # 'Ð’' + 46: 0, # 'Г' + 41: 0, # 'Д' + 48: 0, # 'Е' + 56: 0, # 'Ж' + 51: 0, # 'З' + 42: 1, # 'И' + 60: 0, # 'Й' + 36: 0, # 'К' + 49: 0, # 'Л' + 38: 0, # 'Ðœ' + 31: 1, # 'Ð' + 34: 0, # 'О' + 35: 0, # 'П' + 45: 0, # 'Р' + 32: 0, # 'С' + 40: 0, # 'Т' + 52: 0, # 'У' + 53: 0, # 'Ф' + 55: 0, # 'Ð¥' + 58: 0, # 'Ц' + 50: 0, # 'Ч' + 57: 0, # 'Ш' + 63: 0, # 'Щ' + 62: 0, # 'Ы' + 61: 0, # 'Ь' + 47: 0, # 'Э' + 59: 0, # 'Ю' + 43: 0, # 'Я' + 3: 2, # 'а' + 21: 3, # 'б' + 10: 3, # 'в' + 19: 3, # 'г' + 13: 3, # 'д' + 2: 3, # 'е' + 24: 3, # 'ж' + 20: 3, # 'з' + 4: 3, # 'и' + 23: 3, # 'й' + 11: 3, # 'к' + 8: 3, # 'л' + 12: 3, # 'м' + 5: 3, # 'н' + 1: 2, # 'о' + 15: 3, # 'п' + 9: 3, # 'Ñ€' + 7: 3, # 'Ñ' + 6: 3, # 'Ñ‚' + 14: 3, # 'у' + 39: 2, # 'Ñ„' + 26: 3, # 'Ñ…' + 28: 3, # 'ц' + 22: 3, # 'ч' + 25: 3, # 'ш' + 29: 3, # 'щ' + 54: 0, # 'ÑŠ' + 18: 0, # 'Ñ‹' + 17: 0, # 'ÑŒ' + 30: 2, # 'Ñ' + 27: 3, # 'ÑŽ' + 16: 3, # 'Ñ' + }, + 21: { # 'б' + 37: 0, # 'Ð' + 44: 0, # 'Б' + 33: 0, # 'Ð’' + 46: 0, # 'Г' + 41: 0, # 'Д' + 48: 0, # 'Е' + 56: 0, # 'Ж' + 51: 0, # 'З' + 42: 0, # 'И' + 60: 0, # 'Й' + 36: 1, # 'К' + 49: 0, # 'Л' + 38: 0, # 'Ðœ' + 31: 0, # 'Ð' + 34: 0, # 'О' + 35: 0, # 'П' + 45: 0, # 'Р' + 32: 0, # 'С' + 40: 0, # 'Т' + 52: 0, # 'У' + 53: 0, # 'Ф' + 55: 0, # 'Ð¥' + 58: 0, # 'Ц' + 50: 0, # 'Ч' + 57: 0, # 'Ш' + 63: 0, # 'Щ' + 62: 0, # 'Ы' + 61: 0, # 'Ь' + 47: 0, # 'Э' + 59: 0, # 'Ю' + 43: 0, # 'Я' + 3: 3, # 'а' + 21: 2, # 'б' + 10: 2, # 'в' + 19: 1, # 'г' + 13: 2, # 'д' + 2: 3, # 'е' + 24: 2, # 'ж' + 20: 1, # 'з' + 4: 3, # 'и' + 23: 0, # 'й' + 11: 2, # 'к' + 8: 3, # 'л' + 12: 2, # 'м' + 5: 3, # 'н' + 1: 3, # 'о' + 15: 1, # 'п' + 9: 3, # 'Ñ€' + 7: 3, # 'Ñ' + 6: 2, # 'Ñ‚' + 14: 3, # 'у' + 39: 0, # 'Ñ„' + 26: 2, # 'Ñ…' + 28: 1, # 'ц' + 22: 1, # 'ч' + 25: 2, # 'ш' + 29: 3, # 'щ' + 54: 2, # 'ÑŠ' + 18: 3, # 'Ñ‹' + 17: 2, # 'ÑŒ' + 30: 1, # 'Ñ' + 27: 2, # 'ÑŽ' + 16: 3, # 'Ñ' + }, + 10: { # 'в' + 37: 0, # 'Ð' + 44: 0, # 'Б' + 33: 0, # 'Ð’' + 46: 0, # 'Г' + 41: 0, # 'Д' + 48: 0, # 'Е' + 56: 0, # 'Ж' + 51: 0, # 'З' + 42: 0, # 'И' + 60: 0, # 'Й' + 36: 0, # 'К' + 49: 0, # 'Л' + 38: 0, # 'Ðœ' + 31: 0, # 'Ð' + 34: 0, # 'О' + 35: 0, # 'П' + 45: 0, # 'Р' + 32: 0, # 'С' + 40: 0, # 'Т' + 52: 0, # 'У' + 53: 0, # 'Ф' + 55: 0, # 'Ð¥' + 58: 0, # 'Ц' + 50: 0, # 'Ч' + 57: 0, # 'Ш' + 63: 0, # 'Щ' + 62: 0, # 'Ы' + 61: 0, # 'Ь' + 47: 0, # 'Э' + 59: 0, # 'Ю' + 43: 0, # 'Я' + 3: 3, # 'а' + 21: 2, # 'б' + 10: 2, # 'в' + 19: 2, # 'г' + 13: 3, # 'д' + 2: 3, # 'е' + 24: 1, # 'ж' + 20: 3, # 'з' + 4: 3, # 'и' + 23: 0, # 'й' + 11: 3, # 'к' + 8: 3, # 'л' + 12: 2, # 'м' + 5: 3, # 'н' + 1: 3, # 'о' + 15: 3, # 'п' + 9: 3, # 'Ñ€' + 7: 3, # 'Ñ' + 6: 3, # 'Ñ‚' + 14: 3, # 'у' + 39: 1, # 'Ñ„' + 26: 2, # 'Ñ…' + 28: 2, # 'ц' + 22: 2, # 'ч' + 25: 3, # 'ш' + 29: 2, # 'щ' + 54: 2, # 'ÑŠ' + 18: 3, # 'Ñ‹' + 17: 3, # 'ÑŒ' + 30: 1, # 'Ñ' + 27: 1, # 'ÑŽ' + 16: 3, # 'Ñ' + }, + 19: { # 'г' + 37: 0, # 'Ð' + 44: 0, # 'Б' + 33: 0, # 'Ð’' + 46: 0, # 'Г' + 41: 0, # 'Д' + 48: 0, # 'Е' + 56: 0, # 'Ж' + 51: 0, # 'З' + 42: 0, # 'И' + 60: 0, # 'Й' + 36: 0, # 'К' + 49: 0, # 'Л' + 38: 0, # 'Ðœ' + 31: 0, # 'Ð' + 34: 0, # 'О' + 35: 0, # 'П' + 45: 0, # 'Р' + 32: 0, # 'С' + 40: 0, # 'Т' + 52: 0, # 'У' + 53: 0, # 'Ф' + 55: 0, # 'Ð¥' + 58: 0, # 'Ц' + 50: 0, # 'Ч' + 57: 0, # 'Ш' + 63: 0, # 'Щ' + 62: 0, # 'Ы' + 61: 0, # 'Ь' + 47: 0, # 'Э' + 59: 0, # 'Ю' + 43: 0, # 'Я' + 3: 3, # 'а' + 21: 1, # 'б' + 10: 2, # 'в' + 19: 1, # 'г' + 13: 3, # 'д' + 2: 3, # 'е' + 24: 0, # 'ж' + 20: 1, # 'з' + 4: 3, # 'и' + 23: 0, # 'й' + 11: 2, # 'к' + 8: 3, # 'л' + 12: 2, # 'м' + 5: 3, # 'н' + 1: 3, # 'о' + 15: 0, # 'п' + 9: 3, # 'Ñ€' + 7: 2, # 'Ñ' + 6: 2, # 'Ñ‚' + 14: 3, # 'у' + 39: 1, # 'Ñ„' + 26: 1, # 'Ñ…' + 28: 1, # 'ц' + 22: 2, # 'ч' + 25: 1, # 'ш' + 29: 0, # 'щ' + 54: 0, # 'ÑŠ' + 18: 1, # 'Ñ‹' + 17: 1, # 'ÑŒ' + 30: 1, # 'Ñ' + 27: 1, # 'ÑŽ' + 16: 0, # 'Ñ' + }, + 13: { # 'д' + 37: 0, # 'Ð' + 44: 0, # 'Б' + 33: 0, # 'Ð’' + 46: 0, # 'Г' + 41: 0, # 'Д' + 48: 0, # 'Е' + 56: 0, # 'Ж' + 51: 0, # 'З' + 42: 0, # 'И' + 60: 0, # 'Й' + 36: 0, # 'К' + 49: 0, # 'Л' + 38: 0, # 'Ðœ' + 31: 0, # 'Ð' + 34: 0, # 'О' + 35: 0, # 'П' + 45: 0, # 'Р' + 32: 0, # 'С' + 40: 0, # 'Т' + 52: 0, # 'У' + 53: 0, # 'Ф' + 55: 0, # 'Ð¥' + 58: 0, # 'Ц' + 50: 0, # 'Ч' + 57: 0, # 'Ш' + 63: 0, # 'Щ' + 62: 0, # 'Ы' + 61: 0, # 'Ь' + 47: 0, # 'Э' + 59: 0, # 'Ю' + 43: 0, # 'Я' + 3: 3, # 'а' + 21: 2, # 'б' + 10: 3, # 'в' + 19: 2, # 'г' + 13: 2, # 'д' + 2: 3, # 'е' + 24: 2, # 'ж' + 20: 2, # 'з' + 4: 3, # 'и' + 23: 0, # 'й' + 11: 3, # 'к' + 8: 3, # 'л' + 12: 2, # 'м' + 5: 3, # 'н' + 1: 3, # 'о' + 15: 2, # 'п' + 9: 3, # 'Ñ€' + 7: 3, # 'Ñ' + 6: 3, # 'Ñ‚' + 14: 3, # 'у' + 39: 1, # 'Ñ„' + 26: 2, # 'Ñ…' + 28: 3, # 'ц' + 22: 2, # 'ч' + 25: 2, # 'ш' + 29: 1, # 'щ' + 54: 2, # 'ÑŠ' + 18: 3, # 'Ñ‹' + 17: 3, # 'ÑŒ' + 30: 1, # 'Ñ' + 27: 2, # 'ÑŽ' + 16: 3, # 'Ñ' + }, + 2: { # 'е' + 37: 0, # 'Ð' + 44: 0, # 'Б' + 33: 0, # 'Ð’' + 46: 0, # 'Г' + 41: 0, # 'Д' + 48: 0, # 'Е' + 56: 0, # 'Ж' + 51: 0, # 'З' + 42: 0, # 'И' + 60: 0, # 'Й' + 36: 0, # 'К' + 49: 0, # 'Л' + 38: 0, # 'Ðœ' + 31: 0, # 'Ð' + 34: 0, # 'О' + 35: 0, # 'П' + 45: 0, # 'Р' + 32: 0, # 'С' + 40: 0, # 'Т' + 52: 0, # 'У' + 53: 0, # 'Ф' + 55: 0, # 'Ð¥' + 58: 0, # 'Ц' + 50: 0, # 'Ч' + 57: 0, # 'Ш' + 63: 0, # 'Щ' + 62: 0, # 'Ы' + 61: 0, # 'Ь' + 47: 0, # 'Э' + 59: 0, # 'Ю' + 43: 0, # 'Я' + 3: 2, # 'а' + 21: 3, # 'б' + 10: 3, # 'в' + 19: 3, # 'г' + 13: 3, # 'д' + 2: 3, # 'е' + 24: 3, # 'ж' + 20: 3, # 'з' + 4: 2, # 'и' + 23: 3, # 'й' + 11: 3, # 'к' + 8: 3, # 'л' + 12: 3, # 'м' + 5: 3, # 'н' + 1: 3, # 'о' + 15: 3, # 'п' + 9: 3, # 'Ñ€' + 7: 3, # 'Ñ' + 6: 3, # 'Ñ‚' + 14: 2, # 'у' + 39: 2, # 'Ñ„' + 26: 3, # 'Ñ…' + 28: 3, # 'ц' + 22: 3, # 'ч' + 25: 3, # 'ш' + 29: 3, # 'щ' + 54: 0, # 'ÑŠ' + 18: 0, # 'Ñ‹' + 17: 0, # 'ÑŒ' + 30: 1, # 'Ñ' + 27: 2, # 'ÑŽ' + 16: 3, # 'Ñ' + }, + 24: { # 'ж' + 37: 0, # 'Ð' + 44: 0, # 'Б' + 33: 0, # 'Ð’' + 46: 0, # 'Г' + 41: 0, # 'Д' + 48: 0, # 'Е' + 56: 0, # 'Ж' + 51: 0, # 'З' + 42: 0, # 'И' + 60: 0, # 'Й' + 36: 0, # 'К' + 49: 0, # 'Л' + 38: 0, # 'Ðœ' + 31: 0, # 'Ð' + 34: 0, # 'О' + 35: 0, # 'П' + 45: 0, # 'Р' + 32: 0, # 'С' + 40: 0, # 'Т' + 52: 0, # 'У' + 53: 0, # 'Ф' + 55: 0, # 'Ð¥' + 58: 0, # 'Ц' + 50: 0, # 'Ч' + 57: 0, # 'Ш' + 63: 0, # 'Щ' + 62: 0, # 'Ы' + 61: 0, # 'Ь' + 47: 0, # 'Э' + 59: 0, # 'Ю' + 43: 0, # 'Я' + 3: 3, # 'а' + 21: 2, # 'б' + 10: 1, # 'в' + 19: 2, # 'г' + 13: 3, # 'д' + 2: 3, # 'е' + 24: 2, # 'ж' + 20: 1, # 'з' + 4: 3, # 'и' + 23: 0, # 'й' + 11: 2, # 'к' + 8: 2, # 'л' + 12: 1, # 'м' + 5: 3, # 'н' + 1: 2, # 'о' + 15: 1, # 'п' + 9: 2, # 'Ñ€' + 7: 2, # 'Ñ' + 6: 1, # 'Ñ‚' + 14: 3, # 'у' + 39: 1, # 'Ñ„' + 26: 0, # 'Ñ…' + 28: 1, # 'ц' + 22: 2, # 'ч' + 25: 0, # 'ш' + 29: 0, # 'щ' + 54: 0, # 'ÑŠ' + 18: 1, # 'Ñ‹' + 17: 2, # 'ÑŒ' + 30: 1, # 'Ñ' + 27: 1, # 'ÑŽ' + 16: 1, # 'Ñ' + }, + 20: { # 'з' + 37: 0, # 'Ð' + 44: 0, # 'Б' + 33: 0, # 'Ð’' + 46: 0, # 'Г' + 41: 0, # 'Д' + 48: 0, # 'Е' + 56: 0, # 'Ж' + 51: 0, # 'З' + 42: 0, # 'И' + 60: 0, # 'Й' + 36: 0, # 'К' + 49: 0, # 'Л' + 38: 0, # 'Ðœ' + 31: 0, # 'Ð' + 34: 0, # 'О' + 35: 0, # 'П' + 45: 0, # 'Р' + 32: 0, # 'С' + 40: 0, # 'Т' + 52: 0, # 'У' + 53: 0, # 'Ф' + 55: 0, # 'Ð¥' + 58: 0, # 'Ц' + 50: 0, # 'Ч' + 57: 0, # 'Ш' + 63: 0, # 'Щ' + 62: 0, # 'Ы' + 61: 0, # 'Ь' + 47: 0, # 'Э' + 59: 0, # 'Ю' + 43: 0, # 'Я' + 3: 3, # 'а' + 21: 3, # 'б' + 10: 3, # 'в' + 19: 3, # 'г' + 13: 3, # 'д' + 2: 3, # 'е' + 24: 2, # 'ж' + 20: 2, # 'з' + 4: 3, # 'и' + 23: 0, # 'й' + 11: 3, # 'к' + 8: 3, # 'л' + 12: 3, # 'м' + 5: 3, # 'н' + 1: 3, # 'о' + 15: 0, # 'п' + 9: 3, # 'Ñ€' + 7: 2, # 'Ñ' + 6: 2, # 'Ñ‚' + 14: 3, # 'у' + 39: 0, # 'Ñ„' + 26: 0, # 'Ñ…' + 28: 1, # 'ц' + 22: 2, # 'ч' + 25: 1, # 'ш' + 29: 0, # 'щ' + 54: 2, # 'ÑŠ' + 18: 3, # 'Ñ‹' + 17: 2, # 'ÑŒ' + 30: 1, # 'Ñ' + 27: 1, # 'ÑŽ' + 16: 3, # 'Ñ' + }, + 4: { # 'и' + 37: 1, # 'Ð' + 44: 0, # 'Б' + 33: 0, # 'Ð’' + 46: 0, # 'Г' + 41: 0, # 'Д' + 48: 0, # 'Е' + 56: 0, # 'Ж' + 51: 0, # 'З' + 42: 0, # 'И' + 60: 0, # 'Й' + 36: 0, # 'К' + 49: 0, # 'Л' + 38: 0, # 'Ðœ' + 31: 1, # 'Ð' + 34: 0, # 'О' + 35: 0, # 'П' + 45: 0, # 'Р' + 32: 0, # 'С' + 40: 0, # 'Т' + 52: 0, # 'У' + 53: 0, # 'Ф' + 55: 0, # 'Ð¥' + 58: 0, # 'Ц' + 50: 0, # 'Ч' + 57: 0, # 'Ш' + 63: 0, # 'Щ' + 62: 0, # 'Ы' + 61: 0, # 'Ь' + 47: 0, # 'Э' + 59: 0, # 'Ю' + 43: 0, # 'Я' + 3: 3, # 'а' + 21: 3, # 'б' + 10: 3, # 'в' + 19: 3, # 'г' + 13: 3, # 'д' + 2: 3, # 'е' + 24: 3, # 'ж' + 20: 3, # 'з' + 4: 3, # 'и' + 23: 3, # 'й' + 11: 3, # 'к' + 8: 3, # 'л' + 12: 3, # 'м' + 5: 3, # 'н' + 1: 3, # 'о' + 15: 3, # 'п' + 9: 3, # 'Ñ€' + 7: 3, # 'Ñ' + 6: 3, # 'Ñ‚' + 14: 2, # 'у' + 39: 2, # 'Ñ„' + 26: 3, # 'Ñ…' + 28: 3, # 'ц' + 22: 3, # 'ч' + 25: 3, # 'ш' + 29: 3, # 'щ' + 54: 0, # 'ÑŠ' + 18: 0, # 'Ñ‹' + 17: 0, # 'ÑŒ' + 30: 2, # 'Ñ' + 27: 3, # 'ÑŽ' + 16: 3, # 'Ñ' + }, + 23: { # 'й' + 37: 0, # 'Ð' + 44: 0, # 'Б' + 33: 0, # 'Ð’' + 46: 0, # 'Г' + 41: 0, # 'Д' + 48: 0, # 'Е' + 56: 0, # 'Ж' + 51: 0, # 'З' + 42: 0, # 'И' + 60: 0, # 'Й' + 36: 0, # 'К' + 49: 0, # 'Л' + 38: 0, # 'Ðœ' + 31: 0, # 'Ð' + 34: 0, # 'О' + 35: 0, # 'П' + 45: 0, # 'Р' + 32: 0, # 'С' + 40: 0, # 'Т' + 52: 0, # 'У' + 53: 0, # 'Ф' + 55: 0, # 'Ð¥' + 58: 0, # 'Ц' + 50: 0, # 'Ч' + 57: 0, # 'Ш' + 63: 0, # 'Щ' + 62: 0, # 'Ы' + 61: 0, # 'Ь' + 47: 0, # 'Э' + 59: 0, # 'Ю' + 43: 0, # 'Я' + 3: 1, # 'а' + 21: 1, # 'б' + 10: 1, # 'в' + 19: 2, # 'г' + 13: 3, # 'д' + 2: 2, # 'е' + 24: 0, # 'ж' + 20: 2, # 'з' + 4: 1, # 'и' + 23: 0, # 'й' + 11: 2, # 'к' + 8: 2, # 'л' + 12: 2, # 'м' + 5: 3, # 'н' + 1: 2, # 'о' + 15: 1, # 'п' + 9: 2, # 'Ñ€' + 7: 3, # 'Ñ' + 6: 3, # 'Ñ‚' + 14: 1, # 'у' + 39: 2, # 'Ñ„' + 26: 1, # 'Ñ…' + 28: 2, # 'ц' + 22: 3, # 'ч' + 25: 2, # 'ш' + 29: 1, # 'щ' + 54: 0, # 'ÑŠ' + 18: 0, # 'Ñ‹' + 17: 0, # 'ÑŒ' + 30: 1, # 'Ñ' + 27: 1, # 'ÑŽ' + 16: 2, # 'Ñ' + }, + 11: { # 'к' + 37: 0, # 'Ð' + 44: 0, # 'Б' + 33: 0, # 'Ð’' + 46: 0, # 'Г' + 41: 0, # 'Д' + 48: 0, # 'Е' + 56: 0, # 'Ж' + 51: 0, # 'З' + 42: 0, # 'И' + 60: 0, # 'Й' + 36: 0, # 'К' + 49: 0, # 'Л' + 38: 0, # 'Ðœ' + 31: 0, # 'Ð' + 34: 0, # 'О' + 35: 0, # 'П' + 45: 0, # 'Р' + 32: 0, # 'С' + 40: 0, # 'Т' + 52: 0, # 'У' + 53: 0, # 'Ф' + 55: 0, # 'Ð¥' + 58: 0, # 'Ц' + 50: 0, # 'Ч' + 57: 0, # 'Ш' + 63: 0, # 'Щ' + 62: 0, # 'Ы' + 61: 0, # 'Ь' + 47: 0, # 'Э' + 59: 0, # 'Ю' + 43: 0, # 'Я' + 3: 3, # 'а' + 21: 1, # 'б' + 10: 3, # 'в' + 19: 1, # 'г' + 13: 1, # 'д' + 2: 3, # 'е' + 24: 2, # 'ж' + 20: 2, # 'з' + 4: 3, # 'и' + 23: 0, # 'й' + 11: 2, # 'к' + 8: 3, # 'л' + 12: 1, # 'м' + 5: 3, # 'н' + 1: 3, # 'о' + 15: 0, # 'п' + 9: 3, # 'Ñ€' + 7: 3, # 'Ñ' + 6: 3, # 'Ñ‚' + 14: 3, # 'у' + 39: 1, # 'Ñ„' + 26: 2, # 'Ñ…' + 28: 2, # 'ц' + 22: 1, # 'ч' + 25: 2, # 'ш' + 29: 0, # 'щ' + 54: 0, # 'ÑŠ' + 18: 1, # 'Ñ‹' + 17: 1, # 'ÑŒ' + 30: 1, # 'Ñ' + 27: 1, # 'ÑŽ' + 16: 1, # 'Ñ' + }, + 8: { # 'л' + 37: 0, # 'Ð' + 44: 0, # 'Б' + 33: 0, # 'Ð’' + 46: 0, # 'Г' + 41: 0, # 'Д' + 48: 0, # 'Е' + 56: 0, # 'Ж' + 51: 0, # 'З' + 42: 0, # 'И' + 60: 0, # 'Й' + 36: 0, # 'К' + 49: 0, # 'Л' + 38: 0, # 'Ðœ' + 31: 0, # 'Ð' + 34: 0, # 'О' + 35: 0, # 'П' + 45: 0, # 'Р' + 32: 0, # 'С' + 40: 0, # 'Т' + 52: 0, # 'У' + 53: 0, # 'Ф' + 55: 0, # 'Ð¥' + 58: 0, # 'Ц' + 50: 0, # 'Ч' + 57: 0, # 'Ш' + 63: 0, # 'Щ' + 62: 0, # 'Ы' + 61: 0, # 'Ь' + 47: 0, # 'Э' + 59: 0, # 'Ю' + 43: 0, # 'Я' + 3: 3, # 'а' + 21: 2, # 'б' + 10: 2, # 'в' + 19: 3, # 'г' + 13: 2, # 'д' + 2: 3, # 'е' + 24: 3, # 'ж' + 20: 2, # 'з' + 4: 3, # 'и' + 23: 0, # 'й' + 11: 3, # 'к' + 8: 3, # 'л' + 12: 2, # 'м' + 5: 3, # 'н' + 1: 3, # 'о' + 15: 2, # 'п' + 9: 1, # 'Ñ€' + 7: 3, # 'Ñ' + 6: 2, # 'Ñ‚' + 14: 3, # 'у' + 39: 2, # 'Ñ„' + 26: 2, # 'Ñ…' + 28: 1, # 'ц' + 22: 3, # 'ч' + 25: 2, # 'ш' + 29: 1, # 'щ' + 54: 0, # 'ÑŠ' + 18: 3, # 'Ñ‹' + 17: 3, # 'ÑŒ' + 30: 1, # 'Ñ' + 27: 3, # 'ÑŽ' + 16: 3, # 'Ñ' + }, + 12: { # 'м' + 37: 0, # 'Ð' + 44: 0, # 'Б' + 33: 0, # 'Ð’' + 46: 0, # 'Г' + 41: 0, # 'Д' + 48: 0, # 'Е' + 56: 0, # 'Ж' + 51: 0, # 'З' + 42: 0, # 'И' + 60: 0, # 'Й' + 36: 0, # 'К' + 49: 0, # 'Л' + 38: 0, # 'Ðœ' + 31: 0, # 'Ð' + 34: 0, # 'О' + 35: 0, # 'П' + 45: 0, # 'Р' + 32: 0, # 'С' + 40: 0, # 'Т' + 52: 0, # 'У' + 53: 0, # 'Ф' + 55: 0, # 'Ð¥' + 58: 0, # 'Ц' + 50: 0, # 'Ч' + 57: 0, # 'Ш' + 63: 0, # 'Щ' + 62: 0, # 'Ы' + 61: 0, # 'Ь' + 47: 0, # 'Э' + 59: 0, # 'Ю' + 43: 0, # 'Я' + 3: 3, # 'а' + 21: 2, # 'б' + 10: 2, # 'в' + 19: 2, # 'г' + 13: 1, # 'д' + 2: 3, # 'е' + 24: 1, # 'ж' + 20: 1, # 'з' + 4: 3, # 'и' + 23: 0, # 'й' + 11: 2, # 'к' + 8: 3, # 'л' + 12: 2, # 'м' + 5: 3, # 'н' + 1: 3, # 'о' + 15: 2, # 'п' + 9: 2, # 'Ñ€' + 7: 3, # 'Ñ' + 6: 2, # 'Ñ‚' + 14: 3, # 'у' + 39: 2, # 'Ñ„' + 26: 2, # 'Ñ…' + 28: 2, # 'ц' + 22: 2, # 'ч' + 25: 1, # 'ш' + 29: 1, # 'щ' + 54: 0, # 'ÑŠ' + 18: 3, # 'Ñ‹' + 17: 2, # 'ÑŒ' + 30: 2, # 'Ñ' + 27: 1, # 'ÑŽ' + 16: 3, # 'Ñ' + }, + 5: { # 'н' + 37: 0, # 'Ð' + 44: 0, # 'Б' + 33: 0, # 'Ð’' + 46: 0, # 'Г' + 41: 0, # 'Д' + 48: 0, # 'Е' + 56: 0, # 'Ж' + 51: 0, # 'З' + 42: 0, # 'И' + 60: 0, # 'Й' + 36: 0, # 'К' + 49: 0, # 'Л' + 38: 0, # 'Ðœ' + 31: 0, # 'Ð' + 34: 0, # 'О' + 35: 0, # 'П' + 45: 0, # 'Р' + 32: 0, # 'С' + 40: 0, # 'Т' + 52: 0, # 'У' + 53: 0, # 'Ф' + 55: 0, # 'Ð¥' + 58: 0, # 'Ц' + 50: 0, # 'Ч' + 57: 0, # 'Ш' + 63: 0, # 'Щ' + 62: 0, # 'Ы' + 61: 0, # 'Ь' + 47: 0, # 'Э' + 59: 0, # 'Ю' + 43: 0, # 'Я' + 3: 3, # 'а' + 21: 2, # 'б' + 10: 2, # 'в' + 19: 3, # 'г' + 13: 3, # 'д' + 2: 3, # 'е' + 24: 2, # 'ж' + 20: 2, # 'з' + 4: 3, # 'и' + 23: 0, # 'й' + 11: 3, # 'к' + 8: 2, # 'л' + 12: 1, # 'м' + 5: 3, # 'н' + 1: 3, # 'о' + 15: 1, # 'п' + 9: 2, # 'Ñ€' + 7: 3, # 'Ñ' + 6: 3, # 'Ñ‚' + 14: 3, # 'у' + 39: 2, # 'Ñ„' + 26: 2, # 'Ñ…' + 28: 3, # 'ц' + 22: 3, # 'ч' + 25: 2, # 'ш' + 29: 2, # 'щ' + 54: 1, # 'ÑŠ' + 18: 3, # 'Ñ‹' + 17: 3, # 'ÑŒ' + 30: 1, # 'Ñ' + 27: 3, # 'ÑŽ' + 16: 3, # 'Ñ' + }, + 1: { # 'о' + 37: 0, # 'Ð' + 44: 0, # 'Б' + 33: 0, # 'Ð’' + 46: 0, # 'Г' + 41: 0, # 'Д' + 48: 0, # 'Е' + 56: 0, # 'Ж' + 51: 0, # 'З' + 42: 0, # 'И' + 60: 0, # 'Й' + 36: 0, # 'К' + 49: 0, # 'Л' + 38: 0, # 'Ðœ' + 31: 0, # 'Ð' + 34: 0, # 'О' + 35: 0, # 'П' + 45: 0, # 'Р' + 32: 0, # 'С' + 40: 0, # 'Т' + 52: 0, # 'У' + 53: 0, # 'Ф' + 55: 0, # 'Ð¥' + 58: 0, # 'Ц' + 50: 0, # 'Ч' + 57: 0, # 'Ш' + 63: 0, # 'Щ' + 62: 0, # 'Ы' + 61: 0, # 'Ь' + 47: 0, # 'Э' + 59: 0, # 'Ю' + 43: 0, # 'Я' + 3: 2, # 'а' + 21: 3, # 'б' + 10: 3, # 'в' + 19: 3, # 'г' + 13: 3, # 'д' + 2: 3, # 'е' + 24: 3, # 'ж' + 20: 3, # 'з' + 4: 3, # 'и' + 23: 3, # 'й' + 11: 3, # 'к' + 8: 3, # 'л' + 12: 3, # 'м' + 5: 3, # 'н' + 1: 3, # 'о' + 15: 3, # 'п' + 9: 3, # 'Ñ€' + 7: 3, # 'Ñ' + 6: 3, # 'Ñ‚' + 14: 2, # 'у' + 39: 2, # 'Ñ„' + 26: 3, # 'Ñ…' + 28: 2, # 'ц' + 22: 3, # 'ч' + 25: 3, # 'ш' + 29: 3, # 'щ' + 54: 0, # 'ÑŠ' + 18: 0, # 'Ñ‹' + 17: 0, # 'ÑŒ' + 30: 2, # 'Ñ' + 27: 3, # 'ÑŽ' + 16: 3, # 'Ñ' + }, + 15: { # 'п' + 37: 0, # 'Ð' + 44: 0, # 'Б' + 33: 0, # 'Ð’' + 46: 0, # 'Г' + 41: 0, # 'Д' + 48: 0, # 'Е' + 56: 0, # 'Ж' + 51: 0, # 'З' + 42: 0, # 'И' + 60: 0, # 'Й' + 36: 0, # 'К' + 49: 0, # 'Л' + 38: 0, # 'Ðœ' + 31: 0, # 'Ð' + 34: 0, # 'О' + 35: 0, # 'П' + 45: 0, # 'Р' + 32: 0, # 'С' + 40: 0, # 'Т' + 52: 0, # 'У' + 53: 0, # 'Ф' + 55: 0, # 'Ð¥' + 58: 0, # 'Ц' + 50: 0, # 'Ч' + 57: 0, # 'Ш' + 63: 0, # 'Щ' + 62: 0, # 'Ы' + 61: 0, # 'Ь' + 47: 0, # 'Э' + 59: 0, # 'Ю' + 43: 0, # 'Я' + 3: 3, # 'а' + 21: 1, # 'б' + 10: 0, # 'в' + 19: 0, # 'г' + 13: 0, # 'д' + 2: 3, # 'е' + 24: 0, # 'ж' + 20: 0, # 'з' + 4: 3, # 'и' + 23: 0, # 'й' + 11: 2, # 'к' + 8: 3, # 'л' + 12: 1, # 'м' + 5: 3, # 'н' + 1: 3, # 'о' + 15: 2, # 'п' + 9: 3, # 'Ñ€' + 7: 2, # 'Ñ' + 6: 2, # 'Ñ‚' + 14: 3, # 'у' + 39: 1, # 'Ñ„' + 26: 0, # 'Ñ…' + 28: 2, # 'ц' + 22: 2, # 'ч' + 25: 1, # 'ш' + 29: 1, # 'щ' + 54: 0, # 'ÑŠ' + 18: 3, # 'Ñ‹' + 17: 2, # 'ÑŒ' + 30: 1, # 'Ñ' + 27: 1, # 'ÑŽ' + 16: 3, # 'Ñ' + }, + 9: { # 'Ñ€' + 37: 0, # 'Ð' + 44: 0, # 'Б' + 33: 0, # 'Ð’' + 46: 0, # 'Г' + 41: 0, # 'Д' + 48: 0, # 'Е' + 56: 0, # 'Ж' + 51: 0, # 'З' + 42: 0, # 'И' + 60: 0, # 'Й' + 36: 0, # 'К' + 49: 0, # 'Л' + 38: 0, # 'Ðœ' + 31: 0, # 'Ð' + 34: 0, # 'О' + 35: 0, # 'П' + 45: 0, # 'Р' + 32: 0, # 'С' + 40: 0, # 'Т' + 52: 0, # 'У' + 53: 0, # 'Ф' + 55: 0, # 'Ð¥' + 58: 0, # 'Ц' + 50: 0, # 'Ч' + 57: 0, # 'Ш' + 63: 0, # 'Щ' + 62: 0, # 'Ы' + 61: 0, # 'Ь' + 47: 0, # 'Э' + 59: 0, # 'Ю' + 43: 0, # 'Я' + 3: 3, # 'а' + 21: 2, # 'б' + 10: 3, # 'в' + 19: 3, # 'г' + 13: 3, # 'д' + 2: 3, # 'е' + 24: 3, # 'ж' + 20: 2, # 'з' + 4: 3, # 'и' + 23: 0, # 'й' + 11: 3, # 'к' + 8: 2, # 'л' + 12: 3, # 'м' + 5: 3, # 'н' + 1: 3, # 'о' + 15: 2, # 'п' + 9: 2, # 'Ñ€' + 7: 3, # 'Ñ' + 6: 3, # 'Ñ‚' + 14: 3, # 'у' + 39: 2, # 'Ñ„' + 26: 3, # 'Ñ…' + 28: 2, # 'ц' + 22: 2, # 'ч' + 25: 3, # 'ш' + 29: 2, # 'щ' + 54: 0, # 'ÑŠ' + 18: 3, # 'Ñ‹' + 17: 3, # 'ÑŒ' + 30: 2, # 'Ñ' + 27: 2, # 'ÑŽ' + 16: 3, # 'Ñ' + }, + 7: { # 'Ñ' + 37: 0, # 'Ð' + 44: 0, # 'Б' + 33: 0, # 'Ð’' + 46: 0, # 'Г' + 41: 0, # 'Д' + 48: 0, # 'Е' + 56: 0, # 'Ж' + 51: 1, # 'З' + 42: 0, # 'И' + 60: 0, # 'Й' + 36: 0, # 'К' + 49: 0, # 'Л' + 38: 0, # 'Ðœ' + 31: 0, # 'Ð' + 34: 0, # 'О' + 35: 0, # 'П' + 45: 0, # 'Р' + 32: 0, # 'С' + 40: 0, # 'Т' + 52: 0, # 'У' + 53: 0, # 'Ф' + 55: 0, # 'Ð¥' + 58: 0, # 'Ц' + 50: 0, # 'Ч' + 57: 0, # 'Ш' + 63: 0, # 'Щ' + 62: 0, # 'Ы' + 61: 0, # 'Ь' + 47: 0, # 'Э' + 59: 0, # 'Ю' + 43: 0, # 'Я' + 3: 3, # 'а' + 21: 2, # 'б' + 10: 3, # 'в' + 19: 2, # 'г' + 13: 3, # 'д' + 2: 3, # 'е' + 24: 2, # 'ж' + 20: 2, # 'з' + 4: 3, # 'и' + 23: 0, # 'й' + 11: 3, # 'к' + 8: 3, # 'л' + 12: 3, # 'м' + 5: 3, # 'н' + 1: 3, # 'о' + 15: 3, # 'п' + 9: 3, # 'Ñ€' + 7: 3, # 'Ñ' + 6: 3, # 'Ñ‚' + 14: 3, # 'у' + 39: 2, # 'Ñ„' + 26: 3, # 'Ñ…' + 28: 2, # 'ц' + 22: 3, # 'ч' + 25: 2, # 'ш' + 29: 1, # 'щ' + 54: 2, # 'ÑŠ' + 18: 3, # 'Ñ‹' + 17: 3, # 'ÑŒ' + 30: 2, # 'Ñ' + 27: 3, # 'ÑŽ' + 16: 3, # 'Ñ' + }, + 6: { # 'Ñ‚' + 37: 0, # 'Ð' + 44: 0, # 'Б' + 33: 0, # 'Ð’' + 46: 0, # 'Г' + 41: 0, # 'Д' + 48: 0, # 'Е' + 56: 0, # 'Ж' + 51: 0, # 'З' + 42: 0, # 'И' + 60: 0, # 'Й' + 36: 0, # 'К' + 49: 0, # 'Л' + 38: 0, # 'Ðœ' + 31: 0, # 'Ð' + 34: 0, # 'О' + 35: 0, # 'П' + 45: 0, # 'Р' + 32: 0, # 'С' + 40: 0, # 'Т' + 52: 0, # 'У' + 53: 0, # 'Ф' + 55: 0, # 'Ð¥' + 58: 0, # 'Ц' + 50: 0, # 'Ч' + 57: 0, # 'Ш' + 63: 0, # 'Щ' + 62: 0, # 'Ы' + 61: 0, # 'Ь' + 47: 0, # 'Э' + 59: 0, # 'Ю' + 43: 0, # 'Я' + 3: 3, # 'а' + 21: 2, # 'б' + 10: 3, # 'в' + 19: 2, # 'г' + 13: 2, # 'д' + 2: 3, # 'е' + 24: 1, # 'ж' + 20: 1, # 'з' + 4: 3, # 'и' + 23: 0, # 'й' + 11: 3, # 'к' + 8: 3, # 'л' + 12: 2, # 'м' + 5: 3, # 'н' + 1: 3, # 'о' + 15: 2, # 'п' + 9: 3, # 'Ñ€' + 7: 3, # 'Ñ' + 6: 2, # 'Ñ‚' + 14: 3, # 'у' + 39: 2, # 'Ñ„' + 26: 2, # 'Ñ…' + 28: 2, # 'ц' + 22: 2, # 'ч' + 25: 2, # 'ш' + 29: 2, # 'щ' + 54: 2, # 'ÑŠ' + 18: 3, # 'Ñ‹' + 17: 3, # 'ÑŒ' + 30: 2, # 'Ñ' + 27: 2, # 'ÑŽ' + 16: 3, # 'Ñ' + }, + 14: { # 'у' + 37: 0, # 'Ð' + 44: 0, # 'Б' + 33: 0, # 'Ð’' + 46: 0, # 'Г' + 41: 0, # 'Д' + 48: 0, # 'Е' + 56: 0, # 'Ж' + 51: 0, # 'З' + 42: 0, # 'И' + 60: 0, # 'Й' + 36: 0, # 'К' + 49: 0, # 'Л' + 38: 0, # 'Ðœ' + 31: 0, # 'Ð' + 34: 0, # 'О' + 35: 0, # 'П' + 45: 0, # 'Р' + 32: 0, # 'С' + 40: 0, # 'Т' + 52: 0, # 'У' + 53: 0, # 'Ф' + 55: 0, # 'Ð¥' + 58: 0, # 'Ц' + 50: 0, # 'Ч' + 57: 0, # 'Ш' + 63: 0, # 'Щ' + 62: 0, # 'Ы' + 61: 0, # 'Ь' + 47: 0, # 'Э' + 59: 0, # 'Ю' + 43: 0, # 'Я' + 3: 2, # 'а' + 21: 3, # 'б' + 10: 3, # 'в' + 19: 3, # 'г' + 13: 3, # 'д' + 2: 3, # 'е' + 24: 3, # 'ж' + 20: 3, # 'з' + 4: 2, # 'и' + 23: 2, # 'й' + 11: 3, # 'к' + 8: 3, # 'л' + 12: 3, # 'м' + 5: 3, # 'н' + 1: 2, # 'о' + 15: 3, # 'п' + 9: 3, # 'Ñ€' + 7: 3, # 'Ñ' + 6: 3, # 'Ñ‚' + 14: 1, # 'у' + 39: 2, # 'Ñ„' + 26: 3, # 'Ñ…' + 28: 2, # 'ц' + 22: 3, # 'ч' + 25: 3, # 'ш' + 29: 3, # 'щ' + 54: 0, # 'ÑŠ' + 18: 0, # 'Ñ‹' + 17: 0, # 'ÑŒ' + 30: 2, # 'Ñ' + 27: 3, # 'ÑŽ' + 16: 2, # 'Ñ' + }, + 39: { # 'Ñ„' + 37: 0, # 'Ð' + 44: 0, # 'Б' + 33: 0, # 'Ð’' + 46: 0, # 'Г' + 41: 0, # 'Д' + 48: 0, # 'Е' + 56: 0, # 'Ж' + 51: 0, # 'З' + 42: 0, # 'И' + 60: 0, # 'Й' + 36: 0, # 'К' + 49: 0, # 'Л' + 38: 0, # 'Ðœ' + 31: 0, # 'Ð' + 34: 0, # 'О' + 35: 0, # 'П' + 45: 0, # 'Р' + 32: 0, # 'С' + 40: 0, # 'Т' + 52: 0, # 'У' + 53: 0, # 'Ф' + 55: 0, # 'Ð¥' + 58: 0, # 'Ц' + 50: 0, # 'Ч' + 57: 0, # 'Ш' + 63: 0, # 'Щ' + 62: 0, # 'Ы' + 61: 0, # 'Ь' + 47: 0, # 'Э' + 59: 0, # 'Ю' + 43: 0, # 'Я' + 3: 3, # 'а' + 21: 1, # 'б' + 10: 0, # 'в' + 19: 1, # 'г' + 13: 0, # 'д' + 2: 3, # 'е' + 24: 0, # 'ж' + 20: 0, # 'з' + 4: 3, # 'и' + 23: 0, # 'й' + 11: 1, # 'к' + 8: 2, # 'л' + 12: 1, # 'м' + 5: 1, # 'н' + 1: 3, # 'о' + 15: 1, # 'п' + 9: 2, # 'Ñ€' + 7: 2, # 'Ñ' + 6: 2, # 'Ñ‚' + 14: 2, # 'у' + 39: 2, # 'Ñ„' + 26: 0, # 'Ñ…' + 28: 0, # 'ц' + 22: 1, # 'ч' + 25: 1, # 'ш' + 29: 0, # 'щ' + 54: 0, # 'ÑŠ' + 18: 2, # 'Ñ‹' + 17: 1, # 'ÑŒ' + 30: 2, # 'Ñ' + 27: 1, # 'ÑŽ' + 16: 1, # 'Ñ' + }, + 26: { # 'Ñ…' + 37: 0, # 'Ð' + 44: 0, # 'Б' + 33: 0, # 'Ð’' + 46: 0, # 'Г' + 41: 0, # 'Д' + 48: 0, # 'Е' + 56: 0, # 'Ж' + 51: 0, # 'З' + 42: 0, # 'И' + 60: 0, # 'Й' + 36: 0, # 'К' + 49: 0, # 'Л' + 38: 0, # 'Ðœ' + 31: 0, # 'Ð' + 34: 0, # 'О' + 35: 0, # 'П' + 45: 0, # 'Р' + 32: 0, # 'С' + 40: 0, # 'Т' + 52: 0, # 'У' + 53: 0, # 'Ф' + 55: 0, # 'Ð¥' + 58: 0, # 'Ц' + 50: 0, # 'Ч' + 57: 0, # 'Ш' + 63: 0, # 'Щ' + 62: 0, # 'Ы' + 61: 0, # 'Ь' + 47: 0, # 'Э' + 59: 0, # 'Ю' + 43: 0, # 'Я' + 3: 3, # 'а' + 21: 0, # 'б' + 10: 3, # 'в' + 19: 1, # 'г' + 13: 1, # 'д' + 2: 2, # 'е' + 24: 0, # 'ж' + 20: 1, # 'з' + 4: 3, # 'и' + 23: 0, # 'й' + 11: 1, # 'к' + 8: 2, # 'л' + 12: 2, # 'м' + 5: 3, # 'н' + 1: 3, # 'о' + 15: 1, # 'п' + 9: 3, # 'Ñ€' + 7: 2, # 'Ñ' + 6: 2, # 'Ñ‚' + 14: 2, # 'у' + 39: 1, # 'Ñ„' + 26: 1, # 'Ñ…' + 28: 1, # 'ц' + 22: 1, # 'ч' + 25: 2, # 'ш' + 29: 0, # 'щ' + 54: 1, # 'ÑŠ' + 18: 0, # 'Ñ‹' + 17: 1, # 'ÑŒ' + 30: 1, # 'Ñ' + 27: 1, # 'ÑŽ' + 16: 0, # 'Ñ' + }, + 28: { # 'ц' + 37: 0, # 'Ð' + 44: 0, # 'Б' + 33: 0, # 'Ð’' + 46: 0, # 'Г' + 41: 0, # 'Д' + 48: 0, # 'Е' + 56: 0, # 'Ж' + 51: 0, # 'З' + 42: 0, # 'И' + 60: 0, # 'Й' + 36: 0, # 'К' + 49: 0, # 'Л' + 38: 0, # 'Ðœ' + 31: 0, # 'Ð' + 34: 0, # 'О' + 35: 0, # 'П' + 45: 0, # 'Р' + 32: 0, # 'С' + 40: 0, # 'Т' + 52: 0, # 'У' + 53: 0, # 'Ф' + 55: 0, # 'Ð¥' + 58: 0, # 'Ц' + 50: 0, # 'Ч' + 57: 0, # 'Ш' + 63: 0, # 'Щ' + 62: 0, # 'Ы' + 61: 0, # 'Ь' + 47: 0, # 'Э' + 59: 0, # 'Ю' + 43: 0, # 'Я' + 3: 3, # 'а' + 21: 1, # 'б' + 10: 2, # 'в' + 19: 1, # 'г' + 13: 1, # 'д' + 2: 3, # 'е' + 24: 0, # 'ж' + 20: 1, # 'з' + 4: 3, # 'и' + 23: 0, # 'й' + 11: 2, # 'к' + 8: 1, # 'л' + 12: 1, # 'м' + 5: 1, # 'н' + 1: 3, # 'о' + 15: 0, # 'п' + 9: 1, # 'Ñ€' + 7: 0, # 'Ñ' + 6: 1, # 'Ñ‚' + 14: 3, # 'у' + 39: 0, # 'Ñ„' + 26: 0, # 'Ñ…' + 28: 1, # 'ц' + 22: 0, # 'ч' + 25: 1, # 'ш' + 29: 0, # 'щ' + 54: 0, # 'ÑŠ' + 18: 3, # 'Ñ‹' + 17: 1, # 'ÑŒ' + 30: 0, # 'Ñ' + 27: 1, # 'ÑŽ' + 16: 0, # 'Ñ' + }, + 22: { # 'ч' + 37: 0, # 'Ð' + 44: 0, # 'Б' + 33: 0, # 'Ð’' + 46: 0, # 'Г' + 41: 0, # 'Д' + 48: 0, # 'Е' + 56: 0, # 'Ж' + 51: 0, # 'З' + 42: 0, # 'И' + 60: 0, # 'Й' + 36: 0, # 'К' + 49: 0, # 'Л' + 38: 0, # 'Ðœ' + 31: 0, # 'Ð' + 34: 0, # 'О' + 35: 0, # 'П' + 45: 0, # 'Р' + 32: 0, # 'С' + 40: 0, # 'Т' + 52: 0, # 'У' + 53: 0, # 'Ф' + 55: 0, # 'Ð¥' + 58: 0, # 'Ц' + 50: 0, # 'Ч' + 57: 0, # 'Ш' + 63: 0, # 'Щ' + 62: 0, # 'Ы' + 61: 0, # 'Ь' + 47: 0, # 'Э' + 59: 0, # 'Ю' + 43: 0, # 'Я' + 3: 3, # 'а' + 21: 1, # 'б' + 10: 1, # 'в' + 19: 0, # 'г' + 13: 0, # 'д' + 2: 3, # 'е' + 24: 1, # 'ж' + 20: 0, # 'з' + 4: 3, # 'и' + 23: 0, # 'й' + 11: 3, # 'к' + 8: 2, # 'л' + 12: 1, # 'м' + 5: 3, # 'н' + 1: 2, # 'о' + 15: 0, # 'п' + 9: 2, # 'Ñ€' + 7: 1, # 'Ñ' + 6: 3, # 'Ñ‚' + 14: 3, # 'у' + 39: 1, # 'Ñ„' + 26: 1, # 'Ñ…' + 28: 0, # 'ц' + 22: 1, # 'ч' + 25: 2, # 'ш' + 29: 0, # 'щ' + 54: 0, # 'ÑŠ' + 18: 0, # 'Ñ‹' + 17: 3, # 'ÑŒ' + 30: 0, # 'Ñ' + 27: 0, # 'ÑŽ' + 16: 0, # 'Ñ' + }, + 25: { # 'ш' + 37: 0, # 'Ð' + 44: 0, # 'Б' + 33: 0, # 'Ð’' + 46: 0, # 'Г' + 41: 0, # 'Д' + 48: 0, # 'Е' + 56: 0, # 'Ж' + 51: 0, # 'З' + 42: 0, # 'И' + 60: 0, # 'Й' + 36: 0, # 'К' + 49: 0, # 'Л' + 38: 0, # 'Ðœ' + 31: 0, # 'Ð' + 34: 0, # 'О' + 35: 0, # 'П' + 45: 0, # 'Р' + 32: 0, # 'С' + 40: 0, # 'Т' + 52: 0, # 'У' + 53: 0, # 'Ф' + 55: 0, # 'Ð¥' + 58: 0, # 'Ц' + 50: 0, # 'Ч' + 57: 0, # 'Ш' + 63: 0, # 'Щ' + 62: 0, # 'Ы' + 61: 0, # 'Ь' + 47: 0, # 'Э' + 59: 0, # 'Ю' + 43: 0, # 'Я' + 3: 3, # 'а' + 21: 1, # 'б' + 10: 2, # 'в' + 19: 1, # 'г' + 13: 0, # 'д' + 2: 3, # 'е' + 24: 0, # 'ж' + 20: 0, # 'з' + 4: 3, # 'и' + 23: 0, # 'й' + 11: 3, # 'к' + 8: 3, # 'л' + 12: 2, # 'м' + 5: 3, # 'н' + 1: 3, # 'о' + 15: 2, # 'п' + 9: 2, # 'Ñ€' + 7: 1, # 'Ñ' + 6: 2, # 'Ñ‚' + 14: 3, # 'у' + 39: 2, # 'Ñ„' + 26: 1, # 'Ñ…' + 28: 1, # 'ц' + 22: 1, # 'ч' + 25: 1, # 'ш' + 29: 0, # 'щ' + 54: 0, # 'ÑŠ' + 18: 0, # 'Ñ‹' + 17: 3, # 'ÑŒ' + 30: 1, # 'Ñ' + 27: 1, # 'ÑŽ' + 16: 0, # 'Ñ' + }, + 29: { # 'щ' + 37: 0, # 'Ð' + 44: 0, # 'Б' + 33: 0, # 'Ð’' + 46: 0, # 'Г' + 41: 0, # 'Д' + 48: 0, # 'Е' + 56: 0, # 'Ж' + 51: 0, # 'З' + 42: 0, # 'И' + 60: 0, # 'Й' + 36: 0, # 'К' + 49: 0, # 'Л' + 38: 0, # 'Ðœ' + 31: 0, # 'Ð' + 34: 0, # 'О' + 35: 0, # 'П' + 45: 0, # 'Р' + 32: 0, # 'С' + 40: 0, # 'Т' + 52: 0, # 'У' + 53: 0, # 'Ф' + 55: 0, # 'Ð¥' + 58: 0, # 'Ц' + 50: 0, # 'Ч' + 57: 0, # 'Ш' + 63: 0, # 'Щ' + 62: 0, # 'Ы' + 61: 0, # 'Ь' + 47: 0, # 'Э' + 59: 0, # 'Ю' + 43: 0, # 'Я' + 3: 3, # 'а' + 21: 0, # 'б' + 10: 1, # 'в' + 19: 0, # 'г' + 13: 0, # 'д' + 2: 3, # 'е' + 24: 0, # 'ж' + 20: 0, # 'з' + 4: 3, # 'и' + 23: 0, # 'й' + 11: 0, # 'к' + 8: 0, # 'л' + 12: 1, # 'м' + 5: 2, # 'н' + 1: 1, # 'о' + 15: 0, # 'п' + 9: 2, # 'Ñ€' + 7: 0, # 'Ñ' + 6: 0, # 'Ñ‚' + 14: 2, # 'у' + 39: 0, # 'Ñ„' + 26: 0, # 'Ñ…' + 28: 0, # 'ц' + 22: 0, # 'ч' + 25: 0, # 'ш' + 29: 0, # 'щ' + 54: 0, # 'ÑŠ' + 18: 0, # 'Ñ‹' + 17: 2, # 'ÑŒ' + 30: 0, # 'Ñ' + 27: 0, # 'ÑŽ' + 16: 0, # 'Ñ' + }, + 54: { # 'ÑŠ' + 37: 0, # 'Ð' + 44: 0, # 'Б' + 33: 0, # 'Ð’' + 46: 0, # 'Г' + 41: 0, # 'Д' + 48: 0, # 'Е' + 56: 0, # 'Ж' + 51: 0, # 'З' + 42: 0, # 'И' + 60: 0, # 'Й' + 36: 0, # 'К' + 49: 0, # 'Л' + 38: 0, # 'Ðœ' + 31: 0, # 'Ð' + 34: 0, # 'О' + 35: 0, # 'П' + 45: 0, # 'Р' + 32: 0, # 'С' + 40: 0, # 'Т' + 52: 0, # 'У' + 53: 0, # 'Ф' + 55: 0, # 'Ð¥' + 58: 0, # 'Ц' + 50: 0, # 'Ч' + 57: 0, # 'Ш' + 63: 0, # 'Щ' + 62: 0, # 'Ы' + 61: 0, # 'Ь' + 47: 0, # 'Э' + 59: 0, # 'Ю' + 43: 0, # 'Я' + 3: 0, # 'а' + 21: 0, # 'б' + 10: 0, # 'в' + 19: 0, # 'г' + 13: 0, # 'д' + 2: 2, # 'е' + 24: 0, # 'ж' + 20: 0, # 'з' + 4: 0, # 'и' + 23: 0, # 'й' + 11: 0, # 'к' + 8: 0, # 'л' + 12: 0, # 'м' + 5: 0, # 'н' + 1: 0, # 'о' + 15: 0, # 'п' + 9: 0, # 'Ñ€' + 7: 0, # 'Ñ' + 6: 0, # 'Ñ‚' + 14: 0, # 'у' + 39: 0, # 'Ñ„' + 26: 0, # 'Ñ…' + 28: 0, # 'ц' + 22: 0, # 'ч' + 25: 0, # 'ш' + 29: 0, # 'щ' + 54: 0, # 'ÑŠ' + 18: 0, # 'Ñ‹' + 17: 0, # 'ÑŒ' + 30: 0, # 'Ñ' + 27: 1, # 'ÑŽ' + 16: 2, # 'Ñ' + }, + 18: { # 'Ñ‹' + 37: 0, # 'Ð' + 44: 0, # 'Б' + 33: 0, # 'Ð’' + 46: 0, # 'Г' + 41: 0, # 'Д' + 48: 0, # 'Е' + 56: 0, # 'Ж' + 51: 0, # 'З' + 42: 0, # 'И' + 60: 0, # 'Й' + 36: 0, # 'К' + 49: 0, # 'Л' + 38: 0, # 'Ðœ' + 31: 0, # 'Ð' + 34: 0, # 'О' + 35: 0, # 'П' + 45: 0, # 'Р' + 32: 0, # 'С' + 40: 0, # 'Т' + 52: 0, # 'У' + 53: 0, # 'Ф' + 55: 0, # 'Ð¥' + 58: 0, # 'Ц' + 50: 0, # 'Ч' + 57: 0, # 'Ш' + 63: 0, # 'Щ' + 62: 0, # 'Ы' + 61: 0, # 'Ь' + 47: 0, # 'Э' + 59: 0, # 'Ю' + 43: 0, # 'Я' + 3: 0, # 'а' + 21: 3, # 'б' + 10: 3, # 'в' + 19: 2, # 'г' + 13: 2, # 'д' + 2: 3, # 'е' + 24: 2, # 'ж' + 20: 2, # 'з' + 4: 2, # 'и' + 23: 3, # 'й' + 11: 3, # 'к' + 8: 3, # 'л' + 12: 3, # 'м' + 5: 3, # 'н' + 1: 1, # 'о' + 15: 3, # 'п' + 9: 3, # 'Ñ€' + 7: 3, # 'Ñ' + 6: 3, # 'Ñ‚' + 14: 1, # 'у' + 39: 0, # 'Ñ„' + 26: 3, # 'Ñ…' + 28: 2, # 'ц' + 22: 3, # 'ч' + 25: 3, # 'ш' + 29: 2, # 'щ' + 54: 0, # 'ÑŠ' + 18: 0, # 'Ñ‹' + 17: 0, # 'ÑŒ' + 30: 0, # 'Ñ' + 27: 0, # 'ÑŽ' + 16: 2, # 'Ñ' + }, + 17: { # 'ÑŒ' + 37: 0, # 'Ð' + 44: 0, # 'Б' + 33: 0, # 'Ð’' + 46: 0, # 'Г' + 41: 0, # 'Д' + 48: 0, # 'Е' + 56: 0, # 'Ж' + 51: 0, # 'З' + 42: 0, # 'И' + 60: 0, # 'Й' + 36: 0, # 'К' + 49: 0, # 'Л' + 38: 0, # 'Ðœ' + 31: 0, # 'Ð' + 34: 0, # 'О' + 35: 0, # 'П' + 45: 0, # 'Р' + 32: 0, # 'С' + 40: 0, # 'Т' + 52: 0, # 'У' + 53: 0, # 'Ф' + 55: 0, # 'Ð¥' + 58: 0, # 'Ц' + 50: 0, # 'Ч' + 57: 0, # 'Ш' + 63: 0, # 'Щ' + 62: 0, # 'Ы' + 61: 0, # 'Ь' + 47: 0, # 'Э' + 59: 0, # 'Ю' + 43: 0, # 'Я' + 3: 0, # 'а' + 21: 2, # 'б' + 10: 2, # 'в' + 19: 2, # 'г' + 13: 2, # 'д' + 2: 3, # 'е' + 24: 1, # 'ж' + 20: 3, # 'з' + 4: 2, # 'и' + 23: 0, # 'й' + 11: 3, # 'к' + 8: 0, # 'л' + 12: 3, # 'м' + 5: 3, # 'н' + 1: 2, # 'о' + 15: 2, # 'п' + 9: 1, # 'Ñ€' + 7: 3, # 'Ñ' + 6: 2, # 'Ñ‚' + 14: 0, # 'у' + 39: 2, # 'Ñ„' + 26: 1, # 'Ñ…' + 28: 2, # 'ц' + 22: 2, # 'ч' + 25: 3, # 'ш' + 29: 2, # 'щ' + 54: 0, # 'ÑŠ' + 18: 0, # 'Ñ‹' + 17: 0, # 'ÑŒ' + 30: 1, # 'Ñ' + 27: 3, # 'ÑŽ' + 16: 3, # 'Ñ' + }, + 30: { # 'Ñ' + 37: 0, # 'Ð' + 44: 0, # 'Б' + 33: 0, # 'Ð’' + 46: 0, # 'Г' + 41: 0, # 'Д' + 48: 0, # 'Е' + 56: 0, # 'Ж' + 51: 0, # 'З' + 42: 0, # 'И' + 60: 0, # 'Й' + 36: 0, # 'К' + 49: 0, # 'Л' + 38: 1, # 'Ðœ' + 31: 1, # 'Ð' + 34: 0, # 'О' + 35: 0, # 'П' + 45: 1, # 'Р' + 32: 1, # 'С' + 40: 0, # 'Т' + 52: 0, # 'У' + 53: 1, # 'Ф' + 55: 0, # 'Ð¥' + 58: 0, # 'Ц' + 50: 0, # 'Ч' + 57: 0, # 'Ш' + 63: 0, # 'Щ' + 62: 0, # 'Ы' + 61: 0, # 'Ь' + 47: 0, # 'Э' + 59: 0, # 'Ю' + 43: 0, # 'Я' + 3: 0, # 'а' + 21: 1, # 'б' + 10: 1, # 'в' + 19: 1, # 'г' + 13: 2, # 'д' + 2: 1, # 'е' + 24: 0, # 'ж' + 20: 1, # 'з' + 4: 0, # 'и' + 23: 2, # 'й' + 11: 2, # 'к' + 8: 2, # 'л' + 12: 2, # 'м' + 5: 2, # 'н' + 1: 0, # 'о' + 15: 2, # 'п' + 9: 2, # 'Ñ€' + 7: 2, # 'Ñ' + 6: 3, # 'Ñ‚' + 14: 1, # 'у' + 39: 2, # 'Ñ„' + 26: 1, # 'Ñ…' + 28: 0, # 'ц' + 22: 0, # 'ч' + 25: 1, # 'ш' + 29: 0, # 'щ' + 54: 0, # 'ÑŠ' + 18: 0, # 'Ñ‹' + 17: 0, # 'ÑŒ' + 30: 1, # 'Ñ' + 27: 1, # 'ÑŽ' + 16: 1, # 'Ñ' + }, + 27: { # 'ÑŽ' + 37: 0, # 'Ð' + 44: 0, # 'Б' + 33: 0, # 'Ð’' + 46: 0, # 'Г' + 41: 0, # 'Д' + 48: 0, # 'Е' + 56: 0, # 'Ж' + 51: 0, # 'З' + 42: 0, # 'И' + 60: 0, # 'Й' + 36: 0, # 'К' + 49: 0, # 'Л' + 38: 0, # 'Ðœ' + 31: 0, # 'Ð' + 34: 0, # 'О' + 35: 0, # 'П' + 45: 0, # 'Р' + 32: 0, # 'С' + 40: 0, # 'Т' + 52: 0, # 'У' + 53: 0, # 'Ф' + 55: 0, # 'Ð¥' + 58: 0, # 'Ц' + 50: 0, # 'Ч' + 57: 0, # 'Ш' + 63: 0, # 'Щ' + 62: 0, # 'Ы' + 61: 0, # 'Ь' + 47: 0, # 'Э' + 59: 0, # 'Ю' + 43: 0, # 'Я' + 3: 2, # 'а' + 21: 3, # 'б' + 10: 1, # 'в' + 19: 2, # 'г' + 13: 3, # 'д' + 2: 1, # 'е' + 24: 2, # 'ж' + 20: 2, # 'з' + 4: 1, # 'и' + 23: 1, # 'й' + 11: 2, # 'к' + 8: 2, # 'л' + 12: 2, # 'м' + 5: 2, # 'н' + 1: 1, # 'о' + 15: 2, # 'п' + 9: 2, # 'Ñ€' + 7: 3, # 'Ñ' + 6: 3, # 'Ñ‚' + 14: 0, # 'у' + 39: 1, # 'Ñ„' + 26: 2, # 'Ñ…' + 28: 2, # 'ц' + 22: 2, # 'ч' + 25: 2, # 'ш' + 29: 3, # 'щ' + 54: 0, # 'ÑŠ' + 18: 0, # 'Ñ‹' + 17: 0, # 'ÑŒ' + 30: 1, # 'Ñ' + 27: 2, # 'ÑŽ' + 16: 1, # 'Ñ' + }, + 16: { # 'Ñ' + 37: 0, # 'Ð' + 44: 0, # 'Б' + 33: 0, # 'Ð’' + 46: 0, # 'Г' + 41: 0, # 'Д' + 48: 0, # 'Е' + 56: 0, # 'Ж' + 51: 0, # 'З' + 42: 0, # 'И' + 60: 0, # 'Й' + 36: 0, # 'К' + 49: 0, # 'Л' + 38: 0, # 'Ðœ' + 31: 0, # 'Ð' + 34: 0, # 'О' + 35: 0, # 'П' + 45: 0, # 'Р' + 32: 0, # 'С' + 40: 0, # 'Т' + 52: 0, # 'У' + 53: 0, # 'Ф' + 55: 0, # 'Ð¥' + 58: 0, # 'Ц' + 50: 0, # 'Ч' + 57: 0, # 'Ш' + 63: 0, # 'Щ' + 62: 0, # 'Ы' + 61: 0, # 'Ь' + 47: 0, # 'Э' + 59: 0, # 'Ю' + 43: 0, # 'Я' + 3: 0, # 'а' + 21: 2, # 'б' + 10: 3, # 'в' + 19: 2, # 'г' + 13: 3, # 'д' + 2: 3, # 'е' + 24: 3, # 'ж' + 20: 3, # 'з' + 4: 2, # 'и' + 23: 2, # 'й' + 11: 3, # 'к' + 8: 3, # 'л' + 12: 3, # 'м' + 5: 3, # 'н' + 1: 0, # 'о' + 15: 2, # 'п' + 9: 2, # 'Ñ€' + 7: 3, # 'Ñ' + 6: 3, # 'Ñ‚' + 14: 1, # 'у' + 39: 1, # 'Ñ„' + 26: 3, # 'Ñ…' + 28: 2, # 'ц' + 22: 2, # 'ч' + 25: 2, # 'ш' + 29: 3, # 'щ' + 54: 0, # 'ÑŠ' + 18: 0, # 'Ñ‹' + 17: 0, # 'ÑŒ' + 30: 0, # 'Ñ' + 27: 2, # 'ÑŽ' + 16: 2, # 'Ñ' + }, +} + +# 255: Undefined characters that did not exist in training text +# 254: Carriage/Return +# 253: symbol (punctuation) that does not belong to word +# 252: 0 - 9 +# 251: Control characters + +# Character Mapping Table(s): +IBM866_RUSSIAN_CHAR_TO_ORDER = { + 0: 255, # '\x00' + 1: 255, # '\x01' + 2: 255, # '\x02' + 3: 255, # '\x03' + 4: 255, # '\x04' + 5: 255, # '\x05' + 6: 255, # '\x06' + 7: 255, # '\x07' + 8: 255, # '\x08' + 9: 255, # '\t' + 10: 254, # '\n' + 11: 255, # '\x0b' + 12: 255, # '\x0c' + 13: 254, # '\r' + 14: 255, # '\x0e' + 15: 255, # '\x0f' + 16: 255, # '\x10' + 17: 255, # '\x11' + 18: 255, # '\x12' + 19: 255, # '\x13' + 20: 255, # '\x14' + 21: 255, # '\x15' + 22: 255, # '\x16' + 23: 255, # '\x17' + 24: 255, # '\x18' + 25: 255, # '\x19' + 26: 255, # '\x1a' + 27: 255, # '\x1b' + 28: 255, # '\x1c' + 29: 255, # '\x1d' + 30: 255, # '\x1e' + 31: 255, # '\x1f' + 32: 253, # ' ' + 33: 253, # '!' + 34: 253, # '"' + 35: 253, # '#' + 36: 253, # '$' + 37: 253, # '%' + 38: 253, # '&' + 39: 253, # "'" + 40: 253, # '(' + 41: 253, # ')' + 42: 253, # '*' + 43: 253, # '+' + 44: 253, # ',' + 45: 253, # '-' + 46: 253, # '.' + 47: 253, # '/' + 48: 252, # '0' + 49: 252, # '1' + 50: 252, # '2' + 51: 252, # '3' + 52: 252, # '4' + 53: 252, # '5' + 54: 252, # '6' + 55: 252, # '7' + 56: 252, # '8' + 57: 252, # '9' + 58: 253, # ':' + 59: 253, # ';' + 60: 253, # '<' + 61: 253, # '=' + 62: 253, # '>' + 63: 253, # '?' + 64: 253, # '@' + 65: 142, # 'A' + 66: 143, # 'B' + 67: 144, # 'C' + 68: 145, # 'D' + 69: 146, # 'E' + 70: 147, # 'F' + 71: 148, # 'G' + 72: 149, # 'H' + 73: 150, # 'I' + 74: 151, # 'J' + 75: 152, # 'K' + 76: 74, # 'L' + 77: 153, # 'M' + 78: 75, # 'N' + 79: 154, # 'O' + 80: 155, # 'P' + 81: 156, # 'Q' + 82: 157, # 'R' + 83: 158, # 'S' + 84: 159, # 'T' + 85: 160, # 'U' + 86: 161, # 'V' + 87: 162, # 'W' + 88: 163, # 'X' + 89: 164, # 'Y' + 90: 165, # 'Z' + 91: 253, # '[' + 92: 253, # '\\' + 93: 253, # ']' + 94: 253, # '^' + 95: 253, # '_' + 96: 253, # '`' + 97: 71, # 'a' + 98: 172, # 'b' + 99: 66, # 'c' + 100: 173, # 'd' + 101: 65, # 'e' + 102: 174, # 'f' + 103: 76, # 'g' + 104: 175, # 'h' + 105: 64, # 'i' + 106: 176, # 'j' + 107: 177, # 'k' + 108: 77, # 'l' + 109: 72, # 'm' + 110: 178, # 'n' + 111: 69, # 'o' + 112: 67, # 'p' + 113: 179, # 'q' + 114: 78, # 'r' + 115: 73, # 's' + 116: 180, # 't' + 117: 181, # 'u' + 118: 79, # 'v' + 119: 182, # 'w' + 120: 183, # 'x' + 121: 184, # 'y' + 122: 185, # 'z' + 123: 253, # '{' + 124: 253, # '|' + 125: 253, # '}' + 126: 253, # '~' + 127: 253, # '\x7f' + 128: 37, # 'Ð' + 129: 44, # 'Б' + 130: 33, # 'Ð’' + 131: 46, # 'Г' + 132: 41, # 'Д' + 133: 48, # 'Е' + 134: 56, # 'Ж' + 135: 51, # 'З' + 136: 42, # 'И' + 137: 60, # 'Й' + 138: 36, # 'К' + 139: 49, # 'Л' + 140: 38, # 'Ðœ' + 141: 31, # 'Ð' + 142: 34, # 'О' + 143: 35, # 'П' + 144: 45, # 'Р' + 145: 32, # 'С' + 146: 40, # 'Т' + 147: 52, # 'У' + 148: 53, # 'Ф' + 149: 55, # 'Ð¥' + 150: 58, # 'Ц' + 151: 50, # 'Ч' + 152: 57, # 'Ш' + 153: 63, # 'Щ' + 154: 70, # 'Ъ' + 155: 62, # 'Ы' + 156: 61, # 'Ь' + 157: 47, # 'Э' + 158: 59, # 'Ю' + 159: 43, # 'Я' + 160: 3, # 'а' + 161: 21, # 'б' + 162: 10, # 'в' + 163: 19, # 'г' + 164: 13, # 'д' + 165: 2, # 'е' + 166: 24, # 'ж' + 167: 20, # 'з' + 168: 4, # 'и' + 169: 23, # 'й' + 170: 11, # 'к' + 171: 8, # 'л' + 172: 12, # 'м' + 173: 5, # 'н' + 174: 1, # 'о' + 175: 15, # 'п' + 176: 191, # 'â–‘' + 177: 192, # 'â–’' + 178: 193, # 'â–“' + 179: 194, # '│' + 180: 195, # '┤' + 181: 196, # 'â•¡' + 182: 197, # 'â•¢' + 183: 198, # 'â•–' + 184: 199, # 'â••' + 185: 200, # 'â•£' + 186: 201, # 'â•‘' + 187: 202, # 'â•—' + 188: 203, # 'â•' + 189: 204, # 'â•œ' + 190: 205, # 'â•›' + 191: 206, # 'â”' + 192: 207, # 'â””' + 193: 208, # 'â”´' + 194: 209, # '┬' + 195: 210, # '├' + 196: 211, # '─' + 197: 212, # '┼' + 198: 213, # 'â•ž' + 199: 214, # 'â•Ÿ' + 200: 215, # 'â•š' + 201: 216, # 'â•”' + 202: 217, # 'â•©' + 203: 218, # '╦' + 204: 219, # 'â• ' + 205: 220, # 'â•' + 206: 221, # '╬' + 207: 222, # '╧' + 208: 223, # '╨' + 209: 224, # '╤' + 210: 225, # 'â•¥' + 211: 226, # 'â•™' + 212: 227, # '╘' + 213: 228, # 'â•’' + 214: 229, # 'â•“' + 215: 230, # 'â•«' + 216: 231, # '╪' + 217: 232, # '┘' + 218: 233, # '┌' + 219: 234, # 'â–ˆ' + 220: 235, # 'â–„' + 221: 236, # 'â–Œ' + 222: 237, # 'â–' + 223: 238, # 'â–€' + 224: 9, # 'Ñ€' + 225: 7, # 'Ñ' + 226: 6, # 'Ñ‚' + 227: 14, # 'у' + 228: 39, # 'Ñ„' + 229: 26, # 'Ñ…' + 230: 28, # 'ц' + 231: 22, # 'ч' + 232: 25, # 'ш' + 233: 29, # 'щ' + 234: 54, # 'ÑŠ' + 235: 18, # 'Ñ‹' + 236: 17, # 'ÑŒ' + 237: 30, # 'Ñ' + 238: 27, # 'ÑŽ' + 239: 16, # 'Ñ' + 240: 239, # 'Ð' + 241: 68, # 'Ñ‘' + 242: 240, # 'Є' + 243: 241, # 'Ñ”' + 244: 242, # 'Ї' + 245: 243, # 'Ñ—' + 246: 244, # 'ÐŽ' + 247: 245, # 'Ñž' + 248: 246, # '°' + 249: 247, # '∙' + 250: 248, # '·' + 251: 249, # '√' + 252: 250, # 'â„–' + 253: 251, # '¤' + 254: 252, # 'â– ' + 255: 255, # '\xa0' +} + +IBM866_RUSSIAN_MODEL = SingleByteCharSetModel(charset_name='IBM866', + language='Russian', + char_to_order_map=IBM866_RUSSIAN_CHAR_TO_ORDER, + language_model=RUSSIAN_LANG_MODEL, + typical_positive_ratio=0.976601, + keep_ascii_letters=False, + alphabet='ÐÐБВГДЕЖЗИЙКЛМÐОПРСТУФХЦЧШЩЪЫЬЭЮЯабвгдежзийклмнопрÑтуфхцчшщъыьÑÑŽÑÑ‘') + +WINDOWS_1251_RUSSIAN_CHAR_TO_ORDER = { + 0: 255, # '\x00' + 1: 255, # '\x01' + 2: 255, # '\x02' + 3: 255, # '\x03' + 4: 255, # '\x04' + 5: 255, # '\x05' + 6: 255, # '\x06' + 7: 255, # '\x07' + 8: 255, # '\x08' + 9: 255, # '\t' + 10: 254, # '\n' + 11: 255, # '\x0b' + 12: 255, # '\x0c' + 13: 254, # '\r' + 14: 255, # '\x0e' + 15: 255, # '\x0f' + 16: 255, # '\x10' + 17: 255, # '\x11' + 18: 255, # '\x12' + 19: 255, # '\x13' + 20: 255, # '\x14' + 21: 255, # '\x15' + 22: 255, # '\x16' + 23: 255, # '\x17' + 24: 255, # '\x18' + 25: 255, # '\x19' + 26: 255, # '\x1a' + 27: 255, # '\x1b' + 28: 255, # '\x1c' + 29: 255, # '\x1d' + 30: 255, # '\x1e' + 31: 255, # '\x1f' + 32: 253, # ' ' + 33: 253, # '!' + 34: 253, # '"' + 35: 253, # '#' + 36: 253, # '$' + 37: 253, # '%' + 38: 253, # '&' + 39: 253, # "'" + 40: 253, # '(' + 41: 253, # ')' + 42: 253, # '*' + 43: 253, # '+' + 44: 253, # ',' + 45: 253, # '-' + 46: 253, # '.' + 47: 253, # '/' + 48: 252, # '0' + 49: 252, # '1' + 50: 252, # '2' + 51: 252, # '3' + 52: 252, # '4' + 53: 252, # '5' + 54: 252, # '6' + 55: 252, # '7' + 56: 252, # '8' + 57: 252, # '9' + 58: 253, # ':' + 59: 253, # ';' + 60: 253, # '<' + 61: 253, # '=' + 62: 253, # '>' + 63: 253, # '?' + 64: 253, # '@' + 65: 142, # 'A' + 66: 143, # 'B' + 67: 144, # 'C' + 68: 145, # 'D' + 69: 146, # 'E' + 70: 147, # 'F' + 71: 148, # 'G' + 72: 149, # 'H' + 73: 150, # 'I' + 74: 151, # 'J' + 75: 152, # 'K' + 76: 74, # 'L' + 77: 153, # 'M' + 78: 75, # 'N' + 79: 154, # 'O' + 80: 155, # 'P' + 81: 156, # 'Q' + 82: 157, # 'R' + 83: 158, # 'S' + 84: 159, # 'T' + 85: 160, # 'U' + 86: 161, # 'V' + 87: 162, # 'W' + 88: 163, # 'X' + 89: 164, # 'Y' + 90: 165, # 'Z' + 91: 253, # '[' + 92: 253, # '\\' + 93: 253, # ']' + 94: 253, # '^' + 95: 253, # '_' + 96: 253, # '`' + 97: 71, # 'a' + 98: 172, # 'b' + 99: 66, # 'c' + 100: 173, # 'd' + 101: 65, # 'e' + 102: 174, # 'f' + 103: 76, # 'g' + 104: 175, # 'h' + 105: 64, # 'i' + 106: 176, # 'j' + 107: 177, # 'k' + 108: 77, # 'l' + 109: 72, # 'm' + 110: 178, # 'n' + 111: 69, # 'o' + 112: 67, # 'p' + 113: 179, # 'q' + 114: 78, # 'r' + 115: 73, # 's' + 116: 180, # 't' + 117: 181, # 'u' + 118: 79, # 'v' + 119: 182, # 'w' + 120: 183, # 'x' + 121: 184, # 'y' + 122: 185, # 'z' + 123: 253, # '{' + 124: 253, # '|' + 125: 253, # '}' + 126: 253, # '~' + 127: 253, # '\x7f' + 128: 191, # 'Ђ' + 129: 192, # 'Ѓ' + 130: 193, # '‚' + 131: 194, # 'Ñ“' + 132: 195, # '„' + 133: 196, # '…' + 134: 197, # '†' + 135: 198, # '‡' + 136: 199, # '€' + 137: 200, # '‰' + 138: 201, # 'Љ' + 139: 202, # '‹' + 140: 203, # 'Њ' + 141: 204, # 'ÐŒ' + 142: 205, # 'Ћ' + 143: 206, # 'Ð' + 144: 207, # 'Ñ’' + 145: 208, # '‘' + 146: 209, # '’' + 147: 210, # '“' + 148: 211, # 'â€' + 149: 212, # '•' + 150: 213, # '–' + 151: 214, # '—' + 152: 215, # None + 153: 216, # 'â„¢' + 154: 217, # 'Ñ™' + 155: 218, # '›' + 156: 219, # 'Ñš' + 157: 220, # 'Ñœ' + 158: 221, # 'Ñ›' + 159: 222, # 'ÑŸ' + 160: 223, # '\xa0' + 161: 224, # 'ÐŽ' + 162: 225, # 'Ñž' + 163: 226, # 'Ј' + 164: 227, # '¤' + 165: 228, # 'Ò' + 166: 229, # '¦' + 167: 230, # '§' + 168: 231, # 'Ð' + 169: 232, # '©' + 170: 233, # 'Є' + 171: 234, # '«' + 172: 235, # '¬' + 173: 236, # '\xad' + 174: 237, # '®' + 175: 238, # 'Ї' + 176: 239, # '°' + 177: 240, # '±' + 178: 241, # 'І' + 179: 242, # 'Ñ–' + 180: 243, # 'Ò‘' + 181: 244, # 'µ' + 182: 245, # '¶' + 183: 246, # '·' + 184: 68, # 'Ñ‘' + 185: 247, # 'â„–' + 186: 248, # 'Ñ”' + 187: 249, # '»' + 188: 250, # 'ј' + 189: 251, # 'Ð…' + 190: 252, # 'Ñ•' + 191: 253, # 'Ñ—' + 192: 37, # 'Ð' + 193: 44, # 'Б' + 194: 33, # 'Ð’' + 195: 46, # 'Г' + 196: 41, # 'Д' + 197: 48, # 'Е' + 198: 56, # 'Ж' + 199: 51, # 'З' + 200: 42, # 'И' + 201: 60, # 'Й' + 202: 36, # 'К' + 203: 49, # 'Л' + 204: 38, # 'Ðœ' + 205: 31, # 'Ð' + 206: 34, # 'О' + 207: 35, # 'П' + 208: 45, # 'Р' + 209: 32, # 'С' + 210: 40, # 'Т' + 211: 52, # 'У' + 212: 53, # 'Ф' + 213: 55, # 'Ð¥' + 214: 58, # 'Ц' + 215: 50, # 'Ч' + 216: 57, # 'Ш' + 217: 63, # 'Щ' + 218: 70, # 'Ъ' + 219: 62, # 'Ы' + 220: 61, # 'Ь' + 221: 47, # 'Э' + 222: 59, # 'Ю' + 223: 43, # 'Я' + 224: 3, # 'а' + 225: 21, # 'б' + 226: 10, # 'в' + 227: 19, # 'г' + 228: 13, # 'д' + 229: 2, # 'е' + 230: 24, # 'ж' + 231: 20, # 'з' + 232: 4, # 'и' + 233: 23, # 'й' + 234: 11, # 'к' + 235: 8, # 'л' + 236: 12, # 'м' + 237: 5, # 'н' + 238: 1, # 'о' + 239: 15, # 'п' + 240: 9, # 'Ñ€' + 241: 7, # 'Ñ' + 242: 6, # 'Ñ‚' + 243: 14, # 'у' + 244: 39, # 'Ñ„' + 245: 26, # 'Ñ…' + 246: 28, # 'ц' + 247: 22, # 'ч' + 248: 25, # 'ш' + 249: 29, # 'щ' + 250: 54, # 'ÑŠ' + 251: 18, # 'Ñ‹' + 252: 17, # 'ÑŒ' + 253: 30, # 'Ñ' + 254: 27, # 'ÑŽ' + 255: 16, # 'Ñ' +} + +WINDOWS_1251_RUSSIAN_MODEL = SingleByteCharSetModel(charset_name='windows-1251', + language='Russian', + char_to_order_map=WINDOWS_1251_RUSSIAN_CHAR_TO_ORDER, + language_model=RUSSIAN_LANG_MODEL, + typical_positive_ratio=0.976601, + keep_ascii_letters=False, + alphabet='ÐÐБВГДЕЖЗИЙКЛМÐОПРСТУФХЦЧШЩЪЫЬЭЮЯабвгдежзийклмнопрÑтуфхцчшщъыьÑÑŽÑÑ‘') + +IBM855_RUSSIAN_CHAR_TO_ORDER = { + 0: 255, # '\x00' + 1: 255, # '\x01' + 2: 255, # '\x02' + 3: 255, # '\x03' + 4: 255, # '\x04' + 5: 255, # '\x05' + 6: 255, # '\x06' + 7: 255, # '\x07' + 8: 255, # '\x08' + 9: 255, # '\t' + 10: 254, # '\n' + 11: 255, # '\x0b' + 12: 255, # '\x0c' + 13: 254, # '\r' + 14: 255, # '\x0e' + 15: 255, # '\x0f' + 16: 255, # '\x10' + 17: 255, # '\x11' + 18: 255, # '\x12' + 19: 255, # '\x13' + 20: 255, # '\x14' + 21: 255, # '\x15' + 22: 255, # '\x16' + 23: 255, # '\x17' + 24: 255, # '\x18' + 25: 255, # '\x19' + 26: 255, # '\x1a' + 27: 255, # '\x1b' + 28: 255, # '\x1c' + 29: 255, # '\x1d' + 30: 255, # '\x1e' + 31: 255, # '\x1f' + 32: 253, # ' ' + 33: 253, # '!' + 34: 253, # '"' + 35: 253, # '#' + 36: 253, # '$' + 37: 253, # '%' + 38: 253, # '&' + 39: 253, # "'" + 40: 253, # '(' + 41: 253, # ')' + 42: 253, # '*' + 43: 253, # '+' + 44: 253, # ',' + 45: 253, # '-' + 46: 253, # '.' + 47: 253, # '/' + 48: 252, # '0' + 49: 252, # '1' + 50: 252, # '2' + 51: 252, # '3' + 52: 252, # '4' + 53: 252, # '5' + 54: 252, # '6' + 55: 252, # '7' + 56: 252, # '8' + 57: 252, # '9' + 58: 253, # ':' + 59: 253, # ';' + 60: 253, # '<' + 61: 253, # '=' + 62: 253, # '>' + 63: 253, # '?' + 64: 253, # '@' + 65: 142, # 'A' + 66: 143, # 'B' + 67: 144, # 'C' + 68: 145, # 'D' + 69: 146, # 'E' + 70: 147, # 'F' + 71: 148, # 'G' + 72: 149, # 'H' + 73: 150, # 'I' + 74: 151, # 'J' + 75: 152, # 'K' + 76: 74, # 'L' + 77: 153, # 'M' + 78: 75, # 'N' + 79: 154, # 'O' + 80: 155, # 'P' + 81: 156, # 'Q' + 82: 157, # 'R' + 83: 158, # 'S' + 84: 159, # 'T' + 85: 160, # 'U' + 86: 161, # 'V' + 87: 162, # 'W' + 88: 163, # 'X' + 89: 164, # 'Y' + 90: 165, # 'Z' + 91: 253, # '[' + 92: 253, # '\\' + 93: 253, # ']' + 94: 253, # '^' + 95: 253, # '_' + 96: 253, # '`' + 97: 71, # 'a' + 98: 172, # 'b' + 99: 66, # 'c' + 100: 173, # 'd' + 101: 65, # 'e' + 102: 174, # 'f' + 103: 76, # 'g' + 104: 175, # 'h' + 105: 64, # 'i' + 106: 176, # 'j' + 107: 177, # 'k' + 108: 77, # 'l' + 109: 72, # 'm' + 110: 178, # 'n' + 111: 69, # 'o' + 112: 67, # 'p' + 113: 179, # 'q' + 114: 78, # 'r' + 115: 73, # 's' + 116: 180, # 't' + 117: 181, # 'u' + 118: 79, # 'v' + 119: 182, # 'w' + 120: 183, # 'x' + 121: 184, # 'y' + 122: 185, # 'z' + 123: 253, # '{' + 124: 253, # '|' + 125: 253, # '}' + 126: 253, # '~' + 127: 253, # '\x7f' + 128: 191, # 'Ñ’' + 129: 192, # 'Ђ' + 130: 193, # 'Ñ“' + 131: 194, # 'Ѓ' + 132: 68, # 'Ñ‘' + 133: 195, # 'Ð' + 134: 196, # 'Ñ”' + 135: 197, # 'Є' + 136: 198, # 'Ñ•' + 137: 199, # 'Ð…' + 138: 200, # 'Ñ–' + 139: 201, # 'І' + 140: 202, # 'Ñ—' + 141: 203, # 'Ї' + 142: 204, # 'ј' + 143: 205, # 'Ј' + 144: 206, # 'Ñ™' + 145: 207, # 'Љ' + 146: 208, # 'Ñš' + 147: 209, # 'Њ' + 148: 210, # 'Ñ›' + 149: 211, # 'Ћ' + 150: 212, # 'Ñœ' + 151: 213, # 'ÐŒ' + 152: 214, # 'Ñž' + 153: 215, # 'ÐŽ' + 154: 216, # 'ÑŸ' + 155: 217, # 'Ð' + 156: 27, # 'ÑŽ' + 157: 59, # 'Ю' + 158: 54, # 'ÑŠ' + 159: 70, # 'Ъ' + 160: 3, # 'а' + 161: 37, # 'Ð' + 162: 21, # 'б' + 163: 44, # 'Б' + 164: 28, # 'ц' + 165: 58, # 'Ц' + 166: 13, # 'д' + 167: 41, # 'Д' + 168: 2, # 'е' + 169: 48, # 'Е' + 170: 39, # 'Ñ„' + 171: 53, # 'Ф' + 172: 19, # 'г' + 173: 46, # 'Г' + 174: 218, # '«' + 175: 219, # '»' + 176: 220, # 'â–‘' + 177: 221, # 'â–’' + 178: 222, # 'â–“' + 179: 223, # '│' + 180: 224, # '┤' + 181: 26, # 'Ñ…' + 182: 55, # 'Ð¥' + 183: 4, # 'и' + 184: 42, # 'И' + 185: 225, # 'â•£' + 186: 226, # 'â•‘' + 187: 227, # 'â•—' + 188: 228, # 'â•' + 189: 23, # 'й' + 190: 60, # 'Й' + 191: 229, # 'â”' + 192: 230, # 'â””' + 193: 231, # 'â”´' + 194: 232, # '┬' + 195: 233, # '├' + 196: 234, # '─' + 197: 235, # '┼' + 198: 11, # 'к' + 199: 36, # 'К' + 200: 236, # 'â•š' + 201: 237, # 'â•”' + 202: 238, # 'â•©' + 203: 239, # '╦' + 204: 240, # 'â• ' + 205: 241, # 'â•' + 206: 242, # '╬' + 207: 243, # '¤' + 208: 8, # 'л' + 209: 49, # 'Л' + 210: 12, # 'м' + 211: 38, # 'Ðœ' + 212: 5, # 'н' + 213: 31, # 'Ð' + 214: 1, # 'о' + 215: 34, # 'О' + 216: 15, # 'п' + 217: 244, # '┘' + 218: 245, # '┌' + 219: 246, # 'â–ˆ' + 220: 247, # 'â–„' + 221: 35, # 'П' + 222: 16, # 'Ñ' + 223: 248, # 'â–€' + 224: 43, # 'Я' + 225: 9, # 'Ñ€' + 226: 45, # 'Р' + 227: 7, # 'Ñ' + 228: 32, # 'С' + 229: 6, # 'Ñ‚' + 230: 40, # 'Т' + 231: 14, # 'у' + 232: 52, # 'У' + 233: 24, # 'ж' + 234: 56, # 'Ж' + 235: 10, # 'в' + 236: 33, # 'Ð’' + 237: 17, # 'ÑŒ' + 238: 61, # 'Ь' + 239: 249, # 'â„–' + 240: 250, # '\xad' + 241: 18, # 'Ñ‹' + 242: 62, # 'Ы' + 243: 20, # 'з' + 244: 51, # 'З' + 245: 25, # 'ш' + 246: 57, # 'Ш' + 247: 30, # 'Ñ' + 248: 47, # 'Э' + 249: 29, # 'щ' + 250: 63, # 'Щ' + 251: 22, # 'ч' + 252: 50, # 'Ч' + 253: 251, # '§' + 254: 252, # 'â– ' + 255: 255, # '\xa0' +} + +IBM855_RUSSIAN_MODEL = SingleByteCharSetModel(charset_name='IBM855', + language='Russian', + char_to_order_map=IBM855_RUSSIAN_CHAR_TO_ORDER, + language_model=RUSSIAN_LANG_MODEL, + typical_positive_ratio=0.976601, + keep_ascii_letters=False, + alphabet='ÐÐБВГДЕЖЗИЙКЛМÐОПРСТУФХЦЧШЩЪЫЬЭЮЯабвгдежзийклмнопрÑтуфхцчшщъыьÑÑŽÑÑ‘') + +KOI8_R_RUSSIAN_CHAR_TO_ORDER = { + 0: 255, # '\x00' + 1: 255, # '\x01' + 2: 255, # '\x02' + 3: 255, # '\x03' + 4: 255, # '\x04' + 5: 255, # '\x05' + 6: 255, # '\x06' + 7: 255, # '\x07' + 8: 255, # '\x08' + 9: 255, # '\t' + 10: 254, # '\n' + 11: 255, # '\x0b' + 12: 255, # '\x0c' + 13: 254, # '\r' + 14: 255, # '\x0e' + 15: 255, # '\x0f' + 16: 255, # '\x10' + 17: 255, # '\x11' + 18: 255, # '\x12' + 19: 255, # '\x13' + 20: 255, # '\x14' + 21: 255, # '\x15' + 22: 255, # '\x16' + 23: 255, # '\x17' + 24: 255, # '\x18' + 25: 255, # '\x19' + 26: 255, # '\x1a' + 27: 255, # '\x1b' + 28: 255, # '\x1c' + 29: 255, # '\x1d' + 30: 255, # '\x1e' + 31: 255, # '\x1f' + 32: 253, # ' ' + 33: 253, # '!' + 34: 253, # '"' + 35: 253, # '#' + 36: 253, # '$' + 37: 253, # '%' + 38: 253, # '&' + 39: 253, # "'" + 40: 253, # '(' + 41: 253, # ')' + 42: 253, # '*' + 43: 253, # '+' + 44: 253, # ',' + 45: 253, # '-' + 46: 253, # '.' + 47: 253, # '/' + 48: 252, # '0' + 49: 252, # '1' + 50: 252, # '2' + 51: 252, # '3' + 52: 252, # '4' + 53: 252, # '5' + 54: 252, # '6' + 55: 252, # '7' + 56: 252, # '8' + 57: 252, # '9' + 58: 253, # ':' + 59: 253, # ';' + 60: 253, # '<' + 61: 253, # '=' + 62: 253, # '>' + 63: 253, # '?' + 64: 253, # '@' + 65: 142, # 'A' + 66: 143, # 'B' + 67: 144, # 'C' + 68: 145, # 'D' + 69: 146, # 'E' + 70: 147, # 'F' + 71: 148, # 'G' + 72: 149, # 'H' + 73: 150, # 'I' + 74: 151, # 'J' + 75: 152, # 'K' + 76: 74, # 'L' + 77: 153, # 'M' + 78: 75, # 'N' + 79: 154, # 'O' + 80: 155, # 'P' + 81: 156, # 'Q' + 82: 157, # 'R' + 83: 158, # 'S' + 84: 159, # 'T' + 85: 160, # 'U' + 86: 161, # 'V' + 87: 162, # 'W' + 88: 163, # 'X' + 89: 164, # 'Y' + 90: 165, # 'Z' + 91: 253, # '[' + 92: 253, # '\\' + 93: 253, # ']' + 94: 253, # '^' + 95: 253, # '_' + 96: 253, # '`' + 97: 71, # 'a' + 98: 172, # 'b' + 99: 66, # 'c' + 100: 173, # 'd' + 101: 65, # 'e' + 102: 174, # 'f' + 103: 76, # 'g' + 104: 175, # 'h' + 105: 64, # 'i' + 106: 176, # 'j' + 107: 177, # 'k' + 108: 77, # 'l' + 109: 72, # 'm' + 110: 178, # 'n' + 111: 69, # 'o' + 112: 67, # 'p' + 113: 179, # 'q' + 114: 78, # 'r' + 115: 73, # 's' + 116: 180, # 't' + 117: 181, # 'u' + 118: 79, # 'v' + 119: 182, # 'w' + 120: 183, # 'x' + 121: 184, # 'y' + 122: 185, # 'z' + 123: 253, # '{' + 124: 253, # '|' + 125: 253, # '}' + 126: 253, # '~' + 127: 253, # '\x7f' + 128: 191, # '─' + 129: 192, # '│' + 130: 193, # '┌' + 131: 194, # 'â”' + 132: 195, # 'â””' + 133: 196, # '┘' + 134: 197, # '├' + 135: 198, # '┤' + 136: 199, # '┬' + 137: 200, # 'â”´' + 138: 201, # '┼' + 139: 202, # 'â–€' + 140: 203, # 'â–„' + 141: 204, # 'â–ˆ' + 142: 205, # 'â–Œ' + 143: 206, # 'â–' + 144: 207, # 'â–‘' + 145: 208, # 'â–’' + 146: 209, # 'â–“' + 147: 210, # '⌠' + 148: 211, # 'â– ' + 149: 212, # '∙' + 150: 213, # '√' + 151: 214, # '≈' + 152: 215, # '≤' + 153: 216, # '≥' + 154: 217, # '\xa0' + 155: 218, # '⌡' + 156: 219, # '°' + 157: 220, # '²' + 158: 221, # '·' + 159: 222, # '÷' + 160: 223, # 'â•' + 161: 224, # 'â•‘' + 162: 225, # 'â•’' + 163: 68, # 'Ñ‘' + 164: 226, # 'â•“' + 165: 227, # 'â•”' + 166: 228, # 'â••' + 167: 229, # 'â•–' + 168: 230, # 'â•—' + 169: 231, # '╘' + 170: 232, # 'â•™' + 171: 233, # 'â•š' + 172: 234, # 'â•›' + 173: 235, # 'â•œ' + 174: 236, # 'â•' + 175: 237, # 'â•ž' + 176: 238, # 'â•Ÿ' + 177: 239, # 'â• ' + 178: 240, # 'â•¡' + 179: 241, # 'Ð' + 180: 242, # 'â•¢' + 181: 243, # 'â•£' + 182: 244, # '╤' + 183: 245, # 'â•¥' + 184: 246, # '╦' + 185: 247, # '╧' + 186: 248, # '╨' + 187: 249, # 'â•©' + 188: 250, # '╪' + 189: 251, # 'â•«' + 190: 252, # '╬' + 191: 253, # '©' + 192: 27, # 'ÑŽ' + 193: 3, # 'а' + 194: 21, # 'б' + 195: 28, # 'ц' + 196: 13, # 'д' + 197: 2, # 'е' + 198: 39, # 'Ñ„' + 199: 19, # 'г' + 200: 26, # 'Ñ…' + 201: 4, # 'и' + 202: 23, # 'й' + 203: 11, # 'к' + 204: 8, # 'л' + 205: 12, # 'м' + 206: 5, # 'н' + 207: 1, # 'о' + 208: 15, # 'п' + 209: 16, # 'Ñ' + 210: 9, # 'Ñ€' + 211: 7, # 'Ñ' + 212: 6, # 'Ñ‚' + 213: 14, # 'у' + 214: 24, # 'ж' + 215: 10, # 'в' + 216: 17, # 'ÑŒ' + 217: 18, # 'Ñ‹' + 218: 20, # 'з' + 219: 25, # 'ш' + 220: 30, # 'Ñ' + 221: 29, # 'щ' + 222: 22, # 'ч' + 223: 54, # 'ÑŠ' + 224: 59, # 'Ю' + 225: 37, # 'Ð' + 226: 44, # 'Б' + 227: 58, # 'Ц' + 228: 41, # 'Д' + 229: 48, # 'Е' + 230: 53, # 'Ф' + 231: 46, # 'Г' + 232: 55, # 'Ð¥' + 233: 42, # 'И' + 234: 60, # 'Й' + 235: 36, # 'К' + 236: 49, # 'Л' + 237: 38, # 'Ðœ' + 238: 31, # 'Ð' + 239: 34, # 'О' + 240: 35, # 'П' + 241: 43, # 'Я' + 242: 45, # 'Р' + 243: 32, # 'С' + 244: 40, # 'Т' + 245: 52, # 'У' + 246: 56, # 'Ж' + 247: 33, # 'Ð’' + 248: 61, # 'Ь' + 249: 62, # 'Ы' + 250: 51, # 'З' + 251: 57, # 'Ш' + 252: 47, # 'Э' + 253: 63, # 'Щ' + 254: 50, # 'Ч' + 255: 70, # 'Ъ' +} + +KOI8_R_RUSSIAN_MODEL = SingleByteCharSetModel(charset_name='KOI8-R', + language='Russian', + char_to_order_map=KOI8_R_RUSSIAN_CHAR_TO_ORDER, + language_model=RUSSIAN_LANG_MODEL, + typical_positive_ratio=0.976601, + keep_ascii_letters=False, + alphabet='ÐÐБВГДЕЖЗИЙКЛМÐОПРСТУФХЦЧШЩЪЫЬЭЮЯабвгдежзийклмнопрÑтуфхцчшщъыьÑÑŽÑÑ‘') + +MACCYRILLIC_RUSSIAN_CHAR_TO_ORDER = { + 0: 255, # '\x00' + 1: 255, # '\x01' + 2: 255, # '\x02' + 3: 255, # '\x03' + 4: 255, # '\x04' + 5: 255, # '\x05' + 6: 255, # '\x06' + 7: 255, # '\x07' + 8: 255, # '\x08' + 9: 255, # '\t' + 10: 254, # '\n' + 11: 255, # '\x0b' + 12: 255, # '\x0c' + 13: 254, # '\r' + 14: 255, # '\x0e' + 15: 255, # '\x0f' + 16: 255, # '\x10' + 17: 255, # '\x11' + 18: 255, # '\x12' + 19: 255, # '\x13' + 20: 255, # '\x14' + 21: 255, # '\x15' + 22: 255, # '\x16' + 23: 255, # '\x17' + 24: 255, # '\x18' + 25: 255, # '\x19' + 26: 255, # '\x1a' + 27: 255, # '\x1b' + 28: 255, # '\x1c' + 29: 255, # '\x1d' + 30: 255, # '\x1e' + 31: 255, # '\x1f' + 32: 253, # ' ' + 33: 253, # '!' + 34: 253, # '"' + 35: 253, # '#' + 36: 253, # '$' + 37: 253, # '%' + 38: 253, # '&' + 39: 253, # "'" + 40: 253, # '(' + 41: 253, # ')' + 42: 253, # '*' + 43: 253, # '+' + 44: 253, # ',' + 45: 253, # '-' + 46: 253, # '.' + 47: 253, # '/' + 48: 252, # '0' + 49: 252, # '1' + 50: 252, # '2' + 51: 252, # '3' + 52: 252, # '4' + 53: 252, # '5' + 54: 252, # '6' + 55: 252, # '7' + 56: 252, # '8' + 57: 252, # '9' + 58: 253, # ':' + 59: 253, # ';' + 60: 253, # '<' + 61: 253, # '=' + 62: 253, # '>' + 63: 253, # '?' + 64: 253, # '@' + 65: 142, # 'A' + 66: 143, # 'B' + 67: 144, # 'C' + 68: 145, # 'D' + 69: 146, # 'E' + 70: 147, # 'F' + 71: 148, # 'G' + 72: 149, # 'H' + 73: 150, # 'I' + 74: 151, # 'J' + 75: 152, # 'K' + 76: 74, # 'L' + 77: 153, # 'M' + 78: 75, # 'N' + 79: 154, # 'O' + 80: 155, # 'P' + 81: 156, # 'Q' + 82: 157, # 'R' + 83: 158, # 'S' + 84: 159, # 'T' + 85: 160, # 'U' + 86: 161, # 'V' + 87: 162, # 'W' + 88: 163, # 'X' + 89: 164, # 'Y' + 90: 165, # 'Z' + 91: 253, # '[' + 92: 253, # '\\' + 93: 253, # ']' + 94: 253, # '^' + 95: 253, # '_' + 96: 253, # '`' + 97: 71, # 'a' + 98: 172, # 'b' + 99: 66, # 'c' + 100: 173, # 'd' + 101: 65, # 'e' + 102: 174, # 'f' + 103: 76, # 'g' + 104: 175, # 'h' + 105: 64, # 'i' + 106: 176, # 'j' + 107: 177, # 'k' + 108: 77, # 'l' + 109: 72, # 'm' + 110: 178, # 'n' + 111: 69, # 'o' + 112: 67, # 'p' + 113: 179, # 'q' + 114: 78, # 'r' + 115: 73, # 's' + 116: 180, # 't' + 117: 181, # 'u' + 118: 79, # 'v' + 119: 182, # 'w' + 120: 183, # 'x' + 121: 184, # 'y' + 122: 185, # 'z' + 123: 253, # '{' + 124: 253, # '|' + 125: 253, # '}' + 126: 253, # '~' + 127: 253, # '\x7f' + 128: 37, # 'Ð' + 129: 44, # 'Б' + 130: 33, # 'Ð’' + 131: 46, # 'Г' + 132: 41, # 'Д' + 133: 48, # 'Е' + 134: 56, # 'Ж' + 135: 51, # 'З' + 136: 42, # 'И' + 137: 60, # 'Й' + 138: 36, # 'К' + 139: 49, # 'Л' + 140: 38, # 'Ðœ' + 141: 31, # 'Ð' + 142: 34, # 'О' + 143: 35, # 'П' + 144: 45, # 'Р' + 145: 32, # 'С' + 146: 40, # 'Т' + 147: 52, # 'У' + 148: 53, # 'Ф' + 149: 55, # 'Ð¥' + 150: 58, # 'Ц' + 151: 50, # 'Ч' + 152: 57, # 'Ш' + 153: 63, # 'Щ' + 154: 70, # 'Ъ' + 155: 62, # 'Ы' + 156: 61, # 'Ь' + 157: 47, # 'Э' + 158: 59, # 'Ю' + 159: 43, # 'Я' + 160: 191, # '†' + 161: 192, # '°' + 162: 193, # 'Ò' + 163: 194, # '£' + 164: 195, # '§' + 165: 196, # '•' + 166: 197, # '¶' + 167: 198, # 'І' + 168: 199, # '®' + 169: 200, # '©' + 170: 201, # 'â„¢' + 171: 202, # 'Ђ' + 172: 203, # 'Ñ’' + 173: 204, # '≠' + 174: 205, # 'Ѓ' + 175: 206, # 'Ñ“' + 176: 207, # '∞' + 177: 208, # '±' + 178: 209, # '≤' + 179: 210, # '≥' + 180: 211, # 'Ñ–' + 181: 212, # 'µ' + 182: 213, # 'Ò‘' + 183: 214, # 'Ј' + 184: 215, # 'Є' + 185: 216, # 'Ñ”' + 186: 217, # 'Ї' + 187: 218, # 'Ñ—' + 188: 219, # 'Љ' + 189: 220, # 'Ñ™' + 190: 221, # 'Њ' + 191: 222, # 'Ñš' + 192: 223, # 'ј' + 193: 224, # 'Ð…' + 194: 225, # '¬' + 195: 226, # '√' + 196: 227, # 'Æ’' + 197: 228, # '≈' + 198: 229, # '∆' + 199: 230, # '«' + 200: 231, # '»' + 201: 232, # '…' + 202: 233, # '\xa0' + 203: 234, # 'Ћ' + 204: 235, # 'Ñ›' + 205: 236, # 'ÐŒ' + 206: 237, # 'Ñœ' + 207: 238, # 'Ñ•' + 208: 239, # '–' + 209: 240, # '—' + 210: 241, # '“' + 211: 242, # 'â€' + 212: 243, # '‘' + 213: 244, # '’' + 214: 245, # '÷' + 215: 246, # '„' + 216: 247, # 'ÐŽ' + 217: 248, # 'Ñž' + 218: 249, # 'Ð' + 219: 250, # 'ÑŸ' + 220: 251, # 'â„–' + 221: 252, # 'Ð' + 222: 68, # 'Ñ‘' + 223: 16, # 'Ñ' + 224: 3, # 'а' + 225: 21, # 'б' + 226: 10, # 'в' + 227: 19, # 'г' + 228: 13, # 'д' + 229: 2, # 'е' + 230: 24, # 'ж' + 231: 20, # 'з' + 232: 4, # 'и' + 233: 23, # 'й' + 234: 11, # 'к' + 235: 8, # 'л' + 236: 12, # 'м' + 237: 5, # 'н' + 238: 1, # 'о' + 239: 15, # 'п' + 240: 9, # 'Ñ€' + 241: 7, # 'Ñ' + 242: 6, # 'Ñ‚' + 243: 14, # 'у' + 244: 39, # 'Ñ„' + 245: 26, # 'Ñ…' + 246: 28, # 'ц' + 247: 22, # 'ч' + 248: 25, # 'ш' + 249: 29, # 'щ' + 250: 54, # 'ÑŠ' + 251: 18, # 'Ñ‹' + 252: 17, # 'ÑŒ' + 253: 30, # 'Ñ' + 254: 27, # 'ÑŽ' + 255: 255, # '€' +} + +MACCYRILLIC_RUSSIAN_MODEL = SingleByteCharSetModel(charset_name='MacCyrillic', + language='Russian', + char_to_order_map=MACCYRILLIC_RUSSIAN_CHAR_TO_ORDER, + language_model=RUSSIAN_LANG_MODEL, + typical_positive_ratio=0.976601, + keep_ascii_letters=False, + alphabet='ÐÐБВГДЕЖЗИЙКЛМÐОПРСТУФХЦЧШЩЪЫЬЭЮЯабвгдежзийклмнопрÑтуфхцчшщъыьÑÑŽÑÑ‘') + +ISO_8859_5_RUSSIAN_CHAR_TO_ORDER = { + 0: 255, # '\x00' + 1: 255, # '\x01' + 2: 255, # '\x02' + 3: 255, # '\x03' + 4: 255, # '\x04' + 5: 255, # '\x05' + 6: 255, # '\x06' + 7: 255, # '\x07' + 8: 255, # '\x08' + 9: 255, # '\t' + 10: 254, # '\n' + 11: 255, # '\x0b' + 12: 255, # '\x0c' + 13: 254, # '\r' + 14: 255, # '\x0e' + 15: 255, # '\x0f' + 16: 255, # '\x10' + 17: 255, # '\x11' + 18: 255, # '\x12' + 19: 255, # '\x13' + 20: 255, # '\x14' + 21: 255, # '\x15' + 22: 255, # '\x16' + 23: 255, # '\x17' + 24: 255, # '\x18' + 25: 255, # '\x19' + 26: 255, # '\x1a' + 27: 255, # '\x1b' + 28: 255, # '\x1c' + 29: 255, # '\x1d' + 30: 255, # '\x1e' + 31: 255, # '\x1f' + 32: 253, # ' ' + 33: 253, # '!' + 34: 253, # '"' + 35: 253, # '#' + 36: 253, # '$' + 37: 253, # '%' + 38: 253, # '&' + 39: 253, # "'" + 40: 253, # '(' + 41: 253, # ')' + 42: 253, # '*' + 43: 253, # '+' + 44: 253, # ',' + 45: 253, # '-' + 46: 253, # '.' + 47: 253, # '/' + 48: 252, # '0' + 49: 252, # '1' + 50: 252, # '2' + 51: 252, # '3' + 52: 252, # '4' + 53: 252, # '5' + 54: 252, # '6' + 55: 252, # '7' + 56: 252, # '8' + 57: 252, # '9' + 58: 253, # ':' + 59: 253, # ';' + 60: 253, # '<' + 61: 253, # '=' + 62: 253, # '>' + 63: 253, # '?' + 64: 253, # '@' + 65: 142, # 'A' + 66: 143, # 'B' + 67: 144, # 'C' + 68: 145, # 'D' + 69: 146, # 'E' + 70: 147, # 'F' + 71: 148, # 'G' + 72: 149, # 'H' + 73: 150, # 'I' + 74: 151, # 'J' + 75: 152, # 'K' + 76: 74, # 'L' + 77: 153, # 'M' + 78: 75, # 'N' + 79: 154, # 'O' + 80: 155, # 'P' + 81: 156, # 'Q' + 82: 157, # 'R' + 83: 158, # 'S' + 84: 159, # 'T' + 85: 160, # 'U' + 86: 161, # 'V' + 87: 162, # 'W' + 88: 163, # 'X' + 89: 164, # 'Y' + 90: 165, # 'Z' + 91: 253, # '[' + 92: 253, # '\\' + 93: 253, # ']' + 94: 253, # '^' + 95: 253, # '_' + 96: 253, # '`' + 97: 71, # 'a' + 98: 172, # 'b' + 99: 66, # 'c' + 100: 173, # 'd' + 101: 65, # 'e' + 102: 174, # 'f' + 103: 76, # 'g' + 104: 175, # 'h' + 105: 64, # 'i' + 106: 176, # 'j' + 107: 177, # 'k' + 108: 77, # 'l' + 109: 72, # 'm' + 110: 178, # 'n' + 111: 69, # 'o' + 112: 67, # 'p' + 113: 179, # 'q' + 114: 78, # 'r' + 115: 73, # 's' + 116: 180, # 't' + 117: 181, # 'u' + 118: 79, # 'v' + 119: 182, # 'w' + 120: 183, # 'x' + 121: 184, # 'y' + 122: 185, # 'z' + 123: 253, # '{' + 124: 253, # '|' + 125: 253, # '}' + 126: 253, # '~' + 127: 253, # '\x7f' + 128: 191, # '\x80' + 129: 192, # '\x81' + 130: 193, # '\x82' + 131: 194, # '\x83' + 132: 195, # '\x84' + 133: 196, # '\x85' + 134: 197, # '\x86' + 135: 198, # '\x87' + 136: 199, # '\x88' + 137: 200, # '\x89' + 138: 201, # '\x8a' + 139: 202, # '\x8b' + 140: 203, # '\x8c' + 141: 204, # '\x8d' + 142: 205, # '\x8e' + 143: 206, # '\x8f' + 144: 207, # '\x90' + 145: 208, # '\x91' + 146: 209, # '\x92' + 147: 210, # '\x93' + 148: 211, # '\x94' + 149: 212, # '\x95' + 150: 213, # '\x96' + 151: 214, # '\x97' + 152: 215, # '\x98' + 153: 216, # '\x99' + 154: 217, # '\x9a' + 155: 218, # '\x9b' + 156: 219, # '\x9c' + 157: 220, # '\x9d' + 158: 221, # '\x9e' + 159: 222, # '\x9f' + 160: 223, # '\xa0' + 161: 224, # 'Ð' + 162: 225, # 'Ђ' + 163: 226, # 'Ѓ' + 164: 227, # 'Є' + 165: 228, # 'Ð…' + 166: 229, # 'І' + 167: 230, # 'Ї' + 168: 231, # 'Ј' + 169: 232, # 'Љ' + 170: 233, # 'Њ' + 171: 234, # 'Ћ' + 172: 235, # 'ÐŒ' + 173: 236, # '\xad' + 174: 237, # 'ÐŽ' + 175: 238, # 'Ð' + 176: 37, # 'Ð' + 177: 44, # 'Б' + 178: 33, # 'Ð’' + 179: 46, # 'Г' + 180: 41, # 'Д' + 181: 48, # 'Е' + 182: 56, # 'Ж' + 183: 51, # 'З' + 184: 42, # 'И' + 185: 60, # 'Й' + 186: 36, # 'К' + 187: 49, # 'Л' + 188: 38, # 'Ðœ' + 189: 31, # 'Ð' + 190: 34, # 'О' + 191: 35, # 'П' + 192: 45, # 'Р' + 193: 32, # 'С' + 194: 40, # 'Т' + 195: 52, # 'У' + 196: 53, # 'Ф' + 197: 55, # 'Ð¥' + 198: 58, # 'Ц' + 199: 50, # 'Ч' + 200: 57, # 'Ш' + 201: 63, # 'Щ' + 202: 70, # 'Ъ' + 203: 62, # 'Ы' + 204: 61, # 'Ь' + 205: 47, # 'Э' + 206: 59, # 'Ю' + 207: 43, # 'Я' + 208: 3, # 'а' + 209: 21, # 'б' + 210: 10, # 'в' + 211: 19, # 'г' + 212: 13, # 'д' + 213: 2, # 'е' + 214: 24, # 'ж' + 215: 20, # 'з' + 216: 4, # 'и' + 217: 23, # 'й' + 218: 11, # 'к' + 219: 8, # 'л' + 220: 12, # 'м' + 221: 5, # 'н' + 222: 1, # 'о' + 223: 15, # 'п' + 224: 9, # 'Ñ€' + 225: 7, # 'Ñ' + 226: 6, # 'Ñ‚' + 227: 14, # 'у' + 228: 39, # 'Ñ„' + 229: 26, # 'Ñ…' + 230: 28, # 'ц' + 231: 22, # 'ч' + 232: 25, # 'ш' + 233: 29, # 'щ' + 234: 54, # 'ÑŠ' + 235: 18, # 'Ñ‹' + 236: 17, # 'ÑŒ' + 237: 30, # 'Ñ' + 238: 27, # 'ÑŽ' + 239: 16, # 'Ñ' + 240: 239, # 'â„–' + 241: 68, # 'Ñ‘' + 242: 240, # 'Ñ’' + 243: 241, # 'Ñ“' + 244: 242, # 'Ñ”' + 245: 243, # 'Ñ•' + 246: 244, # 'Ñ–' + 247: 245, # 'Ñ—' + 248: 246, # 'ј' + 249: 247, # 'Ñ™' + 250: 248, # 'Ñš' + 251: 249, # 'Ñ›' + 252: 250, # 'Ñœ' + 253: 251, # '§' + 254: 252, # 'Ñž' + 255: 255, # 'ÑŸ' +} + +ISO_8859_5_RUSSIAN_MODEL = SingleByteCharSetModel(charset_name='ISO-8859-5', + language='Russian', + char_to_order_map=ISO_8859_5_RUSSIAN_CHAR_TO_ORDER, + language_model=RUSSIAN_LANG_MODEL, + typical_positive_ratio=0.976601, + keep_ascii_letters=False, + alphabet='ÐÐБВГДЕЖЗИЙКЛМÐОПРСТУФХЦЧШЩЪЫЬЭЮЯабвгдежзийклмнопрÑтуфхцчшщъыьÑÑŽÑÑ‘') + diff --git a/libs/chardet/langthaimodel.py b/libs/chardet/langthaimodel.py index 15f94c2df..d0191f241 100644 --- a/libs/chardet/langthaimodel.py +++ b/libs/chardet/langthaimodel.py @@ -1,199 +1,4383 @@ -######################## BEGIN LICENSE BLOCK ######################## -# The Original Code is Mozilla Communicator client code. -# -# The Initial Developer of the Original Code is -# Netscape Communications Corporation. -# Portions created by the Initial Developer are Copyright (C) 1998 -# the Initial Developer. All Rights Reserved. -# -# Contributor(s): -# Mark Pilgrim - port to Python -# -# This library is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 2.1 of the License, or (at your option) any later version. -# -# This library is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with this library; if not, write to the Free Software -# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA -# 02110-1301 USA -######################### END LICENSE BLOCK ######################### +#!/usr/bin/env python +# -*- coding: utf-8 -*- -# 255: Control characters that usually does not exist in any text +from chardet.sbcharsetprober import SingleByteCharSetModel + + +# 3: Positive +# 2: Likely +# 1: Unlikely +# 0: Negative + +THAI_LANG_MODEL = { + 5: { # 'à¸' + 5: 2, # 'à¸' + 30: 2, # 'ข' + 24: 2, # 'ค' + 8: 2, # 'ง' + 26: 2, # 'จ' + 52: 0, # 'ฉ' + 34: 1, # 'ช' + 51: 1, # 'ซ' + 47: 0, # 'à¸' + 58: 3, # 'ฎ' + 57: 2, # 'à¸' + 49: 0, # 'à¸' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 2, # 'ณ' + 20: 2, # 'ด' + 19: 3, # 'ต' + 44: 0, # 'ถ' + 14: 2, # 'ท' + 48: 0, # 'ธ' + 3: 2, # 'น' + 17: 1, # 'บ' + 25: 2, # 'ป' + 39: 1, # 'ผ' + 62: 1, # 'à¸' + 31: 1, # 'พ' + 54: 0, # 'ฟ' + 45: 1, # 'ภ' + 9: 2, # 'ม' + 16: 1, # 'ย' + 2: 3, # 'ร' + 61: 2, # 'ฤ' + 15: 3, # 'ล' + 12: 3, # 'ว' + 42: 2, # 'ศ' + 46: 3, # 'ษ' + 18: 2, # 'ส' + 21: 2, # 'ห' + 4: 3, # 'อ' + 63: 1, # 'ฯ' + 22: 2, # 'ะ' + 10: 3, # 'ั' + 1: 3, # 'า' + 36: 3, # 'ำ' + 23: 3, # 'ิ' + 13: 3, # 'ี' + 40: 0, # 'ึ' + 27: 2, # 'ื' + 32: 2, # 'ุ' + 35: 1, # 'ู' + 11: 2, # 'เ' + 28: 2, # 'à¹' + 41: 1, # 'โ' + 29: 1, # 'ใ' + 33: 2, # 'ไ' + 50: 1, # 'ๆ' + 37: 3, # '็' + 6: 3, # '่' + 7: 3, # '้' + 38: 2, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 30: { # 'ข' + 5: 1, # 'à¸' + 30: 0, # 'ข' + 24: 1, # 'ค' + 8: 1, # 'ง' + 26: 1, # 'จ' + 52: 0, # 'ฉ' + 34: 0, # 'ช' + 51: 0, # 'ซ' + 47: 0, # 'à¸' + 58: 0, # 'ฎ' + 57: 0, # 'à¸' + 49: 0, # 'à¸' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 2, # 'ณ' + 20: 0, # 'ด' + 19: 2, # 'ต' + 44: 0, # 'ถ' + 14: 1, # 'ท' + 48: 0, # 'ธ' + 3: 2, # 'น' + 17: 1, # 'บ' + 25: 1, # 'ป' + 39: 0, # 'ผ' + 62: 0, # 'à¸' + 31: 0, # 'พ' + 54: 0, # 'ฟ' + 45: 0, # 'ภ' + 9: 0, # 'ม' + 16: 2, # 'ย' + 2: 1, # 'ร' + 61: 0, # 'ฤ' + 15: 0, # 'ล' + 12: 2, # 'ว' + 42: 0, # 'ศ' + 46: 0, # 'ษ' + 18: 1, # 'ส' + 21: 1, # 'ห' + 4: 3, # 'อ' + 63: 0, # 'ฯ' + 22: 0, # 'ะ' + 10: 3, # 'ั' + 1: 3, # 'า' + 36: 0, # 'ำ' + 23: 0, # 'ิ' + 13: 2, # 'ี' + 40: 3, # 'ึ' + 27: 1, # 'ื' + 32: 1, # 'ุ' + 35: 0, # 'ู' + 11: 0, # 'เ' + 28: 0, # 'à¹' + 41: 0, # 'โ' + 29: 1, # 'ใ' + 33: 0, # 'ไ' + 50: 0, # 'ๆ' + 37: 1, # '็' + 6: 2, # '่' + 7: 3, # '้' + 38: 1, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 24: { # 'ค' + 5: 0, # 'à¸' + 30: 0, # 'ข' + 24: 2, # 'ค' + 8: 2, # 'ง' + 26: 0, # 'จ' + 52: 0, # 'ฉ' + 34: 0, # 'ช' + 51: 0, # 'ซ' + 47: 0, # 'à¸' + 58: 0, # 'ฎ' + 57: 0, # 'à¸' + 49: 0, # 'à¸' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 2, # 'ณ' + 20: 2, # 'ด' + 19: 2, # 'ต' + 44: 0, # 'ถ' + 14: 1, # 'ท' + 48: 0, # 'ธ' + 3: 3, # 'น' + 17: 0, # 'บ' + 25: 1, # 'ป' + 39: 0, # 'ผ' + 62: 0, # 'à¸' + 31: 0, # 'พ' + 54: 0, # 'ฟ' + 45: 0, # 'ภ' + 9: 2, # 'ม' + 16: 2, # 'ย' + 2: 3, # 'ร' + 61: 0, # 'ฤ' + 15: 3, # 'ล' + 12: 3, # 'ว' + 42: 0, # 'ศ' + 46: 0, # 'ษ' + 18: 1, # 'ส' + 21: 0, # 'ห' + 4: 2, # 'อ' + 63: 0, # 'ฯ' + 22: 2, # 'ะ' + 10: 3, # 'ั' + 1: 2, # 'า' + 36: 3, # 'ำ' + 23: 3, # 'ิ' + 13: 2, # 'ี' + 40: 0, # 'ึ' + 27: 3, # 'ื' + 32: 3, # 'ุ' + 35: 2, # 'ู' + 11: 1, # 'เ' + 28: 0, # 'à¹' + 41: 3, # 'โ' + 29: 0, # 'ใ' + 33: 0, # 'ไ' + 50: 0, # 'ๆ' + 37: 1, # '็' + 6: 3, # '่' + 7: 3, # '้' + 38: 3, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 8: { # 'ง' + 5: 3, # 'à¸' + 30: 2, # 'ข' + 24: 3, # 'ค' + 8: 2, # 'ง' + 26: 2, # 'จ' + 52: 1, # 'ฉ' + 34: 2, # 'ช' + 51: 1, # 'ซ' + 47: 0, # 'à¸' + 58: 0, # 'ฎ' + 57: 0, # 'à¸' + 49: 0, # 'à¸' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 0, # 'ณ' + 20: 2, # 'ด' + 19: 2, # 'ต' + 44: 1, # 'ถ' + 14: 3, # 'ท' + 48: 1, # 'ธ' + 3: 3, # 'น' + 17: 2, # 'บ' + 25: 2, # 'ป' + 39: 2, # 'ผ' + 62: 1, # 'à¸' + 31: 2, # 'พ' + 54: 0, # 'ฟ' + 45: 1, # 'ภ' + 9: 2, # 'ม' + 16: 1, # 'ย' + 2: 2, # 'ร' + 61: 0, # 'ฤ' + 15: 2, # 'ล' + 12: 2, # 'ว' + 42: 2, # 'ศ' + 46: 1, # 'ษ' + 18: 3, # 'ส' + 21: 3, # 'ห' + 4: 2, # 'อ' + 63: 0, # 'ฯ' + 22: 0, # 'ะ' + 10: 1, # 'ั' + 1: 3, # 'า' + 36: 0, # 'ำ' + 23: 2, # 'ิ' + 13: 1, # 'ี' + 40: 0, # 'ึ' + 27: 1, # 'ื' + 32: 1, # 'ุ' + 35: 0, # 'ู' + 11: 3, # 'เ' + 28: 2, # 'à¹' + 41: 1, # 'โ' + 29: 2, # 'ใ' + 33: 2, # 'ไ' + 50: 3, # 'ๆ' + 37: 0, # '็' + 6: 2, # '่' + 7: 0, # '้' + 38: 0, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 26: { # 'จ' + 5: 2, # 'à¸' + 30: 1, # 'ข' + 24: 0, # 'ค' + 8: 2, # 'ง' + 26: 3, # 'จ' + 52: 0, # 'ฉ' + 34: 0, # 'ช' + 51: 0, # 'ซ' + 47: 0, # 'à¸' + 58: 0, # 'ฎ' + 57: 0, # 'à¸' + 49: 0, # 'à¸' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 0, # 'ณ' + 20: 2, # 'ด' + 19: 1, # 'ต' + 44: 1, # 'ถ' + 14: 2, # 'ท' + 48: 0, # 'ธ' + 3: 3, # 'น' + 17: 1, # 'บ' + 25: 0, # 'ป' + 39: 0, # 'ผ' + 62: 0, # 'à¸' + 31: 1, # 'พ' + 54: 0, # 'ฟ' + 45: 0, # 'ภ' + 9: 1, # 'ม' + 16: 1, # 'ย' + 2: 3, # 'ร' + 61: 0, # 'ฤ' + 15: 0, # 'ล' + 12: 1, # 'ว' + 42: 0, # 'ศ' + 46: 0, # 'ษ' + 18: 2, # 'ส' + 21: 1, # 'ห' + 4: 2, # 'อ' + 63: 0, # 'ฯ' + 22: 3, # 'ะ' + 10: 3, # 'ั' + 1: 3, # 'า' + 36: 3, # 'ำ' + 23: 2, # 'ิ' + 13: 1, # 'ี' + 40: 3, # 'ึ' + 27: 1, # 'ื' + 32: 3, # 'ุ' + 35: 2, # 'ู' + 11: 1, # 'เ' + 28: 1, # 'à¹' + 41: 0, # 'โ' + 29: 1, # 'ใ' + 33: 1, # 'ไ' + 50: 0, # 'ๆ' + 37: 0, # '็' + 6: 2, # '่' + 7: 2, # '้' + 38: 0, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 52: { # 'ฉ' + 5: 0, # 'à¸' + 30: 0, # 'ข' + 24: 0, # 'ค' + 8: 0, # 'ง' + 26: 0, # 'จ' + 52: 0, # 'ฉ' + 34: 0, # 'ช' + 51: 0, # 'ซ' + 47: 0, # 'à¸' + 58: 0, # 'ฎ' + 57: 0, # 'à¸' + 49: 0, # 'à¸' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 0, # 'ณ' + 20: 0, # 'ด' + 19: 0, # 'ต' + 44: 0, # 'ถ' + 14: 0, # 'ท' + 48: 0, # 'ธ' + 3: 0, # 'น' + 17: 3, # 'บ' + 25: 0, # 'ป' + 39: 0, # 'ผ' + 62: 0, # 'à¸' + 31: 3, # 'พ' + 54: 0, # 'ฟ' + 45: 0, # 'ภ' + 9: 1, # 'ม' + 16: 1, # 'ย' + 2: 0, # 'ร' + 61: 0, # 'ฤ' + 15: 2, # 'ล' + 12: 1, # 'ว' + 42: 0, # 'ศ' + 46: 0, # 'ษ' + 18: 0, # 'ส' + 21: 0, # 'ห' + 4: 0, # 'อ' + 63: 0, # 'ฯ' + 22: 1, # 'ะ' + 10: 1, # 'ั' + 1: 1, # 'า' + 36: 0, # 'ำ' + 23: 1, # 'ิ' + 13: 1, # 'ี' + 40: 0, # 'ึ' + 27: 0, # 'ื' + 32: 1, # 'ุ' + 35: 0, # 'ู' + 11: 0, # 'เ' + 28: 0, # 'à¹' + 41: 0, # 'โ' + 29: 0, # 'ใ' + 33: 0, # 'ไ' + 50: 0, # 'ๆ' + 37: 0, # '็' + 6: 0, # '่' + 7: 0, # '้' + 38: 0, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 34: { # 'ช' + 5: 1, # 'à¸' + 30: 0, # 'ข' + 24: 0, # 'ค' + 8: 1, # 'ง' + 26: 0, # 'จ' + 52: 0, # 'ฉ' + 34: 0, # 'ช' + 51: 0, # 'ซ' + 47: 1, # 'à¸' + 58: 0, # 'ฎ' + 57: 0, # 'à¸' + 49: 0, # 'à¸' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 0, # 'ณ' + 20: 0, # 'ด' + 19: 0, # 'ต' + 44: 0, # 'ถ' + 14: 1, # 'ท' + 48: 0, # 'ธ' + 3: 3, # 'น' + 17: 2, # 'บ' + 25: 0, # 'ป' + 39: 0, # 'ผ' + 62: 0, # 'à¸' + 31: 0, # 'พ' + 54: 0, # 'ฟ' + 45: 0, # 'ภ' + 9: 2, # 'ม' + 16: 1, # 'ย' + 2: 1, # 'ร' + 61: 0, # 'ฤ' + 15: 0, # 'ล' + 12: 1, # 'ว' + 42: 0, # 'ศ' + 46: 0, # 'ษ' + 18: 0, # 'ส' + 21: 0, # 'ห' + 4: 2, # 'อ' + 63: 0, # 'ฯ' + 22: 0, # 'ะ' + 10: 2, # 'ั' + 1: 3, # 'า' + 36: 1, # 'ำ' + 23: 3, # 'ิ' + 13: 2, # 'ี' + 40: 0, # 'ึ' + 27: 3, # 'ื' + 32: 3, # 'ุ' + 35: 1, # 'ู' + 11: 0, # 'เ' + 28: 0, # 'à¹' + 41: 0, # 'โ' + 29: 0, # 'ใ' + 33: 0, # 'ไ' + 50: 0, # 'ๆ' + 37: 1, # '็' + 6: 3, # '่' + 7: 3, # '้' + 38: 0, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 51: { # 'ซ' + 5: 0, # 'à¸' + 30: 0, # 'ข' + 24: 0, # 'ค' + 8: 0, # 'ง' + 26: 0, # 'จ' + 52: 0, # 'ฉ' + 34: 0, # 'ช' + 51: 0, # 'ซ' + 47: 0, # 'à¸' + 58: 0, # 'ฎ' + 57: 0, # 'à¸' + 49: 0, # 'à¸' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 0, # 'ณ' + 20: 0, # 'ด' + 19: 0, # 'ต' + 44: 0, # 'ถ' + 14: 0, # 'ท' + 48: 0, # 'ธ' + 3: 1, # 'น' + 17: 0, # 'บ' + 25: 0, # 'ป' + 39: 0, # 'ผ' + 62: 0, # 'à¸' + 31: 0, # 'พ' + 54: 0, # 'ฟ' + 45: 0, # 'ภ' + 9: 0, # 'ม' + 16: 0, # 'ย' + 2: 0, # 'ร' + 61: 0, # 'ฤ' + 15: 1, # 'ล' + 12: 0, # 'ว' + 42: 0, # 'ศ' + 46: 0, # 'ษ' + 18: 1, # 'ส' + 21: 0, # 'ห' + 4: 2, # 'อ' + 63: 0, # 'ฯ' + 22: 0, # 'ะ' + 10: 1, # 'ั' + 1: 1, # 'า' + 36: 0, # 'ำ' + 23: 1, # 'ิ' + 13: 2, # 'ี' + 40: 3, # 'ึ' + 27: 2, # 'ื' + 32: 1, # 'ุ' + 35: 1, # 'ู' + 11: 1, # 'เ' + 28: 0, # 'à¹' + 41: 0, # 'โ' + 29: 0, # 'ใ' + 33: 0, # 'ไ' + 50: 0, # 'ๆ' + 37: 1, # '็' + 6: 1, # '่' + 7: 2, # '้' + 38: 1, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 47: { # 'à¸' + 5: 1, # 'à¸' + 30: 1, # 'ข' + 24: 0, # 'ค' + 8: 0, # 'ง' + 26: 0, # 'จ' + 52: 0, # 'ฉ' + 34: 1, # 'ช' + 51: 0, # 'ซ' + 47: 3, # 'à¸' + 58: 0, # 'ฎ' + 57: 0, # 'à¸' + 49: 0, # 'à¸' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 0, # 'ณ' + 20: 0, # 'ด' + 19: 0, # 'ต' + 44: 0, # 'ถ' + 14: 1, # 'ท' + 48: 0, # 'ธ' + 3: 0, # 'น' + 17: 1, # 'บ' + 25: 1, # 'ป' + 39: 0, # 'ผ' + 62: 0, # 'à¸' + 31: 0, # 'พ' + 54: 0, # 'ฟ' + 45: 0, # 'ภ' + 9: 1, # 'ม' + 16: 0, # 'ย' + 2: 0, # 'ร' + 61: 0, # 'ฤ' + 15: 1, # 'ล' + 12: 0, # 'ว' + 42: 0, # 'ศ' + 46: 0, # 'ษ' + 18: 1, # 'ส' + 21: 2, # 'ห' + 4: 1, # 'อ' + 63: 0, # 'ฯ' + 22: 1, # 'ะ' + 10: 2, # 'ั' + 1: 3, # 'า' + 36: 0, # 'ำ' + 23: 1, # 'ิ' + 13: 1, # 'ี' + 40: 0, # 'ึ' + 27: 0, # 'ื' + 32: 0, # 'ุ' + 35: 0, # 'ู' + 11: 1, # 'เ' + 28: 1, # 'à¹' + 41: 0, # 'โ' + 29: 1, # 'ใ' + 33: 0, # 'ไ' + 50: 1, # 'ๆ' + 37: 0, # '็' + 6: 2, # '่' + 7: 0, # '้' + 38: 0, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 58: { # 'ฎ' + 5: 2, # 'à¸' + 30: 0, # 'ข' + 24: 0, # 'ค' + 8: 0, # 'ง' + 26: 0, # 'จ' + 52: 0, # 'ฉ' + 34: 0, # 'ช' + 51: 0, # 'ซ' + 47: 0, # 'à¸' + 58: 0, # 'ฎ' + 57: 0, # 'à¸' + 49: 0, # 'à¸' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 0, # 'ณ' + 20: 0, # 'ด' + 19: 0, # 'ต' + 44: 0, # 'ถ' + 14: 0, # 'ท' + 48: 0, # 'ธ' + 3: 0, # 'น' + 17: 0, # 'บ' + 25: 0, # 'ป' + 39: 0, # 'ผ' + 62: 0, # 'à¸' + 31: 0, # 'พ' + 54: 0, # 'ฟ' + 45: 0, # 'ภ' + 9: 0, # 'ม' + 16: 0, # 'ย' + 2: 0, # 'ร' + 61: 0, # 'ฤ' + 15: 0, # 'ล' + 12: 0, # 'ว' + 42: 0, # 'ศ' + 46: 0, # 'ษ' + 18: 0, # 'ส' + 21: 1, # 'ห' + 4: 0, # 'อ' + 63: 0, # 'ฯ' + 22: 0, # 'ะ' + 10: 0, # 'ั' + 1: 0, # 'า' + 36: 0, # 'ำ' + 23: 1, # 'ิ' + 13: 2, # 'ี' + 40: 0, # 'ึ' + 27: 0, # 'ื' + 32: 0, # 'ุ' + 35: 0, # 'ู' + 11: 0, # 'เ' + 28: 0, # 'à¹' + 41: 0, # 'โ' + 29: 0, # 'ใ' + 33: 0, # 'ไ' + 50: 0, # 'ๆ' + 37: 0, # '็' + 6: 0, # '่' + 7: 0, # '้' + 38: 0, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 57: { # 'à¸' + 5: 0, # 'à¸' + 30: 0, # 'ข' + 24: 0, # 'ค' + 8: 0, # 'ง' + 26: 0, # 'จ' + 52: 0, # 'ฉ' + 34: 0, # 'ช' + 51: 0, # 'ซ' + 47: 0, # 'à¸' + 58: 0, # 'ฎ' + 57: 0, # 'à¸' + 49: 0, # 'à¸' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 0, # 'ณ' + 20: 0, # 'ด' + 19: 0, # 'ต' + 44: 0, # 'ถ' + 14: 0, # 'ท' + 48: 0, # 'ธ' + 3: 0, # 'น' + 17: 0, # 'บ' + 25: 0, # 'ป' + 39: 0, # 'ผ' + 62: 0, # 'à¸' + 31: 0, # 'พ' + 54: 0, # 'ฟ' + 45: 0, # 'ภ' + 9: 0, # 'ม' + 16: 0, # 'ย' + 2: 0, # 'ร' + 61: 0, # 'ฤ' + 15: 0, # 'ล' + 12: 0, # 'ว' + 42: 0, # 'ศ' + 46: 0, # 'ษ' + 18: 0, # 'ส' + 21: 0, # 'ห' + 4: 0, # 'อ' + 63: 0, # 'ฯ' + 22: 0, # 'ะ' + 10: 0, # 'ั' + 1: 0, # 'า' + 36: 0, # 'ำ' + 23: 3, # 'ิ' + 13: 1, # 'ี' + 40: 0, # 'ึ' + 27: 0, # 'ื' + 32: 0, # 'ุ' + 35: 0, # 'ู' + 11: 0, # 'เ' + 28: 0, # 'à¹' + 41: 0, # 'โ' + 29: 0, # 'ใ' + 33: 0, # 'ไ' + 50: 0, # 'ๆ' + 37: 0, # '็' + 6: 0, # '่' + 7: 0, # '้' + 38: 0, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 49: { # 'à¸' + 5: 1, # 'à¸' + 30: 0, # 'ข' + 24: 0, # 'ค' + 8: 0, # 'ง' + 26: 0, # 'จ' + 52: 0, # 'ฉ' + 34: 0, # 'ช' + 51: 0, # 'ซ' + 47: 0, # 'à¸' + 58: 0, # 'ฎ' + 57: 0, # 'à¸' + 49: 0, # 'à¸' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 0, # 'ณ' + 20: 0, # 'ด' + 19: 0, # 'ต' + 44: 0, # 'ถ' + 14: 0, # 'ท' + 48: 0, # 'ธ' + 3: 0, # 'น' + 17: 2, # 'บ' + 25: 0, # 'ป' + 39: 0, # 'ผ' + 62: 0, # 'à¸' + 31: 0, # 'พ' + 54: 0, # 'ฟ' + 45: 0, # 'ภ' + 9: 2, # 'ม' + 16: 0, # 'ย' + 2: 0, # 'ร' + 61: 0, # 'ฤ' + 15: 0, # 'ล' + 12: 0, # 'ว' + 42: 1, # 'ศ' + 46: 0, # 'ษ' + 18: 0, # 'ส' + 21: 0, # 'ห' + 4: 1, # 'อ' + 63: 0, # 'ฯ' + 22: 0, # 'ะ' + 10: 0, # 'ั' + 1: 3, # 'า' + 36: 0, # 'ำ' + 23: 0, # 'ิ' + 13: 0, # 'ี' + 40: 0, # 'ึ' + 27: 0, # 'ื' + 32: 0, # 'ุ' + 35: 0, # 'ู' + 11: 0, # 'เ' + 28: 0, # 'à¹' + 41: 0, # 'โ' + 29: 0, # 'ใ' + 33: 0, # 'ไ' + 50: 0, # 'ๆ' + 37: 0, # '็' + 6: 0, # '่' + 7: 0, # '้' + 38: 1, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 53: { # 'ฑ' + 5: 0, # 'à¸' + 30: 0, # 'ข' + 24: 0, # 'ค' + 8: 0, # 'ง' + 26: 0, # 'จ' + 52: 0, # 'ฉ' + 34: 0, # 'ช' + 51: 0, # 'ซ' + 47: 0, # 'à¸' + 58: 0, # 'ฎ' + 57: 0, # 'à¸' + 49: 0, # 'à¸' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 0, # 'ณ' + 20: 0, # 'ด' + 19: 0, # 'ต' + 44: 0, # 'ถ' + 14: 0, # 'ท' + 48: 0, # 'ธ' + 3: 0, # 'น' + 17: 0, # 'บ' + 25: 0, # 'ป' + 39: 0, # 'ผ' + 62: 0, # 'à¸' + 31: 0, # 'พ' + 54: 0, # 'ฟ' + 45: 0, # 'ภ' + 9: 0, # 'ม' + 16: 0, # 'ย' + 2: 0, # 'ร' + 61: 0, # 'ฤ' + 15: 0, # 'ล' + 12: 0, # 'ว' + 42: 0, # 'ศ' + 46: 0, # 'ษ' + 18: 0, # 'ส' + 21: 0, # 'ห' + 4: 0, # 'อ' + 63: 0, # 'ฯ' + 22: 0, # 'ะ' + 10: 0, # 'ั' + 1: 0, # 'า' + 36: 0, # 'ำ' + 23: 2, # 'ิ' + 13: 0, # 'ี' + 40: 0, # 'ึ' + 27: 0, # 'ื' + 32: 0, # 'ุ' + 35: 0, # 'ู' + 11: 0, # 'เ' + 28: 0, # 'à¹' + 41: 0, # 'โ' + 29: 0, # 'ใ' + 33: 0, # 'ไ' + 50: 0, # 'ๆ' + 37: 0, # '็' + 6: 0, # '่' + 7: 0, # '้' + 38: 3, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 55: { # 'ฒ' + 5: 0, # 'à¸' + 30: 0, # 'ข' + 24: 0, # 'ค' + 8: 0, # 'ง' + 26: 0, # 'จ' + 52: 0, # 'ฉ' + 34: 0, # 'ช' + 51: 0, # 'ซ' + 47: 0, # 'à¸' + 58: 0, # 'ฎ' + 57: 0, # 'à¸' + 49: 0, # 'à¸' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 0, # 'ณ' + 20: 0, # 'ด' + 19: 0, # 'ต' + 44: 0, # 'ถ' + 14: 0, # 'ท' + 48: 0, # 'ธ' + 3: 3, # 'น' + 17: 0, # 'บ' + 25: 0, # 'ป' + 39: 0, # 'ผ' + 62: 0, # 'à¸' + 31: 1, # 'พ' + 54: 0, # 'ฟ' + 45: 0, # 'ภ' + 9: 0, # 'ม' + 16: 0, # 'ย' + 2: 0, # 'ร' + 61: 0, # 'ฤ' + 15: 0, # 'ล' + 12: 0, # 'ว' + 42: 0, # 'ศ' + 46: 0, # 'ษ' + 18: 0, # 'ส' + 21: 0, # 'ห' + 4: 0, # 'อ' + 63: 0, # 'ฯ' + 22: 0, # 'ะ' + 10: 0, # 'ั' + 1: 0, # 'า' + 36: 0, # 'ำ' + 23: 1, # 'ิ' + 13: 0, # 'ี' + 40: 0, # 'ึ' + 27: 0, # 'ื' + 32: 0, # 'ุ' + 35: 0, # 'ู' + 11: 0, # 'เ' + 28: 0, # 'à¹' + 41: 0, # 'โ' + 29: 0, # 'ใ' + 33: 0, # 'ไ' + 50: 0, # 'ๆ' + 37: 0, # '็' + 6: 0, # '่' + 7: 0, # '้' + 38: 0, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 43: { # 'ณ' + 5: 1, # 'à¸' + 30: 0, # 'ข' + 24: 0, # 'ค' + 8: 0, # 'ง' + 26: 0, # 'จ' + 52: 0, # 'ฉ' + 34: 0, # 'ช' + 51: 0, # 'ซ' + 47: 0, # 'à¸' + 58: 0, # 'ฎ' + 57: 0, # 'à¸' + 49: 0, # 'à¸' + 53: 3, # 'ฑ' + 55: 0, # 'ฒ' + 43: 0, # 'ณ' + 20: 0, # 'ด' + 19: 0, # 'ต' + 44: 0, # 'ถ' + 14: 0, # 'ท' + 48: 0, # 'ธ' + 3: 0, # 'น' + 17: 0, # 'บ' + 25: 0, # 'ป' + 39: 0, # 'ผ' + 62: 0, # 'à¸' + 31: 0, # 'พ' + 54: 0, # 'ฟ' + 45: 3, # 'ภ' + 9: 0, # 'ม' + 16: 0, # 'ย' + 2: 1, # 'ร' + 61: 0, # 'ฤ' + 15: 0, # 'ล' + 12: 1, # 'ว' + 42: 0, # 'ศ' + 46: 0, # 'ษ' + 18: 1, # 'ส' + 21: 1, # 'ห' + 4: 0, # 'อ' + 63: 0, # 'ฯ' + 22: 3, # 'ะ' + 10: 0, # 'ั' + 1: 3, # 'า' + 36: 0, # 'ำ' + 23: 1, # 'ิ' + 13: 2, # 'ี' + 40: 0, # 'ึ' + 27: 0, # 'ื' + 32: 0, # 'ุ' + 35: 0, # 'ู' + 11: 1, # 'เ' + 28: 1, # 'à¹' + 41: 0, # 'โ' + 29: 1, # 'ใ' + 33: 1, # 'ไ' + 50: 0, # 'ๆ' + 37: 0, # '็' + 6: 0, # '่' + 7: 0, # '้' + 38: 3, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 20: { # 'ด' + 5: 2, # 'à¸' + 30: 2, # 'ข' + 24: 2, # 'ค' + 8: 3, # 'ง' + 26: 2, # 'จ' + 52: 0, # 'ฉ' + 34: 1, # 'ช' + 51: 0, # 'ซ' + 47: 0, # 'à¸' + 58: 0, # 'ฎ' + 57: 0, # 'à¸' + 49: 0, # 'à¸' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 0, # 'ณ' + 20: 1, # 'ด' + 19: 2, # 'ต' + 44: 1, # 'ถ' + 14: 2, # 'ท' + 48: 0, # 'ธ' + 3: 1, # 'น' + 17: 1, # 'บ' + 25: 1, # 'ป' + 39: 1, # 'ผ' + 62: 0, # 'à¸' + 31: 1, # 'พ' + 54: 0, # 'ฟ' + 45: 1, # 'ภ' + 9: 2, # 'ม' + 16: 3, # 'ย' + 2: 2, # 'ร' + 61: 0, # 'ฤ' + 15: 2, # 'ล' + 12: 2, # 'ว' + 42: 0, # 'ศ' + 46: 0, # 'ษ' + 18: 2, # 'ส' + 21: 2, # 'ห' + 4: 1, # 'อ' + 63: 0, # 'ฯ' + 22: 0, # 'ะ' + 10: 3, # 'ั' + 1: 2, # 'า' + 36: 2, # 'ำ' + 23: 3, # 'ิ' + 13: 3, # 'ี' + 40: 1, # 'ึ' + 27: 2, # 'ื' + 32: 3, # 'ุ' + 35: 2, # 'ู' + 11: 2, # 'เ' + 28: 2, # 'à¹' + 41: 1, # 'โ' + 29: 2, # 'ใ' + 33: 2, # 'ไ' + 50: 2, # 'ๆ' + 37: 2, # '็' + 6: 1, # '่' + 7: 3, # '้' + 38: 1, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 19: { # 'ต' + 5: 2, # 'à¸' + 30: 1, # 'ข' + 24: 1, # 'ค' + 8: 0, # 'ง' + 26: 1, # 'จ' + 52: 0, # 'ฉ' + 34: 1, # 'ช' + 51: 0, # 'ซ' + 47: 0, # 'à¸' + 58: 0, # 'ฎ' + 57: 0, # 'à¸' + 49: 0, # 'à¸' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 0, # 'ณ' + 20: 1, # 'ด' + 19: 1, # 'ต' + 44: 2, # 'ถ' + 14: 1, # 'ท' + 48: 0, # 'ธ' + 3: 2, # 'น' + 17: 1, # 'บ' + 25: 1, # 'ป' + 39: 1, # 'ผ' + 62: 0, # 'à¸' + 31: 1, # 'พ' + 54: 0, # 'ฟ' + 45: 2, # 'ภ' + 9: 1, # 'ม' + 16: 1, # 'ย' + 2: 3, # 'ร' + 61: 0, # 'ฤ' + 15: 2, # 'ล' + 12: 1, # 'ว' + 42: 0, # 'ศ' + 46: 0, # 'ษ' + 18: 3, # 'ส' + 21: 0, # 'ห' + 4: 3, # 'อ' + 63: 1, # 'ฯ' + 22: 2, # 'ะ' + 10: 3, # 'ั' + 1: 3, # 'า' + 36: 2, # 'ำ' + 23: 3, # 'ิ' + 13: 2, # 'ี' + 40: 1, # 'ึ' + 27: 1, # 'ื' + 32: 3, # 'ุ' + 35: 2, # 'ู' + 11: 1, # 'เ' + 28: 1, # 'à¹' + 41: 1, # 'โ' + 29: 1, # 'ใ' + 33: 1, # 'ไ' + 50: 0, # 'ๆ' + 37: 2, # '็' + 6: 3, # '่' + 7: 3, # '้' + 38: 2, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 44: { # 'ถ' + 5: 1, # 'à¸' + 30: 0, # 'ข' + 24: 1, # 'ค' + 8: 0, # 'ง' + 26: 1, # 'จ' + 52: 0, # 'ฉ' + 34: 0, # 'ช' + 51: 0, # 'ซ' + 47: 0, # 'à¸' + 58: 0, # 'ฎ' + 57: 0, # 'à¸' + 49: 0, # 'à¸' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 0, # 'ณ' + 20: 0, # 'ด' + 19: 1, # 'ต' + 44: 0, # 'ถ' + 14: 1, # 'ท' + 48: 0, # 'ธ' + 3: 1, # 'น' + 17: 2, # 'บ' + 25: 0, # 'ป' + 39: 0, # 'ผ' + 62: 0, # 'à¸' + 31: 1, # 'พ' + 54: 0, # 'ฟ' + 45: 0, # 'ภ' + 9: 0, # 'ม' + 16: 0, # 'ย' + 2: 1, # 'ร' + 61: 0, # 'ฤ' + 15: 1, # 'ล' + 12: 1, # 'ว' + 42: 0, # 'ศ' + 46: 0, # 'ษ' + 18: 1, # 'ส' + 21: 0, # 'ห' + 4: 1, # 'อ' + 63: 0, # 'ฯ' + 22: 0, # 'ะ' + 10: 2, # 'ั' + 1: 3, # 'า' + 36: 0, # 'ำ' + 23: 2, # 'ิ' + 13: 1, # 'ี' + 40: 3, # 'ึ' + 27: 2, # 'ื' + 32: 2, # 'ุ' + 35: 3, # 'ู' + 11: 1, # 'เ' + 28: 1, # 'à¹' + 41: 0, # 'โ' + 29: 1, # 'ใ' + 33: 1, # 'ไ' + 50: 0, # 'ๆ' + 37: 0, # '็' + 6: 2, # '่' + 7: 3, # '้' + 38: 0, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 14: { # 'ท' + 5: 1, # 'à¸' + 30: 1, # 'ข' + 24: 3, # 'ค' + 8: 1, # 'ง' + 26: 1, # 'จ' + 52: 0, # 'ฉ' + 34: 0, # 'ช' + 51: 0, # 'ซ' + 47: 0, # 'à¸' + 58: 0, # 'ฎ' + 57: 0, # 'à¸' + 49: 0, # 'à¸' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 0, # 'ณ' + 20: 2, # 'ด' + 19: 1, # 'ต' + 44: 0, # 'ถ' + 14: 1, # 'ท' + 48: 3, # 'ธ' + 3: 3, # 'น' + 17: 2, # 'บ' + 25: 2, # 'ป' + 39: 1, # 'ผ' + 62: 0, # 'à¸' + 31: 2, # 'พ' + 54: 0, # 'ฟ' + 45: 0, # 'ภ' + 9: 1, # 'ม' + 16: 3, # 'ย' + 2: 3, # 'ร' + 61: 1, # 'ฤ' + 15: 1, # 'ล' + 12: 2, # 'ว' + 42: 3, # 'ศ' + 46: 1, # 'ษ' + 18: 1, # 'ส' + 21: 0, # 'ห' + 4: 2, # 'อ' + 63: 0, # 'ฯ' + 22: 2, # 'ะ' + 10: 3, # 'ั' + 1: 3, # 'า' + 36: 3, # 'ำ' + 23: 2, # 'ิ' + 13: 3, # 'ี' + 40: 2, # 'ึ' + 27: 1, # 'ื' + 32: 3, # 'ุ' + 35: 1, # 'ู' + 11: 0, # 'เ' + 28: 1, # 'à¹' + 41: 0, # 'โ' + 29: 1, # 'ใ' + 33: 0, # 'ไ' + 50: 0, # 'ๆ' + 37: 1, # '็' + 6: 3, # '่' + 7: 3, # '้' + 38: 2, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 48: { # 'ธ' + 5: 0, # 'à¸' + 30: 0, # 'ข' + 24: 0, # 'ค' + 8: 1, # 'ง' + 26: 0, # 'จ' + 52: 0, # 'ฉ' + 34: 0, # 'ช' + 51: 0, # 'ซ' + 47: 0, # 'à¸' + 58: 0, # 'ฎ' + 57: 0, # 'à¸' + 49: 0, # 'à¸' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 0, # 'ณ' + 20: 0, # 'ด' + 19: 0, # 'ต' + 44: 0, # 'ถ' + 14: 0, # 'ท' + 48: 0, # 'ธ' + 3: 1, # 'น' + 17: 0, # 'บ' + 25: 0, # 'ป' + 39: 0, # 'ผ' + 62: 0, # 'à¸' + 31: 0, # 'พ' + 54: 0, # 'ฟ' + 45: 0, # 'ภ' + 9: 0, # 'ม' + 16: 0, # 'ย' + 2: 2, # 'ร' + 61: 0, # 'ฤ' + 15: 0, # 'ล' + 12: 0, # 'ว' + 42: 0, # 'ศ' + 46: 0, # 'ษ' + 18: 0, # 'ส' + 21: 0, # 'ห' + 4: 0, # 'อ' + 63: 0, # 'ฯ' + 22: 0, # 'ะ' + 10: 0, # 'ั' + 1: 2, # 'า' + 36: 0, # 'ำ' + 23: 3, # 'ิ' + 13: 3, # 'ี' + 40: 0, # 'ึ' + 27: 0, # 'ื' + 32: 2, # 'ุ' + 35: 0, # 'ู' + 11: 0, # 'เ' + 28: 0, # 'à¹' + 41: 0, # 'โ' + 29: 0, # 'ใ' + 33: 0, # 'ไ' + 50: 0, # 'ๆ' + 37: 0, # '็' + 6: 0, # '่' + 7: 0, # '้' + 38: 3, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 3: { # 'น' + 5: 3, # 'à¸' + 30: 2, # 'ข' + 24: 3, # 'ค' + 8: 1, # 'ง' + 26: 2, # 'จ' + 52: 0, # 'ฉ' + 34: 1, # 'ช' + 51: 1, # 'ซ' + 47: 0, # 'à¸' + 58: 0, # 'ฎ' + 57: 0, # 'à¸' + 49: 1, # 'à¸' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 0, # 'ณ' + 20: 3, # 'ด' + 19: 3, # 'ต' + 44: 2, # 'ถ' + 14: 3, # 'ท' + 48: 3, # 'ธ' + 3: 2, # 'น' + 17: 2, # 'บ' + 25: 2, # 'ป' + 39: 2, # 'ผ' + 62: 0, # 'à¸' + 31: 2, # 'พ' + 54: 1, # 'ฟ' + 45: 1, # 'ภ' + 9: 2, # 'ม' + 16: 2, # 'ย' + 2: 2, # 'ร' + 61: 1, # 'ฤ' + 15: 2, # 'ล' + 12: 3, # 'ว' + 42: 1, # 'ศ' + 46: 0, # 'ษ' + 18: 2, # 'ส' + 21: 2, # 'ห' + 4: 3, # 'อ' + 63: 1, # 'ฯ' + 22: 2, # 'ะ' + 10: 3, # 'ั' + 1: 3, # 'า' + 36: 3, # 'ำ' + 23: 3, # 'ิ' + 13: 3, # 'ี' + 40: 3, # 'ึ' + 27: 3, # 'ื' + 32: 3, # 'ุ' + 35: 2, # 'ู' + 11: 3, # 'เ' + 28: 2, # 'à¹' + 41: 3, # 'โ' + 29: 3, # 'ใ' + 33: 3, # 'ไ' + 50: 2, # 'ๆ' + 37: 1, # '็' + 6: 3, # '่' + 7: 3, # '้' + 38: 2, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 17: { # 'บ' + 5: 3, # 'à¸' + 30: 2, # 'ข' + 24: 2, # 'ค' + 8: 1, # 'ง' + 26: 1, # 'จ' + 52: 1, # 'ฉ' + 34: 1, # 'ช' + 51: 1, # 'ซ' + 47: 0, # 'à¸' + 58: 0, # 'ฎ' + 57: 0, # 'à¸' + 49: 0, # 'à¸' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 0, # 'ณ' + 20: 1, # 'ด' + 19: 2, # 'ต' + 44: 1, # 'ถ' + 14: 3, # 'ท' + 48: 0, # 'ธ' + 3: 3, # 'น' + 17: 3, # 'บ' + 25: 2, # 'ป' + 39: 2, # 'ผ' + 62: 0, # 'à¸' + 31: 1, # 'พ' + 54: 1, # 'ฟ' + 45: 1, # 'ภ' + 9: 1, # 'ม' + 16: 0, # 'ย' + 2: 3, # 'ร' + 61: 0, # 'ฤ' + 15: 2, # 'ล' + 12: 3, # 'ว' + 42: 0, # 'ศ' + 46: 0, # 'ษ' + 18: 2, # 'ส' + 21: 2, # 'ห' + 4: 2, # 'อ' + 63: 1, # 'ฯ' + 22: 0, # 'ะ' + 10: 3, # 'ั' + 1: 3, # 'า' + 36: 2, # 'ำ' + 23: 2, # 'ิ' + 13: 2, # 'ี' + 40: 0, # 'ึ' + 27: 2, # 'ื' + 32: 3, # 'ุ' + 35: 2, # 'ู' + 11: 2, # 'เ' + 28: 2, # 'à¹' + 41: 1, # 'โ' + 29: 2, # 'ใ' + 33: 2, # 'ไ' + 50: 0, # 'ๆ' + 37: 1, # '็' + 6: 2, # '่' + 7: 2, # '้' + 38: 0, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 25: { # 'ป' + 5: 2, # 'à¸' + 30: 0, # 'ข' + 24: 1, # 'ค' + 8: 0, # 'ง' + 26: 1, # 'จ' + 52: 0, # 'ฉ' + 34: 0, # 'ช' + 51: 1, # 'ซ' + 47: 0, # 'à¸' + 58: 1, # 'ฎ' + 57: 3, # 'à¸' + 49: 1, # 'à¸' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 0, # 'ณ' + 20: 1, # 'ด' + 19: 1, # 'ต' + 44: 1, # 'ถ' + 14: 1, # 'ท' + 48: 0, # 'ธ' + 3: 2, # 'น' + 17: 0, # 'บ' + 25: 1, # 'ป' + 39: 1, # 'ผ' + 62: 1, # 'à¸' + 31: 1, # 'พ' + 54: 0, # 'ฟ' + 45: 0, # 'ภ' + 9: 1, # 'ม' + 16: 0, # 'ย' + 2: 3, # 'ร' + 61: 0, # 'ฤ' + 15: 3, # 'ล' + 12: 1, # 'ว' + 42: 0, # 'ศ' + 46: 1, # 'ษ' + 18: 2, # 'ส' + 21: 1, # 'ห' + 4: 2, # 'อ' + 63: 0, # 'ฯ' + 22: 1, # 'ะ' + 10: 3, # 'ั' + 1: 1, # 'า' + 36: 0, # 'ำ' + 23: 2, # 'ิ' + 13: 3, # 'ี' + 40: 0, # 'ึ' + 27: 0, # 'ื' + 32: 1, # 'ุ' + 35: 0, # 'ู' + 11: 1, # 'เ' + 28: 2, # 'à¹' + 41: 0, # 'โ' + 29: 1, # 'ใ' + 33: 2, # 'ไ' + 50: 0, # 'ๆ' + 37: 3, # '็' + 6: 1, # '่' + 7: 2, # '้' + 38: 1, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 39: { # 'ผ' + 5: 1, # 'à¸' + 30: 0, # 'ข' + 24: 0, # 'ค' + 8: 1, # 'ง' + 26: 0, # 'จ' + 52: 0, # 'ฉ' + 34: 0, # 'ช' + 51: 0, # 'ซ' + 47: 0, # 'à¸' + 58: 0, # 'ฎ' + 57: 0, # 'à¸' + 49: 0, # 'à¸' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 0, # 'ณ' + 20: 0, # 'ด' + 19: 0, # 'ต' + 44: 0, # 'ถ' + 14: 0, # 'ท' + 48: 0, # 'ธ' + 3: 2, # 'น' + 17: 0, # 'บ' + 25: 0, # 'ป' + 39: 0, # 'ผ' + 62: 0, # 'à¸' + 31: 0, # 'พ' + 54: 0, # 'ฟ' + 45: 0, # 'ภ' + 9: 1, # 'ม' + 16: 2, # 'ย' + 2: 0, # 'ร' + 61: 0, # 'ฤ' + 15: 3, # 'ล' + 12: 0, # 'ว' + 42: 0, # 'ศ' + 46: 0, # 'ษ' + 18: 1, # 'ส' + 21: 0, # 'ห' + 4: 0, # 'อ' + 63: 0, # 'ฯ' + 22: 1, # 'ะ' + 10: 1, # 'ั' + 1: 0, # 'า' + 36: 0, # 'ำ' + 23: 2, # 'ิ' + 13: 0, # 'ี' + 40: 0, # 'ึ' + 27: 1, # 'ื' + 32: 0, # 'ุ' + 35: 3, # 'ู' + 11: 0, # 'เ' + 28: 0, # 'à¹' + 41: 0, # 'โ' + 29: 0, # 'ใ' + 33: 0, # 'ไ' + 50: 0, # 'ๆ' + 37: 0, # '็' + 6: 3, # '่' + 7: 1, # '้' + 38: 0, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 62: { # 'à¸' + 5: 0, # 'à¸' + 30: 0, # 'ข' + 24: 0, # 'ค' + 8: 0, # 'ง' + 26: 0, # 'จ' + 52: 0, # 'ฉ' + 34: 0, # 'ช' + 51: 0, # 'ซ' + 47: 0, # 'à¸' + 58: 0, # 'ฎ' + 57: 0, # 'à¸' + 49: 0, # 'à¸' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 0, # 'ณ' + 20: 0, # 'ด' + 19: 0, # 'ต' + 44: 0, # 'ถ' + 14: 0, # 'ท' + 48: 0, # 'ธ' + 3: 1, # 'น' + 17: 0, # 'บ' + 25: 0, # 'ป' + 39: 0, # 'ผ' + 62: 0, # 'à¸' + 31: 0, # 'พ' + 54: 0, # 'ฟ' + 45: 0, # 'ภ' + 9: 0, # 'ม' + 16: 0, # 'ย' + 2: 1, # 'ร' + 61: 0, # 'ฤ' + 15: 0, # 'ล' + 12: 0, # 'ว' + 42: 0, # 'ศ' + 46: 0, # 'ษ' + 18: 0, # 'ส' + 21: 0, # 'ห' + 4: 0, # 'อ' + 63: 0, # 'ฯ' + 22: 0, # 'ะ' + 10: 1, # 'ั' + 1: 0, # 'า' + 36: 0, # 'ำ' + 23: 0, # 'ิ' + 13: 1, # 'ี' + 40: 2, # 'ึ' + 27: 0, # 'ื' + 32: 0, # 'ุ' + 35: 0, # 'ู' + 11: 0, # 'เ' + 28: 0, # 'à¹' + 41: 0, # 'โ' + 29: 0, # 'ใ' + 33: 0, # 'ไ' + 50: 0, # 'ๆ' + 37: 0, # '็' + 6: 2, # '่' + 7: 1, # '้' + 38: 0, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 31: { # 'พ' + 5: 1, # 'à¸' + 30: 1, # 'ข' + 24: 1, # 'ค' + 8: 1, # 'ง' + 26: 1, # 'จ' + 52: 0, # 'ฉ' + 34: 0, # 'ช' + 51: 0, # 'ซ' + 47: 0, # 'à¸' + 58: 0, # 'ฎ' + 57: 0, # 'à¸' + 49: 0, # 'à¸' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 1, # 'ณ' + 20: 1, # 'ด' + 19: 1, # 'ต' + 44: 0, # 'ถ' + 14: 2, # 'ท' + 48: 1, # 'ธ' + 3: 3, # 'น' + 17: 2, # 'บ' + 25: 0, # 'ป' + 39: 1, # 'ผ' + 62: 0, # 'à¸' + 31: 1, # 'พ' + 54: 0, # 'ฟ' + 45: 0, # 'ภ' + 9: 1, # 'ม' + 16: 2, # 'ย' + 2: 3, # 'ร' + 61: 2, # 'ฤ' + 15: 2, # 'ล' + 12: 2, # 'ว' + 42: 0, # 'ศ' + 46: 0, # 'ษ' + 18: 1, # 'ส' + 21: 1, # 'ห' + 4: 2, # 'อ' + 63: 1, # 'ฯ' + 22: 0, # 'ะ' + 10: 3, # 'ั' + 1: 3, # 'า' + 36: 0, # 'ำ' + 23: 3, # 'ิ' + 13: 2, # 'ี' + 40: 1, # 'ึ' + 27: 3, # 'ื' + 32: 1, # 'ุ' + 35: 2, # 'ู' + 11: 1, # 'เ' + 28: 1, # 'à¹' + 41: 0, # 'โ' + 29: 1, # 'ใ' + 33: 1, # 'ไ' + 50: 0, # 'ๆ' + 37: 1, # '็' + 6: 0, # '่' + 7: 1, # '้' + 38: 3, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 54: { # 'ฟ' + 5: 0, # 'à¸' + 30: 0, # 'ข' + 24: 0, # 'ค' + 8: 0, # 'ง' + 26: 0, # 'จ' + 52: 0, # 'ฉ' + 34: 1, # 'ช' + 51: 0, # 'ซ' + 47: 0, # 'à¸' + 58: 0, # 'ฎ' + 57: 0, # 'à¸' + 49: 0, # 'à¸' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 0, # 'ณ' + 20: 0, # 'ด' + 19: 1, # 'ต' + 44: 0, # 'ถ' + 14: 1, # 'ท' + 48: 0, # 'ธ' + 3: 0, # 'น' + 17: 0, # 'บ' + 25: 0, # 'ป' + 39: 0, # 'ผ' + 62: 0, # 'à¸' + 31: 0, # 'พ' + 54: 2, # 'ฟ' + 45: 0, # 'ภ' + 9: 0, # 'ม' + 16: 0, # 'ย' + 2: 1, # 'ร' + 61: 0, # 'ฤ' + 15: 2, # 'ล' + 12: 0, # 'ว' + 42: 0, # 'ศ' + 46: 0, # 'ษ' + 18: 1, # 'ส' + 21: 0, # 'ห' + 4: 1, # 'อ' + 63: 0, # 'ฯ' + 22: 0, # 'ะ' + 10: 2, # 'ั' + 1: 0, # 'า' + 36: 0, # 'ำ' + 23: 1, # 'ิ' + 13: 1, # 'ี' + 40: 0, # 'ึ' + 27: 1, # 'ื' + 32: 1, # 'ุ' + 35: 0, # 'ู' + 11: 0, # 'เ' + 28: 1, # 'à¹' + 41: 0, # 'โ' + 29: 0, # 'ใ' + 33: 0, # 'ไ' + 50: 0, # 'ๆ' + 37: 0, # '็' + 6: 0, # '่' + 7: 2, # '้' + 38: 0, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 45: { # 'ภ' + 5: 0, # 'à¸' + 30: 0, # 'ข' + 24: 1, # 'ค' + 8: 0, # 'ง' + 26: 0, # 'จ' + 52: 0, # 'ฉ' + 34: 0, # 'ช' + 51: 0, # 'ซ' + 47: 0, # 'à¸' + 58: 0, # 'ฎ' + 57: 0, # 'à¸' + 49: 0, # 'à¸' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 0, # 'ณ' + 20: 0, # 'ด' + 19: 0, # 'ต' + 44: 0, # 'ถ' + 14: 3, # 'ท' + 48: 0, # 'ธ' + 3: 0, # 'น' + 17: 0, # 'บ' + 25: 0, # 'ป' + 39: 0, # 'ผ' + 62: 0, # 'à¸' + 31: 1, # 'พ' + 54: 0, # 'ฟ' + 45: 0, # 'ภ' + 9: 0, # 'ม' + 16: 0, # 'ย' + 2: 1, # 'ร' + 61: 0, # 'ฤ' + 15: 0, # 'ล' + 12: 0, # 'ว' + 42: 0, # 'ศ' + 46: 0, # 'ษ' + 18: 0, # 'ส' + 21: 0, # 'ห' + 4: 0, # 'อ' + 63: 0, # 'ฯ' + 22: 0, # 'ะ' + 10: 3, # 'ั' + 1: 3, # 'า' + 36: 0, # 'ำ' + 23: 1, # 'ิ' + 13: 0, # 'ี' + 40: 0, # 'ึ' + 27: 0, # 'ื' + 32: 0, # 'ุ' + 35: 2, # 'ู' + 11: 0, # 'เ' + 28: 0, # 'à¹' + 41: 0, # 'โ' + 29: 0, # 'ใ' + 33: 0, # 'ไ' + 50: 0, # 'ๆ' + 37: 0, # '็' + 6: 0, # '่' + 7: 0, # '้' + 38: 1, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 9: { # 'ม' + 5: 2, # 'à¸' + 30: 2, # 'ข' + 24: 2, # 'ค' + 8: 2, # 'ง' + 26: 2, # 'จ' + 52: 0, # 'ฉ' + 34: 1, # 'ช' + 51: 1, # 'ซ' + 47: 0, # 'à¸' + 58: 0, # 'ฎ' + 57: 0, # 'à¸' + 49: 0, # 'à¸' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 1, # 'ณ' + 20: 2, # 'ด' + 19: 2, # 'ต' + 44: 1, # 'ถ' + 14: 2, # 'ท' + 48: 1, # 'ธ' + 3: 3, # 'น' + 17: 2, # 'บ' + 25: 2, # 'ป' + 39: 1, # 'ผ' + 62: 0, # 'à¸' + 31: 3, # 'พ' + 54: 0, # 'ฟ' + 45: 1, # 'ภ' + 9: 2, # 'ม' + 16: 1, # 'ย' + 2: 2, # 'ร' + 61: 2, # 'ฤ' + 15: 2, # 'ล' + 12: 2, # 'ว' + 42: 1, # 'ศ' + 46: 1, # 'ษ' + 18: 3, # 'ส' + 21: 3, # 'ห' + 4: 3, # 'อ' + 63: 0, # 'ฯ' + 22: 1, # 'ะ' + 10: 3, # 'ั' + 1: 3, # 'า' + 36: 0, # 'ำ' + 23: 3, # 'ิ' + 13: 3, # 'ี' + 40: 0, # 'ึ' + 27: 3, # 'ื' + 32: 3, # 'ุ' + 35: 3, # 'ู' + 11: 2, # 'เ' + 28: 2, # 'à¹' + 41: 2, # 'โ' + 29: 2, # 'ใ' + 33: 2, # 'ไ' + 50: 1, # 'ๆ' + 37: 1, # '็' + 6: 3, # '่' + 7: 2, # '้' + 38: 1, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 16: { # 'ย' + 5: 3, # 'à¸' + 30: 1, # 'ข' + 24: 2, # 'ค' + 8: 3, # 'ง' + 26: 2, # 'จ' + 52: 0, # 'ฉ' + 34: 2, # 'ช' + 51: 0, # 'ซ' + 47: 2, # 'à¸' + 58: 0, # 'ฎ' + 57: 0, # 'à¸' + 49: 0, # 'à¸' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 0, # 'ณ' + 20: 2, # 'ด' + 19: 2, # 'ต' + 44: 1, # 'ถ' + 14: 2, # 'ท' + 48: 1, # 'ธ' + 3: 3, # 'น' + 17: 3, # 'บ' + 25: 1, # 'ป' + 39: 1, # 'ผ' + 62: 0, # 'à¸' + 31: 1, # 'พ' + 54: 0, # 'ฟ' + 45: 1, # 'ภ' + 9: 2, # 'ม' + 16: 0, # 'ย' + 2: 2, # 'ร' + 61: 0, # 'ฤ' + 15: 1, # 'ล' + 12: 3, # 'ว' + 42: 1, # 'ศ' + 46: 0, # 'ษ' + 18: 2, # 'ส' + 21: 1, # 'ห' + 4: 2, # 'อ' + 63: 0, # 'ฯ' + 22: 2, # 'ะ' + 10: 3, # 'ั' + 1: 3, # 'า' + 36: 0, # 'ำ' + 23: 2, # 'ิ' + 13: 3, # 'ี' + 40: 1, # 'ึ' + 27: 2, # 'ื' + 32: 2, # 'ุ' + 35: 3, # 'ู' + 11: 2, # 'เ' + 28: 1, # 'à¹' + 41: 1, # 'โ' + 29: 2, # 'ใ' + 33: 2, # 'ไ' + 50: 2, # 'ๆ' + 37: 1, # '็' + 6: 3, # '่' + 7: 2, # '้' + 38: 3, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 2: { # 'ร' + 5: 3, # 'à¸' + 30: 2, # 'ข' + 24: 2, # 'ค' + 8: 3, # 'ง' + 26: 2, # 'จ' + 52: 0, # 'ฉ' + 34: 2, # 'ช' + 51: 1, # 'ซ' + 47: 0, # 'à¸' + 58: 0, # 'ฎ' + 57: 0, # 'à¸' + 49: 3, # 'à¸' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 3, # 'ณ' + 20: 2, # 'ด' + 19: 2, # 'ต' + 44: 3, # 'ถ' + 14: 3, # 'ท' + 48: 1, # 'ธ' + 3: 2, # 'น' + 17: 2, # 'บ' + 25: 3, # 'ป' + 39: 2, # 'ผ' + 62: 1, # 'à¸' + 31: 2, # 'พ' + 54: 1, # 'ฟ' + 45: 1, # 'ภ' + 9: 3, # 'ม' + 16: 2, # 'ย' + 2: 3, # 'ร' + 61: 0, # 'ฤ' + 15: 2, # 'ล' + 12: 3, # 'ว' + 42: 2, # 'ศ' + 46: 2, # 'ษ' + 18: 2, # 'ส' + 21: 2, # 'ห' + 4: 3, # 'อ' + 63: 1, # 'ฯ' + 22: 3, # 'ะ' + 10: 3, # 'ั' + 1: 3, # 'า' + 36: 0, # 'ำ' + 23: 3, # 'ิ' + 13: 3, # 'ี' + 40: 2, # 'ึ' + 27: 3, # 'ื' + 32: 3, # 'ุ' + 35: 3, # 'ู' + 11: 3, # 'เ' + 28: 3, # 'à¹' + 41: 1, # 'โ' + 29: 2, # 'ใ' + 33: 1, # 'ไ' + 50: 0, # 'ๆ' + 37: 3, # '็' + 6: 3, # '่' + 7: 3, # '้' + 38: 3, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 61: { # 'ฤ' + 5: 0, # 'à¸' + 30: 0, # 'ข' + 24: 0, # 'ค' + 8: 0, # 'ง' + 26: 0, # 'จ' + 52: 0, # 'ฉ' + 34: 0, # 'ช' + 51: 0, # 'ซ' + 47: 0, # 'à¸' + 58: 0, # 'ฎ' + 57: 0, # 'à¸' + 49: 0, # 'à¸' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 0, # 'ณ' + 20: 0, # 'ด' + 19: 2, # 'ต' + 44: 0, # 'ถ' + 14: 2, # 'ท' + 48: 0, # 'ธ' + 3: 0, # 'น' + 17: 0, # 'บ' + 25: 0, # 'ป' + 39: 0, # 'ผ' + 62: 0, # 'à¸' + 31: 0, # 'พ' + 54: 0, # 'ฟ' + 45: 0, # 'ภ' + 9: 1, # 'ม' + 16: 0, # 'ย' + 2: 0, # 'ร' + 61: 0, # 'ฤ' + 15: 0, # 'ล' + 12: 0, # 'ว' + 42: 0, # 'ศ' + 46: 2, # 'ษ' + 18: 0, # 'ส' + 21: 0, # 'ห' + 4: 0, # 'อ' + 63: 0, # 'ฯ' + 22: 0, # 'ะ' + 10: 0, # 'ั' + 1: 0, # 'า' + 36: 0, # 'ำ' + 23: 0, # 'ิ' + 13: 0, # 'ี' + 40: 0, # 'ึ' + 27: 0, # 'ื' + 32: 0, # 'ุ' + 35: 0, # 'ู' + 11: 0, # 'เ' + 28: 0, # 'à¹' + 41: 0, # 'โ' + 29: 0, # 'ใ' + 33: 0, # 'ไ' + 50: 0, # 'ๆ' + 37: 0, # '็' + 6: 0, # '่' + 7: 0, # '้' + 38: 0, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 15: { # 'ล' + 5: 2, # 'à¸' + 30: 3, # 'ข' + 24: 1, # 'ค' + 8: 3, # 'ง' + 26: 1, # 'จ' + 52: 0, # 'ฉ' + 34: 1, # 'ช' + 51: 0, # 'ซ' + 47: 0, # 'à¸' + 58: 0, # 'ฎ' + 57: 0, # 'à¸' + 49: 0, # 'à¸' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 0, # 'ณ' + 20: 2, # 'ด' + 19: 2, # 'ต' + 44: 1, # 'ถ' + 14: 2, # 'ท' + 48: 0, # 'ธ' + 3: 1, # 'น' + 17: 2, # 'บ' + 25: 2, # 'ป' + 39: 1, # 'ผ' + 62: 0, # 'à¸' + 31: 0, # 'พ' + 54: 0, # 'ฟ' + 45: 1, # 'ภ' + 9: 1, # 'ม' + 16: 3, # 'ย' + 2: 1, # 'ร' + 61: 0, # 'ฤ' + 15: 1, # 'ล' + 12: 1, # 'ว' + 42: 0, # 'ศ' + 46: 0, # 'ษ' + 18: 2, # 'ส' + 21: 1, # 'ห' + 4: 3, # 'อ' + 63: 2, # 'ฯ' + 22: 3, # 'ะ' + 10: 3, # 'ั' + 1: 3, # 'า' + 36: 2, # 'ำ' + 23: 3, # 'ิ' + 13: 3, # 'ี' + 40: 2, # 'ึ' + 27: 3, # 'ื' + 32: 2, # 'ุ' + 35: 3, # 'ู' + 11: 2, # 'เ' + 28: 1, # 'à¹' + 41: 1, # 'โ' + 29: 2, # 'ใ' + 33: 1, # 'ไ' + 50: 0, # 'ๆ' + 37: 2, # '็' + 6: 3, # '่' + 7: 3, # '้' + 38: 2, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 12: { # 'ว' + 5: 3, # 'à¸' + 30: 2, # 'ข' + 24: 1, # 'ค' + 8: 3, # 'ง' + 26: 2, # 'จ' + 52: 0, # 'ฉ' + 34: 1, # 'ช' + 51: 1, # 'ซ' + 47: 0, # 'à¸' + 58: 0, # 'ฎ' + 57: 0, # 'à¸' + 49: 0, # 'à¸' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 1, # 'ณ' + 20: 2, # 'ด' + 19: 1, # 'ต' + 44: 1, # 'ถ' + 14: 1, # 'ท' + 48: 0, # 'ธ' + 3: 3, # 'น' + 17: 2, # 'บ' + 25: 1, # 'ป' + 39: 1, # 'ผ' + 62: 0, # 'à¸' + 31: 1, # 'พ' + 54: 1, # 'ฟ' + 45: 0, # 'ภ' + 9: 3, # 'ม' + 16: 3, # 'ย' + 2: 3, # 'ร' + 61: 0, # 'ฤ' + 15: 3, # 'ล' + 12: 1, # 'ว' + 42: 0, # 'ศ' + 46: 0, # 'ษ' + 18: 2, # 'ส' + 21: 2, # 'ห' + 4: 2, # 'อ' + 63: 0, # 'ฯ' + 22: 2, # 'ะ' + 10: 3, # 'ั' + 1: 3, # 'า' + 36: 0, # 'ำ' + 23: 3, # 'ิ' + 13: 2, # 'ี' + 40: 0, # 'ึ' + 27: 0, # 'ื' + 32: 2, # 'ุ' + 35: 0, # 'ู' + 11: 3, # 'เ' + 28: 2, # 'à¹' + 41: 1, # 'โ' + 29: 1, # 'ใ' + 33: 2, # 'ไ' + 50: 1, # 'ๆ' + 37: 0, # '็' + 6: 3, # '่' + 7: 3, # '้' + 38: 1, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 42: { # 'ศ' + 5: 1, # 'à¸' + 30: 0, # 'ข' + 24: 1, # 'ค' + 8: 0, # 'ง' + 26: 1, # 'จ' + 52: 0, # 'ฉ' + 34: 0, # 'ช' + 51: 0, # 'ซ' + 47: 1, # 'à¸' + 58: 0, # 'ฎ' + 57: 0, # 'à¸' + 49: 0, # 'à¸' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 0, # 'ณ' + 20: 0, # 'ด' + 19: 1, # 'ต' + 44: 0, # 'ถ' + 14: 1, # 'ท' + 48: 0, # 'ธ' + 3: 2, # 'น' + 17: 0, # 'บ' + 25: 0, # 'ป' + 39: 0, # 'ผ' + 62: 0, # 'à¸' + 31: 0, # 'พ' + 54: 0, # 'ฟ' + 45: 0, # 'ภ' + 9: 0, # 'ม' + 16: 0, # 'ย' + 2: 2, # 'ร' + 61: 0, # 'ฤ' + 15: 0, # 'ล' + 12: 2, # 'ว' + 42: 1, # 'ศ' + 46: 2, # 'ษ' + 18: 1, # 'ส' + 21: 0, # 'ห' + 4: 0, # 'อ' + 63: 0, # 'ฯ' + 22: 0, # 'ะ' + 10: 2, # 'ั' + 1: 3, # 'า' + 36: 0, # 'ำ' + 23: 2, # 'ิ' + 13: 0, # 'ี' + 40: 3, # 'ึ' + 27: 0, # 'ื' + 32: 0, # 'ุ' + 35: 2, # 'ู' + 11: 0, # 'เ' + 28: 1, # 'à¹' + 41: 0, # 'โ' + 29: 1, # 'ใ' + 33: 1, # 'ไ' + 50: 0, # 'ๆ' + 37: 0, # '็' + 6: 0, # '่' + 7: 0, # '้' + 38: 1, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 46: { # 'ษ' + 5: 0, # 'à¸' + 30: 0, # 'ข' + 24: 0, # 'ค' + 8: 0, # 'ง' + 26: 0, # 'จ' + 52: 0, # 'ฉ' + 34: 0, # 'ช' + 51: 0, # 'ซ' + 47: 0, # 'à¸' + 58: 2, # 'ฎ' + 57: 1, # 'à¸' + 49: 2, # 'à¸' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 3, # 'ณ' + 20: 0, # 'ด' + 19: 1, # 'ต' + 44: 0, # 'ถ' + 14: 1, # 'ท' + 48: 0, # 'ธ' + 3: 0, # 'น' + 17: 0, # 'บ' + 25: 0, # 'ป' + 39: 0, # 'ผ' + 62: 0, # 'à¸' + 31: 0, # 'พ' + 54: 0, # 'ฟ' + 45: 1, # 'ภ' + 9: 1, # 'ม' + 16: 2, # 'ย' + 2: 2, # 'ร' + 61: 0, # 'ฤ' + 15: 0, # 'ล' + 12: 0, # 'ว' + 42: 1, # 'ศ' + 46: 0, # 'ษ' + 18: 0, # 'ส' + 21: 0, # 'ห' + 4: 0, # 'อ' + 63: 0, # 'ฯ' + 22: 2, # 'ะ' + 10: 2, # 'ั' + 1: 3, # 'า' + 36: 0, # 'ำ' + 23: 0, # 'ิ' + 13: 1, # 'ี' + 40: 0, # 'ึ' + 27: 0, # 'ื' + 32: 0, # 'ุ' + 35: 0, # 'ู' + 11: 1, # 'เ' + 28: 0, # 'à¹' + 41: 0, # 'โ' + 29: 0, # 'ใ' + 33: 0, # 'ไ' + 50: 0, # 'ๆ' + 37: 0, # '็' + 6: 0, # '่' + 7: 0, # '้' + 38: 2, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 18: { # 'ส' + 5: 2, # 'à¸' + 30: 0, # 'ข' + 24: 0, # 'ค' + 8: 2, # 'ง' + 26: 1, # 'จ' + 52: 0, # 'ฉ' + 34: 0, # 'ช' + 51: 0, # 'ซ' + 47: 0, # 'à¸' + 58: 0, # 'ฎ' + 57: 0, # 'à¸' + 49: 0, # 'à¸' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 0, # 'ณ' + 20: 3, # 'ด' + 19: 3, # 'ต' + 44: 3, # 'ถ' + 14: 0, # 'ท' + 48: 0, # 'ธ' + 3: 3, # 'น' + 17: 2, # 'บ' + 25: 1, # 'ป' + 39: 0, # 'ผ' + 62: 0, # 'à¸' + 31: 0, # 'พ' + 54: 0, # 'ฟ' + 45: 2, # 'ภ' + 9: 3, # 'ม' + 16: 1, # 'ย' + 2: 3, # 'ร' + 61: 0, # 'ฤ' + 15: 1, # 'ล' + 12: 2, # 'ว' + 42: 0, # 'ศ' + 46: 0, # 'ษ' + 18: 0, # 'ส' + 21: 2, # 'ห' + 4: 3, # 'อ' + 63: 0, # 'ฯ' + 22: 2, # 'ะ' + 10: 3, # 'ั' + 1: 3, # 'า' + 36: 3, # 'ำ' + 23: 3, # 'ิ' + 13: 3, # 'ี' + 40: 2, # 'ึ' + 27: 3, # 'ื' + 32: 3, # 'ุ' + 35: 3, # 'ู' + 11: 2, # 'เ' + 28: 0, # 'à¹' + 41: 1, # 'โ' + 29: 0, # 'ใ' + 33: 1, # 'ไ' + 50: 0, # 'ๆ' + 37: 0, # '็' + 6: 3, # '่' + 7: 1, # '้' + 38: 2, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 21: { # 'ห' + 5: 3, # 'à¸' + 30: 0, # 'ข' + 24: 0, # 'ค' + 8: 1, # 'ง' + 26: 0, # 'จ' + 52: 0, # 'ฉ' + 34: 0, # 'ช' + 51: 0, # 'ซ' + 47: 2, # 'à¸' + 58: 0, # 'ฎ' + 57: 0, # 'à¸' + 49: 0, # 'à¸' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 0, # 'ณ' + 20: 1, # 'ด' + 19: 3, # 'ต' + 44: 0, # 'ถ' + 14: 0, # 'ท' + 48: 0, # 'ธ' + 3: 3, # 'น' + 17: 0, # 'บ' + 25: 1, # 'ป' + 39: 0, # 'ผ' + 62: 0, # 'à¸' + 31: 1, # 'พ' + 54: 0, # 'ฟ' + 45: 0, # 'ภ' + 9: 3, # 'ม' + 16: 2, # 'ย' + 2: 3, # 'ร' + 61: 0, # 'ฤ' + 15: 3, # 'ล' + 12: 2, # 'ว' + 42: 0, # 'ศ' + 46: 0, # 'ษ' + 18: 0, # 'ส' + 21: 0, # 'ห' + 4: 3, # 'อ' + 63: 0, # 'ฯ' + 22: 1, # 'ะ' + 10: 3, # 'ั' + 1: 3, # 'า' + 36: 0, # 'ำ' + 23: 1, # 'ิ' + 13: 1, # 'ี' + 40: 0, # 'ึ' + 27: 0, # 'ื' + 32: 1, # 'ุ' + 35: 1, # 'ู' + 11: 0, # 'เ' + 28: 0, # 'à¹' + 41: 0, # 'โ' + 29: 0, # 'ใ' + 33: 0, # 'ไ' + 50: 0, # 'ๆ' + 37: 3, # '็' + 6: 3, # '่' + 7: 3, # '้' + 38: 2, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 4: { # 'อ' + 5: 3, # 'à¸' + 30: 1, # 'ข' + 24: 2, # 'ค' + 8: 3, # 'ง' + 26: 1, # 'จ' + 52: 0, # 'ฉ' + 34: 1, # 'ช' + 51: 0, # 'ซ' + 47: 0, # 'à¸' + 58: 0, # 'ฎ' + 57: 0, # 'à¸' + 49: 0, # 'à¸' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 0, # 'ณ' + 20: 3, # 'ด' + 19: 2, # 'ต' + 44: 1, # 'ถ' + 14: 2, # 'ท' + 48: 1, # 'ธ' + 3: 3, # 'น' + 17: 3, # 'บ' + 25: 1, # 'ป' + 39: 1, # 'ผ' + 62: 0, # 'à¸' + 31: 1, # 'พ' + 54: 1, # 'ฟ' + 45: 1, # 'ภ' + 9: 3, # 'ม' + 16: 3, # 'ย' + 2: 3, # 'ร' + 61: 0, # 'ฤ' + 15: 2, # 'ล' + 12: 2, # 'ว' + 42: 1, # 'ศ' + 46: 0, # 'ษ' + 18: 2, # 'ส' + 21: 2, # 'ห' + 4: 3, # 'อ' + 63: 0, # 'ฯ' + 22: 2, # 'ะ' + 10: 3, # 'ั' + 1: 3, # 'า' + 36: 2, # 'ำ' + 23: 2, # 'ิ' + 13: 3, # 'ี' + 40: 0, # 'ึ' + 27: 3, # 'ื' + 32: 3, # 'ุ' + 35: 0, # 'ู' + 11: 3, # 'เ' + 28: 1, # 'à¹' + 41: 1, # 'โ' + 29: 2, # 'ใ' + 33: 2, # 'ไ' + 50: 1, # 'ๆ' + 37: 1, # '็' + 6: 2, # '่' + 7: 2, # '้' + 38: 0, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 63: { # 'ฯ' + 5: 0, # 'à¸' + 30: 0, # 'ข' + 24: 0, # 'ค' + 8: 0, # 'ง' + 26: 0, # 'จ' + 52: 0, # 'ฉ' + 34: 0, # 'ช' + 51: 0, # 'ซ' + 47: 0, # 'à¸' + 58: 0, # 'ฎ' + 57: 0, # 'à¸' + 49: 0, # 'à¸' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 0, # 'ณ' + 20: 0, # 'ด' + 19: 0, # 'ต' + 44: 0, # 'ถ' + 14: 0, # 'ท' + 48: 0, # 'ธ' + 3: 0, # 'น' + 17: 0, # 'บ' + 25: 0, # 'ป' + 39: 0, # 'ผ' + 62: 0, # 'à¸' + 31: 0, # 'พ' + 54: 0, # 'ฟ' + 45: 0, # 'ภ' + 9: 0, # 'ม' + 16: 0, # 'ย' + 2: 0, # 'ร' + 61: 0, # 'ฤ' + 15: 2, # 'ล' + 12: 0, # 'ว' + 42: 0, # 'ศ' + 46: 0, # 'ษ' + 18: 0, # 'ส' + 21: 0, # 'ห' + 4: 0, # 'อ' + 63: 0, # 'ฯ' + 22: 0, # 'ะ' + 10: 0, # 'ั' + 1: 0, # 'า' + 36: 0, # 'ำ' + 23: 0, # 'ิ' + 13: 0, # 'ี' + 40: 0, # 'ึ' + 27: 0, # 'ื' + 32: 0, # 'ุ' + 35: 0, # 'ู' + 11: 0, # 'เ' + 28: 0, # 'à¹' + 41: 0, # 'โ' + 29: 0, # 'ใ' + 33: 0, # 'ไ' + 50: 0, # 'ๆ' + 37: 0, # '็' + 6: 0, # '่' + 7: 0, # '้' + 38: 0, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 22: { # 'ะ' + 5: 3, # 'à¸' + 30: 1, # 'ข' + 24: 2, # 'ค' + 8: 1, # 'ง' + 26: 2, # 'จ' + 52: 0, # 'ฉ' + 34: 3, # 'ช' + 51: 0, # 'ซ' + 47: 0, # 'à¸' + 58: 0, # 'ฎ' + 57: 0, # 'à¸' + 49: 0, # 'à¸' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 0, # 'ณ' + 20: 3, # 'ด' + 19: 3, # 'ต' + 44: 1, # 'ถ' + 14: 3, # 'ท' + 48: 1, # 'ธ' + 3: 2, # 'น' + 17: 3, # 'บ' + 25: 2, # 'ป' + 39: 1, # 'ผ' + 62: 0, # 'à¸' + 31: 2, # 'พ' + 54: 0, # 'ฟ' + 45: 1, # 'ภ' + 9: 3, # 'ม' + 16: 2, # 'ย' + 2: 2, # 'ร' + 61: 0, # 'ฤ' + 15: 2, # 'ล' + 12: 2, # 'ว' + 42: 0, # 'ศ' + 46: 0, # 'ษ' + 18: 3, # 'ส' + 21: 3, # 'ห' + 4: 2, # 'อ' + 63: 1, # 'ฯ' + 22: 1, # 'ะ' + 10: 0, # 'ั' + 1: 0, # 'า' + 36: 0, # 'ำ' + 23: 0, # 'ิ' + 13: 0, # 'ี' + 40: 0, # 'ึ' + 27: 0, # 'ื' + 32: 0, # 'ุ' + 35: 0, # 'ู' + 11: 3, # 'เ' + 28: 2, # 'à¹' + 41: 1, # 'โ' + 29: 2, # 'ใ' + 33: 2, # 'ไ' + 50: 0, # 'ๆ' + 37: 0, # '็' + 6: 0, # '่' + 7: 0, # '้' + 38: 0, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 10: { # 'ั' + 5: 3, # 'à¸' + 30: 0, # 'ข' + 24: 1, # 'ค' + 8: 3, # 'ง' + 26: 3, # 'จ' + 52: 0, # 'ฉ' + 34: 1, # 'ช' + 51: 0, # 'ซ' + 47: 3, # 'à¸' + 58: 0, # 'ฎ' + 57: 0, # 'à¸' + 49: 2, # 'à¸' + 53: 0, # 'ฑ' + 55: 3, # 'ฒ' + 43: 3, # 'ณ' + 20: 3, # 'ด' + 19: 3, # 'ต' + 44: 0, # 'ถ' + 14: 2, # 'ท' + 48: 0, # 'ธ' + 3: 3, # 'น' + 17: 3, # 'บ' + 25: 1, # 'ป' + 39: 0, # 'ผ' + 62: 0, # 'à¸' + 31: 2, # 'พ' + 54: 0, # 'ฟ' + 45: 0, # 'ภ' + 9: 3, # 'ม' + 16: 3, # 'ย' + 2: 0, # 'ร' + 61: 0, # 'ฤ' + 15: 2, # 'ล' + 12: 3, # 'ว' + 42: 2, # 'ศ' + 46: 0, # 'ษ' + 18: 3, # 'ส' + 21: 0, # 'ห' + 4: 0, # 'อ' + 63: 0, # 'ฯ' + 22: 0, # 'ะ' + 10: 0, # 'ั' + 1: 0, # 'า' + 36: 0, # 'ำ' + 23: 0, # 'ิ' + 13: 0, # 'ี' + 40: 0, # 'ึ' + 27: 0, # 'ื' + 32: 0, # 'ุ' + 35: 0, # 'ู' + 11: 0, # 'เ' + 28: 0, # 'à¹' + 41: 0, # 'โ' + 29: 0, # 'ใ' + 33: 0, # 'ไ' + 50: 0, # 'ๆ' + 37: 0, # '็' + 6: 3, # '่' + 7: 3, # '้' + 38: 0, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 1: { # 'า' + 5: 3, # 'à¸' + 30: 2, # 'ข' + 24: 3, # 'ค' + 8: 3, # 'ง' + 26: 3, # 'จ' + 52: 0, # 'ฉ' + 34: 3, # 'ช' + 51: 1, # 'ซ' + 47: 2, # 'à¸' + 58: 0, # 'ฎ' + 57: 0, # 'à¸' + 49: 0, # 'à¸' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 3, # 'ณ' + 20: 3, # 'ด' + 19: 3, # 'ต' + 44: 1, # 'ถ' + 14: 3, # 'ท' + 48: 2, # 'ธ' + 3: 3, # 'น' + 17: 3, # 'บ' + 25: 2, # 'ป' + 39: 1, # 'ผ' + 62: 1, # 'à¸' + 31: 3, # 'พ' + 54: 1, # 'ฟ' + 45: 1, # 'ภ' + 9: 3, # 'ม' + 16: 3, # 'ย' + 2: 3, # 'ร' + 61: 0, # 'ฤ' + 15: 3, # 'ล' + 12: 3, # 'ว' + 42: 2, # 'ศ' + 46: 3, # 'ษ' + 18: 3, # 'ส' + 21: 3, # 'ห' + 4: 2, # 'อ' + 63: 1, # 'ฯ' + 22: 3, # 'ะ' + 10: 0, # 'ั' + 1: 0, # 'า' + 36: 0, # 'ำ' + 23: 0, # 'ิ' + 13: 0, # 'ี' + 40: 0, # 'ึ' + 27: 0, # 'ื' + 32: 0, # 'ุ' + 35: 0, # 'ู' + 11: 3, # 'เ' + 28: 2, # 'à¹' + 41: 1, # 'โ' + 29: 2, # 'ใ' + 33: 2, # 'ไ' + 50: 1, # 'ๆ' + 37: 0, # '็' + 6: 0, # '่' + 7: 0, # '้' + 38: 0, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 36: { # 'ำ' + 5: 2, # 'à¸' + 30: 1, # 'ข' + 24: 3, # 'ค' + 8: 2, # 'ง' + 26: 1, # 'จ' + 52: 0, # 'ฉ' + 34: 0, # 'ช' + 51: 0, # 'ซ' + 47: 0, # 'à¸' + 58: 0, # 'ฎ' + 57: 0, # 'à¸' + 49: 1, # 'à¸' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 0, # 'ณ' + 20: 1, # 'ด' + 19: 1, # 'ต' + 44: 1, # 'ถ' + 14: 1, # 'ท' + 48: 0, # 'ธ' + 3: 3, # 'น' + 17: 1, # 'บ' + 25: 1, # 'ป' + 39: 1, # 'ผ' + 62: 0, # 'à¸' + 31: 1, # 'พ' + 54: 0, # 'ฟ' + 45: 1, # 'ภ' + 9: 1, # 'ม' + 16: 0, # 'ย' + 2: 2, # 'ร' + 61: 0, # 'ฤ' + 15: 2, # 'ล' + 12: 1, # 'ว' + 42: 0, # 'ศ' + 46: 0, # 'ษ' + 18: 1, # 'ส' + 21: 3, # 'ห' + 4: 1, # 'อ' + 63: 0, # 'ฯ' + 22: 0, # 'ะ' + 10: 0, # 'ั' + 1: 0, # 'า' + 36: 0, # 'ำ' + 23: 0, # 'ิ' + 13: 0, # 'ี' + 40: 0, # 'ึ' + 27: 0, # 'ื' + 32: 0, # 'ุ' + 35: 0, # 'ู' + 11: 3, # 'เ' + 28: 2, # 'à¹' + 41: 1, # 'โ' + 29: 2, # 'ใ' + 33: 2, # 'ไ' + 50: 0, # 'ๆ' + 37: 0, # '็' + 6: 0, # '่' + 7: 0, # '้' + 38: 0, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 23: { # 'ิ' + 5: 3, # 'à¸' + 30: 1, # 'ข' + 24: 2, # 'ค' + 8: 3, # 'ง' + 26: 3, # 'จ' + 52: 0, # 'ฉ' + 34: 3, # 'ช' + 51: 0, # 'ซ' + 47: 2, # 'à¸' + 58: 0, # 'ฎ' + 57: 0, # 'à¸' + 49: 0, # 'à¸' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 0, # 'ณ' + 20: 3, # 'ด' + 19: 3, # 'ต' + 44: 1, # 'ถ' + 14: 3, # 'ท' + 48: 3, # 'ธ' + 3: 3, # 'น' + 17: 3, # 'บ' + 25: 2, # 'ป' + 39: 2, # 'ผ' + 62: 0, # 'à¸' + 31: 3, # 'พ' + 54: 1, # 'ฟ' + 45: 2, # 'ภ' + 9: 3, # 'ม' + 16: 2, # 'ย' + 2: 2, # 'ร' + 61: 0, # 'ฤ' + 15: 2, # 'ล' + 12: 3, # 'ว' + 42: 3, # 'ศ' + 46: 2, # 'ษ' + 18: 2, # 'ส' + 21: 3, # 'ห' + 4: 1, # 'อ' + 63: 1, # 'ฯ' + 22: 0, # 'ะ' + 10: 0, # 'ั' + 1: 0, # 'า' + 36: 0, # 'ำ' + 23: 0, # 'ิ' + 13: 0, # 'ี' + 40: 0, # 'ึ' + 27: 0, # 'ื' + 32: 0, # 'ุ' + 35: 0, # 'ู' + 11: 3, # 'เ' + 28: 1, # 'à¹' + 41: 1, # 'โ' + 29: 1, # 'ใ' + 33: 0, # 'ไ' + 50: 0, # 'ๆ' + 37: 0, # '็' + 6: 3, # '่' + 7: 2, # '้' + 38: 2, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 13: { # 'ี' + 5: 3, # 'à¸' + 30: 2, # 'ข' + 24: 2, # 'ค' + 8: 0, # 'ง' + 26: 1, # 'จ' + 52: 0, # 'ฉ' + 34: 1, # 'ช' + 51: 0, # 'ซ' + 47: 0, # 'à¸' + 58: 0, # 'ฎ' + 57: 0, # 'à¸' + 49: 0, # 'à¸' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 0, # 'ณ' + 20: 2, # 'ด' + 19: 1, # 'ต' + 44: 0, # 'ถ' + 14: 2, # 'ท' + 48: 0, # 'ธ' + 3: 1, # 'น' + 17: 2, # 'บ' + 25: 2, # 'ป' + 39: 1, # 'ผ' + 62: 0, # 'à¸' + 31: 2, # 'พ' + 54: 0, # 'ฟ' + 45: 0, # 'ภ' + 9: 2, # 'ม' + 16: 3, # 'ย' + 2: 2, # 'ร' + 61: 0, # 'ฤ' + 15: 1, # 'ล' + 12: 2, # 'ว' + 42: 1, # 'ศ' + 46: 0, # 'ษ' + 18: 2, # 'ส' + 21: 1, # 'ห' + 4: 2, # 'อ' + 63: 0, # 'ฯ' + 22: 0, # 'ะ' + 10: 0, # 'ั' + 1: 0, # 'า' + 36: 0, # 'ำ' + 23: 0, # 'ิ' + 13: 0, # 'ี' + 40: 0, # 'ึ' + 27: 0, # 'ื' + 32: 0, # 'ุ' + 35: 0, # 'ู' + 11: 2, # 'เ' + 28: 2, # 'à¹' + 41: 1, # 'โ' + 29: 1, # 'ใ' + 33: 1, # 'ไ' + 50: 1, # 'ๆ' + 37: 0, # '็' + 6: 3, # '่' + 7: 3, # '้' + 38: 0, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 40: { # 'ึ' + 5: 3, # 'à¸' + 30: 0, # 'ข' + 24: 0, # 'ค' + 8: 3, # 'ง' + 26: 0, # 'จ' + 52: 0, # 'ฉ' + 34: 0, # 'ช' + 51: 0, # 'ซ' + 47: 0, # 'à¸' + 58: 0, # 'ฎ' + 57: 0, # 'à¸' + 49: 0, # 'à¸' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 0, # 'ณ' + 20: 1, # 'ด' + 19: 0, # 'ต' + 44: 0, # 'ถ' + 14: 0, # 'ท' + 48: 0, # 'ธ' + 3: 0, # 'น' + 17: 0, # 'บ' + 25: 0, # 'ป' + 39: 0, # 'ผ' + 62: 0, # 'à¸' + 31: 0, # 'พ' + 54: 0, # 'ฟ' + 45: 0, # 'ภ' + 9: 1, # 'ม' + 16: 0, # 'ย' + 2: 0, # 'ร' + 61: 0, # 'ฤ' + 15: 0, # 'ล' + 12: 0, # 'ว' + 42: 0, # 'ศ' + 46: 0, # 'ษ' + 18: 0, # 'ส' + 21: 0, # 'ห' + 4: 0, # 'อ' + 63: 0, # 'ฯ' + 22: 0, # 'ะ' + 10: 0, # 'ั' + 1: 0, # 'า' + 36: 0, # 'ำ' + 23: 0, # 'ิ' + 13: 0, # 'ี' + 40: 0, # 'ึ' + 27: 0, # 'ื' + 32: 0, # 'ุ' + 35: 0, # 'ู' + 11: 0, # 'เ' + 28: 0, # 'à¹' + 41: 0, # 'โ' + 29: 0, # 'ใ' + 33: 0, # 'ไ' + 50: 0, # 'ๆ' + 37: 0, # '็' + 6: 3, # '่' + 7: 3, # '้' + 38: 0, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 27: { # 'ื' + 5: 0, # 'à¸' + 30: 0, # 'ข' + 24: 0, # 'ค' + 8: 0, # 'ง' + 26: 0, # 'จ' + 52: 0, # 'ฉ' + 34: 1, # 'ช' + 51: 0, # 'ซ' + 47: 0, # 'à¸' + 58: 0, # 'ฎ' + 57: 0, # 'à¸' + 49: 0, # 'à¸' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 0, # 'ณ' + 20: 1, # 'ด' + 19: 0, # 'ต' + 44: 0, # 'ถ' + 14: 0, # 'ท' + 48: 0, # 'ธ' + 3: 2, # 'น' + 17: 3, # 'บ' + 25: 0, # 'ป' + 39: 0, # 'ผ' + 62: 0, # 'à¸' + 31: 0, # 'พ' + 54: 0, # 'ฟ' + 45: 0, # 'ภ' + 9: 2, # 'ม' + 16: 0, # 'ย' + 2: 0, # 'ร' + 61: 0, # 'ฤ' + 15: 0, # 'ล' + 12: 0, # 'ว' + 42: 0, # 'ศ' + 46: 0, # 'ษ' + 18: 0, # 'ส' + 21: 0, # 'ห' + 4: 3, # 'อ' + 63: 0, # 'ฯ' + 22: 0, # 'ะ' + 10: 0, # 'ั' + 1: 0, # 'า' + 36: 0, # 'ำ' + 23: 0, # 'ิ' + 13: 0, # 'ี' + 40: 0, # 'ึ' + 27: 0, # 'ื' + 32: 0, # 'ุ' + 35: 0, # 'ู' + 11: 0, # 'เ' + 28: 0, # 'à¹' + 41: 0, # 'โ' + 29: 0, # 'ใ' + 33: 0, # 'ไ' + 50: 0, # 'ๆ' + 37: 0, # '็' + 6: 3, # '่' + 7: 3, # '้' + 38: 0, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 32: { # 'ุ' + 5: 3, # 'à¸' + 30: 2, # 'ข' + 24: 3, # 'ค' + 8: 3, # 'ง' + 26: 0, # 'จ' + 52: 0, # 'ฉ' + 34: 0, # 'ช' + 51: 0, # 'ซ' + 47: 2, # 'à¸' + 58: 0, # 'ฎ' + 57: 0, # 'à¸' + 49: 0, # 'à¸' + 53: 0, # 'ฑ' + 55: 1, # 'ฒ' + 43: 3, # 'ณ' + 20: 3, # 'ด' + 19: 3, # 'ต' + 44: 1, # 'ถ' + 14: 2, # 'ท' + 48: 1, # 'ธ' + 3: 2, # 'น' + 17: 2, # 'บ' + 25: 2, # 'ป' + 39: 2, # 'ผ' + 62: 0, # 'à¸' + 31: 1, # 'พ' + 54: 0, # 'ฟ' + 45: 1, # 'ภ' + 9: 3, # 'ม' + 16: 1, # 'ย' + 2: 2, # 'ร' + 61: 0, # 'ฤ' + 15: 2, # 'ล' + 12: 1, # 'ว' + 42: 1, # 'ศ' + 46: 2, # 'ษ' + 18: 1, # 'ส' + 21: 1, # 'ห' + 4: 1, # 'อ' + 63: 0, # 'ฯ' + 22: 0, # 'ะ' + 10: 0, # 'ั' + 1: 0, # 'า' + 36: 0, # 'ำ' + 23: 0, # 'ิ' + 13: 0, # 'ี' + 40: 0, # 'ึ' + 27: 0, # 'ื' + 32: 0, # 'ุ' + 35: 0, # 'ู' + 11: 1, # 'เ' + 28: 0, # 'à¹' + 41: 1, # 'โ' + 29: 0, # 'ใ' + 33: 1, # 'ไ' + 50: 0, # 'ๆ' + 37: 0, # '็' + 6: 3, # '่' + 7: 2, # '้' + 38: 1, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 35: { # 'ู' + 5: 3, # 'à¸' + 30: 0, # 'ข' + 24: 0, # 'ค' + 8: 2, # 'ง' + 26: 1, # 'จ' + 52: 0, # 'ฉ' + 34: 0, # 'ช' + 51: 0, # 'ซ' + 47: 2, # 'à¸' + 58: 0, # 'ฎ' + 57: 0, # 'à¸' + 49: 0, # 'à¸' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 1, # 'ณ' + 20: 2, # 'ด' + 19: 2, # 'ต' + 44: 0, # 'ถ' + 14: 1, # 'ท' + 48: 0, # 'ธ' + 3: 2, # 'น' + 17: 0, # 'บ' + 25: 3, # 'ป' + 39: 0, # 'ผ' + 62: 0, # 'à¸' + 31: 0, # 'พ' + 54: 0, # 'ฟ' + 45: 0, # 'ภ' + 9: 2, # 'ม' + 16: 0, # 'ย' + 2: 1, # 'ร' + 61: 0, # 'ฤ' + 15: 3, # 'ล' + 12: 1, # 'ว' + 42: 0, # 'ศ' + 46: 0, # 'ษ' + 18: 0, # 'ส' + 21: 0, # 'ห' + 4: 0, # 'อ' + 63: 0, # 'ฯ' + 22: 0, # 'ะ' + 10: 0, # 'ั' + 1: 0, # 'า' + 36: 0, # 'ำ' + 23: 0, # 'ิ' + 13: 0, # 'ี' + 40: 0, # 'ึ' + 27: 0, # 'ื' + 32: 0, # 'ุ' + 35: 0, # 'ู' + 11: 1, # 'เ' + 28: 1, # 'à¹' + 41: 1, # 'โ' + 29: 0, # 'ใ' + 33: 0, # 'ไ' + 50: 0, # 'ๆ' + 37: 0, # '็' + 6: 3, # '่' + 7: 3, # '้' + 38: 0, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 11: { # 'เ' + 5: 3, # 'à¸' + 30: 3, # 'ข' + 24: 3, # 'ค' + 8: 2, # 'ง' + 26: 3, # 'จ' + 52: 3, # 'ฉ' + 34: 3, # 'ช' + 51: 2, # 'ซ' + 47: 0, # 'à¸' + 58: 0, # 'ฎ' + 57: 0, # 'à¸' + 49: 0, # 'à¸' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 1, # 'ณ' + 20: 3, # 'ด' + 19: 3, # 'ต' + 44: 1, # 'ถ' + 14: 3, # 'ท' + 48: 1, # 'ธ' + 3: 3, # 'น' + 17: 3, # 'บ' + 25: 3, # 'ป' + 39: 2, # 'ผ' + 62: 1, # 'à¸' + 31: 3, # 'พ' + 54: 1, # 'ฟ' + 45: 3, # 'ภ' + 9: 3, # 'ม' + 16: 2, # 'ย' + 2: 3, # 'ร' + 61: 0, # 'ฤ' + 15: 3, # 'ล' + 12: 3, # 'ว' + 42: 2, # 'ศ' + 46: 0, # 'ษ' + 18: 3, # 'ส' + 21: 3, # 'ห' + 4: 3, # 'อ' + 63: 0, # 'ฯ' + 22: 0, # 'ะ' + 10: 0, # 'ั' + 1: 0, # 'า' + 36: 0, # 'ำ' + 23: 0, # 'ิ' + 13: 0, # 'ี' + 40: 0, # 'ึ' + 27: 0, # 'ื' + 32: 0, # 'ุ' + 35: 0, # 'ู' + 11: 0, # 'เ' + 28: 0, # 'à¹' + 41: 0, # 'โ' + 29: 0, # 'ใ' + 33: 0, # 'ไ' + 50: 0, # 'ๆ' + 37: 0, # '็' + 6: 0, # '่' + 7: 0, # '้' + 38: 0, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 28: { # 'à¹' + 5: 3, # 'à¸' + 30: 2, # 'ข' + 24: 2, # 'ค' + 8: 1, # 'ง' + 26: 2, # 'จ' + 52: 0, # 'ฉ' + 34: 1, # 'ช' + 51: 0, # 'ซ' + 47: 0, # 'à¸' + 58: 0, # 'ฎ' + 57: 0, # 'à¸' + 49: 0, # 'à¸' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 0, # 'ณ' + 20: 2, # 'ด' + 19: 3, # 'ต' + 44: 2, # 'ถ' + 14: 3, # 'ท' + 48: 0, # 'ธ' + 3: 3, # 'น' + 17: 3, # 'บ' + 25: 2, # 'ป' + 39: 3, # 'ผ' + 62: 0, # 'à¸' + 31: 2, # 'พ' + 54: 2, # 'ฟ' + 45: 0, # 'ภ' + 9: 2, # 'ม' + 16: 2, # 'ย' + 2: 2, # 'ร' + 61: 0, # 'ฤ' + 15: 3, # 'ล' + 12: 2, # 'ว' + 42: 0, # 'ศ' + 46: 0, # 'ษ' + 18: 3, # 'ส' + 21: 3, # 'ห' + 4: 1, # 'อ' + 63: 0, # 'ฯ' + 22: 0, # 'ะ' + 10: 0, # 'ั' + 1: 0, # 'า' + 36: 0, # 'ำ' + 23: 0, # 'ิ' + 13: 0, # 'ี' + 40: 0, # 'ึ' + 27: 0, # 'ื' + 32: 0, # 'ุ' + 35: 0, # 'ู' + 11: 0, # 'เ' + 28: 0, # 'à¹' + 41: 0, # 'โ' + 29: 0, # 'ใ' + 33: 0, # 'ไ' + 50: 0, # 'ๆ' + 37: 0, # '็' + 6: 0, # '่' + 7: 0, # '้' + 38: 0, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 41: { # 'โ' + 5: 2, # 'à¸' + 30: 1, # 'ข' + 24: 2, # 'ค' + 8: 0, # 'ง' + 26: 1, # 'จ' + 52: 1, # 'ฉ' + 34: 1, # 'ช' + 51: 1, # 'ซ' + 47: 0, # 'à¸' + 58: 0, # 'ฎ' + 57: 0, # 'à¸' + 49: 0, # 'à¸' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 0, # 'ณ' + 20: 3, # 'ด' + 19: 2, # 'ต' + 44: 0, # 'ถ' + 14: 2, # 'ท' + 48: 0, # 'ธ' + 3: 3, # 'น' + 17: 1, # 'บ' + 25: 3, # 'ป' + 39: 0, # 'ผ' + 62: 0, # 'à¸' + 31: 1, # 'พ' + 54: 1, # 'ฟ' + 45: 1, # 'ภ' + 9: 1, # 'ม' + 16: 2, # 'ย' + 2: 2, # 'ร' + 61: 0, # 'ฤ' + 15: 3, # 'ล' + 12: 0, # 'ว' + 42: 1, # 'ศ' + 46: 0, # 'ษ' + 18: 2, # 'ส' + 21: 0, # 'ห' + 4: 2, # 'อ' + 63: 0, # 'ฯ' + 22: 0, # 'ะ' + 10: 0, # 'ั' + 1: 0, # 'า' + 36: 0, # 'ำ' + 23: 0, # 'ิ' + 13: 0, # 'ี' + 40: 0, # 'ึ' + 27: 0, # 'ื' + 32: 0, # 'ุ' + 35: 0, # 'ู' + 11: 0, # 'เ' + 28: 0, # 'à¹' + 41: 0, # 'โ' + 29: 0, # 'ใ' + 33: 0, # 'ไ' + 50: 0, # 'ๆ' + 37: 0, # '็' + 6: 0, # '่' + 7: 0, # '้' + 38: 0, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 29: { # 'ใ' + 5: 2, # 'à¸' + 30: 0, # 'ข' + 24: 1, # 'ค' + 8: 0, # 'ง' + 26: 3, # 'จ' + 52: 0, # 'ฉ' + 34: 3, # 'ช' + 51: 0, # 'ซ' + 47: 0, # 'à¸' + 58: 0, # 'ฎ' + 57: 0, # 'à¸' + 49: 0, # 'à¸' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 0, # 'ณ' + 20: 3, # 'ด' + 19: 1, # 'ต' + 44: 0, # 'ถ' + 14: 0, # 'ท' + 48: 0, # 'ธ' + 3: 3, # 'น' + 17: 2, # 'บ' + 25: 0, # 'ป' + 39: 0, # 'ผ' + 62: 0, # 'à¸' + 31: 0, # 'พ' + 54: 0, # 'ฟ' + 45: 0, # 'ภ' + 9: 0, # 'ม' + 16: 1, # 'ย' + 2: 0, # 'ร' + 61: 0, # 'ฤ' + 15: 0, # 'ล' + 12: 0, # 'ว' + 42: 0, # 'ศ' + 46: 0, # 'ษ' + 18: 3, # 'ส' + 21: 3, # 'ห' + 4: 0, # 'อ' + 63: 0, # 'ฯ' + 22: 0, # 'ะ' + 10: 0, # 'ั' + 1: 0, # 'า' + 36: 0, # 'ำ' + 23: 0, # 'ิ' + 13: 0, # 'ี' + 40: 0, # 'ึ' + 27: 0, # 'ื' + 32: 0, # 'ุ' + 35: 0, # 'ู' + 11: 0, # 'เ' + 28: 0, # 'à¹' + 41: 0, # 'โ' + 29: 0, # 'ใ' + 33: 0, # 'ไ' + 50: 0, # 'ๆ' + 37: 0, # '็' + 6: 0, # '่' + 7: 0, # '้' + 38: 0, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 33: { # 'ไ' + 5: 1, # 'à¸' + 30: 2, # 'ข' + 24: 0, # 'ค' + 8: 0, # 'ง' + 26: 0, # 'จ' + 52: 0, # 'ฉ' + 34: 1, # 'ช' + 51: 1, # 'ซ' + 47: 0, # 'à¸' + 58: 0, # 'ฎ' + 57: 0, # 'à¸' + 49: 0, # 'à¸' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 0, # 'ณ' + 20: 3, # 'ด' + 19: 1, # 'ต' + 44: 0, # 'ถ' + 14: 3, # 'ท' + 48: 0, # 'ธ' + 3: 0, # 'น' + 17: 1, # 'บ' + 25: 3, # 'ป' + 39: 0, # 'ผ' + 62: 0, # 'à¸' + 31: 0, # 'พ' + 54: 2, # 'ฟ' + 45: 0, # 'ภ' + 9: 3, # 'ม' + 16: 0, # 'ย' + 2: 3, # 'ร' + 61: 0, # 'ฤ' + 15: 1, # 'ล' + 12: 3, # 'ว' + 42: 0, # 'ศ' + 46: 0, # 'ษ' + 18: 1, # 'ส' + 21: 2, # 'ห' + 4: 0, # 'อ' + 63: 0, # 'ฯ' + 22: 0, # 'ะ' + 10: 0, # 'ั' + 1: 0, # 'า' + 36: 0, # 'ำ' + 23: 0, # 'ิ' + 13: 0, # 'ี' + 40: 0, # 'ึ' + 27: 0, # 'ื' + 32: 0, # 'ุ' + 35: 0, # 'ู' + 11: 0, # 'เ' + 28: 0, # 'à¹' + 41: 0, # 'โ' + 29: 0, # 'ใ' + 33: 0, # 'ไ' + 50: 0, # 'ๆ' + 37: 0, # '็' + 6: 0, # '่' + 7: 0, # '้' + 38: 0, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 50: { # 'ๆ' + 5: 0, # 'à¸' + 30: 0, # 'ข' + 24: 0, # 'ค' + 8: 0, # 'ง' + 26: 0, # 'จ' + 52: 0, # 'ฉ' + 34: 0, # 'ช' + 51: 0, # 'ซ' + 47: 0, # 'à¸' + 58: 0, # 'ฎ' + 57: 0, # 'à¸' + 49: 0, # 'à¸' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 0, # 'ณ' + 20: 0, # 'ด' + 19: 0, # 'ต' + 44: 0, # 'ถ' + 14: 0, # 'ท' + 48: 0, # 'ธ' + 3: 0, # 'น' + 17: 0, # 'บ' + 25: 0, # 'ป' + 39: 0, # 'ผ' + 62: 0, # 'à¸' + 31: 0, # 'พ' + 54: 0, # 'ฟ' + 45: 0, # 'ภ' + 9: 0, # 'ม' + 16: 0, # 'ย' + 2: 0, # 'ร' + 61: 0, # 'ฤ' + 15: 0, # 'ล' + 12: 0, # 'ว' + 42: 0, # 'ศ' + 46: 0, # 'ษ' + 18: 0, # 'ส' + 21: 0, # 'ห' + 4: 0, # 'อ' + 63: 0, # 'ฯ' + 22: 0, # 'ะ' + 10: 0, # 'ั' + 1: 0, # 'า' + 36: 0, # 'ำ' + 23: 0, # 'ิ' + 13: 0, # 'ี' + 40: 0, # 'ึ' + 27: 0, # 'ื' + 32: 0, # 'ุ' + 35: 0, # 'ู' + 11: 0, # 'เ' + 28: 0, # 'à¹' + 41: 0, # 'โ' + 29: 0, # 'ใ' + 33: 0, # 'ไ' + 50: 0, # 'ๆ' + 37: 0, # '็' + 6: 0, # '่' + 7: 0, # '้' + 38: 0, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 37: { # '็' + 5: 2, # 'à¸' + 30: 1, # 'ข' + 24: 2, # 'ค' + 8: 2, # 'ง' + 26: 3, # 'จ' + 52: 0, # 'ฉ' + 34: 0, # 'ช' + 51: 0, # 'ซ' + 47: 1, # 'à¸' + 58: 0, # 'ฎ' + 57: 0, # 'à¸' + 49: 0, # 'à¸' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 0, # 'ณ' + 20: 1, # 'ด' + 19: 2, # 'ต' + 44: 0, # 'ถ' + 14: 1, # 'ท' + 48: 0, # 'ธ' + 3: 3, # 'น' + 17: 3, # 'บ' + 25: 0, # 'ป' + 39: 0, # 'ผ' + 62: 0, # 'à¸' + 31: 0, # 'พ' + 54: 0, # 'ฟ' + 45: 0, # 'ภ' + 9: 2, # 'ม' + 16: 1, # 'ย' + 2: 0, # 'ร' + 61: 0, # 'ฤ' + 15: 0, # 'ล' + 12: 2, # 'ว' + 42: 0, # 'ศ' + 46: 0, # 'ษ' + 18: 1, # 'ส' + 21: 0, # 'ห' + 4: 1, # 'อ' + 63: 0, # 'ฯ' + 22: 0, # 'ะ' + 10: 0, # 'ั' + 1: 0, # 'า' + 36: 0, # 'ำ' + 23: 0, # 'ิ' + 13: 0, # 'ี' + 40: 0, # 'ึ' + 27: 0, # 'ื' + 32: 0, # 'ุ' + 35: 0, # 'ู' + 11: 1, # 'เ' + 28: 0, # 'à¹' + 41: 0, # 'โ' + 29: 0, # 'ใ' + 33: 1, # 'ไ' + 50: 0, # 'ๆ' + 37: 0, # '็' + 6: 0, # '่' + 7: 0, # '้' + 38: 0, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 6: { # '่' + 5: 2, # 'à¸' + 30: 1, # 'ข' + 24: 2, # 'ค' + 8: 3, # 'ง' + 26: 2, # 'จ' + 52: 0, # 'ฉ' + 34: 1, # 'ช' + 51: 1, # 'ซ' + 47: 0, # 'à¸' + 58: 0, # 'ฎ' + 57: 0, # 'à¸' + 49: 1, # 'à¸' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 0, # 'ณ' + 20: 1, # 'ด' + 19: 2, # 'ต' + 44: 1, # 'ถ' + 14: 2, # 'ท' + 48: 1, # 'ธ' + 3: 3, # 'น' + 17: 1, # 'บ' + 25: 2, # 'ป' + 39: 2, # 'ผ' + 62: 1, # 'à¸' + 31: 1, # 'พ' + 54: 0, # 'ฟ' + 45: 0, # 'ภ' + 9: 3, # 'ม' + 16: 3, # 'ย' + 2: 2, # 'ร' + 61: 0, # 'ฤ' + 15: 2, # 'ล' + 12: 3, # 'ว' + 42: 0, # 'ศ' + 46: 0, # 'ษ' + 18: 2, # 'ส' + 21: 1, # 'ห' + 4: 3, # 'อ' + 63: 0, # 'ฯ' + 22: 1, # 'ะ' + 10: 0, # 'ั' + 1: 3, # 'า' + 36: 2, # 'ำ' + 23: 0, # 'ิ' + 13: 0, # 'ี' + 40: 0, # 'ึ' + 27: 0, # 'ื' + 32: 0, # 'ุ' + 35: 0, # 'ู' + 11: 3, # 'เ' + 28: 2, # 'à¹' + 41: 1, # 'โ' + 29: 2, # 'ใ' + 33: 2, # 'ไ' + 50: 1, # 'ๆ' + 37: 0, # '็' + 6: 0, # '่' + 7: 0, # '้' + 38: 0, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 7: { # '้' + 5: 2, # 'à¸' + 30: 1, # 'ข' + 24: 2, # 'ค' + 8: 3, # 'ง' + 26: 2, # 'จ' + 52: 0, # 'ฉ' + 34: 1, # 'ช' + 51: 1, # 'ซ' + 47: 0, # 'à¸' + 58: 0, # 'ฎ' + 57: 0, # 'à¸' + 49: 0, # 'à¸' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 0, # 'ณ' + 20: 1, # 'ด' + 19: 2, # 'ต' + 44: 1, # 'ถ' + 14: 2, # 'ท' + 48: 0, # 'ธ' + 3: 3, # 'น' + 17: 2, # 'บ' + 25: 2, # 'ป' + 39: 2, # 'ผ' + 62: 0, # 'à¸' + 31: 1, # 'พ' + 54: 1, # 'ฟ' + 45: 0, # 'ภ' + 9: 3, # 'ม' + 16: 2, # 'ย' + 2: 2, # 'ร' + 61: 0, # 'ฤ' + 15: 1, # 'ล' + 12: 3, # 'ว' + 42: 1, # 'ศ' + 46: 0, # 'ษ' + 18: 2, # 'ส' + 21: 2, # 'ห' + 4: 3, # 'อ' + 63: 0, # 'ฯ' + 22: 0, # 'ะ' + 10: 0, # 'ั' + 1: 3, # 'า' + 36: 2, # 'ำ' + 23: 0, # 'ิ' + 13: 0, # 'ี' + 40: 0, # 'ึ' + 27: 0, # 'ื' + 32: 0, # 'ุ' + 35: 0, # 'ู' + 11: 2, # 'เ' + 28: 2, # 'à¹' + 41: 1, # 'โ' + 29: 2, # 'ใ' + 33: 2, # 'ไ' + 50: 0, # 'ๆ' + 37: 0, # '็' + 6: 0, # '่' + 7: 0, # '้' + 38: 0, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 38: { # '์' + 5: 2, # 'à¸' + 30: 1, # 'ข' + 24: 1, # 'ค' + 8: 0, # 'ง' + 26: 1, # 'จ' + 52: 0, # 'ฉ' + 34: 1, # 'ช' + 51: 0, # 'ซ' + 47: 0, # 'à¸' + 58: 0, # 'ฎ' + 57: 0, # 'à¸' + 49: 0, # 'à¸' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 0, # 'ณ' + 20: 2, # 'ด' + 19: 1, # 'ต' + 44: 1, # 'ถ' + 14: 1, # 'ท' + 48: 0, # 'ธ' + 3: 1, # 'น' + 17: 1, # 'บ' + 25: 1, # 'ป' + 39: 0, # 'ผ' + 62: 0, # 'à¸' + 31: 1, # 'พ' + 54: 1, # 'ฟ' + 45: 0, # 'ภ' + 9: 2, # 'ม' + 16: 0, # 'ย' + 2: 1, # 'ร' + 61: 1, # 'ฤ' + 15: 1, # 'ล' + 12: 1, # 'ว' + 42: 0, # 'ศ' + 46: 0, # 'ษ' + 18: 1, # 'ส' + 21: 1, # 'ห' + 4: 2, # 'อ' + 63: 1, # 'ฯ' + 22: 0, # 'ะ' + 10: 0, # 'ั' + 1: 0, # 'า' + 36: 0, # 'ำ' + 23: 0, # 'ิ' + 13: 0, # 'ี' + 40: 0, # 'ึ' + 27: 0, # 'ื' + 32: 0, # 'ุ' + 35: 0, # 'ู' + 11: 2, # 'เ' + 28: 2, # 'à¹' + 41: 1, # 'โ' + 29: 1, # 'ใ' + 33: 1, # 'ไ' + 50: 0, # 'ๆ' + 37: 0, # '็' + 6: 0, # '่' + 7: 0, # '้' + 38: 0, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 56: { # '๑' + 5: 0, # 'à¸' + 30: 0, # 'ข' + 24: 0, # 'ค' + 8: 0, # 'ง' + 26: 0, # 'จ' + 52: 0, # 'ฉ' + 34: 0, # 'ช' + 51: 0, # 'ซ' + 47: 0, # 'à¸' + 58: 0, # 'ฎ' + 57: 0, # 'à¸' + 49: 0, # 'à¸' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 0, # 'ณ' + 20: 0, # 'ด' + 19: 0, # 'ต' + 44: 0, # 'ถ' + 14: 0, # 'ท' + 48: 0, # 'ธ' + 3: 0, # 'น' + 17: 0, # 'บ' + 25: 0, # 'ป' + 39: 0, # 'ผ' + 62: 0, # 'à¸' + 31: 0, # 'พ' + 54: 0, # 'ฟ' + 45: 0, # 'ภ' + 9: 0, # 'ม' + 16: 0, # 'ย' + 2: 0, # 'ร' + 61: 0, # 'ฤ' + 15: 0, # 'ล' + 12: 0, # 'ว' + 42: 0, # 'ศ' + 46: 0, # 'ษ' + 18: 0, # 'ส' + 21: 0, # 'ห' + 4: 0, # 'อ' + 63: 0, # 'ฯ' + 22: 0, # 'ะ' + 10: 0, # 'ั' + 1: 0, # 'า' + 36: 0, # 'ำ' + 23: 0, # 'ิ' + 13: 0, # 'ี' + 40: 0, # 'ึ' + 27: 0, # 'ื' + 32: 0, # 'ุ' + 35: 0, # 'ู' + 11: 0, # 'เ' + 28: 0, # 'à¹' + 41: 0, # 'โ' + 29: 0, # 'ใ' + 33: 0, # 'ไ' + 50: 0, # 'ๆ' + 37: 0, # '็' + 6: 0, # '่' + 7: 0, # '้' + 38: 0, # '์' + 56: 2, # '๑' + 59: 1, # '๒' + 60: 1, # '๕' + }, + 59: { # '๒' + 5: 0, # 'à¸' + 30: 0, # 'ข' + 24: 0, # 'ค' + 8: 0, # 'ง' + 26: 0, # 'จ' + 52: 0, # 'ฉ' + 34: 0, # 'ช' + 51: 0, # 'ซ' + 47: 0, # 'à¸' + 58: 0, # 'ฎ' + 57: 0, # 'à¸' + 49: 0, # 'à¸' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 0, # 'ณ' + 20: 0, # 'ด' + 19: 0, # 'ต' + 44: 0, # 'ถ' + 14: 0, # 'ท' + 48: 0, # 'ธ' + 3: 0, # 'น' + 17: 0, # 'บ' + 25: 0, # 'ป' + 39: 0, # 'ผ' + 62: 0, # 'à¸' + 31: 0, # 'พ' + 54: 0, # 'ฟ' + 45: 0, # 'ภ' + 9: 0, # 'ม' + 16: 0, # 'ย' + 2: 0, # 'ร' + 61: 0, # 'ฤ' + 15: 0, # 'ล' + 12: 0, # 'ว' + 42: 0, # 'ศ' + 46: 0, # 'ษ' + 18: 0, # 'ส' + 21: 0, # 'ห' + 4: 0, # 'อ' + 63: 0, # 'ฯ' + 22: 0, # 'ะ' + 10: 0, # 'ั' + 1: 0, # 'า' + 36: 0, # 'ำ' + 23: 0, # 'ิ' + 13: 0, # 'ี' + 40: 0, # 'ึ' + 27: 0, # 'ื' + 32: 0, # 'ุ' + 35: 0, # 'ู' + 11: 0, # 'เ' + 28: 0, # 'à¹' + 41: 0, # 'โ' + 29: 0, # 'ใ' + 33: 0, # 'ไ' + 50: 0, # 'ๆ' + 37: 0, # '็' + 6: 0, # '่' + 7: 0, # '้' + 38: 0, # '์' + 56: 1, # '๑' + 59: 1, # '๒' + 60: 3, # '๕' + }, + 60: { # '๕' + 5: 0, # 'à¸' + 30: 0, # 'ข' + 24: 0, # 'ค' + 8: 0, # 'ง' + 26: 0, # 'จ' + 52: 0, # 'ฉ' + 34: 0, # 'ช' + 51: 0, # 'ซ' + 47: 0, # 'à¸' + 58: 0, # 'ฎ' + 57: 0, # 'à¸' + 49: 0, # 'à¸' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 0, # 'ณ' + 20: 0, # 'ด' + 19: 0, # 'ต' + 44: 0, # 'ถ' + 14: 0, # 'ท' + 48: 0, # 'ธ' + 3: 0, # 'น' + 17: 0, # 'บ' + 25: 0, # 'ป' + 39: 0, # 'ผ' + 62: 0, # 'à¸' + 31: 0, # 'พ' + 54: 0, # 'ฟ' + 45: 0, # 'ภ' + 9: 0, # 'ม' + 16: 0, # 'ย' + 2: 0, # 'ร' + 61: 0, # 'ฤ' + 15: 0, # 'ล' + 12: 0, # 'ว' + 42: 0, # 'ศ' + 46: 0, # 'ษ' + 18: 0, # 'ส' + 21: 0, # 'ห' + 4: 0, # 'อ' + 63: 0, # 'ฯ' + 22: 0, # 'ะ' + 10: 0, # 'ั' + 1: 0, # 'า' + 36: 0, # 'ำ' + 23: 0, # 'ิ' + 13: 0, # 'ี' + 40: 0, # 'ึ' + 27: 0, # 'ื' + 32: 0, # 'ุ' + 35: 0, # 'ู' + 11: 0, # 'เ' + 28: 0, # 'à¹' + 41: 0, # 'โ' + 29: 0, # 'ใ' + 33: 0, # 'ไ' + 50: 0, # 'ๆ' + 37: 0, # '็' + 6: 0, # '่' + 7: 0, # '้' + 38: 0, # '์' + 56: 2, # '๑' + 59: 1, # '๒' + 60: 0, # '๕' + }, +} + +# 255: Undefined characters that did not exist in training text # 254: Carriage/Return # 253: symbol (punctuation) that does not belong to word # 252: 0 - 9 +# 251: Control characters -# The following result for thai was collected from a limited sample (1M). - -# Character Mapping Table: -TIS620CharToOrderMap = ( -255,255,255,255,255,255,255,255,255,255,254,255,255,254,255,255, # 00 -255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, # 10 -253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253, # 20 -252,252,252,252,252,252,252,252,252,252,253,253,253,253,253,253, # 30 -253,182,106,107,100,183,184,185,101, 94,186,187,108,109,110,111, # 40 -188,189,190, 89, 95,112,113,191,192,193,194,253,253,253,253,253, # 50 -253, 64, 72, 73,114, 74,115,116,102, 81,201,117, 90,103, 78, 82, # 60 - 96,202, 91, 79, 84,104,105, 97, 98, 92,203,253,253,253,253,253, # 70 -209,210,211,212,213, 88,214,215,216,217,218,219,220,118,221,222, -223,224, 99, 85, 83,225,226,227,228,229,230,231,232,233,234,235, -236, 5, 30,237, 24,238, 75, 8, 26, 52, 34, 51,119, 47, 58, 57, - 49, 53, 55, 43, 20, 19, 44, 14, 48, 3, 17, 25, 39, 62, 31, 54, - 45, 9, 16, 2, 61, 15,239, 12, 42, 46, 18, 21, 76, 4, 66, 63, - 22, 10, 1, 36, 23, 13, 40, 27, 32, 35, 86,240,241,242,243,244, - 11, 28, 41, 29, 33,245, 50, 37, 6, 7, 67, 77, 38, 93,246,247, - 68, 56, 59, 65, 69, 60, 70, 80, 71, 87,248,249,250,251,252,253, -) - -# Model Table: -# total sequences: 100% -# first 512 sequences: 92.6386% -# first 1024 sequences:7.3177% -# rest sequences: 1.0230% -# negative sequences: 0.0436% -ThaiLangModel = ( -0,1,3,3,3,3,0,0,3,3,0,3,3,0,3,3,3,3,3,3,3,3,0,0,3,3,3,0,3,3,3,3, -0,3,3,0,0,0,1,3,0,3,3,2,3,3,0,1,2,3,3,3,3,0,2,0,2,0,0,3,2,1,2,2, -3,0,3,3,2,3,0,0,3,3,0,3,3,0,3,3,3,3,3,3,3,3,3,0,3,2,3,0,2,2,2,3, -0,2,3,0,0,0,0,1,0,1,2,3,1,1,3,2,2,0,1,1,0,0,1,0,0,0,0,0,0,0,1,1, -3,3,3,2,3,3,3,3,3,3,3,3,3,3,3,2,2,2,2,2,2,2,3,3,2,3,2,3,3,2,2,2, -3,1,2,3,0,3,3,2,2,1,2,3,3,1,2,0,1,3,0,1,0,0,1,0,0,0,0,0,0,0,1,1, -3,3,2,2,3,3,3,3,1,2,3,3,3,3,3,2,2,2,2,3,3,2,2,3,3,2,2,3,2,3,2,2, -3,3,1,2,3,1,2,2,3,3,1,0,2,1,0,0,3,1,2,1,0,0,1,0,0,0,0,0,0,1,0,1, -3,3,3,3,3,3,2,2,3,3,3,3,2,3,2,2,3,3,2,2,3,2,2,2,2,1,1,3,1,2,1,1, -3,2,1,0,2,1,0,1,0,1,1,0,1,1,0,0,1,0,1,0,0,0,1,0,0,0,0,0,0,0,0,0, -3,3,3,2,3,2,3,3,2,2,3,2,3,3,2,3,1,1,2,3,2,2,2,3,2,2,2,2,2,1,2,1, -2,2,1,1,3,3,2,1,0,1,2,2,0,1,3,0,0,0,1,1,0,0,0,0,0,2,3,0,0,2,1,1, -3,3,2,3,3,2,0,0,3,3,0,3,3,0,2,2,3,1,2,2,1,1,1,0,2,2,2,0,2,2,1,1, -0,2,1,0,2,0,0,2,0,1,0,0,1,0,0,0,1,1,1,1,0,0,0,0,0,0,0,0,0,0,1,0, -3,3,2,3,3,2,0,0,3,3,0,2,3,0,2,1,2,2,2,2,1,2,0,0,2,2,2,0,2,2,1,1, -0,2,1,0,2,0,0,2,0,1,1,0,1,0,0,0,0,0,0,1,0,0,1,0,0,0,0,0,0,0,0,0, -3,3,2,3,2,3,2,0,2,2,1,3,2,1,3,2,1,2,3,2,2,3,0,2,3,2,2,1,2,2,2,2, -1,2,2,0,0,0,0,2,0,1,2,0,1,1,1,0,1,0,3,1,1,0,0,0,0,0,0,0,0,0,1,0, -3,3,2,3,3,2,3,2,2,2,3,2,2,3,2,2,1,2,3,2,2,3,1,3,2,2,2,3,2,2,2,3, -3,2,1,3,0,1,1,1,0,2,1,1,1,1,1,0,1,0,1,1,0,0,0,0,0,0,0,0,0,2,0,0, -1,0,0,3,0,3,3,3,3,3,0,0,3,0,2,2,3,3,3,3,3,0,0,0,1,1,3,0,0,0,0,2, -0,0,1,0,0,0,0,0,0,0,2,3,0,0,0,3,0,2,0,0,0,0,0,3,0,0,0,0,0,0,0,0, -2,0,3,3,3,3,0,0,2,3,0,0,3,0,3,3,2,3,3,3,3,3,0,0,3,3,3,0,0,0,3,3, -0,0,3,0,0,0,0,2,0,0,2,1,1,3,0,0,1,0,0,2,3,0,1,0,0,0,0,0,0,0,1,0, -3,3,3,3,2,3,3,3,3,3,3,3,1,2,1,3,3,2,2,1,2,2,2,3,1,1,2,0,2,1,2,1, -2,2,1,0,0,0,1,1,0,1,0,1,1,0,0,0,0,0,1,1,0,0,1,0,0,0,0,0,0,0,0,0, -3,0,2,1,2,3,3,3,0,2,0,2,2,0,2,1,3,2,2,1,2,1,0,0,2,2,1,0,2,1,2,2, -0,1,1,0,0,0,0,1,0,1,1,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0, -3,3,3,3,2,1,3,3,1,1,3,0,2,3,1,1,3,2,1,1,2,0,2,2,3,2,1,1,1,1,1,2, -3,0,0,1,3,1,2,1,2,0,3,0,0,0,1,0,3,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0, -3,3,1,1,3,2,3,3,3,1,3,2,1,3,2,1,3,2,2,2,2,1,3,3,1,2,1,3,1,2,3,0, -2,1,1,3,2,2,2,1,2,1,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2, -3,3,2,3,2,3,3,2,3,2,3,2,3,3,2,1,0,3,2,2,2,1,2,2,2,1,2,2,1,2,1,1, -2,2,2,3,0,1,3,1,1,1,1,0,1,1,0,2,1,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0, -3,3,3,3,2,3,2,2,1,1,3,2,3,2,3,2,0,3,2,2,1,2,0,2,2,2,1,2,2,2,2,1, -3,2,1,2,2,1,0,2,0,1,0,0,1,1,0,0,0,0,0,1,1,0,1,0,0,0,0,0,0,0,0,1, -3,3,3,3,3,2,3,1,2,3,3,2,2,3,0,1,1,2,0,3,3,2,2,3,0,1,1,3,0,0,0,0, -3,1,0,3,3,0,2,0,2,1,0,0,3,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -3,3,3,2,3,2,3,3,0,1,3,1,1,2,1,2,1,1,3,1,1,0,2,3,1,1,1,1,1,1,1,1, -3,1,1,2,2,2,2,1,1,1,0,0,2,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1, -3,2,2,1,1,2,1,3,3,2,3,2,2,3,2,2,3,1,2,2,1,2,0,3,2,1,2,2,2,2,2,1, -3,2,1,2,2,2,1,1,1,1,0,0,1,1,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0, -3,3,3,3,3,3,3,3,1,3,3,0,2,1,0,3,2,0,0,3,1,0,1,1,0,1,0,0,0,0,0,1, -1,0,0,1,0,3,2,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -3,0,2,2,2,3,0,0,1,3,0,3,2,0,3,2,2,3,3,3,3,3,1,0,2,2,2,0,2,2,1,2, -0,2,3,0,0,0,0,1,0,1,0,0,1,1,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1, -3,0,2,3,1,3,3,2,3,3,0,3,3,0,3,2,2,3,2,3,3,3,0,0,2,2,3,0,1,1,1,3, -0,0,3,0,0,0,2,2,0,1,3,0,1,2,2,2,3,0,0,0,0,0,1,0,0,0,0,0,0,0,0,1, -3,2,3,3,2,0,3,3,2,2,3,1,3,2,1,3,2,0,1,2,2,0,2,3,2,1,0,3,0,0,0,0, -3,0,0,2,3,1,3,0,0,3,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -3,1,3,2,2,2,1,2,0,1,3,1,1,3,1,3,0,0,2,1,1,1,1,2,1,1,1,0,2,1,0,1, -1,2,0,0,0,3,1,1,0,0,0,0,1,0,1,0,0,1,0,1,0,0,0,0,0,3,1,0,0,0,1,0, -3,3,3,3,2,2,2,2,2,1,3,1,1,1,2,0,1,1,2,1,2,1,3,2,0,0,3,1,1,1,1,1, -3,1,0,2,3,0,0,0,3,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,0,2,3,0,3,3,0,2,0,0,0,0,0,0,0,3,0,0,1,0,0,0,0,0,0,0,0,0,0,0, -0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,2,3,1,3,0,0,1,2,0,0,2,0,3,3,2,3,3,3,2,3,0,0,2,2,2,0,0,0,2,2, -0,0,1,0,0,0,0,3,0,0,0,0,2,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0, -0,0,0,3,0,2,0,0,0,0,0,0,0,0,0,0,1,2,3,1,3,3,0,0,1,0,3,0,0,0,0,0, -0,0,3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -3,3,1,2,3,1,2,3,1,0,3,0,2,2,1,0,2,1,1,2,0,1,0,0,1,1,1,1,0,1,0,0, -1,0,0,0,0,1,1,0,3,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -3,3,3,3,2,1,0,1,1,1,3,1,2,2,2,2,2,2,1,1,1,1,0,3,1,0,1,3,1,1,1,1, -1,1,0,2,0,1,3,1,1,0,0,1,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,2,0,1, -3,0,2,2,1,3,3,2,3,3,0,1,1,0,2,2,1,2,1,3,3,1,0,0,3,2,0,0,0,0,2,1, -0,1,0,0,0,0,1,2,0,1,1,3,1,1,2,2,1,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0, -0,0,3,0,0,1,0,0,0,3,0,0,3,0,3,1,0,1,1,1,3,2,0,0,0,3,0,0,0,0,2,0, -0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,2,0,0,0,0,0,0,0,0,0, -3,3,1,3,2,1,3,3,1,2,2,0,1,2,1,0,1,2,0,0,0,0,0,3,0,0,0,3,0,0,0,0, -3,0,0,1,1,1,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -3,0,1,2,0,3,3,3,2,2,0,1,1,0,1,3,0,0,0,2,2,0,0,0,0,3,1,0,1,0,0,0, -0,0,0,0,0,0,0,0,0,1,0,1,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -3,0,2,3,1,2,0,0,2,1,0,3,1,0,1,2,0,1,1,1,1,3,0,0,3,1,1,0,2,2,1,1, -0,2,0,0,0,0,0,1,0,1,0,0,1,1,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -3,0,0,3,1,2,0,0,2,2,0,1,2,0,1,0,1,3,1,2,1,0,0,0,2,0,3,0,0,0,1,0, -0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -3,0,1,1,2,2,0,0,0,2,0,2,1,0,1,1,0,1,1,1,2,1,0,0,1,1,1,0,2,1,1,1, -0,1,1,0,0,0,0,0,0,1,0,0,1,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,1,0,1, -0,0,0,2,0,1,3,1,1,1,1,0,0,0,0,3,2,0,1,0,0,0,1,2,0,0,0,1,0,0,0,0, -0,0,0,3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,0,0,0,3,3,3,3,1,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -1,0,2,3,2,2,0,0,0,1,0,0,0,0,2,3,2,1,2,2,3,0,0,0,2,3,1,0,0,0,1,1, -0,0,1,0,0,0,0,0,0,0,1,0,0,1,0,0,0,0,0,1,1,0,1,0,0,0,0,0,0,0,0,0, -3,3,2,2,0,1,0,0,0,0,2,0,2,0,1,0,0,0,1,1,0,0,0,2,1,0,1,0,1,1,0,0, -0,1,0,2,0,0,1,0,3,0,1,0,0,0,2,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -3,3,1,0,0,1,0,0,0,0,0,1,1,2,0,0,0,0,1,0,0,1,3,1,0,0,0,0,1,1,0,0, -0,1,0,0,0,0,3,0,0,0,0,0,0,3,0,0,0,0,0,0,0,3,0,0,0,0,0,0,0,0,0,0, -3,3,1,1,1,1,2,3,0,0,2,1,1,1,1,1,0,2,1,1,0,0,0,2,1,0,1,2,1,1,0,1, -2,1,0,3,0,0,0,0,3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -1,3,1,0,0,0,0,0,0,0,3,0,0,0,3,0,0,0,0,0,0,0,0,1,1,0,0,0,0,0,0,1, -0,0,0,2,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -3,3,2,0,0,0,0,0,0,1,2,1,0,1,1,0,2,0,0,1,0,0,2,0,0,0,0,0,0,0,0,0, -0,0,0,0,0,0,2,0,0,0,1,3,0,1,0,0,0,2,0,0,0,0,0,0,0,1,2,0,0,0,0,0, -3,3,0,0,1,1,2,0,0,1,2,1,0,1,1,1,0,1,1,0,0,2,1,1,0,1,0,0,1,1,1,0, -0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,3,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0, -2,2,2,1,0,0,0,0,1,0,0,0,0,3,0,0,0,0,0,0,0,0,0,3,0,0,0,0,0,0,0,0, -2,0,0,0,0,0,3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -2,3,0,0,1,1,0,0,0,2,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,0,0,0,0,1,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -1,1,0,1,2,0,1,2,0,0,1,1,0,2,0,1,0,0,1,0,0,0,0,1,0,0,0,2,0,0,0,0, -1,0,0,1,0,1,1,0,3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,1,0,0,0,0,0,0,0,1,1,0,1,1,0,2,1,3,0,0,0,0,1,1,0,0,0,0,0,0,0,3, -1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0, -0,0,0,0,0,0,3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -2,0,1,0,1,0,0,2,0,0,2,0,0,1,1,2,0,0,1,1,0,0,0,1,0,0,0,1,1,0,0,0, -1,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0, -1,0,0,3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,1, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,1,1,0,0,0, -2,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,3,0,0,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -2,0,0,0,0,2,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,1,0,1,0,0,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,1,3,0,0,0, -2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,1,0,0,0,0, -1,0,0,0,0,0,0,0,0,1,0,0,0,0,2,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,1,1,0,0,2,1,0,0,1,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -) - -TIS620ThaiModel = { - 'char_to_order_map': TIS620CharToOrderMap, - 'precedence_matrix': ThaiLangModel, - 'typical_positive_ratio': 0.926386, - 'keep_english_letter': False, - 'charset_name': "TIS-620", - 'language': 'Thai', +# Character Mapping Table(s): +TIS_620_THAI_CHAR_TO_ORDER = { + 0: 255, # '\x00' + 1: 255, # '\x01' + 2: 255, # '\x02' + 3: 255, # '\x03' + 4: 255, # '\x04' + 5: 255, # '\x05' + 6: 255, # '\x06' + 7: 255, # '\x07' + 8: 255, # '\x08' + 9: 255, # '\t' + 10: 254, # '\n' + 11: 255, # '\x0b' + 12: 255, # '\x0c' + 13: 254, # '\r' + 14: 255, # '\x0e' + 15: 255, # '\x0f' + 16: 255, # '\x10' + 17: 255, # '\x11' + 18: 255, # '\x12' + 19: 255, # '\x13' + 20: 255, # '\x14' + 21: 255, # '\x15' + 22: 255, # '\x16' + 23: 255, # '\x17' + 24: 255, # '\x18' + 25: 255, # '\x19' + 26: 255, # '\x1a' + 27: 255, # '\x1b' + 28: 255, # '\x1c' + 29: 255, # '\x1d' + 30: 255, # '\x1e' + 31: 255, # '\x1f' + 32: 253, # ' ' + 33: 253, # '!' + 34: 253, # '"' + 35: 253, # '#' + 36: 253, # '$' + 37: 253, # '%' + 38: 253, # '&' + 39: 253, # "'" + 40: 253, # '(' + 41: 253, # ')' + 42: 253, # '*' + 43: 253, # '+' + 44: 253, # ',' + 45: 253, # '-' + 46: 253, # '.' + 47: 253, # '/' + 48: 252, # '0' + 49: 252, # '1' + 50: 252, # '2' + 51: 252, # '3' + 52: 252, # '4' + 53: 252, # '5' + 54: 252, # '6' + 55: 252, # '7' + 56: 252, # '8' + 57: 252, # '9' + 58: 253, # ':' + 59: 253, # ';' + 60: 253, # '<' + 61: 253, # '=' + 62: 253, # '>' + 63: 253, # '?' + 64: 253, # '@' + 65: 182, # 'A' + 66: 106, # 'B' + 67: 107, # 'C' + 68: 100, # 'D' + 69: 183, # 'E' + 70: 184, # 'F' + 71: 185, # 'G' + 72: 101, # 'H' + 73: 94, # 'I' + 74: 186, # 'J' + 75: 187, # 'K' + 76: 108, # 'L' + 77: 109, # 'M' + 78: 110, # 'N' + 79: 111, # 'O' + 80: 188, # 'P' + 81: 189, # 'Q' + 82: 190, # 'R' + 83: 89, # 'S' + 84: 95, # 'T' + 85: 112, # 'U' + 86: 113, # 'V' + 87: 191, # 'W' + 88: 192, # 'X' + 89: 193, # 'Y' + 90: 194, # 'Z' + 91: 253, # '[' + 92: 253, # '\\' + 93: 253, # ']' + 94: 253, # '^' + 95: 253, # '_' + 96: 253, # '`' + 97: 64, # 'a' + 98: 72, # 'b' + 99: 73, # 'c' + 100: 114, # 'd' + 101: 74, # 'e' + 102: 115, # 'f' + 103: 116, # 'g' + 104: 102, # 'h' + 105: 81, # 'i' + 106: 201, # 'j' + 107: 117, # 'k' + 108: 90, # 'l' + 109: 103, # 'm' + 110: 78, # 'n' + 111: 82, # 'o' + 112: 96, # 'p' + 113: 202, # 'q' + 114: 91, # 'r' + 115: 79, # 's' + 116: 84, # 't' + 117: 104, # 'u' + 118: 105, # 'v' + 119: 97, # 'w' + 120: 98, # 'x' + 121: 92, # 'y' + 122: 203, # 'z' + 123: 253, # '{' + 124: 253, # '|' + 125: 253, # '}' + 126: 253, # '~' + 127: 253, # '\x7f' + 128: 209, # '\x80' + 129: 210, # '\x81' + 130: 211, # '\x82' + 131: 212, # '\x83' + 132: 213, # '\x84' + 133: 88, # '\x85' + 134: 214, # '\x86' + 135: 215, # '\x87' + 136: 216, # '\x88' + 137: 217, # '\x89' + 138: 218, # '\x8a' + 139: 219, # '\x8b' + 140: 220, # '\x8c' + 141: 118, # '\x8d' + 142: 221, # '\x8e' + 143: 222, # '\x8f' + 144: 223, # '\x90' + 145: 224, # '\x91' + 146: 99, # '\x92' + 147: 85, # '\x93' + 148: 83, # '\x94' + 149: 225, # '\x95' + 150: 226, # '\x96' + 151: 227, # '\x97' + 152: 228, # '\x98' + 153: 229, # '\x99' + 154: 230, # '\x9a' + 155: 231, # '\x9b' + 156: 232, # '\x9c' + 157: 233, # '\x9d' + 158: 234, # '\x9e' + 159: 235, # '\x9f' + 160: 236, # None + 161: 5, # 'à¸' + 162: 30, # 'ข' + 163: 237, # 'ฃ' + 164: 24, # 'ค' + 165: 238, # 'ฅ' + 166: 75, # 'ฆ' + 167: 8, # 'ง' + 168: 26, # 'จ' + 169: 52, # 'ฉ' + 170: 34, # 'ช' + 171: 51, # 'ซ' + 172: 119, # 'ฌ' + 173: 47, # 'à¸' + 174: 58, # 'ฎ' + 175: 57, # 'à¸' + 176: 49, # 'à¸' + 177: 53, # 'ฑ' + 178: 55, # 'ฒ' + 179: 43, # 'ณ' + 180: 20, # 'ด' + 181: 19, # 'ต' + 182: 44, # 'ถ' + 183: 14, # 'ท' + 184: 48, # 'ธ' + 185: 3, # 'น' + 186: 17, # 'บ' + 187: 25, # 'ป' + 188: 39, # 'ผ' + 189: 62, # 'à¸' + 190: 31, # 'พ' + 191: 54, # 'ฟ' + 192: 45, # 'ภ' + 193: 9, # 'ม' + 194: 16, # 'ย' + 195: 2, # 'ร' + 196: 61, # 'ฤ' + 197: 15, # 'ล' + 198: 239, # 'ฦ' + 199: 12, # 'ว' + 200: 42, # 'ศ' + 201: 46, # 'ษ' + 202: 18, # 'ส' + 203: 21, # 'ห' + 204: 76, # 'ฬ' + 205: 4, # 'อ' + 206: 66, # 'ฮ' + 207: 63, # 'ฯ' + 208: 22, # 'ะ' + 209: 10, # 'ั' + 210: 1, # 'า' + 211: 36, # 'ำ' + 212: 23, # 'ิ' + 213: 13, # 'ี' + 214: 40, # 'ึ' + 215: 27, # 'ื' + 216: 32, # 'ุ' + 217: 35, # 'ู' + 218: 86, # 'ฺ' + 219: 240, # None + 220: 241, # None + 221: 242, # None + 222: 243, # None + 223: 244, # '฿' + 224: 11, # 'เ' + 225: 28, # 'à¹' + 226: 41, # 'โ' + 227: 29, # 'ใ' + 228: 33, # 'ไ' + 229: 245, # 'ๅ' + 230: 50, # 'ๆ' + 231: 37, # '็' + 232: 6, # '่' + 233: 7, # '้' + 234: 67, # '๊' + 235: 77, # '๋' + 236: 38, # '์' + 237: 93, # 'à¹' + 238: 246, # '๎' + 239: 247, # 'à¹' + 240: 68, # 'à¹' + 241: 56, # '๑' + 242: 59, # '๒' + 243: 65, # '๓' + 244: 69, # '๔' + 245: 60, # '๕' + 246: 70, # '๖' + 247: 80, # '๗' + 248: 71, # '๘' + 249: 87, # '๙' + 250: 248, # '๚' + 251: 249, # '๛' + 252: 250, # None + 253: 251, # None + 254: 252, # None + 255: 253, # None } + +TIS_620_THAI_MODEL = SingleByteCharSetModel(charset_name='TIS-620', + language='Thai', + char_to_order_map=TIS_620_THAI_CHAR_TO_ORDER, + language_model=THAI_LANG_MODEL, + typical_positive_ratio=0.926386, + keep_ascii_letters=False, + alphabet='à¸à¸‚ฃคฅฆงจฉชซฌà¸à¸Žà¸à¸à¸‘ฒณดตถทธนบปผà¸à¸žà¸Ÿà¸ à¸¡à¸¢à¸£à¸¤à¸¥à¸¦à¸§à¸¨à¸©à¸ªà¸«à¸¬à¸­à¸®à¸¯à¸°à¸±à¸²à¸³à¸´à¸µà¸¶à¸·à¸¸à¸¹à¸ºà¸¿à¹€à¹à¹‚ใไๅๆ็่้๊๋์à¹à¹Žà¹à¹à¹‘๒๓๔๕๖๗๘๙๚๛') + diff --git a/libs/chardet/langturkishmodel.py b/libs/chardet/langturkishmodel.py index a427a4573..8ba93224d 100644 --- a/libs/chardet/langturkishmodel.py +++ b/libs/chardet/langturkishmodel.py @@ -1,193 +1,4383 @@ +#!/usr/bin/env python # -*- coding: utf-8 -*- -######################## BEGIN LICENSE BLOCK ######################## -# The Original Code is Mozilla Communicator client code. -# -# The Initial Developer of the Original Code is -# Netscape Communications Corporation. -# Portions created by the Initial Developer are Copyright (C) 1998 -# the Initial Developer. All Rights Reserved. -# -# Contributor(s): -# Mark Pilgrim - port to Python -# Özgür Baskın - Turkish Language Model -# -# This library is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 2.1 of the License, or (at your option) any later version. -# -# This library is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with this library; if not, write to the Free Software -# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA -# 02110-1301 USA -######################### END LICENSE BLOCK ######################### -# 255: Control characters that usually does not exist in any text +from chardet.sbcharsetprober import SingleByteCharSetModel + + +# 3: Positive +# 2: Likely +# 1: Unlikely +# 0: Negative + +TURKISH_LANG_MODEL = { + 23: { # 'A' + 23: 0, # 'A' + 37: 0, # 'B' + 47: 0, # 'C' + 39: 0, # 'D' + 29: 0, # 'E' + 52: 0, # 'F' + 36: 0, # 'G' + 45: 0, # 'H' + 53: 0, # 'I' + 60: 0, # 'J' + 16: 0, # 'K' + 49: 0, # 'L' + 20: 0, # 'M' + 46: 0, # 'N' + 42: 0, # 'O' + 48: 0, # 'P' + 44: 0, # 'R' + 35: 0, # 'S' + 31: 0, # 'T' + 51: 0, # 'U' + 38: 0, # 'V' + 62: 0, # 'W' + 43: 0, # 'Y' + 56: 0, # 'Z' + 1: 3, # 'a' + 21: 0, # 'b' + 28: 0, # 'c' + 12: 2, # 'd' + 2: 3, # 'e' + 18: 0, # 'f' + 27: 1, # 'g' + 25: 1, # 'h' + 3: 1, # 'i' + 24: 0, # 'j' + 10: 2, # 'k' + 5: 1, # 'l' + 13: 1, # 'm' + 4: 1, # 'n' + 15: 0, # 'o' + 26: 0, # 'p' + 7: 1, # 'r' + 8: 1, # 's' + 9: 1, # 't' + 14: 1, # 'u' + 32: 0, # 'v' + 57: 0, # 'w' + 58: 0, # 'x' + 11: 3, # 'y' + 22: 0, # 'z' + 63: 0, # '·' + 54: 0, # 'Ç' + 50: 0, # 'Ö' + 55: 0, # 'Ãœ' + 59: 0, # 'â' + 33: 1, # 'ç' + 61: 0, # 'î' + 34: 0, # 'ö' + 17: 0, # 'ü' + 30: 0, # 'ÄŸ' + 41: 0, # 'Ä°' + 6: 0, # 'ı' + 40: 0, # 'Åž' + 19: 0, # 'ÅŸ' + }, + 37: { # 'B' + 23: 0, # 'A' + 37: 0, # 'B' + 47: 2, # 'C' + 39: 0, # 'D' + 29: 0, # 'E' + 52: 2, # 'F' + 36: 0, # 'G' + 45: 0, # 'H' + 53: 0, # 'I' + 60: 0, # 'J' + 16: 1, # 'K' + 49: 0, # 'L' + 20: 0, # 'M' + 46: 0, # 'N' + 42: 0, # 'O' + 48: 1, # 'P' + 44: 0, # 'R' + 35: 1, # 'S' + 31: 0, # 'T' + 51: 0, # 'U' + 38: 1, # 'V' + 62: 0, # 'W' + 43: 1, # 'Y' + 56: 0, # 'Z' + 1: 2, # 'a' + 21: 0, # 'b' + 28: 2, # 'c' + 12: 0, # 'd' + 2: 3, # 'e' + 18: 0, # 'f' + 27: 0, # 'g' + 25: 0, # 'h' + 3: 0, # 'i' + 24: 0, # 'j' + 10: 0, # 'k' + 5: 0, # 'l' + 13: 1, # 'm' + 4: 1, # 'n' + 15: 0, # 'o' + 26: 0, # 'p' + 7: 0, # 'r' + 8: 0, # 's' + 9: 0, # 't' + 14: 2, # 'u' + 32: 0, # 'v' + 57: 0, # 'w' + 58: 0, # 'x' + 11: 0, # 'y' + 22: 1, # 'z' + 63: 0, # '·' + 54: 0, # 'Ç' + 50: 1, # 'Ö' + 55: 0, # 'Ãœ' + 59: 0, # 'â' + 33: 0, # 'ç' + 61: 0, # 'î' + 34: 1, # 'ö' + 17: 0, # 'ü' + 30: 0, # 'ÄŸ' + 41: 0, # 'Ä°' + 6: 0, # 'ı' + 40: 1, # 'Åž' + 19: 1, # 'ÅŸ' + }, + 47: { # 'C' + 23: 0, # 'A' + 37: 0, # 'B' + 47: 0, # 'C' + 39: 0, # 'D' + 29: 0, # 'E' + 52: 1, # 'F' + 36: 0, # 'G' + 45: 0, # 'H' + 53: 0, # 'I' + 60: 0, # 'J' + 16: 0, # 'K' + 49: 1, # 'L' + 20: 0, # 'M' + 46: 1, # 'N' + 42: 0, # 'O' + 48: 1, # 'P' + 44: 1, # 'R' + 35: 0, # 'S' + 31: 0, # 'T' + 51: 0, # 'U' + 38: 1, # 'V' + 62: 0, # 'W' + 43: 1, # 'Y' + 56: 0, # 'Z' + 1: 3, # 'a' + 21: 0, # 'b' + 28: 2, # 'c' + 12: 0, # 'd' + 2: 3, # 'e' + 18: 0, # 'f' + 27: 0, # 'g' + 25: 0, # 'h' + 3: 0, # 'i' + 24: 2, # 'j' + 10: 1, # 'k' + 5: 2, # 'l' + 13: 2, # 'm' + 4: 2, # 'n' + 15: 1, # 'o' + 26: 0, # 'p' + 7: 2, # 'r' + 8: 0, # 's' + 9: 0, # 't' + 14: 3, # 'u' + 32: 0, # 'v' + 57: 0, # 'w' + 58: 0, # 'x' + 11: 0, # 'y' + 22: 2, # 'z' + 63: 0, # '·' + 54: 0, # 'Ç' + 50: 1, # 'Ö' + 55: 0, # 'Ãœ' + 59: 0, # 'â' + 33: 1, # 'ç' + 61: 0, # 'î' + 34: 1, # 'ö' + 17: 0, # 'ü' + 30: 0, # 'ÄŸ' + 41: 1, # 'Ä°' + 6: 3, # 'ı' + 40: 0, # 'Åž' + 19: 0, # 'ÅŸ' + }, + 39: { # 'D' + 23: 0, # 'A' + 37: 0, # 'B' + 47: 0, # 'C' + 39: 0, # 'D' + 29: 0, # 'E' + 52: 1, # 'F' + 36: 0, # 'G' + 45: 0, # 'H' + 53: 0, # 'I' + 60: 0, # 'J' + 16: 1, # 'K' + 49: 0, # 'L' + 20: 0, # 'M' + 46: 0, # 'N' + 42: 0, # 'O' + 48: 1, # 'P' + 44: 0, # 'R' + 35: 0, # 'S' + 31: 0, # 'T' + 51: 0, # 'U' + 38: 0, # 'V' + 62: 0, # 'W' + 43: 0, # 'Y' + 56: 0, # 'Z' + 1: 2, # 'a' + 21: 0, # 'b' + 28: 2, # 'c' + 12: 0, # 'd' + 2: 2, # 'e' + 18: 0, # 'f' + 27: 0, # 'g' + 25: 0, # 'h' + 3: 0, # 'i' + 24: 0, # 'j' + 10: 0, # 'k' + 5: 1, # 'l' + 13: 3, # 'm' + 4: 0, # 'n' + 15: 1, # 'o' + 26: 0, # 'p' + 7: 0, # 'r' + 8: 0, # 's' + 9: 0, # 't' + 14: 1, # 'u' + 32: 0, # 'v' + 57: 0, # 'w' + 58: 0, # 'x' + 11: 0, # 'y' + 22: 1, # 'z' + 63: 0, # '·' + 54: 1, # 'Ç' + 50: 0, # 'Ö' + 55: 0, # 'Ãœ' + 59: 0, # 'â' + 33: 1, # 'ç' + 61: 0, # 'î' + 34: 0, # 'ö' + 17: 0, # 'ü' + 30: 1, # 'ÄŸ' + 41: 0, # 'Ä°' + 6: 1, # 'ı' + 40: 1, # 'Åž' + 19: 0, # 'ÅŸ' + }, + 29: { # 'E' + 23: 0, # 'A' + 37: 0, # 'B' + 47: 0, # 'C' + 39: 0, # 'D' + 29: 1, # 'E' + 52: 0, # 'F' + 36: 0, # 'G' + 45: 0, # 'H' + 53: 0, # 'I' + 60: 0, # 'J' + 16: 3, # 'K' + 49: 0, # 'L' + 20: 1, # 'M' + 46: 0, # 'N' + 42: 0, # 'O' + 48: 0, # 'P' + 44: 0, # 'R' + 35: 0, # 'S' + 31: 0, # 'T' + 51: 0, # 'U' + 38: 0, # 'V' + 62: 0, # 'W' + 43: 0, # 'Y' + 56: 0, # 'Z' + 1: 3, # 'a' + 21: 0, # 'b' + 28: 0, # 'c' + 12: 2, # 'd' + 2: 3, # 'e' + 18: 0, # 'f' + 27: 1, # 'g' + 25: 0, # 'h' + 3: 1, # 'i' + 24: 1, # 'j' + 10: 0, # 'k' + 5: 3, # 'l' + 13: 3, # 'm' + 4: 3, # 'n' + 15: 0, # 'o' + 26: 0, # 'p' + 7: 0, # 'r' + 8: 1, # 's' + 9: 1, # 't' + 14: 1, # 'u' + 32: 1, # 'v' + 57: 0, # 'w' + 58: 0, # 'x' + 11: 2, # 'y' + 22: 0, # 'z' + 63: 0, # '·' + 54: 0, # 'Ç' + 50: 0, # 'Ö' + 55: 0, # 'Ãœ' + 59: 0, # 'â' + 33: 0, # 'ç' + 61: 0, # 'î' + 34: 0, # 'ö' + 17: 0, # 'ü' + 30: 0, # 'ÄŸ' + 41: 0, # 'Ä°' + 6: 3, # 'ı' + 40: 0, # 'Åž' + 19: 0, # 'ÅŸ' + }, + 52: { # 'F' + 23: 0, # 'A' + 37: 1, # 'B' + 47: 1, # 'C' + 39: 1, # 'D' + 29: 1, # 'E' + 52: 2, # 'F' + 36: 0, # 'G' + 45: 2, # 'H' + 53: 1, # 'I' + 60: 0, # 'J' + 16: 0, # 'K' + 49: 0, # 'L' + 20: 1, # 'M' + 46: 1, # 'N' + 42: 1, # 'O' + 48: 2, # 'P' + 44: 1, # 'R' + 35: 1, # 'S' + 31: 1, # 'T' + 51: 1, # 'U' + 38: 1, # 'V' + 62: 0, # 'W' + 43: 2, # 'Y' + 56: 0, # 'Z' + 1: 0, # 'a' + 21: 1, # 'b' + 28: 1, # 'c' + 12: 1, # 'd' + 2: 0, # 'e' + 18: 1, # 'f' + 27: 0, # 'g' + 25: 0, # 'h' + 3: 2, # 'i' + 24: 1, # 'j' + 10: 0, # 'k' + 5: 0, # 'l' + 13: 1, # 'm' + 4: 2, # 'n' + 15: 1, # 'o' + 26: 0, # 'p' + 7: 2, # 'r' + 8: 1, # 's' + 9: 1, # 't' + 14: 1, # 'u' + 32: 0, # 'v' + 57: 0, # 'w' + 58: 0, # 'x' + 11: 1, # 'y' + 22: 1, # 'z' + 63: 0, # '·' + 54: 0, # 'Ç' + 50: 1, # 'Ö' + 55: 2, # 'Ãœ' + 59: 0, # 'â' + 33: 0, # 'ç' + 61: 0, # 'î' + 34: 2, # 'ö' + 17: 0, # 'ü' + 30: 1, # 'ÄŸ' + 41: 1, # 'Ä°' + 6: 2, # 'ı' + 40: 0, # 'Åž' + 19: 2, # 'ÅŸ' + }, + 36: { # 'G' + 23: 1, # 'A' + 37: 0, # 'B' + 47: 1, # 'C' + 39: 0, # 'D' + 29: 0, # 'E' + 52: 1, # 'F' + 36: 2, # 'G' + 45: 0, # 'H' + 53: 0, # 'I' + 60: 0, # 'J' + 16: 2, # 'K' + 49: 0, # 'L' + 20: 0, # 'M' + 46: 2, # 'N' + 42: 1, # 'O' + 48: 1, # 'P' + 44: 1, # 'R' + 35: 1, # 'S' + 31: 0, # 'T' + 51: 1, # 'U' + 38: 2, # 'V' + 62: 0, # 'W' + 43: 0, # 'Y' + 56: 0, # 'Z' + 1: 3, # 'a' + 21: 0, # 'b' + 28: 1, # 'c' + 12: 0, # 'd' + 2: 3, # 'e' + 18: 0, # 'f' + 27: 0, # 'g' + 25: 0, # 'h' + 3: 0, # 'i' + 24: 1, # 'j' + 10: 1, # 'k' + 5: 0, # 'l' + 13: 3, # 'm' + 4: 2, # 'n' + 15: 0, # 'o' + 26: 1, # 'p' + 7: 0, # 'r' + 8: 1, # 's' + 9: 1, # 't' + 14: 3, # 'u' + 32: 0, # 'v' + 57: 0, # 'w' + 58: 1, # 'x' + 11: 0, # 'y' + 22: 2, # 'z' + 63: 0, # '·' + 54: 1, # 'Ç' + 50: 2, # 'Ö' + 55: 0, # 'Ãœ' + 59: 1, # 'â' + 33: 2, # 'ç' + 61: 0, # 'î' + 34: 0, # 'ö' + 17: 0, # 'ü' + 30: 1, # 'ÄŸ' + 41: 1, # 'Ä°' + 6: 2, # 'ı' + 40: 2, # 'Åž' + 19: 1, # 'ÅŸ' + }, + 45: { # 'H' + 23: 0, # 'A' + 37: 1, # 'B' + 47: 0, # 'C' + 39: 0, # 'D' + 29: 0, # 'E' + 52: 2, # 'F' + 36: 2, # 'G' + 45: 1, # 'H' + 53: 1, # 'I' + 60: 0, # 'J' + 16: 2, # 'K' + 49: 1, # 'L' + 20: 0, # 'M' + 46: 1, # 'N' + 42: 1, # 'O' + 48: 1, # 'P' + 44: 0, # 'R' + 35: 2, # 'S' + 31: 0, # 'T' + 51: 1, # 'U' + 38: 2, # 'V' + 62: 0, # 'W' + 43: 0, # 'Y' + 56: 0, # 'Z' + 1: 3, # 'a' + 21: 0, # 'b' + 28: 2, # 'c' + 12: 0, # 'd' + 2: 3, # 'e' + 18: 0, # 'f' + 27: 0, # 'g' + 25: 0, # 'h' + 3: 2, # 'i' + 24: 0, # 'j' + 10: 1, # 'k' + 5: 0, # 'l' + 13: 2, # 'm' + 4: 0, # 'n' + 15: 1, # 'o' + 26: 1, # 'p' + 7: 1, # 'r' + 8: 0, # 's' + 9: 0, # 't' + 14: 3, # 'u' + 32: 0, # 'v' + 57: 0, # 'w' + 58: 0, # 'x' + 11: 0, # 'y' + 22: 2, # 'z' + 63: 0, # '·' + 54: 1, # 'Ç' + 50: 1, # 'Ö' + 55: 0, # 'Ãœ' + 59: 0, # 'â' + 33: 1, # 'ç' + 61: 0, # 'î' + 34: 1, # 'ö' + 17: 0, # 'ü' + 30: 2, # 'ÄŸ' + 41: 1, # 'Ä°' + 6: 0, # 'ı' + 40: 2, # 'Åž' + 19: 1, # 'ÅŸ' + }, + 53: { # 'I' + 23: 0, # 'A' + 37: 0, # 'B' + 47: 0, # 'C' + 39: 0, # 'D' + 29: 0, # 'E' + 52: 1, # 'F' + 36: 0, # 'G' + 45: 0, # 'H' + 53: 0, # 'I' + 60: 0, # 'J' + 16: 2, # 'K' + 49: 0, # 'L' + 20: 0, # 'M' + 46: 0, # 'N' + 42: 0, # 'O' + 48: 1, # 'P' + 44: 0, # 'R' + 35: 0, # 'S' + 31: 0, # 'T' + 51: 0, # 'U' + 38: 0, # 'V' + 62: 0, # 'W' + 43: 0, # 'Y' + 56: 0, # 'Z' + 1: 2, # 'a' + 21: 0, # 'b' + 28: 2, # 'c' + 12: 0, # 'd' + 2: 2, # 'e' + 18: 0, # 'f' + 27: 0, # 'g' + 25: 0, # 'h' + 3: 0, # 'i' + 24: 0, # 'j' + 10: 0, # 'k' + 5: 2, # 'l' + 13: 2, # 'm' + 4: 0, # 'n' + 15: 0, # 'o' + 26: 0, # 'p' + 7: 0, # 'r' + 8: 0, # 's' + 9: 0, # 't' + 14: 2, # 'u' + 32: 0, # 'v' + 57: 0, # 'w' + 58: 0, # 'x' + 11: 0, # 'y' + 22: 2, # 'z' + 63: 0, # '·' + 54: 1, # 'Ç' + 50: 0, # 'Ö' + 55: 0, # 'Ãœ' + 59: 0, # 'â' + 33: 2, # 'ç' + 61: 0, # 'î' + 34: 1, # 'ö' + 17: 0, # 'ü' + 30: 0, # 'ÄŸ' + 41: 0, # 'Ä°' + 6: 0, # 'ı' + 40: 1, # 'Åž' + 19: 1, # 'ÅŸ' + }, + 60: { # 'J' + 23: 0, # 'A' + 37: 0, # 'B' + 47: 0, # 'C' + 39: 0, # 'D' + 29: 0, # 'E' + 52: 0, # 'F' + 36: 0, # 'G' + 45: 0, # 'H' + 53: 0, # 'I' + 60: 0, # 'J' + 16: 0, # 'K' + 49: 0, # 'L' + 20: 1, # 'M' + 46: 0, # 'N' + 42: 0, # 'O' + 48: 0, # 'P' + 44: 0, # 'R' + 35: 0, # 'S' + 31: 0, # 'T' + 51: 0, # 'U' + 38: 0, # 'V' + 62: 0, # 'W' + 43: 0, # 'Y' + 56: 0, # 'Z' + 1: 0, # 'a' + 21: 1, # 'b' + 28: 0, # 'c' + 12: 1, # 'd' + 2: 0, # 'e' + 18: 0, # 'f' + 27: 0, # 'g' + 25: 0, # 'h' + 3: 1, # 'i' + 24: 0, # 'j' + 10: 0, # 'k' + 5: 0, # 'l' + 13: 0, # 'm' + 4: 1, # 'n' + 15: 0, # 'o' + 26: 0, # 'p' + 7: 0, # 'r' + 8: 1, # 's' + 9: 0, # 't' + 14: 0, # 'u' + 32: 0, # 'v' + 57: 0, # 'w' + 58: 0, # 'x' + 11: 0, # 'y' + 22: 0, # 'z' + 63: 0, # '·' + 54: 0, # 'Ç' + 50: 0, # 'Ö' + 55: 0, # 'Ãœ' + 59: 0, # 'â' + 33: 0, # 'ç' + 61: 0, # 'î' + 34: 0, # 'ö' + 17: 0, # 'ü' + 30: 0, # 'ÄŸ' + 41: 0, # 'Ä°' + 6: 0, # 'ı' + 40: 0, # 'Åž' + 19: 0, # 'ÅŸ' + }, + 16: { # 'K' + 23: 0, # 'A' + 37: 0, # 'B' + 47: 0, # 'C' + 39: 0, # 'D' + 29: 3, # 'E' + 52: 0, # 'F' + 36: 0, # 'G' + 45: 0, # 'H' + 53: 0, # 'I' + 60: 0, # 'J' + 16: 0, # 'K' + 49: 0, # 'L' + 20: 2, # 'M' + 46: 0, # 'N' + 42: 0, # 'O' + 48: 0, # 'P' + 44: 0, # 'R' + 35: 0, # 'S' + 31: 2, # 'T' + 51: 0, # 'U' + 38: 0, # 'V' + 62: 0, # 'W' + 43: 0, # 'Y' + 56: 0, # 'Z' + 1: 2, # 'a' + 21: 3, # 'b' + 28: 0, # 'c' + 12: 3, # 'd' + 2: 1, # 'e' + 18: 3, # 'f' + 27: 3, # 'g' + 25: 3, # 'h' + 3: 3, # 'i' + 24: 2, # 'j' + 10: 3, # 'k' + 5: 0, # 'l' + 13: 0, # 'm' + 4: 3, # 'n' + 15: 0, # 'o' + 26: 1, # 'p' + 7: 3, # 'r' + 8: 3, # 's' + 9: 3, # 't' + 14: 0, # 'u' + 32: 3, # 'v' + 57: 0, # 'w' + 58: 0, # 'x' + 11: 2, # 'y' + 22: 1, # 'z' + 63: 0, # '·' + 54: 0, # 'Ç' + 50: 0, # 'Ö' + 55: 0, # 'Ãœ' + 59: 0, # 'â' + 33: 0, # 'ç' + 61: 0, # 'î' + 34: 0, # 'ö' + 17: 2, # 'ü' + 30: 0, # 'ÄŸ' + 41: 1, # 'Ä°' + 6: 3, # 'ı' + 40: 0, # 'Åž' + 19: 0, # 'ÅŸ' + }, + 49: { # 'L' + 23: 0, # 'A' + 37: 0, # 'B' + 47: 0, # 'C' + 39: 0, # 'D' + 29: 2, # 'E' + 52: 0, # 'F' + 36: 1, # 'G' + 45: 1, # 'H' + 53: 0, # 'I' + 60: 0, # 'J' + 16: 0, # 'K' + 49: 0, # 'L' + 20: 1, # 'M' + 46: 0, # 'N' + 42: 2, # 'O' + 48: 0, # 'P' + 44: 0, # 'R' + 35: 0, # 'S' + 31: 0, # 'T' + 51: 0, # 'U' + 38: 0, # 'V' + 62: 0, # 'W' + 43: 1, # 'Y' + 56: 0, # 'Z' + 1: 0, # 'a' + 21: 3, # 'b' + 28: 0, # 'c' + 12: 2, # 'd' + 2: 0, # 'e' + 18: 0, # 'f' + 27: 0, # 'g' + 25: 0, # 'h' + 3: 2, # 'i' + 24: 0, # 'j' + 10: 1, # 'k' + 5: 0, # 'l' + 13: 0, # 'm' + 4: 2, # 'n' + 15: 1, # 'o' + 26: 1, # 'p' + 7: 1, # 'r' + 8: 1, # 's' + 9: 1, # 't' + 14: 0, # 'u' + 32: 0, # 'v' + 57: 0, # 'w' + 58: 0, # 'x' + 11: 2, # 'y' + 22: 0, # 'z' + 63: 0, # '·' + 54: 0, # 'Ç' + 50: 0, # 'Ö' + 55: 2, # 'Ãœ' + 59: 0, # 'â' + 33: 0, # 'ç' + 61: 0, # 'î' + 34: 1, # 'ö' + 17: 1, # 'ü' + 30: 1, # 'ÄŸ' + 41: 0, # 'Ä°' + 6: 2, # 'ı' + 40: 0, # 'Åž' + 19: 0, # 'ÅŸ' + }, + 20: { # 'M' + 23: 1, # 'A' + 37: 0, # 'B' + 47: 0, # 'C' + 39: 0, # 'D' + 29: 0, # 'E' + 52: 0, # 'F' + 36: 0, # 'G' + 45: 0, # 'H' + 53: 0, # 'I' + 60: 1, # 'J' + 16: 3, # 'K' + 49: 0, # 'L' + 20: 2, # 'M' + 46: 0, # 'N' + 42: 0, # 'O' + 48: 0, # 'P' + 44: 0, # 'R' + 35: 0, # 'S' + 31: 1, # 'T' + 51: 0, # 'U' + 38: 0, # 'V' + 62: 0, # 'W' + 43: 0, # 'Y' + 56: 0, # 'Z' + 1: 3, # 'a' + 21: 2, # 'b' + 28: 0, # 'c' + 12: 3, # 'd' + 2: 3, # 'e' + 18: 0, # 'f' + 27: 1, # 'g' + 25: 1, # 'h' + 3: 2, # 'i' + 24: 2, # 'j' + 10: 2, # 'k' + 5: 2, # 'l' + 13: 3, # 'm' + 4: 3, # 'n' + 15: 0, # 'o' + 26: 1, # 'p' + 7: 3, # 'r' + 8: 0, # 's' + 9: 2, # 't' + 14: 3, # 'u' + 32: 0, # 'v' + 57: 0, # 'w' + 58: 0, # 'x' + 11: 2, # 'y' + 22: 0, # 'z' + 63: 0, # '·' + 54: 0, # 'Ç' + 50: 0, # 'Ö' + 55: 0, # 'Ãœ' + 59: 0, # 'â' + 33: 3, # 'ç' + 61: 0, # 'î' + 34: 0, # 'ö' + 17: 0, # 'ü' + 30: 0, # 'ÄŸ' + 41: 0, # 'Ä°' + 6: 3, # 'ı' + 40: 0, # 'Åž' + 19: 0, # 'ÅŸ' + }, + 46: { # 'N' + 23: 0, # 'A' + 37: 1, # 'B' + 47: 0, # 'C' + 39: 0, # 'D' + 29: 0, # 'E' + 52: 1, # 'F' + 36: 1, # 'G' + 45: 1, # 'H' + 53: 0, # 'I' + 60: 0, # 'J' + 16: 2, # 'K' + 49: 0, # 'L' + 20: 0, # 'M' + 46: 1, # 'N' + 42: 0, # 'O' + 48: 0, # 'P' + 44: 1, # 'R' + 35: 1, # 'S' + 31: 0, # 'T' + 51: 1, # 'U' + 38: 2, # 'V' + 62: 0, # 'W' + 43: 1, # 'Y' + 56: 0, # 'Z' + 1: 3, # 'a' + 21: 0, # 'b' + 28: 2, # 'c' + 12: 0, # 'd' + 2: 3, # 'e' + 18: 0, # 'f' + 27: 1, # 'g' + 25: 0, # 'h' + 3: 0, # 'i' + 24: 2, # 'j' + 10: 1, # 'k' + 5: 1, # 'l' + 13: 3, # 'm' + 4: 2, # 'n' + 15: 1, # 'o' + 26: 1, # 'p' + 7: 1, # 'r' + 8: 0, # 's' + 9: 0, # 't' + 14: 3, # 'u' + 32: 0, # 'v' + 57: 0, # 'w' + 58: 1, # 'x' + 11: 1, # 'y' + 22: 2, # 'z' + 63: 0, # '·' + 54: 1, # 'Ç' + 50: 1, # 'Ö' + 55: 0, # 'Ãœ' + 59: 0, # 'â' + 33: 0, # 'ç' + 61: 0, # 'î' + 34: 1, # 'ö' + 17: 0, # 'ü' + 30: 0, # 'ÄŸ' + 41: 1, # 'Ä°' + 6: 2, # 'ı' + 40: 1, # 'Åž' + 19: 1, # 'ÅŸ' + }, + 42: { # 'O' + 23: 0, # 'A' + 37: 0, # 'B' + 47: 0, # 'C' + 39: 0, # 'D' + 29: 0, # 'E' + 52: 1, # 'F' + 36: 0, # 'G' + 45: 1, # 'H' + 53: 0, # 'I' + 60: 0, # 'J' + 16: 2, # 'K' + 49: 1, # 'L' + 20: 0, # 'M' + 46: 0, # 'N' + 42: 0, # 'O' + 48: 2, # 'P' + 44: 1, # 'R' + 35: 1, # 'S' + 31: 0, # 'T' + 51: 1, # 'U' + 38: 1, # 'V' + 62: 0, # 'W' + 43: 0, # 'Y' + 56: 0, # 'Z' + 1: 3, # 'a' + 21: 0, # 'b' + 28: 2, # 'c' + 12: 0, # 'd' + 2: 2, # 'e' + 18: 0, # 'f' + 27: 0, # 'g' + 25: 0, # 'h' + 3: 0, # 'i' + 24: 0, # 'j' + 10: 0, # 'k' + 5: 3, # 'l' + 13: 3, # 'm' + 4: 0, # 'n' + 15: 1, # 'o' + 26: 0, # 'p' + 7: 0, # 'r' + 8: 0, # 's' + 9: 0, # 't' + 14: 2, # 'u' + 32: 0, # 'v' + 57: 0, # 'w' + 58: 0, # 'x' + 11: 0, # 'y' + 22: 2, # 'z' + 63: 0, # '·' + 54: 2, # 'Ç' + 50: 1, # 'Ö' + 55: 0, # 'Ãœ' + 59: 0, # 'â' + 33: 2, # 'ç' + 61: 0, # 'î' + 34: 1, # 'ö' + 17: 0, # 'ü' + 30: 1, # 'ÄŸ' + 41: 2, # 'Ä°' + 6: 1, # 'ı' + 40: 1, # 'Åž' + 19: 1, # 'ÅŸ' + }, + 48: { # 'P' + 23: 0, # 'A' + 37: 0, # 'B' + 47: 2, # 'C' + 39: 0, # 'D' + 29: 0, # 'E' + 52: 2, # 'F' + 36: 1, # 'G' + 45: 1, # 'H' + 53: 0, # 'I' + 60: 0, # 'J' + 16: 2, # 'K' + 49: 0, # 'L' + 20: 0, # 'M' + 46: 1, # 'N' + 42: 1, # 'O' + 48: 1, # 'P' + 44: 0, # 'R' + 35: 1, # 'S' + 31: 0, # 'T' + 51: 0, # 'U' + 38: 1, # 'V' + 62: 0, # 'W' + 43: 0, # 'Y' + 56: 0, # 'Z' + 1: 2, # 'a' + 21: 0, # 'b' + 28: 2, # 'c' + 12: 0, # 'd' + 2: 3, # 'e' + 18: 0, # 'f' + 27: 0, # 'g' + 25: 0, # 'h' + 3: 0, # 'i' + 24: 0, # 'j' + 10: 1, # 'k' + 5: 0, # 'l' + 13: 2, # 'm' + 4: 0, # 'n' + 15: 2, # 'o' + 26: 0, # 'p' + 7: 0, # 'r' + 8: 0, # 's' + 9: 0, # 't' + 14: 2, # 'u' + 32: 0, # 'v' + 57: 0, # 'w' + 58: 2, # 'x' + 11: 0, # 'y' + 22: 2, # 'z' + 63: 0, # '·' + 54: 1, # 'Ç' + 50: 2, # 'Ö' + 55: 0, # 'Ãœ' + 59: 0, # 'â' + 33: 0, # 'ç' + 61: 0, # 'î' + 34: 2, # 'ö' + 17: 0, # 'ü' + 30: 1, # 'ÄŸ' + 41: 1, # 'Ä°' + 6: 0, # 'ı' + 40: 2, # 'Åž' + 19: 1, # 'ÅŸ' + }, + 44: { # 'R' + 23: 0, # 'A' + 37: 0, # 'B' + 47: 1, # 'C' + 39: 0, # 'D' + 29: 0, # 'E' + 52: 1, # 'F' + 36: 0, # 'G' + 45: 0, # 'H' + 53: 0, # 'I' + 60: 0, # 'J' + 16: 3, # 'K' + 49: 0, # 'L' + 20: 0, # 'M' + 46: 0, # 'N' + 42: 0, # 'O' + 48: 1, # 'P' + 44: 0, # 'R' + 35: 0, # 'S' + 31: 0, # 'T' + 51: 0, # 'U' + 38: 0, # 'V' + 62: 0, # 'W' + 43: 1, # 'Y' + 56: 0, # 'Z' + 1: 3, # 'a' + 21: 1, # 'b' + 28: 1, # 'c' + 12: 0, # 'd' + 2: 2, # 'e' + 18: 0, # 'f' + 27: 0, # 'g' + 25: 0, # 'h' + 3: 0, # 'i' + 24: 0, # 'j' + 10: 1, # 'k' + 5: 2, # 'l' + 13: 2, # 'm' + 4: 0, # 'n' + 15: 1, # 'o' + 26: 0, # 'p' + 7: 0, # 'r' + 8: 0, # 's' + 9: 0, # 't' + 14: 2, # 'u' + 32: 0, # 'v' + 57: 0, # 'w' + 58: 0, # 'x' + 11: 1, # 'y' + 22: 2, # 'z' + 63: 0, # '·' + 54: 0, # 'Ç' + 50: 1, # 'Ö' + 55: 0, # 'Ãœ' + 59: 0, # 'â' + 33: 1, # 'ç' + 61: 0, # 'î' + 34: 1, # 'ö' + 17: 1, # 'ü' + 30: 1, # 'ÄŸ' + 41: 0, # 'Ä°' + 6: 2, # 'ı' + 40: 1, # 'Åž' + 19: 1, # 'ÅŸ' + }, + 35: { # 'S' + 23: 0, # 'A' + 37: 0, # 'B' + 47: 1, # 'C' + 39: 0, # 'D' + 29: 0, # 'E' + 52: 1, # 'F' + 36: 1, # 'G' + 45: 1, # 'H' + 53: 0, # 'I' + 60: 0, # 'J' + 16: 3, # 'K' + 49: 1, # 'L' + 20: 1, # 'M' + 46: 0, # 'N' + 42: 0, # 'O' + 48: 1, # 'P' + 44: 0, # 'R' + 35: 0, # 'S' + 31: 0, # 'T' + 51: 1, # 'U' + 38: 1, # 'V' + 62: 0, # 'W' + 43: 1, # 'Y' + 56: 0, # 'Z' + 1: 3, # 'a' + 21: 0, # 'b' + 28: 2, # 'c' + 12: 0, # 'd' + 2: 3, # 'e' + 18: 0, # 'f' + 27: 0, # 'g' + 25: 0, # 'h' + 3: 0, # 'i' + 24: 0, # 'j' + 10: 1, # 'k' + 5: 1, # 'l' + 13: 2, # 'm' + 4: 1, # 'n' + 15: 0, # 'o' + 26: 0, # 'p' + 7: 0, # 'r' + 8: 0, # 's' + 9: 1, # 't' + 14: 2, # 'u' + 32: 0, # 'v' + 57: 0, # 'w' + 58: 0, # 'x' + 11: 0, # 'y' + 22: 1, # 'z' + 63: 0, # '·' + 54: 2, # 'Ç' + 50: 2, # 'Ö' + 55: 0, # 'Ãœ' + 59: 0, # 'â' + 33: 3, # 'ç' + 61: 0, # 'î' + 34: 1, # 'ö' + 17: 0, # 'ü' + 30: 0, # 'ÄŸ' + 41: 0, # 'Ä°' + 6: 3, # 'ı' + 40: 2, # 'Åž' + 19: 1, # 'ÅŸ' + }, + 31: { # 'T' + 23: 0, # 'A' + 37: 0, # 'B' + 47: 0, # 'C' + 39: 0, # 'D' + 29: 0, # 'E' + 52: 0, # 'F' + 36: 0, # 'G' + 45: 0, # 'H' + 53: 0, # 'I' + 60: 1, # 'J' + 16: 2, # 'K' + 49: 0, # 'L' + 20: 1, # 'M' + 46: 0, # 'N' + 42: 0, # 'O' + 48: 0, # 'P' + 44: 0, # 'R' + 35: 0, # 'S' + 31: 2, # 'T' + 51: 0, # 'U' + 38: 0, # 'V' + 62: 0, # 'W' + 43: 0, # 'Y' + 56: 0, # 'Z' + 1: 3, # 'a' + 21: 2, # 'b' + 28: 0, # 'c' + 12: 1, # 'd' + 2: 3, # 'e' + 18: 2, # 'f' + 27: 2, # 'g' + 25: 0, # 'h' + 3: 1, # 'i' + 24: 1, # 'j' + 10: 2, # 'k' + 5: 2, # 'l' + 13: 3, # 'm' + 4: 3, # 'n' + 15: 0, # 'o' + 26: 2, # 'p' + 7: 2, # 'r' + 8: 0, # 's' + 9: 2, # 't' + 14: 2, # 'u' + 32: 1, # 'v' + 57: 1, # 'w' + 58: 1, # 'x' + 11: 2, # 'y' + 22: 0, # 'z' + 63: 0, # '·' + 54: 0, # 'Ç' + 50: 0, # 'Ö' + 55: 0, # 'Ãœ' + 59: 0, # 'â' + 33: 0, # 'ç' + 61: 0, # 'î' + 34: 0, # 'ö' + 17: 1, # 'ü' + 30: 0, # 'ÄŸ' + 41: 0, # 'Ä°' + 6: 3, # 'ı' + 40: 0, # 'Åž' + 19: 0, # 'ÅŸ' + }, + 51: { # 'U' + 23: 0, # 'A' + 37: 0, # 'B' + 47: 0, # 'C' + 39: 0, # 'D' + 29: 0, # 'E' + 52: 1, # 'F' + 36: 1, # 'G' + 45: 0, # 'H' + 53: 0, # 'I' + 60: 0, # 'J' + 16: 1, # 'K' + 49: 0, # 'L' + 20: 0, # 'M' + 46: 1, # 'N' + 42: 0, # 'O' + 48: 1, # 'P' + 44: 0, # 'R' + 35: 0, # 'S' + 31: 0, # 'T' + 51: 1, # 'U' + 38: 1, # 'V' + 62: 0, # 'W' + 43: 0, # 'Y' + 56: 0, # 'Z' + 1: 3, # 'a' + 21: 0, # 'b' + 28: 1, # 'c' + 12: 0, # 'd' + 2: 3, # 'e' + 18: 0, # 'f' + 27: 2, # 'g' + 25: 0, # 'h' + 3: 0, # 'i' + 24: 0, # 'j' + 10: 1, # 'k' + 5: 1, # 'l' + 13: 3, # 'm' + 4: 2, # 'n' + 15: 0, # 'o' + 26: 1, # 'p' + 7: 0, # 'r' + 8: 0, # 's' + 9: 0, # 't' + 14: 2, # 'u' + 32: 0, # 'v' + 57: 0, # 'w' + 58: 0, # 'x' + 11: 0, # 'y' + 22: 2, # 'z' + 63: 0, # '·' + 54: 1, # 'Ç' + 50: 1, # 'Ö' + 55: 0, # 'Ãœ' + 59: 0, # 'â' + 33: 0, # 'ç' + 61: 0, # 'î' + 34: 0, # 'ö' + 17: 0, # 'ü' + 30: 1, # 'ÄŸ' + 41: 1, # 'Ä°' + 6: 2, # 'ı' + 40: 0, # 'Åž' + 19: 1, # 'ÅŸ' + }, + 38: { # 'V' + 23: 1, # 'A' + 37: 1, # 'B' + 47: 1, # 'C' + 39: 0, # 'D' + 29: 0, # 'E' + 52: 2, # 'F' + 36: 0, # 'G' + 45: 0, # 'H' + 53: 0, # 'I' + 60: 0, # 'J' + 16: 3, # 'K' + 49: 0, # 'L' + 20: 3, # 'M' + 46: 0, # 'N' + 42: 0, # 'O' + 48: 1, # 'P' + 44: 1, # 'R' + 35: 0, # 'S' + 31: 0, # 'T' + 51: 1, # 'U' + 38: 1, # 'V' + 62: 0, # 'W' + 43: 0, # 'Y' + 56: 0, # 'Z' + 1: 3, # 'a' + 21: 0, # 'b' + 28: 2, # 'c' + 12: 0, # 'd' + 2: 3, # 'e' + 18: 0, # 'f' + 27: 0, # 'g' + 25: 0, # 'h' + 3: 0, # 'i' + 24: 0, # 'j' + 10: 0, # 'k' + 5: 2, # 'l' + 13: 2, # 'm' + 4: 0, # 'n' + 15: 2, # 'o' + 26: 0, # 'p' + 7: 0, # 'r' + 8: 0, # 's' + 9: 1, # 't' + 14: 3, # 'u' + 32: 0, # 'v' + 57: 0, # 'w' + 58: 0, # 'x' + 11: 1, # 'y' + 22: 2, # 'z' + 63: 0, # '·' + 54: 1, # 'Ç' + 50: 1, # 'Ö' + 55: 0, # 'Ãœ' + 59: 1, # 'â' + 33: 2, # 'ç' + 61: 0, # 'î' + 34: 1, # 'ö' + 17: 0, # 'ü' + 30: 1, # 'ÄŸ' + 41: 1, # 'Ä°' + 6: 3, # 'ı' + 40: 2, # 'Åž' + 19: 1, # 'ÅŸ' + }, + 62: { # 'W' + 23: 0, # 'A' + 37: 0, # 'B' + 47: 0, # 'C' + 39: 0, # 'D' + 29: 0, # 'E' + 52: 0, # 'F' + 36: 0, # 'G' + 45: 0, # 'H' + 53: 0, # 'I' + 60: 0, # 'J' + 16: 0, # 'K' + 49: 0, # 'L' + 20: 0, # 'M' + 46: 0, # 'N' + 42: 0, # 'O' + 48: 0, # 'P' + 44: 0, # 'R' + 35: 0, # 'S' + 31: 0, # 'T' + 51: 0, # 'U' + 38: 0, # 'V' + 62: 0, # 'W' + 43: 0, # 'Y' + 56: 0, # 'Z' + 1: 0, # 'a' + 21: 0, # 'b' + 28: 0, # 'c' + 12: 0, # 'd' + 2: 0, # 'e' + 18: 0, # 'f' + 27: 0, # 'g' + 25: 0, # 'h' + 3: 0, # 'i' + 24: 0, # 'j' + 10: 0, # 'k' + 5: 0, # 'l' + 13: 0, # 'm' + 4: 0, # 'n' + 15: 0, # 'o' + 26: 0, # 'p' + 7: 0, # 'r' + 8: 0, # 's' + 9: 0, # 't' + 14: 0, # 'u' + 32: 0, # 'v' + 57: 0, # 'w' + 58: 0, # 'x' + 11: 0, # 'y' + 22: 0, # 'z' + 63: 0, # '·' + 54: 0, # 'Ç' + 50: 0, # 'Ö' + 55: 0, # 'Ãœ' + 59: 0, # 'â' + 33: 0, # 'ç' + 61: 0, # 'î' + 34: 0, # 'ö' + 17: 0, # 'ü' + 30: 0, # 'ÄŸ' + 41: 0, # 'Ä°' + 6: 0, # 'ı' + 40: 0, # 'Åž' + 19: 0, # 'ÅŸ' + }, + 43: { # 'Y' + 23: 0, # 'A' + 37: 0, # 'B' + 47: 1, # 'C' + 39: 0, # 'D' + 29: 0, # 'E' + 52: 2, # 'F' + 36: 0, # 'G' + 45: 1, # 'H' + 53: 1, # 'I' + 60: 0, # 'J' + 16: 2, # 'K' + 49: 0, # 'L' + 20: 0, # 'M' + 46: 2, # 'N' + 42: 0, # 'O' + 48: 2, # 'P' + 44: 1, # 'R' + 35: 1, # 'S' + 31: 0, # 'T' + 51: 1, # 'U' + 38: 2, # 'V' + 62: 0, # 'W' + 43: 0, # 'Y' + 56: 0, # 'Z' + 1: 3, # 'a' + 21: 0, # 'b' + 28: 2, # 'c' + 12: 0, # 'd' + 2: 2, # 'e' + 18: 0, # 'f' + 27: 0, # 'g' + 25: 0, # 'h' + 3: 0, # 'i' + 24: 1, # 'j' + 10: 1, # 'k' + 5: 1, # 'l' + 13: 3, # 'm' + 4: 0, # 'n' + 15: 2, # 'o' + 26: 0, # 'p' + 7: 0, # 'r' + 8: 0, # 's' + 9: 0, # 't' + 14: 3, # 'u' + 32: 0, # 'v' + 57: 0, # 'w' + 58: 1, # 'x' + 11: 0, # 'y' + 22: 2, # 'z' + 63: 0, # '·' + 54: 1, # 'Ç' + 50: 2, # 'Ö' + 55: 1, # 'Ãœ' + 59: 1, # 'â' + 33: 0, # 'ç' + 61: 0, # 'î' + 34: 1, # 'ö' + 17: 0, # 'ü' + 30: 1, # 'ÄŸ' + 41: 1, # 'Ä°' + 6: 0, # 'ı' + 40: 2, # 'Åž' + 19: 1, # 'ÅŸ' + }, + 56: { # 'Z' + 23: 0, # 'A' + 37: 0, # 'B' + 47: 0, # 'C' + 39: 0, # 'D' + 29: 0, # 'E' + 52: 0, # 'F' + 36: 0, # 'G' + 45: 0, # 'H' + 53: 0, # 'I' + 60: 0, # 'J' + 16: 0, # 'K' + 49: 0, # 'L' + 20: 0, # 'M' + 46: 0, # 'N' + 42: 0, # 'O' + 48: 0, # 'P' + 44: 0, # 'R' + 35: 0, # 'S' + 31: 0, # 'T' + 51: 0, # 'U' + 38: 0, # 'V' + 62: 0, # 'W' + 43: 0, # 'Y' + 56: 2, # 'Z' + 1: 2, # 'a' + 21: 1, # 'b' + 28: 0, # 'c' + 12: 0, # 'd' + 2: 2, # 'e' + 18: 0, # 'f' + 27: 0, # 'g' + 25: 0, # 'h' + 3: 2, # 'i' + 24: 1, # 'j' + 10: 0, # 'k' + 5: 0, # 'l' + 13: 1, # 'm' + 4: 1, # 'n' + 15: 0, # 'o' + 26: 0, # 'p' + 7: 1, # 'r' + 8: 1, # 's' + 9: 0, # 't' + 14: 2, # 'u' + 32: 0, # 'v' + 57: 0, # 'w' + 58: 0, # 'x' + 11: 0, # 'y' + 22: 0, # 'z' + 63: 0, # '·' + 54: 0, # 'Ç' + 50: 0, # 'Ö' + 55: 0, # 'Ãœ' + 59: 0, # 'â' + 33: 0, # 'ç' + 61: 0, # 'î' + 34: 0, # 'ö' + 17: 1, # 'ü' + 30: 0, # 'ÄŸ' + 41: 0, # 'Ä°' + 6: 1, # 'ı' + 40: 0, # 'Åž' + 19: 0, # 'ÅŸ' + }, + 1: { # 'a' + 23: 3, # 'A' + 37: 0, # 'B' + 47: 1, # 'C' + 39: 0, # 'D' + 29: 3, # 'E' + 52: 0, # 'F' + 36: 1, # 'G' + 45: 1, # 'H' + 53: 0, # 'I' + 60: 0, # 'J' + 16: 0, # 'K' + 49: 0, # 'L' + 20: 3, # 'M' + 46: 1, # 'N' + 42: 0, # 'O' + 48: 1, # 'P' + 44: 0, # 'R' + 35: 0, # 'S' + 31: 3, # 'T' + 51: 0, # 'U' + 38: 1, # 'V' + 62: 0, # 'W' + 43: 0, # 'Y' + 56: 2, # 'Z' + 1: 2, # 'a' + 21: 3, # 'b' + 28: 0, # 'c' + 12: 3, # 'd' + 2: 2, # 'e' + 18: 3, # 'f' + 27: 3, # 'g' + 25: 3, # 'h' + 3: 3, # 'i' + 24: 3, # 'j' + 10: 3, # 'k' + 5: 0, # 'l' + 13: 2, # 'm' + 4: 3, # 'n' + 15: 1, # 'o' + 26: 3, # 'p' + 7: 3, # 'r' + 8: 3, # 's' + 9: 3, # 't' + 14: 3, # 'u' + 32: 3, # 'v' + 57: 2, # 'w' + 58: 0, # 'x' + 11: 3, # 'y' + 22: 0, # 'z' + 63: 1, # '·' + 54: 0, # 'Ç' + 50: 0, # 'Ö' + 55: 0, # 'Ãœ' + 59: 0, # 'â' + 33: 1, # 'ç' + 61: 1, # 'î' + 34: 1, # 'ö' + 17: 3, # 'ü' + 30: 0, # 'ÄŸ' + 41: 0, # 'Ä°' + 6: 3, # 'ı' + 40: 0, # 'Åž' + 19: 1, # 'ÅŸ' + }, + 21: { # 'b' + 23: 0, # 'A' + 37: 0, # 'B' + 47: 0, # 'C' + 39: 0, # 'D' + 29: 0, # 'E' + 52: 0, # 'F' + 36: 1, # 'G' + 45: 0, # 'H' + 53: 0, # 'I' + 60: 1, # 'J' + 16: 2, # 'K' + 49: 0, # 'L' + 20: 2, # 'M' + 46: 0, # 'N' + 42: 0, # 'O' + 48: 0, # 'P' + 44: 0, # 'R' + 35: 0, # 'S' + 31: 1, # 'T' + 51: 0, # 'U' + 38: 0, # 'V' + 62: 0, # 'W' + 43: 1, # 'Y' + 56: 0, # 'Z' + 1: 3, # 'a' + 21: 2, # 'b' + 28: 0, # 'c' + 12: 3, # 'd' + 2: 3, # 'e' + 18: 0, # 'f' + 27: 3, # 'g' + 25: 1, # 'h' + 3: 3, # 'i' + 24: 2, # 'j' + 10: 3, # 'k' + 5: 3, # 'l' + 13: 3, # 'm' + 4: 3, # 'n' + 15: 0, # 'o' + 26: 3, # 'p' + 7: 1, # 'r' + 8: 2, # 's' + 9: 2, # 't' + 14: 2, # 'u' + 32: 1, # 'v' + 57: 0, # 'w' + 58: 1, # 'x' + 11: 3, # 'y' + 22: 0, # 'z' + 63: 0, # '·' + 54: 0, # 'Ç' + 50: 0, # 'Ö' + 55: 0, # 'Ãœ' + 59: 0, # 'â' + 33: 1, # 'ç' + 61: 0, # 'î' + 34: 0, # 'ö' + 17: 0, # 'ü' + 30: 1, # 'ÄŸ' + 41: 0, # 'Ä°' + 6: 2, # 'ı' + 40: 0, # 'Åž' + 19: 0, # 'ÅŸ' + }, + 28: { # 'c' + 23: 0, # 'A' + 37: 1, # 'B' + 47: 1, # 'C' + 39: 1, # 'D' + 29: 2, # 'E' + 52: 0, # 'F' + 36: 2, # 'G' + 45: 2, # 'H' + 53: 1, # 'I' + 60: 0, # 'J' + 16: 0, # 'K' + 49: 0, # 'L' + 20: 2, # 'M' + 46: 1, # 'N' + 42: 1, # 'O' + 48: 2, # 'P' + 44: 1, # 'R' + 35: 1, # 'S' + 31: 2, # 'T' + 51: 2, # 'U' + 38: 2, # 'V' + 62: 0, # 'W' + 43: 3, # 'Y' + 56: 0, # 'Z' + 1: 1, # 'a' + 21: 1, # 'b' + 28: 2, # 'c' + 12: 2, # 'd' + 2: 1, # 'e' + 18: 1, # 'f' + 27: 2, # 'g' + 25: 2, # 'h' + 3: 3, # 'i' + 24: 1, # 'j' + 10: 3, # 'k' + 5: 0, # 'l' + 13: 2, # 'm' + 4: 3, # 'n' + 15: 2, # 'o' + 26: 2, # 'p' + 7: 3, # 'r' + 8: 3, # 's' + 9: 3, # 't' + 14: 1, # 'u' + 32: 0, # 'v' + 57: 1, # 'w' + 58: 0, # 'x' + 11: 2, # 'y' + 22: 1, # 'z' + 63: 1, # '·' + 54: 0, # 'Ç' + 50: 0, # 'Ö' + 55: 1, # 'Ãœ' + 59: 0, # 'â' + 33: 0, # 'ç' + 61: 1, # 'î' + 34: 2, # 'ö' + 17: 2, # 'ü' + 30: 2, # 'ÄŸ' + 41: 1, # 'Ä°' + 6: 3, # 'ı' + 40: 0, # 'Åž' + 19: 2, # 'ÅŸ' + }, + 12: { # 'd' + 23: 1, # 'A' + 37: 0, # 'B' + 47: 0, # 'C' + 39: 0, # 'D' + 29: 0, # 'E' + 52: 0, # 'F' + 36: 0, # 'G' + 45: 0, # 'H' + 53: 0, # 'I' + 60: 2, # 'J' + 16: 3, # 'K' + 49: 0, # 'L' + 20: 3, # 'M' + 46: 0, # 'N' + 42: 0, # 'O' + 48: 0, # 'P' + 44: 0, # 'R' + 35: 1, # 'S' + 31: 1, # 'T' + 51: 0, # 'U' + 38: 0, # 'V' + 62: 0, # 'W' + 43: 0, # 'Y' + 56: 0, # 'Z' + 1: 3, # 'a' + 21: 2, # 'b' + 28: 1, # 'c' + 12: 3, # 'd' + 2: 3, # 'e' + 18: 1, # 'f' + 27: 3, # 'g' + 25: 3, # 'h' + 3: 2, # 'i' + 24: 3, # 'j' + 10: 2, # 'k' + 5: 3, # 'l' + 13: 3, # 'm' + 4: 3, # 'n' + 15: 1, # 'o' + 26: 2, # 'p' + 7: 3, # 'r' + 8: 2, # 's' + 9: 2, # 't' + 14: 3, # 'u' + 32: 1, # 'v' + 57: 0, # 'w' + 58: 1, # 'x' + 11: 3, # 'y' + 22: 1, # 'z' + 63: 1, # '·' + 54: 0, # 'Ç' + 50: 0, # 'Ö' + 55: 0, # 'Ãœ' + 59: 0, # 'â' + 33: 0, # 'ç' + 61: 0, # 'î' + 34: 0, # 'ö' + 17: 1, # 'ü' + 30: 0, # 'ÄŸ' + 41: 0, # 'Ä°' + 6: 2, # 'ı' + 40: 0, # 'Åž' + 19: 0, # 'ÅŸ' + }, + 2: { # 'e' + 23: 2, # 'A' + 37: 0, # 'B' + 47: 2, # 'C' + 39: 0, # 'D' + 29: 3, # 'E' + 52: 1, # 'F' + 36: 0, # 'G' + 45: 0, # 'H' + 53: 0, # 'I' + 60: 0, # 'J' + 16: 1, # 'K' + 49: 0, # 'L' + 20: 3, # 'M' + 46: 1, # 'N' + 42: 0, # 'O' + 48: 1, # 'P' + 44: 1, # 'R' + 35: 0, # 'S' + 31: 3, # 'T' + 51: 0, # 'U' + 38: 1, # 'V' + 62: 0, # 'W' + 43: 1, # 'Y' + 56: 0, # 'Z' + 1: 3, # 'a' + 21: 3, # 'b' + 28: 0, # 'c' + 12: 3, # 'd' + 2: 2, # 'e' + 18: 3, # 'f' + 27: 3, # 'g' + 25: 3, # 'h' + 3: 3, # 'i' + 24: 3, # 'j' + 10: 3, # 'k' + 5: 0, # 'l' + 13: 2, # 'm' + 4: 3, # 'n' + 15: 1, # 'o' + 26: 3, # 'p' + 7: 3, # 'r' + 8: 3, # 's' + 9: 3, # 't' + 14: 3, # 'u' + 32: 3, # 'v' + 57: 2, # 'w' + 58: 0, # 'x' + 11: 3, # 'y' + 22: 1, # 'z' + 63: 1, # '·' + 54: 0, # 'Ç' + 50: 0, # 'Ö' + 55: 0, # 'Ãœ' + 59: 0, # 'â' + 33: 1, # 'ç' + 61: 0, # 'î' + 34: 1, # 'ö' + 17: 3, # 'ü' + 30: 0, # 'ÄŸ' + 41: 0, # 'Ä°' + 6: 3, # 'ı' + 40: 0, # 'Åž' + 19: 0, # 'ÅŸ' + }, + 18: { # 'f' + 23: 0, # 'A' + 37: 0, # 'B' + 47: 0, # 'C' + 39: 0, # 'D' + 29: 0, # 'E' + 52: 0, # 'F' + 36: 0, # 'G' + 45: 0, # 'H' + 53: 0, # 'I' + 60: 0, # 'J' + 16: 2, # 'K' + 49: 0, # 'L' + 20: 2, # 'M' + 46: 0, # 'N' + 42: 0, # 'O' + 48: 0, # 'P' + 44: 0, # 'R' + 35: 0, # 'S' + 31: 2, # 'T' + 51: 0, # 'U' + 38: 0, # 'V' + 62: 0, # 'W' + 43: 0, # 'Y' + 56: 0, # 'Z' + 1: 3, # 'a' + 21: 1, # 'b' + 28: 0, # 'c' + 12: 3, # 'd' + 2: 3, # 'e' + 18: 2, # 'f' + 27: 1, # 'g' + 25: 1, # 'h' + 3: 1, # 'i' + 24: 1, # 'j' + 10: 1, # 'k' + 5: 3, # 'l' + 13: 3, # 'm' + 4: 3, # 'n' + 15: 0, # 'o' + 26: 2, # 'p' + 7: 1, # 'r' + 8: 3, # 's' + 9: 3, # 't' + 14: 1, # 'u' + 32: 2, # 'v' + 57: 0, # 'w' + 58: 0, # 'x' + 11: 1, # 'y' + 22: 0, # 'z' + 63: 0, # '·' + 54: 0, # 'Ç' + 50: 0, # 'Ö' + 55: 0, # 'Ãœ' + 59: 0, # 'â' + 33: 1, # 'ç' + 61: 0, # 'î' + 34: 0, # 'ö' + 17: 1, # 'ü' + 30: 0, # 'ÄŸ' + 41: 0, # 'Ä°' + 6: 1, # 'ı' + 40: 0, # 'Åž' + 19: 0, # 'ÅŸ' + }, + 27: { # 'g' + 23: 0, # 'A' + 37: 0, # 'B' + 47: 0, # 'C' + 39: 0, # 'D' + 29: 0, # 'E' + 52: 0, # 'F' + 36: 0, # 'G' + 45: 0, # 'H' + 53: 0, # 'I' + 60: 0, # 'J' + 16: 3, # 'K' + 49: 0, # 'L' + 20: 0, # 'M' + 46: 0, # 'N' + 42: 0, # 'O' + 48: 0, # 'P' + 44: 0, # 'R' + 35: 1, # 'S' + 31: 1, # 'T' + 51: 0, # 'U' + 38: 2, # 'V' + 62: 0, # 'W' + 43: 0, # 'Y' + 56: 0, # 'Z' + 1: 3, # 'a' + 21: 1, # 'b' + 28: 0, # 'c' + 12: 1, # 'd' + 2: 3, # 'e' + 18: 0, # 'f' + 27: 2, # 'g' + 25: 1, # 'h' + 3: 2, # 'i' + 24: 3, # 'j' + 10: 2, # 'k' + 5: 3, # 'l' + 13: 3, # 'm' + 4: 2, # 'n' + 15: 0, # 'o' + 26: 1, # 'p' + 7: 2, # 'r' + 8: 2, # 's' + 9: 3, # 't' + 14: 3, # 'u' + 32: 1, # 'v' + 57: 0, # 'w' + 58: 0, # 'x' + 11: 1, # 'y' + 22: 0, # 'z' + 63: 1, # '·' + 54: 0, # 'Ç' + 50: 0, # 'Ö' + 55: 0, # 'Ãœ' + 59: 0, # 'â' + 33: 0, # 'ç' + 61: 0, # 'î' + 34: 0, # 'ö' + 17: 0, # 'ü' + 30: 0, # 'ÄŸ' + 41: 0, # 'Ä°' + 6: 2, # 'ı' + 40: 0, # 'Åž' + 19: 0, # 'ÅŸ' + }, + 25: { # 'h' + 23: 0, # 'A' + 37: 0, # 'B' + 47: 0, # 'C' + 39: 0, # 'D' + 29: 0, # 'E' + 52: 0, # 'F' + 36: 0, # 'G' + 45: 0, # 'H' + 53: 0, # 'I' + 60: 0, # 'J' + 16: 2, # 'K' + 49: 0, # 'L' + 20: 0, # 'M' + 46: 0, # 'N' + 42: 0, # 'O' + 48: 0, # 'P' + 44: 0, # 'R' + 35: 0, # 'S' + 31: 0, # 'T' + 51: 0, # 'U' + 38: 0, # 'V' + 62: 0, # 'W' + 43: 0, # 'Y' + 56: 0, # 'Z' + 1: 3, # 'a' + 21: 0, # 'b' + 28: 0, # 'c' + 12: 2, # 'd' + 2: 3, # 'e' + 18: 0, # 'f' + 27: 1, # 'g' + 25: 2, # 'h' + 3: 2, # 'i' + 24: 3, # 'j' + 10: 3, # 'k' + 5: 3, # 'l' + 13: 3, # 'm' + 4: 3, # 'n' + 15: 1, # 'o' + 26: 1, # 'p' + 7: 3, # 'r' + 8: 3, # 's' + 9: 2, # 't' + 14: 3, # 'u' + 32: 2, # 'v' + 57: 1, # 'w' + 58: 0, # 'x' + 11: 1, # 'y' + 22: 0, # 'z' + 63: 0, # '·' + 54: 0, # 'Ç' + 50: 0, # 'Ö' + 55: 0, # 'Ãœ' + 59: 0, # 'â' + 33: 0, # 'ç' + 61: 0, # 'î' + 34: 0, # 'ö' + 17: 0, # 'ü' + 30: 0, # 'ÄŸ' + 41: 0, # 'Ä°' + 6: 3, # 'ı' + 40: 0, # 'Åž' + 19: 0, # 'ÅŸ' + }, + 3: { # 'i' + 23: 2, # 'A' + 37: 0, # 'B' + 47: 0, # 'C' + 39: 0, # 'D' + 29: 0, # 'E' + 52: 0, # 'F' + 36: 0, # 'G' + 45: 0, # 'H' + 53: 0, # 'I' + 60: 1, # 'J' + 16: 3, # 'K' + 49: 0, # 'L' + 20: 3, # 'M' + 46: 0, # 'N' + 42: 1, # 'O' + 48: 0, # 'P' + 44: 0, # 'R' + 35: 1, # 'S' + 31: 2, # 'T' + 51: 0, # 'U' + 38: 1, # 'V' + 62: 0, # 'W' + 43: 0, # 'Y' + 56: 0, # 'Z' + 1: 3, # 'a' + 21: 2, # 'b' + 28: 0, # 'c' + 12: 3, # 'd' + 2: 3, # 'e' + 18: 2, # 'f' + 27: 3, # 'g' + 25: 1, # 'h' + 3: 3, # 'i' + 24: 2, # 'j' + 10: 3, # 'k' + 5: 3, # 'l' + 13: 3, # 'm' + 4: 3, # 'n' + 15: 1, # 'o' + 26: 3, # 'p' + 7: 3, # 'r' + 8: 3, # 's' + 9: 3, # 't' + 14: 3, # 'u' + 32: 2, # 'v' + 57: 1, # 'w' + 58: 1, # 'x' + 11: 3, # 'y' + 22: 1, # 'z' + 63: 1, # '·' + 54: 0, # 'Ç' + 50: 0, # 'Ö' + 55: 1, # 'Ãœ' + 59: 0, # 'â' + 33: 2, # 'ç' + 61: 0, # 'î' + 34: 0, # 'ö' + 17: 3, # 'ü' + 30: 0, # 'ÄŸ' + 41: 1, # 'Ä°' + 6: 2, # 'ı' + 40: 0, # 'Åž' + 19: 0, # 'ÅŸ' + }, + 24: { # 'j' + 23: 0, # 'A' + 37: 0, # 'B' + 47: 0, # 'C' + 39: 0, # 'D' + 29: 0, # 'E' + 52: 0, # 'F' + 36: 0, # 'G' + 45: 0, # 'H' + 53: 0, # 'I' + 60: 1, # 'J' + 16: 2, # 'K' + 49: 0, # 'L' + 20: 2, # 'M' + 46: 0, # 'N' + 42: 0, # 'O' + 48: 1, # 'P' + 44: 0, # 'R' + 35: 0, # 'S' + 31: 1, # 'T' + 51: 0, # 'U' + 38: 0, # 'V' + 62: 0, # 'W' + 43: 0, # 'Y' + 56: 1, # 'Z' + 1: 3, # 'a' + 21: 1, # 'b' + 28: 1, # 'c' + 12: 3, # 'd' + 2: 3, # 'e' + 18: 2, # 'f' + 27: 1, # 'g' + 25: 1, # 'h' + 3: 2, # 'i' + 24: 1, # 'j' + 10: 2, # 'k' + 5: 2, # 'l' + 13: 3, # 'm' + 4: 2, # 'n' + 15: 0, # 'o' + 26: 1, # 'p' + 7: 2, # 'r' + 8: 3, # 's' + 9: 2, # 't' + 14: 3, # 'u' + 32: 2, # 'v' + 57: 0, # 'w' + 58: 2, # 'x' + 11: 1, # 'y' + 22: 0, # 'z' + 63: 0, # '·' + 54: 0, # 'Ç' + 50: 0, # 'Ö' + 55: 0, # 'Ãœ' + 59: 0, # 'â' + 33: 1, # 'ç' + 61: 0, # 'î' + 34: 0, # 'ö' + 17: 1, # 'ü' + 30: 0, # 'ÄŸ' + 41: 0, # 'Ä°' + 6: 3, # 'ı' + 40: 0, # 'Åž' + 19: 0, # 'ÅŸ' + }, + 10: { # 'k' + 23: 0, # 'A' + 37: 0, # 'B' + 47: 0, # 'C' + 39: 0, # 'D' + 29: 0, # 'E' + 52: 0, # 'F' + 36: 0, # 'G' + 45: 0, # 'H' + 53: 0, # 'I' + 60: 0, # 'J' + 16: 3, # 'K' + 49: 0, # 'L' + 20: 2, # 'M' + 46: 0, # 'N' + 42: 0, # 'O' + 48: 0, # 'P' + 44: 0, # 'R' + 35: 0, # 'S' + 31: 3, # 'T' + 51: 0, # 'U' + 38: 1, # 'V' + 62: 0, # 'W' + 43: 0, # 'Y' + 56: 1, # 'Z' + 1: 3, # 'a' + 21: 2, # 'b' + 28: 0, # 'c' + 12: 2, # 'd' + 2: 3, # 'e' + 18: 1, # 'f' + 27: 2, # 'g' + 25: 2, # 'h' + 3: 3, # 'i' + 24: 2, # 'j' + 10: 2, # 'k' + 5: 3, # 'l' + 13: 3, # 'm' + 4: 3, # 'n' + 15: 0, # 'o' + 26: 3, # 'p' + 7: 2, # 'r' + 8: 2, # 's' + 9: 2, # 't' + 14: 3, # 'u' + 32: 0, # 'v' + 57: 0, # 'w' + 58: 1, # 'x' + 11: 3, # 'y' + 22: 0, # 'z' + 63: 1, # '·' + 54: 0, # 'Ç' + 50: 0, # 'Ö' + 55: 0, # 'Ãœ' + 59: 0, # 'â' + 33: 3, # 'ç' + 61: 0, # 'î' + 34: 1, # 'ö' + 17: 3, # 'ü' + 30: 1, # 'ÄŸ' + 41: 0, # 'Ä°' + 6: 3, # 'ı' + 40: 0, # 'Åž' + 19: 1, # 'ÅŸ' + }, + 5: { # 'l' + 23: 0, # 'A' + 37: 0, # 'B' + 47: 0, # 'C' + 39: 0, # 'D' + 29: 3, # 'E' + 52: 0, # 'F' + 36: 0, # 'G' + 45: 0, # 'H' + 53: 0, # 'I' + 60: 0, # 'J' + 16: 0, # 'K' + 49: 0, # 'L' + 20: 2, # 'M' + 46: 0, # 'N' + 42: 0, # 'O' + 48: 0, # 'P' + 44: 0, # 'R' + 35: 0, # 'S' + 31: 1, # 'T' + 51: 0, # 'U' + 38: 0, # 'V' + 62: 0, # 'W' + 43: 0, # 'Y' + 56: 0, # 'Z' + 1: 0, # 'a' + 21: 3, # 'b' + 28: 0, # 'c' + 12: 3, # 'd' + 2: 1, # 'e' + 18: 3, # 'f' + 27: 3, # 'g' + 25: 2, # 'h' + 3: 3, # 'i' + 24: 2, # 'j' + 10: 3, # 'k' + 5: 1, # 'l' + 13: 1, # 'm' + 4: 3, # 'n' + 15: 0, # 'o' + 26: 2, # 'p' + 7: 3, # 'r' + 8: 3, # 's' + 9: 3, # 't' + 14: 2, # 'u' + 32: 2, # 'v' + 57: 0, # 'w' + 58: 0, # 'x' + 11: 3, # 'y' + 22: 0, # 'z' + 63: 0, # '·' + 54: 0, # 'Ç' + 50: 0, # 'Ö' + 55: 0, # 'Ãœ' + 59: 0, # 'â' + 33: 1, # 'ç' + 61: 0, # 'î' + 34: 0, # 'ö' + 17: 2, # 'ü' + 30: 0, # 'ÄŸ' + 41: 0, # 'Ä°' + 6: 3, # 'ı' + 40: 0, # 'Åž' + 19: 0, # 'ÅŸ' + }, + 13: { # 'm' + 23: 1, # 'A' + 37: 0, # 'B' + 47: 0, # 'C' + 39: 0, # 'D' + 29: 3, # 'E' + 52: 0, # 'F' + 36: 0, # 'G' + 45: 0, # 'H' + 53: 0, # 'I' + 60: 0, # 'J' + 16: 0, # 'K' + 49: 0, # 'L' + 20: 3, # 'M' + 46: 0, # 'N' + 42: 0, # 'O' + 48: 0, # 'P' + 44: 0, # 'R' + 35: 0, # 'S' + 31: 3, # 'T' + 51: 0, # 'U' + 38: 0, # 'V' + 62: 0, # 'W' + 43: 1, # 'Y' + 56: 0, # 'Z' + 1: 2, # 'a' + 21: 3, # 'b' + 28: 0, # 'c' + 12: 3, # 'd' + 2: 2, # 'e' + 18: 3, # 'f' + 27: 3, # 'g' + 25: 3, # 'h' + 3: 3, # 'i' + 24: 3, # 'j' + 10: 3, # 'k' + 5: 0, # 'l' + 13: 2, # 'm' + 4: 3, # 'n' + 15: 1, # 'o' + 26: 2, # 'p' + 7: 3, # 'r' + 8: 3, # 's' + 9: 3, # 't' + 14: 2, # 'u' + 32: 2, # 'v' + 57: 1, # 'w' + 58: 0, # 'x' + 11: 3, # 'y' + 22: 0, # 'z' + 63: 0, # '·' + 54: 0, # 'Ç' + 50: 0, # 'Ö' + 55: 0, # 'Ãœ' + 59: 0, # 'â' + 33: 0, # 'ç' + 61: 0, # 'î' + 34: 0, # 'ö' + 17: 3, # 'ü' + 30: 0, # 'ÄŸ' + 41: 0, # 'Ä°' + 6: 3, # 'ı' + 40: 0, # 'Åž' + 19: 1, # 'ÅŸ' + }, + 4: { # 'n' + 23: 1, # 'A' + 37: 0, # 'B' + 47: 0, # 'C' + 39: 0, # 'D' + 29: 0, # 'E' + 52: 0, # 'F' + 36: 0, # 'G' + 45: 1, # 'H' + 53: 0, # 'I' + 60: 2, # 'J' + 16: 3, # 'K' + 49: 0, # 'L' + 20: 3, # 'M' + 46: 0, # 'N' + 42: 0, # 'O' + 48: 0, # 'P' + 44: 0, # 'R' + 35: 0, # 'S' + 31: 2, # 'T' + 51: 0, # 'U' + 38: 0, # 'V' + 62: 0, # 'W' + 43: 0, # 'Y' + 56: 0, # 'Z' + 1: 3, # 'a' + 21: 2, # 'b' + 28: 1, # 'c' + 12: 3, # 'd' + 2: 3, # 'e' + 18: 1, # 'f' + 27: 2, # 'g' + 25: 3, # 'h' + 3: 2, # 'i' + 24: 2, # 'j' + 10: 3, # 'k' + 5: 3, # 'l' + 13: 3, # 'm' + 4: 3, # 'n' + 15: 1, # 'o' + 26: 3, # 'p' + 7: 2, # 'r' + 8: 3, # 's' + 9: 3, # 't' + 14: 3, # 'u' + 32: 2, # 'v' + 57: 0, # 'w' + 58: 2, # 'x' + 11: 3, # 'y' + 22: 0, # 'z' + 63: 0, # '·' + 54: 0, # 'Ç' + 50: 0, # 'Ö' + 55: 0, # 'Ãœ' + 59: 0, # 'â' + 33: 1, # 'ç' + 61: 0, # 'î' + 34: 0, # 'ö' + 17: 2, # 'ü' + 30: 0, # 'ÄŸ' + 41: 0, # 'Ä°' + 6: 1, # 'ı' + 40: 0, # 'Åž' + 19: 0, # 'ÅŸ' + }, + 15: { # 'o' + 23: 0, # 'A' + 37: 0, # 'B' + 47: 1, # 'C' + 39: 0, # 'D' + 29: 0, # 'E' + 52: 2, # 'F' + 36: 1, # 'G' + 45: 1, # 'H' + 53: 1, # 'I' + 60: 0, # 'J' + 16: 3, # 'K' + 49: 2, # 'L' + 20: 0, # 'M' + 46: 2, # 'N' + 42: 1, # 'O' + 48: 2, # 'P' + 44: 1, # 'R' + 35: 0, # 'S' + 31: 0, # 'T' + 51: 0, # 'U' + 38: 0, # 'V' + 62: 0, # 'W' + 43: 0, # 'Y' + 56: 0, # 'Z' + 1: 3, # 'a' + 21: 0, # 'b' + 28: 2, # 'c' + 12: 0, # 'd' + 2: 3, # 'e' + 18: 0, # 'f' + 27: 0, # 'g' + 25: 0, # 'h' + 3: 1, # 'i' + 24: 2, # 'j' + 10: 1, # 'k' + 5: 3, # 'l' + 13: 3, # 'm' + 4: 2, # 'n' + 15: 2, # 'o' + 26: 0, # 'p' + 7: 1, # 'r' + 8: 0, # 's' + 9: 0, # 't' + 14: 3, # 'u' + 32: 0, # 'v' + 57: 0, # 'w' + 58: 2, # 'x' + 11: 0, # 'y' + 22: 2, # 'z' + 63: 0, # '·' + 54: 1, # 'Ç' + 50: 2, # 'Ö' + 55: 0, # 'Ãœ' + 59: 0, # 'â' + 33: 3, # 'ç' + 61: 0, # 'î' + 34: 1, # 'ö' + 17: 0, # 'ü' + 30: 2, # 'ÄŸ' + 41: 2, # 'Ä°' + 6: 3, # 'ı' + 40: 2, # 'Åž' + 19: 2, # 'ÅŸ' + }, + 26: { # 'p' + 23: 0, # 'A' + 37: 0, # 'B' + 47: 0, # 'C' + 39: 0, # 'D' + 29: 0, # 'E' + 52: 0, # 'F' + 36: 0, # 'G' + 45: 0, # 'H' + 53: 0, # 'I' + 60: 0, # 'J' + 16: 3, # 'K' + 49: 0, # 'L' + 20: 1, # 'M' + 46: 0, # 'N' + 42: 0, # 'O' + 48: 0, # 'P' + 44: 0, # 'R' + 35: 0, # 'S' + 31: 0, # 'T' + 51: 0, # 'U' + 38: 0, # 'V' + 62: 0, # 'W' + 43: 0, # 'Y' + 56: 0, # 'Z' + 1: 3, # 'a' + 21: 1, # 'b' + 28: 0, # 'c' + 12: 1, # 'd' + 2: 3, # 'e' + 18: 0, # 'f' + 27: 1, # 'g' + 25: 1, # 'h' + 3: 2, # 'i' + 24: 3, # 'j' + 10: 1, # 'k' + 5: 3, # 'l' + 13: 3, # 'm' + 4: 2, # 'n' + 15: 0, # 'o' + 26: 2, # 'p' + 7: 2, # 'r' + 8: 1, # 's' + 9: 1, # 't' + 14: 3, # 'u' + 32: 0, # 'v' + 57: 0, # 'w' + 58: 1, # 'x' + 11: 1, # 'y' + 22: 0, # 'z' + 63: 0, # '·' + 54: 0, # 'Ç' + 50: 0, # 'Ö' + 55: 0, # 'Ãœ' + 59: 0, # 'â' + 33: 3, # 'ç' + 61: 0, # 'î' + 34: 0, # 'ö' + 17: 1, # 'ü' + 30: 0, # 'ÄŸ' + 41: 0, # 'Ä°' + 6: 3, # 'ı' + 40: 0, # 'Åž' + 19: 0, # 'ÅŸ' + }, + 7: { # 'r' + 23: 0, # 'A' + 37: 0, # 'B' + 47: 0, # 'C' + 39: 0, # 'D' + 29: 0, # 'E' + 52: 1, # 'F' + 36: 0, # 'G' + 45: 0, # 'H' + 53: 0, # 'I' + 60: 2, # 'J' + 16: 3, # 'K' + 49: 0, # 'L' + 20: 2, # 'M' + 46: 0, # 'N' + 42: 0, # 'O' + 48: 0, # 'P' + 44: 0, # 'R' + 35: 0, # 'S' + 31: 2, # 'T' + 51: 1, # 'U' + 38: 0, # 'V' + 62: 0, # 'W' + 43: 0, # 'Y' + 56: 1, # 'Z' + 1: 3, # 'a' + 21: 1, # 'b' + 28: 0, # 'c' + 12: 3, # 'd' + 2: 3, # 'e' + 18: 0, # 'f' + 27: 2, # 'g' + 25: 3, # 'h' + 3: 2, # 'i' + 24: 2, # 'j' + 10: 3, # 'k' + 5: 3, # 'l' + 13: 3, # 'm' + 4: 3, # 'n' + 15: 0, # 'o' + 26: 2, # 'p' + 7: 3, # 'r' + 8: 3, # 's' + 9: 3, # 't' + 14: 3, # 'u' + 32: 2, # 'v' + 57: 0, # 'w' + 58: 1, # 'x' + 11: 2, # 'y' + 22: 0, # 'z' + 63: 1, # '·' + 54: 0, # 'Ç' + 50: 0, # 'Ö' + 55: 0, # 'Ãœ' + 59: 0, # 'â' + 33: 2, # 'ç' + 61: 0, # 'î' + 34: 0, # 'ö' + 17: 3, # 'ü' + 30: 0, # 'ÄŸ' + 41: 0, # 'Ä°' + 6: 2, # 'ı' + 40: 0, # 'Åž' + 19: 0, # 'ÅŸ' + }, + 8: { # 's' + 23: 1, # 'A' + 37: 0, # 'B' + 47: 0, # 'C' + 39: 0, # 'D' + 29: 0, # 'E' + 52: 0, # 'F' + 36: 1, # 'G' + 45: 0, # 'H' + 53: 0, # 'I' + 60: 0, # 'J' + 16: 3, # 'K' + 49: 0, # 'L' + 20: 3, # 'M' + 46: 0, # 'N' + 42: 0, # 'O' + 48: 0, # 'P' + 44: 0, # 'R' + 35: 0, # 'S' + 31: 2, # 'T' + 51: 0, # 'U' + 38: 0, # 'V' + 62: 0, # 'W' + 43: 0, # 'Y' + 56: 1, # 'Z' + 1: 3, # 'a' + 21: 2, # 'b' + 28: 1, # 'c' + 12: 3, # 'd' + 2: 3, # 'e' + 18: 0, # 'f' + 27: 2, # 'g' + 25: 2, # 'h' + 3: 2, # 'i' + 24: 3, # 'j' + 10: 3, # 'k' + 5: 3, # 'l' + 13: 3, # 'm' + 4: 3, # 'n' + 15: 0, # 'o' + 26: 3, # 'p' + 7: 3, # 'r' + 8: 3, # 's' + 9: 3, # 't' + 14: 3, # 'u' + 32: 2, # 'v' + 57: 0, # 'w' + 58: 1, # 'x' + 11: 2, # 'y' + 22: 1, # 'z' + 63: 0, # '·' + 54: 0, # 'Ç' + 50: 0, # 'Ö' + 55: 0, # 'Ãœ' + 59: 0, # 'â' + 33: 2, # 'ç' + 61: 0, # 'î' + 34: 0, # 'ö' + 17: 2, # 'ü' + 30: 0, # 'ÄŸ' + 41: 0, # 'Ä°' + 6: 3, # 'ı' + 40: 0, # 'Åž' + 19: 1, # 'ÅŸ' + }, + 9: { # 't' + 23: 0, # 'A' + 37: 0, # 'B' + 47: 0, # 'C' + 39: 0, # 'D' + 29: 0, # 'E' + 52: 0, # 'F' + 36: 0, # 'G' + 45: 0, # 'H' + 53: 0, # 'I' + 60: 1, # 'J' + 16: 3, # 'K' + 49: 0, # 'L' + 20: 2, # 'M' + 46: 0, # 'N' + 42: 0, # 'O' + 48: 0, # 'P' + 44: 0, # 'R' + 35: 0, # 'S' + 31: 2, # 'T' + 51: 0, # 'U' + 38: 0, # 'V' + 62: 0, # 'W' + 43: 0, # 'Y' + 56: 1, # 'Z' + 1: 3, # 'a' + 21: 3, # 'b' + 28: 0, # 'c' + 12: 3, # 'd' + 2: 3, # 'e' + 18: 2, # 'f' + 27: 2, # 'g' + 25: 2, # 'h' + 3: 2, # 'i' + 24: 2, # 'j' + 10: 3, # 'k' + 5: 3, # 'l' + 13: 3, # 'm' + 4: 3, # 'n' + 15: 0, # 'o' + 26: 2, # 'p' + 7: 3, # 'r' + 8: 3, # 's' + 9: 3, # 't' + 14: 3, # 'u' + 32: 3, # 'v' + 57: 0, # 'w' + 58: 2, # 'x' + 11: 2, # 'y' + 22: 0, # 'z' + 63: 0, # '·' + 54: 0, # 'Ç' + 50: 0, # 'Ö' + 55: 0, # 'Ãœ' + 59: 0, # 'â' + 33: 3, # 'ç' + 61: 0, # 'î' + 34: 0, # 'ö' + 17: 2, # 'ü' + 30: 0, # 'ÄŸ' + 41: 0, # 'Ä°' + 6: 3, # 'ı' + 40: 0, # 'Åž' + 19: 0, # 'ÅŸ' + }, + 14: { # 'u' + 23: 3, # 'A' + 37: 0, # 'B' + 47: 0, # 'C' + 39: 0, # 'D' + 29: 3, # 'E' + 52: 0, # 'F' + 36: 0, # 'G' + 45: 1, # 'H' + 53: 0, # 'I' + 60: 1, # 'J' + 16: 0, # 'K' + 49: 0, # 'L' + 20: 3, # 'M' + 46: 2, # 'N' + 42: 0, # 'O' + 48: 1, # 'P' + 44: 0, # 'R' + 35: 0, # 'S' + 31: 3, # 'T' + 51: 0, # 'U' + 38: 0, # 'V' + 62: 0, # 'W' + 43: 1, # 'Y' + 56: 2, # 'Z' + 1: 2, # 'a' + 21: 3, # 'b' + 28: 0, # 'c' + 12: 3, # 'd' + 2: 2, # 'e' + 18: 2, # 'f' + 27: 3, # 'g' + 25: 3, # 'h' + 3: 3, # 'i' + 24: 2, # 'j' + 10: 3, # 'k' + 5: 0, # 'l' + 13: 3, # 'm' + 4: 3, # 'n' + 15: 0, # 'o' + 26: 3, # 'p' + 7: 3, # 'r' + 8: 3, # 's' + 9: 3, # 't' + 14: 3, # 'u' + 32: 2, # 'v' + 57: 2, # 'w' + 58: 0, # 'x' + 11: 3, # 'y' + 22: 0, # 'z' + 63: 1, # '·' + 54: 0, # 'Ç' + 50: 0, # 'Ö' + 55: 0, # 'Ãœ' + 59: 0, # 'â' + 33: 0, # 'ç' + 61: 0, # 'î' + 34: 0, # 'ö' + 17: 3, # 'ü' + 30: 1, # 'ÄŸ' + 41: 0, # 'Ä°' + 6: 3, # 'ı' + 40: 0, # 'Åž' + 19: 0, # 'ÅŸ' + }, + 32: { # 'v' + 23: 0, # 'A' + 37: 0, # 'B' + 47: 0, # 'C' + 39: 0, # 'D' + 29: 0, # 'E' + 52: 0, # 'F' + 36: 0, # 'G' + 45: 0, # 'H' + 53: 0, # 'I' + 60: 0, # 'J' + 16: 3, # 'K' + 49: 0, # 'L' + 20: 1, # 'M' + 46: 0, # 'N' + 42: 0, # 'O' + 48: 0, # 'P' + 44: 0, # 'R' + 35: 0, # 'S' + 31: 0, # 'T' + 51: 0, # 'U' + 38: 0, # 'V' + 62: 0, # 'W' + 43: 0, # 'Y' + 56: 0, # 'Z' + 1: 3, # 'a' + 21: 0, # 'b' + 28: 0, # 'c' + 12: 3, # 'd' + 2: 3, # 'e' + 18: 0, # 'f' + 27: 0, # 'g' + 25: 0, # 'h' + 3: 0, # 'i' + 24: 1, # 'j' + 10: 1, # 'k' + 5: 3, # 'l' + 13: 2, # 'm' + 4: 3, # 'n' + 15: 0, # 'o' + 26: 1, # 'p' + 7: 1, # 'r' + 8: 2, # 's' + 9: 3, # 't' + 14: 3, # 'u' + 32: 1, # 'v' + 57: 0, # 'w' + 58: 0, # 'x' + 11: 0, # 'y' + 22: 0, # 'z' + 63: 0, # '·' + 54: 0, # 'Ç' + 50: 0, # 'Ö' + 55: 0, # 'Ãœ' + 59: 0, # 'â' + 33: 2, # 'ç' + 61: 0, # 'î' + 34: 0, # 'ö' + 17: 0, # 'ü' + 30: 0, # 'ÄŸ' + 41: 0, # 'Ä°' + 6: 1, # 'ı' + 40: 0, # 'Åž' + 19: 0, # 'ÅŸ' + }, + 57: { # 'w' + 23: 0, # 'A' + 37: 0, # 'B' + 47: 0, # 'C' + 39: 0, # 'D' + 29: 0, # 'E' + 52: 0, # 'F' + 36: 0, # 'G' + 45: 0, # 'H' + 53: 0, # 'I' + 60: 0, # 'J' + 16: 0, # 'K' + 49: 0, # 'L' + 20: 0, # 'M' + 46: 0, # 'N' + 42: 0, # 'O' + 48: 0, # 'P' + 44: 0, # 'R' + 35: 0, # 'S' + 31: 0, # 'T' + 51: 1, # 'U' + 38: 0, # 'V' + 62: 0, # 'W' + 43: 0, # 'Y' + 56: 0, # 'Z' + 1: 1, # 'a' + 21: 0, # 'b' + 28: 0, # 'c' + 12: 0, # 'd' + 2: 2, # 'e' + 18: 0, # 'f' + 27: 0, # 'g' + 25: 1, # 'h' + 3: 0, # 'i' + 24: 0, # 'j' + 10: 1, # 'k' + 5: 0, # 'l' + 13: 0, # 'm' + 4: 1, # 'n' + 15: 0, # 'o' + 26: 0, # 'p' + 7: 0, # 'r' + 8: 1, # 's' + 9: 0, # 't' + 14: 1, # 'u' + 32: 0, # 'v' + 57: 2, # 'w' + 58: 0, # 'x' + 11: 0, # 'y' + 22: 0, # 'z' + 63: 1, # '·' + 54: 0, # 'Ç' + 50: 0, # 'Ö' + 55: 0, # 'Ãœ' + 59: 0, # 'â' + 33: 0, # 'ç' + 61: 0, # 'î' + 34: 0, # 'ö' + 17: 1, # 'ü' + 30: 0, # 'ÄŸ' + 41: 0, # 'Ä°' + 6: 0, # 'ı' + 40: 0, # 'Åž' + 19: 0, # 'ÅŸ' + }, + 58: { # 'x' + 23: 0, # 'A' + 37: 0, # 'B' + 47: 0, # 'C' + 39: 0, # 'D' + 29: 1, # 'E' + 52: 0, # 'F' + 36: 0, # 'G' + 45: 0, # 'H' + 53: 0, # 'I' + 60: 1, # 'J' + 16: 0, # 'K' + 49: 0, # 'L' + 20: 1, # 'M' + 46: 0, # 'N' + 42: 0, # 'O' + 48: 0, # 'P' + 44: 0, # 'R' + 35: 0, # 'S' + 31: 0, # 'T' + 51: 0, # 'U' + 38: 0, # 'V' + 62: 0, # 'W' + 43: 0, # 'Y' + 56: 0, # 'Z' + 1: 0, # 'a' + 21: 1, # 'b' + 28: 0, # 'c' + 12: 2, # 'd' + 2: 1, # 'e' + 18: 0, # 'f' + 27: 0, # 'g' + 25: 0, # 'h' + 3: 2, # 'i' + 24: 2, # 'j' + 10: 1, # 'k' + 5: 0, # 'l' + 13: 0, # 'm' + 4: 2, # 'n' + 15: 0, # 'o' + 26: 0, # 'p' + 7: 1, # 'r' + 8: 2, # 's' + 9: 1, # 't' + 14: 0, # 'u' + 32: 0, # 'v' + 57: 0, # 'w' + 58: 0, # 'x' + 11: 2, # 'y' + 22: 0, # 'z' + 63: 0, # '·' + 54: 0, # 'Ç' + 50: 0, # 'Ö' + 55: 0, # 'Ãœ' + 59: 0, # 'â' + 33: 0, # 'ç' + 61: 0, # 'î' + 34: 0, # 'ö' + 17: 1, # 'ü' + 30: 0, # 'ÄŸ' + 41: 0, # 'Ä°' + 6: 2, # 'ı' + 40: 0, # 'Åž' + 19: 0, # 'ÅŸ' + }, + 11: { # 'y' + 23: 1, # 'A' + 37: 0, # 'B' + 47: 0, # 'C' + 39: 0, # 'D' + 29: 0, # 'E' + 52: 0, # 'F' + 36: 0, # 'G' + 45: 0, # 'H' + 53: 0, # 'I' + 60: 1, # 'J' + 16: 3, # 'K' + 49: 0, # 'L' + 20: 1, # 'M' + 46: 0, # 'N' + 42: 0, # 'O' + 48: 0, # 'P' + 44: 0, # 'R' + 35: 0, # 'S' + 31: 1, # 'T' + 51: 0, # 'U' + 38: 0, # 'V' + 62: 0, # 'W' + 43: 1, # 'Y' + 56: 1, # 'Z' + 1: 3, # 'a' + 21: 1, # 'b' + 28: 0, # 'c' + 12: 2, # 'd' + 2: 3, # 'e' + 18: 0, # 'f' + 27: 2, # 'g' + 25: 2, # 'h' + 3: 2, # 'i' + 24: 1, # 'j' + 10: 2, # 'k' + 5: 3, # 'l' + 13: 3, # 'm' + 4: 3, # 'n' + 15: 0, # 'o' + 26: 1, # 'p' + 7: 2, # 'r' + 8: 1, # 's' + 9: 2, # 't' + 14: 3, # 'u' + 32: 0, # 'v' + 57: 0, # 'w' + 58: 1, # 'x' + 11: 3, # 'y' + 22: 0, # 'z' + 63: 0, # '·' + 54: 0, # 'Ç' + 50: 0, # 'Ö' + 55: 0, # 'Ãœ' + 59: 0, # 'â' + 33: 3, # 'ç' + 61: 0, # 'î' + 34: 0, # 'ö' + 17: 2, # 'ü' + 30: 0, # 'ÄŸ' + 41: 0, # 'Ä°' + 6: 3, # 'ı' + 40: 0, # 'Åž' + 19: 0, # 'ÅŸ' + }, + 22: { # 'z' + 23: 2, # 'A' + 37: 2, # 'B' + 47: 1, # 'C' + 39: 2, # 'D' + 29: 3, # 'E' + 52: 1, # 'F' + 36: 2, # 'G' + 45: 2, # 'H' + 53: 1, # 'I' + 60: 0, # 'J' + 16: 0, # 'K' + 49: 0, # 'L' + 20: 3, # 'M' + 46: 2, # 'N' + 42: 2, # 'O' + 48: 2, # 'P' + 44: 1, # 'R' + 35: 1, # 'S' + 31: 3, # 'T' + 51: 2, # 'U' + 38: 2, # 'V' + 62: 0, # 'W' + 43: 2, # 'Y' + 56: 1, # 'Z' + 1: 1, # 'a' + 21: 2, # 'b' + 28: 1, # 'c' + 12: 2, # 'd' + 2: 2, # 'e' + 18: 3, # 'f' + 27: 2, # 'g' + 25: 2, # 'h' + 3: 3, # 'i' + 24: 2, # 'j' + 10: 3, # 'k' + 5: 0, # 'l' + 13: 2, # 'm' + 4: 3, # 'n' + 15: 2, # 'o' + 26: 2, # 'p' + 7: 3, # 'r' + 8: 3, # 's' + 9: 3, # 't' + 14: 0, # 'u' + 32: 2, # 'v' + 57: 0, # 'w' + 58: 0, # 'x' + 11: 3, # 'y' + 22: 2, # 'z' + 63: 1, # '·' + 54: 0, # 'Ç' + 50: 0, # 'Ö' + 55: 2, # 'Ãœ' + 59: 1, # 'â' + 33: 0, # 'ç' + 61: 0, # 'î' + 34: 2, # 'ö' + 17: 2, # 'ü' + 30: 2, # 'ÄŸ' + 41: 1, # 'Ä°' + 6: 3, # 'ı' + 40: 1, # 'Åž' + 19: 2, # 'ÅŸ' + }, + 63: { # '·' + 23: 0, # 'A' + 37: 0, # 'B' + 47: 0, # 'C' + 39: 0, # 'D' + 29: 0, # 'E' + 52: 0, # 'F' + 36: 0, # 'G' + 45: 0, # 'H' + 53: 0, # 'I' + 60: 0, # 'J' + 16: 0, # 'K' + 49: 0, # 'L' + 20: 0, # 'M' + 46: 0, # 'N' + 42: 0, # 'O' + 48: 0, # 'P' + 44: 0, # 'R' + 35: 0, # 'S' + 31: 0, # 'T' + 51: 0, # 'U' + 38: 0, # 'V' + 62: 0, # 'W' + 43: 0, # 'Y' + 56: 0, # 'Z' + 1: 0, # 'a' + 21: 0, # 'b' + 28: 0, # 'c' + 12: 0, # 'd' + 2: 1, # 'e' + 18: 0, # 'f' + 27: 0, # 'g' + 25: 0, # 'h' + 3: 0, # 'i' + 24: 0, # 'j' + 10: 0, # 'k' + 5: 0, # 'l' + 13: 2, # 'm' + 4: 0, # 'n' + 15: 0, # 'o' + 26: 0, # 'p' + 7: 0, # 'r' + 8: 0, # 's' + 9: 0, # 't' + 14: 2, # 'u' + 32: 0, # 'v' + 57: 0, # 'w' + 58: 0, # 'x' + 11: 0, # 'y' + 22: 0, # 'z' + 63: 0, # '·' + 54: 0, # 'Ç' + 50: 0, # 'Ö' + 55: 0, # 'Ãœ' + 59: 0, # 'â' + 33: 0, # 'ç' + 61: 0, # 'î' + 34: 0, # 'ö' + 17: 0, # 'ü' + 30: 0, # 'ÄŸ' + 41: 0, # 'Ä°' + 6: 0, # 'ı' + 40: 0, # 'Åž' + 19: 0, # 'ÅŸ' + }, + 54: { # 'Ç' + 23: 0, # 'A' + 37: 0, # 'B' + 47: 1, # 'C' + 39: 1, # 'D' + 29: 0, # 'E' + 52: 0, # 'F' + 36: 1, # 'G' + 45: 1, # 'H' + 53: 1, # 'I' + 60: 0, # 'J' + 16: 0, # 'K' + 49: 0, # 'L' + 20: 0, # 'M' + 46: 0, # 'N' + 42: 1, # 'O' + 48: 1, # 'P' + 44: 0, # 'R' + 35: 0, # 'S' + 31: 0, # 'T' + 51: 1, # 'U' + 38: 1, # 'V' + 62: 0, # 'W' + 43: 2, # 'Y' + 56: 0, # 'Z' + 1: 0, # 'a' + 21: 1, # 'b' + 28: 0, # 'c' + 12: 1, # 'd' + 2: 0, # 'e' + 18: 0, # 'f' + 27: 1, # 'g' + 25: 0, # 'h' + 3: 3, # 'i' + 24: 0, # 'j' + 10: 1, # 'k' + 5: 0, # 'l' + 13: 0, # 'm' + 4: 2, # 'n' + 15: 1, # 'o' + 26: 0, # 'p' + 7: 2, # 'r' + 8: 0, # 's' + 9: 1, # 't' + 14: 0, # 'u' + 32: 2, # 'v' + 57: 0, # 'w' + 58: 0, # 'x' + 11: 0, # 'y' + 22: 0, # 'z' + 63: 0, # '·' + 54: 0, # 'Ç' + 50: 0, # 'Ö' + 55: 2, # 'Ãœ' + 59: 0, # 'â' + 33: 0, # 'ç' + 61: 0, # 'î' + 34: 1, # 'ö' + 17: 0, # 'ü' + 30: 0, # 'ÄŸ' + 41: 0, # 'Ä°' + 6: 2, # 'ı' + 40: 0, # 'Åž' + 19: 1, # 'ÅŸ' + }, + 50: { # 'Ö' + 23: 0, # 'A' + 37: 0, # 'B' + 47: 1, # 'C' + 39: 1, # 'D' + 29: 2, # 'E' + 52: 0, # 'F' + 36: 1, # 'G' + 45: 2, # 'H' + 53: 0, # 'I' + 60: 0, # 'J' + 16: 0, # 'K' + 49: 0, # 'L' + 20: 1, # 'M' + 46: 1, # 'N' + 42: 2, # 'O' + 48: 2, # 'P' + 44: 1, # 'R' + 35: 0, # 'S' + 31: 0, # 'T' + 51: 1, # 'U' + 38: 1, # 'V' + 62: 0, # 'W' + 43: 2, # 'Y' + 56: 0, # 'Z' + 1: 0, # 'a' + 21: 2, # 'b' + 28: 1, # 'c' + 12: 2, # 'd' + 2: 0, # 'e' + 18: 1, # 'f' + 27: 1, # 'g' + 25: 1, # 'h' + 3: 2, # 'i' + 24: 0, # 'j' + 10: 2, # 'k' + 5: 0, # 'l' + 13: 0, # 'm' + 4: 3, # 'n' + 15: 2, # 'o' + 26: 2, # 'p' + 7: 3, # 'r' + 8: 1, # 's' + 9: 2, # 't' + 14: 0, # 'u' + 32: 1, # 'v' + 57: 0, # 'w' + 58: 0, # 'x' + 11: 0, # 'y' + 22: 1, # 'z' + 63: 0, # '·' + 54: 0, # 'Ç' + 50: 0, # 'Ö' + 55: 0, # 'Ãœ' + 59: 0, # 'â' + 33: 0, # 'ç' + 61: 0, # 'î' + 34: 2, # 'ö' + 17: 2, # 'ü' + 30: 1, # 'ÄŸ' + 41: 0, # 'Ä°' + 6: 2, # 'ı' + 40: 0, # 'Åž' + 19: 1, # 'ÅŸ' + }, + 55: { # 'Ãœ' + 23: 0, # 'A' + 37: 0, # 'B' + 47: 0, # 'C' + 39: 0, # 'D' + 29: 0, # 'E' + 52: 2, # 'F' + 36: 0, # 'G' + 45: 0, # 'H' + 53: 0, # 'I' + 60: 0, # 'J' + 16: 1, # 'K' + 49: 0, # 'L' + 20: 0, # 'M' + 46: 0, # 'N' + 42: 0, # 'O' + 48: 1, # 'P' + 44: 0, # 'R' + 35: 0, # 'S' + 31: 0, # 'T' + 51: 0, # 'U' + 38: 1, # 'V' + 62: 0, # 'W' + 43: 0, # 'Y' + 56: 0, # 'Z' + 1: 2, # 'a' + 21: 0, # 'b' + 28: 2, # 'c' + 12: 0, # 'd' + 2: 2, # 'e' + 18: 0, # 'f' + 27: 1, # 'g' + 25: 0, # 'h' + 3: 0, # 'i' + 24: 0, # 'j' + 10: 0, # 'k' + 5: 1, # 'l' + 13: 1, # 'm' + 4: 1, # 'n' + 15: 0, # 'o' + 26: 0, # 'p' + 7: 0, # 'r' + 8: 0, # 's' + 9: 1, # 't' + 14: 2, # 'u' + 32: 0, # 'v' + 57: 0, # 'w' + 58: 0, # 'x' + 11: 0, # 'y' + 22: 1, # 'z' + 63: 0, # '·' + 54: 0, # 'Ç' + 50: 1, # 'Ö' + 55: 0, # 'Ãœ' + 59: 0, # 'â' + 33: 0, # 'ç' + 61: 0, # 'î' + 34: 1, # 'ö' + 17: 0, # 'ü' + 30: 1, # 'ÄŸ' + 41: 1, # 'Ä°' + 6: 0, # 'ı' + 40: 0, # 'Åž' + 19: 1, # 'ÅŸ' + }, + 59: { # 'â' + 23: 0, # 'A' + 37: 0, # 'B' + 47: 0, # 'C' + 39: 0, # 'D' + 29: 0, # 'E' + 52: 0, # 'F' + 36: 1, # 'G' + 45: 0, # 'H' + 53: 0, # 'I' + 60: 0, # 'J' + 16: 1, # 'K' + 49: 0, # 'L' + 20: 0, # 'M' + 46: 0, # 'N' + 42: 0, # 'O' + 48: 0, # 'P' + 44: 0, # 'R' + 35: 0, # 'S' + 31: 0, # 'T' + 51: 0, # 'U' + 38: 0, # 'V' + 62: 0, # 'W' + 43: 0, # 'Y' + 56: 0, # 'Z' + 1: 2, # 'a' + 21: 0, # 'b' + 28: 0, # 'c' + 12: 0, # 'd' + 2: 2, # 'e' + 18: 0, # 'f' + 27: 0, # 'g' + 25: 0, # 'h' + 3: 0, # 'i' + 24: 0, # 'j' + 10: 0, # 'k' + 5: 0, # 'l' + 13: 2, # 'm' + 4: 0, # 'n' + 15: 1, # 'o' + 26: 0, # 'p' + 7: 0, # 'r' + 8: 0, # 's' + 9: 0, # 't' + 14: 2, # 'u' + 32: 0, # 'v' + 57: 0, # 'w' + 58: 0, # 'x' + 11: 0, # 'y' + 22: 1, # 'z' + 63: 0, # '·' + 54: 0, # 'Ç' + 50: 0, # 'Ö' + 55: 0, # 'Ãœ' + 59: 0, # 'â' + 33: 0, # 'ç' + 61: 0, # 'î' + 34: 0, # 'ö' + 17: 0, # 'ü' + 30: 0, # 'ÄŸ' + 41: 0, # 'Ä°' + 6: 1, # 'ı' + 40: 1, # 'Åž' + 19: 0, # 'ÅŸ' + }, + 33: { # 'ç' + 23: 0, # 'A' + 37: 0, # 'B' + 47: 0, # 'C' + 39: 0, # 'D' + 29: 3, # 'E' + 52: 0, # 'F' + 36: 0, # 'G' + 45: 0, # 'H' + 53: 0, # 'I' + 60: 0, # 'J' + 16: 0, # 'K' + 49: 0, # 'L' + 20: 1, # 'M' + 46: 0, # 'N' + 42: 0, # 'O' + 48: 0, # 'P' + 44: 0, # 'R' + 35: 0, # 'S' + 31: 2, # 'T' + 51: 0, # 'U' + 38: 1, # 'V' + 62: 0, # 'W' + 43: 0, # 'Y' + 56: 0, # 'Z' + 1: 0, # 'a' + 21: 3, # 'b' + 28: 0, # 'c' + 12: 2, # 'd' + 2: 0, # 'e' + 18: 2, # 'f' + 27: 1, # 'g' + 25: 3, # 'h' + 3: 3, # 'i' + 24: 0, # 'j' + 10: 3, # 'k' + 5: 0, # 'l' + 13: 0, # 'm' + 4: 3, # 'n' + 15: 0, # 'o' + 26: 1, # 'p' + 7: 3, # 'r' + 8: 2, # 's' + 9: 3, # 't' + 14: 0, # 'u' + 32: 2, # 'v' + 57: 0, # 'w' + 58: 0, # 'x' + 11: 2, # 'y' + 22: 0, # 'z' + 63: 0, # '·' + 54: 0, # 'Ç' + 50: 0, # 'Ö' + 55: 0, # 'Ãœ' + 59: 0, # 'â' + 33: 0, # 'ç' + 61: 0, # 'î' + 34: 0, # 'ö' + 17: 1, # 'ü' + 30: 0, # 'ÄŸ' + 41: 0, # 'Ä°' + 6: 3, # 'ı' + 40: 0, # 'Åž' + 19: 0, # 'ÅŸ' + }, + 61: { # 'î' + 23: 0, # 'A' + 37: 0, # 'B' + 47: 0, # 'C' + 39: 0, # 'D' + 29: 0, # 'E' + 52: 0, # 'F' + 36: 0, # 'G' + 45: 0, # 'H' + 53: 0, # 'I' + 60: 0, # 'J' + 16: 0, # 'K' + 49: 0, # 'L' + 20: 0, # 'M' + 46: 0, # 'N' + 42: 0, # 'O' + 48: 0, # 'P' + 44: 0, # 'R' + 35: 0, # 'S' + 31: 0, # 'T' + 51: 0, # 'U' + 38: 0, # 'V' + 62: 0, # 'W' + 43: 0, # 'Y' + 56: 1, # 'Z' + 1: 2, # 'a' + 21: 0, # 'b' + 28: 0, # 'c' + 12: 0, # 'd' + 2: 2, # 'e' + 18: 0, # 'f' + 27: 0, # 'g' + 25: 0, # 'h' + 3: 0, # 'i' + 24: 1, # 'j' + 10: 0, # 'k' + 5: 0, # 'l' + 13: 1, # 'm' + 4: 1, # 'n' + 15: 0, # 'o' + 26: 0, # 'p' + 7: 0, # 'r' + 8: 0, # 's' + 9: 0, # 't' + 14: 1, # 'u' + 32: 0, # 'v' + 57: 0, # 'w' + 58: 0, # 'x' + 11: 0, # 'y' + 22: 1, # 'z' + 63: 0, # '·' + 54: 0, # 'Ç' + 50: 0, # 'Ö' + 55: 0, # 'Ãœ' + 59: 0, # 'â' + 33: 0, # 'ç' + 61: 1, # 'î' + 34: 0, # 'ö' + 17: 0, # 'ü' + 30: 0, # 'ÄŸ' + 41: 0, # 'Ä°' + 6: 1, # 'ı' + 40: 0, # 'Åž' + 19: 0, # 'ÅŸ' + }, + 34: { # 'ö' + 23: 0, # 'A' + 37: 1, # 'B' + 47: 1, # 'C' + 39: 0, # 'D' + 29: 0, # 'E' + 52: 2, # 'F' + 36: 1, # 'G' + 45: 1, # 'H' + 53: 0, # 'I' + 60: 0, # 'J' + 16: 3, # 'K' + 49: 1, # 'L' + 20: 0, # 'M' + 46: 1, # 'N' + 42: 1, # 'O' + 48: 2, # 'P' + 44: 1, # 'R' + 35: 1, # 'S' + 31: 1, # 'T' + 51: 1, # 'U' + 38: 1, # 'V' + 62: 0, # 'W' + 43: 0, # 'Y' + 56: 1, # 'Z' + 1: 3, # 'a' + 21: 1, # 'b' + 28: 2, # 'c' + 12: 1, # 'd' + 2: 3, # 'e' + 18: 0, # 'f' + 27: 2, # 'g' + 25: 2, # 'h' + 3: 1, # 'i' + 24: 2, # 'j' + 10: 1, # 'k' + 5: 2, # 'l' + 13: 3, # 'm' + 4: 2, # 'n' + 15: 2, # 'o' + 26: 0, # 'p' + 7: 0, # 'r' + 8: 3, # 's' + 9: 1, # 't' + 14: 3, # 'u' + 32: 0, # 'v' + 57: 0, # 'w' + 58: 0, # 'x' + 11: 1, # 'y' + 22: 2, # 'z' + 63: 0, # '·' + 54: 1, # 'Ç' + 50: 2, # 'Ö' + 55: 0, # 'Ãœ' + 59: 0, # 'â' + 33: 2, # 'ç' + 61: 0, # 'î' + 34: 2, # 'ö' + 17: 0, # 'ü' + 30: 2, # 'ÄŸ' + 41: 1, # 'Ä°' + 6: 1, # 'ı' + 40: 2, # 'Åž' + 19: 1, # 'ÅŸ' + }, + 17: { # 'ü' + 23: 0, # 'A' + 37: 0, # 'B' + 47: 1, # 'C' + 39: 0, # 'D' + 29: 0, # 'E' + 52: 0, # 'F' + 36: 0, # 'G' + 45: 0, # 'H' + 53: 0, # 'I' + 60: 1, # 'J' + 16: 1, # 'K' + 49: 0, # 'L' + 20: 1, # 'M' + 46: 0, # 'N' + 42: 0, # 'O' + 48: 0, # 'P' + 44: 0, # 'R' + 35: 0, # 'S' + 31: 1, # 'T' + 51: 0, # 'U' + 38: 0, # 'V' + 62: 0, # 'W' + 43: 0, # 'Y' + 56: 1, # 'Z' + 1: 3, # 'a' + 21: 0, # 'b' + 28: 0, # 'c' + 12: 1, # 'd' + 2: 3, # 'e' + 18: 1, # 'f' + 27: 2, # 'g' + 25: 0, # 'h' + 3: 1, # 'i' + 24: 1, # 'j' + 10: 2, # 'k' + 5: 3, # 'l' + 13: 2, # 'm' + 4: 3, # 'n' + 15: 0, # 'o' + 26: 2, # 'p' + 7: 2, # 'r' + 8: 3, # 's' + 9: 2, # 't' + 14: 3, # 'u' + 32: 1, # 'v' + 57: 1, # 'w' + 58: 0, # 'x' + 11: 0, # 'y' + 22: 0, # 'z' + 63: 0, # '·' + 54: 0, # 'Ç' + 50: 0, # 'Ö' + 55: 0, # 'Ãœ' + 59: 0, # 'â' + 33: 1, # 'ç' + 61: 0, # 'î' + 34: 0, # 'ö' + 17: 2, # 'ü' + 30: 0, # 'ÄŸ' + 41: 0, # 'Ä°' + 6: 2, # 'ı' + 40: 0, # 'Åž' + 19: 0, # 'ÅŸ' + }, + 30: { # 'ÄŸ' + 23: 0, # 'A' + 37: 2, # 'B' + 47: 1, # 'C' + 39: 0, # 'D' + 29: 0, # 'E' + 52: 2, # 'F' + 36: 1, # 'G' + 45: 0, # 'H' + 53: 1, # 'I' + 60: 0, # 'J' + 16: 3, # 'K' + 49: 0, # 'L' + 20: 1, # 'M' + 46: 2, # 'N' + 42: 2, # 'O' + 48: 1, # 'P' + 44: 1, # 'R' + 35: 0, # 'S' + 31: 1, # 'T' + 51: 0, # 'U' + 38: 2, # 'V' + 62: 0, # 'W' + 43: 2, # 'Y' + 56: 0, # 'Z' + 1: 3, # 'a' + 21: 0, # 'b' + 28: 2, # 'c' + 12: 0, # 'd' + 2: 2, # 'e' + 18: 0, # 'f' + 27: 0, # 'g' + 25: 0, # 'h' + 3: 0, # 'i' + 24: 3, # 'j' + 10: 1, # 'k' + 5: 2, # 'l' + 13: 3, # 'm' + 4: 0, # 'n' + 15: 1, # 'o' + 26: 0, # 'p' + 7: 1, # 'r' + 8: 0, # 's' + 9: 0, # 't' + 14: 3, # 'u' + 32: 0, # 'v' + 57: 0, # 'w' + 58: 0, # 'x' + 11: 0, # 'y' + 22: 2, # 'z' + 63: 0, # '·' + 54: 2, # 'Ç' + 50: 2, # 'Ö' + 55: 0, # 'Ãœ' + 59: 0, # 'â' + 33: 1, # 'ç' + 61: 0, # 'î' + 34: 2, # 'ö' + 17: 0, # 'ü' + 30: 1, # 'ÄŸ' + 41: 2, # 'Ä°' + 6: 2, # 'ı' + 40: 2, # 'Åž' + 19: 1, # 'ÅŸ' + }, + 41: { # 'Ä°' + 23: 0, # 'A' + 37: 0, # 'B' + 47: 1, # 'C' + 39: 1, # 'D' + 29: 1, # 'E' + 52: 0, # 'F' + 36: 2, # 'G' + 45: 2, # 'H' + 53: 0, # 'I' + 60: 0, # 'J' + 16: 0, # 'K' + 49: 0, # 'L' + 20: 2, # 'M' + 46: 1, # 'N' + 42: 1, # 'O' + 48: 2, # 'P' + 44: 0, # 'R' + 35: 1, # 'S' + 31: 1, # 'T' + 51: 1, # 'U' + 38: 1, # 'V' + 62: 0, # 'W' + 43: 2, # 'Y' + 56: 0, # 'Z' + 1: 1, # 'a' + 21: 2, # 'b' + 28: 1, # 'c' + 12: 2, # 'd' + 2: 1, # 'e' + 18: 0, # 'f' + 27: 3, # 'g' + 25: 2, # 'h' + 3: 2, # 'i' + 24: 2, # 'j' + 10: 2, # 'k' + 5: 0, # 'l' + 13: 1, # 'm' + 4: 3, # 'n' + 15: 1, # 'o' + 26: 1, # 'p' + 7: 3, # 'r' + 8: 3, # 's' + 9: 2, # 't' + 14: 0, # 'u' + 32: 0, # 'v' + 57: 1, # 'w' + 58: 0, # 'x' + 11: 2, # 'y' + 22: 0, # 'z' + 63: 0, # '·' + 54: 0, # 'Ç' + 50: 0, # 'Ö' + 55: 1, # 'Ãœ' + 59: 1, # 'â' + 33: 0, # 'ç' + 61: 0, # 'î' + 34: 1, # 'ö' + 17: 1, # 'ü' + 30: 2, # 'ÄŸ' + 41: 0, # 'Ä°' + 6: 3, # 'ı' + 40: 0, # 'Åž' + 19: 1, # 'ÅŸ' + }, + 6: { # 'ı' + 23: 2, # 'A' + 37: 0, # 'B' + 47: 0, # 'C' + 39: 0, # 'D' + 29: 0, # 'E' + 52: 0, # 'F' + 36: 1, # 'G' + 45: 0, # 'H' + 53: 0, # 'I' + 60: 2, # 'J' + 16: 3, # 'K' + 49: 0, # 'L' + 20: 3, # 'M' + 46: 1, # 'N' + 42: 0, # 'O' + 48: 0, # 'P' + 44: 0, # 'R' + 35: 0, # 'S' + 31: 2, # 'T' + 51: 0, # 'U' + 38: 0, # 'V' + 62: 0, # 'W' + 43: 2, # 'Y' + 56: 1, # 'Z' + 1: 3, # 'a' + 21: 2, # 'b' + 28: 1, # 'c' + 12: 3, # 'd' + 2: 3, # 'e' + 18: 3, # 'f' + 27: 3, # 'g' + 25: 2, # 'h' + 3: 3, # 'i' + 24: 3, # 'j' + 10: 3, # 'k' + 5: 3, # 'l' + 13: 3, # 'm' + 4: 3, # 'n' + 15: 0, # 'o' + 26: 3, # 'p' + 7: 3, # 'r' + 8: 3, # 's' + 9: 3, # 't' + 14: 3, # 'u' + 32: 3, # 'v' + 57: 1, # 'w' + 58: 1, # 'x' + 11: 3, # 'y' + 22: 0, # 'z' + 63: 1, # '·' + 54: 0, # 'Ç' + 50: 0, # 'Ö' + 55: 0, # 'Ãœ' + 59: 0, # 'â' + 33: 2, # 'ç' + 61: 0, # 'î' + 34: 0, # 'ö' + 17: 3, # 'ü' + 30: 0, # 'ÄŸ' + 41: 0, # 'Ä°' + 6: 3, # 'ı' + 40: 0, # 'Åž' + 19: 0, # 'ÅŸ' + }, + 40: { # 'Åž' + 23: 0, # 'A' + 37: 0, # 'B' + 47: 1, # 'C' + 39: 1, # 'D' + 29: 1, # 'E' + 52: 0, # 'F' + 36: 1, # 'G' + 45: 2, # 'H' + 53: 1, # 'I' + 60: 0, # 'J' + 16: 0, # 'K' + 49: 0, # 'L' + 20: 2, # 'M' + 46: 1, # 'N' + 42: 1, # 'O' + 48: 2, # 'P' + 44: 2, # 'R' + 35: 1, # 'S' + 31: 1, # 'T' + 51: 0, # 'U' + 38: 1, # 'V' + 62: 0, # 'W' + 43: 2, # 'Y' + 56: 1, # 'Z' + 1: 0, # 'a' + 21: 2, # 'b' + 28: 0, # 'c' + 12: 2, # 'd' + 2: 0, # 'e' + 18: 3, # 'f' + 27: 0, # 'g' + 25: 2, # 'h' + 3: 3, # 'i' + 24: 2, # 'j' + 10: 1, # 'k' + 5: 0, # 'l' + 13: 1, # 'm' + 4: 3, # 'n' + 15: 2, # 'o' + 26: 0, # 'p' + 7: 3, # 'r' + 8: 2, # 's' + 9: 2, # 't' + 14: 1, # 'u' + 32: 3, # 'v' + 57: 0, # 'w' + 58: 0, # 'x' + 11: 2, # 'y' + 22: 0, # 'z' + 63: 0, # '·' + 54: 0, # 'Ç' + 50: 0, # 'Ö' + 55: 1, # 'Ãœ' + 59: 0, # 'â' + 33: 0, # 'ç' + 61: 0, # 'î' + 34: 2, # 'ö' + 17: 1, # 'ü' + 30: 2, # 'ÄŸ' + 41: 0, # 'Ä°' + 6: 2, # 'ı' + 40: 1, # 'Åž' + 19: 2, # 'ÅŸ' + }, + 19: { # 'ÅŸ' + 23: 0, # 'A' + 37: 0, # 'B' + 47: 1, # 'C' + 39: 0, # 'D' + 29: 0, # 'E' + 52: 2, # 'F' + 36: 1, # 'G' + 45: 0, # 'H' + 53: 0, # 'I' + 60: 0, # 'J' + 16: 3, # 'K' + 49: 2, # 'L' + 20: 0, # 'M' + 46: 1, # 'N' + 42: 1, # 'O' + 48: 1, # 'P' + 44: 1, # 'R' + 35: 1, # 'S' + 31: 0, # 'T' + 51: 1, # 'U' + 38: 1, # 'V' + 62: 0, # 'W' + 43: 1, # 'Y' + 56: 0, # 'Z' + 1: 3, # 'a' + 21: 1, # 'b' + 28: 2, # 'c' + 12: 0, # 'd' + 2: 3, # 'e' + 18: 0, # 'f' + 27: 2, # 'g' + 25: 1, # 'h' + 3: 1, # 'i' + 24: 0, # 'j' + 10: 2, # 'k' + 5: 2, # 'l' + 13: 3, # 'm' + 4: 0, # 'n' + 15: 0, # 'o' + 26: 1, # 'p' + 7: 3, # 'r' + 8: 0, # 's' + 9: 0, # 't' + 14: 3, # 'u' + 32: 0, # 'v' + 57: 0, # 'w' + 58: 0, # 'x' + 11: 0, # 'y' + 22: 2, # 'z' + 63: 0, # '·' + 54: 1, # 'Ç' + 50: 2, # 'Ö' + 55: 0, # 'Ãœ' + 59: 0, # 'â' + 33: 1, # 'ç' + 61: 1, # 'î' + 34: 2, # 'ö' + 17: 0, # 'ü' + 30: 1, # 'ÄŸ' + 41: 1, # 'Ä°' + 6: 1, # 'ı' + 40: 1, # 'Åž' + 19: 1, # 'ÅŸ' + }, +} + +# 255: Undefined characters that did not exist in training text # 254: Carriage/Return # 253: symbol (punctuation) that does not belong to word # 252: 0 - 9 +# 251: Control characters -# Character Mapping Table: -Latin5_TurkishCharToOrderMap = ( -255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, -255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, -255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, -255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, -255, 23, 37, 47, 39, 29, 52, 36, 45, 53, 60, 16, 49, 20, 46, 42, - 48, 69, 44, 35, 31, 51, 38, 62, 65, 43, 56,255,255,255,255,255, -255, 1, 21, 28, 12, 2, 18, 27, 25, 3, 24, 10, 5, 13, 4, 15, - 26, 64, 7, 8, 9, 14, 32, 57, 58, 11, 22,255,255,255,255,255, -180,179,178,177,176,175,174,173,172,171,170,169,168,167,166,165, -164,163,162,161,160,159,101,158,157,156,155,154,153,152,151,106, -150,149,148,147,146,145,144,100,143,142,141,140,139,138,137,136, - 94, 80, 93,135,105,134,133, 63,132,131,130,129,128,127,126,125, -124,104, 73, 99, 79, 85,123, 54,122, 98, 92,121,120, 91,103,119, - 68,118,117, 97,116,115, 50, 90,114,113,112,111, 55, 41, 40, 86, - 89, 70, 59, 78, 71, 82, 88, 33, 77, 66, 84, 83,110, 75, 61, 96, - 30, 67,109, 74, 87,102, 34, 95, 81,108, 76, 72, 17, 6, 19,107, -) - -TurkishLangModel = ( -3,2,3,3,3,1,3,3,3,3,3,3,3,3,2,1,1,3,3,1,3,3,0,3,3,3,3,3,0,3,1,3, -3,2,1,0,0,1,1,0,0,0,1,0,0,1,1,1,1,0,0,0,0,0,0,0,2,2,0,0,1,0,0,1, -3,2,2,3,3,0,3,3,3,3,3,3,3,2,3,1,0,3,3,1,3,3,0,3,3,3,3,3,0,3,0,3, -3,1,1,0,1,0,1,0,0,0,0,0,0,1,1,1,1,0,0,0,0,0,0,0,2,2,0,0,0,1,0,1, -3,3,2,3,3,0,3,3,3,3,3,3,3,2,3,1,1,3,3,0,3,3,1,2,3,3,3,3,0,3,0,3, -3,1,1,0,0,0,1,0,0,0,0,1,1,0,1,2,1,0,0,0,1,0,0,0,0,2,0,0,0,0,0,1, -3,3,3,3,3,3,2,3,3,3,3,3,3,3,3,1,3,3,2,0,3,2,1,2,2,1,3,3,0,0,0,2, -2,2,0,1,0,0,1,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,1,0,1,1,0,1,0,0,1, -3,3,3,2,3,3,1,2,3,3,3,3,3,3,3,1,3,2,1,0,3,2,0,1,2,3,3,2,1,0,0,2, -2,1,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,2,0,2,0,0,0, -1,0,1,3,3,1,3,3,3,3,3,3,3,1,2,0,0,2,3,0,2,3,0,0,2,2,2,3,0,3,0,1, -2,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,0,3,3,3,0,3,2,0,2,3,2,3,3,1,0,0,2, -3,2,0,0,1,0,0,0,0,0,0,2,0,0,1,0,0,0,0,0,0,0,0,0,1,1,1,0,2,0,0,1, -3,3,3,2,3,3,2,3,3,3,3,2,3,3,3,0,3,3,0,0,2,1,0,0,2,3,2,2,0,0,0,2, -2,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,0,0,0,1,0,1,0,2,0,0,1, -3,3,3,2,3,3,3,3,3,3,3,2,3,3,3,0,3,2,0,1,3,2,1,1,3,2,3,2,1,0,0,2, -2,2,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,1,0,0,0,0,0, -3,3,3,2,3,3,3,3,3,3,3,2,3,3,3,0,3,2,2,0,2,3,0,0,2,2,2,2,0,0,0,2, -3,3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,2,0,1,0,0,0, -3,3,3,3,3,3,3,2,2,2,2,3,2,3,3,0,3,3,1,1,2,2,0,0,2,2,3,2,0,0,1,3, -0,3,1,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,1,0,0,0,0,1, -3,3,3,2,3,3,3,2,1,2,2,3,2,3,3,0,3,2,0,0,1,1,0,1,1,2,1,2,0,0,0,1, -0,3,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,1,0,1,0,1,0,0,0, -3,3,3,2,3,3,2,3,2,2,2,3,3,3,3,1,3,1,1,0,3,2,1,1,3,3,2,3,1,0,0,1, -1,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,2,0,0,1, -3,2,2,3,3,0,3,3,3,3,3,3,3,2,2,1,0,3,3,1,3,3,0,1,3,3,2,3,0,3,0,3, -2,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0, -2,2,2,3,3,0,3,3,3,3,3,3,3,3,3,0,0,3,2,0,3,3,0,3,2,3,3,3,0,3,1,3, -2,0,0,0,0,0,0,0,0,0,0,1,0,1,2,0,1,0,0,0,0,0,0,0,2,2,0,0,1,0,0,1, -3,3,3,1,2,3,3,1,0,0,1,0,0,3,3,2,3,0,0,2,0,0,2,0,2,0,0,0,2,0,2,0, -0,3,1,0,1,0,0,0,2,2,1,0,1,1,2,1,2,2,2,0,2,1,1,0,0,0,2,0,0,0,0,0, -1,2,1,3,3,0,3,3,3,3,3,2,3,0,0,0,0,2,3,0,2,3,1,0,2,3,1,3,0,3,0,2, -3,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -3,3,3,1,3,3,2,2,3,2,2,0,1,2,3,0,1,2,1,0,1,0,0,0,1,0,2,2,0,0,0,1, -1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,1,1,0,0,1,0,0,0, -3,3,3,1,3,3,1,1,3,3,1,1,3,3,1,0,2,1,2,0,2,1,0,0,1,1,2,1,0,0,0,2, -2,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -3,3,3,1,0,2,1,3,0,0,2,0,0,3,3,0,3,0,0,1,0,1,2,0,0,1,1,2,2,0,1,0, -0,1,2,1,1,0,1,0,1,1,1,1,1,0,1,1,1,2,2,1,2,0,1,0,0,0,0,0,0,1,0,0, -3,3,3,2,3,2,3,3,0,2,2,2,3,3,3,0,3,0,0,0,2,2,0,1,2,1,1,1,0,0,0,1, -0,3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0, -3,3,3,3,3,3,2,1,2,2,3,3,3,3,2,0,2,0,0,0,2,2,0,0,2,1,3,3,0,0,1,1, -1,1,0,0,1,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,1,0,0,0, -1,1,2,3,3,0,3,3,3,3,3,3,2,2,0,2,0,2,3,2,3,2,2,2,2,2,2,2,1,3,2,3, -2,0,2,1,2,2,2,2,1,1,2,2,1,2,2,1,2,0,0,2,1,1,0,2,1,0,0,1,0,0,0,1, -2,3,3,1,1,1,0,1,1,1,2,3,2,1,1,0,0,0,0,0,0,0,0,0,0,1,0,1,0,0,0,0, -0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -3,3,3,2,2,2,3,2,3,2,2,1,3,3,3,0,2,1,2,0,2,1,0,0,1,1,1,1,1,0,0,1, -2,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,1,0,2,0,1,0,0,0, -3,3,3,2,3,3,3,3,3,2,3,1,2,3,3,1,2,0,0,0,0,0,0,0,3,2,1,1,0,0,0,0, -2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0, -3,3,3,2,2,3,3,2,1,1,1,1,1,3,3,0,3,1,0,0,1,1,0,0,3,1,2,1,0,0,0,0, -0,3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0, -3,3,3,2,2,3,2,2,2,3,2,1,1,3,3,0,3,0,0,0,0,1,0,0,3,1,1,2,0,0,0,1, -1,0,0,1,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1, -1,1,1,3,3,0,3,3,3,3,3,2,2,2,1,2,0,2,1,2,2,1,1,0,1,2,2,2,2,2,2,2, -0,0,2,1,2,1,2,1,0,1,1,3,1,2,1,1,2,0,0,2,0,1,0,1,0,1,0,0,0,1,0,1, -3,3,3,1,3,3,3,0,1,1,0,2,2,3,1,0,3,0,0,0,1,0,0,0,1,0,0,1,0,1,0,0, -1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -3,3,2,0,0,2,2,1,0,0,1,0,0,3,3,1,3,0,0,1,1,0,2,0,3,0,0,0,2,0,1,1, -0,1,2,0,1,2,2,0,2,2,2,2,1,0,2,1,1,0,2,0,2,1,2,0,0,0,0,0,0,0,0,0, -3,3,3,1,3,2,3,2,0,2,2,2,1,3,2,0,2,1,2,0,1,2,0,0,1,0,2,2,0,0,0,2, -1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,0,1,0,0,0, -3,3,3,0,3,3,1,1,2,3,1,0,3,2,3,0,3,0,0,0,1,0,0,0,1,0,1,0,0,0,0,0, -1,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,0,3,3,0,3,3,2,3,3,2,2,0,0,0,0,1,2,0,1,3,0,0,0,3,1,1,0,3,0,2, -2,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -3,3,3,1,2,2,1,0,3,1,1,1,1,3,3,2,3,0,0,1,0,1,2,0,2,2,0,2,2,0,2,1, -0,2,2,1,1,1,1,0,2,1,1,0,1,1,1,1,2,1,2,1,2,0,1,0,1,0,0,0,0,0,0,0, -3,3,3,0,1,1,3,0,0,1,1,0,0,2,2,0,3,0,0,1,1,0,1,0,0,0,0,0,2,0,0,0, -0,3,1,0,1,0,1,0,2,0,0,1,0,1,0,1,1,1,2,1,1,0,2,0,0,0,0,0,0,0,0,0, -3,3,3,0,2,0,2,0,1,1,1,0,0,3,3,0,2,0,0,1,0,0,2,1,1,0,1,0,1,0,1,0, -0,2,0,1,2,0,2,0,2,1,1,0,1,0,2,1,1,0,2,1,1,0,1,0,0,0,1,1,0,0,0,0, -3,2,3,0,1,0,0,0,0,0,0,0,0,1,2,0,1,0,0,1,0,0,1,0,0,0,0,0,2,0,0,0, -0,0,1,1,0,0,1,0,1,0,0,1,0,0,0,2,1,0,1,0,2,0,0,0,0,0,0,0,0,0,0,0, -3,3,3,0,0,2,3,0,0,1,0,1,0,2,3,2,3,0,0,1,3,0,2,1,0,0,0,0,2,0,1,0, -0,2,1,0,0,1,1,0,2,1,0,0,1,0,0,1,1,0,1,1,2,0,1,0,0,0,0,1,0,0,0,0, -3,2,2,0,0,1,1,0,0,0,0,0,0,3,1,1,1,0,0,0,0,0,1,0,0,0,0,0,2,0,1,0, -0,1,0,0,0,0,0,0,1,0,0,0,0,0,0,0,1,0,0,0,1,0,1,0,0,0,0,0,0,0,0,0, -0,0,0,3,3,0,2,3,2,2,1,2,2,1,1,2,0,1,3,2,2,2,0,0,2,2,0,0,0,1,2,1, -3,0,2,1,1,0,1,1,1,0,1,2,2,2,1,1,2,0,0,0,0,1,0,1,1,0,0,0,0,0,0,0, -0,1,1,2,3,0,3,3,3,2,2,2,2,1,0,1,0,1,0,1,2,2,0,0,2,2,1,3,1,1,2,1, -0,0,1,1,2,0,1,1,0,0,1,2,0,2,1,1,2,0,0,1,0,0,0,1,0,1,0,1,0,0,0,0, -3,3,2,0,0,3,1,0,0,0,0,0,0,3,2,1,2,0,0,1,0,0,2,0,0,0,0,0,2,0,1,0, -0,2,1,1,0,0,1,0,1,2,0,0,1,1,0,0,2,1,1,1,1,0,2,0,0,0,0,0,0,0,0,0, -3,3,2,0,0,1,0,0,0,0,1,0,0,3,3,2,2,0,0,1,0,0,2,0,1,0,0,0,2,0,1,0, -0,0,1,1,0,0,2,0,2,1,0,0,1,1,2,1,2,0,2,1,2,1,1,1,0,0,1,1,0,0,0,0, -3,3,2,0,0,2,2,0,0,0,1,1,0,2,2,1,3,1,0,1,0,1,2,0,0,0,0,0,1,0,1,0, -0,1,1,0,0,0,0,0,1,0,0,1,0,0,0,1,1,0,1,0,1,0,0,0,0,0,0,0,0,0,0,0, -3,3,3,2,0,0,0,1,0,0,1,0,0,2,3,1,2,0,0,1,0,0,2,0,0,0,1,0,2,0,2,0, -0,1,1,2,2,1,2,0,2,1,1,0,0,1,1,0,1,1,1,1,2,1,1,0,0,0,0,0,0,0,0,0, -3,3,3,0,2,1,2,1,0,0,1,1,0,3,3,1,2,0,0,1,0,0,2,0,2,0,1,1,2,0,0,0, -0,0,1,1,1,1,2,0,1,1,0,1,1,1,1,0,0,0,1,1,1,0,1,0,0,0,1,0,0,0,0,0, -3,3,3,0,2,2,3,2,0,0,1,0,0,2,3,1,0,0,0,0,0,0,2,0,2,0,0,0,2,0,0,0, -0,1,1,0,0,0,1,0,0,1,0,1,1,0,1,0,1,1,1,0,1,0,0,0,0,0,0,0,0,0,0,0, -3,2,3,0,0,0,0,0,0,0,1,0,0,2,2,2,2,0,0,1,0,0,2,0,0,0,0,0,2,0,1,0, -0,0,2,1,1,0,1,0,2,1,1,0,0,1,1,2,1,0,2,0,2,0,1,0,0,0,2,0,0,0,0,0, -0,0,0,2,2,0,2,1,1,1,1,2,2,0,0,1,0,1,0,0,1,3,0,0,0,0,1,0,0,2,1,0, -0,0,1,0,1,0,0,0,0,0,2,1,0,1,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0, -2,0,0,2,3,0,2,3,1,2,2,0,2,0,0,2,0,2,1,1,1,2,1,0,0,1,2,1,1,2,1,0, -1,0,2,0,1,0,1,1,0,0,2,2,1,2,1,1,2,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0, -3,3,3,0,2,1,2,0,0,0,1,0,0,3,2,0,1,0,0,1,0,0,2,0,0,0,1,2,1,0,1,0, -0,0,0,0,1,0,1,0,0,1,0,0,0,0,1,0,1,0,1,1,1,0,1,0,0,0,0,0,0,0,0,0, -0,0,0,2,2,0,2,2,1,1,0,1,1,1,1,1,0,0,1,2,1,1,1,0,1,0,0,0,1,1,1,1, -0,0,2,1,0,1,1,1,0,1,1,2,1,2,1,1,2,0,1,1,2,1,0,2,0,0,0,0,0,0,0,0, -3,2,2,0,0,2,0,0,0,0,0,0,0,2,2,0,2,0,0,1,0,0,2,0,0,0,0,0,2,0,0,0, -0,2,1,0,0,0,0,0,1,0,0,0,0,0,0,0,1,0,0,0,1,0,1,0,0,0,0,0,0,0,0,0, -0,0,0,3,2,0,2,2,0,1,1,0,1,0,0,1,0,0,0,1,0,1,0,0,0,0,0,1,0,0,0,0, -2,0,1,0,1,0,1,1,0,0,1,2,0,1,0,1,1,0,0,1,0,1,0,2,0,0,0,0,0,0,0,0, -2,2,2,0,1,1,0,0,0,1,0,0,0,1,2,0,1,0,0,1,0,0,1,0,0,0,0,1,2,0,1,0, -0,0,1,0,0,0,1,0,0,1,0,0,0,0,0,0,1,0,1,0,2,0,0,0,0,0,0,0,0,0,0,0, -2,2,2,2,1,0,1,1,1,0,0,0,0,1,2,0,0,1,0,0,0,1,0,0,1,0,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0, -1,1,2,0,1,0,0,0,1,0,1,0,0,0,1,0,0,1,0,0,0,0,0,0,0,1,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,2,0,0,0,0,0,1, -0,0,1,2,2,0,2,1,2,1,1,2,2,0,0,0,0,1,0,0,1,1,0,0,2,0,0,0,0,1,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0, -2,2,2,0,0,0,1,0,0,0,0,0,0,2,2,1,1,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0, -0,0,0,0,1,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,0,1,1,0,0,0,1,0,0,0,1,0,0,0,0,0,0,0,1,1,0,0,0,0,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -2,2,2,0,1,0,1,0,0,0,0,0,0,1,1,0,0,0,0,0,0,0,1,0,1,0,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,1,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,1,0,0,0,0,0,0,0,0,0,0,2,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -) - -Latin5TurkishModel = { - 'char_to_order_map': Latin5_TurkishCharToOrderMap, - 'precedence_matrix': TurkishLangModel, - 'typical_positive_ratio': 0.970290, - 'keep_english_letter': True, - 'charset_name': "ISO-8859-9", - 'language': 'Turkish', +# Character Mapping Table(s): +ISO_8859_9_TURKISH_CHAR_TO_ORDER = { + 0: 255, # '\x00' + 1: 255, # '\x01' + 2: 255, # '\x02' + 3: 255, # '\x03' + 4: 255, # '\x04' + 5: 255, # '\x05' + 6: 255, # '\x06' + 7: 255, # '\x07' + 8: 255, # '\x08' + 9: 255, # '\t' + 10: 255, # '\n' + 11: 255, # '\x0b' + 12: 255, # '\x0c' + 13: 255, # '\r' + 14: 255, # '\x0e' + 15: 255, # '\x0f' + 16: 255, # '\x10' + 17: 255, # '\x11' + 18: 255, # '\x12' + 19: 255, # '\x13' + 20: 255, # '\x14' + 21: 255, # '\x15' + 22: 255, # '\x16' + 23: 255, # '\x17' + 24: 255, # '\x18' + 25: 255, # '\x19' + 26: 255, # '\x1a' + 27: 255, # '\x1b' + 28: 255, # '\x1c' + 29: 255, # '\x1d' + 30: 255, # '\x1e' + 31: 255, # '\x1f' + 32: 255, # ' ' + 33: 255, # '!' + 34: 255, # '"' + 35: 255, # '#' + 36: 255, # '$' + 37: 255, # '%' + 38: 255, # '&' + 39: 255, # "'" + 40: 255, # '(' + 41: 255, # ')' + 42: 255, # '*' + 43: 255, # '+' + 44: 255, # ',' + 45: 255, # '-' + 46: 255, # '.' + 47: 255, # '/' + 48: 255, # '0' + 49: 255, # '1' + 50: 255, # '2' + 51: 255, # '3' + 52: 255, # '4' + 53: 255, # '5' + 54: 255, # '6' + 55: 255, # '7' + 56: 255, # '8' + 57: 255, # '9' + 58: 255, # ':' + 59: 255, # ';' + 60: 255, # '<' + 61: 255, # '=' + 62: 255, # '>' + 63: 255, # '?' + 64: 255, # '@' + 65: 23, # 'A' + 66: 37, # 'B' + 67: 47, # 'C' + 68: 39, # 'D' + 69: 29, # 'E' + 70: 52, # 'F' + 71: 36, # 'G' + 72: 45, # 'H' + 73: 53, # 'I' + 74: 60, # 'J' + 75: 16, # 'K' + 76: 49, # 'L' + 77: 20, # 'M' + 78: 46, # 'N' + 79: 42, # 'O' + 80: 48, # 'P' + 81: 69, # 'Q' + 82: 44, # 'R' + 83: 35, # 'S' + 84: 31, # 'T' + 85: 51, # 'U' + 86: 38, # 'V' + 87: 62, # 'W' + 88: 65, # 'X' + 89: 43, # 'Y' + 90: 56, # 'Z' + 91: 255, # '[' + 92: 255, # '\\' + 93: 255, # ']' + 94: 255, # '^' + 95: 255, # '_' + 96: 255, # '`' + 97: 1, # 'a' + 98: 21, # 'b' + 99: 28, # 'c' + 100: 12, # 'd' + 101: 2, # 'e' + 102: 18, # 'f' + 103: 27, # 'g' + 104: 25, # 'h' + 105: 3, # 'i' + 106: 24, # 'j' + 107: 10, # 'k' + 108: 5, # 'l' + 109: 13, # 'm' + 110: 4, # 'n' + 111: 15, # 'o' + 112: 26, # 'p' + 113: 64, # 'q' + 114: 7, # 'r' + 115: 8, # 's' + 116: 9, # 't' + 117: 14, # 'u' + 118: 32, # 'v' + 119: 57, # 'w' + 120: 58, # 'x' + 121: 11, # 'y' + 122: 22, # 'z' + 123: 255, # '{' + 124: 255, # '|' + 125: 255, # '}' + 126: 255, # '~' + 127: 255, # '\x7f' + 128: 180, # '\x80' + 129: 179, # '\x81' + 130: 178, # '\x82' + 131: 177, # '\x83' + 132: 176, # '\x84' + 133: 175, # '\x85' + 134: 174, # '\x86' + 135: 173, # '\x87' + 136: 172, # '\x88' + 137: 171, # '\x89' + 138: 170, # '\x8a' + 139: 169, # '\x8b' + 140: 168, # '\x8c' + 141: 167, # '\x8d' + 142: 166, # '\x8e' + 143: 165, # '\x8f' + 144: 164, # '\x90' + 145: 163, # '\x91' + 146: 162, # '\x92' + 147: 161, # '\x93' + 148: 160, # '\x94' + 149: 159, # '\x95' + 150: 101, # '\x96' + 151: 158, # '\x97' + 152: 157, # '\x98' + 153: 156, # '\x99' + 154: 155, # '\x9a' + 155: 154, # '\x9b' + 156: 153, # '\x9c' + 157: 152, # '\x9d' + 158: 151, # '\x9e' + 159: 106, # '\x9f' + 160: 150, # '\xa0' + 161: 149, # '¡' + 162: 148, # '¢' + 163: 147, # '£' + 164: 146, # '¤' + 165: 145, # 'Â¥' + 166: 144, # '¦' + 167: 100, # '§' + 168: 143, # '¨' + 169: 142, # '©' + 170: 141, # 'ª' + 171: 140, # '«' + 172: 139, # '¬' + 173: 138, # '\xad' + 174: 137, # '®' + 175: 136, # '¯' + 176: 94, # '°' + 177: 80, # '±' + 178: 93, # '²' + 179: 135, # '³' + 180: 105, # '´' + 181: 134, # 'µ' + 182: 133, # '¶' + 183: 63, # '·' + 184: 132, # '¸' + 185: 131, # '¹' + 186: 130, # 'º' + 187: 129, # '»' + 188: 128, # '¼' + 189: 127, # '½' + 190: 126, # '¾' + 191: 125, # '¿' + 192: 124, # 'À' + 193: 104, # 'Ã' + 194: 73, # 'Â' + 195: 99, # 'Ã' + 196: 79, # 'Ä' + 197: 85, # 'Ã…' + 198: 123, # 'Æ' + 199: 54, # 'Ç' + 200: 122, # 'È' + 201: 98, # 'É' + 202: 92, # 'Ê' + 203: 121, # 'Ë' + 204: 120, # 'ÃŒ' + 205: 91, # 'Ã' + 206: 103, # 'ÃŽ' + 207: 119, # 'Ã' + 208: 68, # 'Äž' + 209: 118, # 'Ñ' + 210: 117, # 'Ã’' + 211: 97, # 'Ó' + 212: 116, # 'Ô' + 213: 115, # 'Õ' + 214: 50, # 'Ö' + 215: 90, # '×' + 216: 114, # 'Ø' + 217: 113, # 'Ù' + 218: 112, # 'Ú' + 219: 111, # 'Û' + 220: 55, # 'Ãœ' + 221: 41, # 'Ä°' + 222: 40, # 'Åž' + 223: 86, # 'ß' + 224: 89, # 'à' + 225: 70, # 'á' + 226: 59, # 'â' + 227: 78, # 'ã' + 228: 71, # 'ä' + 229: 82, # 'Ã¥' + 230: 88, # 'æ' + 231: 33, # 'ç' + 232: 77, # 'è' + 233: 66, # 'é' + 234: 84, # 'ê' + 235: 83, # 'ë' + 236: 110, # 'ì' + 237: 75, # 'í' + 238: 61, # 'î' + 239: 96, # 'ï' + 240: 30, # 'ÄŸ' + 241: 67, # 'ñ' + 242: 109, # 'ò' + 243: 74, # 'ó' + 244: 87, # 'ô' + 245: 102, # 'õ' + 246: 34, # 'ö' + 247: 95, # '÷' + 248: 81, # 'ø' + 249: 108, # 'ù' + 250: 76, # 'ú' + 251: 72, # 'û' + 252: 17, # 'ü' + 253: 6, # 'ı' + 254: 19, # 'ÅŸ' + 255: 107, # 'ÿ' } + +ISO_8859_9_TURKISH_MODEL = SingleByteCharSetModel(charset_name='ISO-8859-9', + language='Turkish', + char_to_order_map=ISO_8859_9_TURKISH_CHAR_TO_ORDER, + language_model=TURKISH_LANG_MODEL, + typical_positive_ratio=0.97029, + keep_ascii_letters=True, + alphabet='ABCDEFGHIJKLMNOPRSTUVYZabcdefghijklmnoprstuvyzÂÇÎÖÛÜâçîöûüĞğİıŞş') + diff --git a/libs/beaker/ext/__init__.py b/libs/chardet/metadata/__init__.py similarity index 100% rename from libs/beaker/ext/__init__.py rename to libs/chardet/metadata/__init__.py diff --git a/libs/chardet/metadata/languages.py b/libs/chardet/metadata/languages.py new file mode 100644 index 000000000..3237d5abf --- /dev/null +++ b/libs/chardet/metadata/languages.py @@ -0,0 +1,310 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +Metadata about languages used by our model training code for our +SingleByteCharSetProbers. Could be used for other things in the future. + +This code is based on the language metadata from the uchardet project. +""" +from __future__ import absolute_import, print_function + +from string import ascii_letters + + +# TODO: Add Ukranian (KOI8-U) + +class Language(object): + """Metadata about a language useful for training models + + :ivar name: The human name for the language, in English. + :type name: str + :ivar iso_code: 2-letter ISO 639-1 if possible, 3-letter ISO code otherwise, + or use another catalog as a last resort. + :type iso_code: str + :ivar use_ascii: Whether or not ASCII letters should be included in trained + models. + :type use_ascii: bool + :ivar charsets: The charsets we want to support and create data for. + :type charsets: list of str + :ivar alphabet: The characters in the language's alphabet. If `use_ascii` is + `True`, you only need to add those not in the ASCII set. + :type alphabet: str + :ivar wiki_start_pages: The Wikipedia pages to start from if we're crawling + Wikipedia for training data. + :type wiki_start_pages: list of str + """ + def __init__(self, name=None, iso_code=None, use_ascii=True, charsets=None, + alphabet=None, wiki_start_pages=None): + super(Language, self).__init__() + self.name = name + self.iso_code = iso_code + self.use_ascii = use_ascii + self.charsets = charsets + if self.use_ascii: + if alphabet: + alphabet += ascii_letters + else: + alphabet = ascii_letters + elif not alphabet: + raise ValueError('Must supply alphabet if use_ascii is False') + self.alphabet = ''.join(sorted(set(alphabet))) if alphabet else None + self.wiki_start_pages = wiki_start_pages + + def __repr__(self): + return '{}({})'.format(self.__class__.__name__, + ', '.join('{}={!r}'.format(k, v) + for k, v in self.__dict__.items() + if not k.startswith('_'))) + + +LANGUAGES = {'Arabic': Language(name='Arabic', + iso_code='ar', + use_ascii=False, + # We only support encodings that use isolated + # forms, because the current recommendation is + # that the rendering system handles presentation + # forms. This means we purposefully skip IBM864. + charsets=['ISO-8859-6', 'WINDOWS-1256', + 'CP720', 'CP864'], + alphabet=u'ءآأؤإئابةتثجحخدذرزسشصضطظعغػؼؽؾؿـÙقكلمنهوىيًٌÙÙŽÙÙÙ‘', + wiki_start_pages=[u'الصÙحة_الرئيسية']), + 'Belarusian': Language(name='Belarusian', + iso_code='be', + use_ascii=False, + charsets=['ISO-8859-5', 'WINDOWS-1251', + 'IBM866', 'MacCyrillic'], + alphabet=(u'ÐБВГДЕÐЖЗІЙКЛМÐОПРСТУЎФХЦЧШЫЬЭЮЯ' + u'абвгдеёжзійклмнопрÑтуўфхцчшыьÑÑŽÑʼ'), + wiki_start_pages=[u'ГалоўнаÑ_Ñтаронка']), + 'Bulgarian': Language(name='Bulgarian', + iso_code='bg', + use_ascii=False, + charsets=['ISO-8859-5', 'WINDOWS-1251', + 'IBM855'], + alphabet=(u'ÐБВГДЕЖЗИЙКЛМÐОПРСТУФХЦЧШЩЪЬЮЯ' + u'абвгдежзийклмнопрÑтуфхцчшщъьюÑ'), + wiki_start_pages=[u'Ðачална_Ñтраница']), + 'Czech': Language(name='Czech', + iso_code='cz', + use_ascii=True, + charsets=['ISO-8859-2', 'WINDOWS-1250'], + alphabet=u'áÄÄéěíňóřšťúůýžÃČĎÉĚÃŇÓŘŠŤÚŮÃŽ', + wiki_start_pages=[u'Hlavní_strana']), + 'Danish': Language(name='Danish', + iso_code='da', + use_ascii=True, + charsets=['ISO-8859-1', 'ISO-8859-15', + 'WINDOWS-1252'], + alphabet=u'æøåÆØÅ', + wiki_start_pages=[u'Forside']), + 'German': Language(name='German', + iso_code='de', + use_ascii=True, + charsets=['ISO-8859-1', 'WINDOWS-1252'], + alphabet=u'äöüßÄÖÜ', + wiki_start_pages=[u'Wikipedia:Hauptseite']), + 'Greek': Language(name='Greek', + iso_code='el', + use_ascii=False, + charsets=['ISO-8859-7', 'WINDOWS-1253'], + alphabet=(u'αβγδεζηθικλμνξοπÏσςτυφχψωάέήίόÏÏŽ' + u'ΑΒΓΔΕΖΗΘΙΚΛΜÎΞΟΠΡΣΣΤΥΦΧΨΩΆΈΉΊΌΎÎ'), + wiki_start_pages=[u'ΠÏλη:ΚÏÏια']), + 'English': Language(name='English', + iso_code='en', + use_ascii=True, + charsets=['ISO-8859-1', 'WINDOWS-1252'], + wiki_start_pages=[u'Main_Page']), + 'Esperanto': Language(name='Esperanto', + iso_code='eo', + # Q, W, X, and Y not used at all + use_ascii=False, + charsets=['ISO-8859-3'], + alphabet=(u'abcĉdefgÄhÄ¥ijĵklmnoprsÅtuÅ­vz' + u'ABCĈDEFGÄœHĤIJÄ´KLMNOPRSÅœTUŬVZ'), + wiki_start_pages=[u'Vikipedio:ĈefpaÄo']), + 'Spanish': Language(name='Spanish', + iso_code='es', + use_ascii=True, + charsets=['ISO-8859-1', 'ISO-8859-15', + 'WINDOWS-1252'], + alphabet=u'ñáéíóúüÑÃÉÃÓÚÜ', + wiki_start_pages=[u'Wikipedia:Portada']), + 'Estonian': Language(name='Estonian', + iso_code='et', + use_ascii=False, + charsets=['ISO-8859-4', 'ISO-8859-13', + 'WINDOWS-1257'], + # C, F, Å , Q, W, X, Y, Z, Ž are only for + # loanwords + alphabet=(u'ABDEGHIJKLMNOPRSTUVÕÄÖÜ' + u'abdeghijklmnoprstuvõäöü'), + wiki_start_pages=[u'Esileht']), + 'Finnish': Language(name='Finnish', + iso_code='fi', + use_ascii=True, + charsets=['ISO-8859-1', 'ISO-8859-15', + 'WINDOWS-1252'], + alphabet=u'ÅÄÖŠŽåäöšž', + wiki_start_pages=[u'Wikipedia:Etusivu']), + 'French': Language(name='French', + iso_code='fr', + use_ascii=True, + charsets=['ISO-8859-1', 'ISO-8859-15', + 'WINDOWS-1252'], + alphabet=u'œàâçèéîïùûêŒÀÂÇÈÉÎÃÙÛÊ', + wiki_start_pages=[u'Wikipédia:Accueil_principal', + u'BÅ“uf (animal)']), + 'Hebrew': Language(name='Hebrew', + iso_code='he', + use_ascii=False, + charsets=['ISO-8859-8', 'WINDOWS-1255'], + alphabet=u'×בגדהוזחטיךכל×מןנסעףפץצקרשתװױײ', + wiki_start_pages=[u'עמוד_ר×שי']), + 'Croatian': Language(name='Croatian', + iso_code='hr', + # Q, W, X, Y are only used for foreign words. + use_ascii=False, + charsets=['ISO-8859-2', 'WINDOWS-1250'], + alphabet=(u'abcÄćdÄ‘efghijklmnoprsÅ¡tuvzž' + u'ABCČĆDÄEFGHIJKLMNOPRSÅ TUVZŽ'), + wiki_start_pages=[u'Glavna_stranica']), + 'Hungarian': Language(name='Hungarian', + iso_code='hu', + # Q, W, X, Y are only used for foreign words. + use_ascii=False, + charsets=['ISO-8859-2', 'WINDOWS-1250'], + alphabet=(u'abcdefghijklmnoprstuvzáéíóöőúüű' + u'ABCDEFGHIJKLMNOPRSTUVZÃÉÃÓÖÅÚÜŰ'), + wiki_start_pages=[u'KezdÅ‘lap']), + 'Italian': Language(name='Italian', + iso_code='it', + use_ascii=True, + charsets=['ISO-8859-1', 'ISO-8859-15', + 'WINDOWS-1252'], + alphabet=u'ÀÈÉÌÒÓÙàèéìòóù', + wiki_start_pages=[u'Pagina_principale']), + 'Lithuanian': Language(name='Lithuanian', + iso_code='lt', + use_ascii=False, + charsets=['ISO-8859-13', 'WINDOWS-1257', + 'ISO-8859-4'], + # Q, W, and X not used at all + alphabet=(u'AÄ„BCÄŒDEĘĖFGHIÄ®YJKLMNOPRSÅ TUŲŪVZŽ' + u'aÄ…bcÄdeęėfghiįyjklmnoprsÅ¡tuųūvzž'), + wiki_start_pages=[u'Pagrindinis_puslapis']), + 'Latvian': Language(name='Latvian', + iso_code='lv', + use_ascii=False, + charsets=['ISO-8859-13', 'WINDOWS-1257', + 'ISO-8859-4'], + # Q, W, X, Y are only for loanwords + alphabet=(u'AÄ€BCÄŒDEÄ’FGÄ¢HIĪJKĶLÄ»MNÅ…OPRSÅ TUŪVZŽ' + u'aÄbcÄdeÄ“fgÄ£hiÄ«jkÄ·lļmnņoprsÅ¡tuÅ«vzž'), + wiki_start_pages=[u'SÄkumlapa']), + 'Macedonian': Language(name='Macedonian', + iso_code='mk', + use_ascii=False, + charsets=['ISO-8859-5', 'WINDOWS-1251', + 'MacCyrillic', 'IBM855'], + alphabet=(u'ÐБВГДЃЕЖЗЅИЈКЛЉМÐЊОПРСТЌУФХЦЧÐШ' + u'абвгдѓежзѕијклљмнњопрÑтќуфхцчџш'), + wiki_start_pages=[u'Главна_Ñтраница']), + 'Dutch': Language(name='Dutch', + iso_code='nl', + use_ascii=True, + charsets=['ISO-8859-1', 'WINDOWS-1252'], + wiki_start_pages=[u'Hoofdpagina']), + 'Polish': Language(name='Polish', + iso_code='pl', + # Q and X are only used for foreign words. + use_ascii=False, + charsets=['ISO-8859-2', 'WINDOWS-1250'], + alphabet=(u'AÄ„BCĆDEĘFGHIJKLÅMNŃOÓPRSÅšTUWYZŹŻ' + u'aÄ…bcćdeÄ™fghijklÅ‚mnÅ„oóprsÅ›tuwyzźż'), + wiki_start_pages=[u'Wikipedia:Strona_główna']), + 'Portuguese': Language(name='Portuguese', + iso_code='pt', + use_ascii=True, + charsets=['ISO-8859-1', 'ISO-8859-15', + 'WINDOWS-1252'], + alphabet=u'ÃÂÃÀÇÉÊÃÓÔÕÚáâãàçéêíóôõú', + wiki_start_pages=[u'Wikipédia:Página_principal']), + 'Romanian': Language(name='Romanian', + iso_code='ro', + use_ascii=True, + charsets=['ISO-8859-2', 'WINDOWS-1250'], + alphabet=u'ăâîșțĂÂÎȘȚ', + wiki_start_pages=[u'Pagina_principală']), + 'Russian': Language(name='Russian', + iso_code='ru', + use_ascii=False, + charsets=['ISO-8859-5', 'WINDOWS-1251', + 'KOI8-R', 'MacCyrillic', 'IBM866', + 'IBM855'], + alphabet=(u'абвгдеёжзийклмнопрÑтуфхцчшщъыьÑÑŽÑ' + u'ÐБВГДЕÐЖЗИЙКЛМÐОПРСТУФХЦЧШЩЪЫЬЭЮЯ'), + wiki_start_pages=[u'ЗаглавнаÑ_Ñтраница']), + 'Slovak': Language(name='Slovak', + iso_code='sk', + use_ascii=True, + charsets=['ISO-8859-2', 'WINDOWS-1250'], + alphabet=u'áäÄÄéíĺľňóôŕšťúýžÃÄČĎÉÃĹĽŇÓÔŔŠŤÚÃŽ', + wiki_start_pages=[u'Hlavná_stránka']), + 'Slovene': Language(name='Slovene', + iso_code='sl', + # Q, W, X, Y are only used for foreign words. + use_ascii=False, + charsets=['ISO-8859-2', 'WINDOWS-1250'], + alphabet=(u'abcÄdefghijklmnoprsÅ¡tuvzž' + u'ABCÄŒDEFGHIJKLMNOPRSÅ TUVZŽ'), + wiki_start_pages=[u'Glavna_stran']), + # Serbian can be written in both Latin and Cyrillic, but there's no + # simple way to get the Latin alphabet pages from Wikipedia through + # the API, so for now we just support Cyrillic. + 'Serbian': Language(name='Serbian', + iso_code='sr', + alphabet=(u'ÐБВГДЂЕЖЗИЈКЛЉМÐЊОПРСТЋУФХЦЧÐШ' + u'абвгдђежзијклљмнњопрÑтћуфхцчџш'), + charsets=['ISO-8859-5', 'WINDOWS-1251', + 'MacCyrillic', 'IBM855'], + wiki_start_pages=[u'Главна_Ñтрана']), + 'Thai': Language(name='Thai', + iso_code='th', + use_ascii=False, + charsets=['ISO-8859-11', 'TIS-620', 'CP874'], + alphabet=u'à¸à¸‚ฃคฅฆงจฉชซฌà¸à¸Žà¸à¸à¸‘ฒณดตถทธนบปผà¸à¸žà¸Ÿà¸ à¸¡à¸¢à¸£à¸¤à¸¥à¸¦à¸§à¸¨à¸©à¸ªà¸«à¸¬à¸­à¸®à¸¯à¸°à¸±à¸²à¸³à¸´à¸µà¸¶à¸·à¸ºà¸¸à¸¹à¸¿à¹€à¹à¹‚ใไๅๆ็่้๊๋์à¹à¹Žà¹à¹à¹‘๒๓๔๕๖๗๘๙๚๛', + wiki_start_pages=[u'หน้าหลัà¸']), + 'Turkish': Language(name='Turkish', + iso_code='tr', + # Q, W, and X are not used by Turkish + use_ascii=False, + charsets=['ISO-8859-3', 'ISO-8859-9', + 'WINDOWS-1254'], + alphabet=(u'abcçdefgÄŸhıijklmnoöprsÅŸtuüvyzâîû' + u'ABCÇDEFGÄžHIÄ°JKLMNOÖPRSÅžTUÃœVYZÂÎÛ'), + wiki_start_pages=[u'Ana_Sayfa']), + 'Vietnamese': Language(name='Vietnamese', + iso_code='vi', + use_ascii=False, + # Windows-1258 is the only common 8-bit + # Vietnamese encoding supported by Python. + # From Wikipedia: + # For systems that lack support for Unicode, + # dozens of 8-bit Vietnamese code pages are + # available.[1] The most common are VISCII + # (TCVN 5712:1993), VPS, and Windows-1258.[3] + # Where ASCII is required, such as when + # ensuring readability in plain text e-mail, + # Vietnamese letters are often encoded + # according to Vietnamese Quoted-Readable + # (VIQR) or VSCII Mnemonic (VSCII-MNEM),[4] + # though usage of either variable-width + # scheme has declined dramatically following + # the adoption of Unicode on the World Wide + # Web. + charsets=['WINDOWS-1258'], + alphabet=(u'aăâbcdÄ‘eêghiklmnoôơpqrstuÆ°vxy' + u'AĂÂBCDÄEÊGHIKLMNOÔƠPQRSTUƯVXY'), + wiki_start_pages=[u'Chữ_Quốc_ngữ']), + } diff --git a/libs/chardet/sbcharsetprober.py b/libs/chardet/sbcharsetprober.py index 0adb51de5..46ba835c6 100644 --- a/libs/chardet/sbcharsetprober.py +++ b/libs/chardet/sbcharsetprober.py @@ -26,10 +26,22 @@ # 02110-1301 USA ######################### END LICENSE BLOCK ######################### +from collections import namedtuple + from .charsetprober import CharSetProber from .enums import CharacterCategory, ProbingState, SequenceLikelihood +SingleByteCharSetModel = namedtuple('SingleByteCharSetModel', + ['charset_name', + 'language', + 'char_to_order_map', + 'language_model', + 'typical_positive_ratio', + 'keep_ascii_letters', + 'alphabet']) + + class SingleByteCharSetProber(CharSetProber): SAMPLE_SIZE = 64 SB_ENOUGH_REL_THRESHOLD = 1024 # 0.25 * SAMPLE_SIZE^2 @@ -65,25 +77,25 @@ class SingleByteCharSetProber(CharSetProber): if self._name_prober: return self._name_prober.charset_name else: - return self._model['charset_name'] + return self._model.charset_name @property def language(self): if self._name_prober: return self._name_prober.language else: - return self._model.get('language') + return self._model.language def feed(self, byte_str): - if not self._model['keep_english_letter']: + # TODO: Make filter_international_words keep things in self.alphabet + if not self._model.keep_ascii_letters: byte_str = self.filter_international_words(byte_str) if not byte_str: return self.state - char_to_order_map = self._model['char_to_order_map'] - for i, c in enumerate(byte_str): - # XXX: Order is in range 1-64, so one would think we want 0-63 here, - # but that leads to 27 more test failures than before. - order = char_to_order_map[c] + char_to_order_map = self._model.char_to_order_map + language_model = self._model.language_model + for char in byte_str: + order = char_to_order_map.get(char, CharacterCategory.UNDEFINED) # XXX: This was SYMBOL_CAT_ORDER before, with a value of 250, but # CharacterCategory.SYMBOL is actually 253, so we use CONTROL # to make it closer to the original intent. The only difference @@ -91,20 +103,21 @@ class SingleByteCharSetProber(CharSetProber): # _total_char purposes. if order < CharacterCategory.CONTROL: self._total_char += 1 + # TODO: Follow uchardet's lead and discount confidence for frequent + # control characters. + # See https://github.com/BYVoid/uchardet/commit/55b4f23971db61 if order < self.SAMPLE_SIZE: self._freq_char += 1 if self._last_order < self.SAMPLE_SIZE: self._total_seqs += 1 if not self._reversed: - i = (self._last_order * self.SAMPLE_SIZE) + order - model = self._model['precedence_matrix'][i] - else: # reverse the order of the letters in the lookup - i = (order * self.SAMPLE_SIZE) + self._last_order - model = self._model['precedence_matrix'][i] - self._seq_counters[model] += 1 + lm_cat = language_model[self._last_order][order] + else: + lm_cat = language_model[order][self._last_order] + self._seq_counters[lm_cat] += 1 self._last_order = order - charset_name = self._model['charset_name'] + charset_name = self._model.charset_name if self.state == ProbingState.DETECTING: if self._total_seqs > self.SB_ENOUGH_REL_THRESHOLD: confidence = self.get_confidence() @@ -125,7 +138,7 @@ class SingleByteCharSetProber(CharSetProber): r = 0.01 if self._total_seqs > 0: r = ((1.0 * self._seq_counters[SequenceLikelihood.POSITIVE]) / - self._total_seqs / self._model['typical_positive_ratio']) + self._total_seqs / self._model.typical_positive_ratio) r = r * self._freq_char / self._total_char if r >= 1.0: r = 0.99 diff --git a/libs/chardet/sbcsgroupprober.py b/libs/chardet/sbcsgroupprober.py index 98e95dc1a..bdeef4e15 100644 --- a/libs/chardet/sbcsgroupprober.py +++ b/libs/chardet/sbcsgroupprober.py @@ -27,47 +27,57 @@ ######################### END LICENSE BLOCK ######################### from .charsetgroupprober import CharSetGroupProber -from .sbcharsetprober import SingleByteCharSetProber -from .langcyrillicmodel import (Win1251CyrillicModel, Koi8rModel, - Latin5CyrillicModel, MacCyrillicModel, - Ibm866Model, Ibm855Model) -from .langgreekmodel import Latin7GreekModel, Win1253GreekModel -from .langbulgarianmodel import Latin5BulgarianModel, Win1251BulgarianModel -# from .langhungarianmodel import Latin2HungarianModel, Win1250HungarianModel -from .langthaimodel import TIS620ThaiModel -from .langhebrewmodel import Win1255HebrewModel from .hebrewprober import HebrewProber -from .langturkishmodel import Latin5TurkishModel +from .langbulgarianmodel import (ISO_8859_5_BULGARIAN_MODEL, + WINDOWS_1251_BULGARIAN_MODEL) +from .langgreekmodel import ISO_8859_7_GREEK_MODEL, WINDOWS_1253_GREEK_MODEL +from .langhebrewmodel import WINDOWS_1255_HEBREW_MODEL +# from .langhungarianmodel import (ISO_8859_2_HUNGARIAN_MODEL, +# WINDOWS_1250_HUNGARIAN_MODEL) +from .langrussianmodel import (IBM855_RUSSIAN_MODEL, IBM866_RUSSIAN_MODEL, + ISO_8859_5_RUSSIAN_MODEL, KOI8_R_RUSSIAN_MODEL, + MACCYRILLIC_RUSSIAN_MODEL, + WINDOWS_1251_RUSSIAN_MODEL) +from .langthaimodel import TIS_620_THAI_MODEL +from .langturkishmodel import ISO_8859_9_TURKISH_MODEL +from .sbcharsetprober import SingleByteCharSetProber class SBCSGroupProber(CharSetGroupProber): def __init__(self): super(SBCSGroupProber, self).__init__() + hebrew_prober = HebrewProber() + logical_hebrew_prober = SingleByteCharSetProber(WINDOWS_1255_HEBREW_MODEL, + False, hebrew_prober) + # TODO: See if using ISO-8859-8 Hebrew model works better here, since + # it's actually the visual one + visual_hebrew_prober = SingleByteCharSetProber(WINDOWS_1255_HEBREW_MODEL, + True, hebrew_prober) + hebrew_prober.set_model_probers(logical_hebrew_prober, + visual_hebrew_prober) + # TODO: ORDER MATTERS HERE. I changed the order vs what was in master + # and several tests failed that did not before. Some thought + # should be put into the ordering, and we should consider making + # order not matter here, because that is very counter-intuitive. self.probers = [ - SingleByteCharSetProber(Win1251CyrillicModel), - SingleByteCharSetProber(Koi8rModel), - SingleByteCharSetProber(Latin5CyrillicModel), - SingleByteCharSetProber(MacCyrillicModel), - SingleByteCharSetProber(Ibm866Model), - SingleByteCharSetProber(Ibm855Model), - SingleByteCharSetProber(Latin7GreekModel), - SingleByteCharSetProber(Win1253GreekModel), - SingleByteCharSetProber(Latin5BulgarianModel), - SingleByteCharSetProber(Win1251BulgarianModel), + SingleByteCharSetProber(WINDOWS_1251_RUSSIAN_MODEL), + SingleByteCharSetProber(KOI8_R_RUSSIAN_MODEL), + SingleByteCharSetProber(ISO_8859_5_RUSSIAN_MODEL), + SingleByteCharSetProber(MACCYRILLIC_RUSSIAN_MODEL), + SingleByteCharSetProber(IBM866_RUSSIAN_MODEL), + SingleByteCharSetProber(IBM855_RUSSIAN_MODEL), + SingleByteCharSetProber(ISO_8859_7_GREEK_MODEL), + SingleByteCharSetProber(WINDOWS_1253_GREEK_MODEL), + SingleByteCharSetProber(ISO_8859_5_BULGARIAN_MODEL), + SingleByteCharSetProber(WINDOWS_1251_BULGARIAN_MODEL), # TODO: Restore Hungarian encodings (iso-8859-2 and windows-1250) # after we retrain model. - # SingleByteCharSetProber(Latin2HungarianModel), - # SingleByteCharSetProber(Win1250HungarianModel), - SingleByteCharSetProber(TIS620ThaiModel), - SingleByteCharSetProber(Latin5TurkishModel), + # SingleByteCharSetProber(ISO_8859_2_HUNGARIAN_MODEL), + # SingleByteCharSetProber(WINDOWS_1250_HUNGARIAN_MODEL), + SingleByteCharSetProber(TIS_620_THAI_MODEL), + SingleByteCharSetProber(ISO_8859_9_TURKISH_MODEL), + hebrew_prober, + logical_hebrew_prober, + visual_hebrew_prober, ] - hebrew_prober = HebrewProber() - logical_hebrew_prober = SingleByteCharSetProber(Win1255HebrewModel, - False, hebrew_prober) - visual_hebrew_prober = SingleByteCharSetProber(Win1255HebrewModel, True, - hebrew_prober) - hebrew_prober.set_model_probers(logical_hebrew_prober, visual_hebrew_prober) - self.probers.extend([hebrew_prober, logical_hebrew_prober, - visual_hebrew_prober]) - self.reset() diff --git a/libs/chardet/universaldetector.py b/libs/chardet/universaldetector.py index 7b4e92d61..055a8ac1b 100644 --- a/libs/chardet/universaldetector.py +++ b/libs/chardet/universaldetector.py @@ -266,7 +266,7 @@ class UniversalDetector(object): 'language': max_prober.language} # Log all prober confidences if none met MINIMUM_THRESHOLD - if self.logger.getEffectiveLevel() == logging.DEBUG: + if self.logger.getEffectiveLevel() <= logging.DEBUG: if self.result['encoding'] is None: self.logger.debug('no probers hit minimum threshold') for group_prober in self._charset_probers: @@ -280,7 +280,7 @@ class UniversalDetector(object): prober.get_confidence()) else: self.logger.debug('%s %s confidence = %s', - prober.charset_name, - prober.language, - prober.get_confidence()) + group_prober.charset_name, + group_prober.language, + group_prober.get_confidence()) return self.result diff --git a/libs/chardet/version.py b/libs/chardet/version.py index bb2a34a70..70369b9d6 100644 --- a/libs/chardet/version.py +++ b/libs/chardet/version.py @@ -5,5 +5,5 @@ from within setup.py and from chardet subpackages. :author: Dan Blanchard (dan.blanchard@gmail.com) """ -__version__ = "3.0.4" +__version__ = "4.0.0" VERSION = __version__.split('.') diff --git a/libs/click/__init__.py b/libs/click/__init__.py index d3c33660a..a2ed5d135 100644 --- a/libs/click/__init__.py +++ b/libs/click/__init__.py @@ -1,97 +1,75 @@ -# -*- coding: utf-8 -*- """ -click -~~~~~ - Click is a simple Python module inspired by the stdlib optparse to make writing command line scripts fun. Unlike other modules, it's based around a simple API that does not come with too much magic and is composable. - -:copyright: © 2014 by the Pallets team. -:license: BSD, see LICENSE.rst for more details. """ +from .core import Argument as Argument +from .core import BaseCommand as BaseCommand +from .core import Command as Command +from .core import CommandCollection as CommandCollection +from .core import Context as Context +from .core import Group as Group +from .core import MultiCommand as MultiCommand +from .core import Option as Option +from .core import Parameter as Parameter +from .decorators import argument as argument +from .decorators import command as command +from .decorators import confirmation_option as confirmation_option +from .decorators import group as group +from .decorators import help_option as help_option +from .decorators import make_pass_decorator as make_pass_decorator +from .decorators import option as option +from .decorators import pass_context as pass_context +from .decorators import pass_obj as pass_obj +from .decorators import password_option as password_option +from .decorators import version_option as version_option +from .exceptions import Abort as Abort +from .exceptions import BadArgumentUsage as BadArgumentUsage +from .exceptions import BadOptionUsage as BadOptionUsage +from .exceptions import BadParameter as BadParameter +from .exceptions import ClickException as ClickException +from .exceptions import FileError as FileError +from .exceptions import MissingParameter as MissingParameter +from .exceptions import NoSuchOption as NoSuchOption +from .exceptions import UsageError as UsageError +from .formatting import HelpFormatter as HelpFormatter +from .formatting import wrap_text as wrap_text +from .globals import get_current_context as get_current_context +from .parser import OptionParser as OptionParser +from .termui import clear as clear +from .termui import confirm as confirm +from .termui import echo_via_pager as echo_via_pager +from .termui import edit as edit +from .termui import get_terminal_size as get_terminal_size +from .termui import getchar as getchar +from .termui import launch as launch +from .termui import pause as pause +from .termui import progressbar as progressbar +from .termui import prompt as prompt +from .termui import secho as secho +from .termui import style as style +from .termui import unstyle as unstyle +from .types import BOOL as BOOL +from .types import Choice as Choice +from .types import DateTime as DateTime +from .types import File as File +from .types import FLOAT as FLOAT +from .types import FloatRange as FloatRange +from .types import INT as INT +from .types import IntRange as IntRange +from .types import ParamType as ParamType +from .types import Path as Path +from .types import STRING as STRING +from .types import Tuple as Tuple +from .types import UNPROCESSED as UNPROCESSED +from .types import UUID as UUID +from .utils import echo as echo +from .utils import format_filename as format_filename +from .utils import get_app_dir as get_app_dir +from .utils import get_binary_stream as get_binary_stream +from .utils import get_os_args as get_os_args +from .utils import get_text_stream as get_text_stream +from .utils import open_file as open_file -# Core classes -from .core import Context, BaseCommand, Command, MultiCommand, Group, \ - CommandCollection, Parameter, Option, Argument - -# Globals -from .globals import get_current_context - -# Decorators -from .decorators import pass_context, pass_obj, make_pass_decorator, \ - command, group, argument, option, confirmation_option, \ - password_option, version_option, help_option - -# Types -from .types import ParamType, File, Path, Choice, IntRange, Tuple, \ - DateTime, STRING, INT, FLOAT, BOOL, UUID, UNPROCESSED, FloatRange - -# Utilities -from .utils import echo, get_binary_stream, get_text_stream, open_file, \ - format_filename, get_app_dir, get_os_args - -# Terminal functions -from .termui import prompt, confirm, get_terminal_size, echo_via_pager, \ - progressbar, clear, style, unstyle, secho, edit, launch, getchar, \ - pause - -# Exceptions -from .exceptions import ClickException, UsageError, BadParameter, \ - FileError, Abort, NoSuchOption, BadOptionUsage, BadArgumentUsage, \ - MissingParameter - -# Formatting -from .formatting import HelpFormatter, wrap_text - -# Parsing -from .parser import OptionParser - - -__all__ = [ - # Core classes - 'Context', 'BaseCommand', 'Command', 'MultiCommand', 'Group', - 'CommandCollection', 'Parameter', 'Option', 'Argument', - - # Globals - 'get_current_context', - - # Decorators - 'pass_context', 'pass_obj', 'make_pass_decorator', 'command', 'group', - 'argument', 'option', 'confirmation_option', 'password_option', - 'version_option', 'help_option', - - # Types - 'ParamType', 'File', 'Path', 'Choice', 'IntRange', 'Tuple', - 'DateTime', 'STRING', 'INT', 'FLOAT', 'BOOL', 'UUID', 'UNPROCESSED', - 'FloatRange', - - # Utilities - 'echo', 'get_binary_stream', 'get_text_stream', 'open_file', - 'format_filename', 'get_app_dir', 'get_os_args', - - # Terminal functions - 'prompt', 'confirm', 'get_terminal_size', 'echo_via_pager', - 'progressbar', 'clear', 'style', 'unstyle', 'secho', 'edit', 'launch', - 'getchar', 'pause', - - # Exceptions - 'ClickException', 'UsageError', 'BadParameter', 'FileError', - 'Abort', 'NoSuchOption', 'BadOptionUsage', 'BadArgumentUsage', - 'MissingParameter', - - # Formatting - 'HelpFormatter', 'wrap_text', - - # Parsing - 'OptionParser', -] - - -# Controls if click should emit the warning about the use of unicode -# literals. -disable_unicode_literals_warning = False - - -__version__ = '7.0' +__version__ = "8.0.3" diff --git a/libs/click/_bashcomplete.py b/libs/click/_bashcomplete.py deleted file mode 100644 index a5f1084c9..000000000 --- a/libs/click/_bashcomplete.py +++ /dev/null @@ -1,293 +0,0 @@ -import copy -import os -import re - -from .utils import echo -from .parser import split_arg_string -from .core import MultiCommand, Option, Argument -from .types import Choice - -try: - from collections import abc -except ImportError: - import collections as abc - -WORDBREAK = '=' - -# Note, only BASH version 4.4 and later have the nosort option. -COMPLETION_SCRIPT_BASH = ''' -%(complete_func)s() { - local IFS=$'\n' - COMPREPLY=( $( env COMP_WORDS="${COMP_WORDS[*]}" \\ - COMP_CWORD=$COMP_CWORD \\ - %(autocomplete_var)s=complete $1 ) ) - return 0 -} - -%(complete_func)setup() { - local COMPLETION_OPTIONS="" - local BASH_VERSION_ARR=(${BASH_VERSION//./ }) - # Only BASH version 4.4 and later have the nosort option. - if [ ${BASH_VERSION_ARR[0]} -gt 4 ] || ([ ${BASH_VERSION_ARR[0]} -eq 4 ] && [ ${BASH_VERSION_ARR[1]} -ge 4 ]); then - COMPLETION_OPTIONS="-o nosort" - fi - - complete $COMPLETION_OPTIONS -F %(complete_func)s %(script_names)s -} - -%(complete_func)setup -''' - -COMPLETION_SCRIPT_ZSH = ''' -%(complete_func)s() { - local -a completions - local -a completions_with_descriptions - local -a response - response=("${(@f)$( env COMP_WORDS=\"${words[*]}\" \\ - COMP_CWORD=$((CURRENT-1)) \\ - %(autocomplete_var)s=\"complete_zsh\" \\ - %(script_names)s )}") - - for key descr in ${(kv)response}; do - if [[ "$descr" == "_" ]]; then - completions+=("$key") - else - completions_with_descriptions+=("$key":"$descr") - fi - done - - if [ -n "$completions_with_descriptions" ]; then - _describe -V unsorted completions_with_descriptions -U -Q - fi - - if [ -n "$completions" ]; then - compadd -U -V unsorted -Q -a completions - fi - compstate[insert]="automenu" -} - -compdef %(complete_func)s %(script_names)s -''' - -_invalid_ident_char_re = re.compile(r'[^a-zA-Z0-9_]') - - -def get_completion_script(prog_name, complete_var, shell): - cf_name = _invalid_ident_char_re.sub('', prog_name.replace('-', '_')) - script = COMPLETION_SCRIPT_ZSH if shell == 'zsh' else COMPLETION_SCRIPT_BASH - return (script % { - 'complete_func': '_%s_completion' % cf_name, - 'script_names': prog_name, - 'autocomplete_var': complete_var, - }).strip() + ';' - - -def resolve_ctx(cli, prog_name, args): - """ - Parse into a hierarchy of contexts. Contexts are connected through the parent variable. - :param cli: command definition - :param prog_name: the program that is running - :param args: full list of args - :return: the final context/command parsed - """ - ctx = cli.make_context(prog_name, args, resilient_parsing=True) - args = ctx.protected_args + ctx.args - while args: - if isinstance(ctx.command, MultiCommand): - if not ctx.command.chain: - cmd_name, cmd, args = ctx.command.resolve_command(ctx, args) - if cmd is None: - return ctx - ctx = cmd.make_context(cmd_name, args, parent=ctx, - resilient_parsing=True) - args = ctx.protected_args + ctx.args - else: - # Walk chained subcommand contexts saving the last one. - while args: - cmd_name, cmd, args = ctx.command.resolve_command(ctx, args) - if cmd is None: - return ctx - sub_ctx = cmd.make_context(cmd_name, args, parent=ctx, - allow_extra_args=True, - allow_interspersed_args=False, - resilient_parsing=True) - args = sub_ctx.args - ctx = sub_ctx - args = sub_ctx.protected_args + sub_ctx.args - else: - break - return ctx - - -def start_of_option(param_str): - """ - :param param_str: param_str to check - :return: whether or not this is the start of an option declaration (i.e. starts "-" or "--") - """ - return param_str and param_str[:1] == '-' - - -def is_incomplete_option(all_args, cmd_param): - """ - :param all_args: the full original list of args supplied - :param cmd_param: the current command paramter - :return: whether or not the last option declaration (i.e. starts "-" or "--") is incomplete and - corresponds to this cmd_param. In other words whether this cmd_param option can still accept - values - """ - if not isinstance(cmd_param, Option): - return False - if cmd_param.is_flag: - return False - last_option = None - for index, arg_str in enumerate(reversed([arg for arg in all_args if arg != WORDBREAK])): - if index + 1 > cmd_param.nargs: - break - if start_of_option(arg_str): - last_option = arg_str - - return True if last_option and last_option in cmd_param.opts else False - - -def is_incomplete_argument(current_params, cmd_param): - """ - :param current_params: the current params and values for this argument as already entered - :param cmd_param: the current command parameter - :return: whether or not the last argument is incomplete and corresponds to this cmd_param. In - other words whether or not the this cmd_param argument can still accept values - """ - if not isinstance(cmd_param, Argument): - return False - current_param_values = current_params[cmd_param.name] - if current_param_values is None: - return True - if cmd_param.nargs == -1: - return True - if isinstance(current_param_values, abc.Iterable) \ - and cmd_param.nargs > 1 and len(current_param_values) < cmd_param.nargs: - return True - return False - - -def get_user_autocompletions(ctx, args, incomplete, cmd_param): - """ - :param ctx: context associated with the parsed command - :param args: full list of args - :param incomplete: the incomplete text to autocomplete - :param cmd_param: command definition - :return: all the possible user-specified completions for the param - """ - results = [] - if isinstance(cmd_param.type, Choice): - # Choices don't support descriptions. - results = [(c, None) - for c in cmd_param.type.choices if str(c).startswith(incomplete)] - elif cmd_param.autocompletion is not None: - dynamic_completions = cmd_param.autocompletion(ctx=ctx, - args=args, - incomplete=incomplete) - results = [c if isinstance(c, tuple) else (c, None) - for c in dynamic_completions] - return results - - -def get_visible_commands_starting_with(ctx, starts_with): - """ - :param ctx: context associated with the parsed command - :starts_with: string that visible commands must start with. - :return: all visible (not hidden) commands that start with starts_with. - """ - for c in ctx.command.list_commands(ctx): - if c.startswith(starts_with): - command = ctx.command.get_command(ctx, c) - if not command.hidden: - yield command - - -def add_subcommand_completions(ctx, incomplete, completions_out): - # Add subcommand completions. - if isinstance(ctx.command, MultiCommand): - completions_out.extend( - [(c.name, c.get_short_help_str()) for c in get_visible_commands_starting_with(ctx, incomplete)]) - - # Walk up the context list and add any other completion possibilities from chained commands - while ctx.parent is not None: - ctx = ctx.parent - if isinstance(ctx.command, MultiCommand) and ctx.command.chain: - remaining_commands = [c for c in get_visible_commands_starting_with(ctx, incomplete) - if c.name not in ctx.protected_args] - completions_out.extend([(c.name, c.get_short_help_str()) for c in remaining_commands]) - - -def get_choices(cli, prog_name, args, incomplete): - """ - :param cli: command definition - :param prog_name: the program that is running - :param args: full list of args - :param incomplete: the incomplete text to autocomplete - :return: all the possible completions for the incomplete - """ - all_args = copy.deepcopy(args) - - ctx = resolve_ctx(cli, prog_name, args) - if ctx is None: - return [] - - # In newer versions of bash long opts with '='s are partitioned, but it's easier to parse - # without the '=' - if start_of_option(incomplete) and WORDBREAK in incomplete: - partition_incomplete = incomplete.partition(WORDBREAK) - all_args.append(partition_incomplete[0]) - incomplete = partition_incomplete[2] - elif incomplete == WORDBREAK: - incomplete = '' - - completions = [] - if start_of_option(incomplete): - # completions for partial options - for param in ctx.command.params: - if isinstance(param, Option) and not param.hidden: - param_opts = [param_opt for param_opt in param.opts + - param.secondary_opts if param_opt not in all_args or param.multiple] - completions.extend([(o, param.help) for o in param_opts if o.startswith(incomplete)]) - return completions - # completion for option values from user supplied values - for param in ctx.command.params: - if is_incomplete_option(all_args, param): - return get_user_autocompletions(ctx, all_args, incomplete, param) - # completion for argument values from user supplied values - for param in ctx.command.params: - if is_incomplete_argument(ctx.params, param): - return get_user_autocompletions(ctx, all_args, incomplete, param) - - add_subcommand_completions(ctx, incomplete, completions) - # Sort before returning so that proper ordering can be enforced in custom types. - return sorted(completions) - - -def do_complete(cli, prog_name, include_descriptions): - cwords = split_arg_string(os.environ['COMP_WORDS']) - cword = int(os.environ['COMP_CWORD']) - args = cwords[1:cword] - try: - incomplete = cwords[cword] - except IndexError: - incomplete = '' - - for item in get_choices(cli, prog_name, args, incomplete): - echo(item[0]) - if include_descriptions: - # ZSH has trouble dealing with empty array parameters when returned from commands, so use a well defined character '_' to indicate no description is present. - echo(item[1] if item[1] else '_') - - return True - - -def bashcomplete(cli, prog_name, complete_var, complete_instr): - if complete_instr.startswith('source'): - shell = 'zsh' if complete_instr == 'source_zsh' else 'bash' - echo(get_completion_script(prog_name, complete_var, shell)) - return True - elif complete_instr == 'complete' or complete_instr == 'complete_zsh': - return do_complete(cli, prog_name, complete_instr == 'complete_zsh') - return False diff --git a/libs/click/_compat.py b/libs/click/_compat.py index 937e2301d..b9e1f0d39 100644 --- a/libs/click/_compat.py +++ b/libs/click/_compat.py @@ -1,92 +1,90 @@ -import re +import codecs import io import os +import re import sys -import codecs +import typing as t from weakref import WeakKeyDictionary - -PY2 = sys.version_info[0] == 2 -CYGWIN = sys.platform.startswith('cygwin') +CYGWIN = sys.platform.startswith("cygwin") +MSYS2 = sys.platform.startswith("win") and ("GCC" in sys.version) # Determine local App Engine environment, per Google's own suggestion -APP_ENGINE = ('APPENGINE_RUNTIME' in os.environ and - 'Development/' in os.environ['SERVER_SOFTWARE']) -WIN = sys.platform.startswith('win') and not APP_ENGINE -DEFAULT_COLUMNS = 80 +APP_ENGINE = "APPENGINE_RUNTIME" in os.environ and "Development/" in os.environ.get( + "SERVER_SOFTWARE", "" +) +WIN = sys.platform.startswith("win") and not APP_ENGINE and not MSYS2 +auto_wrap_for_ansi: t.Optional[t.Callable[[t.TextIO], t.TextIO]] = None +_ansi_re = re.compile(r"\033\[[;?0-9]*[a-zA-Z]") -_ansi_re = re.compile(r'\033\[((?:\d|;)*)([a-zA-Z])') - - -def get_filesystem_encoding(): +def get_filesystem_encoding() -> str: return sys.getfilesystemencoding() or sys.getdefaultencoding() -def _make_text_stream(stream, encoding, errors, - force_readable=False, force_writable=False): +def _make_text_stream( + stream: t.BinaryIO, + encoding: t.Optional[str], + errors: t.Optional[str], + force_readable: bool = False, + force_writable: bool = False, +) -> t.TextIO: if encoding is None: encoding = get_best_encoding(stream) if errors is None: - errors = 'replace' - return _NonClosingTextIOWrapper(stream, encoding, errors, - line_buffering=True, - force_readable=force_readable, - force_writable=force_writable) + errors = "replace" + return _NonClosingTextIOWrapper( + stream, + encoding, + errors, + line_buffering=True, + force_readable=force_readable, + force_writable=force_writable, + ) -def is_ascii_encoding(encoding): +def is_ascii_encoding(encoding: str) -> bool: """Checks if a given encoding is ascii.""" try: - return codecs.lookup(encoding).name == 'ascii' + return codecs.lookup(encoding).name == "ascii" except LookupError: return False -def get_best_encoding(stream): +def get_best_encoding(stream: t.IO) -> str: """Returns the default stream encoding if not found.""" - rv = getattr(stream, 'encoding', None) or sys.getdefaultencoding() + rv = getattr(stream, "encoding", None) or sys.getdefaultencoding() if is_ascii_encoding(rv): - return 'utf-8' + return "utf-8" return rv class _NonClosingTextIOWrapper(io.TextIOWrapper): + def __init__( + self, + stream: t.BinaryIO, + encoding: t.Optional[str], + errors: t.Optional[str], + force_readable: bool = False, + force_writable: bool = False, + **extra: t.Any, + ) -> None: + self._stream = stream = t.cast( + t.BinaryIO, _FixupStream(stream, force_readable, force_writable) + ) + super().__init__(stream, encoding, errors, **extra) - def __init__(self, stream, encoding, errors, - force_readable=False, force_writable=False, **extra): - self._stream = stream = _FixupStream(stream, force_readable, - force_writable) - io.TextIOWrapper.__init__(self, stream, encoding, errors, **extra) - - # The io module is a place where the Python 3 text behavior - # was forced upon Python 2, so we need to unbreak - # it to look like Python 2. - if PY2: - def write(self, x): - if isinstance(x, str) or is_bytes(x): - try: - self.flush() - except Exception: - pass - return self.buffer.write(str(x)) - return io.TextIOWrapper.write(self, x) - - def writelines(self, lines): - for line in lines: - self.write(line) - - def __del__(self): + def __del__(self) -> None: try: self.detach() except Exception: pass - def isatty(self): + def isatty(self) -> bool: # https://bitbucket.org/pypy/pypy/issue/1803 return self._stream.isatty() -class _FixupStream(object): +class _FixupStream: """The new io interface needs more from streams than streams traditionally implement. As such, this fix-up code is necessary in some circumstances. @@ -96,56 +94,58 @@ class _FixupStream(object): of jupyter notebook). """ - def __init__(self, stream, force_readable=False, force_writable=False): + def __init__( + self, + stream: t.BinaryIO, + force_readable: bool = False, + force_writable: bool = False, + ): self._stream = stream self._force_readable = force_readable self._force_writable = force_writable - def __getattr__(self, name): + def __getattr__(self, name: str) -> t.Any: return getattr(self._stream, name) - def read1(self, size): - f = getattr(self._stream, 'read1', None) + def read1(self, size: int) -> bytes: + f = getattr(self._stream, "read1", None) + if f is not None: - return f(size) - # We only dispatch to readline instead of read in Python 2 as we - # do not want cause problems with the different implementation - # of line buffering. - if PY2: - return self._stream.readline(size) + return t.cast(bytes, f(size)) + return self._stream.read(size) - def readable(self): + def readable(self) -> bool: if self._force_readable: return True - x = getattr(self._stream, 'readable', None) + x = getattr(self._stream, "readable", None) if x is not None: - return x() + return t.cast(bool, x()) try: self._stream.read(0) except Exception: return False return True - def writable(self): + def writable(self) -> bool: if self._force_writable: return True - x = getattr(self._stream, 'writable', None) + x = getattr(self._stream, "writable", None) if x is not None: - return x() + return t.cast(bool, x()) try: - self._stream.write('') + self._stream.write("") # type: ignore except Exception: try: - self._stream.write(b'') + self._stream.write(b"") except Exception: return False return True - def seekable(self): - x = getattr(self._stream, 'seekable', None) + def seekable(self) -> bool: + x = getattr(self._stream, "seekable", None) if x is not None: - return x() + return t.cast(bool, x()) try: self._stream.seek(self._stream.tell()) except Exception: @@ -153,518 +153,443 @@ class _FixupStream(object): return True -if PY2: - text_type = unicode - bytes = str - raw_input = raw_input - string_types = (str, unicode) - int_types = (int, long) - iteritems = lambda x: x.iteritems() - range_type = xrange - - def is_bytes(x): - return isinstance(x, (buffer, bytearray)) - - _identifier_re = re.compile(r'^[a-zA-Z_][a-zA-Z0-9_]*$') - - # For Windows, we need to force stdout/stdin/stderr to binary if it's - # fetched for that. This obviously is not the most correct way to do - # it as it changes global state. Unfortunately, there does not seem to - # be a clear better way to do it as just reopening the file in binary - # mode does not change anything. - # - # An option would be to do what Python 3 does and to open the file as - # binary only, patch it back to the system, and then use a wrapper - # stream that converts newlines. It's not quite clear what's the - # correct option here. - # - # This code also lives in _winconsole for the fallback to the console - # emulation stream. - # - # There are also Windows environments where the `msvcrt` module is not - # available (which is why we use try-catch instead of the WIN variable - # here), such as the Google App Engine development server on Windows. In - # those cases there is just nothing we can do. - def set_binary_mode(f): - return f - +def _is_binary_reader(stream: t.IO, default: bool = False) -> bool: try: - import msvcrt - except ImportError: - pass - else: - def set_binary_mode(f): - try: - fileno = f.fileno() - except Exception: - pass - else: - msvcrt.setmode(fileno, os.O_BINARY) - return f + return isinstance(stream.read(0), bytes) + except Exception: + return default + # This happens in some cases where the stream was already + # closed. In this case, we assume the default. + +def _is_binary_writer(stream: t.IO, default: bool = False) -> bool: try: - import fcntl - except ImportError: - pass - else: - def set_binary_mode(f): - try: - fileno = f.fileno() - except Exception: - pass - else: - flags = fcntl.fcntl(fileno, fcntl.F_GETFL) - fcntl.fcntl(fileno, fcntl.F_SETFL, flags & ~os.O_NONBLOCK) - return f - - def isidentifier(x): - return _identifier_re.search(x) is not None - - def get_binary_stdin(): - return set_binary_mode(sys.stdin) - - def get_binary_stdout(): - _wrap_std_stream('stdout') - return set_binary_mode(sys.stdout) - - def get_binary_stderr(): - _wrap_std_stream('stderr') - return set_binary_mode(sys.stderr) - - def get_text_stdin(encoding=None, errors=None): - rv = _get_windows_console_stream(sys.stdin, encoding, errors) - if rv is not None: - return rv - return _make_text_stream(sys.stdin, encoding, errors, - force_readable=True) - - def get_text_stdout(encoding=None, errors=None): - _wrap_std_stream('stdout') - rv = _get_windows_console_stream(sys.stdout, encoding, errors) - if rv is not None: - return rv - return _make_text_stream(sys.stdout, encoding, errors, - force_writable=True) - - def get_text_stderr(encoding=None, errors=None): - _wrap_std_stream('stderr') - rv = _get_windows_console_stream(sys.stderr, encoding, errors) - if rv is not None: - return rv - return _make_text_stream(sys.stderr, encoding, errors, - force_writable=True) - - def filename_to_ui(value): - if isinstance(value, bytes): - value = value.decode(get_filesystem_encoding(), 'replace') - return value -else: - import io - text_type = str - raw_input = input - string_types = (str,) - int_types = (int,) - range_type = range - isidentifier = lambda x: x.isidentifier() - iteritems = lambda x: iter(x.items()) - - def is_bytes(x): - return isinstance(x, (bytes, memoryview, bytearray)) - - def _is_binary_reader(stream, default=False): + stream.write(b"") + except Exception: try: - return isinstance(stream.read(0), bytes) + stream.write("") + return False except Exception: - return default - # This happens in some cases where the stream was already - # closed. In this case, we assume the default. - - def _is_binary_writer(stream, default=False): - try: - stream.write(b'') - except Exception: - try: - stream.write('') - return False - except Exception: - pass - return default - return True - - def _find_binary_reader(stream): - # We need to figure out if the given stream is already binary. - # This can happen because the official docs recommend detaching - # the streams to get binary streams. Some code might do this, so - # we need to deal with this case explicitly. - if _is_binary_reader(stream, False): - return stream - - buf = getattr(stream, 'buffer', None) - - # Same situation here; this time we assume that the buffer is - # actually binary in case it's closed. - if buf is not None and _is_binary_reader(buf, True): - return buf - - def _find_binary_writer(stream): - # We need to figure out if the given stream is already binary. - # This can happen because the official docs recommend detatching - # the streams to get binary streams. Some code might do this, so - # we need to deal with this case explicitly. - if _is_binary_writer(stream, False): - return stream - - buf = getattr(stream, 'buffer', None) - - # Same situation here; this time we assume that the buffer is - # actually binary in case it's closed. - if buf is not None and _is_binary_writer(buf, True): - return buf - - def _stream_is_misconfigured(stream): - """A stream is misconfigured if its encoding is ASCII.""" - # If the stream does not have an encoding set, we assume it's set - # to ASCII. This appears to happen in certain unittest - # environments. It's not quite clear what the correct behavior is - # but this at least will force Click to recover somehow. - return is_ascii_encoding(getattr(stream, 'encoding', None) or 'ascii') - - def _is_compatible_text_stream(stream, encoding, errors): - stream_encoding = getattr(stream, 'encoding', None) - stream_errors = getattr(stream, 'errors', None) - - # Perfect match. - if stream_encoding == encoding and stream_errors == errors: - return True - - # Otherwise, it's only a compatible stream if we did not ask for - # an encoding. - if encoding is None: - return stream_encoding is not None - - return False - - def _force_correct_text_reader(text_reader, encoding, errors, - force_readable=False): - if _is_binary_reader(text_reader, False): - binary_reader = text_reader - else: - # If there is no target encoding set, we need to verify that the - # reader is not actually misconfigured. - if encoding is None and not _stream_is_misconfigured(text_reader): - return text_reader - - if _is_compatible_text_stream(text_reader, encoding, errors): - return text_reader - - # If the reader has no encoding, we try to find the underlying - # binary reader for it. If that fails because the environment is - # misconfigured, we silently go with the same reader because this - # is too common to happen. In that case, mojibake is better than - # exceptions. - binary_reader = _find_binary_reader(text_reader) - if binary_reader is None: - return text_reader - - # At this point, we default the errors to replace instead of strict - # because nobody handles those errors anyways and at this point - # we're so fundamentally fucked that nothing can repair it. - if errors is None: - errors = 'replace' - return _make_text_stream(binary_reader, encoding, errors, - force_readable=force_readable) - - def _force_correct_text_writer(text_writer, encoding, errors, - force_writable=False): - if _is_binary_writer(text_writer, False): - binary_writer = text_writer - else: - # If there is no target encoding set, we need to verify that the - # writer is not actually misconfigured. - if encoding is None and not _stream_is_misconfigured(text_writer): - return text_writer - - if _is_compatible_text_stream(text_writer, encoding, errors): - return text_writer - - # If the writer has no encoding, we try to find the underlying - # binary writer for it. If that fails because the environment is - # misconfigured, we silently go with the same writer because this - # is too common to happen. In that case, mojibake is better than - # exceptions. - binary_writer = _find_binary_writer(text_writer) - if binary_writer is None: - return text_writer - - # At this point, we default the errors to replace instead of strict - # because nobody handles those errors anyways and at this point - # we're so fundamentally fucked that nothing can repair it. - if errors is None: - errors = 'replace' - return _make_text_stream(binary_writer, encoding, errors, - force_writable=force_writable) - - def get_binary_stdin(): - reader = _find_binary_reader(sys.stdin) - if reader is None: - raise RuntimeError('Was not able to determine binary ' - 'stream for sys.stdin.') - return reader - - def get_binary_stdout(): - writer = _find_binary_writer(sys.stdout) - if writer is None: - raise RuntimeError('Was not able to determine binary ' - 'stream for sys.stdout.') - return writer - - def get_binary_stderr(): - writer = _find_binary_writer(sys.stderr) - if writer is None: - raise RuntimeError('Was not able to determine binary ' - 'stream for sys.stderr.') - return writer - - def get_text_stdin(encoding=None, errors=None): - rv = _get_windows_console_stream(sys.stdin, encoding, errors) - if rv is not None: - return rv - return _force_correct_text_reader(sys.stdin, encoding, errors, - force_readable=True) - - def get_text_stdout(encoding=None, errors=None): - rv = _get_windows_console_stream(sys.stdout, encoding, errors) - if rv is not None: - return rv - return _force_correct_text_writer(sys.stdout, encoding, errors, - force_writable=True) - - def get_text_stderr(encoding=None, errors=None): - rv = _get_windows_console_stream(sys.stderr, encoding, errors) - if rv is not None: - return rv - return _force_correct_text_writer(sys.stderr, encoding, errors, - force_writable=True) - - def filename_to_ui(value): - if isinstance(value, bytes): - value = value.decode(get_filesystem_encoding(), 'replace') - else: - value = value.encode('utf-8', 'surrogateescape') \ - .decode('utf-8', 'replace') - return value + pass + return default + return True -def get_streerror(e, default=None): - if hasattr(e, 'strerror'): - msg = e.strerror +def _find_binary_reader(stream: t.IO) -> t.Optional[t.BinaryIO]: + # We need to figure out if the given stream is already binary. + # This can happen because the official docs recommend detaching + # the streams to get binary streams. Some code might do this, so + # we need to deal with this case explicitly. + if _is_binary_reader(stream, False): + return t.cast(t.BinaryIO, stream) + + buf = getattr(stream, "buffer", None) + + # Same situation here; this time we assume that the buffer is + # actually binary in case it's closed. + if buf is not None and _is_binary_reader(buf, True): + return t.cast(t.BinaryIO, buf) + + return None + + +def _find_binary_writer(stream: t.IO) -> t.Optional[t.BinaryIO]: + # We need to figure out if the given stream is already binary. + # This can happen because the official docs recommend detaching + # the streams to get binary streams. Some code might do this, so + # we need to deal with this case explicitly. + if _is_binary_writer(stream, False): + return t.cast(t.BinaryIO, stream) + + buf = getattr(stream, "buffer", None) + + # Same situation here; this time we assume that the buffer is + # actually binary in case it's closed. + if buf is not None and _is_binary_writer(buf, True): + return t.cast(t.BinaryIO, buf) + + return None + + +def _stream_is_misconfigured(stream: t.TextIO) -> bool: + """A stream is misconfigured if its encoding is ASCII.""" + # If the stream does not have an encoding set, we assume it's set + # to ASCII. This appears to happen in certain unittest + # environments. It's not quite clear what the correct behavior is + # but this at least will force Click to recover somehow. + return is_ascii_encoding(getattr(stream, "encoding", None) or "ascii") + + +def _is_compat_stream_attr(stream: t.TextIO, attr: str, value: t.Optional[str]) -> bool: + """A stream attribute is compatible if it is equal to the + desired value or the desired value is unset and the attribute + has a value. + """ + stream_value = getattr(stream, attr, None) + return stream_value == value or (value is None and stream_value is not None) + + +def _is_compatible_text_stream( + stream: t.TextIO, encoding: t.Optional[str], errors: t.Optional[str] +) -> bool: + """Check if a stream's encoding and errors attributes are + compatible with the desired values. + """ + return _is_compat_stream_attr( + stream, "encoding", encoding + ) and _is_compat_stream_attr(stream, "errors", errors) + + +def _force_correct_text_stream( + text_stream: t.IO, + encoding: t.Optional[str], + errors: t.Optional[str], + is_binary: t.Callable[[t.IO, bool], bool], + find_binary: t.Callable[[t.IO], t.Optional[t.BinaryIO]], + force_readable: bool = False, + force_writable: bool = False, +) -> t.TextIO: + if is_binary(text_stream, False): + binary_reader = t.cast(t.BinaryIO, text_stream) else: - if default is not None: - msg = default - else: - msg = str(e) - if isinstance(msg, bytes): - msg = msg.decode('utf-8', 'replace') - return msg + text_stream = t.cast(t.TextIO, text_stream) + # If the stream looks compatible, and won't default to a + # misconfigured ascii encoding, return it as-is. + if _is_compatible_text_stream(text_stream, encoding, errors) and not ( + encoding is None and _stream_is_misconfigured(text_stream) + ): + return text_stream + + # Otherwise, get the underlying binary reader. + possible_binary_reader = find_binary(text_stream) + + # If that's not possible, silently use the original reader + # and get mojibake instead of exceptions. + if possible_binary_reader is None: + return text_stream + + binary_reader = possible_binary_reader + + # Default errors to replace instead of strict in order to get + # something that works. + if errors is None: + errors = "replace" + + # Wrap the binary stream in a text stream with the correct + # encoding parameters. + return _make_text_stream( + binary_reader, + encoding, + errors, + force_readable=force_readable, + force_writable=force_writable, + ) -def open_stream(filename, mode='r', encoding=None, errors='strict', - atomic=False): +def _force_correct_text_reader( + text_reader: t.IO, + encoding: t.Optional[str], + errors: t.Optional[str], + force_readable: bool = False, +) -> t.TextIO: + return _force_correct_text_stream( + text_reader, + encoding, + errors, + _is_binary_reader, + _find_binary_reader, + force_readable=force_readable, + ) + + +def _force_correct_text_writer( + text_writer: t.IO, + encoding: t.Optional[str], + errors: t.Optional[str], + force_writable: bool = False, +) -> t.TextIO: + return _force_correct_text_stream( + text_writer, + encoding, + errors, + _is_binary_writer, + _find_binary_writer, + force_writable=force_writable, + ) + + +def get_binary_stdin() -> t.BinaryIO: + reader = _find_binary_reader(sys.stdin) + if reader is None: + raise RuntimeError("Was not able to determine binary stream for sys.stdin.") + return reader + + +def get_binary_stdout() -> t.BinaryIO: + writer = _find_binary_writer(sys.stdout) + if writer is None: + raise RuntimeError("Was not able to determine binary stream for sys.stdout.") + return writer + + +def get_binary_stderr() -> t.BinaryIO: + writer = _find_binary_writer(sys.stderr) + if writer is None: + raise RuntimeError("Was not able to determine binary stream for sys.stderr.") + return writer + + +def get_text_stdin( + encoding: t.Optional[str] = None, errors: t.Optional[str] = None +) -> t.TextIO: + rv = _get_windows_console_stream(sys.stdin, encoding, errors) + if rv is not None: + return rv + return _force_correct_text_reader(sys.stdin, encoding, errors, force_readable=True) + + +def get_text_stdout( + encoding: t.Optional[str] = None, errors: t.Optional[str] = None +) -> t.TextIO: + rv = _get_windows_console_stream(sys.stdout, encoding, errors) + if rv is not None: + return rv + return _force_correct_text_writer(sys.stdout, encoding, errors, force_writable=True) + + +def get_text_stderr( + encoding: t.Optional[str] = None, errors: t.Optional[str] = None +) -> t.TextIO: + rv = _get_windows_console_stream(sys.stderr, encoding, errors) + if rv is not None: + return rv + return _force_correct_text_writer(sys.stderr, encoding, errors, force_writable=True) + + +def _wrap_io_open( + file: t.Union[str, os.PathLike, int], + mode: str, + encoding: t.Optional[str], + errors: t.Optional[str], +) -> t.IO: + """Handles not passing ``encoding`` and ``errors`` in binary mode.""" + if "b" in mode: + return open(file, mode) + + return open(file, mode, encoding=encoding, errors=errors) + + +def open_stream( + filename: str, + mode: str = "r", + encoding: t.Optional[str] = None, + errors: t.Optional[str] = "strict", + atomic: bool = False, +) -> t.Tuple[t.IO, bool]: + binary = "b" in mode + # Standard streams first. These are simple because they don't need # special handling for the atomic flag. It's entirely ignored. - if filename == '-': - if any(m in mode for m in ['w', 'a', 'x']): - if 'b' in mode: + if filename == "-": + if any(m in mode for m in ["w", "a", "x"]): + if binary: return get_binary_stdout(), False return get_text_stdout(encoding=encoding, errors=errors), False - if 'b' in mode: + if binary: return get_binary_stdin(), False return get_text_stdin(encoding=encoding, errors=errors), False # Non-atomic writes directly go out through the regular open functions. if not atomic: - if encoding is None: - return open(filename, mode), True - return io.open(filename, mode, encoding=encoding, errors=errors), True + return _wrap_io_open(filename, mode, encoding, errors), True # Some usability stuff for atomic writes - if 'a' in mode: + if "a" in mode: raise ValueError( - 'Appending to an existing file is not supported, because that ' - 'would involve an expensive `copy`-operation to a temporary ' - 'file. Open the file in normal `w`-mode and copy explicitly ' - 'if that\'s what you\'re after.' + "Appending to an existing file is not supported, because that" + " would involve an expensive `copy`-operation to a temporary" + " file. Open the file in normal `w`-mode and copy explicitly" + " if that's what you're after." ) - if 'x' in mode: - raise ValueError('Use the `overwrite`-parameter instead.') - if 'w' not in mode: - raise ValueError('Atomic writes only make sense with `w`-mode.') + if "x" in mode: + raise ValueError("Use the `overwrite`-parameter instead.") + if "w" not in mode: + raise ValueError("Atomic writes only make sense with `w`-mode.") # Atomic writes are more complicated. They work by opening a file # as a proxy in the same folder and then using the fdopen # functionality to wrap it in a Python file. Then we wrap it in an # atomic file that moves the file over on close. - import tempfile - fd, tmp_filename = tempfile.mkstemp(dir=os.path.dirname(filename), - prefix='.__atomic-write') + import errno + import random - if encoding is not None: - f = io.open(fd, mode, encoding=encoding, errors=errors) - else: - f = os.fdopen(fd, mode) + try: + perm: t.Optional[int] = os.stat(filename).st_mode + except OSError: + perm = None - return _AtomicFile(f, tmp_filename, os.path.realpath(filename)), True + flags = os.O_RDWR | os.O_CREAT | os.O_EXCL + + if binary: + flags |= getattr(os, "O_BINARY", 0) + + while True: + tmp_filename = os.path.join( + os.path.dirname(filename), + f".__atomic-write{random.randrange(1 << 32):08x}", + ) + try: + fd = os.open(tmp_filename, flags, 0o666 if perm is None else perm) + break + except OSError as e: + if e.errno == errno.EEXIST or ( + os.name == "nt" + and e.errno == errno.EACCES + and os.path.isdir(e.filename) + and os.access(e.filename, os.W_OK) + ): + continue + raise + + if perm is not None: + os.chmod(tmp_filename, perm) # in case perm includes bits in umask + + f = _wrap_io_open(fd, mode, encoding, errors) + af = _AtomicFile(f, tmp_filename, os.path.realpath(filename)) + return t.cast(t.IO, af), True -# Used in a destructor call, needs extra protection from interpreter cleanup. -if hasattr(os, 'replace'): - _replace = os.replace - _can_replace = True -else: - _replace = os.rename - _can_replace = not WIN - - -class _AtomicFile(object): - - def __init__(self, f, tmp_filename, real_filename): +class _AtomicFile: + def __init__(self, f: t.IO, tmp_filename: str, real_filename: str) -> None: self._f = f self._tmp_filename = tmp_filename self._real_filename = real_filename self.closed = False @property - def name(self): + def name(self) -> str: return self._real_filename - def close(self, delete=False): + def close(self, delete: bool = False) -> None: if self.closed: return self._f.close() - if not _can_replace: - try: - os.remove(self._real_filename) - except OSError: - pass - _replace(self._tmp_filename, self._real_filename) + os.replace(self._tmp_filename, self._real_filename) self.closed = True - def __getattr__(self, name): + def __getattr__(self, name: str) -> t.Any: return getattr(self._f, name) - def __enter__(self): + def __enter__(self) -> "_AtomicFile": return self - def __exit__(self, exc_type, exc_value, tb): + def __exit__(self, exc_type, exc_value, tb): # type: ignore self.close(delete=exc_type is not None) - def __repr__(self): + def __repr__(self) -> str: return repr(self._f) -auto_wrap_for_ansi = None -colorama = None -get_winterm_size = None +def strip_ansi(value: str) -> str: + return _ansi_re.sub("", value) -def strip_ansi(value): - return _ansi_re.sub('', value) +def _is_jupyter_kernel_output(stream: t.IO) -> bool: + while isinstance(stream, (_FixupStream, _NonClosingTextIOWrapper)): + stream = stream._stream + + return stream.__class__.__module__.startswith("ipykernel.") -def should_strip_ansi(stream=None, color=None): +def should_strip_ansi( + stream: t.Optional[t.IO] = None, color: t.Optional[bool] = None +) -> bool: if color is None: if stream is None: stream = sys.stdin - return not isatty(stream) + return not isatty(stream) and not _is_jupyter_kernel_output(stream) return not color -# If we're on Windows, we provide transparent integration through -# colorama. This will make ANSI colors through the echo function -# work automatically. -if WIN: - # Windows has a smaller terminal - DEFAULT_COLUMNS = 79 +# On Windows, wrap the output streams with colorama to support ANSI +# color codes. +# NOTE: double check is needed so mypy does not analyze this on Linux +if sys.platform.startswith("win") and WIN: + from ._winconsole import _get_windows_console_stream - from ._winconsole import _get_windows_console_stream, _wrap_std_stream - - def _get_argv_encoding(): + def _get_argv_encoding() -> str: import locale + return locale.getpreferredencoding() - if PY2: - def raw_input(prompt=''): - sys.stderr.flush() - if prompt: - stdout = _default_text_stdout() - stdout.write(prompt) - stdin = _default_text_stdin() - return stdin.readline().rstrip('\r\n') + _ansi_stream_wrappers: t.MutableMapping[t.TextIO, t.TextIO] = WeakKeyDictionary() + + def auto_wrap_for_ansi( + stream: t.TextIO, color: t.Optional[bool] = None + ) -> t.TextIO: + """Support ANSI color and style codes on Windows by wrapping a + stream with colorama. + """ + try: + cached = _ansi_stream_wrappers.get(stream) + except Exception: + cached = None + + if cached is not None: + return cached - try: import colorama - except ImportError: - pass - else: - _ansi_stream_wrappers = WeakKeyDictionary() - def auto_wrap_for_ansi(stream, color=None): - """This function wraps a stream so that calls through colorama - are issued to the win32 console API to recolor on demand. It - also ensures to reset the colors if a write call is interrupted - to not destroy the console afterwards. - """ + strip = should_strip_ansi(stream, color) + ansi_wrapper = colorama.AnsiToWin32(stream, strip=strip) + rv = t.cast(t.TextIO, ansi_wrapper.stream) + _write = rv.write + + def _safe_write(s): try: - cached = _ansi_stream_wrappers.get(stream) - except Exception: - cached = None - if cached is not None: - return cached - strip = should_strip_ansi(stream, color) - ansi_wrapper = colorama.AnsiToWin32(stream, strip=strip) - rv = ansi_wrapper.stream - _write = rv.write + return _write(s) + except BaseException: + ansi_wrapper.reset_all() + raise - def _safe_write(s): - try: - return _write(s) - except: - ansi_wrapper.reset_all() - raise + rv.write = _safe_write + + try: + _ansi_stream_wrappers[stream] = rv + except Exception: + pass + + return rv - rv.write = _safe_write - try: - _ansi_stream_wrappers[stream] = rv - except Exception: - pass - return rv - def get_winterm_size(): - win = colorama.win32.GetConsoleScreenBufferInfo( - colorama.win32.STDOUT).srWindow - return win.Right - win.Left, win.Bottom - win.Top else: - def _get_argv_encoding(): - return getattr(sys.stdin, 'encoding', None) or get_filesystem_encoding() - _get_windows_console_stream = lambda *x: None - _wrap_std_stream = lambda *x: None + def _get_argv_encoding() -> str: + return getattr(sys.stdin, "encoding", None) or get_filesystem_encoding() + + def _get_windows_console_stream( + f: t.TextIO, encoding: t.Optional[str], errors: t.Optional[str] + ) -> t.Optional[t.TextIO]: + return None -def term_len(x): +def term_len(x: str) -> int: return len(strip_ansi(x)) -def isatty(stream): +def isatty(stream: t.IO) -> bool: try: return stream.isatty() except Exception: return False -def _make_cached_stream_func(src_func, wrapper_func): - cache = WeakKeyDictionary() - def func(): +def _make_cached_stream_func( + src_func: t.Callable[[], t.TextIO], wrapper_func: t.Callable[[], t.TextIO] +) -> t.Callable[[], t.TextIO]: + cache: t.MutableMapping[t.TextIO, t.TextIO] = WeakKeyDictionary() + + def func() -> t.TextIO: stream = src_func() try: rv = cache.get(stream) @@ -674,30 +599,29 @@ def _make_cached_stream_func(src_func, wrapper_func): return rv rv = wrapper_func() try: - stream = src_func() # In case wrapper_func() modified the stream cache[stream] = rv except Exception: pass return rv + return func -_default_text_stdin = _make_cached_stream_func( - lambda: sys.stdin, get_text_stdin) -_default_text_stdout = _make_cached_stream_func( - lambda: sys.stdout, get_text_stdout) -_default_text_stderr = _make_cached_stream_func( - lambda: sys.stderr, get_text_stderr) +_default_text_stdin = _make_cached_stream_func(lambda: sys.stdin, get_text_stdin) +_default_text_stdout = _make_cached_stream_func(lambda: sys.stdout, get_text_stdout) +_default_text_stderr = _make_cached_stream_func(lambda: sys.stderr, get_text_stderr) -binary_streams = { - 'stdin': get_binary_stdin, - 'stdout': get_binary_stdout, - 'stderr': get_binary_stderr, +binary_streams: t.Mapping[str, t.Callable[[], t.BinaryIO]] = { + "stdin": get_binary_stdin, + "stdout": get_binary_stdout, + "stderr": get_binary_stderr, } -text_streams = { - 'stdin': get_text_stdin, - 'stdout': get_text_stdout, - 'stderr': get_text_stderr, +text_streams: t.Mapping[ + str, t.Callable[[t.Optional[str], t.Optional[str]], t.TextIO] +] = { + "stdin": get_text_stdin, + "stdout": get_text_stdout, + "stderr": get_text_stderr, } diff --git a/libs/click/_termui_impl.py b/libs/click/_termui_impl.py index 00a8e5ef1..39c1d08f4 100644 --- a/libs/click/_termui_impl.py +++ b/libs/click/_termui_impl.py @@ -1,62 +1,56 @@ -# -*- coding: utf-8 -*- """ -click._termui_impl -~~~~~~~~~~~~~~~~~~ - This module contains implementations for the termui module. To keep the import time of Click down, some infrequently used functionality is placed in this module and only imported as needed. - -:copyright: © 2014 by the Pallets team. -:license: BSD, see LICENSE.rst for more details. """ - +import contextlib +import math import os import sys import time -import math -import contextlib -from ._compat import _default_text_stdout, range_type, PY2, isatty, \ - open_stream, strip_ansi, term_len, get_best_encoding, WIN, int_types, \ - CYGWIN -from .utils import echo +import typing as t +from gettext import gettext as _ + +from ._compat import _default_text_stdout +from ._compat import CYGWIN +from ._compat import get_best_encoding +from ._compat import isatty +from ._compat import open_stream +from ._compat import strip_ansi +from ._compat import term_len +from ._compat import WIN from .exceptions import ClickException +from .utils import echo +V = t.TypeVar("V") -if os.name == 'nt': - BEFORE_BAR = '\r' - AFTER_BAR = '\n' +if os.name == "nt": + BEFORE_BAR = "\r" + AFTER_BAR = "\n" else: - BEFORE_BAR = '\r\033[?25l' - AFTER_BAR = '\033[?25h\n' + BEFORE_BAR = "\r\033[?25l" + AFTER_BAR = "\033[?25h\n" -def _length_hint(obj): - """Returns the length hint of an object.""" - try: - return len(obj) - except (AttributeError, TypeError): - try: - get_hint = type(obj).__length_hint__ - except AttributeError: - return None - try: - hint = get_hint(obj) - except TypeError: - return None - if hint is NotImplemented or \ - not isinstance(hint, int_types) or \ - hint < 0: - return None - return hint - - -class ProgressBar(object): - - def __init__(self, iterable, length=None, fill_char='#', empty_char=' ', - bar_template='%(bar)s', info_sep=' ', show_eta=True, - show_percent=None, show_pos=False, item_show_func=None, - label=None, file=None, color=None, width=30): +class ProgressBar(t.Generic[V]): + def __init__( + self, + iterable: t.Optional[t.Iterable[V]], + length: t.Optional[int] = None, + fill_char: str = "#", + empty_char: str = " ", + bar_template: str = "%(bar)s", + info_sep: str = " ", + show_eta: bool = True, + show_percent: t.Optional[bool] = None, + show_pos: bool = False, + item_show_func: t.Optional[t.Callable[[t.Optional[V]], t.Optional[str]]] = None, + label: t.Optional[str] = None, + file: t.Optional[t.TextIO] = None, + color: t.Optional[bool] = None, + update_min_steps: int = 1, + width: int = 30, + ) -> None: self.fill_char = fill_char self.empty_char = empty_char self.bar_template = bar_template @@ -65,77 +59,87 @@ class ProgressBar(object): self.show_percent = show_percent self.show_pos = show_pos self.item_show_func = item_show_func - self.label = label or '' + self.label = label or "" if file is None: file = _default_text_stdout() self.file = file self.color = color + self.update_min_steps = update_min_steps + self._completed_intervals = 0 self.width = width self.autowidth = width == 0 if length is None: - length = _length_hint(iterable) + from operator import length_hint + + length = length_hint(iterable, -1) + + if length == -1: + length = None if iterable is None: if length is None: - raise TypeError('iterable or length is required') - iterable = range_type(length) + raise TypeError("iterable or length is required") + iterable = t.cast(t.Iterable[V], range(length)) self.iter = iter(iterable) self.length = length - self.length_known = length is not None self.pos = 0 - self.avg = [] + self.avg: t.List[float] = [] self.start = self.last_eta = time.time() self.eta_known = False self.finished = False - self.max_width = None + self.max_width: t.Optional[int] = None self.entered = False - self.current_item = None + self.current_item: t.Optional[V] = None self.is_hidden = not isatty(self.file) - self._last_line = None - self.short_limit = 0.5 + self._last_line: t.Optional[str] = None - def __enter__(self): + def __enter__(self) -> "ProgressBar": self.entered = True self.render_progress() return self - def __exit__(self, exc_type, exc_value, tb): + def __exit__(self, exc_type, exc_value, tb): # type: ignore self.render_finish() - def __iter__(self): + def __iter__(self) -> t.Iterator[V]: if not self.entered: - raise RuntimeError('You need to use progress bars in a with block.') + raise RuntimeError("You need to use progress bars in a with block.") self.render_progress() return self.generator() - def is_fast(self): - return time.time() - self.start <= self.short_limit + def __next__(self) -> V: + # Iteration is defined in terms of a generator function, + # returned by iter(self); use that to define next(). This works + # because `self.iter` is an iterable consumed by that generator, + # so it is re-entry safe. Calling `next(self.generator())` + # twice works and does "what you want". + return next(iter(self)) - def render_finish(self): - if self.is_hidden or self.is_fast(): + def render_finish(self) -> None: + if self.is_hidden: return self.file.write(AFTER_BAR) self.file.flush() @property - def pct(self): + def pct(self) -> float: if self.finished: return 1.0 - return min(self.pos / (float(self.length) or 1), 1.0) + return min(self.pos / (float(self.length or 1) or 1), 1.0) @property - def time_per_iteration(self): + def time_per_iteration(self) -> float: if not self.avg: return 0.0 return sum(self.avg) / float(len(self.avg)) @property - def eta(self): - if self.length_known and not self.finished: + def eta(self) -> float: + if self.length is not None and not self.finished: return self.time_per_iteration * (self.length - self.pos) return 0.0 - def format_eta(self): + def format_eta(self) -> str: if self.eta_known: t = int(self.eta) seconds = t % 60 @@ -145,41 +149,44 @@ class ProgressBar(object): hours = t % 24 t //= 24 if t > 0: - days = t - return '%dd %02d:%02d:%02d' % (days, hours, minutes, seconds) + return f"{t}d {hours:02}:{minutes:02}:{seconds:02}" else: - return '%02d:%02d:%02d' % (hours, minutes, seconds) - return '' + return f"{hours:02}:{minutes:02}:{seconds:02}" + return "" - def format_pos(self): + def format_pos(self) -> str: pos = str(self.pos) - if self.length_known: - pos += '/%s' % self.length + if self.length is not None: + pos += f"/{self.length}" return pos - def format_pct(self): - return ('% 4d%%' % int(self.pct * 100))[1:] + def format_pct(self) -> str: + return f"{int(self.pct * 100): 4}%"[1:] - def format_bar(self): - if self.length_known: + def format_bar(self) -> str: + if self.length is not None: bar_length = int(self.pct * self.width) bar = self.fill_char * bar_length bar += self.empty_char * (self.width - bar_length) elif self.finished: bar = self.fill_char * self.width else: - bar = list(self.empty_char * (self.width or 1)) + chars = list(self.empty_char * (self.width or 1)) if self.time_per_iteration != 0: - bar[int((math.cos(self.pos * self.time_per_iteration) - / 2.0 + 0.5) * self.width)] = self.fill_char - bar = ''.join(bar) + chars[ + int( + (math.cos(self.pos * self.time_per_iteration) / 2.0 + 0.5) + * self.width + ) + ] = self.fill_char + bar = "".join(chars) return bar - def format_progress_line(self): + def format_progress_line(self) -> str: show_percent = self.show_percent info_bits = [] - if self.length_known and show_percent is None: + if self.length is not None and show_percent is None: show_percent = not self.show_pos if self.show_pos: @@ -193,16 +200,25 @@ class ProgressBar(object): if item_info is not None: info_bits.append(item_info) - return (self.bar_template % { - 'label': self.label, - 'bar': self.format_bar(), - 'info': self.info_sep.join(info_bits) - }).rstrip() + return ( + self.bar_template + % { + "label": self.label, + "bar": self.format_bar(), + "info": self.info_sep.join(info_bits), + } + ).rstrip() - def render_progress(self): - from .termui import get_terminal_size + def render_progress(self) -> None: + import shutil if self.is_hidden: + # Only output the label as it changes if the output is not a + # TTY. Use file=stderr if you expect to be piping stdout. + if self._last_line != self.label: + self._last_line = self.label + echo(self.label, file=self.file, color=self.color) + return buf = [] @@ -211,10 +227,10 @@ class ProgressBar(object): old_width = self.width self.width = 0 clutter_length = term_len(self.format_progress_line()) - new_width = max(0, get_terminal_size()[0] - clutter_length) + new_width = max(0, shutil.get_terminal_size().columns - clutter_length) if new_width < old_width: buf.append(BEFORE_BAR) - buf.append(' ' * self.max_width) + buf.append(" " * self.max_width) # type: ignore self.max_width = new_width self.width = new_width @@ -229,18 +245,18 @@ class ProgressBar(object): self.max_width = line_len buf.append(line) - buf.append(' ' * (clear_width - line_len)) - line = ''.join(buf) + buf.append(" " * (clear_width - line_len)) + line = "".join(buf) # Render the line only if it changed. - if line != self._last_line and not self.is_fast(): + if line != self._last_line: self._last_line = line echo(line, file=self.file, color=self.color, nl=False) self.file.flush() - def make_step(self, n_steps): + def make_step(self, n_steps: int) -> None: self.pos += n_steps - if self.length_known and self.pos >= self.length: + if self.length is not None and self.pos >= self.length: self.finished = True if (time.time() - self.last_eta) < 1.0: @@ -258,97 +274,134 @@ class ProgressBar(object): self.avg = self.avg[-6:] + [step] - self.eta_known = self.length_known + self.eta_known = self.length is not None - def update(self, n_steps): - self.make_step(n_steps) - self.render_progress() + def update(self, n_steps: int, current_item: t.Optional[V] = None) -> None: + """Update the progress bar by advancing a specified number of + steps, and optionally set the ``current_item`` for this new + position. - def finish(self): - self.eta_known = 0 + :param n_steps: Number of steps to advance. + :param current_item: Optional item to set as ``current_item`` + for the updated position. + + .. versionchanged:: 8.0 + Added the ``current_item`` optional parameter. + + .. versionchanged:: 8.0 + Only render when the number of steps meets the + ``update_min_steps`` threshold. + """ + if current_item is not None: + self.current_item = current_item + + self._completed_intervals += n_steps + + if self._completed_intervals >= self.update_min_steps: + self.make_step(self._completed_intervals) + self.render_progress() + self._completed_intervals = 0 + + def finish(self) -> None: + self.eta_known = False self.current_item = None self.finished = True - def generator(self): - """ - Returns a generator which yields the items added to the bar during - construction, and updates the progress bar *after* the yielded block - returns. + def generator(self) -> t.Iterator[V]: + """Return a generator which yields the items added to the bar + during construction, and updates the progress bar *after* the + yielded block returns. """ + # WARNING: the iterator interface for `ProgressBar` relies on + # this and only works because this is a simple generator which + # doesn't create or manage additional state. If this function + # changes, the impact should be evaluated both against + # `iter(bar)` and `next(bar)`. `next()` in particular may call + # `self.generator()` repeatedly, and this must remain safe in + # order for that interface to work. if not self.entered: - raise RuntimeError('You need to use progress bars in a with block.') + raise RuntimeError("You need to use progress bars in a with block.") if self.is_hidden: - for rv in self.iter: - yield rv + yield from self.iter else: for rv in self.iter: self.current_item = rv + + # This allows show_item_func to be updated before the + # item is processed. Only trigger at the beginning of + # the update interval. + if self._completed_intervals == 0: + self.render_progress() + yield rv self.update(1) + self.finish() self.render_progress() -def pager(generator, color=None): +def pager(generator: t.Iterable[str], color: t.Optional[bool] = None) -> None: """Decide what method to use for paging through text.""" stdout = _default_text_stdout() if not isatty(sys.stdin) or not isatty(stdout): return _nullpager(stdout, generator, color) - pager_cmd = (os.environ.get('PAGER', None) or '').strip() + pager_cmd = (os.environ.get("PAGER", None) or "").strip() if pager_cmd: if WIN: return _tempfilepager(generator, pager_cmd, color) return _pipepager(generator, pager_cmd, color) - if os.environ.get('TERM') in ('dumb', 'emacs'): + if os.environ.get("TERM") in ("dumb", "emacs"): return _nullpager(stdout, generator, color) - if WIN or sys.platform.startswith('os2'): - return _tempfilepager(generator, 'more <', color) - if hasattr(os, 'system') and os.system('(less) 2>/dev/null') == 0: - return _pipepager(generator, 'less', color) + if WIN or sys.platform.startswith("os2"): + return _tempfilepager(generator, "more <", color) + if hasattr(os, "system") and os.system("(less) 2>/dev/null") == 0: + return _pipepager(generator, "less", color) import tempfile + fd, filename = tempfile.mkstemp() os.close(fd) try: - if hasattr(os, 'system') and os.system('more "%s"' % filename) == 0: - return _pipepager(generator, 'more', color) + if hasattr(os, "system") and os.system(f'more "{filename}"') == 0: + return _pipepager(generator, "more", color) return _nullpager(stdout, generator, color) finally: os.unlink(filename) -def _pipepager(generator, cmd, color): +def _pipepager(generator: t.Iterable[str], cmd: str, color: t.Optional[bool]) -> None: """Page through text by feeding it to another program. Invoking a pager through this might support colors. """ import subprocess + env = dict(os.environ) # If we're piping to less we might support colors under the # condition that - cmd_detail = cmd.rsplit('/', 1)[-1].split() - if color is None and cmd_detail[0] == 'less': - less_flags = os.environ.get('LESS', '') + ' '.join(cmd_detail[1:]) + cmd_detail = cmd.rsplit("/", 1)[-1].split() + if color is None and cmd_detail[0] == "less": + less_flags = f"{os.environ.get('LESS', '')}{' '.join(cmd_detail[1:])}" if not less_flags: - env['LESS'] = '-R' + env["LESS"] = "-R" color = True - elif 'r' in less_flags or 'R' in less_flags: + elif "r" in less_flags or "R" in less_flags: color = True - c = subprocess.Popen(cmd, shell=True, stdin=subprocess.PIPE, - env=env) - encoding = get_best_encoding(c.stdin) + c = subprocess.Popen(cmd, shell=True, stdin=subprocess.PIPE, env=env) + stdin = t.cast(t.BinaryIO, c.stdin) + encoding = get_best_encoding(stdin) try: for text in generator: if not color: text = strip_ansi(text) - c.stdin.write(text.encode(encoding, 'replace')) - except (IOError, KeyboardInterrupt): + stdin.write(text.encode(encoding, "replace")) + except (OSError, KeyboardInterrupt): pass else: - c.stdin.close() + stdin.close() # Less doesn't respect ^C, but catches it for its own UI purposes (aborting # search or other commands inside less). @@ -367,24 +420,30 @@ def _pipepager(generator, cmd, color): break -def _tempfilepager(generator, cmd, color): +def _tempfilepager( + generator: t.Iterable[str], cmd: str, color: t.Optional[bool] +) -> None: """Page through text by invoking a program on a temporary file.""" import tempfile - filename = tempfile.mktemp() + + fd, filename = tempfile.mkstemp() # TODO: This never terminates if the passed generator never terminates. text = "".join(generator) if not color: text = strip_ansi(text) encoding = get_best_encoding(sys.stdout) - with open_stream(filename, 'wb')[0] as f: + with open_stream(filename, "wb")[0] as f: f.write(text.encode(encoding)) try: - os.system(cmd + ' "' + filename + '"') + os.system(f'{cmd} "{filename}"') finally: + os.close(fd) os.unlink(filename) -def _nullpager(stream, generator, color): +def _nullpager( + stream: t.TextIO, generator: t.Iterable[str], color: t.Optional[bool] +) -> None: """Simply print unformatted text. This is the ultimate fallback.""" for text in generator: if not color: @@ -392,159 +451,184 @@ def _nullpager(stream, generator, color): stream.write(text) -class Editor(object): - - def __init__(self, editor=None, env=None, require_save=True, - extension='.txt'): +class Editor: + def __init__( + self, + editor: t.Optional[str] = None, + env: t.Optional[t.Mapping[str, str]] = None, + require_save: bool = True, + extension: str = ".txt", + ) -> None: self.editor = editor self.env = env self.require_save = require_save self.extension = extension - def get_editor(self): + def get_editor(self) -> str: if self.editor is not None: return self.editor - for key in 'VISUAL', 'EDITOR': + for key in "VISUAL", "EDITOR": rv = os.environ.get(key) if rv: return rv if WIN: - return 'notepad' - for editor in 'vim', 'nano': - if os.system('which %s >/dev/null 2>&1' % editor) == 0: + return "notepad" + for editor in "sensible-editor", "vim", "nano": + if os.system(f"which {editor} >/dev/null 2>&1") == 0: return editor - return 'vi' + return "vi" - def edit_file(self, filename): + def edit_file(self, filename: str) -> None: import subprocess + editor = self.get_editor() + environ: t.Optional[t.Dict[str, str]] = None + if self.env: environ = os.environ.copy() environ.update(self.env) - else: - environ = None + try: - c = subprocess.Popen('%s "%s"' % (editor, filename), - env=environ, shell=True) + c = subprocess.Popen(f'{editor} "{filename}"', env=environ, shell=True) exit_code = c.wait() if exit_code != 0: - raise ClickException('%s: Editing failed!' % editor) + raise ClickException( + _("{editor}: Editing failed").format(editor=editor) + ) except OSError as e: - raise ClickException('%s: Editing failed: %s' % (editor, e)) + raise ClickException( + _("{editor}: Editing failed: {e}").format(editor=editor, e=e) + ) from e - def edit(self, text): + def edit(self, text: t.Optional[t.AnyStr]) -> t.Optional[t.AnyStr]: import tempfile - text = text or '' - if text and not text.endswith('\n'): - text += '\n' + if not text: + data = b"" + elif isinstance(text, (bytes, bytearray)): + data = text + else: + if text and not text.endswith("\n"): + text += "\n" - fd, name = tempfile.mkstemp(prefix='editor-', suffix=self.extension) - try: if WIN: - encoding = 'utf-8-sig' - text = text.replace('\n', '\r\n') + data = text.replace("\n", "\r\n").encode("utf-8-sig") else: - encoding = 'utf-8' - text = text.encode(encoding) + data = text.encode("utf-8") - f = os.fdopen(fd, 'wb') - f.write(text) - f.close() + fd, name = tempfile.mkstemp(prefix="editor-", suffix=self.extension) + f: t.BinaryIO + + try: + with os.fdopen(fd, "wb") as f: + f.write(data) + + # If the filesystem resolution is 1 second, like Mac OS + # 10.12 Extended, or 2 seconds, like FAT32, and the editor + # closes very fast, require_save can fail. Set the modified + # time to be 2 seconds in the past to work around this. + os.utime(name, (os.path.getatime(name), os.path.getmtime(name) - 2)) + # Depending on the resolution, the exact value might not be + # recorded, so get the new recorded value. timestamp = os.path.getmtime(name) self.edit_file(name) - if self.require_save \ - and os.path.getmtime(name) == timestamp: + if self.require_save and os.path.getmtime(name) == timestamp: return None - f = open(name, 'rb') - try: + with open(name, "rb") as f: rv = f.read() - finally: - f.close() - return rv.decode('utf-8-sig').replace('\r\n', '\n') + + if isinstance(text, (bytes, bytearray)): + return rv + + return rv.decode("utf-8-sig").replace("\r\n", "\n") # type: ignore finally: os.unlink(name) -def open_url(url, wait=False, locate=False): +def open_url(url: str, wait: bool = False, locate: bool = False) -> int: import subprocess - def _unquote_file(url): - try: - import urllib - except ImportError: - import urllib - if url.startswith('file://'): - url = urllib.unquote(url[7:]) + def _unquote_file(url: str) -> str: + from urllib.parse import unquote + + if url.startswith("file://"): + url = unquote(url[7:]) + return url - if sys.platform == 'darwin': - args = ['open'] + if sys.platform == "darwin": + args = ["open"] if wait: - args.append('-W') + args.append("-W") if locate: - args.append('-R') + args.append("-R") args.append(_unquote_file(url)) - null = open('/dev/null', 'w') + null = open("/dev/null", "w") try: return subprocess.Popen(args, stderr=null).wait() finally: null.close() elif WIN: if locate: - url = _unquote_file(url) - args = 'explorer /select,"%s"' % _unquote_file( - url.replace('"', '')) + url = _unquote_file(url.replace('"', "")) + args = f'explorer /select,"{url}"' else: - args = 'start %s "" "%s"' % ( - wait and '/WAIT' or '', url.replace('"', '')) + url = url.replace('"', "") + wait_str = "/WAIT" if wait else "" + args = f'start {wait_str} "" "{url}"' return os.system(args) elif CYGWIN: if locate: - url = _unquote_file(url) - args = 'cygstart "%s"' % (os.path.dirname(url).replace('"', '')) + url = os.path.dirname(_unquote_file(url).replace('"', "")) + args = f'cygstart "{url}"' else: - args = 'cygstart %s "%s"' % ( - wait and '-w' or '', url.replace('"', '')) + url = url.replace('"', "") + wait_str = "-w" if wait else "" + args = f'cygstart {wait_str} "{url}"' return os.system(args) try: if locate: - url = os.path.dirname(_unquote_file(url)) or '.' + url = os.path.dirname(_unquote_file(url)) or "." else: url = _unquote_file(url) - c = subprocess.Popen(['xdg-open', url]) + c = subprocess.Popen(["xdg-open", url]) if wait: return c.wait() return 0 except OSError: - if url.startswith(('http://', 'https://')) and not locate and not wait: + if url.startswith(("http://", "https://")) and not locate and not wait: import webbrowser + webbrowser.open(url) return 0 return 1 -def _translate_ch_to_exc(ch): - if ch == u'\x03': +def _translate_ch_to_exc(ch: str) -> t.Optional[BaseException]: + if ch == "\x03": raise KeyboardInterrupt() - if ch == u'\x04' and not WIN: # Unix-like, Ctrl+D + + if ch == "\x04" and not WIN: # Unix-like, Ctrl+D raise EOFError() - if ch == u'\x1a' and WIN: # Windows, Ctrl+Z + + if ch == "\x1a" and WIN: # Windows, Ctrl+Z raise EOFError() + return None + if WIN: import msvcrt @contextlib.contextmanager - def raw_terminal(): - yield + def raw_terminal() -> t.Iterator[int]: + yield -1 - def getchar(echo): + def getchar(echo: bool) -> str: # The function `getch` will return a bytes object corresponding to # the pressed character. Since Windows 10 build 1803, it will also # return \x00 when called a second time after pressing a regular key. @@ -574,48 +658,61 @@ if WIN: # # Anyway, Click doesn't claim to do this Right(tm), and using `getwch` # is doing the right thing in more situations than with `getch`. + func: t.Callable[[], str] + if echo: - func = msvcrt.getwche + func = msvcrt.getwche # type: ignore else: - func = msvcrt.getwch + func = msvcrt.getwch # type: ignore rv = func() - if rv in (u'\x00', u'\xe0'): + + if rv in ("\x00", "\xe0"): # \x00 and \xe0 are control characters that indicate special key, # see above. rv += func() + _translate_ch_to_exc(rv) return rv + + else: import tty import termios @contextlib.contextmanager - def raw_terminal(): + def raw_terminal() -> t.Iterator[int]: + f: t.Optional[t.TextIO] + fd: int + if not isatty(sys.stdin): - f = open('/dev/tty') + f = open("/dev/tty") fd = f.fileno() else: fd = sys.stdin.fileno() f = None + try: old_settings = termios.tcgetattr(fd) + try: tty.setraw(fd) yield fd finally: termios.tcsetattr(fd, termios.TCSADRAIN, old_settings) sys.stdout.flush() + if f is not None: f.close() except termios.error: pass - def getchar(echo): + def getchar(echo: bool) -> str: with raw_terminal() as fd: - ch = os.read(fd, 32) - ch = ch.decode(get_best_encoding(sys.stdin), 'replace') + ch = os.read(fd, 32).decode(get_best_encoding(sys.stdin), "replace") + if echo and isatty(sys.stdout): sys.stdout.write(ch) + _translate_ch_to_exc(ch) return ch diff --git a/libs/click/_textwrap.py b/libs/click/_textwrap.py index 7e776031e..b47dcbd42 100644 --- a/libs/click/_textwrap.py +++ b/libs/click/_textwrap.py @@ -1,10 +1,16 @@ import textwrap +import typing as t from contextlib import contextmanager class TextWrapper(textwrap.TextWrapper): - - def _handle_long_word(self, reversed_chunks, cur_line, cur_len, width): + def _handle_long_word( + self, + reversed_chunks: t.List[str], + cur_line: t.List[str], + cur_len: int, + width: int, + ) -> None: space_left = max(width - cur_len, 1) if self.break_long_words: @@ -17,22 +23,27 @@ class TextWrapper(textwrap.TextWrapper): cur_line.append(reversed_chunks.pop()) @contextmanager - def extra_indent(self, indent): + def extra_indent(self, indent: str) -> t.Iterator[None]: old_initial_indent = self.initial_indent old_subsequent_indent = self.subsequent_indent self.initial_indent += indent self.subsequent_indent += indent + try: yield finally: self.initial_indent = old_initial_indent self.subsequent_indent = old_subsequent_indent - def indent_only(self, text): + def indent_only(self, text: str) -> str: rv = [] + for idx, line in enumerate(text.splitlines()): indent = self.initial_indent + if idx > 0: indent = self.subsequent_indent - rv.append(indent + line) - return '\n'.join(rv) + + rv.append(f"{indent}{line}") + + return "\n".join(rv) diff --git a/libs/click/_unicodefun.py b/libs/click/_unicodefun.py index 620edff37..9cb30c384 100644 --- a/libs/click/_unicodefun.py +++ b/libs/click/_unicodefun.py @@ -1,125 +1,100 @@ -import os -import sys import codecs - -from ._compat import PY2 +import os +from gettext import gettext as _ -# If someone wants to vendor click, we want to ensure the -# correct package is discovered. Ideally we could use a -# relative import here but unfortunately Python does not -# support that. -click = sys.modules[__name__.rsplit('.', 1)[0]] - - -def _find_unicode_literals_frame(): - import __future__ - if not hasattr(sys, '_getframe'): # not all Python implementations have it - return 0 - frm = sys._getframe(1) - idx = 1 - while frm is not None: - if frm.f_globals.get('__name__', '').startswith('click.'): - frm = frm.f_back - idx += 1 - elif frm.f_code.co_flags & __future__.unicode_literals.compiler_flag: - return idx - else: - break - return 0 - - -def _check_for_unicode_literals(): - if not __debug__: - return - if not PY2 or click.disable_unicode_literals_warning: - return - bad_frame = _find_unicode_literals_frame() - if bad_frame <= 0: - return - from warnings import warn - warn(Warning('Click detected the use of the unicode_literals ' - '__future__ import. This is heavily discouraged ' - 'because it can introduce subtle bugs in your ' - 'code. You should instead use explicit u"" literals ' - 'for your unicode strings. For more information see ' - 'https://click.palletsprojects.com/python3/'), - stacklevel=bad_frame) - - -def _verify_python3_env(): - """Ensures that the environment is good for unicode on Python 3.""" - if PY2: - return +def _verify_python_env() -> None: + """Ensures that the environment is good for Unicode.""" try: - import locale - fs_enc = codecs.lookup(locale.getpreferredencoding()).name + from locale import getpreferredencoding + + fs_enc = codecs.lookup(getpreferredencoding()).name except Exception: - fs_enc = 'ascii' - if fs_enc != 'ascii': + fs_enc = "ascii" + + if fs_enc != "ascii": return - extra = '' - if os.name == 'posix': + extra = [ + _( + "Click will abort further execution because Python was" + " configured to use ASCII as encoding for the environment." + " Consult https://click.palletsprojects.com/unicode-support/" + " for mitigation steps." + ) + ] + + if os.name == "posix": import subprocess + try: - rv = subprocess.Popen(['locale', '-a'], stdout=subprocess.PIPE, - stderr=subprocess.PIPE).communicate()[0] + rv = subprocess.Popen( + ["locale", "-a"], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + encoding="ascii", + errors="replace", + ).communicate()[0] except OSError: - rv = b'' + rv = "" + good_locales = set() has_c_utf8 = False - # Make sure we're operating on text here. - if isinstance(rv, bytes): - rv = rv.decode('ascii', 'replace') - for line in rv.splitlines(): locale = line.strip() - if locale.lower().endswith(('.utf-8', '.utf8')): + + if locale.lower().endswith((".utf-8", ".utf8")): good_locales.add(locale) - if locale.lower() in ('c.utf8', 'c.utf-8'): + + if locale.lower() in ("c.utf8", "c.utf-8"): has_c_utf8 = True - extra += '\n\n' if not good_locales: - extra += ( - 'Additional information: on this system no suitable UTF-8\n' - 'locales were discovered. This most likely requires resolving\n' - 'by reconfiguring the locale system.' + extra.append( + _( + "Additional information: on this system no suitable" + " UTF-8 locales were discovered. This most likely" + " requires resolving by reconfiguring the locale" + " system." + ) ) elif has_c_utf8: - extra += ( - 'This system supports the C.UTF-8 locale which is recommended.\n' - 'You might be able to resolve your issue by exporting the\n' - 'following environment variables:\n\n' - ' export LC_ALL=C.UTF-8\n' - ' export LANG=C.UTF-8' + extra.append( + _( + "This system supports the C.UTF-8 locale which is" + " recommended. You might be able to resolve your" + " issue by exporting the following environment" + " variables:" + ) ) + extra.append(" export LC_ALL=C.UTF-8\n export LANG=C.UTF-8") else: - extra += ( - 'This system lists a couple of UTF-8 supporting locales that\n' - 'you can pick from. The following suitable locales were\n' - 'discovered: %s' - ) % ', '.join(sorted(good_locales)) + extra.append( + _( + "This system lists some UTF-8 supporting locales" + " that you can pick from. The following suitable" + " locales were discovered: {locales}" + ).format(locales=", ".join(sorted(good_locales))) + ) bad_locale = None - for locale in os.environ.get('LC_ALL'), os.environ.get('LANG'): - if locale and locale.lower().endswith(('.utf-8', '.utf8')): - bad_locale = locale - if locale is not None: - break - if bad_locale is not None: - extra += ( - '\n\nClick discovered that you exported a UTF-8 locale\n' - 'but the locale system could not pick up from it because\n' - 'it does not exist. The exported locale is "%s" but it\n' - 'is not supported' - ) % bad_locale - raise RuntimeError( - 'Click will abort further execution because Python 3 was' - ' configured to use ASCII as encoding for the environment.' - ' Consult https://click.palletsprojects.com/en/7.x/python3/ for' - ' mitigation steps.' + extra - ) + for env_locale in os.environ.get("LC_ALL"), os.environ.get("LANG"): + if env_locale and env_locale.lower().endswith((".utf-8", ".utf8")): + bad_locale = env_locale + + if env_locale is not None: + break + + if bad_locale is not None: + extra.append( + _( + "Click discovered that you exported a UTF-8 locale" + " but the locale system could not pick up from it" + " because it does not exist. The exported locale is" + " {locale!r} but it is not supported." + ).format(locale=bad_locale) + ) + + raise RuntimeError("\n\n".join(extra)) diff --git a/libs/click/_winconsole.py b/libs/click/_winconsole.py index bbb080dda..6b20df315 100644 --- a/libs/click/_winconsole.py +++ b/libs/click/_winconsole.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This module is based on the excellent work by Adam BartoÅ¡ who # provided a lot of what went into the implementation here in # the discussion to issue1602 in the Python bug tracker. @@ -6,26 +5,32 @@ # There are some general differences in regards to how this works # compared to the original patches as we do not need to patch # the entire interpreter but just work in our little world of -# echo and prmopt. - +# echo and prompt. import io -import os import sys -import zlib import time -import ctypes -import msvcrt -from ._compat import _NonClosingTextIOWrapper, text_type, PY2 -from ctypes import byref, POINTER, c_int, c_char, c_char_p, \ - c_void_p, py_object, c_ssize_t, c_ulong, windll, WINFUNCTYPE -try: - from ctypes import pythonapi - PyObject_GetBuffer = pythonapi.PyObject_GetBuffer - PyBuffer_Release = pythonapi.PyBuffer_Release -except ImportError: - pythonapi = None -from ctypes.wintypes import LPWSTR, LPCWSTR +import typing as t +from ctypes import byref +from ctypes import c_char +from ctypes import c_char_p +from ctypes import c_int +from ctypes import c_ssize_t +from ctypes import c_ulong +from ctypes import c_void_p +from ctypes import POINTER +from ctypes import py_object +from ctypes import Structure +from ctypes.wintypes import DWORD +from ctypes.wintypes import HANDLE +from ctypes.wintypes import LPCWSTR +from ctypes.wintypes import LPWSTR +from ._compat import _NonClosingTextIOWrapper + +assert sys.platform == "win32" +import msvcrt # noqa: E402 +from ctypes import windll # noqa: E402 +from ctypes import WINFUNCTYPE # noqa: E402 c_ssize_p = POINTER(c_ssize_t) @@ -33,19 +38,18 @@ kernel32 = windll.kernel32 GetStdHandle = kernel32.GetStdHandle ReadConsoleW = kernel32.ReadConsoleW WriteConsoleW = kernel32.WriteConsoleW +GetConsoleMode = kernel32.GetConsoleMode GetLastError = kernel32.GetLastError -GetCommandLineW = WINFUNCTYPE(LPWSTR)( - ('GetCommandLineW', windll.kernel32)) -CommandLineToArgvW = WINFUNCTYPE( - POINTER(LPWSTR), LPCWSTR, POINTER(c_int))( - ('CommandLineToArgvW', windll.shell32)) - +GetCommandLineW = WINFUNCTYPE(LPWSTR)(("GetCommandLineW", windll.kernel32)) +CommandLineToArgvW = WINFUNCTYPE(POINTER(LPWSTR), LPCWSTR, POINTER(c_int))( + ("CommandLineToArgvW", windll.shell32) +) +LocalFree = WINFUNCTYPE(c_void_p, c_void_p)(("LocalFree", windll.kernel32)) STDIN_HANDLE = GetStdHandle(-10) STDOUT_HANDLE = GetStdHandle(-11) STDERR_HANDLE = GetStdHandle(-12) - PyBUF_SIMPLE = 0 PyBUF_WRITABLE = 1 @@ -57,38 +61,40 @@ STDIN_FILENO = 0 STDOUT_FILENO = 1 STDERR_FILENO = 2 -EOF = b'\x1a' +EOF = b"\x1a" MAX_BYTES_WRITTEN = 32767 - -class Py_buffer(ctypes.Structure): - _fields_ = [ - ('buf', c_void_p), - ('obj', py_object), - ('len', c_ssize_t), - ('itemsize', c_ssize_t), - ('readonly', c_int), - ('ndim', c_int), - ('format', c_char_p), - ('shape', c_ssize_p), - ('strides', c_ssize_p), - ('suboffsets', c_ssize_p), - ('internal', c_void_p) - ] - - if PY2: - _fields_.insert(-1, ('smalltable', c_ssize_t * 2)) - - -# On PyPy we cannot get buffers so our ability to operate here is -# serverly limited. -if pythonapi is None: +try: + from ctypes import pythonapi +except ImportError: + # On PyPy we cannot get buffers so our ability to operate here is + # severely limited. get_buffer = None else: + + class Py_buffer(Structure): + _fields_ = [ + ("buf", c_void_p), + ("obj", py_object), + ("len", c_ssize_t), + ("itemsize", c_ssize_t), + ("readonly", c_int), + ("ndim", c_int), + ("format", c_char_p), + ("shape", c_ssize_p), + ("strides", c_ssize_p), + ("suboffsets", c_ssize_p), + ("internal", c_void_p), + ] + + PyObject_GetBuffer = pythonapi.PyObject_GetBuffer + PyBuffer_Release = pythonapi.PyBuffer_Release + def get_buffer(obj, writable=False): buf = Py_buffer() flags = PyBUF_WRITABLE if writable else PyBUF_SIMPLE PyObject_GetBuffer(py_object(obj), byref(buf), flags) + try: buffer_type = c_char * buf.len return buffer_type.from_address(buf.buf) @@ -97,17 +103,15 @@ else: class _WindowsConsoleRawIOBase(io.RawIOBase): - def __init__(self, handle): self.handle = handle def isatty(self): - io.RawIOBase.isatty(self) + super().isatty() return True class _WindowsConsoleReader(_WindowsConsoleRawIOBase): - def readable(self): return True @@ -116,20 +120,26 @@ class _WindowsConsoleReader(_WindowsConsoleRawIOBase): if not bytes_to_be_read: return 0 elif bytes_to_be_read % 2: - raise ValueError('cannot read odd number of bytes from ' - 'UTF-16-LE encoded console') + raise ValueError( + "cannot read odd number of bytes from UTF-16-LE encoded console" + ) buffer = get_buffer(b, writable=True) code_units_to_be_read = bytes_to_be_read // 2 code_units_read = c_ulong() - rv = ReadConsoleW(self.handle, buffer, code_units_to_be_read, - byref(code_units_read), None) + rv = ReadConsoleW( + HANDLE(self.handle), + buffer, + code_units_to_be_read, + byref(code_units_read), + None, + ) if GetLastError() == ERROR_OPERATION_ABORTED: # wait for KeyboardInterrupt time.sleep(0.1) if not rv: - raise OSError('Windows error: %s' % GetLastError()) + raise OSError(f"Windows error: {GetLastError()}") if buffer[0] == EOF: return 0 @@ -137,27 +147,30 @@ class _WindowsConsoleReader(_WindowsConsoleRawIOBase): class _WindowsConsoleWriter(_WindowsConsoleRawIOBase): - def writable(self): return True @staticmethod def _get_error_message(errno): if errno == ERROR_SUCCESS: - return 'ERROR_SUCCESS' + return "ERROR_SUCCESS" elif errno == ERROR_NOT_ENOUGH_MEMORY: - return 'ERROR_NOT_ENOUGH_MEMORY' - return 'Windows error %s' % errno + return "ERROR_NOT_ENOUGH_MEMORY" + return f"Windows error {errno}" def write(self, b): bytes_to_be_written = len(b) buf = get_buffer(b) - code_units_to_be_written = min(bytes_to_be_written, - MAX_BYTES_WRITTEN) // 2 + code_units_to_be_written = min(bytes_to_be_written, MAX_BYTES_WRITTEN) // 2 code_units_written = c_ulong() - WriteConsoleW(self.handle, buf, code_units_to_be_written, - byref(code_units_written), None) + WriteConsoleW( + HANDLE(self.handle), + buf, + code_units_to_be_written, + byref(code_units_written), + None, + ) bytes_written = 2 * code_units_written.value if bytes_written == 0 and bytes_to_be_written > 0: @@ -165,18 +178,17 @@ class _WindowsConsoleWriter(_WindowsConsoleRawIOBase): return bytes_written -class ConsoleStream(object): - - def __init__(self, text_stream, byte_stream): +class ConsoleStream: + def __init__(self, text_stream: t.TextIO, byte_stream: t.BinaryIO) -> None: self._text_stream = text_stream self.buffer = byte_stream @property - def name(self): + def name(self) -> str: return self.buffer.name - def write(self, x): - if isinstance(x, text_type): + def write(self, x: t.AnyStr) -> int: + if isinstance(x, str): return self._text_stream.write(x) try: self.flush() @@ -184,124 +196,84 @@ class ConsoleStream(object): pass return self.buffer.write(x) - def writelines(self, lines): + def writelines(self, lines: t.Iterable[t.AnyStr]) -> None: for line in lines: self.write(line) - def __getattr__(self, name): + def __getattr__(self, name: str) -> t.Any: return getattr(self._text_stream, name) - def isatty(self): + def isatty(self) -> bool: return self.buffer.isatty() def __repr__(self): - return '' % ( - self.name, - self.encoding, - ) + return f"" -class WindowsChunkedWriter(object): - """ - Wraps a stream (such as stdout), acting as a transparent proxy for all - attribute access apart from method 'write()' which we wrap to write in - limited chunks due to a Windows limitation on binary console streams. - """ - def __init__(self, wrapped): - # double-underscore everything to prevent clashes with names of - # attributes on the wrapped stream object. - self.__wrapped = wrapped - - def __getattr__(self, name): - return getattr(self.__wrapped, name) - - def write(self, text): - total_to_write = len(text) - written = 0 - - while written < total_to_write: - to_write = min(total_to_write - written, MAX_BYTES_WRITTEN) - self.__wrapped.write(text[written:written+to_write]) - written += to_write - - -_wrapped_std_streams = set() - - -def _wrap_std_stream(name): - # Python 2 & Windows 7 and below - if PY2 and sys.getwindowsversion()[:2] <= (6, 1) and name not in _wrapped_std_streams: - setattr(sys, name, WindowsChunkedWriter(getattr(sys, name))) - _wrapped_std_streams.add(name) - - -def _get_text_stdin(buffer_stream): +def _get_text_stdin(buffer_stream: t.BinaryIO) -> t.TextIO: text_stream = _NonClosingTextIOWrapper( io.BufferedReader(_WindowsConsoleReader(STDIN_HANDLE)), - 'utf-16-le', 'strict', line_buffering=True) - return ConsoleStream(text_stream, buffer_stream) + "utf-16-le", + "strict", + line_buffering=True, + ) + return t.cast(t.TextIO, ConsoleStream(text_stream, buffer_stream)) -def _get_text_stdout(buffer_stream): +def _get_text_stdout(buffer_stream: t.BinaryIO) -> t.TextIO: text_stream = _NonClosingTextIOWrapper( io.BufferedWriter(_WindowsConsoleWriter(STDOUT_HANDLE)), - 'utf-16-le', 'strict', line_buffering=True) - return ConsoleStream(text_stream, buffer_stream) + "utf-16-le", + "strict", + line_buffering=True, + ) + return t.cast(t.TextIO, ConsoleStream(text_stream, buffer_stream)) -def _get_text_stderr(buffer_stream): +def _get_text_stderr(buffer_stream: t.BinaryIO) -> t.TextIO: text_stream = _NonClosingTextIOWrapper( io.BufferedWriter(_WindowsConsoleWriter(STDERR_HANDLE)), - 'utf-16-le', 'strict', line_buffering=True) - return ConsoleStream(text_stream, buffer_stream) + "utf-16-le", + "strict", + line_buffering=True, + ) + return t.cast(t.TextIO, ConsoleStream(text_stream, buffer_stream)) -if PY2: - def _hash_py_argv(): - return zlib.crc32('\x00'.join(sys.argv[1:])) - - _initial_argv_hash = _hash_py_argv() - - def _get_windows_argv(): - argc = c_int(0) - argv_unicode = CommandLineToArgvW(GetCommandLineW(), byref(argc)) - argv = [argv_unicode[i] for i in range(0, argc.value)] - - if not hasattr(sys, 'frozen'): - argv = argv[1:] - while len(argv) > 0: - arg = argv[0] - if not arg.startswith('-') or arg == '-': - break - argv = argv[1:] - if arg.startswith(('-c', '-m')): - break - - return argv[1:] - - -_stream_factories = { +_stream_factories: t.Mapping[int, t.Callable[[t.BinaryIO], t.TextIO]] = { 0: _get_text_stdin, 1: _get_text_stdout, 2: _get_text_stderr, } -def _get_windows_console_stream(f, encoding, errors): - if get_buffer is not None and \ - encoding in ('utf-16-le', None) \ - and errors in ('strict', None) and \ - hasattr(f, 'isatty') and f.isatty(): +def _is_console(f: t.TextIO) -> bool: + if not hasattr(f, "fileno"): + return False + + try: + fileno = f.fileno() + except (OSError, io.UnsupportedOperation): + return False + + handle = msvcrt.get_osfhandle(fileno) + return bool(GetConsoleMode(handle, byref(DWORD()))) + + +def _get_windows_console_stream( + f: t.TextIO, encoding: t.Optional[str], errors: t.Optional[str] +) -> t.Optional[t.TextIO]: + if ( + get_buffer is not None + and encoding in {"utf-16-le", None} + and errors in {"strict", None} + and _is_console(f) + ): func = _stream_factories.get(f.fileno()) if func is not None: - if not PY2: - f = getattr(f, 'buffer', None) - if f is None: - return None - else: - # If we are on Python 2 we need to set the stream that we - # deal with to binary mode as otherwise the exercise if a - # bit moot. The same problems apply as for - # get_binary_stdin and friends from _compat. - msvcrt.setmode(f.fileno(), os.O_BINARY) - return func(f) + b = getattr(f, "buffer", None) + + if b is None: + return None + + return func(b) diff --git a/libs/click/core.py b/libs/click/core.py index 7a1e3422b..f2263544a 100644 --- a/libs/click/core.py +++ b/libs/click/core.py @@ -1,106 +1,103 @@ +import enum import errno -import inspect import os import sys +import typing +import typing as t +from collections import abc from contextlib import contextmanager -from itertools import repeat +from contextlib import ExitStack +from functools import partial from functools import update_wrapper +from gettext import gettext as _ +from gettext import ngettext +from itertools import repeat -from .types import convert_type, IntRange, BOOL -from .utils import PacifyFlushWrapper, make_str, make_default_short_help, \ - echo, get_os_args -from .exceptions import ClickException, UsageError, BadParameter, Abort, \ - MissingParameter, Exit -from .termui import prompt, confirm, style -from .formatting import HelpFormatter, join_options -from .parser import OptionParser, split_opt -from .globals import push_context, pop_context +from . import types +from ._unicodefun import _verify_python_env +from .exceptions import Abort +from .exceptions import BadParameter +from .exceptions import ClickException +from .exceptions import Exit +from .exceptions import MissingParameter +from .exceptions import UsageError +from .formatting import HelpFormatter +from .formatting import join_options +from .globals import pop_context +from .globals import push_context +from .parser import _flag_needs_value +from .parser import OptionParser +from .parser import split_opt +from .termui import confirm +from .termui import prompt +from .termui import style +from .utils import _detect_program_name +from .utils import _expand_args +from .utils import echo +from .utils import make_default_short_help +from .utils import make_str +from .utils import PacifyFlushWrapper -from ._compat import PY2, isidentifier, iteritems, string_types -from ._unicodefun import _check_for_unicode_literals, _verify_python3_env +if t.TYPE_CHECKING: + import typing_extensions as te + from .shell_completion import CompletionItem + +F = t.TypeVar("F", bound=t.Callable[..., t.Any]) +V = t.TypeVar("V") -_missing = object() +def _complete_visible_commands( + ctx: "Context", incomplete: str +) -> t.Iterator[t.Tuple[str, "Command"]]: + """List all the subcommands of a group that start with the + incomplete value and aren't hidden. - -SUBCOMMAND_METAVAR = 'COMMAND [ARGS]...' -SUBCOMMANDS_METAVAR = 'COMMAND1 [ARGS]... [COMMAND2 [ARGS]...]...' - -DEPRECATED_HELP_NOTICE = ' (DEPRECATED)' -DEPRECATED_INVOKE_NOTICE = 'DeprecationWarning: ' + \ - 'The command %(name)s is deprecated.' - - -def _maybe_show_deprecated_notice(cmd): - if cmd.deprecated: - echo(style(DEPRECATED_INVOKE_NOTICE % {'name': cmd.name}, fg='red'), err=True) - - -def fast_exit(code): - """Exit without garbage collection, this speeds up exit by about 10ms for - things like bash completion. + :param ctx: Invocation context for the group. + :param incomplete: Value being completed. May be empty. """ - sys.stdout.flush() - sys.stderr.flush() - os._exit(code) + multi = t.cast(MultiCommand, ctx.command) + + for name in multi.list_commands(ctx): + if name.startswith(incomplete): + command = multi.get_command(ctx, name) + + if command is not None and not command.hidden: + yield name, command -def _bashcomplete(cmd, prog_name, complete_var=None): - """Internal handler for the bash completion support.""" - if complete_var is None: - complete_var = '_%s_COMPLETE' % (prog_name.replace('-', '_')).upper() - complete_instr = os.environ.get(complete_var) - if not complete_instr: - return - - from ._bashcomplete import bashcomplete - if bashcomplete(cmd, prog_name, complete_var, complete_instr): - fast_exit(1) - - -def _check_multicommand(base_command, cmd_name, cmd, register=False): +def _check_multicommand( + base_command: "MultiCommand", cmd_name: str, cmd: "Command", register: bool = False +) -> None: if not base_command.chain or not isinstance(cmd, MultiCommand): return if register: - hint = 'It is not possible to add multi commands as children to ' \ - 'another multi command that is in chain mode' + hint = ( + "It is not possible to add multi commands as children to" + " another multi command that is in chain mode." + ) else: - hint = 'Found a multi command as subcommand to a multi command ' \ - 'that is in chain mode. This is not supported' - raise RuntimeError('%s. Command "%s" is set to chain and "%s" was ' - 'added as subcommand but it in itself is a ' - 'multi command. ("%s" is a %s within a chained ' - '%s named "%s").' % ( - hint, base_command.name, cmd_name, - cmd_name, cmd.__class__.__name__, - base_command.__class__.__name__, - base_command.name)) + hint = ( + "Found a multi command as subcommand to a multi command" + " that is in chain mode. This is not supported." + ) + raise RuntimeError( + f"{hint}. Command {base_command.name!r} is set to chain and" + f" {cmd_name!r} was added as a subcommand but it in itself is a" + f" multi command. ({cmd_name!r} is a {type(cmd).__name__}" + f" within a chained {type(base_command).__name__} named" + f" {base_command.name!r})." + ) -def batch(iterable, batch_size): +def batch(iterable: t.Iterable[V], batch_size: int) -> t.List[t.Tuple[V, ...]]: return list(zip(*repeat(iter(iterable), batch_size))) -def invoke_param_callback(callback, ctx, param, value): - code = getattr(callback, '__code__', None) - args = getattr(code, 'co_argcount', 3) - - if args < 3: - # This will become a warning in Click 3.0: - from warnings import warn - warn(Warning('Invoked legacy parameter callback "%s". The new ' - 'signature for such callbacks starting with ' - 'click 2.0 is (ctx, param, value).' - % callback), stacklevel=3) - return callback(ctx, value) - return callback(ctx, param, value) - - @contextmanager -def augment_usage_errors(ctx, param=None): - """Context manager that attaches extra information to exceptions that - fly. - """ +def augment_usage_errors( + ctx: "Context", param: t.Optional["Parameter"] = None +) -> t.Iterator[None]: + """Context manager that attaches extra information to exceptions.""" try: yield except BadParameter as e: @@ -115,22 +112,53 @@ def augment_usage_errors(ctx, param=None): raise -def iter_params_for_processing(invocation_order, declaration_order): +def iter_params_for_processing( + invocation_order: t.Sequence["Parameter"], + declaration_order: t.Sequence["Parameter"], +) -> t.List["Parameter"]: """Given a sequence of parameters in the order as should be considered for processing and an iterable of parameters that exist, this returns a list in the correct order as they should be processed. """ - def sort_key(item): + + def sort_key(item: "Parameter") -> t.Tuple[bool, float]: try: - idx = invocation_order.index(item) + idx: float = invocation_order.index(item) except ValueError: - idx = float('inf') - return (not item.is_eager, idx) + idx = float("inf") + + return not item.is_eager, idx return sorted(declaration_order, key=sort_key) -class Context(object): +class ParameterSource(enum.Enum): + """This is an :class:`~enum.Enum` that indicates the source of a + parameter's value. + + Use :meth:`click.Context.get_parameter_source` to get the + source for a parameter by name. + + .. versionchanged:: 8.0 + Use :class:`~enum.Enum` and drop the ``validate`` method. + + .. versionchanged:: 8.0 + Added the ``PROMPT`` value. + """ + + COMMANDLINE = enum.auto() + """The value was provided by the command line args.""" + ENVIRONMENT = enum.auto() + """The value was provided with an environment variable.""" + DEFAULT = enum.auto() + """Used the default specified by the parameter.""" + DEFAULT_MAP = enum.auto() + """Used a default provided by :attr:`Context.default_map`.""" + PROMPT = enum.auto() + """Used a prompt to confirm a default or provide a value.""" + + +class Context: """The context is a special internal object that holds state relevant for the script execution at every single level. It's normally invisible to commands unless they opt-in to getting access to it. @@ -142,18 +170,6 @@ class Context(object): A context can be used as context manager in which case it will call :meth:`close` on teardown. - .. versionadded:: 2.0 - Added the `resilient_parsing`, `help_option_names`, - `token_normalize_func` parameters. - - .. versionadded:: 3.0 - Added the `allow_extra_args` and `allow_interspersed_args` - parameters. - - .. versionadded:: 4.0 - Added the `color`, `ignore_unknown_options`, and - `max_content_width` parameters. - :param command: the command class for this context. :param parent: the parent context. :param info_name: the info name for this invocation. Generally this @@ -208,43 +224,88 @@ class Context(object): codes are used in texts that Click prints which is by default not the case. This for instance would affect help output. + :param show_default: Show defaults for all options. If not set, + defaults to the value from a parent context. Overrides an + option's ``show_default`` argument. + + .. versionchanged:: 8.0 + The ``show_default`` parameter defaults to the value from the + parent context. + + .. versionchanged:: 7.1 + Added the ``show_default`` parameter. + + .. versionchanged:: 4.0 + Added the ``color``, ``ignore_unknown_options``, and + ``max_content_width`` parameters. + + .. versionchanged:: 3.0 + Added the ``allow_extra_args`` and ``allow_interspersed_args`` + parameters. + + .. versionchanged:: 2.0 + Added the ``resilient_parsing``, ``help_option_names``, and + ``token_normalize_func`` parameters. """ - def __init__(self, command, parent=None, info_name=None, obj=None, - auto_envvar_prefix=None, default_map=None, - terminal_width=None, max_content_width=None, - resilient_parsing=False, allow_extra_args=None, - allow_interspersed_args=None, - ignore_unknown_options=None, help_option_names=None, - token_normalize_func=None, color=None): + #: The formatter class to create with :meth:`make_formatter`. + #: + #: .. versionadded:: 8.0 + formatter_class: t.Type["HelpFormatter"] = HelpFormatter + + def __init__( + self, + command: "Command", + parent: t.Optional["Context"] = None, + info_name: t.Optional[str] = None, + obj: t.Optional[t.Any] = None, + auto_envvar_prefix: t.Optional[str] = None, + default_map: t.Optional[t.Dict[str, t.Any]] = None, + terminal_width: t.Optional[int] = None, + max_content_width: t.Optional[int] = None, + resilient_parsing: bool = False, + allow_extra_args: t.Optional[bool] = None, + allow_interspersed_args: t.Optional[bool] = None, + ignore_unknown_options: t.Optional[bool] = None, + help_option_names: t.Optional[t.List[str]] = None, + token_normalize_func: t.Optional[t.Callable[[str], str]] = None, + color: t.Optional[bool] = None, + show_default: t.Optional[bool] = None, + ) -> None: #: the parent context or `None` if none exists. self.parent = parent #: the :class:`Command` for this context. self.command = command #: the descriptive information name self.info_name = info_name - #: the parsed parameters except if the value is hidden in which - #: case it's not remembered. - self.params = {} + #: Map of parameter names to their parsed values. Parameters + #: with ``expose_value=False`` are not stored. + self.params: t.Dict[str, t.Any] = {} #: the leftover arguments. - self.args = [] + self.args: t.List[str] = [] #: protected arguments. These are arguments that are prepended #: to `args` when certain parsing scenarios are encountered but #: must be never propagated to another arguments. This is used #: to implement nested parsing. - self.protected_args = [] + self.protected_args: t.List[str] = [] + if obj is None and parent is not None: obj = parent.obj + #: the user object stored. - self.obj = obj - self._meta = getattr(parent, 'meta', {}) + self.obj: t.Any = obj + self._meta: t.Dict[str, t.Any] = getattr(parent, "meta", {}) #: A dictionary (-like object) with defaults for parameters. - if default_map is None \ - and parent is not None \ - and parent.default_map is not None: + if ( + default_map is None + and info_name is not None + and parent is not None + and parent.default_map is not None + ): default_map = parent.default_map.get(info_name) - self.default_map = default_map + + self.default_map: t.Optional[t.Dict[str, t.Any]] = default_map #: This flag indicates if a subcommand is going to be executed. A #: group callback can use this information to figure out if it's @@ -255,22 +316,25 @@ class Context(object): #: If chaining is enabled this will be set to ``'*'`` in case #: any commands are executed. It is however not possible to #: figure out which ones. If you require this knowledge you - #: should use a :func:`resultcallback`. - self.invoked_subcommand = None + #: should use a :func:`result_callback`. + self.invoked_subcommand: t.Optional[str] = None if terminal_width is None and parent is not None: terminal_width = parent.terminal_width + #: The width of the terminal (None is autodetection). - self.terminal_width = terminal_width + self.terminal_width: t.Optional[int] = terminal_width if max_content_width is None and parent is not None: max_content_width = parent.max_content_width + #: The maximum width of formatted content (None implies a sensible #: default which is 80 for most things). - self.max_content_width = max_content_width + self.max_content_width: t.Optional[int] = max_content_width if allow_extra_args is None: allow_extra_args = command.allow_extra_args + #: Indicates if the context allows extra args or if it should #: fail on parsing. #: @@ -279,14 +343,16 @@ class Context(object): if allow_interspersed_args is None: allow_interspersed_args = command.allow_interspersed_args + #: Indicates if the context allows mixing of arguments and #: options or not. #: #: .. versionadded:: 3.0 - self.allow_interspersed_args = allow_interspersed_args + self.allow_interspersed_args: bool = allow_interspersed_args if ignore_unknown_options is None: ignore_unknown_options = command.ignore_unknown_options + #: Instructs click to ignore options that a command does not #: understand and will store it on the context for later #: processing. This is primarily useful for situations where you @@ -295,64 +361,102 @@ class Context(object): #: forward all arguments. #: #: .. versionadded:: 4.0 - self.ignore_unknown_options = ignore_unknown_options + self.ignore_unknown_options: bool = ignore_unknown_options if help_option_names is None: if parent is not None: help_option_names = parent.help_option_names else: - help_option_names = ['--help'] + help_option_names = ["--help"] #: The names for the help options. - self.help_option_names = help_option_names + self.help_option_names: t.List[str] = help_option_names if token_normalize_func is None and parent is not None: token_normalize_func = parent.token_normalize_func #: An optional normalization function for tokens. This is #: options, choices, commands etc. - self.token_normalize_func = token_normalize_func + self.token_normalize_func: t.Optional[ + t.Callable[[str], str] + ] = token_normalize_func #: Indicates if resilient parsing is enabled. In that case Click #: will do its best to not cause any failures and default values #: will be ignored. Useful for completion. - self.resilient_parsing = resilient_parsing + self.resilient_parsing: bool = resilient_parsing # If there is no envvar prefix yet, but the parent has one and # the command on this level has a name, we can expand the envvar # prefix automatically. if auto_envvar_prefix is None: - if parent is not None \ - and parent.auto_envvar_prefix is not None and \ - self.info_name is not None: - auto_envvar_prefix = '%s_%s' % (parent.auto_envvar_prefix, - self.info_name.upper()) + if ( + parent is not None + and parent.auto_envvar_prefix is not None + and self.info_name is not None + ): + auto_envvar_prefix = ( + f"{parent.auto_envvar_prefix}_{self.info_name.upper()}" + ) else: auto_envvar_prefix = auto_envvar_prefix.upper() - self.auto_envvar_prefix = auto_envvar_prefix + + if auto_envvar_prefix is not None: + auto_envvar_prefix = auto_envvar_prefix.replace("-", "_") + + self.auto_envvar_prefix: t.Optional[str] = auto_envvar_prefix if color is None and parent is not None: color = parent.color #: Controls if styling output is wanted or not. - self.color = color + self.color: t.Optional[bool] = color - self._close_callbacks = [] + if show_default is None and parent is not None: + show_default = parent.show_default + + #: Show option default values when formatting help text. + self.show_default: t.Optional[bool] = show_default + + self._close_callbacks: t.List[t.Callable[[], t.Any]] = [] self._depth = 0 + self._parameter_source: t.Dict[str, ParameterSource] = {} + self._exit_stack = ExitStack() - def __enter__(self): + def to_info_dict(self) -> t.Dict[str, t.Any]: + """Gather information that could be useful for a tool generating + user-facing documentation. This traverses the entire CLI + structure. + + .. code-block:: python + + with Context(cli) as ctx: + info = ctx.to_info_dict() + + .. versionadded:: 8.0 + """ + return { + "command": self.command.to_info_dict(self), + "info_name": self.info_name, + "allow_extra_args": self.allow_extra_args, + "allow_interspersed_args": self.allow_interspersed_args, + "ignore_unknown_options": self.ignore_unknown_options, + "auto_envvar_prefix": self.auto_envvar_prefix, + } + + def __enter__(self) -> "Context": self._depth += 1 push_context(self) return self - def __exit__(self, exc_type, exc_value, tb): + def __exit__(self, exc_type, exc_value, tb): # type: ignore self._depth -= 1 if self._depth == 0: self.close() pop_context() @contextmanager - def scope(self, cleanup=True): + def scope(self, cleanup: bool = True) -> t.Iterator["Context"]: """This helper method can be used with the context object to promote it to the current thread local (see :func:`get_current_context`). The default behavior of this is to invoke the cleanup functions which @@ -390,7 +494,7 @@ class Context(object): self._depth -= 1 @property - def meta(self): + def meta(self) -> t.Dict[str, t.Any]: """This is a dictionary which is shared with all the contexts that are nested. It exists so that click utilities can store some state here if they need to. It is however the responsibility of @@ -404,7 +508,7 @@ class Context(object): Example usage:: - LANG_KEY = __name__ + '.lang' + LANG_KEY = f'{__name__}.lang' def set_language(value): ctx = get_current_context() @@ -417,58 +521,109 @@ class Context(object): """ return self._meta - def make_formatter(self): - """Creates the formatter for the help and usage output.""" - return HelpFormatter(width=self.terminal_width, - max_width=self.max_content_width) + def make_formatter(self) -> HelpFormatter: + """Creates the :class:`~click.HelpFormatter` for the help and + usage output. - def call_on_close(self, f): - """This decorator remembers a function as callback that should be - executed when the context tears down. This is most useful to bind - resource handling to the script execution. For instance, file objects - opened by the :class:`File` type will register their close callbacks - here. + To quickly customize the formatter class used without overriding + this method, set the :attr:`formatter_class` attribute. - :param f: the function to execute on teardown. + .. versionchanged:: 8.0 + Added the :attr:`formatter_class` attribute. """ - self._close_callbacks.append(f) - return f + return self.formatter_class( + width=self.terminal_width, max_width=self.max_content_width + ) - def close(self): - """Invokes all close callbacks.""" - for cb in self._close_callbacks: - cb() - self._close_callbacks = [] + def with_resource(self, context_manager: t.ContextManager[V]) -> V: + """Register a resource as if it were used in a ``with`` + statement. The resource will be cleaned up when the context is + popped. + + Uses :meth:`contextlib.ExitStack.enter_context`. It calls the + resource's ``__enter__()`` method and returns the result. When + the context is popped, it closes the stack, which calls the + resource's ``__exit__()`` method. + + To register a cleanup function for something that isn't a + context manager, use :meth:`call_on_close`. Or use something + from :mod:`contextlib` to turn it into a context manager first. + + .. code-block:: python + + @click.group() + @click.option("--name") + @click.pass_context + def cli(ctx): + ctx.obj = ctx.with_resource(connect_db(name)) + + :param context_manager: The context manager to enter. + :return: Whatever ``context_manager.__enter__()`` returns. + + .. versionadded:: 8.0 + """ + return self._exit_stack.enter_context(context_manager) + + def call_on_close(self, f: t.Callable[..., t.Any]) -> t.Callable[..., t.Any]: + """Register a function to be called when the context tears down. + + This can be used to close resources opened during the script + execution. Resources that support Python's context manager + protocol which would be used in a ``with`` statement should be + registered with :meth:`with_resource` instead. + + :param f: The function to execute on teardown. + """ + return self._exit_stack.callback(f) + + def close(self) -> None: + """Invoke all close callbacks registered with + :meth:`call_on_close`, and exit all context managers entered + with :meth:`with_resource`. + """ + self._exit_stack.close() + # In case the context is reused, create a new exit stack. + self._exit_stack = ExitStack() @property - def command_path(self): + def command_path(self) -> str: """The computed command path. This is used for the ``usage`` information on the help page. It's automatically created by combining the info names of the chain of contexts to the root. """ - rv = '' + rv = "" if self.info_name is not None: rv = self.info_name if self.parent is not None: - rv = self.parent.command_path + ' ' + rv + parent_command_path = [self.parent.command_path] + + if isinstance(self.parent.command, Command): + for param in self.parent.command.get_params(self): + parent_command_path.extend(param.get_usage_pieces(self)) + + rv = f"{' '.join(parent_command_path)} {rv}" return rv.lstrip() - def find_root(self): + def find_root(self) -> "Context": """Finds the outermost context.""" node = self while node.parent is not None: node = node.parent return node - def find_object(self, object_type): + def find_object(self, object_type: t.Type[V]) -> t.Optional[V]: """Finds the closest object of a given type.""" - node = self + node: t.Optional["Context"] = self + while node is not None: if isinstance(node.obj, object_type): return node.obj + node = node.parent - def ensure_object(self, object_type): + return None + + def ensure_object(self, object_type: t.Type[V]) -> V: """Like :meth:`find_object` but sets the innermost object to a new instance of `object_type` if it does not exist. """ @@ -477,17 +632,39 @@ class Context(object): self.obj = rv = object_type() return rv - def lookup_default(self, name): - """Looks up the default for a parameter name. This by default - looks into the :attr:`default_map` if available. + @typing.overload + def lookup_default( + self, name: str, call: "te.Literal[True]" = True + ) -> t.Optional[t.Any]: + ... + + @typing.overload + def lookup_default( + self, name: str, call: "te.Literal[False]" = ... + ) -> t.Optional[t.Union[t.Any, t.Callable[[], t.Any]]]: + ... + + def lookup_default(self, name: str, call: bool = True) -> t.Optional[t.Any]: + """Get the default for a parameter from :attr:`default_map`. + + :param name: Name of the parameter. + :param call: If the default is a callable, call it. Disable to + return the callable instead. + + .. versionchanged:: 8.0 + Added the ``call`` parameter. """ if self.default_map is not None: - rv = self.default_map.get(name) - if callable(rv): - rv = rv() - return rv + value = self.default_map.get(name) - def fail(self, message): + if call and callable(value): + return value() + + return value + + return None + + def fail(self, message: str) -> "te.NoReturn": """Aborts the execution of the program with a specific error message. @@ -495,27 +672,40 @@ class Context(object): """ raise UsageError(message, self) - def abort(self): + def abort(self) -> "te.NoReturn": """Aborts the script.""" raise Abort() - def exit(self, code=0): + def exit(self, code: int = 0) -> "te.NoReturn": """Exits the application with a given exit code.""" raise Exit(code) - def get_usage(self): + def get_usage(self) -> str: """Helper method to get formatted usage string for the current context and command. """ return self.command.get_usage(self) - def get_help(self): + def get_help(self) -> str: """Helper method to get formatted help page for the current context and command. """ return self.command.get_help(self) - def invoke(*args, **kwargs): + def _make_sub_context(self, command: "Command") -> "Context": + """Create a new context of the same type as this context, but + for a new command. + + :meta private: + """ + return type(self)(command, info_name=command.name, parent=self) + + def invoke( + __self, # noqa: B902 + __callback: t.Union["Command", t.Callable[..., t.Any]], + *args: t.Any, + **kwargs: t.Any, + ) -> t.Any: """Invokes a command callback in exactly the way it expects. There are two ways to invoke this method: @@ -530,50 +720,89 @@ class Context(object): in against the intention of this code and no context was created. For more information about this change and why it was done in a bugfix release see :ref:`upgrade-to-3.2`. - """ - self, callback = args[:2] - ctx = self - # It's also possible to invoke another command which might or - # might not have a callback. In that case we also fill - # in defaults and make a new context for this command. - if isinstance(callback, Command): - other_cmd = callback - callback = other_cmd.callback - ctx = Context(other_cmd, info_name=other_cmd.name, parent=self) - if callback is None: - raise TypeError('The given command does not have a ' - 'callback that can be invoked.') + .. versionchanged:: 8.0 + All ``kwargs`` are tracked in :attr:`params` so they will be + passed if :meth:`forward` is called at multiple levels. + """ + if isinstance(__callback, Command): + other_cmd = __callback + + if other_cmd.callback is None: + raise TypeError( + "The given command does not have a callback that can be invoked." + ) + else: + __callback = other_cmd.callback + + ctx = __self._make_sub_context(other_cmd) for param in other_cmd.params: if param.name not in kwargs and param.expose_value: - kwargs[param.name] = param.get_default(ctx) + kwargs[param.name] = param.type_cast_value( # type: ignore + ctx, param.get_default(ctx) + ) - args = args[2:] - with augment_usage_errors(self): + # Track all kwargs as params, so that forward() will pass + # them on in subsequent calls. + ctx.params.update(kwargs) + else: + ctx = __self + + with augment_usage_errors(__self): with ctx: - return callback(*args, **kwargs) + return __callback(*args, **kwargs) - def forward(*args, **kwargs): + def forward( + __self, __cmd: "Command", *args: t.Any, **kwargs: t.Any # noqa: B902 + ) -> t.Any: """Similar to :meth:`invoke` but fills in default keyword arguments from the current context if the other command expects it. This cannot invoke callbacks directly, only other commands. + + .. versionchanged:: 8.0 + All ``kwargs`` are tracked in :attr:`params` so they will be + passed if ``forward`` is called at multiple levels. """ - self, cmd = args[:2] + # Can only forward to other commands, not direct callbacks. + if not isinstance(__cmd, Command): + raise TypeError("Callback is not a command.") - # It's also possible to invoke another command which might or - # might not have a callback. - if not isinstance(cmd, Command): - raise TypeError('Callback is not a command.') - - for param in self.params: + for param in __self.params: if param not in kwargs: - kwargs[param] = self.params[param] + kwargs[param] = __self.params[param] - return self.invoke(cmd, **kwargs) + return __self.invoke(__cmd, *args, **kwargs) + + def set_parameter_source(self, name: str, source: ParameterSource) -> None: + """Set the source of a parameter. This indicates the location + from which the value of the parameter was obtained. + + :param name: The name of the parameter. + :param source: A member of :class:`~click.core.ParameterSource`. + """ + self._parameter_source[name] = source + + def get_parameter_source(self, name: str) -> t.Optional[ParameterSource]: + """Get the source of a parameter. This indicates the location + from which the value of the parameter was obtained. + + This can be useful for determining when a user specified a value + on the command line that is the same as the default value. It + will be :attr:`~click.core.ParameterSource.DEFAULT` only if the + value was actually taken from the default. + + :param name: The name of the parameter. + :rtype: ParameterSource + + .. versionchanged:: 8.0 + Returns ``None`` if the parameter was not provided from any + source. + """ + return self._parameter_source.get(name) -class BaseCommand(object): +class BaseCommand: """The base command implements the minimal API contract of commands. Most code will never use this as it does not implement a lot of useful functionality but it can act as the direct subclass of alternative @@ -594,6 +823,11 @@ class BaseCommand(object): :param context_settings: an optional dictionary with defaults that are passed to the context object. """ + + #: The context class to create with :meth:`make_context`. + #: + #: .. versionadded:: 8.0 + context_class: t.Type[Context] = Context #: the default for the :attr:`Context.allow_extra_args` flag. allow_extra_args = False #: the default for the :attr:`Context.allow_interspersed_args` flag. @@ -601,62 +835,158 @@ class BaseCommand(object): #: the default for the :attr:`Context.ignore_unknown_options` flag. ignore_unknown_options = False - def __init__(self, name, context_settings=None): + def __init__( + self, + name: t.Optional[str], + context_settings: t.Optional[t.Dict[str, t.Any]] = None, + ) -> None: #: the name the command thinks it has. Upon registering a command #: on a :class:`Group` the group will default the command name #: with this information. You should instead use the #: :class:`Context`\'s :attr:`~Context.info_name` attribute. self.name = name + if context_settings is None: context_settings = {} + #: an optional dictionary with defaults passed to the context. - self.context_settings = context_settings + self.context_settings: t.Dict[str, t.Any] = context_settings - def get_usage(self, ctx): - raise NotImplementedError('Base commands cannot get usage') + def to_info_dict(self, ctx: Context) -> t.Dict[str, t.Any]: + """Gather information that could be useful for a tool generating + user-facing documentation. This traverses the entire structure + below this command. - def get_help(self, ctx): - raise NotImplementedError('Base commands cannot get help') + Use :meth:`click.Context.to_info_dict` to traverse the entire + CLI structure. - def make_context(self, info_name, args, parent=None, **extra): + :param ctx: A :class:`Context` representing this command. + + .. versionadded:: 8.0 + """ + return {"name": self.name} + + def __repr__(self) -> str: + return f"<{self.__class__.__name__} {self.name}>" + + def get_usage(self, ctx: Context) -> str: + raise NotImplementedError("Base commands cannot get usage") + + def get_help(self, ctx: Context) -> str: + raise NotImplementedError("Base commands cannot get help") + + def make_context( + self, + info_name: t.Optional[str], + args: t.List[str], + parent: t.Optional[Context] = None, + **extra: t.Any, + ) -> Context: """This function when given an info name and arguments will kick off the parsing and create a new :class:`Context`. It does not invoke the actual command callback though. - :param info_name: the info name for this invokation. Generally this + To quickly customize the context class used without overriding + this method, set the :attr:`context_class` attribute. + + :param info_name: the info name for this invocation. Generally this is the most descriptive name for the script or command. For the toplevel script it's usually the name of the script, for commands below it it's - the name of the script. + the name of the command. :param args: the arguments to parse as list of strings. :param parent: the parent context if available. :param extra: extra keyword arguments forwarded to the context constructor. + + .. versionchanged:: 8.0 + Added the :attr:`context_class` attribute. """ - for key, value in iteritems(self.context_settings): + for key, value in self.context_settings.items(): if key not in extra: extra[key] = value - ctx = Context(self, info_name=info_name, parent=parent, **extra) + + ctx = self.context_class( + self, info_name=info_name, parent=parent, **extra # type: ignore + ) + with ctx.scope(cleanup=False): self.parse_args(ctx, args) return ctx - def parse_args(self, ctx, args): + def parse_args(self, ctx: Context, args: t.List[str]) -> t.List[str]: """Given a context and a list of arguments this creates the parser and parses the arguments, then modifies the context as necessary. This is automatically invoked by :meth:`make_context`. """ - raise NotImplementedError('Base commands do not know how to parse ' - 'arguments.') + raise NotImplementedError("Base commands do not know how to parse arguments.") - def invoke(self, ctx): + def invoke(self, ctx: Context) -> t.Any: """Given a context, this invokes the command. The default implementation is raising a not implemented error. """ - raise NotImplementedError('Base commands are not invokable by default') + raise NotImplementedError("Base commands are not invokable by default") - def main(self, args=None, prog_name=None, complete_var=None, - standalone_mode=True, **extra): + def shell_complete(self, ctx: Context, incomplete: str) -> t.List["CompletionItem"]: + """Return a list of completions for the incomplete value. Looks + at the names of chained multi-commands. + + Any command could be part of a chained multi-command, so sibling + commands are valid at any point during command completion. Other + command classes will return more completions. + + :param ctx: Invocation context for this command. + :param incomplete: Value being completed. May be empty. + + .. versionadded:: 8.0 + """ + from click.shell_completion import CompletionItem + + results: t.List["CompletionItem"] = [] + + while ctx.parent is not None: + ctx = ctx.parent + + if isinstance(ctx.command, MultiCommand) and ctx.command.chain: + results.extend( + CompletionItem(name, help=command.get_short_help_str()) + for name, command in _complete_visible_commands(ctx, incomplete) + if name not in ctx.protected_args + ) + + return results + + @typing.overload + def main( + self, + args: t.Optional[t.Sequence[str]] = None, + prog_name: t.Optional[str] = None, + complete_var: t.Optional[str] = None, + standalone_mode: "te.Literal[True]" = True, + **extra: t.Any, + ) -> "te.NoReturn": + ... + + @typing.overload + def main( + self, + args: t.Optional[t.Sequence[str]] = None, + prog_name: t.Optional[str] = None, + complete_var: t.Optional[str] = None, + standalone_mode: bool = ..., + **extra: t.Any, + ) -> t.Any: + ... + + def main( + self, + args: t.Optional[t.Sequence[str]] = None, + prog_name: t.Optional[str] = None, + complete_var: t.Optional[str] = None, + standalone_mode: bool = True, + windows_expand_args: bool = True, + **extra: t.Any, + ) -> t.Any: """This is the way to invoke a script with all the bells and whistles as a command line application. This will always terminate the application after a call. If this is not wanted, ``SystemExit`` @@ -665,9 +995,6 @@ class BaseCommand(object): This method is also available by directly calling the instance of a :class:`Command`. - .. versionadded:: 3.0 - Added the `standalone_mode` flag to control the standalone mode. - :param args: the arguments that should be used for parsing. If not provided, ``sys.argv[1:]`` is used. :param prog_name: the program name that should be used. By default @@ -686,30 +1013,39 @@ class BaseCommand(object): propagated to the caller and the return value of this function is the return value of :meth:`invoke`. + :param windows_expand_args: Expand glob patterns, user dir, and + env vars in command line args on Windows. :param extra: extra keyword arguments are forwarded to the context constructor. See :class:`Context` for more information. + + .. versionchanged:: 8.0.1 + Added the ``windows_expand_args`` parameter to allow + disabling command line arg expansion on Windows. + + .. versionchanged:: 8.0 + When taking arguments from ``sys.argv`` on Windows, glob + patterns, user dir, and env vars are expanded. + + .. versionchanged:: 3.0 + Added the ``standalone_mode`` parameter. """ - # If we are in Python 3, we will verify that the environment is - # sane at this point or reject further execution to avoid a - # broken script. - if not PY2: - _verify_python3_env() - else: - _check_for_unicode_literals() + # Verify that the environment is configured correctly, or reject + # further execution to avoid a broken script. + _verify_python_env() if args is None: - args = get_os_args() + args = sys.argv[1:] + + if os.name == "nt" and windows_expand_args: + args = _expand_args(args) else: args = list(args) if prog_name is None: - prog_name = make_str(os.path.basename( - sys.argv and sys.argv[0] or __file__)) + prog_name = _detect_program_name() - # Hook for the Bash completion. This only activates if the Bash - # completion is actually enabled, otherwise this is quite a fast - # noop. - _bashcomplete(self, prog_name, complete_var) + # Process shell completion requests and exit early. + self._main_shell_completion(extra, prog_name, complete_var) try: try: @@ -727,16 +1063,16 @@ class BaseCommand(object): ctx.exit() except (EOFError, KeyboardInterrupt): echo(file=sys.stderr) - raise Abort() + raise Abort() from None except ClickException as e: if not standalone_mode: raise e.show() sys.exit(e.exit_code) - except IOError as e: + except OSError as e: if e.errno == errno.EPIPE: - sys.stdout = PacifyFlushWrapper(sys.stdout) - sys.stderr = PacifyFlushWrapper(sys.stderr) + sys.stdout = t.cast(t.TextIO, PacifyFlushWrapper(sys.stdout)) + sys.stderr = t.cast(t.TextIO, PacifyFlushWrapper(sys.stderr)) sys.exit(1) else: raise @@ -756,10 +1092,38 @@ class BaseCommand(object): except Abort: if not standalone_mode: raise - echo('Aborted!', file=sys.stderr) + echo(_("Aborted!"), file=sys.stderr) sys.exit(1) - def __call__(self, *args, **kwargs): + def _main_shell_completion( + self, + ctx_args: t.Dict[str, t.Any], + prog_name: str, + complete_var: t.Optional[str] = None, + ) -> None: + """Check if the shell is asking for tab completion, process + that, then exit early. Called from :meth:`main` before the + program is invoked. + + :param prog_name: Name of the executable in the shell. + :param complete_var: Name of the environment variable that holds + the completion instruction. Defaults to + ``_{PROG_NAME}_COMPLETE``. + """ + if complete_var is None: + complete_var = f"_{prog_name}_COMPLETE".replace("-", "_").upper() + + instruction = os.environ.get(complete_var) + + if not instruction: + return + + from .shell_completion import shell_complete + + rv = shell_complete(self, ctx_args, prog_name, complete_var, instruction) + sys.exit(rv) + + def __call__(self, *args: t.Any, **kwargs: t.Any) -> t.Any: """Alias for :meth:`main`.""" return self.main(*args, **kwargs) @@ -771,6 +1135,10 @@ class Command(BaseCommand): .. versionchanged:: 2.0 Added the `context_settings` parameter. + .. versionchanged:: 8.0 + Added repr showing the command name + .. versionchanged:: 7.1 + Added the `no_args_is_help` parameter. :param name: the name of the command to use unless a group overrides it. :param context_settings: an optional dictionary with defaults that are @@ -785,108 +1153,168 @@ class Command(BaseCommand): shown on the command listing of the parent command. :param add_help_option: by default each command registers a ``--help`` option. This can be disabled by this parameter. + :param no_args_is_help: this controls what happens if no arguments are + provided. This option is disabled by default. + If enabled this will add ``--help`` as argument + if no arguments are passed :param hidden: hide this command from help outputs. :param deprecated: issues a message indicating that the command is deprecated. """ - def __init__(self, name, context_settings=None, callback=None, - params=None, help=None, epilog=None, short_help=None, - options_metavar='[OPTIONS]', add_help_option=True, - hidden=False, deprecated=False): - BaseCommand.__init__(self, name, context_settings) + def __init__( + self, + name: t.Optional[str], + context_settings: t.Optional[t.Dict[str, t.Any]] = None, + callback: t.Optional[t.Callable[..., t.Any]] = None, + params: t.Optional[t.List["Parameter"]] = None, + help: t.Optional[str] = None, + epilog: t.Optional[str] = None, + short_help: t.Optional[str] = None, + options_metavar: t.Optional[str] = "[OPTIONS]", + add_help_option: bool = True, + no_args_is_help: bool = False, + hidden: bool = False, + deprecated: bool = False, + ) -> None: + super().__init__(name, context_settings) #: the callback to execute when the command fires. This might be #: `None` in which case nothing happens. self.callback = callback #: the list of parameters for this command in the order they #: should show up in the help page and execute. Eager parameters #: will automatically be handled before non eager ones. - self.params = params or [] + self.params: t.List["Parameter"] = params or [] + # if a form feed (page break) is found in the help text, truncate help # text to the content preceding the first form feed - if help and '\f' in help: - help = help.split('\f', 1)[0] + if help and "\f" in help: + help = help.split("\f", 1)[0] + self.help = help self.epilog = epilog self.options_metavar = options_metavar self.short_help = short_help self.add_help_option = add_help_option + self.no_args_is_help = no_args_is_help self.hidden = hidden self.deprecated = deprecated - def get_usage(self, ctx): + def to_info_dict(self, ctx: Context) -> t.Dict[str, t.Any]: + info_dict = super().to_info_dict(ctx) + info_dict.update( + params=[param.to_info_dict() for param in self.get_params(ctx)], + help=self.help, + epilog=self.epilog, + short_help=self.short_help, + hidden=self.hidden, + deprecated=self.deprecated, + ) + return info_dict + + def get_usage(self, ctx: Context) -> str: + """Formats the usage line into a string and returns it. + + Calls :meth:`format_usage` internally. + """ formatter = ctx.make_formatter() self.format_usage(ctx, formatter) - return formatter.getvalue().rstrip('\n') + return formatter.getvalue().rstrip("\n") - def get_params(self, ctx): + def get_params(self, ctx: Context) -> t.List["Parameter"]: rv = self.params help_option = self.get_help_option(ctx) + if help_option is not None: - rv = rv + [help_option] + rv = [*rv, help_option] + return rv - def format_usage(self, ctx, formatter): - """Writes the usage line into the formatter.""" - pieces = self.collect_usage_pieces(ctx) - formatter.write_usage(ctx.command_path, ' '.join(pieces)) + def format_usage(self, ctx: Context, formatter: HelpFormatter) -> None: + """Writes the usage line into the formatter. - def collect_usage_pieces(self, ctx): + This is a low-level method called by :meth:`get_usage`. + """ + pieces = self.collect_usage_pieces(ctx) + formatter.write_usage(ctx.command_path, " ".join(pieces)) + + def collect_usage_pieces(self, ctx: Context) -> t.List[str]: """Returns all the pieces that go into the usage line and returns it as a list of strings. """ - rv = [self.options_metavar] + rv = [self.options_metavar] if self.options_metavar else [] + for param in self.get_params(ctx): rv.extend(param.get_usage_pieces(ctx)) + return rv - def get_help_option_names(self, ctx): + def get_help_option_names(self, ctx: Context) -> t.List[str]: """Returns the names for the help option.""" all_names = set(ctx.help_option_names) for param in self.params: all_names.difference_update(param.opts) all_names.difference_update(param.secondary_opts) - return all_names + return list(all_names) - def get_help_option(self, ctx): + def get_help_option(self, ctx: Context) -> t.Optional["Option"]: """Returns the help option object.""" help_options = self.get_help_option_names(ctx) - if not help_options or not self.add_help_option: - return - def show_help(ctx, param, value): + if not help_options or not self.add_help_option: + return None + + def show_help(ctx: Context, param: "Parameter", value: str) -> None: if value and not ctx.resilient_parsing: echo(ctx.get_help(), color=ctx.color) ctx.exit() - return Option(help_options, is_flag=True, - is_eager=True, expose_value=False, - callback=show_help, - help='Show this message and exit.') - def make_parser(self, ctx): + return Option( + help_options, + is_flag=True, + is_eager=True, + expose_value=False, + callback=show_help, + help=_("Show this message and exit."), + ) + + def make_parser(self, ctx: Context) -> OptionParser: """Creates the underlying option parser for this command.""" parser = OptionParser(ctx) for param in self.get_params(ctx): param.add_to_parser(parser, ctx) return parser - def get_help(self, ctx): - """Formats the help into a string and returns it. This creates a - formatter and will call into the following formatting methods: + def get_help(self, ctx: Context) -> str: + """Formats the help into a string and returns it. + + Calls :meth:`format_help` internally. """ formatter = ctx.make_formatter() self.format_help(ctx, formatter) - return formatter.getvalue().rstrip('\n') + return formatter.getvalue().rstrip("\n") - def get_short_help_str(self, limit=45): - """Gets short help for the command or makes it by shortening the long help string.""" - return self.short_help or self.help and make_default_short_help(self.help, limit) or '' + def get_short_help_str(self, limit: int = 45) -> str: + """Gets short help for the command or makes it by shortening the + long help string. + """ + text = self.short_help or "" - def format_help(self, ctx, formatter): + if not text and self.help: + text = make_default_short_help(self.help, limit) + + if self.deprecated: + text = _("(Deprecated) {text}").format(text=text) + + return text.strip() + + def format_help(self, ctx: Context, formatter: HelpFormatter) -> None: """Writes the help into the formatter if it exists. - This calls into the following methods: + This is a low-level method called by :meth:`get_help`. + + This calls the following methods: - :meth:`format_usage` - :meth:`format_help_text` @@ -898,21 +1326,20 @@ class Command(BaseCommand): self.format_options(ctx, formatter) self.format_epilog(ctx, formatter) - def format_help_text(self, ctx, formatter): + def format_help_text(self, ctx: Context, formatter: HelpFormatter) -> None: """Writes the help text to the formatter if it exists.""" - if self.help: - formatter.write_paragraph() - with formatter.indentation(): - help_text = self.help - if self.deprecated: - help_text += DEPRECATED_HELP_NOTICE - formatter.write_text(help_text) - elif self.deprecated: - formatter.write_paragraph() - with formatter.indentation(): - formatter.write_text(DEPRECATED_HELP_NOTICE) + text = self.help or "" - def format_options(self, ctx, formatter): + if self.deprecated: + text = _("(Deprecated) {text}").format(text=text) + + if text: + formatter.write_paragraph() + + with formatter.indentation(): + formatter.write_text(text) + + def format_options(self, ctx: Context, formatter: HelpFormatter) -> None: """Writes all the options into the formatter if they exist.""" opts = [] for param in self.get_params(ctx): @@ -921,40 +1348,87 @@ class Command(BaseCommand): opts.append(rv) if opts: - with formatter.section('Options'): + with formatter.section(_("Options")): formatter.write_dl(opts) - def format_epilog(self, ctx, formatter): + def format_epilog(self, ctx: Context, formatter: HelpFormatter) -> None: """Writes the epilog into the formatter if it exists.""" if self.epilog: formatter.write_paragraph() with formatter.indentation(): formatter.write_text(self.epilog) - def parse_args(self, ctx, args): + def parse_args(self, ctx: Context, args: t.List[str]) -> t.List[str]: + if not args and self.no_args_is_help and not ctx.resilient_parsing: + echo(ctx.get_help(), color=ctx.color) + ctx.exit() + parser = self.make_parser(ctx) opts, args, param_order = parser.parse_args(args=args) - for param in iter_params_for_processing( - param_order, self.get_params(ctx)): + for param in iter_params_for_processing(param_order, self.get_params(ctx)): value, args = param.handle_parse_result(ctx, opts, args) if args and not ctx.allow_extra_args and not ctx.resilient_parsing: - ctx.fail('Got unexpected extra argument%s (%s)' - % (len(args) != 1 and 's' or '', - ' '.join(map(make_str, args)))) + ctx.fail( + ngettext( + "Got unexpected extra argument ({args})", + "Got unexpected extra arguments ({args})", + len(args), + ).format(args=" ".join(map(str, args))) + ) ctx.args = args return args - def invoke(self, ctx): + def invoke(self, ctx: Context) -> t.Any: """Given a context, this invokes the attached callback (if it exists) in the right way. """ - _maybe_show_deprecated_notice(self) + if self.deprecated: + message = _( + "DeprecationWarning: The command {name!r} is deprecated." + ).format(name=self.name) + echo(style(message, fg="red"), err=True) + if self.callback is not None: return ctx.invoke(self.callback, **ctx.params) + def shell_complete(self, ctx: Context, incomplete: str) -> t.List["CompletionItem"]: + """Return a list of completions for the incomplete value. Looks + at the names of options and chained multi-commands. + + :param ctx: Invocation context for this command. + :param incomplete: Value being completed. May be empty. + + .. versionadded:: 8.0 + """ + from click.shell_completion import CompletionItem + + results: t.List["CompletionItem"] = [] + + if incomplete and not incomplete[0].isalnum(): + for param in self.get_params(ctx): + if ( + not isinstance(param, Option) + or param.hidden + or ( + not param.multiple + and ctx.get_parameter_source(param.name) # type: ignore + is ParameterSource.COMMANDLINE + ) + ): + continue + + results.extend( + CompletionItem(name, help=param.help) + for name in [*param.opts, *param.secondary_opts] + if name.startswith(incomplete) + ) + + results.extend(super().shell_complete(ctx, incomplete)) + return results + class MultiCommand(Command): """A multi command is the basic implementation of a command that @@ -976,48 +1450,81 @@ class MultiCommand(Command): is enabled. This restricts the form of commands in that they cannot have optional arguments but it allows multiple commands to be chained together. - :param result_callback: the result callback to attach to this multi - command. + :param result_callback: The result callback to attach to this multi + command. This can be set or changed later with the + :meth:`result_callback` decorator. """ + allow_extra_args = True allow_interspersed_args = False - def __init__(self, name=None, invoke_without_command=False, - no_args_is_help=None, subcommand_metavar=None, - chain=False, result_callback=None, **attrs): - Command.__init__(self, name, **attrs) + def __init__( + self, + name: t.Optional[str] = None, + invoke_without_command: bool = False, + no_args_is_help: t.Optional[bool] = None, + subcommand_metavar: t.Optional[str] = None, + chain: bool = False, + result_callback: t.Optional[t.Callable[..., t.Any]] = None, + **attrs: t.Any, + ) -> None: + super().__init__(name, **attrs) + if no_args_is_help is None: no_args_is_help = not invoke_without_command + self.no_args_is_help = no_args_is_help self.invoke_without_command = invoke_without_command + if subcommand_metavar is None: if chain: - subcommand_metavar = SUBCOMMANDS_METAVAR + subcommand_metavar = "COMMAND1 [ARGS]... [COMMAND2 [ARGS]...]..." else: - subcommand_metavar = SUBCOMMAND_METAVAR + subcommand_metavar = "COMMAND [ARGS]..." + self.subcommand_metavar = subcommand_metavar self.chain = chain - #: The result callback that is stored. This can be set or - #: overridden with the :func:`resultcallback` decorator. - self.result_callback = result_callback + # The result callback that is stored. This can be set or + # overridden with the :func:`result_callback` decorator. + self._result_callback = result_callback if self.chain: for param in self.params: if isinstance(param, Argument) and not param.required: - raise RuntimeError('Multi commands in chain mode cannot ' - 'have optional arguments.') + raise RuntimeError( + "Multi commands in chain mode cannot have" + " optional arguments." + ) - def collect_usage_pieces(self, ctx): - rv = Command.collect_usage_pieces(self, ctx) + def to_info_dict(self, ctx: Context) -> t.Dict[str, t.Any]: + info_dict = super().to_info_dict(ctx) + commands = {} + + for name in self.list_commands(ctx): + command = self.get_command(ctx, name) + + if command is None: + continue + + sub_ctx = ctx._make_sub_context(command) + + with sub_ctx.scope(cleanup=False): + commands[name] = command.to_info_dict(sub_ctx) + + info_dict.update(commands=commands, chain=self.chain) + return info_dict + + def collect_usage_pieces(self, ctx: Context) -> t.List[str]: + rv = super().collect_usage_pieces(ctx) rv.append(self.subcommand_metavar) return rv - def format_options(self, ctx, formatter): - Command.format_options(self, ctx, formatter) + def format_options(self, ctx: Context, formatter: HelpFormatter) -> None: + super().format_options(ctx, formatter) self.format_commands(ctx, formatter) - def resultcallback(self, replace=False): - """Adds a result callback to the chain command. By default if a + def result_callback(self, replace: bool = False) -> t.Callable[[F], F]: + """Adds a result callback to the command. By default if a result callback is already registered this will chain them but this can be disabled with the `replace` parameter. The result callback is invoked with the return value of the subcommand @@ -1032,28 +1539,47 @@ class MultiCommand(Command): def cli(input): return 42 - @cli.resultcallback() + @cli.result_callback() def process_result(result, input): return result + input - .. versionadded:: 3.0 - :param replace: if set to `True` an already existing result callback will be removed. + + .. versionchanged:: 8.0 + Renamed from ``resultcallback``. + + .. versionadded:: 3.0 """ - def decorator(f): - old_callback = self.result_callback + + def decorator(f: F) -> F: + old_callback = self._result_callback + if old_callback is None or replace: - self.result_callback = f + self._result_callback = f return f - def function(__value, *args, **kwargs): - return f(old_callback(__value, *args, **kwargs), - *args, **kwargs) - self.result_callback = rv = update_wrapper(function, f) + + def function(__value, *args, **kwargs): # type: ignore + inner = old_callback(__value, *args, **kwargs) # type: ignore + return f(inner, *args, **kwargs) + + self._result_callback = rv = update_wrapper(t.cast(F, function), f) return rv + return decorator - def format_commands(self, ctx, formatter): + def resultcallback(self, replace: bool = False) -> t.Callable[[F], F]: + import warnings + + warnings.warn( + "'resultcallback' has been renamed to 'result_callback'." + " The old name will be removed in Click 8.1.", + DeprecationWarning, + stacklevel=2, + ) + return self.result_callback(replace=replace) + + def format_commands(self, ctx: Context, formatter: HelpFormatter) -> None: """Extra format methods for multi methods that adds all the commands after the options. """ @@ -1078,15 +1604,16 @@ class MultiCommand(Command): rows.append((subcommand, help)) if rows: - with formatter.section('Commands'): + with formatter.section(_("Commands")): formatter.write_dl(rows) - def parse_args(self, ctx, args): + def parse_args(self, ctx: Context, args: t.List[str]) -> t.List[str]: if not args and self.no_args_is_help and not ctx.resilient_parsing: echo(ctx.get_help(), color=ctx.color) ctx.exit() - rest = Command.parse_args(self, ctx, args) + rest = super().parse_args(ctx, args) + if self.chain: ctx.protected_args = rest ctx.args = [] @@ -1095,30 +1622,24 @@ class MultiCommand(Command): return ctx.args - def invoke(self, ctx): - def _process_result(value): - if self.result_callback is not None: - value = ctx.invoke(self.result_callback, value, - **ctx.params) + def invoke(self, ctx: Context) -> t.Any: + def _process_result(value: t.Any) -> t.Any: + if self._result_callback is not None: + value = ctx.invoke(self._result_callback, value, **ctx.params) return value if not ctx.protected_args: - # If we are invoked without command the chain flag controls - # how this happens. If we are not in chain mode, the return - # value here is the return value of the command. - # If however we are in chain mode, the return value is the - # return value of the result processor invoked with an empty - # list (which means that no subcommand actually was executed). if self.invoke_without_command: - if not self.chain: - return Command.invoke(self, ctx) + # No subcommand was invoked, so the result callback is + # invoked with None for regular groups, or an empty list + # for chained groups. with ctx: - Command.invoke(self, ctx) - return _process_result([]) - ctx.fail('Missing command.') + super().invoke(ctx) + return _process_result([] if self.chain else None) + ctx.fail(_("Missing command.")) # Fetch args back out - args = ctx.protected_args + ctx.args + args = [*ctx.protected_args, *ctx.args] ctx.args = [] ctx.protected_args = [] @@ -1130,8 +1651,9 @@ class MultiCommand(Command): # resources until the result processor has worked. with ctx: cmd_name, cmd, args = self.resolve_command(ctx, args) + assert cmd is not None ctx.invoked_subcommand = cmd_name - Command.invoke(self, ctx) + super().invoke(ctx) sub_ctx = cmd.make_context(cmd_name, args, parent=ctx) with sub_ctx: return _process_result(sub_ctx.command.invoke(sub_ctx)) @@ -1142,8 +1664,8 @@ class MultiCommand(Command): # set to ``*`` to inform the command that subcommands are executed # but nothing else. with ctx: - ctx.invoked_subcommand = args and '*' or None - Command.invoke(self, ctx) + ctx.invoked_subcommand = "*" if args else None + super().invoke(ctx) # Otherwise we make every single context and invoke them in a # chain. In that case the return value to the result processor @@ -1151,9 +1673,14 @@ class MultiCommand(Command): contexts = [] while args: cmd_name, cmd, args = self.resolve_command(ctx, args) - sub_ctx = cmd.make_context(cmd_name, args, parent=ctx, - allow_extra_args=True, - allow_interspersed_args=False) + assert cmd is not None + sub_ctx = cmd.make_context( + cmd_name, + args, + parent=ctx, + allow_extra_args=True, + allow_interspersed_args=False, + ) contexts.append(sub_ctx) args, sub_ctx.args = sub_ctx.args, [] @@ -1163,7 +1690,9 @@ class MultiCommand(Command): rv.append(sub_ctx.command.invoke(sub_ctx)) return _process_result(rv) - def resolve_command(self, ctx, args): + def resolve_command( + self, ctx: Context, args: t.List[str] + ) -> t.Tuple[t.Optional[str], t.Optional[Command], t.List[str]]: cmd_name = make_str(args[0]) original_cmd_name = cmd_name @@ -1185,73 +1714,162 @@ class MultiCommand(Command): if cmd is None and not ctx.resilient_parsing: if split_opt(cmd_name)[0]: self.parse_args(ctx, ctx.args) - ctx.fail('No such command "%s".' % original_cmd_name) + ctx.fail(_("No such command {name!r}.").format(name=original_cmd_name)) + return cmd_name if cmd else None, cmd, args[1:] - return cmd_name, cmd, args[1:] - - def get_command(self, ctx, cmd_name): + def get_command(self, ctx: Context, cmd_name: str) -> t.Optional[Command]: """Given a context and a command name, this returns a :class:`Command` object if it exists or returns `None`. """ - raise NotImplementedError() + raise NotImplementedError - def list_commands(self, ctx): + def list_commands(self, ctx: Context) -> t.List[str]: """Returns a list of subcommand names in the order they should appear. """ return [] + def shell_complete(self, ctx: Context, incomplete: str) -> t.List["CompletionItem"]: + """Return a list of completions for the incomplete value. Looks + at the names of options, subcommands, and chained + multi-commands. + + :param ctx: Invocation context for this command. + :param incomplete: Value being completed. May be empty. + + .. versionadded:: 8.0 + """ + from click.shell_completion import CompletionItem + + results = [ + CompletionItem(name, help=command.get_short_help_str()) + for name, command in _complete_visible_commands(ctx, incomplete) + ] + results.extend(super().shell_complete(ctx, incomplete)) + return results + class Group(MultiCommand): - """A group allows a command to have subcommands attached. This is the - most common way to implement nesting in Click. + """A group allows a command to have subcommands attached. This is + the most common way to implement nesting in Click. - :param commands: a dictionary of commands. + :param name: The name of the group command. + :param commands: A dict mapping names to :class:`Command` objects. + Can also be a list of :class:`Command`, which will use + :attr:`Command.name` to create the dict. + :param attrs: Other command arguments described in + :class:`MultiCommand`, :class:`Command`, and + :class:`BaseCommand`. + + .. versionchanged:: 8.0 + The ``commmands`` argument can be a list of command objects. """ - def __init__(self, name=None, commands=None, **attrs): - MultiCommand.__init__(self, name, **attrs) - #: the registered subcommands by their exported names. - self.commands = commands or {} + #: If set, this is used by the group's :meth:`command` decorator + #: as the default :class:`Command` class. This is useful to make all + #: subcommands use a custom command class. + #: + #: .. versionadded:: 8.0 + command_class: t.Optional[t.Type[Command]] = None - def add_command(self, cmd, name=None): + #: If set, this is used by the group's :meth:`group` decorator + #: as the default :class:`Group` class. This is useful to make all + #: subgroups use a custom group class. + #: + #: If set to the special value :class:`type` (literally + #: ``group_class = type``), this group's class will be used as the + #: default class. This makes a custom group class continue to make + #: custom groups. + #: + #: .. versionadded:: 8.0 + group_class: t.Optional[t.Union[t.Type["Group"], t.Type[type]]] = None + # Literal[type] isn't valid, so use Type[type] + + def __init__( + self, + name: t.Optional[str] = None, + commands: t.Optional[t.Union[t.Dict[str, Command], t.Sequence[Command]]] = None, + **attrs: t.Any, + ) -> None: + super().__init__(name, **attrs) + + if commands is None: + commands = {} + elif isinstance(commands, abc.Sequence): + commands = {c.name: c for c in commands if c.name is not None} + + #: The registered subcommands by their exported names. + self.commands: t.Dict[str, Command] = commands + + def add_command(self, cmd: Command, name: t.Optional[str] = None) -> None: """Registers another :class:`Command` with this group. If the name is not provided, the name of the command is used. """ name = name or cmd.name if name is None: - raise TypeError('Command has no name.') + raise TypeError("Command has no name.") _check_multicommand(self, name, cmd, register=True) self.commands[name] = cmd - def command(self, *args, **kwargs): + def command( + self, *args: t.Any, **kwargs: t.Any + ) -> t.Callable[[t.Callable[..., t.Any]], Command]: """A shortcut decorator for declaring and attaching a command to - the group. This takes the same arguments as :func:`command` but - immediately registers the created command with this instance by - calling into :meth:`add_command`. + the group. This takes the same arguments as :func:`command` and + immediately registers the created command with this group by + calling :meth:`add_command`. + + To customize the command class used, set the + :attr:`command_class` attribute. + + .. versionchanged:: 8.0 + Added the :attr:`command_class` attribute. """ - def decorator(f): + from .decorators import command + + if self.command_class is not None and "cls" not in kwargs: + kwargs["cls"] = self.command_class + + def decorator(f: t.Callable[..., t.Any]) -> Command: cmd = command(*args, **kwargs)(f) self.add_command(cmd) return cmd + return decorator - def group(self, *args, **kwargs): + def group( + self, *args: t.Any, **kwargs: t.Any + ) -> t.Callable[[t.Callable[..., t.Any]], "Group"]: """A shortcut decorator for declaring and attaching a group to - the group. This takes the same arguments as :func:`group` but - immediately registers the created command with this instance by - calling into :meth:`add_command`. + the group. This takes the same arguments as :func:`group` and + immediately registers the created group with this group by + calling :meth:`add_command`. + + To customize the group class used, set the :attr:`group_class` + attribute. + + .. versionchanged:: 8.0 + Added the :attr:`group_class` attribute. """ - def decorator(f): + from .decorators import group + + if self.group_class is not None and "cls" not in kwargs: + if self.group_class is type: + kwargs["cls"] = type(self) + else: + kwargs["cls"] = self.group_class + + def decorator(f: t.Callable[..., t.Any]) -> "Group": cmd = group(*args, **kwargs)(f) self.add_command(cmd) return cmd + return decorator - def get_command(self, ctx, cmd_name): + def get_command(self, ctx: Context, cmd_name: str) -> t.Optional[Command]: return self.commands.get(cmd_name) - def list_commands(self, ctx): + def list_commands(self, ctx: Context) -> t.List[str]: return sorted(self.commands) @@ -1262,31 +1880,52 @@ class CommandCollection(MultiCommand): provides all the commands for each of them. """ - def __init__(self, name=None, sources=None, **attrs): - MultiCommand.__init__(self, name, **attrs) + def __init__( + self, + name: t.Optional[str] = None, + sources: t.Optional[t.List[MultiCommand]] = None, + **attrs: t.Any, + ) -> None: + super().__init__(name, **attrs) #: The list of registered multi commands. - self.sources = sources or [] + self.sources: t.List[MultiCommand] = sources or [] - def add_source(self, multi_cmd): + def add_source(self, multi_cmd: MultiCommand) -> None: """Adds a new multi command to the chain dispatcher.""" self.sources.append(multi_cmd) - def get_command(self, ctx, cmd_name): + def get_command(self, ctx: Context, cmd_name: str) -> t.Optional[Command]: for source in self.sources: rv = source.get_command(ctx, cmd_name) + if rv is not None: if self.chain: _check_multicommand(self, cmd_name, rv) + return rv - def list_commands(self, ctx): - rv = set() + return None + + def list_commands(self, ctx: Context) -> t.List[str]: + rv: t.Set[str] = set() + for source in self.sources: rv.update(source.list_commands(ctx)) + return sorted(rv) -class Parameter(object): +def _check_iter(value: t.Any) -> t.Iterator[t.Any]: + """Check if the value is iterable but not a string. Raises a type + error, or return an iterator over the value. + """ + if isinstance(value, str): + raise TypeError + + return iter(value) + + +class Parameter: r"""A parameter to a command comes in two versions: they are either :class:`Option`\s or :class:`Argument`\s. Other subclasses are currently not supported by design as some of the internals for parsing are @@ -1294,12 +1933,6 @@ class Parameter(object): Some settings are supported by both options and arguments. - .. versionchanged:: 2.0 - Changed signature for parameter callback to also be passed the - parameter. In Click 2.0, the old callback format will still work, - but it will raise a warning to give you change to migrate the - code easier. - :param param_decls: the parameter declarations for this option or argument. This is a list of flags or argument names. @@ -1310,14 +1943,15 @@ class Parameter(object): :param default: the default value if omitted. This can also be a callable, in which case it's invoked when the default is needed without any arguments. - :param callback: a callback that should be executed after the parameter - was matched. This is called as ``fn(ctx, param, - value)`` and needs to return the value. Before Click - 2.0, the signature was ``(ctx, value)``. + :param callback: A function to further process or validate the value + after type conversion. It is called as ``f(ctx, param, value)`` + and must return the value. It is called for all sources, + including prompts. :param nargs: the number of arguments to match. If not ``1`` the return value is a tuple instead of single value. The default for nargs is ``1`` (except if the type is a tuple, then it's - the arity of the tuple). + the arity of the tuple). If ``nargs=-1``, all remaining + parameters are collected. :param metavar: how the value is represented in the help page. :param expose_value: if this is `True` then the value is passed onwards to the command callback and stored on the context, @@ -1327,17 +1961,75 @@ class Parameter(object): order of processing. :param envvar: a string or list of strings that are environment variables that should be checked. + :param shell_complete: A function that returns custom shell + completions. Used instead of the param's type completion if + given. Takes ``ctx, param, incomplete`` and must return a list + of :class:`~click.shell_completion.CompletionItem` or a list of + strings. + + .. versionchanged:: 8.0 + ``process_value`` validates required parameters and bounded + ``nargs``, and invokes the parameter callback before returning + the value. This allows the callback to validate prompts. + ``full_process_value`` is removed. + + .. versionchanged:: 8.0 + ``autocompletion`` is renamed to ``shell_complete`` and has new + semantics described above. The old name is deprecated and will + be removed in 8.1, until then it will be wrapped to match the + new requirements. + + .. versionchanged:: 8.0 + For ``multiple=True, nargs>1``, the default must be a list of + tuples. + + .. versionchanged:: 8.0 + Setting a default is no longer required for ``nargs>1``, it will + default to ``None``. ``multiple=True`` or ``nargs=-1`` will + default to ``()``. + + .. versionchanged:: 7.1 + Empty environment variables are ignored rather than taking the + empty string value. This makes it possible for scripts to clear + variables if they can't unset them. + + .. versionchanged:: 2.0 + Changed signature for parameter callback to also be passed the + parameter. The old callback format will still work, but it will + raise a warning to give you a chance to migrate the code easier. """ - param_type_name = 'parameter' - def __init__(self, param_decls=None, type=None, required=False, - default=None, callback=None, nargs=None, metavar=None, - expose_value=True, is_eager=False, envvar=None, - autocompletion=None): - self.name, self.opts, self.secondary_opts = \ - self._parse_decls(param_decls or (), expose_value) + param_type_name = "parameter" - self.type = convert_type(type, default) + def __init__( + self, + param_decls: t.Optional[t.Sequence[str]] = None, + type: t.Optional[t.Union[types.ParamType, t.Any]] = None, + required: bool = False, + default: t.Optional[t.Union[t.Any, t.Callable[[], t.Any]]] = None, + callback: t.Optional[t.Callable[[Context, "Parameter", t.Any], t.Any]] = None, + nargs: t.Optional[int] = None, + multiple: bool = False, + metavar: t.Optional[str] = None, + expose_value: bool = True, + is_eager: bool = False, + envvar: t.Optional[t.Union[str, t.Sequence[str]]] = None, + shell_complete: t.Optional[ + t.Callable[ + [Context, "Parameter", str], + t.Union[t.List["CompletionItem"], t.List[str]], + ] + ] = None, + autocompletion: t.Optional[ + t.Callable[ + [Context, t.List[str], str], t.List[t.Union[t.Tuple[str, str], str]] + ] + ] = None, + ) -> None: + self.name, self.opts, self.secondary_opts = self._parse_decls( + param_decls or (), expose_value + ) + self.type = types.convert_type(type, default) # Default nargs to what the type tells us if we have that # information available. @@ -1350,151 +2042,355 @@ class Parameter(object): self.required = required self.callback = callback self.nargs = nargs - self.multiple = False + self.multiple = multiple self.expose_value = expose_value self.default = default self.is_eager = is_eager self.metavar = metavar self.envvar = envvar - self.autocompletion = autocompletion + + if autocompletion is not None: + import warnings + + warnings.warn( + "'autocompletion' is renamed to 'shell_complete'. The old name is" + " deprecated and will be removed in Click 8.1. See the docs about" + " 'Parameter' for information about new behavior.", + DeprecationWarning, + stacklevel=2, + ) + + def shell_complete( + ctx: Context, param: "Parameter", incomplete: str + ) -> t.List["CompletionItem"]: + from click.shell_completion import CompletionItem + + out = [] + + for c in autocompletion(ctx, [], incomplete): # type: ignore + if isinstance(c, tuple): + c = CompletionItem(c[0], help=c[1]) + elif isinstance(c, str): + c = CompletionItem(c) + + if c.value.startswith(incomplete): + out.append(c) + + return out + + self._custom_shell_complete = shell_complete + + if __debug__: + if self.type.is_composite and nargs != self.type.arity: + raise ValueError( + f"'nargs' must be {self.type.arity} (or None) for" + f" type {self.type!r}, but it was {nargs}." + ) + + # Skip no default or callable default. + check_default = default if not callable(default) else None + + if check_default is not None: + if multiple: + try: + # Only check the first value against nargs. + check_default = next(_check_iter(check_default), None) + except TypeError: + raise ValueError( + "'default' must be a list when 'multiple' is true." + ) from None + + # Can be None for multiple with empty default. + if nargs != 1 and check_default is not None: + try: + _check_iter(check_default) + except TypeError: + if multiple: + message = ( + "'default' must be a list of lists when 'multiple' is" + " true and 'nargs' != 1." + ) + else: + message = "'default' must be a list when 'nargs' != 1." + + raise ValueError(message) from None + + if nargs > 1 and len(check_default) != nargs: + subject = "item length" if multiple else "length" + raise ValueError( + f"'default' {subject} must match nargs={nargs}." + ) + + def to_info_dict(self) -> t.Dict[str, t.Any]: + """Gather information that could be useful for a tool generating + user-facing documentation. + + Use :meth:`click.Context.to_info_dict` to traverse the entire + CLI structure. + + .. versionadded:: 8.0 + """ + return { + "name": self.name, + "param_type_name": self.param_type_name, + "opts": self.opts, + "secondary_opts": self.secondary_opts, + "type": self.type.to_info_dict(), + "required": self.required, + "nargs": self.nargs, + "multiple": self.multiple, + "default": self.default, + "envvar": self.envvar, + } + + def __repr__(self) -> str: + return f"<{self.__class__.__name__} {self.name}>" + + def _parse_decls( + self, decls: t.Sequence[str], expose_value: bool + ) -> t.Tuple[t.Optional[str], t.List[str], t.List[str]]: + raise NotImplementedError() @property - def human_readable_name(self): + def human_readable_name(self) -> str: """Returns the human readable name of this parameter. This is the same as the name for options, but the metavar for arguments. """ - return self.name + return self.name # type: ignore - def make_metavar(self): + def make_metavar(self) -> str: if self.metavar is not None: return self.metavar + metavar = self.type.get_metavar(self) + if metavar is None: metavar = self.type.name.upper() + if self.nargs != 1: - metavar += '...' + metavar += "..." + return metavar - def get_default(self, ctx): - """Given a context variable this calculates the default value.""" - # Otherwise go with the regular default. - if callable(self.default): - rv = self.default() - else: - rv = self.default - return self.type_cast_value(ctx, rv) + @typing.overload + def get_default( + self, ctx: Context, call: "te.Literal[True]" = True + ) -> t.Optional[t.Any]: + ... - def add_to_parser(self, parser, ctx): - pass + @typing.overload + def get_default( + self, ctx: Context, call: bool = ... + ) -> t.Optional[t.Union[t.Any, t.Callable[[], t.Any]]]: + ... + + def get_default( + self, ctx: Context, call: bool = True + ) -> t.Optional[t.Union[t.Any, t.Callable[[], t.Any]]]: + """Get the default for the parameter. Tries + :meth:`Context.lookup_default` first, then the local default. + + :param ctx: Current context. + :param call: If the default is a callable, call it. Disable to + return the callable instead. + + .. versionchanged:: 8.0.2 + Type casting is no longer performed when getting a default. + + .. versionchanged:: 8.0.1 + Type casting can fail in resilient parsing mode. Invalid + defaults will not prevent showing help text. + + .. versionchanged:: 8.0 + Looks at ``ctx.default_map`` first. + + .. versionchanged:: 8.0 + Added the ``call`` parameter. + """ + value = ctx.lookup_default(self.name, call=False) # type: ignore - def consume_value(self, ctx, opts): - value = opts.get(self.name) if value is None: - value = self.value_from_envvar(ctx) - if value is None: - value = ctx.lookup_default(self.name) + value = self.default + + if call and callable(value): + value = value() + return value - def type_cast_value(self, ctx, value): - """Given a value this runs it properly through the type system. - This automatically handles things like `nargs` and `multiple` as - well as composite types. + def add_to_parser(self, parser: OptionParser, ctx: Context) -> None: + raise NotImplementedError() + + def consume_value( + self, ctx: Context, opts: t.Mapping[str, t.Any] + ) -> t.Tuple[t.Any, ParameterSource]: + value = opts.get(self.name) # type: ignore + source = ParameterSource.COMMANDLINE + + if value is None: + value = self.value_from_envvar(ctx) + source = ParameterSource.ENVIRONMENT + + if value is None: + value = ctx.lookup_default(self.name) # type: ignore + source = ParameterSource.DEFAULT_MAP + + if value is None: + value = self.get_default(ctx) + source = ParameterSource.DEFAULT + + return value, source + + def type_cast_value(self, ctx: Context, value: t.Any) -> t.Any: + """Convert and validate a value against the option's + :attr:`type`, :attr:`multiple`, and :attr:`nargs`. """ - if self.type.is_composite: - if self.nargs <= 1: - raise TypeError('Attempted to invoke composite type ' - 'but nargs has been set to %s. This is ' - 'not supported; nargs needs to be set to ' - 'a fixed value > 1.' % self.nargs) - if self.multiple: - return tuple(self.type(x or (), self, ctx) for x in value or ()) - return self.type(value or (), self, ctx) + if value is None: + return () if self.multiple or self.nargs == -1 else None - def _convert(value, level): - if level == 0: - return self.type(value, self, ctx) - return tuple(_convert(x, level - 1) for x in value or ()) - return _convert(value, (self.nargs != 1) + bool(self.multiple)) + def check_iter(value: t.Any) -> t.Iterator: + try: + return _check_iter(value) + except TypeError: + # This should only happen when passing in args manually, + # the parser should construct an iterable when parsing + # the command line. + raise BadParameter( + _("Value must be an iterable."), ctx=ctx, param=self + ) from None - def process_value(self, ctx, value): - """Given a value and context this runs the logic to convert the - value as necessary. - """ - # If the value we were given is None we do nothing. This way - # code that calls this can easily figure out if something was - # not provided. Otherwise it would be converted into an empty - # tuple for multiple invocations which is inconvenient. - if value is not None: - return self.type_cast_value(ctx, value) + if self.nargs == 1 or self.type.is_composite: + convert: t.Callable[[t.Any], t.Any] = partial( + self.type, param=self, ctx=ctx + ) + elif self.nargs == -1: - def value_is_missing(self, value): + def convert(value: t.Any) -> t.Tuple: + return tuple(self.type(x, self, ctx) for x in check_iter(value)) + + else: # nargs > 1 + + def convert(value: t.Any) -> t.Tuple: + value = tuple(check_iter(value)) + + if len(value) != self.nargs: + raise BadParameter( + ngettext( + "Takes {nargs} values but 1 was given.", + "Takes {nargs} values but {len} were given.", + len(value), + ).format(nargs=self.nargs, len=len(value)), + ctx=ctx, + param=self, + ) + + return tuple(self.type(x, self, ctx) for x in value) + + if self.multiple: + return tuple(convert(x) for x in check_iter(value)) + + return convert(value) + + def value_is_missing(self, value: t.Any) -> bool: if value is None: return True + if (self.nargs != 1 or self.multiple) and value == (): return True + return False - def full_process_value(self, ctx, value): - value = self.process_value(ctx, value) - - if value is None and not ctx.resilient_parsing: - value = self.get_default(ctx) + def process_value(self, ctx: Context, value: t.Any) -> t.Any: + value = self.type_cast_value(ctx, value) if self.required and self.value_is_missing(value): raise MissingParameter(ctx=ctx, param=self) + if self.callback is not None: + value = self.callback(ctx, self, value) + return value - def resolve_envvar_value(self, ctx): + def resolve_envvar_value(self, ctx: Context) -> t.Optional[str]: if self.envvar is None: - return - if isinstance(self.envvar, (tuple, list)): + return None + + if isinstance(self.envvar, str): + rv = os.environ.get(self.envvar) + + if rv: + return rv + else: for envvar in self.envvar: rv = os.environ.get(envvar) - if rv is not None: - return rv - else: - return os.environ.get(self.envvar) - def value_from_envvar(self, ctx): - rv = self.resolve_envvar_value(ctx) + if rv: + return rv + + return None + + def value_from_envvar(self, ctx: Context) -> t.Optional[t.Any]: + rv: t.Optional[t.Any] = self.resolve_envvar_value(ctx) + if rv is not None and self.nargs != 1: rv = self.type.split_envvar_value(rv) + return rv - def handle_parse_result(self, ctx, opts, args): + def handle_parse_result( + self, ctx: Context, opts: t.Mapping[str, t.Any], args: t.List[str] + ) -> t.Tuple[t.Any, t.List[str]]: with augment_usage_errors(ctx, param=self): - value = self.consume_value(ctx, opts) + value, source = self.consume_value(ctx, opts) + ctx.set_parameter_source(self.name, source) # type: ignore + try: - value = self.full_process_value(ctx, value) + value = self.process_value(ctx, value) except Exception: if not ctx.resilient_parsing: raise + value = None - if self.callback is not None: - try: - value = invoke_param_callback( - self.callback, ctx, self, value) - except Exception: - if not ctx.resilient_parsing: - raise if self.expose_value: - ctx.params[self.name] = value + ctx.params[self.name] = value # type: ignore + return value, args - def get_help_record(self, ctx): + def get_help_record(self, ctx: Context) -> t.Optional[t.Tuple[str, str]]: pass - def get_usage_pieces(self, ctx): + def get_usage_pieces(self, ctx: Context) -> t.List[str]: return [] - def get_error_hint(self, ctx): + def get_error_hint(self, ctx: Context) -> str: """Get a stringified version of the param for use in error messages to indicate which param caused the error. """ hint_list = self.opts or [self.human_readable_name] - return ' / '.join('"%s"' % x for x in hint_list) + return " / ".join(f"'{x}'" for x in hint_list) + + def shell_complete(self, ctx: Context, incomplete: str) -> t.List["CompletionItem"]: + """Return a list of completions for the incomplete value. If a + ``shell_complete`` function was given during init, it is used. + Otherwise, the :attr:`type` + :meth:`~click.types.ParamType.shell_complete` function is used. + + :param ctx: Invocation context for this command. + :param incomplete: Value being completed. May be empty. + + .. versionadded:: 8.0 + """ + if self._custom_shell_complete is not None: + results = self._custom_shell_complete(ctx, self, incomplete) + + if results and isinstance(results[0], str): + from click.shell_completion import CompletionItem + + results = [CompletionItem(c) for c in results] + + return t.cast(t.List["CompletionItem"], results) + + return self.type.shell_complete(ctx, self, incomplete) class Option(Parameter): @@ -1513,8 +2409,12 @@ class Option(Parameter): :param prompt: if set to `True` or a non empty string then the user will be prompted for input. If set to `True` the prompt will be the option name capitalized. - :param confirmation_prompt: if set then the value will need to be confirmed - if it was prompted for. + :param confirmation_prompt: Prompt a second time to confirm the + value if it was prompted for. Can be set to a string instead of + ``True`` to customize the message. + :param prompt_required: If set to ``False``, the user will be + prompted for input only when the option was specified as a flag + without a value. :param hide_input: if this is `True` then the input on the prompt will be hidden from the user. This is useful for password input. @@ -1534,96 +2434,149 @@ class Option(Parameter): context. :param help: the help string. :param hidden: hide this option from help outputs. - """ - param_type_name = 'option' - def __init__(self, param_decls=None, show_default=False, - prompt=False, confirmation_prompt=False, - hide_input=False, is_flag=None, flag_value=None, - multiple=False, count=False, allow_from_autoenv=True, - type=None, help=None, hidden=False, show_choices=True, - show_envvar=False, **attrs): - default_is_missing = attrs.get('default', _missing) is _missing - Parameter.__init__(self, param_decls, type=type, **attrs) + .. versionchanged:: 8.0.1 + ``type`` is detected from ``flag_value`` if given. + """ + + param_type_name = "option" + + def __init__( + self, + param_decls: t.Optional[t.Sequence[str]] = None, + show_default: t.Union[bool, str] = False, + prompt: t.Union[bool, str] = False, + confirmation_prompt: t.Union[bool, str] = False, + prompt_required: bool = True, + hide_input: bool = False, + is_flag: t.Optional[bool] = None, + flag_value: t.Optional[t.Any] = None, + multiple: bool = False, + count: bool = False, + allow_from_autoenv: bool = True, + type: t.Optional[t.Union[types.ParamType, t.Any]] = None, + help: t.Optional[str] = None, + hidden: bool = False, + show_choices: bool = True, + show_envvar: bool = False, + **attrs: t.Any, + ) -> None: + default_is_missing = "default" not in attrs + super().__init__(param_decls, type=type, multiple=multiple, **attrs) if prompt is True: - prompt_text = self.name.replace('_', ' ').capitalize() + if self.name is None: + raise TypeError("'name' is required with 'prompt=True'.") + + prompt_text: t.Optional[str] = self.name.replace("_", " ").capitalize() elif prompt is False: prompt_text = None else: - prompt_text = prompt + prompt_text = t.cast(str, prompt) + self.prompt = prompt_text self.confirmation_prompt = confirmation_prompt + self.prompt_required = prompt_required self.hide_input = hide_input self.hidden = hidden - # Flags + # If prompt is enabled but not required, then the option can be + # used as a flag to indicate using prompt or flag_value. + self._flag_needs_value = self.prompt is not None and not self.prompt_required + if is_flag is None: if flag_value is not None: + # Implicitly a flag because flag_value was set. is_flag = True + elif self._flag_needs_value: + # Not a flag, but when used as a flag it shows a prompt. + is_flag = False else: + # Implicitly a flag because flag options were given. is_flag = bool(self.secondary_opts) + elif is_flag is False and not self._flag_needs_value: + # Not a flag, and prompt is not enabled, can be used as a + # flag if flag_value is set. + self._flag_needs_value = flag_value is not None + if is_flag and default_is_missing: - self.default = False + self.default: t.Union[t.Any, t.Callable[[], t.Any]] = False + if flag_value is None: flag_value = not self.default - self.is_flag = is_flag - self.flag_value = flag_value - if self.is_flag and isinstance(self.flag_value, bool) \ - and type is None: - self.type = BOOL - self.is_bool_flag = True - else: - self.is_bool_flag = False + + if is_flag and type is None: + # Re-guess the type from the flag value instead of the + # default. + self.type = types.convert_type(None, flag_value) + + self.is_flag: bool = is_flag + self.is_bool_flag = is_flag and isinstance(self.type, types.BoolParamType) + self.flag_value: t.Any = flag_value # Counting self.count = count if count: if type is None: - self.type = IntRange(min=0) + self.type = types.IntRange(min=0) if default_is_missing: self.default = 0 - self.multiple = multiple self.allow_from_autoenv = allow_from_autoenv self.help = help self.show_default = show_default self.show_choices = show_choices self.show_envvar = show_envvar - # Sanity check for stuff we don't support if __debug__: - if self.nargs < 0: - raise TypeError('Options cannot have nargs < 0') + if self.nargs == -1: + raise TypeError("nargs=-1 is not supported for options.") + if self.prompt and self.is_flag and not self.is_bool_flag: - raise TypeError('Cannot prompt for flags that are not bools.') + raise TypeError("'prompt' is not valid for non-boolean flag.") + if not self.is_bool_flag and self.secondary_opts: - raise TypeError('Got secondary option for non boolean flag.') - if self.is_bool_flag and self.hide_input \ - and self.prompt is not None: - raise TypeError('Hidden input does not work with boolean ' - 'flag prompts.') + raise TypeError("Secondary flag is not valid for non-boolean flag.") + + if self.is_bool_flag and self.hide_input and self.prompt is not None: + raise TypeError( + "'prompt' with 'hide_input' is not valid for boolean flag." + ) + if self.count: if self.multiple: - raise TypeError('Options cannot be multiple and count ' - 'at the same time.') - elif self.is_flag: - raise TypeError('Options cannot be count and flags at ' - 'the same time.') + raise TypeError("'count' is not valid with 'multiple'.") - def _parse_decls(self, decls, expose_value): + if self.is_flag: + raise TypeError("'count' is not valid with 'is_flag'.") + + def to_info_dict(self) -> t.Dict[str, t.Any]: + info_dict = super().to_info_dict() + info_dict.update( + help=self.help, + prompt=self.prompt, + is_flag=self.is_flag, + flag_value=self.flag_value, + count=self.count, + hidden=self.hidden, + ) + return info_dict + + def _parse_decls( + self, decls: t.Sequence[str], expose_value: bool + ) -> t.Tuple[t.Optional[str], t.List[str], t.List[str]]: opts = [] secondary_opts = [] name = None possible_names = [] for decl in decls: - if isidentifier(decl): + if decl.isidentifier(): if name is not None: - raise TypeError('Name defined twice') + raise TypeError(f"Name '{name}' defined twice") name = decl else: - split_char = decl[:1] == '/' and ';' or '/' + split_char = ";" if decl[:1] == "/" else "/" if split_char in decl: first, second = decl.split(split_char, 1) first = first.rstrip() @@ -1633,123 +2586,209 @@ class Option(Parameter): second = second.lstrip() if second: secondary_opts.append(second.lstrip()) + if first == second: + raise ValueError( + f"Boolean option {decl!r} cannot use the" + " same flag for true/false." + ) else: possible_names.append(split_opt(decl)) opts.append(decl) if name is None and possible_names: possible_names.sort(key=lambda x: -len(x[0])) # group long options first - name = possible_names[0][1].replace('-', '_').lower() - if not isidentifier(name): + name = possible_names[0][1].replace("-", "_").lower() + if not name.isidentifier(): name = None if name is None: if not expose_value: return None, opts, secondary_opts - raise TypeError('Could not determine name for option') + raise TypeError("Could not determine name for option") if not opts and not secondary_opts: - raise TypeError('No options defined but a name was passed (%s). ' - 'Did you mean to declare an argument instead ' - 'of an option?' % name) + raise TypeError( + f"No options defined but a name was passed ({name})." + " Did you mean to declare an argument instead? Did" + f" you mean to pass '--{name}'?" + ) return name, opts, secondary_opts - def add_to_parser(self, parser, ctx): - kwargs = { - 'dest': self.name, - 'nargs': self.nargs, - 'obj': self, - } - + def add_to_parser(self, parser: OptionParser, ctx: Context) -> None: if self.multiple: - action = 'append' + action = "append" elif self.count: - action = 'count' + action = "count" else: - action = 'store' + action = "store" if self.is_flag: - kwargs.pop('nargs', None) + action = f"{action}_const" + if self.is_bool_flag and self.secondary_opts: - parser.add_option(self.opts, action=action + '_const', - const=True, **kwargs) - parser.add_option(self.secondary_opts, action=action + - '_const', const=False, **kwargs) + parser.add_option( + obj=self, opts=self.opts, dest=self.name, action=action, const=True + ) + parser.add_option( + obj=self, + opts=self.secondary_opts, + dest=self.name, + action=action, + const=False, + ) else: - parser.add_option(self.opts, action=action + '_const', - const=self.flag_value, - **kwargs) + parser.add_option( + obj=self, + opts=self.opts, + dest=self.name, + action=action, + const=self.flag_value, + ) else: - kwargs['action'] = action - parser.add_option(self.opts, **kwargs) + parser.add_option( + obj=self, + opts=self.opts, + dest=self.name, + action=action, + nargs=self.nargs, + ) - def get_help_record(self, ctx): + def get_help_record(self, ctx: Context) -> t.Optional[t.Tuple[str, str]]: if self.hidden: - return - any_prefix_is_slash = [] + return None + + any_prefix_is_slash = False + + def _write_opts(opts: t.Sequence[str]) -> str: + nonlocal any_prefix_is_slash - def _write_opts(opts): rv, any_slashes = join_options(opts) + if any_slashes: - any_prefix_is_slash[:] = [True] + any_prefix_is_slash = True + if not self.is_flag and not self.count: - rv += ' ' + self.make_metavar() + rv += f" {self.make_metavar()}" + return rv rv = [_write_opts(self.opts)] + if self.secondary_opts: rv.append(_write_opts(self.secondary_opts)) - help = self.help or '' + help = self.help or "" extra = [] + if self.show_envvar: envvar = self.envvar + if envvar is None: - if self.allow_from_autoenv and \ - ctx.auto_envvar_prefix is not None: - envvar = '%s_%s' % (ctx.auto_envvar_prefix, self.name.upper()) + if ( + self.allow_from_autoenv + and ctx.auto_envvar_prefix is not None + and self.name is not None + ): + envvar = f"{ctx.auto_envvar_prefix}_{self.name.upper()}" + if envvar is not None: - extra.append('env var: %s' % ( - ', '.join('%s' % d for d in envvar) - if isinstance(envvar, (list, tuple)) - else envvar, )) - if self.default is not None and self.show_default: - if isinstance(self.show_default, string_types): - default_string = '({})'.format(self.show_default) - elif isinstance(self.default, (list, tuple)): - default_string = ', '.join('%s' % d for d in self.default) - elif inspect.isfunction(self.default): - default_string = "(dynamic)" + var_str = ( + envvar + if isinstance(envvar, str) + else ", ".join(str(d) for d in envvar) + ) + extra.append(_("env var: {var}").format(var=var_str)) + + # Temporarily enable resilient parsing to avoid type casting + # failing for the default. Might be possible to extend this to + # help formatting in general. + resilient = ctx.resilient_parsing + ctx.resilient_parsing = True + + try: + default_value = self.get_default(ctx, call=False) + finally: + ctx.resilient_parsing = resilient + + show_default_is_str = isinstance(self.show_default, str) + + if show_default_is_str or ( + default_value is not None and (self.show_default or ctx.show_default) + ): + if show_default_is_str: + default_string = f"({self.show_default})" + elif isinstance(default_value, (list, tuple)): + default_string = ", ".join(str(d) for d in default_value) + elif callable(default_value): + default_string = _("(dynamic)") + elif self.is_bool_flag and self.secondary_opts: + # For boolean flags that have distinct True/False opts, + # use the opt without prefix instead of the value. + default_string = split_opt( + (self.opts if self.default else self.secondary_opts)[0] + )[1] else: - default_string = self.default - extra.append('default: {}'.format(default_string)) + default_string = str(default_value) + + if default_string: + extra.append(_("default: {default}").format(default=default_string)) + + if ( + isinstance(self.type, types._NumberRangeBase) + # skip count with default range type + and not (self.count and self.type.min == 0 and self.type.max is None) + ): + range_str = self.type._describe_range() + + if range_str: + extra.append(range_str) if self.required: - extra.append('required') + extra.append(_("required")) + if extra: - help = '%s[%s]' % (help and help + ' ' or '', '; '.join(extra)) + extra_str = "; ".join(extra) + help = f"{help} [{extra_str}]" if help else f"[{extra_str}]" - return ((any_prefix_is_slash and '; ' or ' / ').join(rv), help) + return ("; " if any_prefix_is_slash else " / ").join(rv), help - def get_default(self, ctx): - # If we're a non boolean flag out default is more complex because + @typing.overload + def get_default( + self, ctx: Context, call: "te.Literal[True]" = True + ) -> t.Optional[t.Any]: + ... + + @typing.overload + def get_default( + self, ctx: Context, call: bool = ... + ) -> t.Optional[t.Union[t.Any, t.Callable[[], t.Any]]]: + ... + + def get_default( + self, ctx: Context, call: bool = True + ) -> t.Optional[t.Union[t.Any, t.Callable[[], t.Any]]]: + # If we're a non boolean flag our default is more complex because # we need to look at all flags in the same group to figure out # if we're the the default one in which case we return the flag # value as default. if self.is_flag and not self.is_bool_flag: for param in ctx.command.params: if param.name == self.name and param.default: - return param.flag_value - return None - return Parameter.get_default(self, ctx) + return param.flag_value # type: ignore - def prompt_for_value(self, ctx): + return None + + return super().get_default(ctx, call=call) + + def prompt_for_value(self, ctx: Context) -> t.Any: """This is an alternative flow that can be activated in the full value processing if a value does not exist. It will prompt the user until a valid value exists and then returns the processed value as result. """ + assert self.prompt is not None + # Calculate the default before prompting anything to be stable. default = self.get_default(ctx) @@ -1758,36 +2797,84 @@ class Option(Parameter): if self.is_bool_flag: return confirm(self.prompt, default) - return prompt(self.prompt, default=default, type=self.type, - hide_input=self.hide_input, show_choices=self.show_choices, - confirmation_prompt=self.confirmation_prompt, - value_proc=lambda x: self.process_value(ctx, x)) + return prompt( + self.prompt, + default=default, + type=self.type, + hide_input=self.hide_input, + show_choices=self.show_choices, + confirmation_prompt=self.confirmation_prompt, + value_proc=lambda x: self.process_value(ctx, x), + ) + + def resolve_envvar_value(self, ctx: Context) -> t.Optional[str]: + rv = super().resolve_envvar_value(ctx) - def resolve_envvar_value(self, ctx): - rv = Parameter.resolve_envvar_value(self, ctx) if rv is not None: return rv - if self.allow_from_autoenv and \ - ctx.auto_envvar_prefix is not None: - envvar = '%s_%s' % (ctx.auto_envvar_prefix, self.name.upper()) - return os.environ.get(envvar) - def value_from_envvar(self, ctx): - rv = self.resolve_envvar_value(ctx) - if rv is None: - return None - value_depth = (self.nargs != 1) + bool(self.multiple) - if value_depth > 0 and rv is not None: - rv = self.type.split_envvar_value(rv) - if self.multiple and self.nargs != 1: - rv = batch(rv, self.nargs) + if ( + self.allow_from_autoenv + and ctx.auto_envvar_prefix is not None + and self.name is not None + ): + envvar = f"{ctx.auto_envvar_prefix}_{self.name.upper()}" + rv = os.environ.get(envvar) + return rv - def full_process_value(self, ctx, value): - if value is None and self.prompt is not None \ - and not ctx.resilient_parsing: - return self.prompt_for_value(ctx) - return Parameter.full_process_value(self, ctx, value) + def value_from_envvar(self, ctx: Context) -> t.Optional[t.Any]: + rv: t.Optional[t.Any] = self.resolve_envvar_value(ctx) + + if rv is None: + return None + + value_depth = (self.nargs != 1) + bool(self.multiple) + + if value_depth > 0: + rv = self.type.split_envvar_value(rv) + + if self.multiple and self.nargs != 1: + rv = batch(rv, self.nargs) + + return rv + + def consume_value( + self, ctx: Context, opts: t.Mapping[str, "Parameter"] + ) -> t.Tuple[t.Any, ParameterSource]: + value, source = super().consume_value(ctx, opts) + + # The parser will emit a sentinel value if the option can be + # given as a flag without a value. This is different from None + # to distinguish from the flag not being given at all. + if value is _flag_needs_value: + if self.prompt is not None and not ctx.resilient_parsing: + value = self.prompt_for_value(ctx) + source = ParameterSource.PROMPT + else: + value = self.flag_value + source = ParameterSource.COMMANDLINE + + elif ( + self.multiple + and value is not None + and any(v is _flag_needs_value for v in value) + ): + value = [self.flag_value if v is _flag_needs_value else v for v in value] + source = ParameterSource.COMMANDLINE + + # The value wasn't set, or used the param's default, prompt if + # prompting is enabled. + elif ( + source in {None, ParameterSource.DEFAULT} + and self.prompt is not None + and (self.required or self.prompt_required) + and not ctx.resilient_parsing + ): + value = self.prompt_for_value(ctx) + source = ParameterSource.PROMPT + + return value, source class Argument(Parameter): @@ -1797,60 +2884,70 @@ class Argument(Parameter): All parameters are passed onwards to the parameter constructor. """ - param_type_name = 'argument' - def __init__(self, param_decls, required=None, **attrs): + param_type_name = "argument" + + def __init__( + self, + param_decls: t.Sequence[str], + required: t.Optional[bool] = None, + **attrs: t.Any, + ) -> None: if required is None: - if attrs.get('default') is not None: + if attrs.get("default") is not None: required = False else: - required = attrs.get('nargs', 1) > 0 - Parameter.__init__(self, param_decls, required=required, **attrs) - if self.default is not None and self.nargs < 0: - raise TypeError('nargs=-1 in combination with a default value ' - 'is not supported.') + required = attrs.get("nargs", 1) > 0 + + if "multiple" in attrs: + raise TypeError("__init__() got an unexpected keyword argument 'multiple'.") + + super().__init__(param_decls, required=required, **attrs) + + if __debug__: + if self.default is not None and self.nargs == -1: + raise TypeError("'default' is not supported for nargs=-1.") @property - def human_readable_name(self): + def human_readable_name(self) -> str: if self.metavar is not None: return self.metavar - return self.name.upper() + return self.name.upper() # type: ignore - def make_metavar(self): + def make_metavar(self) -> str: if self.metavar is not None: return self.metavar var = self.type.get_metavar(self) if not var: - var = self.name.upper() + var = self.name.upper() # type: ignore if not self.required: - var = '[%s]' % var + var = f"[{var}]" if self.nargs != 1: - var += '...' + var += "..." return var - def _parse_decls(self, decls, expose_value): + def _parse_decls( + self, decls: t.Sequence[str], expose_value: bool + ) -> t.Tuple[t.Optional[str], t.List[str], t.List[str]]: if not decls: if not expose_value: return None, [], [] - raise TypeError('Could not determine name for argument') + raise TypeError("Could not determine name for argument") if len(decls) == 1: name = arg = decls[0] - name = name.replace('-', '_').lower() + name = name.replace("-", "_").lower() else: - raise TypeError('Arguments take exactly one ' - 'parameter declaration, got %d' % len(decls)) + raise TypeError( + "Arguments take exactly one parameter declaration, got" + f" {len(decls)}." + ) return name, [arg], [] - def get_usage_pieces(self, ctx): + def get_usage_pieces(self, ctx: Context) -> t.List[str]: return [self.make_metavar()] - def get_error_hint(self, ctx): - return '"%s"' % self.make_metavar() + def get_error_hint(self, ctx: Context) -> str: + return f"'{self.make_metavar()}'" - def add_to_parser(self, parser, ctx): - parser.add_argument(dest=self.name, nargs=self.nargs, - obj=self) - - -# Circular dependency between decorators and core -from .decorators import command, group + def add_to_parser(self, parser: OptionParser, ctx: Context) -> None: + parser.add_argument(dest=self.name, nargs=self.nargs, obj=self) diff --git a/libs/click/decorators.py b/libs/click/decorators.py index c57c53086..f1cc005af 100644 --- a/libs/click/decorators.py +++ b/libs/click/decorators.py @@ -1,34 +1,48 @@ -import sys import inspect - +import types +import typing as t from functools import update_wrapper +from gettext import gettext as _ -from ._compat import iteritems -from ._unicodefun import _check_for_unicode_literals -from .utils import echo +from .core import Argument +from .core import Command +from .core import Context +from .core import Group +from .core import Option +from .core import Parameter from .globals import get_current_context +from .utils import echo + +F = t.TypeVar("F", bound=t.Callable[..., t.Any]) +FC = t.TypeVar("FC", t.Callable[..., t.Any], Command) -def pass_context(f): +def pass_context(f: F) -> F: """Marks a callback as wanting to receive the current context object as first argument. """ - def new_func(*args, **kwargs): + + def new_func(*args, **kwargs): # type: ignore return f(get_current_context(), *args, **kwargs) - return update_wrapper(new_func, f) + + return update_wrapper(t.cast(F, new_func), f) -def pass_obj(f): +def pass_obj(f: F) -> F: """Similar to :func:`pass_context`, but only pass the object on the context onwards (:attr:`Context.obj`). This is useful if that object represents the state of a nested system. """ - def new_func(*args, **kwargs): + + def new_func(*args, **kwargs): # type: ignore return f(get_current_context().obj, *args, **kwargs) - return update_wrapper(new_func, f) + + return update_wrapper(t.cast(F, new_func), f) -def make_pass_decorator(object_type, ensure=False): +def make_pass_decorator( + object_type: t.Type, ensure: bool = False +) -> "t.Callable[[F], F]": """Given an object type this creates a decorator that will work similar to :func:`pass_obj` but instead of passing the object of the current context, it will find the innermost context of type @@ -50,53 +64,107 @@ def make_pass_decorator(object_type, ensure=False): :param ensure: if set to `True`, a new object will be created and remembered on the context if it's not there yet. """ - def decorator(f): - def new_func(*args, **kwargs): + + def decorator(f: F) -> F: + def new_func(*args, **kwargs): # type: ignore ctx = get_current_context() + if ensure: obj = ctx.ensure_object(object_type) else: obj = ctx.find_object(object_type) + if obj is None: - raise RuntimeError('Managed to invoke callback without a ' - 'context object of type %r existing' - % object_type.__name__) + raise RuntimeError( + "Managed to invoke callback without a context" + f" object of type {object_type.__name__!r}" + " existing." + ) + return ctx.invoke(f, obj, *args, **kwargs) - return update_wrapper(new_func, f) + + return update_wrapper(t.cast(F, new_func), f) + return decorator -def _make_command(f, name, attrs, cls): +def pass_meta_key( + key: str, *, doc_description: t.Optional[str] = None +) -> "t.Callable[[F], F]": + """Create a decorator that passes a key from + :attr:`click.Context.meta` as the first argument to the decorated + function. + + :param key: Key in ``Context.meta`` to pass. + :param doc_description: Description of the object being passed, + inserted into the decorator's docstring. Defaults to "the 'key' + key from Context.meta". + + .. versionadded:: 8.0 + """ + + def decorator(f: F) -> F: + def new_func(*args, **kwargs): # type: ignore + ctx = get_current_context() + obj = ctx.meta[key] + return ctx.invoke(f, obj, *args, **kwargs) + + return update_wrapper(t.cast(F, new_func), f) + + if doc_description is None: + doc_description = f"the {key!r} key from :attr:`click.Context.meta`" + + decorator.__doc__ = ( + f"Decorator that passes {doc_description} as the first argument" + " to the decorated function." + ) + return decorator + + +def _make_command( + f: F, + name: t.Optional[str], + attrs: t.MutableMapping[str, t.Any], + cls: t.Type[Command], +) -> Command: if isinstance(f, Command): - raise TypeError('Attempted to convert a callback into a ' - 'command twice.') + raise TypeError("Attempted to convert a callback into a command twice.") + try: - params = f.__click_params__ + params = f.__click_params__ # type: ignore params.reverse() - del f.__click_params__ + del f.__click_params__ # type: ignore except AttributeError: params = [] - help = attrs.get('help') + + help = attrs.get("help") + if help is None: help = inspect.getdoc(f) - if isinstance(help, bytes): - help = help.decode('utf-8') else: help = inspect.cleandoc(help) - attrs['help'] = help - _check_for_unicode_literals() - return cls(name=name or f.__name__.lower().replace('_', '-'), - callback=f, params=params, **attrs) + + attrs["help"] = help + return cls( + name=name or f.__name__.lower().replace("_", "-"), + callback=f, + params=params, + **attrs, + ) -def command(name=None, cls=None, **attrs): +def command( + name: t.Optional[str] = None, + cls: t.Optional[t.Type[Command]] = None, + **attrs: t.Any, +) -> t.Callable[[F], Command]: r"""Creates a new :class:`Command` and uses the decorated function as callback. This will also automatically attach all decorated :func:`option`\s and :func:`argument`\s as parameters to the command. - The name of the command defaults to the name of the function. If you - want to change that, you can pass the intended name as the first - argument. + The name of the command defaults to the name of the function with + underscores replaced by dashes. If you want to change that, you can + pass the intended name as the first argument. All keyword arguments are forwarded to the underlying command class. @@ -111,32 +179,35 @@ def command(name=None, cls=None, **attrs): """ if cls is None: cls = Command - def decorator(f): - cmd = _make_command(f, name, attrs, cls) + + def decorator(f: t.Callable[..., t.Any]) -> Command: + cmd = _make_command(f, name, attrs, cls) # type: ignore cmd.__doc__ = f.__doc__ return cmd + return decorator -def group(name=None, **attrs): +def group(name: t.Optional[str] = None, **attrs: t.Any) -> t.Callable[[F], Group]: """Creates a new :class:`Group` with a function as callback. This works otherwise the same as :func:`command` just that the `cls` parameter is set to :class:`Group`. """ - attrs.setdefault('cls', Group) - return command(name, **attrs) + attrs.setdefault("cls", Group) + return t.cast(Group, command(name, **attrs)) -def _param_memo(f, param): +def _param_memo(f: FC, param: Parameter) -> None: if isinstance(f, Command): f.params.append(param) else: - if not hasattr(f, '__click_params__'): - f.__click_params__ = [] - f.__click_params__.append(param) + if not hasattr(f, "__click_params__"): + f.__click_params__ = [] # type: ignore + + f.__click_params__.append(param) # type: ignore -def argument(*param_decls, **attrs): +def argument(*param_decls: str, **attrs: t.Any) -> t.Callable[[FC], FC]: """Attaches an argument to the command. All positional arguments are passed as parameter declarations to :class:`Argument`; all keyword arguments are forwarded unchanged (except ``cls``). @@ -146,14 +217,16 @@ def argument(*param_decls, **attrs): :param cls: the argument class to instantiate. This defaults to :class:`Argument`. """ - def decorator(f): - ArgumentClass = attrs.pop('cls', Argument) + + def decorator(f: FC) -> FC: + ArgumentClass = attrs.pop("cls", Argument) _param_memo(f, ArgumentClass(param_decls, **attrs)) return f + return decorator -def option(*param_decls, **attrs): +def option(*param_decls: str, **attrs: t.Any) -> t.Callable[[FC], FC]: """Attaches an option to the command. All positional arguments are passed as parameter declarations to :class:`Option`; all keyword arguments are forwarded unchanged (except ``cls``). @@ -163,149 +236,201 @@ def option(*param_decls, **attrs): :param cls: the option class to instantiate. This defaults to :class:`Option`. """ - def decorator(f): + + def decorator(f: FC) -> FC: # Issue 926, copy attrs, so pre-defined options can re-use the same cls= option_attrs = attrs.copy() - if 'help' in option_attrs: - option_attrs['help'] = inspect.cleandoc(option_attrs['help']) - OptionClass = option_attrs.pop('cls', Option) + if "help" in option_attrs: + option_attrs["help"] = inspect.cleandoc(option_attrs["help"]) + OptionClass = option_attrs.pop("cls", Option) _param_memo(f, OptionClass(param_decls, **option_attrs)) return f + return decorator -def confirmation_option(*param_decls, **attrs): - """Shortcut for confirmation prompts that can be ignored by passing - ``--yes`` as parameter. +def confirmation_option(*param_decls: str, **kwargs: t.Any) -> t.Callable[[FC], FC]: + """Add a ``--yes`` option which shows a prompt before continuing if + not passed. If the prompt is declined, the program will exit. - This is equivalent to decorating a function with :func:`option` with - the following parameters:: - - def callback(ctx, param, value): - if not value: - ctx.abort() - - @click.command() - @click.option('--yes', is_flag=True, callback=callback, - expose_value=False, prompt='Do you want to continue?') - def dropdb(): - pass + :param param_decls: One or more option names. Defaults to the single + value ``"--yes"``. + :param kwargs: Extra arguments are passed to :func:`option`. """ - def decorator(f): - def callback(ctx, param, value): - if not value: - ctx.abort() - attrs.setdefault('is_flag', True) - attrs.setdefault('callback', callback) - attrs.setdefault('expose_value', False) - attrs.setdefault('prompt', 'Do you want to continue?') - attrs.setdefault('help', 'Confirm the action without prompting.') - return option(*(param_decls or ('--yes',)), **attrs)(f) - return decorator + + def callback(ctx: Context, param: Parameter, value: bool) -> None: + if not value: + ctx.abort() + + if not param_decls: + param_decls = ("--yes",) + + kwargs.setdefault("is_flag", True) + kwargs.setdefault("callback", callback) + kwargs.setdefault("expose_value", False) + kwargs.setdefault("prompt", "Do you want to continue?") + kwargs.setdefault("help", "Confirm the action without prompting.") + return option(*param_decls, **kwargs) -def password_option(*param_decls, **attrs): - """Shortcut for password prompts. +def password_option(*param_decls: str, **kwargs: t.Any) -> t.Callable[[FC], FC]: + """Add a ``--password`` option which prompts for a password, hiding + input and asking to enter the value again for confirmation. - This is equivalent to decorating a function with :func:`option` with - the following parameters:: - - @click.command() - @click.option('--password', prompt=True, confirmation_prompt=True, - hide_input=True) - def changeadmin(password): - pass + :param param_decls: One or more option names. Defaults to the single + value ``"--password"``. + :param kwargs: Extra arguments are passed to :func:`option`. """ - def decorator(f): - attrs.setdefault('prompt', True) - attrs.setdefault('confirmation_prompt', True) - attrs.setdefault('hide_input', True) - return option(*(param_decls or ('--password',)), **attrs)(f) - return decorator + if not param_decls: + param_decls = ("--password",) + + kwargs.setdefault("prompt", True) + kwargs.setdefault("confirmation_prompt", True) + kwargs.setdefault("hide_input", True) + return option(*param_decls, **kwargs) -def version_option(version=None, *param_decls, **attrs): - """Adds a ``--version`` option which immediately ends the program - printing out the version number. This is implemented as an eager - option that prints the version and exits the program in the callback. +def version_option( + version: t.Optional[str] = None, + *param_decls: str, + package_name: t.Optional[str] = None, + prog_name: t.Optional[str] = None, + message: t.Optional[str] = None, + **kwargs: t.Any, +) -> t.Callable[[FC], FC]: + """Add a ``--version`` option which immediately prints the version + number and exits the program. - :param version: the version number to show. If not provided Click - attempts an auto discovery via setuptools. - :param prog_name: the name of the program (defaults to autodetection) - :param message: custom message to show instead of the default - (``'%(prog)s, version %(version)s'``) - :param others: everything else is forwarded to :func:`option`. + If ``version`` is not provided, Click will try to detect it using + :func:`importlib.metadata.version` to get the version for the + ``package_name``. On Python < 3.8, the ``importlib_metadata`` + backport must be installed. + + If ``package_name`` is not provided, Click will try to detect it by + inspecting the stack frames. This will be used to detect the + version, so it must match the name of the installed package. + + :param version: The version number to show. If not provided, Click + will try to detect it. + :param param_decls: One or more option names. Defaults to the single + value ``"--version"``. + :param package_name: The package name to detect the version from. If + not provided, Click will try to detect it. + :param prog_name: The name of the CLI to show in the message. If not + provided, it will be detected from the command. + :param message: The message to show. The values ``%(prog)s``, + ``%(package)s``, and ``%(version)s`` are available. Defaults to + ``"%(prog)s, version %(version)s"``. + :param kwargs: Extra arguments are passed to :func:`option`. + :raise RuntimeError: ``version`` could not be detected. + + .. versionchanged:: 8.0 + Add the ``package_name`` parameter, and the ``%(package)s`` + value for messages. + + .. versionchanged:: 8.0 + Use :mod:`importlib.metadata` instead of ``pkg_resources``. The + version is detected based on the package name, not the entry + point name. The Python package name must match the installed + package name, or be passed with ``package_name=``. """ - if version is None: - if hasattr(sys, '_getframe'): - module = sys._getframe(1).f_globals.get('__name__') - else: - module = '' + if message is None: + message = _("%(prog)s, version %(version)s") - def decorator(f): - prog_name = attrs.pop('prog_name', None) - message = attrs.pop('message', '%(prog)s, version %(version)s') + if version is None and package_name is None: + frame = inspect.currentframe() + f_back = frame.f_back if frame is not None else None + f_globals = f_back.f_globals if f_back is not None else None + # break reference cycle + # https://docs.python.org/3/library/inspect.html#the-interpreter-stack + del frame - def callback(ctx, param, value): - if not value or ctx.resilient_parsing: - return - prog = prog_name - if prog is None: - prog = ctx.find_root().info_name - ver = version - if ver is None: - try: - import pkg_resources - except ImportError: - pass - else: - for dist in pkg_resources.working_set: - scripts = dist.get_entry_map().get('console_scripts') or {} - for script_name, entry_point in iteritems(scripts): - if entry_point.module_name == module: - ver = dist.version - break - if ver is None: - raise RuntimeError('Could not determine version') - echo(message % { - 'prog': prog, - 'version': ver, - }, color=ctx.color) - ctx.exit() + if f_globals is not None: + package_name = f_globals.get("__name__") - attrs.setdefault('is_flag', True) - attrs.setdefault('expose_value', False) - attrs.setdefault('is_eager', True) - attrs.setdefault('help', 'Show the version and exit.') - attrs['callback'] = callback - return option(*(param_decls or ('--version',)), **attrs)(f) - return decorator + if package_name == "__main__": + package_name = f_globals.get("__package__") + + if package_name: + package_name = package_name.partition(".")[0] + + def callback(ctx: Context, param: Parameter, value: bool) -> None: + if not value or ctx.resilient_parsing: + return + + nonlocal prog_name + nonlocal version + + if prog_name is None: + prog_name = ctx.find_root().info_name + + if version is None and package_name is not None: + metadata: t.Optional[types.ModuleType] + + try: + from importlib import metadata # type: ignore + except ImportError: + # Python < 3.8 + import importlib_metadata as metadata # type: ignore + + try: + version = metadata.version(package_name) # type: ignore + except metadata.PackageNotFoundError: # type: ignore + raise RuntimeError( + f"{package_name!r} is not installed. Try passing" + " 'package_name' instead." + ) from None + + if version is None: + raise RuntimeError( + f"Could not determine the version for {package_name!r} automatically." + ) + + echo( + t.cast(str, message) + % {"prog": prog_name, "package": package_name, "version": version}, + color=ctx.color, + ) + ctx.exit() + + if not param_decls: + param_decls = ("--version",) + + kwargs.setdefault("is_flag", True) + kwargs.setdefault("expose_value", False) + kwargs.setdefault("is_eager", True) + kwargs.setdefault("help", _("Show the version and exit.")) + kwargs["callback"] = callback + return option(*param_decls, **kwargs) -def help_option(*param_decls, **attrs): - """Adds a ``--help`` option which immediately ends the program - printing out the help page. This is usually unnecessary to add as - this is added by default to all commands unless suppressed. +def help_option(*param_decls: str, **kwargs: t.Any) -> t.Callable[[FC], FC]: + """Add a ``--help`` option which immediately prints the help page + and exits the program. - Like :func:`version_option`, this is implemented as eager option that - prints in the callback and exits. + This is usually unnecessary, as the ``--help`` option is added to + each command automatically unless ``add_help_option=False`` is + passed. - All arguments are forwarded to :func:`option`. + :param param_decls: One or more option names. Defaults to the single + value ``"--help"``. + :param kwargs: Extra arguments are passed to :func:`option`. """ - def decorator(f): - def callback(ctx, param, value): - if value and not ctx.resilient_parsing: - echo(ctx.get_help(), color=ctx.color) - ctx.exit() - attrs.setdefault('is_flag', True) - attrs.setdefault('expose_value', False) - attrs.setdefault('help', 'Show this message and exit.') - attrs.setdefault('is_eager', True) - attrs['callback'] = callback - return option(*(param_decls or ('--help',)), **attrs)(f) - return decorator + def callback(ctx: Context, param: Parameter, value: bool) -> None: + if not value or ctx.resilient_parsing: + return -# Circular dependencies between core and decorators -from .core import Command, Group, Argument, Option + echo(ctx.get_help(), color=ctx.color) + ctx.exit() + + if not param_decls: + param_decls = ("--help",) + + kwargs.setdefault("is_flag", True) + kwargs.setdefault("expose_value", False) + kwargs.setdefault("is_eager", True) + kwargs.setdefault("help", _("Show this message and exit.")) + kwargs["callback"] = callback + return option(*param_decls, **kwargs) diff --git a/libs/click/exceptions.py b/libs/click/exceptions.py index 6fa17658c..9e20b3eb5 100644 --- a/libs/click/exceptions.py +++ b/libs/click/exceptions.py @@ -1,43 +1,46 @@ -from ._compat import PY2, filename_to_ui, get_text_stderr +import os +import typing as t +from gettext import gettext as _ +from gettext import ngettext + +from ._compat import get_text_stderr from .utils import echo +if t.TYPE_CHECKING: + from .core import Context + from .core import Parameter + + +def _join_param_hints( + param_hint: t.Optional[t.Union[t.Sequence[str], str]] +) -> t.Optional[str]: + if param_hint is not None and not isinstance(param_hint, str): + return " / ".join(repr(x) for x in param_hint) -def _join_param_hints(param_hint): - if isinstance(param_hint, (tuple, list)): - return ' / '.join('"%s"' % x for x in param_hint) return param_hint class ClickException(Exception): """An exception that Click can handle and show to the user.""" - #: The exit code for this exception + #: The exit code for this exception. exit_code = 1 - def __init__(self, message): - ctor_msg = message - if PY2: - if ctor_msg is not None: - ctor_msg = ctor_msg.encode('utf-8') - Exception.__init__(self, ctor_msg) + def __init__(self, message: str) -> None: + super().__init__(message) self.message = message - def format_message(self): + def format_message(self) -> str: return self.message - def __str__(self): + def __str__(self) -> str: return self.message - if PY2: - __unicode__ = __str__ - - def __str__(self): - return self.message.encode('utf-8') - - def show(self, file=None): + def show(self, file: t.Optional[t.IO] = None) -> None: if file is None: file = get_text_stderr() - echo('Error: %s' % self.format_message(), file=file) + + echo(_("Error: {message}").format(message=self.format_message()), file=file) class UsageError(ClickException): @@ -48,26 +51,35 @@ class UsageError(ClickException): :param ctx: optionally the context that caused this error. Click will fill in the context automatically in some situations. """ + exit_code = 2 - def __init__(self, message, ctx=None): - ClickException.__init__(self, message) + def __init__(self, message: str, ctx: t.Optional["Context"] = None) -> None: + super().__init__(message) self.ctx = ctx - self.cmd = self.ctx and self.ctx.command or None + self.cmd = self.ctx.command if self.ctx else None - def show(self, file=None): + def show(self, file: t.Optional[t.IO] = None) -> None: if file is None: file = get_text_stderr() color = None - hint = '' - if (self.cmd is not None and - self.cmd.get_help_option(self.ctx) is not None): - hint = ('Try "%s %s" for help.\n' - % (self.ctx.command_path, self.ctx.help_option_names[0])) + hint = "" + if ( + self.ctx is not None + and self.ctx.command.get_help_option(self.ctx) is not None + ): + hint = _("Try '{command} {option}' for help.").format( + command=self.ctx.command_path, option=self.ctx.help_option_names[0] + ) + hint = f"{hint}\n" if self.ctx is not None: color = self.ctx.color - echo(self.ctx.get_usage() + '\n%s' % hint, file=file, color=color) - echo('Error: %s' % self.format_message(), file=file, color=color) + echo(f"{self.ctx.get_usage()}\n{hint}", file=file, color=color) + echo( + _("Error: {message}").format(message=self.format_message()), + file=file, + color=color, + ) class BadParameter(UsageError): @@ -88,22 +100,28 @@ class BadParameter(UsageError): each item is quoted and separated. """ - def __init__(self, message, ctx=None, param=None, - param_hint=None): - UsageError.__init__(self, message, ctx) + def __init__( + self, + message: str, + ctx: t.Optional["Context"] = None, + param: t.Optional["Parameter"] = None, + param_hint: t.Optional[str] = None, + ) -> None: + super().__init__(message, ctx) self.param = param self.param_hint = param_hint - def format_message(self): + def format_message(self) -> str: if self.param_hint is not None: param_hint = self.param_hint elif self.param is not None: - param_hint = self.param.get_error_hint(self.ctx) + param_hint = self.param.get_error_hint(self.ctx) # type: ignore else: - return 'Invalid value: %s' % self.message - param_hint = _join_param_hints(param_hint) + return _("Invalid value: {message}").format(message=self.message) - return 'Invalid value for %s: %s' % (param_hint, self.message) + return _("Invalid value for {param_hint}: {message}").format( + param_hint=_join_param_hints(param_hint), message=self.message + ) class MissingParameter(BadParameter): @@ -118,19 +136,27 @@ class MissingParameter(BadParameter): ``'option'`` or ``'argument'``. """ - def __init__(self, message=None, ctx=None, param=None, - param_hint=None, param_type=None): - BadParameter.__init__(self, message, ctx, param, param_hint) + def __init__( + self, + message: t.Optional[str] = None, + ctx: t.Optional["Context"] = None, + param: t.Optional["Parameter"] = None, + param_hint: t.Optional[str] = None, + param_type: t.Optional[str] = None, + ) -> None: + super().__init__(message or "", ctx, param, param_hint) self.param_type = param_type - def format_message(self): + def format_message(self) -> str: if self.param_hint is not None: - param_hint = self.param_hint + param_hint: t.Optional[str] = self.param_hint elif self.param is not None: - param_hint = self.param.get_error_hint(self.ctx) + param_hint = self.param.get_error_hint(self.ctx) # type: ignore else: param_hint = None + param_hint = _join_param_hints(param_hint) + param_hint = f" {param_hint}" if param_hint else "" param_type = self.param_type if param_type is None and self.param is not None: @@ -141,16 +167,30 @@ class MissingParameter(BadParameter): msg_extra = self.param.type.get_missing_message(self.param) if msg_extra: if msg: - msg += '. ' + msg_extra + msg += f". {msg_extra}" else: msg = msg_extra - return 'Missing %s%s%s%s' % ( - param_type, - param_hint and ' %s' % param_hint or '', - msg and '. ' or '.', - msg or '', - ) + msg = f" {msg}" if msg else "" + + # Translate param_type for known types. + if param_type == "argument": + missing = _("Missing argument") + elif param_type == "option": + missing = _("Missing option") + elif param_type == "parameter": + missing = _("Missing parameter") + else: + missing = _("Missing {param_type}").format(param_type=param_type) + + return f"{missing}{param_hint}.{msg}" + + def __str__(self) -> str: + if not self.message: + param_name = self.param.name if self.param else None + return _("Missing parameter: {param_name}").format(param_name=param_name) + else: + return self.message class NoSuchOption(UsageError): @@ -160,23 +200,31 @@ class NoSuchOption(UsageError): .. versionadded:: 4.0 """ - def __init__(self, option_name, message=None, possibilities=None, - ctx=None): + def __init__( + self, + option_name: str, + message: t.Optional[str] = None, + possibilities: t.Optional[t.Sequence[str]] = None, + ctx: t.Optional["Context"] = None, + ) -> None: if message is None: - message = 'no such option: %s' % option_name - UsageError.__init__(self, message, ctx) + message = _("No such option: {name}").format(name=option_name) + + super().__init__(message, ctx) self.option_name = option_name self.possibilities = possibilities - def format_message(self): - bits = [self.message] - if self.possibilities: - if len(self.possibilities) == 1: - bits.append('Did you mean %s?' % self.possibilities[0]) - else: - possibilities = sorted(self.possibilities) - bits.append('(Possible options: %s)' % ', '.join(possibilities)) - return ' '.join(bits) + def format_message(self) -> str: + if not self.possibilities: + return self.message + + possibility_str = ", ".join(sorted(self.possibilities)) + suggest = ngettext( + "Did you mean {possibility}?", + "(Possible options: {possibilities})", + len(self.possibilities), + ).format(possibility=possibility_str, possibilities=possibility_str) + return f"{self.message} {suggest}" class BadOptionUsage(UsageError): @@ -189,8 +237,10 @@ class BadOptionUsage(UsageError): :param option_name: the name of the option being used incorrectly. """ - def __init__(self, option_name, message, ctx=None): - UsageError.__init__(self, message, ctx) + def __init__( + self, option_name: str, message: str, ctx: t.Optional["Context"] = None + ) -> None: + super().__init__(message, ctx) self.option_name = option_name @@ -202,23 +252,22 @@ class BadArgumentUsage(UsageError): .. versionadded:: 6.0 """ - def __init__(self, message, ctx=None): - UsageError.__init__(self, message, ctx) - class FileError(ClickException): """Raised if a file cannot be opened.""" - def __init__(self, filename, hint=None): - ui_filename = filename_to_ui(filename) + def __init__(self, filename: str, hint: t.Optional[str] = None) -> None: if hint is None: - hint = 'unknown error' - ClickException.__init__(self, hint) - self.ui_filename = ui_filename + hint = _("unknown error") + + super().__init__(hint) + self.ui_filename = os.fsdecode(filename) self.filename = filename - def format_message(self): - return 'Could not open file %s: %s' % (self.ui_filename, self.message) + def format_message(self) -> str: + return _("Could not open file {filename!r}: {message}").format( + filename=self.ui_filename, message=self.message + ) class Abort(RuntimeError): @@ -231,5 +280,8 @@ class Exit(RuntimeError): :param code: the status code to exit with. """ - def __init__(self, code=0): + + __slots__ = ("exit_code",) + + def __init__(self, code: int = 0) -> None: self.exit_code = code diff --git a/libs/click/formatting.py b/libs/click/formatting.py index a3d6a4d38..ddd2a2f82 100644 --- a/libs/click/formatting.py +++ b/libs/click/formatting.py @@ -1,29 +1,38 @@ +import typing as t from contextlib import contextmanager -from .termui import get_terminal_size -from .parser import split_opt -from ._compat import term_len +from gettext import gettext as _ +from ._compat import term_len +from .parser import split_opt # Can force a width. This is used by the test system -FORCED_WIDTH = None +FORCED_WIDTH: t.Optional[int] = None -def measure_table(rows): - widths = {} +def measure_table(rows: t.Iterable[t.Tuple[str, str]]) -> t.Tuple[int, ...]: + widths: t.Dict[int, int] = {} + for row in rows: for idx, col in enumerate(row): widths[idx] = max(widths.get(idx, 0), term_len(col)) + return tuple(y for x, y in sorted(widths.items())) -def iter_rows(rows, col_count): +def iter_rows( + rows: t.Iterable[t.Tuple[str, str]], col_count: int +) -> t.Iterator[t.Tuple[str, ...]]: for row in rows: - row = tuple(row) - yield row + ('',) * (col_count - len(row)) + yield row + ("",) * (col_count - len(row)) -def wrap_text(text, width=78, initial_indent='', subsequent_indent='', - preserve_paragraphs=False): +def wrap_text( + text: str, + width: int = 78, + initial_indent: str = "", + subsequent_indent: str = "", + preserve_paragraphs: bool = False, +) -> str: """A helper function that intelligently wraps text. By default, it assumes that it operates on a single paragraph of text but if the `preserve_paragraphs` parameter is provided it will intelligently @@ -43,24 +52,28 @@ def wrap_text(text, width=78, initial_indent='', subsequent_indent='', intelligently handle paragraphs. """ from ._textwrap import TextWrapper + text = text.expandtabs() - wrapper = TextWrapper(width, initial_indent=initial_indent, - subsequent_indent=subsequent_indent, - replace_whitespace=False) + wrapper = TextWrapper( + width, + initial_indent=initial_indent, + subsequent_indent=subsequent_indent, + replace_whitespace=False, + ) if not preserve_paragraphs: return wrapper.fill(text) - p = [] - buf = [] + p: t.List[t.Tuple[int, bool, str]] = [] + buf: t.List[str] = [] indent = None - def _flush_par(): + def _flush_par() -> None: if not buf: return - if buf[0].strip() == '\b': - p.append((indent or 0, True, '\n'.join(buf[1:]))) + if buf[0].strip() == "\b": + p.append((indent or 0, True, "\n".join(buf[1:]))) else: - p.append((indent or 0, False, ' '.join(buf))) + p.append((indent or 0, False, " ".join(buf))) del buf[:] for line in text.splitlines(): @@ -77,16 +90,16 @@ def wrap_text(text, width=78, initial_indent='', subsequent_indent='', rv = [] for indent, raw, text in p: - with wrapper.extra_indent(' ' * indent): + with wrapper.extra_indent(" " * indent): if raw: rv.append(wrapper.indent_only(text)) else: rv.append(wrapper.fill(text)) - return '\n\n'.join(rv) + return "\n\n".join(rv) -class HelpFormatter(object): +class HelpFormatter: """This class helps with formatting text-based help pages. It's usually just needed for very special internal cases, but it's also exposed so that developers can write their own fancy outputs. @@ -98,79 +111,108 @@ class HelpFormatter(object): width clamped to a maximum of 78. """ - def __init__(self, indent_increment=2, width=None, max_width=None): + def __init__( + self, + indent_increment: int = 2, + width: t.Optional[int] = None, + max_width: t.Optional[int] = None, + ) -> None: + import shutil + self.indent_increment = indent_increment if max_width is None: max_width = 80 if width is None: width = FORCED_WIDTH if width is None: - width = max(min(get_terminal_size()[0], max_width) - 2, 50) + width = max(min(shutil.get_terminal_size().columns, max_width) - 2, 50) self.width = width self.current_indent = 0 - self.buffer = [] + self.buffer: t.List[str] = [] - def write(self, string): + def write(self, string: str) -> None: """Writes a unicode string into the internal buffer.""" self.buffer.append(string) - def indent(self): + def indent(self) -> None: """Increases the indentation.""" self.current_indent += self.indent_increment - def dedent(self): + def dedent(self) -> None: """Decreases the indentation.""" self.current_indent -= self.indent_increment - def write_usage(self, prog, args='', prefix='Usage: '): + def write_usage( + self, prog: str, args: str = "", prefix: t.Optional[str] = None + ) -> None: """Writes a usage line into the buffer. :param prog: the program name. :param args: whitespace separated list of arguments. - :param prefix: the prefix for the first line. + :param prefix: The prefix for the first line. Defaults to + ``"Usage: "``. """ - usage_prefix = '%*s%s ' % (self.current_indent, prefix, prog) + if prefix is None: + prefix = f"{_('Usage:')} " + + usage_prefix = f"{prefix:>{self.current_indent}}{prog} " text_width = self.width - self.current_indent if text_width >= (term_len(usage_prefix) + 20): # The arguments will fit to the right of the prefix. - indent = ' ' * term_len(usage_prefix) - self.write(wrap_text(args, text_width, - initial_indent=usage_prefix, - subsequent_indent=indent)) + indent = " " * term_len(usage_prefix) + self.write( + wrap_text( + args, + text_width, + initial_indent=usage_prefix, + subsequent_indent=indent, + ) + ) else: # The prefix is too long, put the arguments on the next line. self.write(usage_prefix) - self.write('\n') - indent = ' ' * (max(self.current_indent, term_len(prefix)) + 4) - self.write(wrap_text(args, text_width, - initial_indent=indent, - subsequent_indent=indent)) + self.write("\n") + indent = " " * (max(self.current_indent, term_len(prefix)) + 4) + self.write( + wrap_text( + args, text_width, initial_indent=indent, subsequent_indent=indent + ) + ) - self.write('\n') + self.write("\n") - def write_heading(self, heading): + def write_heading(self, heading: str) -> None: """Writes a heading into the buffer.""" - self.write('%*s%s:\n' % (self.current_indent, '', heading)) + self.write(f"{'':>{self.current_indent}}{heading}:\n") - def write_paragraph(self): + def write_paragraph(self) -> None: """Writes a paragraph into the buffer.""" if self.buffer: - self.write('\n') + self.write("\n") - def write_text(self, text): + def write_text(self, text: str) -> None: """Writes re-indented text into the buffer. This rewraps and preserves paragraphs. """ - text_width = max(self.width - self.current_indent, 11) - indent = ' ' * self.current_indent - self.write(wrap_text(text, text_width, - initial_indent=indent, - subsequent_indent=indent, - preserve_paragraphs=True)) - self.write('\n') + indent = " " * self.current_indent + self.write( + wrap_text( + text, + self.width, + initial_indent=indent, + subsequent_indent=indent, + preserve_paragraphs=True, + ) + ) + self.write("\n") - def write_dl(self, rows, col_max=30, col_spacing=2): + def write_dl( + self, + rows: t.Sequence[t.Tuple[str, str]], + col_max: int = 30, + col_spacing: int = 2, + ) -> None: """Writes a definition list into the buffer. This is how options and commands are usually formatted. @@ -182,33 +224,35 @@ class HelpFormatter(object): rows = list(rows) widths = measure_table(rows) if len(widths) != 2: - raise TypeError('Expected two columns for definition list') + raise TypeError("Expected two columns for definition list") first_col = min(widths[0], col_max) + col_spacing for first, second in iter_rows(rows, len(widths)): - self.write('%*s%s' % (self.current_indent, '', first)) + self.write(f"{'':>{self.current_indent}}{first}") if not second: - self.write('\n') + self.write("\n") continue if term_len(first) <= first_col - col_spacing: - self.write(' ' * (first_col - term_len(first))) + self.write(" " * (first_col - term_len(first))) else: - self.write('\n') - self.write(' ' * (first_col + self.current_indent)) + self.write("\n") + self.write(" " * (first_col + self.current_indent)) text_width = max(self.width - first_col - 2, 10) - lines = iter(wrap_text(second, text_width).splitlines()) + wrapped_text = wrap_text(second, text_width, preserve_paragraphs=True) + lines = wrapped_text.splitlines() + if lines: - self.write(next(lines) + '\n') - for line in lines: - self.write('%*s%s\n' % ( - first_col + self.current_indent, '', line)) + self.write(f"{lines[0]}\n") + + for line in lines[1:]: + self.write(f"{'':>{first_col + self.current_indent}}{line}\n") else: - self.write('\n') + self.write("\n") @contextmanager - def section(self, name): + def section(self, name: str) -> t.Iterator[None]: """Helpful context manager that writes a paragraph, a heading, and the indents. @@ -223,7 +267,7 @@ class HelpFormatter(object): self.dedent() @contextmanager - def indentation(self): + def indentation(self) -> t.Iterator[None]: """A context manager that increases the indentation.""" self.indent() try: @@ -231,12 +275,12 @@ class HelpFormatter(object): finally: self.dedent() - def getvalue(self): + def getvalue(self) -> str: """Returns the buffer contents.""" - return ''.join(self.buffer) + return "".join(self.buffer) -def join_options(options): +def join_options(options: t.Sequence[str]) -> t.Tuple[str, bool]: """Given a list of option strings this joins them in the most appropriate way and returns them in the form ``(formatted_string, any_prefix_is_slash)`` where the second item in the tuple is a flag that @@ -244,13 +288,14 @@ def join_options(options): """ rv = [] any_prefix_is_slash = False + for opt in options: prefix = split_opt(opt)[0] - if prefix == '/': + + if prefix == "/": any_prefix_is_slash = True + rv.append((len(prefix), opt)) rv.sort(key=lambda x: x[0]) - - rv = ', '.join(x[1] for x in rv) - return rv, any_prefix_is_slash + return ", ".join(x[1] for x in rv), any_prefix_is_slash diff --git a/libs/click/globals.py b/libs/click/globals.py index 843b594ab..a7b0c9317 100644 --- a/libs/click/globals.py +++ b/libs/click/globals.py @@ -1,10 +1,25 @@ +import typing +import typing as t from threading import local +if t.TYPE_CHECKING: + import typing_extensions as te + from .core import Context _local = local() -def get_current_context(silent=False): +@typing.overload +def get_current_context(silent: "te.Literal[False]" = False) -> "Context": + ... + + +@typing.overload +def get_current_context(silent: bool = ...) -> t.Optional["Context"]: + ... + + +def get_current_context(silent: bool = False) -> t.Optional["Context"]: """Returns the current click context. This can be used as a way to access the current context object from anywhere. This is a more implicit alternative to the :func:`pass_context` decorator. This function is @@ -15,34 +30,40 @@ def get_current_context(silent=False): .. versionadded:: 5.0 - :param silent: is set to `True` the return value is `None` if no context + :param silent: if set to `True` the return value is `None` if no context is available. The default behavior is to raise a :exc:`RuntimeError`. """ try: - return getattr(_local, 'stack')[-1] - except (AttributeError, IndexError): + return t.cast("Context", _local.stack[-1]) + except (AttributeError, IndexError) as e: if not silent: - raise RuntimeError('There is no active click context.') + raise RuntimeError("There is no active click context.") from e + + return None -def push_context(ctx): +def push_context(ctx: "Context") -> None: """Pushes a new context to the current stack.""" - _local.__dict__.setdefault('stack', []).append(ctx) + _local.__dict__.setdefault("stack", []).append(ctx) -def pop_context(): +def pop_context() -> None: """Removes the top level from the stack.""" _local.stack.pop() -def resolve_color_default(color=None): - """"Internal helper to get the default value of the color flag. If a +def resolve_color_default(color: t.Optional[bool] = None) -> t.Optional[bool]: + """Internal helper to get the default value of the color flag. If a value is passed it's returned unchanged, otherwise it's looked up from the current context. """ if color is not None: return color + ctx = get_current_context(silent=True) + if ctx is not None: return ctx.color + + return None diff --git a/libs/click/parser.py b/libs/click/parser.py index 1c3ae9c8e..2d5a2ed7b 100644 --- a/libs/click/parser.py +++ b/libs/click/parser.py @@ -1,8 +1,4 @@ -# -*- coding: utf-8 -*- """ -click.parser -~~~~~~~~~~~~ - This module started out as largely a copy paste from the stdlib's optparse module with the features removed that we do not need from optparse because we implement them in Click on a higher level (for @@ -14,15 +10,45 @@ The reason this is a different module and not optparse from the stdlib is that there are differences in 2.x and 3.x about the error messages generated and optparse in the stdlib uses gettext for no good reason and might cause us issues. + +Click uses parts of optparse written by Gregory P. Ward and maintained +by the Python Software Foundation. This is limited to code in parser.py. + +Copyright 2001-2006 Gregory P. Ward. All rights reserved. +Copyright 2002-2006 Python Software Foundation. All rights reserved. """ - -import re +# This code uses parts of optparse written by Gregory P. Ward and +# maintained by the Python Software Foundation. +# Copyright 2001-2006 Gregory P. Ward +# Copyright 2002-2006 Python Software Foundation +import typing as t from collections import deque -from .exceptions import UsageError, NoSuchOption, BadOptionUsage, \ - BadArgumentUsage +from gettext import gettext as _ +from gettext import ngettext + +from .exceptions import BadArgumentUsage +from .exceptions import BadOptionUsage +from .exceptions import NoSuchOption +from .exceptions import UsageError + +if t.TYPE_CHECKING: + import typing_extensions as te + from .core import Argument as CoreArgument + from .core import Context + from .core import Option as CoreOption + from .core import Parameter as CoreParameter + +V = t.TypeVar("V") + +# Sentinel value that indicates an option was passed as a flag without a +# value but is not a flag option. Option.consume_value uses this to +# prompt or use the flag_value. +_flag_needs_value = object() -def _unpack_args(args, nargs_spec): +def _unpack_args( + args: t.Sequence[str], nargs_spec: t.Sequence[int] +) -> t.Tuple[t.Sequence[t.Union[str, t.Sequence[t.Optional[str]], None]], t.List[str]]: """Given an iterable of arguments and an iterable of nargs specifications, it returns a tuple with all the unpacked arguments at the first index and all remaining arguments as the second. @@ -34,10 +60,10 @@ def _unpack_args(args, nargs_spec): """ args = deque(args) nargs_spec = deque(nargs_spec) - rv = [] - spos = None + rv: t.List[t.Union[str, t.Tuple[t.Optional[str], ...], None]] = [] + spos: t.Optional[int] = None - def _fetch(c): + def _fetch(c: "te.Deque[V]") -> t.Optional[V]: try: if spos is None: return c.popleft() @@ -48,18 +74,25 @@ def _unpack_args(args, nargs_spec): while nargs_spec: nargs = _fetch(nargs_spec) + + if nargs is None: + continue + if nargs == 1: rv.append(_fetch(args)) elif nargs > 1: x = [_fetch(args) for _ in range(nargs)] + # If we're reversed, we're pulling in the arguments in reverse, # so we need to turn them around. if spos is not None: x.reverse() + rv.append(tuple(x)) elif nargs < 0: if spos is not None: - raise TypeError('Cannot have two nargs < 0') + raise TypeError("Cannot have two nargs < 0") + spos = len(rv) rv.append(None) @@ -68,54 +101,71 @@ def _unpack_args(args, nargs_spec): if spos is not None: rv[spos] = tuple(args) args = [] - rv[spos + 1:] = reversed(rv[spos + 1:]) + rv[spos + 1 :] = reversed(rv[spos + 1 :]) return tuple(rv), list(args) -def _error_opt_args(nargs, opt): - if nargs == 1: - raise BadOptionUsage(opt, '%s option requires an argument' % opt) - raise BadOptionUsage(opt, '%s option requires %d arguments' % (opt, nargs)) - - -def split_opt(opt): +def split_opt(opt: str) -> t.Tuple[str, str]: first = opt[:1] if first.isalnum(): - return '', opt + return "", opt if opt[1:2] == first: return opt[:2], opt[2:] return first, opt[1:] -def normalize_opt(opt, ctx): +def normalize_opt(opt: str, ctx: t.Optional["Context"]) -> str: if ctx is None or ctx.token_normalize_func is None: return opt prefix, opt = split_opt(opt) - return prefix + ctx.token_normalize_func(opt) + return f"{prefix}{ctx.token_normalize_func(opt)}" -def split_arg_string(string): - """Given an argument string this attempts to split it into small parts.""" - rv = [] - for match in re.finditer(r"('([^'\\]*(?:\\.[^'\\]*)*)'" - r'|"([^"\\]*(?:\\.[^"\\]*)*)"' - r'|\S+)\s*', string, re.S): - arg = match.group().strip() - if arg[:1] == arg[-1:] and arg[:1] in '"\'': - arg = arg[1:-1].encode('ascii', 'backslashreplace') \ - .decode('unicode-escape') - try: - arg = type(string)(arg) - except UnicodeError: - pass - rv.append(arg) - return rv +def split_arg_string(string: str) -> t.List[str]: + """Split an argument string as with :func:`shlex.split`, but don't + fail if the string is incomplete. Ignores a missing closing quote or + incomplete escape sequence and uses the partial token as-is. + + .. code-block:: python + + split_arg_string("example 'my file") + ["example", "my file"] + + split_arg_string("example my\\") + ["example", "my"] + + :param string: String to split. + """ + import shlex + + lex = shlex.shlex(string, posix=True) + lex.whitespace_split = True + lex.commenters = "" + out = [] + + try: + for token in lex: + out.append(token) + except ValueError: + # Raised when end-of-string is reached in an invalid state. Use + # the partial token as-is. The quote or escape character is in + # lex.state, not lex.token. + out.append(lex.token) + + return out -class Option(object): - - def __init__(self, opts, dest, action=None, nargs=1, const=None, obj=None): +class Option: + def __init__( + self, + obj: "CoreOption", + opts: t.Sequence[str], + dest: t.Optional[str], + action: t.Optional[str] = None, + nargs: int = 1, + const: t.Optional[t.Any] = None, + ): self._short_opts = [] self._long_opts = [] self.prefixes = set() @@ -123,8 +173,7 @@ class Option(object): for opt in opts: prefix, value = split_opt(opt) if not prefix: - raise ValueError('Invalid start character for option (%s)' - % opt) + raise ValueError(f"Invalid start character for option ({opt})") self.prefixes.add(prefix[0]) if len(prefix) == 1 and len(value) == 1: self._short_opts.append(opt) @@ -133,7 +182,7 @@ class Option(object): self.prefixes.add(prefix) if action is None: - action = 'store' + action = "store" self.dest = dest self.action = action @@ -142,54 +191,66 @@ class Option(object): self.obj = obj @property - def takes_value(self): - return self.action in ('store', 'append') + def takes_value(self) -> bool: + return self.action in ("store", "append") - def process(self, value, state): - if self.action == 'store': - state.opts[self.dest] = value - elif self.action == 'store_const': - state.opts[self.dest] = self.const - elif self.action == 'append': - state.opts.setdefault(self.dest, []).append(value) - elif self.action == 'append_const': - state.opts.setdefault(self.dest, []).append(self.const) - elif self.action == 'count': - state.opts[self.dest] = state.opts.get(self.dest, 0) + 1 + def process(self, value: str, state: "ParsingState") -> None: + if self.action == "store": + state.opts[self.dest] = value # type: ignore + elif self.action == "store_const": + state.opts[self.dest] = self.const # type: ignore + elif self.action == "append": + state.opts.setdefault(self.dest, []).append(value) # type: ignore + elif self.action == "append_const": + state.opts.setdefault(self.dest, []).append(self.const) # type: ignore + elif self.action == "count": + state.opts[self.dest] = state.opts.get(self.dest, 0) + 1 # type: ignore else: - raise ValueError('unknown action %r' % self.action) + raise ValueError(f"unknown action '{self.action}'") state.order.append(self.obj) -class Argument(object): - - def __init__(self, dest, nargs=1, obj=None): +class Argument: + def __init__(self, obj: "CoreArgument", dest: t.Optional[str], nargs: int = 1): self.dest = dest self.nargs = nargs self.obj = obj - def process(self, value, state): + def process( + self, + value: t.Union[t.Optional[str], t.Sequence[t.Optional[str]]], + state: "ParsingState", + ) -> None: if self.nargs > 1: + assert value is not None holes = sum(1 for x in value if x is None) if holes == len(value): value = None elif holes != 0: - raise BadArgumentUsage('argument %s takes %d values' - % (self.dest, self.nargs)) - state.opts[self.dest] = value + raise BadArgumentUsage( + _("Argument {name!r} takes {nargs} values.").format( + name=self.dest, nargs=self.nargs + ) + ) + + if self.nargs == -1 and self.obj.envvar is not None and value == (): + # Replace empty tuple with None so that a value from the + # environment may be tried. + value = None + + state.opts[self.dest] = value # type: ignore state.order.append(self.obj) -class ParsingState(object): - - def __init__(self, rargs): - self.opts = {} - self.largs = [] +class ParsingState: + def __init__(self, rargs: t.List[str]) -> None: + self.opts: t.Dict[str, t.Any] = {} + self.largs: t.List[str] = [] self.rargs = rargs - self.order = [] + self.order: t.List["CoreParameter"] = [] -class OptionParser(object): +class OptionParser: """The option parser is an internal class that is ultimately used to parse options and arguments. It's modelled after optparse and brings a similar but vastly simplified API. It should generally not be used @@ -203,7 +264,7 @@ class OptionParser(object): should go with. """ - def __init__(self, ctx=None): + def __init__(self, ctx: t.Optional["Context"] = None) -> None: #: The :class:`~click.Context` for this parser. This might be #: `None` for some advanced use cases. self.ctx = ctx @@ -217,46 +278,54 @@ class OptionParser(object): #: second mode where it will ignore it and continue processing #: after shifting all the unknown options into the resulting args. self.ignore_unknown_options = False + if ctx is not None: self.allow_interspersed_args = ctx.allow_interspersed_args self.ignore_unknown_options = ctx.ignore_unknown_options - self._short_opt = {} - self._long_opt = {} - self._opt_prefixes = set(['-', '--']) - self._args = [] - def add_option(self, opts, dest, action=None, nargs=1, const=None, - obj=None): + self._short_opt: t.Dict[str, Option] = {} + self._long_opt: t.Dict[str, Option] = {} + self._opt_prefixes = {"-", "--"} + self._args: t.List[Argument] = [] + + def add_option( + self, + obj: "CoreOption", + opts: t.Sequence[str], + dest: t.Optional[str], + action: t.Optional[str] = None, + nargs: int = 1, + const: t.Optional[t.Any] = None, + ) -> None: """Adds a new option named `dest` to the parser. The destination is not inferred (unlike with optparse) and needs to be explicitly provided. Action can be any of ``store``, ``store_const``, - ``append``, ``appnd_const`` or ``count``. + ``append``, ``append_const`` or ``count``. The `obj` can be used to identify the option in the order list that is returned from the parser. """ - if obj is None: - obj = dest opts = [normalize_opt(opt, self.ctx) for opt in opts] - option = Option(opts, dest, action=action, nargs=nargs, - const=const, obj=obj) + option = Option(obj, opts, dest, action=action, nargs=nargs, const=const) self._opt_prefixes.update(option.prefixes) for opt in option._short_opts: self._short_opt[opt] = option for opt in option._long_opts: self._long_opt[opt] = option - def add_argument(self, dest, nargs=1, obj=None): + def add_argument( + self, obj: "CoreArgument", dest: t.Optional[str], nargs: int = 1 + ) -> None: """Adds a positional argument named `dest` to the parser. The `obj` can be used to identify the option in the order list that is returned from the parser. """ - if obj is None: - obj = dest - self._args.append(Argument(dest=dest, nargs=nargs, obj=obj)) + self._args.append(Argument(obj, dest=dest, nargs=nargs)) - def parse_args(self, args): + def parse_args( + self, args: t.List[str] + ) -> t.Tuple[t.Dict[str, t.Any], t.List[str], t.List["CoreParameter"]]: """Parses positional arguments and returns ``(values, args, order)`` for the parsed options and arguments as well as the leftover arguments if there are any. The order is a list of objects as they @@ -272,9 +341,10 @@ class OptionParser(object): raise return state.opts, state.largs, state.order - def _process_args_for_args(self, state): - pargs, args = _unpack_args(state.largs + state.rargs, - [x.nargs for x in self._args]) + def _process_args_for_args(self, state: ParsingState) -> None: + pargs, args = _unpack_args( + state.largs + state.rargs, [x.nargs for x in self._args] + ) for idx, arg in enumerate(self._args): arg.process(pargs[idx], state) @@ -282,13 +352,13 @@ class OptionParser(object): state.largs = args state.rargs = [] - def _process_args_for_options(self, state): + def _process_args_for_options(self, state: ParsingState) -> None: while state.rargs: arg = state.rargs.pop(0) arglen = len(arg) # Double dashes always handled explicitly regardless of what # prefixes are valid. - if arg == '--': + if arg == "--": return elif arg[:1] in self._opt_prefixes and arglen > 1: self._process_opts(arg, state) @@ -318,10 +388,13 @@ class OptionParser(object): # *empty* -- still a subset of [arg0, ..., arg(i-1)], but # not a very interesting subset! - def _match_long_opt(self, opt, explicit_value, state): + def _match_long_opt( + self, opt: str, explicit_value: t.Optional[str], state: ParsingState + ) -> None: if opt not in self._long_opt: - possibilities = [word for word in self._long_opt - if word.startswith(opt)] + from difflib import get_close_matches + + possibilities = get_close_matches(opt, self._long_opt) raise NoSuchOption(opt, possibilities=possibilities, ctx=self.ctx) option = self._long_opt[opt] @@ -333,31 +406,26 @@ class OptionParser(object): if explicit_value is not None: state.rargs.insert(0, explicit_value) - nargs = option.nargs - if len(state.rargs) < nargs: - _error_opt_args(nargs, opt) - elif nargs == 1: - value = state.rargs.pop(0) - else: - value = tuple(state.rargs[:nargs]) - del state.rargs[:nargs] + value = self._get_value_from_state(opt, option, state) elif explicit_value is not None: - raise BadOptionUsage(opt, '%s option does not take a value' % opt) + raise BadOptionUsage( + opt, _("Option {name!r} does not take a value.").format(name=opt) + ) else: value = None option.process(value, state) - def _match_short_opt(self, arg, state): + def _match_short_opt(self, arg: str, state: ParsingState) -> None: stop = False i = 1 prefix = arg[0] unknown_options = [] for ch in arg[1:]: - opt = normalize_opt(prefix + ch, self.ctx) + opt = normalize_opt(f"{prefix}{ch}", self.ctx) option = self._short_opt.get(opt) i += 1 @@ -373,14 +441,7 @@ class OptionParser(object): state.rargs.insert(0, arg[i:]) stop = True - nargs = option.nargs - if len(state.rargs) < nargs: - _error_opt_args(nargs, opt) - elif nargs == 1: - value = state.rargs.pop(0) - else: - value = tuple(state.rargs[:nargs]) - del state.rargs[:nargs] + value = self._get_value_from_state(opt, option, state) else: value = None @@ -395,15 +456,53 @@ class OptionParser(object): # to the state as new larg. This way there is basic combinatorics # that can be achieved while still ignoring unknown arguments. if self.ignore_unknown_options and unknown_options: - state.largs.append(prefix + ''.join(unknown_options)) + state.largs.append(f"{prefix}{''.join(unknown_options)}") - def _process_opts(self, arg, state): + def _get_value_from_state( + self, option_name: str, option: Option, state: ParsingState + ) -> t.Any: + nargs = option.nargs + + if len(state.rargs) < nargs: + if option.obj._flag_needs_value: + # Option allows omitting the value. + value = _flag_needs_value + else: + raise BadOptionUsage( + option_name, + ngettext( + "Option {name!r} requires an argument.", + "Option {name!r} requires {nargs} arguments.", + nargs, + ).format(name=option_name, nargs=nargs), + ) + elif nargs == 1: + next_rarg = state.rargs[0] + + if ( + option.obj._flag_needs_value + and isinstance(next_rarg, str) + and next_rarg[:1] in self._opt_prefixes + and len(next_rarg) > 1 + ): + # The next arg looks like the start of an option, don't + # use it as the value if omitting the value is allowed. + value = _flag_needs_value + else: + value = state.rargs.pop(0) + else: + value = tuple(state.rargs[:nargs]) + del state.rargs[:nargs] + + return value + + def _process_opts(self, arg: str, state: ParsingState) -> None: explicit_value = None # Long option handling happens in two parts. The first part is # supporting explicitly attached values. In any case, we will try # to long match the option first. - if '=' in arg: - long_opt, explicit_value = arg.split('=', 1) + if "=" in arg: + long_opt, explicit_value = arg.split("=", 1) else: long_opt = arg norm_long_opt = normalize_opt(long_opt, self.ctx) @@ -421,7 +520,10 @@ class OptionParser(object): # short option code and will instead raise the no option # error. if arg[:2] not in self._opt_prefixes: - return self._match_short_opt(arg, state) + self._match_short_opt(arg, state) + return + if not self.ignore_unknown_options: raise + state.largs.append(arg) diff --git a/libs/git/test/fixtures/ls_tree_empty b/libs/click/py.typed similarity index 100% rename from libs/git/test/fixtures/ls_tree_empty rename to libs/click/py.typed diff --git a/libs/click/shell_completion.py b/libs/click/shell_completion.py new file mode 100644 index 000000000..cad080da6 --- /dev/null +++ b/libs/click/shell_completion.py @@ -0,0 +1,581 @@ +import os +import re +import typing as t +from gettext import gettext as _ + +from .core import Argument +from .core import BaseCommand +from .core import Context +from .core import MultiCommand +from .core import Option +from .core import Parameter +from .core import ParameterSource +from .parser import split_arg_string +from .utils import echo + + +def shell_complete( + cli: BaseCommand, + ctx_args: t.Dict[str, t.Any], + prog_name: str, + complete_var: str, + instruction: str, +) -> int: + """Perform shell completion for the given CLI program. + + :param cli: Command being called. + :param ctx_args: Extra arguments to pass to + ``cli.make_context``. + :param prog_name: Name of the executable in the shell. + :param complete_var: Name of the environment variable that holds + the completion instruction. + :param instruction: Value of ``complete_var`` with the completion + instruction and shell, in the form ``instruction_shell``. + :return: Status code to exit with. + """ + shell, _, instruction = instruction.partition("_") + comp_cls = get_completion_class(shell) + + if comp_cls is None: + return 1 + + comp = comp_cls(cli, ctx_args, prog_name, complete_var) + + if instruction == "source": + echo(comp.source()) + return 0 + + if instruction == "complete": + echo(comp.complete()) + return 0 + + return 1 + + +class CompletionItem: + """Represents a completion value and metadata about the value. The + default metadata is ``type`` to indicate special shell handling, + and ``help`` if a shell supports showing a help string next to the + value. + + Arbitrary parameters can be passed when creating the object, and + accessed using ``item.attr``. If an attribute wasn't passed, + accessing it returns ``None``. + + :param value: The completion suggestion. + :param type: Tells the shell script to provide special completion + support for the type. Click uses ``"dir"`` and ``"file"``. + :param help: String shown next to the value if supported. + :param kwargs: Arbitrary metadata. The built-in implementations + don't use this, but custom type completions paired with custom + shell support could use it. + """ + + __slots__ = ("value", "type", "help", "_info") + + def __init__( + self, + value: t.Any, + type: str = "plain", + help: t.Optional[str] = None, + **kwargs: t.Any, + ) -> None: + self.value = value + self.type = type + self.help = help + self._info = kwargs + + def __getattr__(self, name: str) -> t.Any: + return self._info.get(name) + + +# Only Bash >= 4.4 has the nosort option. +_SOURCE_BASH = """\ +%(complete_func)s() { + local IFS=$'\\n' + local response + + response=$(env COMP_WORDS="${COMP_WORDS[*]}" COMP_CWORD=$COMP_CWORD \ +%(complete_var)s=bash_complete $1) + + for completion in $response; do + IFS=',' read type value <<< "$completion" + + if [[ $type == 'dir' ]]; then + COMREPLY=() + compopt -o dirnames + elif [[ $type == 'file' ]]; then + COMREPLY=() + compopt -o default + elif [[ $type == 'plain' ]]; then + COMPREPLY+=($value) + fi + done + + return 0 +} + +%(complete_func)s_setup() { + complete -o nosort -F %(complete_func)s %(prog_name)s +} + +%(complete_func)s_setup; +""" + +_SOURCE_ZSH = """\ +#compdef %(prog_name)s + +%(complete_func)s() { + local -a completions + local -a completions_with_descriptions + local -a response + (( ! $+commands[%(prog_name)s] )) && return 1 + + response=("${(@f)$(env COMP_WORDS="${words[*]}" COMP_CWORD=$((CURRENT-1)) \ +%(complete_var)s=zsh_complete %(prog_name)s)}") + + for type key descr in ${response}; do + if [[ "$type" == "plain" ]]; then + if [[ "$descr" == "_" ]]; then + completions+=("$key") + else + completions_with_descriptions+=("$key":"$descr") + fi + elif [[ "$type" == "dir" ]]; then + _path_files -/ + elif [[ "$type" == "file" ]]; then + _path_files -f + fi + done + + if [ -n "$completions_with_descriptions" ]; then + _describe -V unsorted completions_with_descriptions -U + fi + + if [ -n "$completions" ]; then + compadd -U -V unsorted -a completions + fi +} + +compdef %(complete_func)s %(prog_name)s; +""" + +_SOURCE_FISH = """\ +function %(complete_func)s; + set -l response; + + for value in (env %(complete_var)s=fish_complete COMP_WORDS=(commandline -cp) \ +COMP_CWORD=(commandline -t) %(prog_name)s); + set response $response $value; + end; + + for completion in $response; + set -l metadata (string split "," $completion); + + if test $metadata[1] = "dir"; + __fish_complete_directories $metadata[2]; + else if test $metadata[1] = "file"; + __fish_complete_path $metadata[2]; + else if test $metadata[1] = "plain"; + echo $metadata[2]; + end; + end; +end; + +complete --no-files --command %(prog_name)s --arguments \ +"(%(complete_func)s)"; +""" + + +class ShellComplete: + """Base class for providing shell completion support. A subclass for + a given shell will override attributes and methods to implement the + completion instructions (``source`` and ``complete``). + + :param cli: Command being called. + :param prog_name: Name of the executable in the shell. + :param complete_var: Name of the environment variable that holds + the completion instruction. + + .. versionadded:: 8.0 + """ + + name: t.ClassVar[str] + """Name to register the shell as with :func:`add_completion_class`. + This is used in completion instructions (``{name}_source`` and + ``{name}_complete``). + """ + + source_template: t.ClassVar[str] + """Completion script template formatted by :meth:`source`. This must + be provided by subclasses. + """ + + def __init__( + self, + cli: BaseCommand, + ctx_args: t.Dict[str, t.Any], + prog_name: str, + complete_var: str, + ) -> None: + self.cli = cli + self.ctx_args = ctx_args + self.prog_name = prog_name + self.complete_var = complete_var + + @property + def func_name(self) -> str: + """The name of the shell function defined by the completion + script. + """ + safe_name = re.sub(r"\W*", "", self.prog_name.replace("-", "_"), re.ASCII) + return f"_{safe_name}_completion" + + def source_vars(self) -> t.Dict[str, t.Any]: + """Vars for formatting :attr:`source_template`. + + By default this provides ``complete_func``, ``complete_var``, + and ``prog_name``. + """ + return { + "complete_func": self.func_name, + "complete_var": self.complete_var, + "prog_name": self.prog_name, + } + + def source(self) -> str: + """Produce the shell script that defines the completion + function. By default this ``%``-style formats + :attr:`source_template` with the dict returned by + :meth:`source_vars`. + """ + return self.source_template % self.source_vars() + + def get_completion_args(self) -> t.Tuple[t.List[str], str]: + """Use the env vars defined by the shell script to return a + tuple of ``args, incomplete``. This must be implemented by + subclasses. + """ + raise NotImplementedError + + def get_completions( + self, args: t.List[str], incomplete: str + ) -> t.List[CompletionItem]: + """Determine the context and last complete command or parameter + from the complete args. Call that object's ``shell_complete`` + method to get the completions for the incomplete value. + + :param args: List of complete args before the incomplete value. + :param incomplete: Value being completed. May be empty. + """ + ctx = _resolve_context(self.cli, self.ctx_args, self.prog_name, args) + obj, incomplete = _resolve_incomplete(ctx, args, incomplete) + return obj.shell_complete(ctx, incomplete) + + def format_completion(self, item: CompletionItem) -> str: + """Format a completion item into the form recognized by the + shell script. This must be implemented by subclasses. + + :param item: Completion item to format. + """ + raise NotImplementedError + + def complete(self) -> str: + """Produce the completion data to send back to the shell. + + By default this calls :meth:`get_completion_args`, gets the + completions, then calls :meth:`format_completion` for each + completion. + """ + args, incomplete = self.get_completion_args() + completions = self.get_completions(args, incomplete) + out = [self.format_completion(item) for item in completions] + return "\n".join(out) + + +class BashComplete(ShellComplete): + """Shell completion for Bash.""" + + name = "bash" + source_template = _SOURCE_BASH + + def _check_version(self) -> None: + import subprocess + + output = subprocess.run( + ["bash", "-c", "echo ${BASH_VERSION}"], stdout=subprocess.PIPE + ) + match = re.search(r"^(\d+)\.(\d+)\.\d+", output.stdout.decode()) + + if match is not None: + major, minor = match.groups() + + if major < "4" or major == "4" and minor < "4": + raise RuntimeError( + _( + "Shell completion is not supported for Bash" + " versions older than 4.4." + ) + ) + else: + raise RuntimeError( + _("Couldn't detect Bash version, shell completion is not supported.") + ) + + def source(self) -> str: + self._check_version() + return super().source() + + def get_completion_args(self) -> t.Tuple[t.List[str], str]: + cwords = split_arg_string(os.environ["COMP_WORDS"]) + cword = int(os.environ["COMP_CWORD"]) + args = cwords[1:cword] + + try: + incomplete = cwords[cword] + except IndexError: + incomplete = "" + + return args, incomplete + + def format_completion(self, item: CompletionItem) -> str: + return f"{item.type},{item.value}" + + +class ZshComplete(ShellComplete): + """Shell completion for Zsh.""" + + name = "zsh" + source_template = _SOURCE_ZSH + + def get_completion_args(self) -> t.Tuple[t.List[str], str]: + cwords = split_arg_string(os.environ["COMP_WORDS"]) + cword = int(os.environ["COMP_CWORD"]) + args = cwords[1:cword] + + try: + incomplete = cwords[cword] + except IndexError: + incomplete = "" + + return args, incomplete + + def format_completion(self, item: CompletionItem) -> str: + return f"{item.type}\n{item.value}\n{item.help if item.help else '_'}" + + +class FishComplete(ShellComplete): + """Shell completion for Fish.""" + + name = "fish" + source_template = _SOURCE_FISH + + def get_completion_args(self) -> t.Tuple[t.List[str], str]: + cwords = split_arg_string(os.environ["COMP_WORDS"]) + incomplete = os.environ["COMP_CWORD"] + args = cwords[1:] + + # Fish stores the partial word in both COMP_WORDS and + # COMP_CWORD, remove it from complete args. + if incomplete and args and args[-1] == incomplete: + args.pop() + + return args, incomplete + + def format_completion(self, item: CompletionItem) -> str: + if item.help: + return f"{item.type},{item.value}\t{item.help}" + + return f"{item.type},{item.value}" + + +_available_shells: t.Dict[str, t.Type[ShellComplete]] = { + "bash": BashComplete, + "fish": FishComplete, + "zsh": ZshComplete, +} + + +def add_completion_class( + cls: t.Type[ShellComplete], name: t.Optional[str] = None +) -> None: + """Register a :class:`ShellComplete` subclass under the given name. + The name will be provided by the completion instruction environment + variable during completion. + + :param cls: The completion class that will handle completion for the + shell. + :param name: Name to register the class under. Defaults to the + class's ``name`` attribute. + """ + if name is None: + name = cls.name + + _available_shells[name] = cls + + +def get_completion_class(shell: str) -> t.Optional[t.Type[ShellComplete]]: + """Look up a registered :class:`ShellComplete` subclass by the name + provided by the completion instruction environment variable. If the + name isn't registered, returns ``None``. + + :param shell: Name the class is registered under. + """ + return _available_shells.get(shell) + + +def _is_incomplete_argument(ctx: Context, param: Parameter) -> bool: + """Determine if the given parameter is an argument that can still + accept values. + + :param ctx: Invocation context for the command represented by the + parsed complete args. + :param param: Argument object being checked. + """ + if not isinstance(param, Argument): + return False + + assert param.name is not None + value = ctx.params[param.name] + return ( + param.nargs == -1 + or ctx.get_parameter_source(param.name) is not ParameterSource.COMMANDLINE + or ( + param.nargs > 1 + and isinstance(value, (tuple, list)) + and len(value) < param.nargs + ) + ) + + +def _start_of_option(value: str) -> bool: + """Check if the value looks like the start of an option.""" + if not value: + return False + + c = value[0] + # Allow "/" since that starts a path. + return not c.isalnum() and c != "/" + + +def _is_incomplete_option(args: t.List[str], param: Parameter) -> bool: + """Determine if the given parameter is an option that needs a value. + + :param args: List of complete args before the incomplete value. + :param param: Option object being checked. + """ + if not isinstance(param, Option): + return False + + if param.is_flag: + return False + + last_option = None + + for index, arg in enumerate(reversed(args)): + if index + 1 > param.nargs: + break + + if _start_of_option(arg): + last_option = arg + + return last_option is not None and last_option in param.opts + + +def _resolve_context( + cli: BaseCommand, ctx_args: t.Dict[str, t.Any], prog_name: str, args: t.List[str] +) -> Context: + """Produce the context hierarchy starting with the command and + traversing the complete arguments. This only follows the commands, + it doesn't trigger input prompts or callbacks. + + :param cli: Command being called. + :param prog_name: Name of the executable in the shell. + :param args: List of complete args before the incomplete value. + """ + ctx_args["resilient_parsing"] = True + ctx = cli.make_context(prog_name, args.copy(), **ctx_args) + args = ctx.protected_args + ctx.args + + while args: + command = ctx.command + + if isinstance(command, MultiCommand): + if not command.chain: + name, cmd, args = command.resolve_command(ctx, args) + + if cmd is None: + return ctx + + ctx = cmd.make_context(name, args, parent=ctx, resilient_parsing=True) + args = ctx.protected_args + ctx.args + else: + while args: + name, cmd, args = command.resolve_command(ctx, args) + + if cmd is None: + return ctx + + sub_ctx = cmd.make_context( + name, + args, + parent=ctx, + allow_extra_args=True, + allow_interspersed_args=False, + resilient_parsing=True, + ) + args = sub_ctx.args + + ctx = sub_ctx + args = [*sub_ctx.protected_args, *sub_ctx.args] + else: + break + + return ctx + + +def _resolve_incomplete( + ctx: Context, args: t.List[str], incomplete: str +) -> t.Tuple[t.Union[BaseCommand, Parameter], str]: + """Find the Click object that will handle the completion of the + incomplete value. Return the object and the incomplete value. + + :param ctx: Invocation context for the command represented by + the parsed complete args. + :param args: List of complete args before the incomplete value. + :param incomplete: Value being completed. May be empty. + """ + # Different shells treat an "=" between a long option name and + # value differently. Might keep the value joined, return the "=" + # as a separate item, or return the split name and value. Always + # split and discard the "=" to make completion easier. + if incomplete == "=": + incomplete = "" + elif "=" in incomplete and _start_of_option(incomplete): + name, _, incomplete = incomplete.partition("=") + args.append(name) + + # The "--" marker tells Click to stop treating values as options + # even if they start with the option character. If it hasn't been + # given and the incomplete arg looks like an option, the current + # command will provide option name completions. + if "--" not in args and _start_of_option(incomplete): + return ctx.command, incomplete + + params = ctx.command.get_params(ctx) + + # If the last complete arg is an option name with an incomplete + # value, the option will provide value completions. + for param in params: + if _is_incomplete_option(args, param): + return param, incomplete + + # It's not an option name or value. The first argument without a + # parsed value will provide value completions. + for param in params: + if _is_incomplete_argument(ctx, param): + return param, incomplete + + # There were no unparsed arguments, the command may be a group that + # will provide command name completions. + return ctx.command, incomplete diff --git a/libs/click/termui.py b/libs/click/termui.py index bf9a3aa16..cf8d5f132 100644 --- a/libs/click/termui.py +++ b/libs/click/termui.py @@ -1,81 +1,110 @@ +import inspect +import io +import itertools import os import sys -import struct -import inspect -import itertools +import typing +import typing as t +from gettext import gettext as _ -from ._compat import raw_input, text_type, string_types, \ - isatty, strip_ansi, get_winterm_size, DEFAULT_COLUMNS, WIN -from .utils import echo -from .exceptions import Abort, UsageError -from .types import convert_type, Choice, Path +from ._compat import isatty +from ._compat import strip_ansi +from ._compat import WIN +from .exceptions import Abort +from .exceptions import UsageError from .globals import resolve_color_default +from .types import Choice +from .types import convert_type +from .types import ParamType +from .utils import echo +from .utils import LazyFile +if t.TYPE_CHECKING: + from ._termui_impl import ProgressBar + +V = t.TypeVar("V") # The prompt functions to use. The doc tools currently override these # functions to customize how they work. -visible_prompt_func = raw_input +visible_prompt_func: t.Callable[[str], str] = input _ansi_colors = { - 'black': 30, - 'red': 31, - 'green': 32, - 'yellow': 33, - 'blue': 34, - 'magenta': 35, - 'cyan': 36, - 'white': 37, - 'reset': 39, - 'bright_black': 90, - 'bright_red': 91, - 'bright_green': 92, - 'bright_yellow': 93, - 'bright_blue': 94, - 'bright_magenta': 95, - 'bright_cyan': 96, - 'bright_white': 97, + "black": 30, + "red": 31, + "green": 32, + "yellow": 33, + "blue": 34, + "magenta": 35, + "cyan": 36, + "white": 37, + "reset": 39, + "bright_black": 90, + "bright_red": 91, + "bright_green": 92, + "bright_yellow": 93, + "bright_blue": 94, + "bright_magenta": 95, + "bright_cyan": 96, + "bright_white": 97, } -_ansi_reset_all = '\033[0m' +_ansi_reset_all = "\033[0m" -def hidden_prompt_func(prompt): +def hidden_prompt_func(prompt: str) -> str: import getpass + return getpass.getpass(prompt) -def _build_prompt(text, suffix, show_default=False, default=None, show_choices=True, type=None): +def _build_prompt( + text: str, + suffix: str, + show_default: bool = False, + default: t.Optional[t.Any] = None, + show_choices: bool = True, + type: t.Optional[ParamType] = None, +) -> str: prompt = text if type is not None and show_choices and isinstance(type, Choice): - prompt += ' (' + ", ".join(map(str, type.choices)) + ')' + prompt += f" ({', '.join(map(str, type.choices))})" if default is not None and show_default: - prompt = '%s [%s]' % (prompt, default) - return prompt + suffix + prompt = f"{prompt} [{_format_default(default)}]" + return f"{prompt}{suffix}" -def prompt(text, default=None, hide_input=False, confirmation_prompt=False, - type=None, value_proc=None, prompt_suffix=': ', show_default=True, - err=False, show_choices=True): +def _format_default(default: t.Any) -> t.Any: + if isinstance(default, (io.IOBase, LazyFile)) and hasattr(default, "name"): + return default.name # type: ignore + + return default + + +def prompt( + text: str, + default: t.Optional[t.Any] = None, + hide_input: bool = False, + confirmation_prompt: t.Union[bool, str] = False, + type: t.Optional[t.Union[ParamType, t.Any]] = None, + value_proc: t.Optional[t.Callable[[str], t.Any]] = None, + prompt_suffix: str = ": ", + show_default: bool = True, + err: bool = False, + show_choices: bool = True, +) -> t.Any: """Prompts a user for input. This is a convenience function that can be used to prompt a user for input later. If the user aborts the input by sending a interrupt signal, this function will catch it and raise a :exc:`Abort` exception. - .. versionadded:: 7.0 - Added the show_choices parameter. - - .. versionadded:: 6.0 - Added unicode support for cmd.exe on Windows. - - .. versionadded:: 4.0 - Added the `err` parameter. - :param text: the text to show for the prompt. :param default: the default value to use if no input happens. If this is not given it will prompt until it's aborted. :param hide_input: if this is set to true then the input value will be hidden. - :param confirmation_prompt: asks for confirmation for the value. + :param confirmation_prompt: Prompt a second time to confirm the + value. Can be set to a string instead of ``True`` to customize + the message. :param type: the type to use to check the value against. :param value_proc: if this parameter is provided it's a function that is invoked instead of the type conversion to @@ -88,93 +117,134 @@ def prompt(text, default=None, hide_input=False, confirmation_prompt=False, For example if type is a Choice of either day or week, show_choices is true and text is "Group by" then the prompt will be "Group by (day, week): ". - """ - result = None - def prompt_func(text): - f = hide_input and hidden_prompt_func or visible_prompt_func + .. versionadded:: 8.0 + ``confirmation_prompt`` can be a custom string. + + .. versionadded:: 7.0 + Added the ``show_choices`` parameter. + + .. versionadded:: 6.0 + Added unicode support for cmd.exe on Windows. + + .. versionadded:: 4.0 + Added the `err` parameter. + + """ + + def prompt_func(text: str) -> str: + f = hidden_prompt_func if hide_input else visible_prompt_func try: # Write the prompt separately so that we get nice # coloring through colorama on Windows - echo(text, nl=False, err=err) - return f('') + echo(text.rstrip(" "), nl=False, err=err) + # Echo a space to stdout to work around an issue where + # readline causes backspace to clear the whole line. + return f(" ") except (KeyboardInterrupt, EOFError): # getpass doesn't print a newline if the user aborts input with ^C. # Allegedly this behavior is inherited from getpass(3). # A doc bug has been filed at https://bugs.python.org/issue24711 if hide_input: echo(None, err=err) - raise Abort() + raise Abort() from None if value_proc is None: value_proc = convert_type(type, default) - prompt = _build_prompt(text, prompt_suffix, show_default, default, show_choices, type) + prompt = _build_prompt( + text, prompt_suffix, show_default, default, show_choices, type + ) - while 1: - while 1: + if confirmation_prompt: + if confirmation_prompt is True: + confirmation_prompt = _("Repeat for confirmation") + + confirmation_prompt = t.cast(str, confirmation_prompt) + confirmation_prompt = _build_prompt(confirmation_prompt, prompt_suffix) + + while True: + while True: value = prompt_func(prompt) if value: break elif default is not None: - if isinstance(value_proc, Path): - # validate Path default value(exists, dir_okay etc.) - value = default - break - return default + value = default + break try: result = value_proc(value) except UsageError as e: - echo('Error: %s' % e.message, err=err) + if hide_input: + echo(_("Error: The value you entered was invalid."), err=err) + else: + echo(_("Error: {e.message}").format(e=e), err=err) # noqa: B306 continue if not confirmation_prompt: return result - while 1: - value2 = prompt_func('Repeat for confirmation: ') + while True: + confirmation_prompt = t.cast(str, confirmation_prompt) + value2 = prompt_func(confirmation_prompt) if value2: break if value == value2: return result - echo('Error: the two entered values do not match', err=err) + echo(_("Error: The two entered values do not match."), err=err) -def confirm(text, default=False, abort=False, prompt_suffix=': ', - show_default=True, err=False): +def confirm( + text: str, + default: t.Optional[bool] = False, + abort: bool = False, + prompt_suffix: str = ": ", + show_default: bool = True, + err: bool = False, +) -> bool: """Prompts for confirmation (yes/no question). If the user aborts the input by sending a interrupt signal this function will catch it and raise a :exc:`Abort` exception. - .. versionadded:: 4.0 - Added the `err` parameter. - :param text: the question to ask. - :param default: the default for the prompt. + :param default: The default value to use when no input is given. If + ``None``, repeat until input is given. :param abort: if this is set to `True` a negative answer aborts the exception by raising :exc:`Abort`. :param prompt_suffix: a suffix that should be added to the prompt. :param show_default: shows or hides the default value in the prompt. :param err: if set to true the file defaults to ``stderr`` instead of ``stdout``, the same as with echo. + + .. versionchanged:: 8.0 + Repeat until input is given if ``default`` is ``None``. + + .. versionadded:: 4.0 + Added the ``err`` parameter. """ - prompt = _build_prompt(text, prompt_suffix, show_default, - default and 'Y/n' or 'y/N') - while 1: + prompt = _build_prompt( + text, + prompt_suffix, + show_default, + "y/n" if default is None else ("Y/n" if default else "y/N"), + ) + + while True: try: # Write the prompt separately so that we get nice # coloring through colorama on Windows - echo(prompt, nl=False, err=err) - value = visible_prompt_func('').lower().strip() + echo(prompt.rstrip(" "), nl=False, err=err) + # Echo a space to stdout to work around an issue where + # readline causes backspace to clear the whole line. + value = visible_prompt_func(" ").lower().strip() except (KeyboardInterrupt, EOFError): - raise Abort() - if value in ('y', 'yes'): + raise Abort() from None + if value in ("y", "yes"): rv = True - elif value in ('n', 'no'): + elif value in ("n", "no"): rv = False - elif value == '': + elif default is not None and value == "": rv = default else: - echo('Error: invalid input', err=err) + echo(_("Error: invalid input"), err=err) continue break if abort and not rv: @@ -182,54 +252,30 @@ def confirm(text, default=False, abort=False, prompt_suffix=': ', return rv -def get_terminal_size(): +def get_terminal_size() -> os.terminal_size: """Returns the current size of the terminal as tuple in the form ``(width, height)`` in columns and rows. + + .. deprecated:: 8.0 + Will be removed in Click 8.1. Use + :func:`shutil.get_terminal_size` instead. """ - # If shutil has get_terminal_size() (Python 3.3 and later) use that - if sys.version_info >= (3, 3): - import shutil - shutil_get_terminal_size = getattr(shutil, 'get_terminal_size', None) - if shutil_get_terminal_size: - sz = shutil_get_terminal_size() - return sz.columns, sz.lines + import shutil + import warnings - # We provide a sensible default for get_winterm_size() when being invoked - # inside a subprocess. Without this, it would not provide a useful input. - if get_winterm_size is not None: - size = get_winterm_size() - if size == (0, 0): - return (79, 24) - else: - return size - - def ioctl_gwinsz(fd): - try: - import fcntl - import termios - cr = struct.unpack( - 'hh', fcntl.ioctl(fd, termios.TIOCGWINSZ, '1234')) - except Exception: - return - return cr - - cr = ioctl_gwinsz(0) or ioctl_gwinsz(1) or ioctl_gwinsz(2) - if not cr: - try: - fd = os.open(os.ctermid(), os.O_RDONLY) - try: - cr = ioctl_gwinsz(fd) - finally: - os.close(fd) - except Exception: - pass - if not cr or not cr[0] or not cr[1]: - cr = (os.environ.get('LINES', 25), - os.environ.get('COLUMNS', DEFAULT_COLUMNS)) - return int(cr[1]), int(cr[0]) + warnings.warn( + "'click.get_terminal_size()' is deprecated and will be removed" + " in Click 8.1. Use 'shutil.get_terminal_size()' instead.", + DeprecationWarning, + stacklevel=2, + ) + return shutil.get_terminal_size() -def echo_via_pager(text_or_generator, color=None): +def echo_via_pager( + text_or_generator: t.Union[t.Iterable[str], t.Callable[[], t.Iterable[str]], str], + color: t.Optional[bool] = None, +) -> None: """This function takes a text and shows it via an environment specific pager on stdout. @@ -244,25 +290,37 @@ def echo_via_pager(text_or_generator, color=None): color = resolve_color_default(color) if inspect.isgeneratorfunction(text_or_generator): - i = text_or_generator() - elif isinstance(text_or_generator, string_types): + i = t.cast(t.Callable[[], t.Iterable[str]], text_or_generator)() + elif isinstance(text_or_generator, str): i = [text_or_generator] else: - i = iter(text_or_generator) + i = iter(t.cast(t.Iterable[str], text_or_generator)) # convert every element of i to a text type if necessary - text_generator = (el if isinstance(el, string_types) else text_type(el) - for el in i) + text_generator = (el if isinstance(el, str) else str(el) for el in i) from ._termui_impl import pager + return pager(itertools.chain(text_generator, "\n"), color) -def progressbar(iterable=None, length=None, label=None, show_eta=True, - show_percent=None, show_pos=False, - item_show_func=None, fill_char='#', empty_char='-', - bar_template='%(label)s [%(bar)s] %(info)s', - info_sep=' ', width=36, file=None, color=None): +def progressbar( + iterable: t.Optional[t.Iterable[V]] = None, + length: t.Optional[int] = None, + label: t.Optional[str] = None, + show_eta: bool = True, + show_percent: t.Optional[bool] = None, + show_pos: bool = False, + item_show_func: t.Optional[t.Callable[[t.Optional[V]], t.Optional[str]]] = None, + fill_char: str = "#", + empty_char: str = "-", + bar_template: str = "%(label)s [%(bar)s] %(info)s", + info_sep: str = " ", + width: int = 36, + file: t.Optional[t.TextIO] = None, + color: t.Optional[bool] = None, + update_min_steps: int = 1, +) -> "ProgressBar[V]": """This function creates an iterable context manager that can be used to iterate over something while showing a progress bar. It will either iterate over the `iterable` or `length` items (that are counted @@ -272,11 +330,17 @@ def progressbar(iterable=None, length=None, label=None, show_eta=True, will not be rendered if the file is not a terminal. The context manager creates the progress bar. When the context - manager is entered the progress bar is already displayed. With every + manager is entered the progress bar is already created. With every iteration over the progress bar, the iterable passed to the bar is advanced and the bar is updated. When the context manager exits, a newline is printed and the progress bar is finalized on screen. + Note: The progress bar is currently designed for use cases where the + total progress can be expected to take at least several seconds. + Because of this, the ProgressBar class object won't display + progress that is considered too fast, and progress where the time + between steps is less than a second. + No printing must happen or the progress bar will be unintentionally destroyed. @@ -296,11 +360,19 @@ def progressbar(iterable=None, length=None, label=None, show_eta=True, process_chunk(chunk) bar.update(chunks.bytes) - .. versionadded:: 2.0 + The ``update()`` method also takes an optional value specifying the + ``current_item`` at the new position. This is useful when used + together with ``item_show_func`` to customize the output for each + manual step:: - .. versionadded:: 4.0 - Added the `color` parameter. Added a `update` method to the - progressbar object. + with click.progressbar( + length=total_size, + label='Unzipping archive', + item_show_func=lambda a: a.filename + ) as bar: + for archive in zip_file: + archive.extract() + bar.update(archive.size, archive) :param iterable: an iterable to iterate over. If not provided the length is required. @@ -319,10 +391,10 @@ def progressbar(iterable=None, length=None, label=None, show_eta=True, `False` if not. :param show_pos: enables or disables the absolute position display. The default is `False`. - :param item_show_func: a function called with the current item which - can return a string to show the current item - next to the progress bar. Note that the current - item can be `None`! + :param item_show_func: A function called with the current item which + can return a string to show next to the progress bar. If the + function returns ``None`` nothing is shown. The current item can + be ``None``, such as when entering and exiting the bar. :param fill_char: the character to use to show the filled part of the progress bar. :param empty_char: the character to use to show the non-filled part of @@ -334,24 +406,57 @@ def progressbar(iterable=None, length=None, label=None, show_eta=True, :param info_sep: the separator between multiple info items (eta etc.) :param width: the width of the progress bar in characters, 0 means full terminal width - :param file: the file to write to. If this is not a terminal then - only the label is printed. + :param file: The file to write to. If this is not a terminal then + only the label is printed. :param color: controls if the terminal supports ANSI colors or not. The default is autodetection. This is only needed if ANSI codes are included anywhere in the progress bar output which is not the case by default. + :param update_min_steps: Render only when this many updates have + completed. This allows tuning for very fast iterators. + + .. versionchanged:: 8.0 + Output is shown even if execution time is less than 0.5 seconds. + + .. versionchanged:: 8.0 + ``item_show_func`` shows the current item, not the previous one. + + .. versionchanged:: 8.0 + Labels are echoed if the output is not a TTY. Reverts a change + in 7.0 that removed all output. + + .. versionadded:: 8.0 + Added the ``update_min_steps`` parameter. + + .. versionchanged:: 4.0 + Added the ``color`` parameter. Added the ``update`` method to + the object. + + .. versionadded:: 2.0 """ from ._termui_impl import ProgressBar + color = resolve_color_default(color) - return ProgressBar(iterable=iterable, length=length, show_eta=show_eta, - show_percent=show_percent, show_pos=show_pos, - item_show_func=item_show_func, fill_char=fill_char, - empty_char=empty_char, bar_template=bar_template, - info_sep=info_sep, file=file, label=label, - width=width, color=color) + return ProgressBar( + iterable=iterable, + length=length, + show_eta=show_eta, + show_percent=show_percent, + show_pos=show_pos, + item_show_func=item_show_func, + fill_char=fill_char, + empty_char=empty_char, + bar_template=bar_template, + info_sep=info_sep, + file=file, + label=label, + width=width, + color=color, + update_min_steps=update_min_steps, + ) -def clear(): +def clear() -> None: """Clears the terminal screen. This will have the effect of clearing the whole visible space of the terminal and moving the cursor to the top left. This does not do anything if not connected to a terminal. @@ -360,17 +465,39 @@ def clear(): """ if not isatty(sys.stdout): return - # If we're on Windows and we don't have colorama available, then we - # clear the screen by shelling out. Otherwise we can use an escape - # sequence. if WIN: - os.system('cls') + os.system("cls") else: - sys.stdout.write('\033[2J\033[1;1H') + sys.stdout.write("\033[2J\033[1;1H") -def style(text, fg=None, bg=None, bold=None, dim=None, underline=None, - blink=None, reverse=None, reset=True): +def _interpret_color( + color: t.Union[int, t.Tuple[int, int, int], str], offset: int = 0 +) -> str: + if isinstance(color, int): + return f"{38 + offset};5;{color:d}" + + if isinstance(color, (tuple, list)): + r, g, b = color + return f"{38 + offset};2;{r:d};{g:d};{b:d}" + + return str(_ansi_colors[color] + offset) + + +def style( + text: t.Any, + fg: t.Optional[t.Union[int, t.Tuple[int, int, int], str]] = None, + bg: t.Optional[t.Union[int, t.Tuple[int, int, int], str]] = None, + bold: t.Optional[bool] = None, + dim: t.Optional[bool] = None, + underline: t.Optional[bool] = None, + overline: t.Optional[bool] = None, + italic: t.Optional[bool] = None, + blink: t.Optional[bool] = None, + reverse: t.Optional[bool] = None, + strikethrough: t.Optional[bool] = None, + reset: bool = True, +) -> str: """Styles a text with ANSI styles and returns the new string. By default the styling is self contained which means that at the end of the string a reset code is issued. This can be prevented by @@ -381,6 +508,7 @@ def style(text, fg=None, bg=None, bold=None, dim=None, underline=None, click.echo(click.style('Hello World!', fg='green')) click.echo(click.style('ATTENTION!', blink=True)) click.echo(click.style('Some things', reverse=True, fg='cyan')) + click.echo(click.style('More colors', fg=(255, 12, 128), bg=117)) Supported color names: @@ -402,10 +530,15 @@ def style(text, fg=None, bg=None, bold=None, dim=None, underline=None, * ``bright_white`` * ``reset`` (reset the color code only) - .. versionadded:: 2.0 + If the terminal supports it, color may also be specified as: - .. versionadded:: 7.0 - Added support for bright colors. + - An integer in the interval [0, 255]. The terminal must support + 8-bit/256-color mode. + - An RGB tuple of three integers in [0, 255]. The terminal must + support 24-bit/true-color mode. + + See https://en.wikipedia.org/wiki/ANSI_color and + https://gist.github.com/XVilka/8346728 for more information. :param text: the string to style with ansi codes. :param fg: if provided this will become the foreground color. @@ -414,42 +547,73 @@ def style(text, fg=None, bg=None, bold=None, dim=None, underline=None, :param dim: if provided this will enable or disable dim mode. This is badly supported. :param underline: if provided this will enable or disable underline. + :param overline: if provided this will enable or disable overline. + :param italic: if provided this will enable or disable italic. :param blink: if provided this will enable or disable blinking. :param reverse: if provided this will enable or disable inverse rendering (foreground becomes background and the other way round). + :param strikethrough: if provided this will enable or disable + striking through text. :param reset: by default a reset-all code is added at the end of the string which means that styles do not carry over. This can be disabled to compose styles. + + .. versionchanged:: 8.0 + A non-string ``message`` is converted to a string. + + .. versionchanged:: 8.0 + Added support for 256 and RGB color codes. + + .. versionchanged:: 8.0 + Added the ``strikethrough``, ``italic``, and ``overline`` + parameters. + + .. versionchanged:: 7.0 + Added support for bright colors. + + .. versionadded:: 2.0 """ + if not isinstance(text, str): + text = str(text) + bits = [] + if fg: try: - bits.append('\033[%dm' % (_ansi_colors[fg])) + bits.append(f"\033[{_interpret_color(fg)}m") except KeyError: - raise TypeError('Unknown color %r' % fg) + raise TypeError(f"Unknown color {fg!r}") from None + if bg: try: - bits.append('\033[%dm' % (_ansi_colors[bg] + 10)) + bits.append(f"\033[{_interpret_color(bg, 10)}m") except KeyError: - raise TypeError('Unknown color %r' % bg) + raise TypeError(f"Unknown color {bg!r}") from None + if bold is not None: - bits.append('\033[%dm' % (1 if bold else 22)) + bits.append(f"\033[{1 if bold else 22}m") if dim is not None: - bits.append('\033[%dm' % (2 if dim else 22)) + bits.append(f"\033[{2 if dim else 22}m") if underline is not None: - bits.append('\033[%dm' % (4 if underline else 24)) + bits.append(f"\033[{4 if underline else 24}m") + if overline is not None: + bits.append(f"\033[{53 if overline else 55}m") + if italic is not None: + bits.append(f"\033[{3 if italic else 23}m") if blink is not None: - bits.append('\033[%dm' % (5 if blink else 25)) + bits.append(f"\033[{5 if blink else 25}m") if reverse is not None: - bits.append('\033[%dm' % (7 if reverse else 27)) + bits.append(f"\033[{7 if reverse else 27}m") + if strikethrough is not None: + bits.append(f"\033[{9 if strikethrough else 29}m") bits.append(text) if reset: bits.append(_ansi_reset_all) - return ''.join(bits) + return "".join(bits) -def unstyle(text): +def unstyle(text: str) -> str: """Removes ANSI styling information from a string. Usually it's not necessary to use this function as Click's echo function will automatically remove styling if necessary. @@ -461,7 +625,14 @@ def unstyle(text): return strip_ansi(text) -def secho(message=None, file=None, nl=True, err=False, color=None, **styles): +def secho( + message: t.Optional[t.Any] = None, + file: t.Optional[t.IO] = None, + nl: bool = True, + err: bool = False, + color: t.Optional[bool] = None, + **styles: t.Any, +) -> None: """This function combines :func:`echo` and :func:`style` into one call. As such the following two calls are the same:: @@ -471,15 +642,31 @@ def secho(message=None, file=None, nl=True, err=False, color=None, **styles): All keyword arguments are forwarded to the underlying functions depending on which one they go with. + Non-string types will be converted to :class:`str`. However, + :class:`bytes` are passed directly to :meth:`echo` without applying + style. If you want to style bytes that represent text, call + :meth:`bytes.decode` first. + + .. versionchanged:: 8.0 + A non-string ``message`` is converted to a string. Bytes are + passed through without style applied. + .. versionadded:: 2.0 """ - if message is not None: + if message is not None and not isinstance(message, (bytes, bytearray)): message = style(message, **styles) + return echo(message, file=file, nl=nl, err=err, color=color) -def edit(text=None, editor=None, env=None, require_save=True, - extension='.txt', filename=None): +def edit( + text: t.Optional[t.AnyStr] = None, + editor: t.Optional[str] = None, + env: t.Optional[t.Mapping[str, str]] = None, + require_save: bool = True, + extension: str = ".txt", + filename: t.Optional[str] = None, +) -> t.Optional[t.AnyStr]: r"""Edits the given text in the defined editor. If an editor is given (should be the full path to the executable but the regular operating system search path is used for finding the executable) it overrides @@ -508,14 +695,17 @@ def edit(text=None, editor=None, env=None, require_save=True, file as an indirection in that case. """ from ._termui_impl import Editor - editor = Editor(editor=editor, env=env, require_save=require_save, - extension=extension) + + ed = Editor(editor=editor, env=env, require_save=require_save, extension=extension) + if filename is None: - return editor.edit(text) - editor.edit_file(filename) + return ed.edit(text) + + ed.edit_file(filename) + return None -def launch(url, wait=False, locate=False): +def launch(url: str, wait: bool = False, locate: bool = False) -> int: """This function launches the given URL (or filename) in the default viewer application for this file type. If this is an executable, it might launch the executable in a new session. The return value is @@ -530,7 +720,9 @@ def launch(url, wait=False, locate=False): .. versionadded:: 2.0 :param url: URL or filename of the thing to launch. - :param wait: waits for the program to stop. + :param wait: Wait for the program to exit before returning. This + only works if the launched program blocks. In particular, + ``xdg-open`` on Linux does not block. :param locate: if this is set to `True` then instead of launching the application associated with the URL it will attempt to launch a file manager with the file located. This @@ -538,15 +730,16 @@ def launch(url, wait=False, locate=False): the filesystem. """ from ._termui_impl import open_url + return open_url(url, wait=wait, locate=locate) # If this is provided, getchar() calls into this instead. This is used # for unittesting purposes. -_getchar = None +_getchar: t.Optional[t.Callable[[bool], str]] = None -def getchar(echo=False): +def getchar(echo: bool = False) -> str: """Fetches a single character from the terminal and returns it. This will always return a unicode character and under certain rare circumstances this might return more than one character. The @@ -566,18 +759,23 @@ def getchar(echo=False): :param echo: if set to `True`, the character read will also show up on the terminal. The default is to not show it. """ - f = _getchar - if f is None: + global _getchar + + if _getchar is None: from ._termui_impl import getchar as f - return f(echo) + + _getchar = f + + return _getchar(echo) -def raw_terminal(): +def raw_terminal() -> t.ContextManager[int]: from ._termui_impl import raw_terminal as f + return f() -def pause(info='Press any key to continue ...', err=False): +def pause(info: t.Optional[str] = None, err: bool = False) -> None: """This command stops execution and waits for the user to press any key to continue. This is similar to the Windows batch "pause" command. If the program is not run through a terminal, this command @@ -588,12 +786,17 @@ def pause(info='Press any key to continue ...', err=False): .. versionadded:: 4.0 Added the `err` parameter. - :param info: the info string to print before pausing. + :param info: The message to print before pausing. Defaults to + ``"Press any key to continue..."``. :param err: if set to message goes to ``stderr`` instead of ``stdout``, the same as with echo. """ if not isatty(sys.stdin) or not isatty(sys.stdout): return + + if info is None: + info = _("Press any key to continue...") + try: if info: echo(info, nl=False, err=err) diff --git a/libs/click/testing.py b/libs/click/testing.py index 1b2924e0b..d19b850fc 100644 --- a/libs/click/testing.py +++ b/libs/click/testing.py @@ -1,86 +1,128 @@ -import os -import sys -import shutil -import tempfile import contextlib +import io +import os import shlex +import shutil +import sys +import tempfile +import typing as t +from types import TracebackType -from ._compat import iteritems, PY2, string_types +from . import formatting +from . import termui +from . import utils +from ._compat import _find_binary_reader + +if t.TYPE_CHECKING: + from .core import BaseCommand -# If someone wants to vendor click, we want to ensure the -# correct package is discovered. Ideally we could use a -# relative import here but unfortunately Python does not -# support that. -clickpkg = sys.modules[__name__.rsplit('.', 1)[0]] - - -if PY2: - from cStringIO import StringIO -else: - import io - from ._compat import _find_binary_reader - - -class EchoingStdin(object): - - def __init__(self, input, output): +class EchoingStdin: + def __init__(self, input: t.BinaryIO, output: t.BinaryIO) -> None: self._input = input self._output = output + self._paused = False - def __getattr__(self, x): + def __getattr__(self, x: str) -> t.Any: return getattr(self._input, x) - def _echo(self, rv): - self._output.write(rv) + def _echo(self, rv: bytes) -> bytes: + if not self._paused: + self._output.write(rv) + return rv - def read(self, n=-1): + def read(self, n: int = -1) -> bytes: return self._echo(self._input.read(n)) - def readline(self, n=-1): + def read1(self, n: int = -1) -> bytes: + return self._echo(self._input.read1(n)) # type: ignore + + def readline(self, n: int = -1) -> bytes: return self._echo(self._input.readline(n)) - def readlines(self): + def readlines(self) -> t.List[bytes]: return [self._echo(x) for x in self._input.readlines()] - def __iter__(self): + def __iter__(self) -> t.Iterator[bytes]: return iter(self._echo(x) for x in self._input) - def __repr__(self): + def __repr__(self) -> str: return repr(self._input) -def make_input_stream(input, charset): +@contextlib.contextmanager +def _pause_echo(stream: t.Optional[EchoingStdin]) -> t.Iterator[None]: + if stream is None: + yield + else: + stream._paused = True + yield + stream._paused = False + + +class _NamedTextIOWrapper(io.TextIOWrapper): + def __init__( + self, buffer: t.BinaryIO, name: str, mode: str, **kwargs: t.Any + ) -> None: + super().__init__(buffer, **kwargs) + self._name = name + self._mode = mode + + @property + def name(self) -> str: + return self._name + + @property + def mode(self) -> str: + return self._mode + + +def make_input_stream( + input: t.Optional[t.Union[str, bytes, t.IO]], charset: str +) -> t.BinaryIO: # Is already an input stream. - if hasattr(input, 'read'): - if PY2: - return input - rv = _find_binary_reader(input) + if hasattr(input, "read"): + rv = _find_binary_reader(t.cast(t.IO, input)) + if rv is not None: return rv - raise TypeError('Could not find binary reader for input stream.') + + raise TypeError("Could not find binary reader for input stream.") if input is None: - input = b'' - elif not isinstance(input, bytes): + input = b"" + elif isinstance(input, str): input = input.encode(charset) - if PY2: - return StringIO(input) - return io.BytesIO(input) + + return io.BytesIO(t.cast(bytes, input)) -class Result(object): +class Result: """Holds the captured result of an invoked CLI script.""" - def __init__(self, runner, stdout_bytes, stderr_bytes, exit_code, - exception, exc_info=None): + def __init__( + self, + runner: "CliRunner", + stdout_bytes: bytes, + stderr_bytes: t.Optional[bytes], + return_value: t.Any, + exit_code: int, + exception: t.Optional[BaseException], + exc_info: t.Optional[ + t.Tuple[t.Type[BaseException], BaseException, TracebackType] + ] = None, + ): #: The runner that created the result self.runner = runner #: The standard output as bytes. self.stdout_bytes = stdout_bytes - #: The standard error as bytes, or False(y) if not available + #: The standard error as bytes, or None if not available self.stderr_bytes = stderr_bytes + #: The value returned from the invoked command. + #: + #: .. versionadded:: 8.0 + self.return_value = return_value #: The exit code as integer. self.exit_code = exit_code #: The exception that happened if one did. @@ -89,41 +131,38 @@ class Result(object): self.exc_info = exc_info @property - def output(self): + def output(self) -> str: """The (standard) output as unicode string.""" return self.stdout @property - def stdout(self): + def stdout(self) -> str: """The standard output as unicode string.""" - return self.stdout_bytes.decode(self.runner.charset, 'replace') \ - .replace('\r\n', '\n') - - @property - def stderr(self): - """The standard error as unicode string.""" - if not self.stderr_bytes: - raise ValueError("stderr not separately captured") - return self.stderr_bytes.decode(self.runner.charset, 'replace') \ - .replace('\r\n', '\n') - - - def __repr__(self): - return '<%s %s>' % ( - type(self).__name__, - self.exception and repr(self.exception) or 'okay', + return self.stdout_bytes.decode(self.runner.charset, "replace").replace( + "\r\n", "\n" ) + @property + def stderr(self) -> str: + """The standard error as unicode string.""" + if self.stderr_bytes is None: + raise ValueError("stderr not separately captured") + return self.stderr_bytes.decode(self.runner.charset, "replace").replace( + "\r\n", "\n" + ) -class CliRunner(object): + def __repr__(self) -> str: + exc_str = repr(self.exception) if self.exception else "okay" + return f"<{type(self).__name__} {exc_str}>" + + +class CliRunner: """The CLI runner provides functionality to invoke a Click command line script for unittesting purposes in a isolated environment. This only works in single-threaded systems without any concurrency as it changes the global interpreter state. - :param charset: the character set for the input and output data. This is - UTF-8 by default and should not be changed currently as - the reporting to Click only works in Python 2 properly. + :param charset: the character set for the input and output data. :param env: a dictionary with environment variables for overriding. :param echo_stdin: if this is set to `True`, then reading from stdin writes to stdout. This is useful for showing examples in @@ -136,23 +175,28 @@ class CliRunner(object): independently """ - def __init__(self, charset=None, env=None, echo_stdin=False, - mix_stderr=True): - if charset is None: - charset = 'utf-8' + def __init__( + self, + charset: str = "utf-8", + env: t.Optional[t.Mapping[str, t.Optional[str]]] = None, + echo_stdin: bool = False, + mix_stderr: bool = True, + ) -> None: self.charset = charset self.env = env or {} self.echo_stdin = echo_stdin self.mix_stderr = mix_stderr - def get_default_prog_name(self, cli): + def get_default_prog_name(self, cli: "BaseCommand") -> str: """Given a command object it will return the default program name for it. The default is the `name` attribute or ``"root"`` if not set. """ - return cli.name or 'root' + return cli.name or "root" - def make_env(self, overrides=None): + def make_env( + self, overrides: t.Optional[t.Mapping[str, t.Optional[str]]] = None + ) -> t.Mapping[str, t.Optional[str]]: """Returns the environment overrides for invoking a script.""" rv = dict(self.env) if overrides: @@ -160,7 +204,12 @@ class CliRunner(object): return rv @contextlib.contextmanager - def isolation(self, input=None, env=None, color=False): + def isolation( + self, + input: t.Optional[t.Union[str, bytes, t.IO]] = None, + env: t.Optional[t.Mapping[str, t.Optional[str]]] = None, + color: bool = False, + ) -> t.Iterator[t.Tuple[io.BytesIO, t.Optional[io.BytesIO]]]: """A context manager that sets up the isolation for invoking of a command line tool. This sets up stdin with the given input data and `os.environ` with the overrides from the given dictionary. @@ -169,87 +218,107 @@ class CliRunner(object): This is automatically done in the :meth:`invoke` method. - .. versionadded:: 4.0 - The ``color`` parameter was added. - :param input: the input stream to put into sys.stdin. :param env: the environment overrides as dictionary. :param color: whether the output should contain color codes. The application can still override this explicitly. + + .. versionchanged:: 8.0 + ``stderr`` is opened with ``errors="backslashreplace"`` + instead of the default ``"strict"``. + + .. versionchanged:: 4.0 + Added the ``color`` parameter. """ - input = make_input_stream(input, self.charset) + bytes_input = make_input_stream(input, self.charset) + echo_input = None old_stdin = sys.stdin old_stdout = sys.stdout old_stderr = sys.stderr - old_forced_width = clickpkg.formatting.FORCED_WIDTH - clickpkg.formatting.FORCED_WIDTH = 80 + old_forced_width = formatting.FORCED_WIDTH + formatting.FORCED_WIDTH = 80 env = self.make_env(env) - if PY2: - bytes_output = StringIO() - if self.echo_stdin: - input = EchoingStdin(input, bytes_output) - sys.stdout = bytes_output - if not self.mix_stderr: - bytes_error = StringIO() - sys.stderr = bytes_error - else: - bytes_output = io.BytesIO() - if self.echo_stdin: - input = EchoingStdin(input, bytes_output) - input = io.TextIOWrapper(input, encoding=self.charset) - sys.stdout = io.TextIOWrapper( - bytes_output, encoding=self.charset) - if not self.mix_stderr: - bytes_error = io.BytesIO() - sys.stderr = io.TextIOWrapper( - bytes_error, encoding=self.charset) + bytes_output = io.BytesIO() + if self.echo_stdin: + bytes_input = echo_input = t.cast( + t.BinaryIO, EchoingStdin(bytes_input, bytes_output) + ) + + sys.stdin = text_input = _NamedTextIOWrapper( + bytes_input, encoding=self.charset, name="", mode="r" + ) + + if self.echo_stdin: + # Force unbuffered reads, otherwise TextIOWrapper reads a + # large chunk which is echoed early. + text_input._CHUNK_SIZE = 1 # type: ignore + + sys.stdout = _NamedTextIOWrapper( + bytes_output, encoding=self.charset, name="", mode="w" + ) + + bytes_error = None if self.mix_stderr: sys.stderr = sys.stdout + else: + bytes_error = io.BytesIO() + sys.stderr = _NamedTextIOWrapper( + bytes_error, + encoding=self.charset, + name="", + mode="w", + errors="backslashreplace", + ) - sys.stdin = input - - def visible_input(prompt=None): - sys.stdout.write(prompt or '') - val = input.readline().rstrip('\r\n') - sys.stdout.write(val + '\n') + @_pause_echo(echo_input) # type: ignore + def visible_input(prompt: t.Optional[str] = None) -> str: + sys.stdout.write(prompt or "") + val = text_input.readline().rstrip("\r\n") + sys.stdout.write(f"{val}\n") sys.stdout.flush() return val - def hidden_input(prompt=None): - sys.stdout.write((prompt or '') + '\n') + @_pause_echo(echo_input) # type: ignore + def hidden_input(prompt: t.Optional[str] = None) -> str: + sys.stdout.write(f"{prompt or ''}\n") sys.stdout.flush() - return input.readline().rstrip('\r\n') + return text_input.readline().rstrip("\r\n") - def _getchar(echo): + @_pause_echo(echo_input) # type: ignore + def _getchar(echo: bool) -> str: char = sys.stdin.read(1) + if echo: sys.stdout.write(char) - sys.stdout.flush() + + sys.stdout.flush() return char default_color = color - def should_strip_ansi(stream=None, color=None): + def should_strip_ansi( + stream: t.Optional[t.IO] = None, color: t.Optional[bool] = None + ) -> bool: if color is None: return not default_color return not color - old_visible_prompt_func = clickpkg.termui.visible_prompt_func - old_hidden_prompt_func = clickpkg.termui.hidden_prompt_func - old__getchar_func = clickpkg.termui._getchar - old_should_strip_ansi = clickpkg.utils.should_strip_ansi - clickpkg.termui.visible_prompt_func = visible_input - clickpkg.termui.hidden_prompt_func = hidden_input - clickpkg.termui._getchar = _getchar - clickpkg.utils.should_strip_ansi = should_strip_ansi + old_visible_prompt_func = termui.visible_prompt_func + old_hidden_prompt_func = termui.hidden_prompt_func + old__getchar_func = termui._getchar + old_should_strip_ansi = utils.should_strip_ansi # type: ignore + termui.visible_prompt_func = visible_input + termui.hidden_prompt_func = hidden_input + termui._getchar = _getchar + utils.should_strip_ansi = should_strip_ansi # type: ignore old_env = {} try: - for key, value in iteritems(env): + for key, value in env.items(): old_env[key] = os.environ.get(key) if value is None: try: @@ -258,9 +327,9 @@ class CliRunner(object): pass else: os.environ[key] = value - yield (bytes_output, not self.mix_stderr and bytes_error) + yield (bytes_output, bytes_error) finally: - for key, value in iteritems(old_env): + for key, value in old_env.items(): if value is None: try: del os.environ[key] @@ -271,14 +340,22 @@ class CliRunner(object): sys.stdout = old_stdout sys.stderr = old_stderr sys.stdin = old_stdin - clickpkg.termui.visible_prompt_func = old_visible_prompt_func - clickpkg.termui.hidden_prompt_func = old_hidden_prompt_func - clickpkg.termui._getchar = old__getchar_func - clickpkg.utils.should_strip_ansi = old_should_strip_ansi - clickpkg.formatting.FORCED_WIDTH = old_forced_width + termui.visible_prompt_func = old_visible_prompt_func + termui.hidden_prompt_func = old_hidden_prompt_func + termui._getchar = old__getchar_func + utils.should_strip_ansi = old_should_strip_ansi # type: ignore + formatting.FORCED_WIDTH = old_forced_width - def invoke(self, cli, args=None, input=None, env=None, - catch_exceptions=True, color=False, mix_stderr=False, **extra): + def invoke( + self, + cli: "BaseCommand", + args: t.Optional[t.Union[str, t.Sequence[str]]] = None, + input: t.Optional[t.Union[str, bytes, t.IO]] = None, + env: t.Optional[t.Mapping[str, t.Optional[str]]] = None, + catch_exceptions: bool = True, + color: bool = False, + **extra: t.Any, + ) -> Result: """Invokes a command in an isolated environment. The arguments are forwarded directly to the command line script, the `extra` keyword arguments are passed to the :meth:`~clickpkg.Command.main` function of @@ -286,16 +363,6 @@ class CliRunner(object): This returns a :class:`Result` object. - .. versionadded:: 3.0 - The ``catch_exceptions`` parameter was added. - - .. versionchanged:: 3.0 - The result object now has an `exc_info` attribute with the - traceback if available. - - .. versionadded:: 4.0 - The ``color`` parameter was added. - :param cli: the command to invoke :param args: the arguments to invoke. It may be given as an iterable or a string. When given as string it will be interpreted @@ -308,13 +375,28 @@ class CliRunner(object): :param extra: the keyword arguments to pass to :meth:`main`. :param color: whether the output should contain color codes. The application can still override this explicitly. + + .. versionchanged:: 8.0 + The result object has the ``return_value`` attribute with + the value returned from the invoked command. + + .. versionchanged:: 4.0 + Added the ``color`` parameter. + + .. versionchanged:: 3.0 + Added the ``catch_exceptions`` parameter. + + .. versionchanged:: 3.0 + The result object has the ``exc_info`` attribute with the + traceback if available. """ exc_info = None with self.isolation(input=input, env=env, color=color) as outstreams: - exception = None + return_value = None + exception: t.Optional[BaseException] = None exit_code = 0 - if isinstance(args, string_types): + if isinstance(args, str): args = shlex.split(args) try: @@ -323,20 +405,23 @@ class CliRunner(object): prog_name = self.get_default_prog_name(cli) try: - cli.main(args=args or (), prog_name=prog_name, **extra) + return_value = cli.main(args=args or (), prog_name=prog_name, **extra) except SystemExit as e: exc_info = sys.exc_info() - exit_code = e.code - if exit_code is None: - exit_code = 0 + e_code = t.cast(t.Optional[t.Union[int, t.Any]], e.code) - if exit_code != 0: + if e_code is None: + e_code = 0 + + if e_code != 0: exception = e - if not isinstance(exit_code, int): - sys.stdout.write(str(exit_code)) - sys.stdout.write('\n') - exit_code = 1 + if not isinstance(e_code, int): + sys.stdout.write(str(e_code)) + sys.stdout.write("\n") + e_code = 1 + + exit_code = e_code except Exception as e: if not catch_exceptions: @@ -347,28 +432,48 @@ class CliRunner(object): finally: sys.stdout.flush() stdout = outstreams[0].getvalue() - stderr = outstreams[1] and outstreams[1].getvalue() + if self.mix_stderr: + stderr = None + else: + stderr = outstreams[1].getvalue() # type: ignore - return Result(runner=self, - stdout_bytes=stdout, - stderr_bytes=stderr, - exit_code=exit_code, - exception=exception, - exc_info=exc_info) + return Result( + runner=self, + stdout_bytes=stdout, + stderr_bytes=stderr, + return_value=return_value, + exit_code=exit_code, + exception=exception, + exc_info=exc_info, # type: ignore + ) @contextlib.contextmanager - def isolated_filesystem(self): - """A context manager that creates a temporary folder and changes - the current working directory to it for isolated filesystem tests. + def isolated_filesystem( + self, temp_dir: t.Optional[t.Union[str, os.PathLike]] = None + ) -> t.Iterator[str]: + """A context manager that creates a temporary directory and + changes the current working directory to it. This isolates tests + that affect the contents of the CWD to prevent them from + interfering with each other. + + :param temp_dir: Create the temporary directory under this + directory. If given, the created directory is not removed + when exiting. + + .. versionchanged:: 8.0 + Added the ``temp_dir`` parameter. """ cwd = os.getcwd() - t = tempfile.mkdtemp() + t = tempfile.mkdtemp(dir=temp_dir) os.chdir(t) + try: yield t finally: os.chdir(cwd) - try: - shutil.rmtree(t) - except (OSError, IOError): - pass + + if temp_dir is None: + try: + shutil.rmtree(t) + except OSError: # noqa: B014 + pass diff --git a/libs/click/types.py b/libs/click/types.py index 1f88032f5..103d21829 100644 --- a/libs/click/types.py +++ b/libs/click/types.py @@ -1,30 +1,47 @@ import os import stat +import typing as t from datetime import datetime +from gettext import gettext as _ +from gettext import ngettext -from ._compat import open_stream, text_type, filename_to_ui, \ - get_filesystem_encoding, get_streerror, _get_argv_encoding, PY2 +from ._compat import _get_argv_encoding +from ._compat import get_filesystem_encoding +from ._compat import open_stream from .exceptions import BadParameter -from .utils import safecall, LazyFile +from .utils import LazyFile +from .utils import safecall + +if t.TYPE_CHECKING: + import typing_extensions as te + from .core import Context + from .core import Parameter + from .shell_completion import CompletionItem -class ParamType(object): - """Helper for converting values through types. The following is - necessary for a valid type: +class ParamType: + """Represents the type of a parameter. Validates and converts values + from the command line or Python into the correct type. - * it needs a name - * it needs to pass through None unchanged - * it needs to convert from a string - * it needs to convert its result type through unchanged - (eg: needs to be idempotent) - * it needs to be able to deal with param and context being `None`. - This can be the case when the object is used with prompt - inputs. + To implement a custom type, subclass and implement at least the + following: + + - The :attr:`name` class attribute must be set. + - Calling an instance of the type with ``None`` must return + ``None``. This is already implemented by default. + - :meth:`convert` must convert string values to the correct type. + - :meth:`convert` must accept values that are already the correct + type. + - It must be able to convert a value if the ``ctx`` and ``param`` + arguments are ``None``. This can occur when converting prompt + input. """ - is_composite = False + + is_composite: t.ClassVar[bool] = False + arity: t.ClassVar[int] = 1 #: the descriptive name of this type - name = None + name: str #: if a list of this type is expected and the value is pulled from a #: string environment variable, this is what splits it up. `None` @@ -32,29 +49,66 @@ class ParamType(object): #: whitespace splits them up. The exception are paths and files which #: are split by ``os.path.pathsep`` by default (":" on Unix and ";" on #: Windows). - envvar_list_splitter = None + envvar_list_splitter: t.ClassVar[t.Optional[str]] = None - def __call__(self, value, param=None, ctx=None): + def to_info_dict(self) -> t.Dict[str, t.Any]: + """Gather information that could be useful for a tool generating + user-facing documentation. + + Use :meth:`click.Context.to_info_dict` to traverse the entire + CLI structure. + + .. versionadded:: 8.0 + """ + # The class name without the "ParamType" suffix. + param_type = type(self).__name__.partition("ParamType")[0] + param_type = param_type.partition("ParameterType")[0] + return {"param_type": param_type, "name": self.name} + + def __call__( + self, + value: t.Any, + param: t.Optional["Parameter"] = None, + ctx: t.Optional["Context"] = None, + ) -> t.Any: if value is not None: return self.convert(value, param, ctx) - def get_metavar(self, param): + def get_metavar(self, param: "Parameter") -> t.Optional[str]: """Returns the metavar default for this param if it provides one.""" - def get_missing_message(self, param): + def get_missing_message(self, param: "Parameter") -> t.Optional[str]: """Optionally might return extra information about a missing parameter. .. versionadded:: 2.0 """ - def convert(self, value, param, ctx): - """Converts the value. This is not invoked for values that are - `None` (the missing value). + def convert( + self, value: t.Any, param: t.Optional["Parameter"], ctx: t.Optional["Context"] + ) -> t.Any: + """Convert the value to the correct type. This is not called if + the value is ``None`` (the missing value). + + This must accept string values from the command line, as well as + values that are already the correct type. It may also convert + other compatible types. + + The ``param`` and ``ctx`` arguments may be ``None`` in certain + situations, such as when converting prompt input. + + If the value cannot be converted, call :meth:`fail` with a + descriptive message. + + :param value: The value to convert. + :param param: The parameter that is using this type to convert + its value. May be ``None``. + :param ctx: The current context that arrived at this value. May + be ``None``. """ return value - def split_envvar_value(self, rv): + def split_envvar_value(self, rv: str) -> t.Sequence[str]: """Given a value from an environment variable this splits it up into small chunks depending on the defined envvar list splitter. @@ -62,52 +116,85 @@ class ParamType(object): then leading and trailing whitespace is ignored. Otherwise, leading and trailing splitters usually lead to empty items being included. """ - return (rv or '').split(self.envvar_list_splitter) + return (rv or "").split(self.envvar_list_splitter) - def fail(self, message, param=None, ctx=None): + def fail( + self, + message: str, + param: t.Optional["Parameter"] = None, + ctx: t.Optional["Context"] = None, + ) -> "t.NoReturn": """Helper method to fail with an invalid value message.""" raise BadParameter(message, ctx=ctx, param=param) + def shell_complete( + self, ctx: "Context", param: "Parameter", incomplete: str + ) -> t.List["CompletionItem"]: + """Return a list of + :class:`~click.shell_completion.CompletionItem` objects for the + incomplete value. Most types do not provide completions, but + some do, and this allows custom types to provide custom + completions as well. + + :param ctx: Invocation context for this command. + :param param: The parameter that is requesting completion. + :param incomplete: Value being completed. May be empty. + + .. versionadded:: 8.0 + """ + return [] + class CompositeParamType(ParamType): is_composite = True @property - def arity(self): + def arity(self) -> int: # type: ignore raise NotImplementedError() class FuncParamType(ParamType): - - def __init__(self, func): + def __init__(self, func: t.Callable[[t.Any], t.Any]) -> None: self.name = func.__name__ self.func = func - def convert(self, value, param, ctx): + def to_info_dict(self) -> t.Dict[str, t.Any]: + info_dict = super().to_info_dict() + info_dict["func"] = self.func + return info_dict + + def convert( + self, value: t.Any, param: t.Optional["Parameter"], ctx: t.Optional["Context"] + ) -> t.Any: try: return self.func(value) except ValueError: try: - value = text_type(value) + value = str(value) except UnicodeError: - value = str(value).decode('utf-8', 'replace') + value = value.decode("utf-8", "replace") + self.fail(value, param, ctx) class UnprocessedParamType(ParamType): - name = 'text' + name = "text" - def convert(self, value, param, ctx): + def convert( + self, value: t.Any, param: t.Optional["Parameter"], ctx: t.Optional["Context"] + ) -> t.Any: return value - def __repr__(self): - return 'UNPROCESSED' + def __repr__(self) -> str: + return "UNPROCESSED" class StringParamType(ParamType): - name = 'text' + name = "text" - def convert(self, value, param, ctx): + def convert( + self, value: t.Any, param: t.Optional["Parameter"], ctx: t.Optional["Context"] + ) -> t.Any: if isinstance(value, bytes): enc = _get_argv_encoding() try: @@ -118,12 +205,14 @@ class StringParamType(ParamType): try: value = value.decode(fs_enc) except UnicodeError: - value = value.decode('utf-8', 'replace') + value = value.decode("utf-8", "replace") + else: + value = value.decode("utf-8", "replace") return value - return value + return str(value) - def __repr__(self): - return 'STRING' + def __repr__(self) -> str: + return "STRING" class Choice(ParamType): @@ -133,54 +222,104 @@ class Choice(ParamType): You should only pass a list or tuple of choices. Other iterables (like generators) may lead to surprising results. + The resulting value will always be one of the originally passed choices + regardless of ``case_sensitive`` or any ``ctx.token_normalize_func`` + being specified. + See :ref:`choice-opts` for an example. :param case_sensitive: Set to false to make choices case insensitive. Defaults to true. """ - name = 'choice' + name = "choice" - def __init__(self, choices, case_sensitive=True): + def __init__(self, choices: t.Sequence[str], case_sensitive: bool = True) -> None: self.choices = choices self.case_sensitive = case_sensitive - def get_metavar(self, param): - return '[%s]' % '|'.join(self.choices) + def to_info_dict(self) -> t.Dict[str, t.Any]: + info_dict = super().to_info_dict() + info_dict["choices"] = self.choices + info_dict["case_sensitive"] = self.case_sensitive + return info_dict - def get_missing_message(self, param): - return 'Choose from:\n\t%s.' % ',\n\t'.join(self.choices) + def get_metavar(self, param: "Parameter") -> str: + choices_str = "|".join(self.choices) - def convert(self, value, param, ctx): - # Exact match - if value in self.choices: - return value + # Use curly braces to indicate a required argument. + if param.required and param.param_type_name == "argument": + return f"{{{choices_str}}}" + # Use square braces to indicate an option or optional argument. + return f"[{choices_str}]" + + def get_missing_message(self, param: "Parameter") -> str: + return _("Choose from:\n\t{choices}").format(choices=",\n\t".join(self.choices)) + + def convert( + self, value: t.Any, param: t.Optional["Parameter"], ctx: t.Optional["Context"] + ) -> t.Any: # Match through normalization and case sensitivity # first do token_normalize_func, then lowercase # preserve original `value` to produce an accurate message in # `self.fail` normed_value = value - normed_choices = self.choices + normed_choices = {choice: choice for choice in self.choices} - if ctx is not None and \ - ctx.token_normalize_func is not None: + if ctx is not None and ctx.token_normalize_func is not None: normed_value = ctx.token_normalize_func(value) - normed_choices = [ctx.token_normalize_func(choice) for choice in - self.choices] + normed_choices = { + ctx.token_normalize_func(normed_choice): original + for normed_choice, original in normed_choices.items() + } if not self.case_sensitive: - normed_value = normed_value.lower() - normed_choices = [choice.lower() for choice in normed_choices] + normed_value = normed_value.casefold() + normed_choices = { + normed_choice.casefold(): original + for normed_choice, original in normed_choices.items() + } if normed_value in normed_choices: - return normed_value + return normed_choices[normed_value] - self.fail('invalid choice: %s. (choose from %s)' % - (value, ', '.join(self.choices)), param, ctx) + choices_str = ", ".join(map(repr, self.choices)) + self.fail( + ngettext( + "{value!r} is not {choice}.", + "{value!r} is not one of {choices}.", + len(self.choices), + ).format(value=value, choice=choices_str, choices=choices_str), + param, + ctx, + ) - def __repr__(self): - return 'Choice(%r)' % list(self.choices) + def __repr__(self) -> str: + return f"Choice({list(self.choices)})" + + def shell_complete( + self, ctx: "Context", param: "Parameter", incomplete: str + ) -> t.List["CompletionItem"]: + """Complete choices that start with the incomplete value. + + :param ctx: Invocation context for this command. + :param param: The parameter that is requesting completion. + :param incomplete: Value being completed. May be empty. + + .. versionadded:: 8.0 + """ + from click.shell_completion import CompletionItem + + str_choices = map(str, self.choices) + + if self.case_sensitive: + matched = (c for c in str_choices if c.startswith(incomplete)) + else: + incomplete = incomplete.lower() + matched = (c for c in str_choices if c.lower().startswith(incomplete)) + + return [CompletionItem(c) for c in matched] class DateTime(ParamType): @@ -203,175 +342,289 @@ class DateTime(ParamType): ``'%Y-%m-%d'``, ``'%Y-%m-%dT%H:%M:%S'``, ``'%Y-%m-%d %H:%M:%S'``. """ - name = 'datetime' - def __init__(self, formats=None): - self.formats = formats or [ - '%Y-%m-%d', - '%Y-%m-%dT%H:%M:%S', - '%Y-%m-%d %H:%M:%S' - ] + name = "datetime" - def get_metavar(self, param): - return '[{}]'.format('|'.join(self.formats)) + def __init__(self, formats: t.Optional[t.Sequence[str]] = None): + self.formats = formats or ["%Y-%m-%d", "%Y-%m-%dT%H:%M:%S", "%Y-%m-%d %H:%M:%S"] - def _try_to_convert_date(self, value, format): + def to_info_dict(self) -> t.Dict[str, t.Any]: + info_dict = super().to_info_dict() + info_dict["formats"] = self.formats + return info_dict + + def get_metavar(self, param: "Parameter") -> str: + return f"[{'|'.join(self.formats)}]" + + def _try_to_convert_date(self, value: t.Any, format: str) -> t.Optional[datetime]: try: return datetime.strptime(value, format) except ValueError: return None - def convert(self, value, param, ctx): - # Exact match + def convert( + self, value: t.Any, param: t.Optional["Parameter"], ctx: t.Optional["Context"] + ) -> t.Any: + if isinstance(value, datetime): + return value + for format in self.formats: - dtime = self._try_to_convert_date(value, format) - if dtime: - return dtime + converted = self._try_to_convert_date(value, format) + if converted is not None: + return converted + + formats_str = ", ".join(map(repr, self.formats)) self.fail( - 'invalid datetime format: {}. (choose from {})'.format( - value, ', '.join(self.formats))) + ngettext( + "{value!r} does not match the format {format}.", + "{value!r} does not match the formats {formats}.", + len(self.formats), + ).format(value=value, format=formats_str, formats=formats_str), + param, + ctx, + ) - def __repr__(self): - return 'DateTime' + def __repr__(self) -> str: + return "DateTime" -class IntParamType(ParamType): - name = 'integer' +class _NumberParamTypeBase(ParamType): + _number_class: t.ClassVar[t.Type] - def convert(self, value, param, ctx): + def convert( + self, value: t.Any, param: t.Optional["Parameter"], ctx: t.Optional["Context"] + ) -> t.Any: try: - return int(value) - except (ValueError, UnicodeError): - self.fail('%s is not a valid integer' % value, param, ctx) - - def __repr__(self): - return 'INT' + return self._number_class(value) + except ValueError: + self.fail( + _("{value!r} is not a valid {number_type}.").format( + value=value, number_type=self.name + ), + param, + ctx, + ) -class IntRange(IntParamType): - """A parameter that works similar to :data:`click.INT` but restricts - the value to fit into a range. The default behavior is to fail if the - value falls outside the range, but it can also be silently clamped - between the two edges. - - See :ref:`ranges` for an example. - """ - name = 'integer range' - - def __init__(self, min=None, max=None, clamp=False): +class _NumberRangeBase(_NumberParamTypeBase): + def __init__( + self, + min: t.Optional[float] = None, + max: t.Optional[float] = None, + min_open: bool = False, + max_open: bool = False, + clamp: bool = False, + ) -> None: self.min = min self.max = max + self.min_open = min_open + self.max_open = max_open self.clamp = clamp - def convert(self, value, param, ctx): - rv = IntParamType.convert(self, value, param, ctx) + def to_info_dict(self) -> t.Dict[str, t.Any]: + info_dict = super().to_info_dict() + info_dict.update( + min=self.min, + max=self.max, + min_open=self.min_open, + max_open=self.max_open, + clamp=self.clamp, + ) + return info_dict + + def convert( + self, value: t.Any, param: t.Optional["Parameter"], ctx: t.Optional["Context"] + ) -> t.Any: + import operator + + rv = super().convert(value, param, ctx) + lt_min: bool = self.min is not None and ( + operator.le if self.min_open else operator.lt + )(rv, self.min) + gt_max: bool = self.max is not None and ( + operator.ge if self.max_open else operator.gt + )(rv, self.max) + if self.clamp: - if self.min is not None and rv < self.min: - return self.min - if self.max is not None and rv > self.max: - return self.max - if self.min is not None and rv < self.min or \ - self.max is not None and rv > self.max: - if self.min is None: - self.fail('%s is bigger than the maximum valid value ' - '%s.' % (rv, self.max), param, ctx) - elif self.max is None: - self.fail('%s is smaller than the minimum valid value ' - '%s.' % (rv, self.min), param, ctx) - else: - self.fail('%s is not in the valid range of %s to %s.' - % (rv, self.min, self.max), param, ctx) + if lt_min: + return self._clamp(self.min, 1, self.min_open) # type: ignore + + if gt_max: + return self._clamp(self.max, -1, self.max_open) # type: ignore + + if lt_min or gt_max: + self.fail( + _("{value} is not in the range {range}.").format( + value=rv, range=self._describe_range() + ), + param, + ctx, + ) + return rv - def __repr__(self): - return 'IntRange(%r, %r)' % (self.min, self.max) + def _clamp(self, bound: float, dir: "te.Literal[1, -1]", open: bool) -> float: + """Find the valid value to clamp to bound in the given + direction. + + :param bound: The boundary value. + :param dir: 1 or -1 indicating the direction to move. + :param open: If true, the range does not include the bound. + """ + raise NotImplementedError + + def _describe_range(self) -> str: + """Describe the range for use in help text.""" + if self.min is None: + op = "<" if self.max_open else "<=" + return f"x{op}{self.max}" + + if self.max is None: + op = ">" if self.min_open else ">=" + return f"x{op}{self.min}" + + lop = "<" if self.min_open else "<=" + rop = "<" if self.max_open else "<=" + return f"{self.min}{lop}x{rop}{self.max}" + + def __repr__(self) -> str: + clamp = " clamped" if self.clamp else "" + return f"<{type(self).__name__} {self._describe_range()}{clamp}>" -class FloatParamType(ParamType): - name = 'float' +class IntParamType(_NumberParamTypeBase): + name = "integer" + _number_class = int - def convert(self, value, param, ctx): - try: - return float(value) - except (UnicodeError, ValueError): - self.fail('%s is not a valid floating point value' % - value, param, ctx) - - def __repr__(self): - return 'FLOAT' + def __repr__(self) -> str: + return "INT" -class FloatRange(FloatParamType): - """A parameter that works similar to :data:`click.FLOAT` but restricts - the value to fit into a range. The default behavior is to fail if the - value falls outside the range, but it can also be silently clamped - between the two edges. +class IntRange(_NumberRangeBase, IntParamType): + """Restrict an :data:`click.INT` value to a range of accepted + values. See :ref:`ranges`. - See :ref:`ranges` for an example. + If ``min`` or ``max`` are not passed, any value is accepted in that + direction. If ``min_open`` or ``max_open`` are enabled, the + corresponding boundary is not included in the range. + + If ``clamp`` is enabled, a value outside the range is clamped to the + boundary instead of failing. + + .. versionchanged:: 8.0 + Added the ``min_open`` and ``max_open`` parameters. """ - name = 'float range' - def __init__(self, min=None, max=None, clamp=False): - self.min = min - self.max = max - self.clamp = clamp + name = "integer range" - def convert(self, value, param, ctx): - rv = FloatParamType.convert(self, value, param, ctx) - if self.clamp: - if self.min is not None and rv < self.min: - return self.min - if self.max is not None and rv > self.max: - return self.max - if self.min is not None and rv < self.min or \ - self.max is not None and rv > self.max: - if self.min is None: - self.fail('%s is bigger than the maximum valid value ' - '%s.' % (rv, self.max), param, ctx) - elif self.max is None: - self.fail('%s is smaller than the minimum valid value ' - '%s.' % (rv, self.min), param, ctx) - else: - self.fail('%s is not in the valid range of %s to %s.' - % (rv, self.min, self.max), param, ctx) - return rv + def _clamp( # type: ignore + self, bound: int, dir: "te.Literal[1, -1]", open: bool + ) -> int: + if not open: + return bound - def __repr__(self): - return 'FloatRange(%r, %r)' % (self.min, self.max) + return bound + dir + + +class FloatParamType(_NumberParamTypeBase): + name = "float" + _number_class = float + + def __repr__(self) -> str: + return "FLOAT" + + +class FloatRange(_NumberRangeBase, FloatParamType): + """Restrict a :data:`click.FLOAT` value to a range of accepted + values. See :ref:`ranges`. + + If ``min`` or ``max`` are not passed, any value is accepted in that + direction. If ``min_open`` or ``max_open`` are enabled, the + corresponding boundary is not included in the range. + + If ``clamp`` is enabled, a value outside the range is clamped to the + boundary instead of failing. This is not supported if either + boundary is marked ``open``. + + .. versionchanged:: 8.0 + Added the ``min_open`` and ``max_open`` parameters. + """ + + name = "float range" + + def __init__( + self, + min: t.Optional[float] = None, + max: t.Optional[float] = None, + min_open: bool = False, + max_open: bool = False, + clamp: bool = False, + ) -> None: + super().__init__( + min=min, max=max, min_open=min_open, max_open=max_open, clamp=clamp + ) + + if (min_open or max_open) and clamp: + raise TypeError("Clamping is not supported for open bounds.") + + def _clamp(self, bound: float, dir: "te.Literal[1, -1]", open: bool) -> float: + if not open: + return bound + + # Could use Python 3.9's math.nextafter here, but clamping an + # open float range doesn't seem to be particularly useful. It's + # left up to the user to write a callback to do it if needed. + raise RuntimeError("Clamping is not supported for open bounds.") class BoolParamType(ParamType): - name = 'boolean' + name = "boolean" - def convert(self, value, param, ctx): - if isinstance(value, bool): + def convert( + self, value: t.Any, param: t.Optional["Parameter"], ctx: t.Optional["Context"] + ) -> t.Any: + if value in {False, True}: return bool(value) - value = value.lower() - if value in ('true', 't', '1', 'yes', 'y'): - return True - elif value in ('false', 'f', '0', 'no', 'n'): - return False - self.fail('%s is not a valid boolean' % value, param, ctx) - def __repr__(self): - return 'BOOL' + norm = value.strip().lower() + + if norm in {"1", "true", "t", "yes", "y", "on"}: + return True + + if norm in {"0", "false", "f", "no", "n", "off"}: + return False + + self.fail( + _("{value!r} is not a valid boolean.").format(value=value), param, ctx + ) + + def __repr__(self) -> str: + return "BOOL" class UUIDParameterType(ParamType): - name = 'uuid' + name = "uuid" - def convert(self, value, param, ctx): + def convert( + self, value: t.Any, param: t.Optional["Parameter"], ctx: t.Optional["Context"] + ) -> t.Any: import uuid - try: - if PY2 and isinstance(value, text_type): - value = value.encode('ascii') - return uuid.UUID(value) - except (UnicodeError, ValueError): - self.fail('%s is not a valid UUID value' % value, param, ctx) - def __repr__(self): - return 'UUID' + if isinstance(value, uuid.UUID): + return value + + value = value.strip() + + try: + return uuid.UUID(value) + except ValueError: + self.fail( + _("{value!r} is not a valid UUID.").format(value=value), param, ctx + ) + + def __repr__(self) -> str: + return "UUID" class File(ParamType): @@ -400,43 +653,64 @@ class File(ParamType): See :ref:`file-args` for more information. """ - name = 'filename' + + name = "filename" envvar_list_splitter = os.path.pathsep - def __init__(self, mode='r', encoding=None, errors='strict', lazy=None, - atomic=False): + def __init__( + self, + mode: str = "r", + encoding: t.Optional[str] = None, + errors: t.Optional[str] = "strict", + lazy: t.Optional[bool] = None, + atomic: bool = False, + ) -> None: self.mode = mode self.encoding = encoding self.errors = errors self.lazy = lazy self.atomic = atomic - def resolve_lazy_flag(self, value): + def to_info_dict(self) -> t.Dict[str, t.Any]: + info_dict = super().to_info_dict() + info_dict.update(mode=self.mode, encoding=self.encoding) + return info_dict + + def resolve_lazy_flag(self, value: t.Any) -> bool: if self.lazy is not None: return self.lazy - if value == '-': + if value == "-": return False - elif 'w' in self.mode: + elif "w" in self.mode: return True return False - def convert(self, value, param, ctx): + def convert( + self, value: t.Any, param: t.Optional["Parameter"], ctx: t.Optional["Context"] + ) -> t.Any: try: - if hasattr(value, 'read') or hasattr(value, 'write'): + if hasattr(value, "read") or hasattr(value, "write"): return value lazy = self.resolve_lazy_flag(value) if lazy: - f = LazyFile(value, self.mode, self.encoding, self.errors, - atomic=self.atomic) + f: t.IO = t.cast( + t.IO, + LazyFile( + value, self.mode, self.encoding, self.errors, atomic=self.atomic + ), + ) + if ctx is not None: - ctx.call_on_close(f.close_intelligently) + ctx.call_on_close(f.close_intelligently) # type: ignore + return f - f, should_close = open_stream(value, self.mode, - self.encoding, self.errors, - atomic=self.atomic) + f, should_close = open_stream( + value, self.mode, self.encoding, self.errors, atomic=self.atomic + ) + # If a context is provided, we automatically close the file # at the end of the context execution (or flush out). If a # context does not exist, it's the caller's responsibility to @@ -447,12 +721,26 @@ class File(ParamType): ctx.call_on_close(safecall(f.close)) else: ctx.call_on_close(safecall(f.flush)) + return f - except (IOError, OSError) as e: - self.fail('Could not open file: %s: %s' % ( - filename_to_ui(value), - get_streerror(e), - ), param, ctx) + except OSError as e: # noqa: B014 + self.fail(f"{os.fsdecode(value)!r}: {e.strerror}", param, ctx) + + def shell_complete( + self, ctx: "Context", param: "Parameter", incomplete: str + ) -> t.List["CompletionItem"]: + """Return a special completion marker that tells the completion + system to use the shell to provide file path completions. + + :param ctx: Invocation context for this command. + :param param: The parameter that is requesting completion. + :param incomplete: Value being completed. May be empty. + + .. versionadded:: 8.0 + """ + from click.shell_completion import CompletionItem + + return [CompletionItem(incomplete, type="file")] class Path(ParamType): @@ -461,9 +749,6 @@ class Path(ParamType): handle it returns just the filename. Secondly, it can perform various basic checks about what the file or directory should be. - .. versionchanged:: 6.0 - `allow_dash` was added. - :param exists: if set to true, the file or directory needs to exist for this value to be valid. If this is not required and a file does indeed not exist, then all further checks are @@ -479,17 +764,30 @@ class Path(ParamType): supposed to be done by the shell only. :param allow_dash: If this is set to `True`, a single dash to indicate standard streams is permitted. - :param path_type: optionally a string type that should be used to - represent the path. The default is `None` which - means the return value will be either bytes or - unicode depending on what makes most sense given the - input data Click deals with. + :param path_type: Convert the incoming path value to this type. If + ``None``, keep Python's default, which is ``str``. Useful to + convert to :class:`pathlib.Path`. + + .. versionchanged:: 8.0 + Allow passing ``type=pathlib.Path``. + + .. versionchanged:: 6.0 + Added the ``allow_dash`` parameter. """ + envvar_list_splitter = os.path.pathsep - def __init__(self, exists=False, file_okay=True, dir_okay=True, - writable=False, readable=True, resolve_path=False, - allow_dash=False, path_type=None): + def __init__( + self, + exists: bool = False, + file_okay: bool = True, + dir_okay: bool = True, + writable: bool = False, + readable: bool = True, + resolve_path: bool = False, + allow_dash: bool = False, + path_type: t.Optional[t.Type] = None, + ): self.exists = exists self.file_okay = file_okay self.dir_okay = dir_okay @@ -500,65 +798,116 @@ class Path(ParamType): self.type = path_type if self.file_okay and not self.dir_okay: - self.name = 'file' - self.path_type = 'File' + self.name = _("file") elif self.dir_okay and not self.file_okay: - self.name = 'directory' - self.path_type = 'Directory' + self.name = _("directory") else: - self.name = 'path' - self.path_type = 'Path' + self.name = _("path") - def coerce_path_result(self, rv): + def to_info_dict(self) -> t.Dict[str, t.Any]: + info_dict = super().to_info_dict() + info_dict.update( + exists=self.exists, + file_okay=self.file_okay, + dir_okay=self.dir_okay, + writable=self.writable, + readable=self.readable, + allow_dash=self.allow_dash, + ) + return info_dict + + def coerce_path_result(self, rv: t.Any) -> t.Any: if self.type is not None and not isinstance(rv, self.type): - if self.type is text_type: - rv = rv.decode(get_filesystem_encoding()) + if self.type is str: + rv = os.fsdecode(rv) + elif self.type is bytes: + rv = os.fsencode(rv) else: - rv = rv.encode(get_filesystem_encoding()) + rv = self.type(rv) + return rv - def convert(self, value, param, ctx): + def convert( + self, value: t.Any, param: t.Optional["Parameter"], ctx: t.Optional["Context"] + ) -> t.Any: rv = value - is_dash = self.file_okay and self.allow_dash and rv in (b'-', '-') + is_dash = self.file_okay and self.allow_dash and rv in (b"-", "-") if not is_dash: if self.resolve_path: - rv = os.path.realpath(rv) + # os.path.realpath doesn't resolve symlinks on Windows + # until Python 3.8. Use pathlib for now. + import pathlib + + rv = os.fsdecode(pathlib.Path(rv).resolve()) try: st = os.stat(rv) except OSError: if not self.exists: return self.coerce_path_result(rv) - self.fail('%s "%s" does not exist.' % ( - self.path_type, - filename_to_ui(value) - ), param, ctx) + self.fail( + _("{name} {filename!r} does not exist.").format( + name=self.name.title(), filename=os.fsdecode(value) + ), + param, + ctx, + ) if not self.file_okay and stat.S_ISREG(st.st_mode): - self.fail('%s "%s" is a file.' % ( - self.path_type, - filename_to_ui(value) - ), param, ctx) + self.fail( + _("{name} {filename!r} is a file.").format( + name=self.name.title(), filename=os.fsdecode(value) + ), + param, + ctx, + ) if not self.dir_okay and stat.S_ISDIR(st.st_mode): - self.fail('%s "%s" is a directory.' % ( - self.path_type, - filename_to_ui(value) - ), param, ctx) - if self.writable and not os.access(value, os.W_OK): - self.fail('%s "%s" is not writable.' % ( - self.path_type, - filename_to_ui(value) - ), param, ctx) - if self.readable and not os.access(value, os.R_OK): - self.fail('%s "%s" is not readable.' % ( - self.path_type, - filename_to_ui(value) - ), param, ctx) + self.fail( + _("{name} {filename!r} is a directory.").format( + name=self.name.title(), filename=os.fsdecode(value) + ), + param, + ctx, + ) + if self.writable and not os.access(rv, os.W_OK): + self.fail( + _("{name} {filename!r} is not writable.").format( + name=self.name.title(), filename=os.fsdecode(value) + ), + param, + ctx, + ) + if self.readable and not os.access(rv, os.R_OK): + self.fail( + _("{name} {filename!r} is not readable.").format( + name=self.name.title(), filename=os.fsdecode(value) + ), + param, + ctx, + ) return self.coerce_path_result(rv) + def shell_complete( + self, ctx: "Context", param: "Parameter", incomplete: str + ) -> t.List["CompletionItem"]: + """Return a special completion marker that tells the completion + system to use the shell to provide path completions for only + directories or any paths. + + :param ctx: Invocation context for this command. + :param param: The parameter that is requesting completion. + :param incomplete: Value being completed. May be empty. + + .. versionadded:: 8.0 + """ + from click.shell_completion import CompletionItem + + type = "dir" if self.dir_okay and not self.file_okay else "file" + return [CompletionItem(incomplete, type=type)] + class Tuple(CompositeParamType): """The default behavior of Click is to apply a type on a value directly. @@ -574,72 +923,107 @@ class Tuple(CompositeParamType): :param types: a list of types that should be used for the tuple items. """ - def __init__(self, types): + def __init__(self, types: t.Sequence[t.Union[t.Type, ParamType]]) -> None: self.types = [convert_type(ty) for ty in types] - @property - def name(self): - return "<" + " ".join(ty.name for ty in self.types) + ">" + def to_info_dict(self) -> t.Dict[str, t.Any]: + info_dict = super().to_info_dict() + info_dict["types"] = [t.to_info_dict() for t in self.types] + return info_dict @property - def arity(self): + def name(self) -> str: # type: ignore + return f"<{' '.join(ty.name for ty in self.types)}>" + + @property + def arity(self) -> int: # type: ignore return len(self.types) - def convert(self, value, param, ctx): - if len(value) != len(self.types): - raise TypeError('It would appear that nargs is set to conflict ' - 'with the composite type arity.') + def convert( + self, value: t.Any, param: t.Optional["Parameter"], ctx: t.Optional["Context"] + ) -> t.Any: + len_type = len(self.types) + len_value = len(value) + + if len_value != len_type: + self.fail( + ngettext( + "{len_type} values are required, but {len_value} was given.", + "{len_type} values are required, but {len_value} were given.", + len_value, + ).format(len_type=len_type, len_value=len_value), + param=param, + ctx=ctx, + ) + return tuple(ty(x, param, ctx) for ty, x in zip(self.types, value)) -def convert_type(ty, default=None): - """Converts a callable or python ty into the most appropriate param - ty. +def convert_type(ty: t.Optional[t.Any], default: t.Optional[t.Any] = None) -> ParamType: + """Find the most appropriate :class:`ParamType` for the given Python + type. If the type isn't provided, it can be inferred from a default + value. """ guessed_type = False + if ty is None and default is not None: - if isinstance(default, tuple): - ty = tuple(map(type, default)) + if isinstance(default, (tuple, list)): + # If the default is empty, ty will remain None and will + # return STRING. + if default: + item = default[0] + + # A tuple of tuples needs to detect the inner types. + # Can't call convert recursively because that would + # incorrectly unwind the tuple to a single type. + if isinstance(item, (tuple, list)): + ty = tuple(map(type, item)) + else: + ty = type(item) else: ty = type(default) + guessed_type = True if isinstance(ty, tuple): return Tuple(ty) + if isinstance(ty, ParamType): return ty - if ty is text_type or ty is str or ty is None: + + if ty is str or ty is None: return STRING + if ty is int: return INT - # Booleans are only okay if not guessed. This is done because for - # flags the default value is actually a bit of a lie in that it - # indicates which of the flags is the one we want. See get_default() - # for more information. - if ty is bool and not guessed_type: - return BOOL + if ty is float: return FLOAT + + if ty is bool: + return BOOL + if guessed_type: return STRING - # Catch a common mistake if __debug__: try: if issubclass(ty, ParamType): - raise AssertionError('Attempted to use an uninstantiated ' - 'parameter type (%s).' % ty) + raise AssertionError( + f"Attempted to use an uninstantiated parameter type ({ty})." + ) except TypeError: + # ty is an instance (correct), so issubclass fails. pass + return FuncParamType(ty) #: A dummy parameter type that just does nothing. From a user's -#: perspective this appears to just be the same as `STRING` but internally -#: no string conversion takes place. This is necessary to achieve the -#: same bytes/unicode behavior on Python 2/3 in situations where you want -#: to not convert argument types. This is usually useful when working -#: with file paths as they can appear in bytes and unicode. +#: perspective this appears to just be the same as `STRING` but +#: internally no string conversion takes place if the input was bytes. +#: This is usually useful when working with file paths as they can +#: appear in bytes and unicode. #: #: For path related uses the :class:`Path` type is a better choice but #: there are situations where an unprocessed type is useful which is why diff --git a/libs/click/utils.py b/libs/click/utils.py index fc84369fc..16033d623 100644 --- a/libs/click/utils.py +++ b/libs/click/utils.py @@ -1,92 +1,130 @@ import os import sys +import typing as t +from functools import update_wrapper +from types import ModuleType +from ._compat import _default_text_stderr +from ._compat import _default_text_stdout +from ._compat import _find_binary_writer +from ._compat import auto_wrap_for_ansi +from ._compat import binary_streams +from ._compat import get_filesystem_encoding +from ._compat import open_stream +from ._compat import should_strip_ansi +from ._compat import strip_ansi +from ._compat import text_streams +from ._compat import WIN from .globals import resolve_color_default -from ._compat import text_type, open_stream, get_filesystem_encoding, \ - get_streerror, string_types, PY2, binary_streams, text_streams, \ - filename_to_ui, auto_wrap_for_ansi, strip_ansi, should_strip_ansi, \ - _default_text_stdout, _default_text_stderr, is_bytes, WIN +if t.TYPE_CHECKING: + import typing_extensions as te -if not PY2: - from ._compat import _find_binary_writer -elif WIN: - from ._winconsole import _get_windows_argv, \ - _hash_py_argv, _initial_argv_hash +F = t.TypeVar("F", bound=t.Callable[..., t.Any]) -echo_native_types = string_types + (bytes, bytearray) +def _posixify(name: str) -> str: + return "-".join(name.split()).lower() -def _posixify(name): - return '-'.join(name.split()).lower() - - -def safecall(func): +def safecall(func: F) -> F: """Wraps a function so that it swallows exceptions.""" - def wrapper(*args, **kwargs): + + def wrapper(*args, **kwargs): # type: ignore try: return func(*args, **kwargs) except Exception: pass - return wrapper + + return update_wrapper(t.cast(F, wrapper), func) -def make_str(value): +def make_str(value: t.Any) -> str: """Converts a value into a valid string.""" if isinstance(value, bytes): try: return value.decode(get_filesystem_encoding()) except UnicodeError: - return value.decode('utf-8', 'replace') - return text_type(value) + return value.decode("utf-8", "replace") + return str(value) -def make_default_short_help(help, max_length=45): - """Return a condensed version of help string.""" +def make_default_short_help(help: str, max_length: int = 45) -> str: + """Returns a condensed version of help string.""" + # Consider only the first paragraph. + paragraph_end = help.find("\n\n") + + if paragraph_end != -1: + help = help[:paragraph_end] + + # Collapse newlines, tabs, and spaces. words = help.split() + + if not words: + return "" + + # The first paragraph started with a "no rewrap" marker, ignore it. + if words[0] == "\b": + words = words[1:] + total_length = 0 - result = [] - done = False + last_index = len(words) - 1 - for word in words: - if word[-1:] == '.': - done = True - new_length = result and 1 + len(word) or len(word) - if total_length + new_length > max_length: - result.append('...') - done = True - else: - if result: - result.append(' ') - result.append(word) - if done: + for i, word in enumerate(words): + total_length += len(word) + (i > 0) + + if total_length > max_length: # too long, truncate break - total_length += new_length - return ''.join(result) + if word[-1] == ".": # sentence end, truncate without "..." + return " ".join(words[: i + 1]) + + if total_length == max_length and i != last_index: + break # not at sentence end, truncate with "..." + else: + return " ".join(words) # no truncation needed + + # Account for the length of the suffix. + total_length += len("...") + + # remove words until the length is short enough + while i > 0: + total_length -= len(words[i]) + (i > 0) + + if total_length <= max_length: + break + + i -= 1 + + return " ".join(words[:i]) + "..." -class LazyFile(object): +class LazyFile: """A lazy file works like a regular file but it does not fully open the file but it does perform some basic checks early to see if the filename parameter does make sense. This is useful for safely opening files for writing. """ - def __init__(self, filename, mode='r', encoding=None, errors='strict', - atomic=False): + def __init__( + self, + filename: str, + mode: str = "r", + encoding: t.Optional[str] = None, + errors: t.Optional[str] = "strict", + atomic: bool = False, + ): self.name = filename self.mode = mode self.encoding = encoding self.errors = errors self.atomic = atomic + self._f: t.Optional[t.IO] - if filename == '-': - self._f, self.should_close = open_stream(filename, mode, - encoding, errors) + if filename == "-": + self._f, self.should_close = open_stream(filename, mode, encoding, errors) else: - if 'r' in mode: + if "r" in mode: # Open and close the file in case we're opening it for # reading so that we can catch at least some errors in # some cases early. @@ -94,15 +132,15 @@ class LazyFile(object): self._f = None self.should_close = True - def __getattr__(self, name): + def __getattr__(self, name: str) -> t.Any: return getattr(self.open(), name) - def __repr__(self): + def __repr__(self) -> str: if self._f is not None: return repr(self._f) - return '' % (self.name, self.mode) + return f"" - def open(self): + def open(self) -> t.IO: """Opens the file if it's not yet open. This call might fail with a :exc:`FileError`. Not handling this error will produce an error that Click shows. @@ -110,106 +148,103 @@ class LazyFile(object): if self._f is not None: return self._f try: - rv, self.should_close = open_stream(self.name, self.mode, - self.encoding, - self.errors, - atomic=self.atomic) - except (IOError, OSError) as e: + rv, self.should_close = open_stream( + self.name, self.mode, self.encoding, self.errors, atomic=self.atomic + ) + except OSError as e: # noqa: E402 from .exceptions import FileError - raise FileError(self.name, hint=get_streerror(e)) + + raise FileError(self.name, hint=e.strerror) from e self._f = rv return rv - def close(self): + def close(self) -> None: """Closes the underlying file, no matter what.""" if self._f is not None: self._f.close() - def close_intelligently(self): + def close_intelligently(self) -> None: """This function only closes the file if it was opened by the lazy file wrapper. For instance this will never close stdin. """ if self.should_close: self.close() - def __enter__(self): + def __enter__(self) -> "LazyFile": return self - def __exit__(self, exc_type, exc_value, tb): + def __exit__(self, exc_type, exc_value, tb): # type: ignore self.close_intelligently() - def __iter__(self): + def __iter__(self) -> t.Iterator[t.AnyStr]: self.open() - return iter(self._f) + return iter(self._f) # type: ignore -class KeepOpenFile(object): - - def __init__(self, file): +class KeepOpenFile: + def __init__(self, file: t.IO) -> None: self._file = file - def __getattr__(self, name): + def __getattr__(self, name: str) -> t.Any: return getattr(self._file, name) - def __enter__(self): + def __enter__(self) -> "KeepOpenFile": return self - def __exit__(self, exc_type, exc_value, tb): + def __exit__(self, exc_type, exc_value, tb): # type: ignore pass - def __repr__(self): + def __repr__(self) -> str: return repr(self._file) - def __iter__(self): + def __iter__(self) -> t.Iterator[t.AnyStr]: return iter(self._file) -def echo(message=None, file=None, nl=True, err=False, color=None): - """Prints a message plus a newline to the given file or stdout. On - first sight, this looks like the print function, but it has improved - support for handling Unicode and binary data that does not fail no - matter how badly configured the system is. +def echo( + message: t.Optional[t.Any] = None, + file: t.Optional[t.IO] = None, + nl: bool = True, + err: bool = False, + color: t.Optional[bool] = None, +) -> None: + """Print a message and newline to stdout or a file. This should be + used instead of :func:`print` because it provides better support + for different data, files, and environments. - Primarily it means that you can print binary data as well as Unicode - data on both 2.x and 3.x to the given file in the most appropriate way - possible. This is a very carefree function in that it will try its - best to not fail. As of Click 6.0 this includes support for unicode - output on the Windows console. + Compared to :func:`print`, this does the following: - In addition to that, if `colorama`_ is installed, the echo function will - also support clever handling of ANSI codes. Essentially it will then - do the following: + - Ensures that the output encoding is not misconfigured on Linux. + - Supports Unicode in the Windows console. + - Supports writing to binary outputs, and supports writing bytes + to text outputs. + - Supports colors and styles on Windows. + - Removes ANSI color and style codes if the output does not look + like an interactive terminal. + - Always flushes the output. - - add transparent handling of ANSI color codes on Windows. - - hide ANSI codes automatically if the destination file is not a - terminal. - - .. _colorama: https://pypi.org/project/colorama/ + :param message: The string or bytes to output. Other objects are + converted to strings. + :param file: The file to write to. Defaults to ``stdout``. + :param err: Write to ``stderr`` instead of ``stdout``. + :param nl: Print a newline after the message. Enabled by default. + :param color: Force showing or hiding colors and other styles. By + default Click will remove color if the output does not look like + an interactive terminal. .. versionchanged:: 6.0 - As of Click 6.0 the echo function will properly support unicode - output on the windows console. Not that click does not modify - the interpreter in any way which means that `sys.stdout` or the - print statement or function will still not provide unicode support. - - .. versionchanged:: 2.0 - Starting with version 2.0 of Click, the echo function will work - with colorama if it's installed. - - .. versionadded:: 3.0 - The `err` parameter was added. + Support Unicode output on the Windows console. Click does not + modify ``sys.stdout``, so ``sys.stdout.write()`` and ``print()`` + will still not support Unicode. .. versionchanged:: 4.0 - Added the `color` flag. + Added the ``color`` parameter. - :param message: the message to print - :param file: the file to write to (defaults to ``stdout``) - :param err: if set to true the file defaults to ``stderr`` instead of - ``stdout``. This is faster and easier than calling - :func:`get_text_stderr` yourself. - :param nl: if set to `True` (the default) a newline is printed afterwards. - :param color: controls if the terminal supports ANSI colors or not. The - default is autodetection. + .. versionadded:: 3.0 + Added the ``err`` parameter. + + .. versionchanged:: 2.0 + Support colors on Windows if colorama is installed. """ if file is None: if err: @@ -218,70 +253,73 @@ def echo(message=None, file=None, nl=True, err=False, color=None): file = _default_text_stdout() # Convert non bytes/text into the native string type. - if message is not None and not isinstance(message, echo_native_types): - message = text_type(message) + if message is not None and not isinstance(message, (str, bytes, bytearray)): + out: t.Optional[t.Union[str, bytes]] = str(message) + else: + out = message if nl: - message = message or u'' - if isinstance(message, text_type): - message += u'\n' + out = out or "" + if isinstance(out, str): + out += "\n" else: - message += b'\n' + out += b"\n" - # If there is a message, and we're in Python 3, and the value looks - # like bytes, we manually need to find the binary stream and write the - # message in there. This is done separately so that most stream - # types will work as you would expect. Eg: you can write to StringIO - # for other cases. - if message and not PY2 and is_bytes(message): + if not out: + file.flush() + return + + # If there is a message and the value looks like bytes, we manually + # need to find the binary stream and write the message in there. + # This is done separately so that most stream types will work as you + # would expect. Eg: you can write to StringIO for other cases. + if isinstance(out, (bytes, bytearray)): binary_file = _find_binary_writer(file) + if binary_file is not None: file.flush() - binary_file.write(message) + binary_file.write(out) binary_file.flush() return - # ANSI-style support. If there is no message or we are dealing with - # bytes nothing is happening. If we are connected to a file we want - # to strip colors. If we are on windows we either wrap the stream - # to strip the color or we use the colorama support to translate the - # ansi codes to API calls. - if message and not is_bytes(message): + # ANSI style code support. For no message or bytes, nothing happens. + # When outputting to a file instead of a terminal, strip codes. + else: color = resolve_color_default(color) + if should_strip_ansi(file, color): - message = strip_ansi(message) + out = strip_ansi(out) elif WIN: if auto_wrap_for_ansi is not None: - file = auto_wrap_for_ansi(file) + file = auto_wrap_for_ansi(file) # type: ignore elif not color: - message = strip_ansi(message) + out = strip_ansi(out) - if message: - file.write(message) + file.write(out) # type: ignore file.flush() -def get_binary_stream(name): - """Returns a system stream for byte processing. This essentially - returns the stream from the sys module with the given name but it - solves some compatibility issues between different Python versions. - Primarily this function is necessary for getting binary streams on - Python 3. +def get_binary_stream(name: "te.Literal['stdin', 'stdout', 'stderr']") -> t.BinaryIO: + """Returns a system stream for byte processing. :param name: the name of the stream to open. Valid names are ``'stdin'``, ``'stdout'`` and ``'stderr'`` """ opener = binary_streams.get(name) if opener is None: - raise TypeError('Unknown standard stream %r' % name) + raise TypeError(f"Unknown standard stream '{name}'") return opener() -def get_text_stream(name, encoding=None, errors='strict'): +def get_text_stream( + name: "te.Literal['stdin', 'stdout', 'stderr']", + encoding: t.Optional[str] = None, + errors: t.Optional[str] = "strict", +) -> t.TextIO: """Returns a system stream for text processing. This usually returns a wrapped stream around a binary stream returned from - :func:`get_binary_stream` but it also can take shortcuts on Python 3 - for already correctly configured streams. + :func:`get_binary_stream` but it also can take shortcuts for already + correctly configured streams. :param name: the name of the stream to open. Valid names are ``'stdin'``, ``'stdout'`` and ``'stderr'`` @@ -290,12 +328,18 @@ def get_text_stream(name, encoding=None, errors='strict'): """ opener = text_streams.get(name) if opener is None: - raise TypeError('Unknown standard stream %r' % name) + raise TypeError(f"Unknown standard stream '{name}'") return opener(encoding, errors) -def open_file(filename, mode='r', encoding=None, errors='strict', - lazy=False, atomic=False): +def open_file( + filename: str, + mode: str = "r", + encoding: t.Optional[str] = None, + errors: t.Optional[str] = "strict", + lazy: bool = False, + atomic: bool = False, +) -> t.IO: """This is similar to how the :class:`File` works but for manual usage. Files are opened non lazy by default. This can open regular files as well as stdin/stdout if ``'-'`` is passed. @@ -319,36 +363,35 @@ def open_file(filename, mode='r', encoding=None, errors='strict', moved on close. """ if lazy: - return LazyFile(filename, mode, encoding, errors, atomic=atomic) - f, should_close = open_stream(filename, mode, encoding, errors, - atomic=atomic) + return t.cast(t.IO, LazyFile(filename, mode, encoding, errors, atomic=atomic)) + f, should_close = open_stream(filename, mode, encoding, errors, atomic=atomic) if not should_close: - f = KeepOpenFile(f) + f = t.cast(t.IO, KeepOpenFile(f)) return f -def get_os_args(): - """This returns the argument part of sys.argv in the most appropriate - form for processing. What this means is that this return value is in - a format that works for Click to process but does not necessarily - correspond well to what's actually standard for the interpreter. +def get_os_args() -> t.Sequence[str]: + """Returns the argument part of ``sys.argv``, removing the first + value which is the name of the script. - On most environments the return value is ``sys.argv[:1]`` unchanged. - However if you are on Windows and running Python 2 the return value - will actually be a list of unicode strings instead because the - default behavior on that platform otherwise will not be able to - carry all possible values that sys.argv can have. - - .. versionadded:: 6.0 + .. deprecated:: 8.0 + Will be removed in Click 8.1. Access ``sys.argv[1:]`` directly + instead. """ - # We can only extract the unicode argv if sys.argv has not been - # changed since the startup of the application. - if PY2 and WIN and _initial_argv_hash == _hash_py_argv(): - return _get_windows_argv() + import warnings + + warnings.warn( + "'get_os_args' is deprecated and will be removed in Click 8.1." + " Access 'sys.argv[1:]' directly instead.", + DeprecationWarning, + stacklevel=2, + ) return sys.argv[1:] -def format_filename(filename, shorten=False): +def format_filename( + filename: t.Union[str, bytes, os.PathLike], shorten: bool = False +) -> str: """Formats a filename for user display. The main purpose of this function is to ensure that the filename can be displayed at all. This will decode the filename to unicode if necessary in a way that it will @@ -362,10 +405,11 @@ def format_filename(filename, shorten=False): """ if shorten: filename = os.path.basename(filename) - return filename_to_ui(filename) + + return os.fsdecode(filename) -def get_app_dir(app_name, roaming=True, force_posix=False): +def get_app_dir(app_name: str, roaming: bool = True, force_posix: bool = False) -> str: r"""Returns the config folder for the application. The default behavior is to return whatever is most appropriate for the operating system. @@ -380,13 +424,9 @@ def get_app_dir(app_name, roaming=True, force_posix=False): ``~/.config/foo-bar`` Unix (POSIX): ``~/.foo-bar`` - Win XP (roaming): - ``C:\Documents and Settings\\Local Settings\Application Data\Foo Bar`` - Win XP (not roaming): - ``C:\Documents and Settings\\Application Data\Foo Bar`` - Win 7 (roaming): + Windows (roaming): ``C:\Users\\AppData\Roaming\Foo Bar`` - Win 7 (not roaming): + Windows (not roaming): ``C:\Users\\AppData\Local\Foo Bar`` .. versionadded:: 2.0 @@ -401,22 +441,24 @@ def get_app_dir(app_name, roaming=True, force_posix=False): application support folder. """ if WIN: - key = roaming and 'APPDATA' or 'LOCALAPPDATA' + key = "APPDATA" if roaming else "LOCALAPPDATA" folder = os.environ.get(key) if folder is None: - folder = os.path.expanduser('~') + folder = os.path.expanduser("~") return os.path.join(folder, app_name) if force_posix: - return os.path.join(os.path.expanduser('~/.' + _posixify(app_name))) - if sys.platform == 'darwin': - return os.path.join(os.path.expanduser( - '~/Library/Application Support'), app_name) + return os.path.join(os.path.expanduser(f"~/.{_posixify(app_name)}")) + if sys.platform == "darwin": + return os.path.join( + os.path.expanduser("~/Library/Application Support"), app_name + ) return os.path.join( - os.environ.get('XDG_CONFIG_HOME', os.path.expanduser('~/.config')), - _posixify(app_name)) + os.environ.get("XDG_CONFIG_HOME", os.path.expanduser("~/.config")), + _posixify(app_name), + ) -class PacifyFlushWrapper(object): +class PacifyFlushWrapper: """This wrapper is used to catch and suppress BrokenPipeErrors resulting from ``.flush()`` being called on broken pipe during the shutdown/final-GC of the Python interpreter. Notably ``.flush()`` is always called on @@ -425,16 +467,113 @@ class PacifyFlushWrapper(object): pipe, all calls and attributes are proxied. """ - def __init__(self, wrapped): + def __init__(self, wrapped: t.IO) -> None: self.wrapped = wrapped - def flush(self): + def flush(self) -> None: try: self.wrapped.flush() - except IOError as e: + except OSError as e: import errno + if e.errno != errno.EPIPE: raise - def __getattr__(self, attr): + def __getattr__(self, attr: str) -> t.Any: return getattr(self.wrapped, attr) + + +def _detect_program_name( + path: t.Optional[str] = None, _main: ModuleType = sys.modules["__main__"] +) -> str: + """Determine the command used to run the program, for use in help + text. If a file or entry point was executed, the file name is + returned. If ``python -m`` was used to execute a module or package, + ``python -m name`` is returned. + + This doesn't try to be too precise, the goal is to give a concise + name for help text. Files are only shown as their name without the + path. ``python`` is only shown for modules, and the full path to + ``sys.executable`` is not shown. + + :param path: The Python file being executed. Python puts this in + ``sys.argv[0]``, which is used by default. + :param _main: The ``__main__`` module. This should only be passed + during internal testing. + + .. versionadded:: 8.0 + Based on command args detection in the Werkzeug reloader. + + :meta private: + """ + if not path: + path = sys.argv[0] + + # The value of __package__ indicates how Python was called. It may + # not exist if a setuptools script is installed as an egg. It may be + # set incorrectly for entry points created with pip on Windows. + if getattr(_main, "__package__", None) is None or ( + os.name == "nt" + and _main.__package__ == "" + and not os.path.exists(path) + and os.path.exists(f"{path}.exe") + ): + # Executed a file, like "python app.py". + return os.path.basename(path) + + # Executed a module, like "python -m example". + # Rewritten by Python from "-m script" to "/path/to/script.py". + # Need to look at main module to determine how it was executed. + py_module = t.cast(str, _main.__package__) + name = os.path.splitext(os.path.basename(path))[0] + + # A submodule like "example.cli". + if name != "__main__": + py_module = f"{py_module}.{name}" + + return f"python -m {py_module.lstrip('.')}" + + +def _expand_args( + args: t.Iterable[str], + *, + user: bool = True, + env: bool = True, + glob_recursive: bool = True, +) -> t.List[str]: + """Simulate Unix shell expansion with Python functions. + + See :func:`glob.glob`, :func:`os.path.expanduser`, and + :func:`os.path.expandvars`. + + This intended for use on Windows, where the shell does not do any + expansion. It may not exactly match what a Unix shell would do. + + :param args: List of command line arguments to expand. + :param user: Expand user home directory. + :param env: Expand environment variables. + :param glob_recursive: ``**`` matches directories recursively. + + .. versionadded:: 8.0 + + :meta private: + """ + from glob import glob + + out = [] + + for arg in args: + if user: + arg = os.path.expanduser(arg) + + if env: + arg = os.path.expandvars(arg) + + matches = glob(arg, recursive=glob_recursive) + + if not matches: + out.append(arg) + else: + out.extend(matches) + + return out diff --git a/libs/contextlib2.py b/libs/contextlib2.py deleted file mode 100644 index f08df14ce..000000000 --- a/libs/contextlib2.py +++ /dev/null @@ -1,436 +0,0 @@ -"""contextlib2 - backports and enhancements to the contextlib module""" - -import sys -import warnings -from collections import deque -from functools import wraps - -__all__ = ["contextmanager", "closing", "ContextDecorator", "ExitStack", - "redirect_stdout", "redirect_stderr", "suppress"] - -# Backwards compatibility -__all__ += ["ContextStack"] - -class ContextDecorator(object): - "A base class or mixin that enables context managers to work as decorators." - - def refresh_cm(self): - """Returns the context manager used to actually wrap the call to the - decorated function. - - The default implementation just returns *self*. - - Overriding this method allows otherwise one-shot context managers - like _GeneratorContextManager to support use as decorators via - implicit recreation. - - DEPRECATED: refresh_cm was never added to the standard library's - ContextDecorator API - """ - warnings.warn("refresh_cm was never added to the standard library", - DeprecationWarning) - return self._recreate_cm() - - def _recreate_cm(self): - """Return a recreated instance of self. - - Allows an otherwise one-shot context manager like - _GeneratorContextManager to support use as - a decorator via implicit recreation. - - This is a private interface just for _GeneratorContextManager. - See issue #11647 for details. - """ - return self - - def __call__(self, func): - @wraps(func) - def inner(*args, **kwds): - with self._recreate_cm(): - return func(*args, **kwds) - return inner - - -class _GeneratorContextManager(ContextDecorator): - """Helper for @contextmanager decorator.""" - - def __init__(self, func, args, kwds): - self.gen = func(*args, **kwds) - self.func, self.args, self.kwds = func, args, kwds - # Issue 19330: ensure context manager instances have good docstrings - doc = getattr(func, "__doc__", None) - if doc is None: - doc = type(self).__doc__ - self.__doc__ = doc - # Unfortunately, this still doesn't provide good help output when - # inspecting the created context manager instances, since pydoc - # currently bypasses the instance docstring and shows the docstring - # for the class instead. - # See http://bugs.python.org/issue19404 for more details. - - def _recreate_cm(self): - # _GCM instances are one-shot context managers, so the - # CM must be recreated each time a decorated function is - # called - return self.__class__(self.func, self.args, self.kwds) - - def __enter__(self): - try: - return next(self.gen) - except StopIteration: - raise RuntimeError("generator didn't yield") - - def __exit__(self, type, value, traceback): - if type is None: - try: - next(self.gen) - except StopIteration: - return - else: - raise RuntimeError("generator didn't stop") - else: - if value is None: - # Need to force instantiation so we can reliably - # tell if we get the same exception back - value = type() - try: - self.gen.throw(type, value, traceback) - raise RuntimeError("generator didn't stop after throw()") - except StopIteration as exc: - # Suppress StopIteration *unless* it's the same exception that - # was passed to throw(). This prevents a StopIteration - # raised inside the "with" statement from being suppressed. - return exc is not value - except RuntimeError as exc: - # Don't re-raise the passed in exception - if exc is value: - return False - # Likewise, avoid suppressing if a StopIteration exception - # was passed to throw() and later wrapped into a RuntimeError - # (see PEP 479). - if _HAVE_EXCEPTION_CHAINING and exc.__cause__ is value: - return False - raise - except: - # only re-raise if it's *not* the exception that was - # passed to throw(), because __exit__() must not raise - # an exception unless __exit__() itself failed. But throw() - # has to raise the exception to signal propagation, so this - # fixes the impedance mismatch between the throw() protocol - # and the __exit__() protocol. - # - if sys.exc_info()[1] is not value: - raise - - -def contextmanager(func): - """@contextmanager decorator. - - Typical usage: - - @contextmanager - def some_generator(): - - try: - yield - finally: - - - This makes this: - - with some_generator() as : - - - equivalent to this: - - - try: - = - - finally: - - - """ - @wraps(func) - def helper(*args, **kwds): - return _GeneratorContextManager(func, args, kwds) - return helper - - -class closing(object): - """Context to automatically close something at the end of a block. - - Code like this: - - with closing(.open()) as f: - - - is equivalent to this: - - f = .open() - try: - - finally: - f.close() - - """ - def __init__(self, thing): - self.thing = thing - def __enter__(self): - return self.thing - def __exit__(self, *exc_info): - self.thing.close() - - -class _RedirectStream(object): - - _stream = None - - def __init__(self, new_target): - self._new_target = new_target - # We use a list of old targets to make this CM re-entrant - self._old_targets = [] - - def __enter__(self): - self._old_targets.append(getattr(sys, self._stream)) - setattr(sys, self._stream, self._new_target) - return self._new_target - - def __exit__(self, exctype, excinst, exctb): - setattr(sys, self._stream, self._old_targets.pop()) - - -class redirect_stdout(_RedirectStream): - """Context manager for temporarily redirecting stdout to another file. - - # How to send help() to stderr - with redirect_stdout(sys.stderr): - help(dir) - - # How to write help() to a file - with open('help.txt', 'w') as f: - with redirect_stdout(f): - help(pow) - """ - - _stream = "stdout" - - -class redirect_stderr(_RedirectStream): - """Context manager for temporarily redirecting stderr to another file.""" - - _stream = "stderr" - - -class suppress(object): - """Context manager to suppress specified exceptions - - After the exception is suppressed, execution proceeds with the next - statement following the with statement. - - with suppress(FileNotFoundError): - os.remove(somefile) - # Execution still resumes here if the file was already removed - """ - - def __init__(self, *exceptions): - self._exceptions = exceptions - - def __enter__(self): - pass - - def __exit__(self, exctype, excinst, exctb): - # Unlike isinstance and issubclass, CPython exception handling - # currently only looks at the concrete type hierarchy (ignoring - # the instance and subclass checking hooks). While Guido considers - # that a bug rather than a feature, it's a fairly hard one to fix - # due to various internal implementation details. suppress provides - # the simpler issubclass based semantics, rather than trying to - # exactly reproduce the limitations of the CPython interpreter. - # - # See http://bugs.python.org/issue12029 for more details - return exctype is not None and issubclass(exctype, self._exceptions) - - -# Context manipulation is Python 3 only -_HAVE_EXCEPTION_CHAINING = sys.version_info[0] >= 3 -if _HAVE_EXCEPTION_CHAINING: - def _make_context_fixer(frame_exc): - def _fix_exception_context(new_exc, old_exc): - # Context may not be correct, so find the end of the chain - while 1: - exc_context = new_exc.__context__ - if exc_context is old_exc: - # Context is already set correctly (see issue 20317) - return - if exc_context is None or exc_context is frame_exc: - break - new_exc = exc_context - # Change the end of the chain to point to the exception - # we expect it to reference - new_exc.__context__ = old_exc - return _fix_exception_context - - def _reraise_with_existing_context(exc_details): - try: - # bare "raise exc_details[1]" replaces our carefully - # set-up context - fixed_ctx = exc_details[1].__context__ - raise exc_details[1] - except BaseException: - exc_details[1].__context__ = fixed_ctx - raise -else: - # No exception context in Python 2 - def _make_context_fixer(frame_exc): - return lambda new_exc, old_exc: None - - # Use 3 argument raise in Python 2, - # but use exec to avoid SyntaxError in Python 3 - def _reraise_with_existing_context(exc_details): - exc_type, exc_value, exc_tb = exc_details - exec ("raise exc_type, exc_value, exc_tb") - -# Handle old-style classes if they exist -try: - from types import InstanceType -except ImportError: - # Python 3 doesn't have old-style classes - _get_type = type -else: - # Need to handle old-style context managers on Python 2 - def _get_type(obj): - obj_type = type(obj) - if obj_type is InstanceType: - return obj.__class__ # Old-style class - return obj_type # New-style class - -# Inspired by discussions on http://bugs.python.org/issue13585 -class ExitStack(object): - """Context manager for dynamic management of a stack of exit callbacks - - For example: - - with ExitStack() as stack: - files = [stack.enter_context(open(fname)) for fname in filenames] - # All opened files will automatically be closed at the end of - # the with statement, even if attempts to open files later - # in the list raise an exception - - """ - def __init__(self): - self._exit_callbacks = deque() - - def pop_all(self): - """Preserve the context stack by transferring it to a new instance""" - new_stack = type(self)() - new_stack._exit_callbacks = self._exit_callbacks - self._exit_callbacks = deque() - return new_stack - - def _push_cm_exit(self, cm, cm_exit): - """Helper to correctly register callbacks to __exit__ methods""" - def _exit_wrapper(*exc_details): - return cm_exit(cm, *exc_details) - _exit_wrapper.__self__ = cm - self.push(_exit_wrapper) - - def push(self, exit): - """Registers a callback with the standard __exit__ method signature - - Can suppress exceptions the same way __exit__ methods can. - - Also accepts any object with an __exit__ method (registering a call - to the method instead of the object itself) - """ - # We use an unbound method rather than a bound method to follow - # the standard lookup behaviour for special methods - _cb_type = _get_type(exit) - try: - exit_method = _cb_type.__exit__ - except AttributeError: - # Not a context manager, so assume its a callable - self._exit_callbacks.append(exit) - else: - self._push_cm_exit(exit, exit_method) - return exit # Allow use as a decorator - - def callback(self, callback, *args, **kwds): - """Registers an arbitrary callback and arguments. - - Cannot suppress exceptions. - """ - def _exit_wrapper(exc_type, exc, tb): - callback(*args, **kwds) - # We changed the signature, so using @wraps is not appropriate, but - # setting __wrapped__ may still help with introspection - _exit_wrapper.__wrapped__ = callback - self.push(_exit_wrapper) - return callback # Allow use as a decorator - - def enter_context(self, cm): - """Enters the supplied context manager - - If successful, also pushes its __exit__ method as a callback and - returns the result of the __enter__ method. - """ - # We look up the special methods on the type to match the with statement - _cm_type = _get_type(cm) - _exit = _cm_type.__exit__ - result = _cm_type.__enter__(cm) - self._push_cm_exit(cm, _exit) - return result - - def close(self): - """Immediately unwind the context stack""" - self.__exit__(None, None, None) - - def __enter__(self): - return self - - def __exit__(self, *exc_details): - received_exc = exc_details[0] is not None - - # We manipulate the exception state so it behaves as though - # we were actually nesting multiple with statements - frame_exc = sys.exc_info()[1] - _fix_exception_context = _make_context_fixer(frame_exc) - - # Callbacks are invoked in LIFO order to match the behaviour of - # nested context managers - suppressed_exc = False - pending_raise = False - while self._exit_callbacks: - cb = self._exit_callbacks.pop() - try: - if cb(*exc_details): - suppressed_exc = True - pending_raise = False - exc_details = (None, None, None) - except: - new_exc_details = sys.exc_info() - # simulate the stack of exceptions by setting the context - _fix_exception_context(new_exc_details[1], exc_details[1]) - pending_raise = True - exc_details = new_exc_details - if pending_raise: - _reraise_with_existing_context(exc_details) - return received_exc and suppressed_exc - -# Preserve backwards compatibility -class ContextStack(ExitStack): - """Backwards compatibility alias for ExitStack""" - - def __init__(self): - warnings.warn("ContextStack has been renamed to ExitStack", - DeprecationWarning) - super(ContextStack, self).__init__() - - def register_exit(self, callback): - return self.push(callback) - - def register(self, callback, *args, **kwds): - return self.callback(callback, *args, **kwds) - - def preserve(self): - return self.pop_all() diff --git a/libs/dateutil/__init__.py b/libs/dateutil/__init__.py index ba89aa70b..0defb82e2 100644 --- a/libs/dateutil/__init__.py +++ b/libs/dateutil/__init__.py @@ -1,2 +1,8 @@ # -*- coding: utf-8 -*- -__version__ = "2.6.0" +try: + from ._version import version as __version__ +except ImportError: + __version__ = 'unknown' + +__all__ = ['easter', 'parser', 'relativedelta', 'rrule', 'tz', + 'utils', 'zoneinfo'] diff --git a/libs/dateutil/_common.py b/libs/dateutil/_common.py index cd2a33860..4eb2659bd 100644 --- a/libs/dateutil/_common.py +++ b/libs/dateutil/_common.py @@ -2,6 +2,7 @@ Common code used in multiple modules. """ + class weekday(object): __slots__ = ["weekday", "n"] @@ -23,7 +24,14 @@ class weekday(object): return False return True - __hash__ = None + def __hash__(self): + return hash(( + self.weekday, + self.n, + )) + + def __ne__(self, other): + return not (self == other) def __repr__(self): s = ("MO", "TU", "WE", "TH", "FR", "SA", "SU")[self.weekday] @@ -31,3 +39,5 @@ class weekday(object): return s else: return "%s(%+d)" % (s, self.n) + +# vim:ts=4:sw=4:et diff --git a/libs/dateutil/_version.py b/libs/dateutil/_version.py index 713fe0dfe..b723056a7 100644 --- a/libs/dateutil/_version.py +++ b/libs/dateutil/_version.py @@ -1,4 +1,5 @@ # coding: utf-8 # file generated by setuptools_scm # don't change, don't track in version control -version = '2.7.3' +version = '2.8.2' +version_tuple = (2, 8, 2) diff --git a/libs/dateutil/easter.py b/libs/dateutil/easter.py index e4def97f9..f74d1f744 100644 --- a/libs/dateutil/easter.py +++ b/libs/dateutil/easter.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- """ -This module offers a generic easter computing method for any given year, using +This module offers a generic Easter computing method for any given year, using Western, Orthodox or Julian algorithms. """ @@ -21,15 +21,15 @@ def easter(year, method=EASTER_WESTERN): quoted in "Explanatory Supplement to the Astronomical Almanac", P. Kenneth Seidelmann, editor. - This algorithm implements three different easter + This algorithm implements three different Easter calculation methods: - 1 - Original calculation in Julian calendar, valid in - dates after 326 AD - 2 - Original method, with date converted to Gregorian - calendar, valid in years 1583 to 4099 - 3 - Revised method, in Gregorian calendar, valid in - years 1583 to 4099 as well + 1. Original calculation in Julian calendar, valid in + dates after 326 AD + 2. Original method, with date converted to Gregorian + calendar, valid in years 1583 to 4099 + 3. Revised method, in Gregorian calendar, valid in + years 1583 to 4099 as well These methods are represented by the constants: @@ -41,11 +41,11 @@ def easter(year, method=EASTER_WESTERN): More about the algorithm may be found at: - http://users.chariot.net.au/~gmarts/eastalg.htm + `GM Arts: Easter Algorithms `_ and - http://www.tondering.dk/claus/calendar.html + `The Calendar FAQ: Easter `_ """ diff --git a/libs/dateutil/parser.py b/libs/dateutil/parser.py deleted file mode 100644 index 147b3f2ca..000000000 --- a/libs/dateutil/parser.py +++ /dev/null @@ -1,1360 +0,0 @@ -# -*- coding:iso-8859-1 -*- -""" -This module offers a generic date/time string parser which is able to parse -most known formats to represent a date and/or time. - -This module attempts to be forgiving with regards to unlikely input formats, -returning a datetime object even for dates which are ambiguous. If an element -of a date/time stamp is omitted, the following rules are applied: -- If AM or PM is left unspecified, a 24-hour clock is assumed, however, an hour - on a 12-hour clock (``0 <= hour <= 12``) *must* be specified if AM or PM is - specified. -- If a time zone is omitted, a timezone-naive datetime is returned. - -If any other elements are missing, they are taken from the -:class:`datetime.datetime` object passed to the parameter ``default``. If this -results in a day number exceeding the valid number of days per month, the -value falls back to the end of the month. - -Additional resources about date/time string formats can be found below: - -- `A summary of the international standard date and time notation - `_ -- `W3C Date and Time Formats `_ -- `Time Formats (Planetary Rings Node) `_ -- `CPAN ParseDate module - `_ -- `Java SimpleDateFormat Class - `_ -""" -from __future__ import unicode_literals - -import datetime -import string -import time -import collections -import re -from io import StringIO -from calendar import monthrange, isleap - -from six import text_type, binary_type, integer_types - -from . import relativedelta -from . import tz - -__all__ = ["parse", "parserinfo"] - - -class _timelex(object): - # Fractional seconds are sometimes split by a comma - _split_decimal = re.compile("([\.,])") - - def __init__(self, instream): - if isinstance(instream, binary_type): - instream = instream.decode() - - if isinstance(instream, text_type): - instream = StringIO(instream) - - if getattr(instream, 'read', None) is None: - raise TypeError('Parser must be a string or character stream, not ' - '{itype}'.format(itype=instream.__class__.__name__)) - - self.instream = instream - self.charstack = [] - self.tokenstack = [] - self.eof = False - - def get_token(self): - """ - This function breaks the time string into lexical units (tokens), which - can be parsed by the parser. Lexical units are demarcated by changes in - the character set, so any continuous string of letters is considered - one unit, any continuous string of numbers is considered one unit. - - The main complication arises from the fact that dots ('.') can be used - both as separators (e.g. "Sep.20.2009") or decimal points (e.g. - "4:30:21.447"). As such, it is necessary to read the full context of - any dot-separated strings before breaking it into tokens; as such, this - function maintains a "token stack", for when the ambiguous context - demands that multiple tokens be parsed at once. - """ - if self.tokenstack: - return self.tokenstack.pop(0) - - seenletters = False - token = None - state = None - - while not self.eof: - # We only realize that we've reached the end of a token when we - # find a character that's not part of the current token - since - # that character may be part of the next token, it's stored in the - # charstack. - if self.charstack: - nextchar = self.charstack.pop(0) - else: - nextchar = self.instream.read(1) - while nextchar == '\x00': - nextchar = self.instream.read(1) - - if not nextchar: - self.eof = True - break - elif not state: - # First character of the token - determines if we're starting - # to parse a word, a number or something else. - token = nextchar - if self.isword(nextchar): - state = 'a' - elif self.isnum(nextchar): - state = '0' - elif self.isspace(nextchar): - token = ' ' - break # emit token - else: - break # emit token - elif state == 'a': - # If we've already started reading a word, we keep reading - # letters until we find something that's not part of a word. - seenletters = True - if self.isword(nextchar): - token += nextchar - elif nextchar == '.': - token += nextchar - state = 'a.' - else: - self.charstack.append(nextchar) - break # emit token - elif state == '0': - # If we've already started reading a number, we keep reading - # numbers until we find something that doesn't fit. - if self.isnum(nextchar): - token += nextchar - elif nextchar == '.' or (nextchar == ',' and len(token) >= 2): - token += nextchar - state = '0.' - else: - self.charstack.append(nextchar) - break # emit token - elif state == 'a.': - # If we've seen some letters and a dot separator, continue - # parsing, and the tokens will be broken up later. - seenletters = True - if nextchar == '.' or self.isword(nextchar): - token += nextchar - elif self.isnum(nextchar) and token[-1] == '.': - token += nextchar - state = '0.' - else: - self.charstack.append(nextchar) - break # emit token - elif state == '0.': - # If we've seen at least one dot separator, keep going, we'll - # break up the tokens later. - if nextchar == '.' or self.isnum(nextchar): - token += nextchar - elif self.isword(nextchar) and token[-1] == '.': - token += nextchar - state = 'a.' - else: - self.charstack.append(nextchar) - break # emit token - - if (state in ('a.', '0.') and (seenletters or token.count('.') > 1 or - token[-1] in '.,')): - l = self._split_decimal.split(token) - token = l[0] - for tok in l[1:]: - if tok: - self.tokenstack.append(tok) - - if state == '0.' and token.count('.') == 0: - token = token.replace(',', '.') - - return token - - def __iter__(self): - return self - - def __next__(self): - token = self.get_token() - if token is None: - raise StopIteration - - return token - - def next(self): - return self.__next__() # Python 2.x support - - @classmethod - def split(cls, s): - return list(cls(s)) - - @classmethod - def isword(cls, nextchar): - """ Whether or not the next character is part of a word """ - return nextchar.isalpha() - - @classmethod - def isnum(cls, nextchar): - """ Whether the next character is part of a number """ - return nextchar.isdigit() - - @classmethod - def isspace(cls, nextchar): - """ Whether the next character is whitespace """ - return nextchar.isspace() - - -class _resultbase(object): - - def __init__(self): - for attr in self.__slots__: - setattr(self, attr, None) - - def _repr(self, classname): - l = [] - for attr in self.__slots__: - value = getattr(self, attr) - if value is not None: - l.append("%s=%s" % (attr, repr(value))) - return "%s(%s)" % (classname, ", ".join(l)) - - def __len__(self): - return (sum(getattr(self, attr) is not None - for attr in self.__slots__)) - - def __repr__(self): - return self._repr(self.__class__.__name__) - - -class parserinfo(object): - """ - Class which handles what inputs are accepted. Subclass this to customize - the language and acceptable values for each parameter. - - :param dayfirst: - Whether to interpret the first value in an ambiguous 3-integer date - (e.g. 01/05/09) as the day (``True``) or month (``False``). If - ``yearfirst`` is set to ``True``, this distinguishes between YDM - and YMD. Default is ``False``. - - :param yearfirst: - Whether to interpret the first value in an ambiguous 3-integer date - (e.g. 01/05/09) as the year. If ``True``, the first number is taken - to be the year, otherwise the last number is taken to be the year. - Default is ``False``. - """ - - # m from a.m/p.m, t from ISO T separator - JUMP = [" ", ".", ",", ";", "-", "/", "'", - "at", "on", "and", "ad", "m", "t", "of", - "st", "nd", "rd", "th"] - - WEEKDAYS = [("Mon", "Monday"), - ("Tue", "Tuesday"), - ("Wed", "Wednesday"), - ("Thu", "Thursday"), - ("Fri", "Friday"), - ("Sat", "Saturday"), - ("Sun", "Sunday")] - MONTHS = [("Jan", "January"), - ("Feb", "February"), - ("Mar", "March"), - ("Apr", "April"), - ("May", "May"), - ("Jun", "June"), - ("Jul", "July"), - ("Aug", "August"), - ("Sep", "Sept", "September"), - ("Oct", "October"), - ("Nov", "November"), - ("Dec", "December")] - HMS = [("h", "hour", "hours"), - ("m", "minute", "minutes"), - ("s", "second", "seconds")] - AMPM = [("am", "a"), - ("pm", "p")] - UTCZONE = ["UTC", "GMT", "Z"] - PERTAIN = ["of"] - TZOFFSET = {} - - def __init__(self, dayfirst=False, yearfirst=False): - self._jump = self._convert(self.JUMP) - self._weekdays = self._convert(self.WEEKDAYS) - self._months = self._convert(self.MONTHS) - self._hms = self._convert(self.HMS) - self._ampm = self._convert(self.AMPM) - self._utczone = self._convert(self.UTCZONE) - self._pertain = self._convert(self.PERTAIN) - - self.dayfirst = dayfirst - self.yearfirst = yearfirst - - self._year = time.localtime().tm_year - self._century = self._year // 100 * 100 - - def _convert(self, lst): - dct = {} - for i, v in enumerate(lst): - if isinstance(v, tuple): - for v in v: - dct[v.lower()] = i - else: - dct[v.lower()] = i - return dct - - def jump(self, name): - return name.lower() in self._jump - - def weekday(self, name): - if len(name) >= 3: - try: - return self._weekdays[name.lower()] - except KeyError: - pass - return None - - def month(self, name): - if len(name) >= 3: - try: - return self._months[name.lower()] + 1 - except KeyError: - pass - return None - - def hms(self, name): - try: - return self._hms[name.lower()] - except KeyError: - return None - - def ampm(self, name): - try: - return self._ampm[name.lower()] - except KeyError: - return None - - def pertain(self, name): - return name.lower() in self._pertain - - def utczone(self, name): - return name.lower() in self._utczone - - def tzoffset(self, name): - if name in self._utczone: - return 0 - - return self.TZOFFSET.get(name) - - def convertyear(self, year, century_specified=False): - if year < 100 and not century_specified: - year += self._century - if abs(year - self._year) >= 50: - if year < self._year: - year += 100 - else: - year -= 100 - return year - - def validate(self, res): - # move to info - if res.year is not None: - res.year = self.convertyear(res.year, res.century_specified) - - if res.tzoffset == 0 and not res.tzname or res.tzname == 'Z': - res.tzname = "UTC" - res.tzoffset = 0 - elif res.tzoffset != 0 and res.tzname and self.utczone(res.tzname): - res.tzoffset = 0 - return True - - -class _ymd(list): - def __init__(self, tzstr, *args, **kwargs): - super(self.__class__, self).__init__(*args, **kwargs) - self.century_specified = False - self.tzstr = tzstr - - @staticmethod - def token_could_be_year(token, year): - try: - return int(token) == year - except ValueError: - return False - - @staticmethod - def find_potential_year_tokens(year, tokens): - return [token for token in tokens if _ymd.token_could_be_year(token, year)] - - def find_probable_year_index(self, tokens): - """ - attempt to deduce if a pre 100 year was lost - due to padded zeros being taken off - """ - for index, token in enumerate(self): - potential_year_tokens = _ymd.find_potential_year_tokens(token, tokens) - if len(potential_year_tokens) == 1 and len(potential_year_tokens[0]) > 2: - return index - - def append(self, val): - if hasattr(val, '__len__'): - if val.isdigit() and len(val) > 2: - self.century_specified = True - elif val > 100: - self.century_specified = True - - super(self.__class__, self).append(int(val)) - - def resolve_ymd(self, mstridx, yearfirst, dayfirst): - len_ymd = len(self) - year, month, day = (None, None, None) - - if len_ymd > 3: - raise ValueError("More than three YMD values") - elif len_ymd == 1 or (mstridx != -1 and len_ymd == 2): - # One member, or two members with a month string - if mstridx != -1: - month = self[mstridx] - del self[mstridx] - - if len_ymd > 1 or mstridx == -1: - if self[0] > 31: - year = self[0] - else: - day = self[0] - - elif len_ymd == 2: - # Two members with numbers - if self[0] > 31: - # 99-01 - year, month = self - elif self[1] > 31: - # 01-99 - month, year = self - elif dayfirst and self[1] <= 12: - # 13-01 - day, month = self - else: - # 01-13 - month, day = self - - elif len_ymd == 3: - # Three members - if mstridx == 0: - month, day, year = self - elif mstridx == 1: - if self[0] > 31 or (yearfirst and self[2] <= 31): - # 99-Jan-01 - year, month, day = self - else: - # 01-Jan-01 - # Give precendence to day-first, since - # two-digit years is usually hand-written. - day, month, year = self - - elif mstridx == 2: - # WTF!? - if self[1] > 31: - # 01-99-Jan - day, year, month = self - else: - # 99-01-Jan - year, day, month = self - - else: - if self[0] > 31 or \ - self.find_probable_year_index(_timelex.split(self.tzstr)) == 0 or \ - (yearfirst and self[1] <= 12 and self[2] <= 31): - # 99-01-01 - if dayfirst and self[2] <= 12: - year, day, month = self - else: - year, month, day = self - elif self[0] > 12 or (dayfirst and self[1] <= 12): - # 13-01-01 - day, month, year = self - else: - # 01-13-01 - month, day, year = self - - return year, month, day - - -class parser(object): - def __init__(self, info=None): - self.info = info or parserinfo() - - def parse(self, timestr, default=None, ignoretz=False, tzinfos=None, **kwargs): - """ - Parse the date/time string into a :class:`datetime.datetime` object. - - :param timestr: - Any date/time string using the supported formats. - - :param default: - The default datetime object, if this is a datetime object and not - ``None``, elements specified in ``timestr`` replace elements in the - default object. - - :param ignoretz: - If set ``True``, time zones in parsed strings are ignored and a - naive :class:`datetime.datetime` object is returned. - - :param tzinfos: - Additional time zone names / aliases which may be present in the - string. This argument maps time zone names (and optionally offsets - from those time zones) to time zones. This parameter can be a - dictionary with timezone aliases mapping time zone names to time - zones or a function taking two parameters (``tzname`` and - ``tzoffset``) and returning a time zone. - - The timezones to which the names are mapped can be an integer - offset from UTC in minutes or a :class:`tzinfo` object. - - .. doctest:: - :options: +NORMALIZE_WHITESPACE - - >>> from dateutil.parser import parse - >>> from dateutil.tz import gettz - >>> tzinfos = {"BRST": -10800, "CST": gettz("America/Chicago")} - >>> parse("2012-01-19 17:21:00 BRST", tzinfos=tzinfos) - datetime.datetime(2012, 1, 19, 17, 21, tzinfo=tzoffset(u'BRST', -10800)) - >>> parse("2012-01-19 17:21:00 CST", tzinfos=tzinfos) - datetime.datetime(2012, 1, 19, 17, 21, - tzinfo=tzfile('/usr/share/zoneinfo/America/Chicago')) - - This parameter is ignored if ``ignoretz`` is set. - - :param **kwargs: - Keyword arguments as passed to ``_parse()``. - - :return: - Returns a :class:`datetime.datetime` object or, if the - ``fuzzy_with_tokens`` option is ``True``, returns a tuple, the - first element being a :class:`datetime.datetime` object, the second - a tuple containing the fuzzy tokens. - - :raises ValueError: - Raised for invalid or unknown string format, if the provided - :class:`tzinfo` is not in a valid format, or if an invalid date - would be created. - - :raises OverflowError: - Raised if the parsed date exceeds the largest valid C integer on - your system. - """ - - if default is None: - effective_dt = datetime.datetime.now() - default = datetime.datetime.now().replace(hour=0, minute=0, - second=0, microsecond=0) - else: - effective_dt = default - - res, skipped_tokens = self._parse(timestr, **kwargs) - - if res is None: - raise ValueError("Unknown string format") - - if len(res) == 0: - raise ValueError("String does not contain a date.") - - repl = {} - for attr in ("year", "month", "day", "hour", - "minute", "second", "microsecond"): - value = getattr(res, attr) - if value is not None: - repl[attr] = value - - if 'day' not in repl: - # If the default day exceeds the last day of the month, fall back to - # the end of the month. - cyear = default.year if res.year is None else res.year - cmonth = default.month if res.month is None else res.month - cday = default.day if res.day is None else res.day - - if cday > monthrange(cyear, cmonth)[1]: - repl['day'] = monthrange(cyear, cmonth)[1] - - ret = default.replace(**repl) - - if res.weekday is not None and not res.day: - ret = ret+relativedelta.relativedelta(weekday=res.weekday) - - if not ignoretz: - if (isinstance(tzinfos, collections.Callable) or - tzinfos and res.tzname in tzinfos): - - if isinstance(tzinfos, collections.Callable): - tzdata = tzinfos(res.tzname, res.tzoffset) - else: - tzdata = tzinfos.get(res.tzname) - - if isinstance(tzdata, datetime.tzinfo): - tzinfo = tzdata - elif isinstance(tzdata, text_type): - tzinfo = tz.tzstr(tzdata) - elif isinstance(tzdata, integer_types): - tzinfo = tz.tzoffset(res.tzname, tzdata) - else: - raise ValueError("Offset must be tzinfo subclass, " - "tz string, or int offset.") - ret = ret.replace(tzinfo=tzinfo) - elif res.tzname and res.tzname in time.tzname: - ret = ret.replace(tzinfo=tz.tzlocal()) - elif res.tzoffset == 0: - ret = ret.replace(tzinfo=tz.tzutc()) - elif res.tzoffset: - ret = ret.replace(tzinfo=tz.tzoffset(res.tzname, res.tzoffset)) - - if kwargs.get('fuzzy_with_tokens', False): - return ret, skipped_tokens - else: - return ret - - class _result(_resultbase): - __slots__ = ["year", "month", "day", "weekday", - "hour", "minute", "second", "microsecond", - "tzname", "tzoffset", "ampm"] - - def _parse(self, timestr, dayfirst=None, yearfirst=None, fuzzy=False, - fuzzy_with_tokens=False): - """ - Private method which performs the heavy lifting of parsing, called from - ``parse()``, which passes on its ``kwargs`` to this function. - - :param timestr: - The string to parse. - - :param dayfirst: - Whether to interpret the first value in an ambiguous 3-integer date - (e.g. 01/05/09) as the day (``True``) or month (``False``). If - ``yearfirst`` is set to ``True``, this distinguishes between YDM - and YMD. If set to ``None``, this value is retrieved from the - current :class:`parserinfo` object (which itself defaults to - ``False``). - - :param yearfirst: - Whether to interpret the first value in an ambiguous 3-integer date - (e.g. 01/05/09) as the year. If ``True``, the first number is taken - to be the year, otherwise the last number is taken to be the year. - If this is set to ``None``, the value is retrieved from the current - :class:`parserinfo` object (which itself defaults to ``False``). - - :param fuzzy: - Whether to allow fuzzy parsing, allowing for string like "Today is - January 1, 2047 at 8:21:00AM". - - :param fuzzy_with_tokens: - If ``True``, ``fuzzy`` is automatically set to True, and the parser - will return a tuple where the first element is the parsed - :class:`datetime.datetime` datetimestamp and the second element is - a tuple containing the portions of the string which were ignored: - - .. doctest:: - - >>> from dateutil.parser import parse - >>> parse("Today is January 1, 2047 at 8:21:00AM", fuzzy_with_tokens=True) - (datetime.datetime(2047, 1, 1, 8, 21), (u'Today is ', u' ', u'at ')) - - """ - if fuzzy_with_tokens: - fuzzy = True - - info = self.info - - if dayfirst is None: - dayfirst = info.dayfirst - - if yearfirst is None: - yearfirst = info.yearfirst - - res = self._result() - l = _timelex.split(timestr) # Splits the timestr into tokens - - # keep up with the last token skipped so we can recombine - # consecutively skipped tokens (-2 for when i begins at 0). - last_skipped_token_i = -2 - skipped_tokens = list() - - try: - # year/month/day list - ymd = _ymd(timestr) - - # Index of the month string in ymd - mstridx = -1 - - len_l = len(l) - i = 0 - while i < len_l: - - # Check if it's a number - try: - value_repr = l[i] - value = float(value_repr) - except ValueError: - value = None - - if value is not None: - # Token is a number - len_li = len(l[i]) - i += 1 - - if (len(ymd) == 3 and len_li in (2, 4) - and res.hour is None and (i >= len_l or (l[i] != ':' and - info.hms(l[i]) is None))): - # 19990101T23[59] - s = l[i-1] - res.hour = int(s[:2]) - - if len_li == 4: - res.minute = int(s[2:]) - - elif len_li == 6 or (len_li > 6 and l[i-1].find('.') == 6): - # YYMMDD or HHMMSS[.ss] - s = l[i-1] - - if not ymd and l[i-1].find('.') == -1: - #ymd.append(info.convertyear(int(s[:2]))) - - ymd.append(s[:2]) - ymd.append(s[2:4]) - ymd.append(s[4:]) - else: - # 19990101T235959[.59] - res.hour = int(s[:2]) - res.minute = int(s[2:4]) - res.second, res.microsecond = _parsems(s[4:]) - - elif len_li in (8, 12, 14): - # YYYYMMDD - s = l[i-1] - ymd.append(s[:4]) - ymd.append(s[4:6]) - ymd.append(s[6:8]) - - if len_li > 8: - res.hour = int(s[8:10]) - res.minute = int(s[10:12]) - - if len_li > 12: - res.second = int(s[12:]) - - elif ((i < len_l and info.hms(l[i]) is not None) or - (i+1 < len_l and l[i] == ' ' and - info.hms(l[i+1]) is not None)): - - # HH[ ]h or MM[ ]m or SS[.ss][ ]s - if l[i] == ' ': - i += 1 - - idx = info.hms(l[i]) - - while True: - if idx == 0: - res.hour = int(value) - - if value % 1: - res.minute = int(60*(value % 1)) - - elif idx == 1: - res.minute = int(value) - - if value % 1: - res.second = int(60*(value % 1)) - - elif idx == 2: - res.second, res.microsecond = \ - _parsems(value_repr) - - i += 1 - - if i >= len_l or idx == 2: - break - - # 12h00 - try: - value_repr = l[i] - value = float(value_repr) - except ValueError: - break - else: - i += 1 - idx += 1 - - if i < len_l: - newidx = info.hms(l[i]) - - if newidx is not None: - idx = newidx - - elif (i == len_l and l[i-2] == ' ' and - info.hms(l[i-3]) is not None): - # X h MM or X m SS - idx = info.hms(l[i-3]) + 1 - - if idx == 1: - res.minute = int(value) - - if value % 1: - res.second = int(60*(value % 1)) - elif idx == 2: - res.second, res.microsecond = \ - _parsems(value_repr) - i += 1 - - elif i+1 < len_l and l[i] == ':': - # HH:MM[:SS[.ss]] - res.hour = int(value) - i += 1 - value = float(l[i]) - res.minute = int(value) - - if value % 1: - res.second = int(60*(value % 1)) - - i += 1 - - if i < len_l and l[i] == ':': - res.second, res.microsecond = _parsems(l[i+1]) - i += 2 - - elif i < len_l and l[i] in ('-', '/', '.'): - sep = l[i] - ymd.append(value_repr) - i += 1 - - if i < len_l and not info.jump(l[i]): - try: - # 01-01[-01] - ymd.append(l[i]) - except ValueError: - # 01-Jan[-01] - value = info.month(l[i]) - - if value is not None: - ymd.append(value) - assert mstridx == -1 - mstridx = len(ymd)-1 - else: - return None, None - - i += 1 - - if i < len_l and l[i] == sep: - # We have three members - i += 1 - value = info.month(l[i]) - - if value is not None: - ymd.append(value) - mstridx = len(ymd)-1 - assert mstridx == -1 - else: - ymd.append(l[i]) - - i += 1 - elif i >= len_l or info.jump(l[i]): - if i+1 < len_l and info.ampm(l[i+1]) is not None: - # 12 am - res.hour = int(value) - - if res.hour < 12 and info.ampm(l[i+1]) == 1: - res.hour += 12 - elif res.hour == 12 and info.ampm(l[i+1]) == 0: - res.hour = 0 - - i += 1 - else: - # Year, month or day - ymd.append(value) - i += 1 - elif info.ampm(l[i]) is not None: - - # 12am - res.hour = int(value) - - if res.hour < 12 and info.ampm(l[i]) == 1: - res.hour += 12 - elif res.hour == 12 and info.ampm(l[i]) == 0: - res.hour = 0 - i += 1 - - elif not fuzzy: - return None, None - else: - i += 1 - continue - - # Check weekday - value = info.weekday(l[i]) - if value is not None: - res.weekday = value - i += 1 - continue - - # Check month name - value = info.month(l[i]) - if value is not None: - ymd.append(value) - assert mstridx == -1 - mstridx = len(ymd)-1 - - i += 1 - if i < len_l: - if l[i] in ('-', '/'): - # Jan-01[-99] - sep = l[i] - i += 1 - ymd.append(l[i]) - i += 1 - - if i < len_l and l[i] == sep: - # Jan-01-99 - i += 1 - ymd.append(l[i]) - i += 1 - - elif (i+3 < len_l and l[i] == l[i+2] == ' ' - and info.pertain(l[i+1])): - # Jan of 01 - # In this case, 01 is clearly year - try: - value = int(l[i+3]) - except ValueError: - # Wrong guess - pass - else: - # Convert it here to become unambiguous - ymd.append(str(info.convertyear(value))) - i += 4 - continue - - # Check am/pm - value = info.ampm(l[i]) - if value is not None: - # For fuzzy parsing, 'a' or 'am' (both valid English words) - # may erroneously trigger the AM/PM flag. Deal with that - # here. - val_is_ampm = True - - # If there's already an AM/PM flag, this one isn't one. - if fuzzy and res.ampm is not None: - val_is_ampm = False - - # If AM/PM is found and hour is not, raise a ValueError - if res.hour is None: - if fuzzy: - val_is_ampm = False - else: - raise ValueError('No hour specified with ' + - 'AM or PM flag.') - elif not 0 <= res.hour <= 12: - # If AM/PM is found, it's a 12 hour clock, so raise - # an error for invalid range - if fuzzy: - val_is_ampm = False - else: - raise ValueError('Invalid hour specified for ' + - '12-hour clock.') - - if val_is_ampm: - if value == 1 and res.hour < 12: - res.hour += 12 - elif value == 0 and res.hour == 12: - res.hour = 0 - - res.ampm = value - - i += 1 - continue - - # Check for a timezone name - if (res.hour is not None and len(l[i]) <= 5 and - res.tzname is None and res.tzoffset is None and - not [x for x in l[i] if x not in - string.ascii_uppercase]): - res.tzname = l[i] - res.tzoffset = info.tzoffset(res.tzname) - i += 1 - - # Check for something like GMT+3, or BRST+3. Notice - # that it doesn't mean "I am 3 hours after GMT", but - # "my time +3 is GMT". If found, we reverse the - # logic so that timezone parsing code will get it - # right. - if i < len_l and l[i] in ('+', '-'): - l[i] = ('+', '-')[l[i] == '+'] - res.tzoffset = None - if info.utczone(res.tzname): - # With something like GMT+3, the timezone - # is *not* GMT. - res.tzname = None - - continue - - # Check for a numbered timezone - if res.hour is not None and l[i] in ('+', '-'): - signal = (-1, 1)[l[i] == '+'] - i += 1 - len_li = len(l[i]) - - if len_li == 4: - # -0300 - res.tzoffset = int(l[i][:2])*3600+int(l[i][2:])*60 - elif i+1 < len_l and l[i+1] == ':': - # -03:00 - res.tzoffset = int(l[i])*3600+int(l[i+2])*60 - i += 2 - elif len_li <= 2: - # -[0]3 - res.tzoffset = int(l[i][:2])*3600 - else: - return None, None - i += 1 - - res.tzoffset *= signal - - # Look for a timezone name between parenthesis - if (i+3 < len_l and - info.jump(l[i]) and l[i+1] == '(' and l[i+3] == ')' and - 3 <= len(l[i+2]) <= 5 and - not [x for x in l[i+2] - if x not in string.ascii_uppercase]): - # -0300 (BRST) - res.tzname = l[i+2] - i += 4 - continue - - # Check jumps - if not (info.jump(l[i]) or fuzzy): - return None, None - - if last_skipped_token_i == i - 1: - # recombine the tokens - skipped_tokens[-1] += l[i] - else: - # just append - skipped_tokens.append(l[i]) - last_skipped_token_i = i - i += 1 - - # Process year/month/day - year, month, day = ymd.resolve_ymd(mstridx, yearfirst, dayfirst) - if year is not None: - res.year = year - res.century_specified = ymd.century_specified - - if month is not None: - res.month = month - - if day is not None: - res.day = day - - except (IndexError, ValueError, AssertionError): - return None, None - - if not info.validate(res): - return None, None - - if fuzzy_with_tokens: - return res, tuple(skipped_tokens) - else: - return res, None - -DEFAULTPARSER = parser() - - -def parse(timestr, parserinfo=None, **kwargs): - """ - - Parse a string in one of the supported formats, using the - ``parserinfo`` parameters. - - :param timestr: - A string containing a date/time stamp. - - :param parserinfo: - A :class:`parserinfo` object containing parameters for the parser. - If ``None``, the default arguments to the :class:`parserinfo` - constructor are used. - - The ``**kwargs`` parameter takes the following keyword arguments: - - :param default: - The default datetime object, if this is a datetime object and not - ``None``, elements specified in ``timestr`` replace elements in the - default object. - - :param ignoretz: - If set ``True``, time zones in parsed strings are ignored and a naive - :class:`datetime` object is returned. - - :param tzinfos: - Additional time zone names / aliases which may be present in the - string. This argument maps time zone names (and optionally offsets - from those time zones) to time zones. This parameter can be a - dictionary with timezone aliases mapping time zone names to time - zones or a function taking two parameters (``tzname`` and - ``tzoffset``) and returning a time zone. - - The timezones to which the names are mapped can be an integer - offset from UTC in minutes or a :class:`tzinfo` object. - - .. doctest:: - :options: +NORMALIZE_WHITESPACE - - >>> from dateutil.parser import parse - >>> from dateutil.tz import gettz - >>> tzinfos = {"BRST": -10800, "CST": gettz("America/Chicago")} - >>> parse("2012-01-19 17:21:00 BRST", tzinfos=tzinfos) - datetime.datetime(2012, 1, 19, 17, 21, tzinfo=tzoffset(u'BRST', -10800)) - >>> parse("2012-01-19 17:21:00 CST", tzinfos=tzinfos) - datetime.datetime(2012, 1, 19, 17, 21, - tzinfo=tzfile('/usr/share/zoneinfo/America/Chicago')) - - This parameter is ignored if ``ignoretz`` is set. - - :param dayfirst: - Whether to interpret the first value in an ambiguous 3-integer date - (e.g. 01/05/09) as the day (``True``) or month (``False``). If - ``yearfirst`` is set to ``True``, this distinguishes between YDM and - YMD. If set to ``None``, this value is retrieved from the current - :class:`parserinfo` object (which itself defaults to ``False``). - - :param yearfirst: - Whether to interpret the first value in an ambiguous 3-integer date - (e.g. 01/05/09) as the year. If ``True``, the first number is taken to - be the year, otherwise the last number is taken to be the year. If - this is set to ``None``, the value is retrieved from the current - :class:`parserinfo` object (which itself defaults to ``False``). - - :param fuzzy: - Whether to allow fuzzy parsing, allowing for string like "Today is - January 1, 2047 at 8:21:00AM". - - :param fuzzy_with_tokens: - If ``True``, ``fuzzy`` is automatically set to True, and the parser - will return a tuple where the first element is the parsed - :class:`datetime.datetime` datetimestamp and the second element is - a tuple containing the portions of the string which were ignored: - - .. doctest:: - - >>> from dateutil.parser import parse - >>> parse("Today is January 1, 2047 at 8:21:00AM", fuzzy_with_tokens=True) - (datetime.datetime(2011, 1, 1, 8, 21), (u'Today is ', u' ', u'at ')) - - :return: - Returns a :class:`datetime.datetime` object or, if the - ``fuzzy_with_tokens`` option is ``True``, returns a tuple, the - first element being a :class:`datetime.datetime` object, the second - a tuple containing the fuzzy tokens. - - :raises ValueError: - Raised for invalid or unknown string format, if the provided - :class:`tzinfo` is not in a valid format, or if an invalid date - would be created. - - :raises OverflowError: - Raised if the parsed date exceeds the largest valid C integer on - your system. - """ - if parserinfo: - return parser(parserinfo).parse(timestr, **kwargs) - else: - return DEFAULTPARSER.parse(timestr, **kwargs) - - -class _tzparser(object): - - class _result(_resultbase): - - __slots__ = ["stdabbr", "stdoffset", "dstabbr", "dstoffset", - "start", "end"] - - class _attr(_resultbase): - __slots__ = ["month", "week", "weekday", - "yday", "jyday", "day", "time"] - - def __repr__(self): - return self._repr("") - - def __init__(self): - _resultbase.__init__(self) - self.start = self._attr() - self.end = self._attr() - - def parse(self, tzstr): - res = self._result() - l = _timelex.split(tzstr) - try: - - len_l = len(l) - - i = 0 - while i < len_l: - # BRST+3[BRDT[+2]] - j = i - while j < len_l and not [x for x in l[j] - if x in "0123456789:,-+"]: - j += 1 - if j != i: - if not res.stdabbr: - offattr = "stdoffset" - res.stdabbr = "".join(l[i:j]) - else: - offattr = "dstoffset" - res.dstabbr = "".join(l[i:j]) - i = j - if (i < len_l and (l[i] in ('+', '-') or l[i][0] in - "0123456789")): - if l[i] in ('+', '-'): - # Yes, that's right. See the TZ variable - # documentation. - signal = (1, -1)[l[i] == '+'] - i += 1 - else: - signal = -1 - len_li = len(l[i]) - if len_li == 4: - # -0300 - setattr(res, offattr, (int(l[i][:2])*3600 + - int(l[i][2:])*60)*signal) - elif i+1 < len_l and l[i+1] == ':': - # -03:00 - setattr(res, offattr, - (int(l[i])*3600+int(l[i+2])*60)*signal) - i += 2 - elif len_li <= 2: - # -[0]3 - setattr(res, offattr, - int(l[i][:2])*3600*signal) - else: - return None - i += 1 - if res.dstabbr: - break - else: - break - - if i < len_l: - for j in range(i, len_l): - if l[j] == ';': - l[j] = ',' - - assert l[i] == ',' - - i += 1 - - if i >= len_l: - pass - elif (8 <= l.count(',') <= 9 and - not [y for x in l[i:] if x != ',' - for y in x if y not in "0123456789"]): - # GMT0BST,3,0,30,3600,10,0,26,7200[,3600] - for x in (res.start, res.end): - x.month = int(l[i]) - i += 2 - if l[i] == '-': - value = int(l[i+1])*-1 - i += 1 - else: - value = int(l[i]) - i += 2 - if value: - x.week = value - x.weekday = (int(l[i])-1) % 7 - else: - x.day = int(l[i]) - i += 2 - x.time = int(l[i]) - i += 2 - if i < len_l: - if l[i] in ('-', '+'): - signal = (-1, 1)[l[i] == "+"] - i += 1 - else: - signal = 1 - res.dstoffset = (res.stdoffset+int(l[i]))*signal - elif (l.count(',') == 2 and l[i:].count('/') <= 2 and - not [y for x in l[i:] if x not in (',', '/', 'J', 'M', - '.', '-', ':') - for y in x if y not in "0123456789"]): - for x in (res.start, res.end): - if l[i] == 'J': - # non-leap year day (1 based) - i += 1 - x.jyday = int(l[i]) - elif l[i] == 'M': - # month[-.]week[-.]weekday - i += 1 - x.month = int(l[i]) - i += 1 - assert l[i] in ('-', '.') - i += 1 - x.week = int(l[i]) - if x.week == 5: - x.week = -1 - i += 1 - assert l[i] in ('-', '.') - i += 1 - x.weekday = (int(l[i])-1) % 7 - else: - # year day (zero based) - x.yday = int(l[i])+1 - - i += 1 - - if i < len_l and l[i] == '/': - i += 1 - # start time - len_li = len(l[i]) - if len_li == 4: - # -0300 - x.time = (int(l[i][:2])*3600+int(l[i][2:])*60) - elif i+1 < len_l and l[i+1] == ':': - # -03:00 - x.time = int(l[i])*3600+int(l[i+2])*60 - i += 2 - if i+1 < len_l and l[i+1] == ':': - i += 2 - x.time += int(l[i]) - elif len_li <= 2: - # -[0]3 - x.time = (int(l[i][:2])*3600) - else: - return None - i += 1 - - assert i == len_l or l[i] == ',' - - i += 1 - - assert i >= len_l - - except (IndexError, ValueError, AssertionError): - return None - - return res - - -DEFAULTTZPARSER = _tzparser() - - -def _parsetz(tzstr): - return DEFAULTTZPARSER.parse(tzstr) - - -def _parsems(value): - """Parse a I[.F] seconds value into (seconds, microseconds).""" - if "." not in value: - return int(value), 0 - else: - i, f = value.split(".") - return int(i), int(f.ljust(6, "0")[:6]) - - -# vim:ts=4:sw=4:et diff --git a/libs/dateutil/parser/__init__.py b/libs/dateutil/parser/__init__.py index 216762c09..d174b0e4d 100644 --- a/libs/dateutil/parser/__init__.py +++ b/libs/dateutil/parser/__init__.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -from ._parser import parse, parser, parserinfo +from ._parser import parse, parser, parserinfo, ParserError from ._parser import DEFAULTPARSER, DEFAULTTZPARSER from ._parser import UnknownTimezoneWarning @@ -9,6 +9,7 @@ from .isoparser import isoparser, isoparse __all__ = ['parse', 'parser', 'parserinfo', 'isoparse', 'isoparser', + 'ParserError', 'UnknownTimezoneWarning'] diff --git a/libs/dateutil/parser/_parser.py b/libs/dateutil/parser/_parser.py index 9d2bb795c..37d1663b2 100644 --- a/libs/dateutil/parser/_parser.py +++ b/libs/dateutil/parser/_parser.py @@ -20,11 +20,11 @@ value falls back to the end of the month. Additional resources about date/time string formats can be found below: - `A summary of the international standard date and time notation - `_ -- `W3C Date and Time Formats `_ + `_ +- `W3C Date and Time Formats `_ - `Time Formats (Planetary Rings Node) `_ - `CPAN ParseDate module - `_ + `_ - `Java SimpleDateFormat Class `_ """ @@ -40,7 +40,7 @@ from calendar import monthrange from io import StringIO import six -from six import binary_type, integer_types, text_type +from six import integer_types, text_type from decimal import Decimal @@ -49,7 +49,7 @@ from warnings import warn from .. import relativedelta from .. import tz -__all__ = ["parse", "parserinfo"] +__all__ = ["parse", "parserinfo", "ParserError"] # TODO: pandas.core.tools.datetimes imports this explicitly. Might be worth @@ -60,14 +60,8 @@ class _timelex(object): _split_decimal = re.compile("([.,])") def __init__(self, instream): - if six.PY2: - # In Python 2, we can't duck type properly because unicode has - # a 'decode' function, and we'd be double-decoding - if isinstance(instream, (binary_type, bytearray)): - instream = instream.decode() - else: - if getattr(instream, 'decode', None) is not None: - instream = instream.decode() + if isinstance(instream, (bytes, bytearray)): + instream = instream.decode() if isinstance(instream, text_type): instream = StringIO(instream) @@ -291,7 +285,7 @@ class parserinfo(object): ("s", "second", "seconds")] AMPM = [("am", "a"), ("pm", "p")] - UTCZONE = ["UTC", "GMT", "Z"] + UTCZONE = ["UTC", "GMT", "Z", "z"] PERTAIN = ["of"] TZOFFSET = {} # TODO: ERA = ["AD", "BC", "CE", "BCE", "Stardate", @@ -388,7 +382,8 @@ class parserinfo(object): if res.year is not None: res.year = self.convertyear(res.year, res.century_specified) - if res.tzoffset == 0 and not res.tzname or res.tzname == 'Z': + if ((res.tzoffset == 0 and not res.tzname) or + (res.tzname == 'Z' or res.tzname == 'z')): res.tzname = "UTC" res.tzoffset = 0 elif res.tzoffset != 0 and res.tzname and self.utczone(res.tzname): @@ -422,7 +417,7 @@ class _ymd(list): elif not self.has_month: return 1 <= value <= 31 elif not self.has_year: - # Be permissive, assume leapyear + # Be permissive, assume leap year month = self[self.mstridx] return 1 <= value <= monthrange(2000, month)[1] else: @@ -538,7 +533,7 @@ class _ymd(list): year, month, day = self else: # 01-Jan-01 - # Give precendence to day-first, since + # Give precedence to day-first, since # two-digit years is usually hand-written. day, month, year = self @@ -625,7 +620,7 @@ class parser(object): first element being a :class:`datetime.datetime` object, the second a tuple containing the fuzzy tokens. - :raises ValueError: + :raises ParserError: Raised for invalid or unknown string format, if the provided :class:`tzinfo` is not in a valid format, or if an invalid date would be created. @@ -645,12 +640,15 @@ class parser(object): res, skipped_tokens = self._parse(timestr, **kwargs) if res is None: - raise ValueError("Unknown string format:", timestr) + raise ParserError("Unknown string format: %s", timestr) if len(res) == 0: - raise ValueError("String does not contain a date:", timestr) + raise ParserError("String does not contain a date: %s", timestr) - ret = self._build_naive(res, default) + try: + ret = self._build_naive(res, default) + except ValueError as e: + six.raise_from(ParserError(str(e) + ": %s", timestr), e) if not ignoretz: ret = self._build_tzaware(ret, res, tzinfos) @@ -1021,7 +1019,7 @@ class parser(object): hms_idx = idx + 2 elif idx > 0 and info.hms(tokens[idx-1]) is not None: - # There is a "h", "m", or "s" preceeding this token. Since neither + # There is a "h", "m", or "s" preceding this token. Since neither # of the previous cases was hit, there is no label following this # token, so we use the previous label. # e.g. the "04" in "12h04" @@ -1060,7 +1058,8 @@ class parser(object): tzname is None and tzoffset is None and len(token) <= 5 and - all(x in string.ascii_uppercase for x in token)) + (all(x in string.ascii_uppercase for x in token) + or token in self.info.UTCZONE)) def _ampm_valid(self, hour, ampm, fuzzy): """ @@ -1100,7 +1099,7 @@ class parser(object): def _parse_min_sec(self, value): # TODO: Every usage of this function sets res.second to the return # value. Are there any cases where second will be returned as None and - # we *dont* want to set res.second = None? + # we *don't* want to set res.second = None? minute = int(value) second = None @@ -1109,14 +1108,6 @@ class parser(object): second = int(60 * sec_remainder) return (minute, second) - def _parsems(self, value): - """Parse a I[.F] seconds value into (seconds, microseconds).""" - if "." not in value: - return int(value), 0 - else: - i, f = value.split(".") - return int(i), int(f.ljust(6, "0")[:6]) - def _parse_hms(self, idx, tokens, info, hms_idx): # TODO: Is this going to admit a lot of false-positives for when we # just happen to have digits and "h", "m" or "s" characters in non-date @@ -1135,21 +1126,35 @@ class parser(object): return (new_idx, hms) - def _recombine_skipped(self, tokens, skipped_idxs): - """ - >>> tokens = ["foo", " ", "bar", " ", "19June2000", "baz"] - >>> skipped_idxs = [0, 1, 2, 5] - >>> _recombine_skipped(tokens, skipped_idxs) - ["foo bar", "baz"] - """ - skipped_tokens = [] - for i, idx in enumerate(sorted(skipped_idxs)): - if i > 0 and idx - 1 == skipped_idxs[i - 1]: - skipped_tokens[-1] = skipped_tokens[-1] + tokens[idx] - else: - skipped_tokens.append(tokens[idx]) + # ------------------------------------------------------------------ + # Handling for individual tokens. These are kept as methods instead + # of functions for the sake of customizability via subclassing. - return skipped_tokens + def _parsems(self, value): + """Parse a I[.F] seconds value into (seconds, microseconds).""" + if "." not in value: + return int(value), 0 + else: + i, f = value.split(".") + return int(i), int(f.ljust(6, "0")[:6]) + + def _to_decimal(self, val): + try: + decimal_value = Decimal(val) + # See GH 662, edge case, infinite value should not be converted + # via `_to_decimal` + if not decimal_value.is_finite(): + raise ValueError("Converted decimal value is infinite or NaN") + except Exception as e: + msg = "Could not convert %s to decimal" % val + six.raise_from(ValueError(msg), e) + else: + return decimal_value + + # ------------------------------------------------------------------ + # Post-Parsing construction of datetime output. These are kept as + # methods instead of functions for the sake of customizability via + # subclassing. def _build_tzinfo(self, tzinfos, tzname, tzoffset): if callable(tzinfos): @@ -1164,6 +1169,9 @@ class parser(object): tzinfo = tz.tzstr(tzdata) elif isinstance(tzdata, integer_types): tzinfo = tz.tzoffset(tzname, tzdata) + else: + raise TypeError("Offset must be tzinfo subclass, tz string, " + "or int offset.") return tzinfo def _build_tzaware(self, naive, res, tzinfos): @@ -1181,10 +1189,10 @@ class parser(object): # This is mostly relevant for winter GMT zones parsed in the UK if (aware.tzname() != res.tzname and res.tzname in self.info.UTCZONE): - aware = aware.replace(tzinfo=tz.tzutc()) + aware = aware.replace(tzinfo=tz.UTC) elif res.tzoffset == 0: - aware = naive.replace(tzinfo=tz.tzutc()) + aware = naive.replace(tzinfo=tz.UTC) elif res.tzoffset: aware = naive.replace(tzinfo=tz.tzoffset(res.tzname, res.tzoffset)) @@ -1239,17 +1247,21 @@ class parser(object): return dt - def _to_decimal(self, val): - try: - decimal_value = Decimal(val) - # See GH 662, edge case, infinite value should not be converted via `_to_decimal` - if not decimal_value.is_finite(): - raise ValueError("Converted decimal value is infinite or NaN") - except Exception as e: - msg = "Could not convert %s to decimal" % val - six.raise_from(ValueError(msg), e) - else: - return decimal_value + def _recombine_skipped(self, tokens, skipped_idxs): + """ + >>> tokens = ["foo", " ", "bar", " ", "19June2000", "baz"] + >>> skipped_idxs = [0, 1, 2, 5] + >>> _recombine_skipped(tokens, skipped_idxs) + ["foo bar", "baz"] + """ + skipped_tokens = [] + for i, idx in enumerate(sorted(skipped_idxs)): + if i > 0 and idx - 1 == skipped_idxs[i - 1]: + skipped_tokens[-1] = skipped_tokens[-1] + tokens[idx] + else: + skipped_tokens.append(tokens[idx]) + + return skipped_tokens DEFAULTPARSER = parser() @@ -1341,10 +1353,10 @@ def parse(timestr, parserinfo=None, **kwargs): first element being a :class:`datetime.datetime` object, the second a tuple containing the fuzzy tokens. - :raises ValueError: - Raised for invalid or unknown string format, if the provided - :class:`tzinfo` is not in a valid format, or if an invalid date - would be created. + :raises ParserError: + Raised for invalid or unknown string formats, if the provided + :class:`tzinfo` is not in a valid format, or if an invalid date would + be created. :raises OverflowError: Raised if the parsed date exceeds the largest valid C integer on @@ -1573,6 +1585,29 @@ DEFAULTTZPARSER = _tzparser() def _parsetz(tzstr): return DEFAULTTZPARSER.parse(tzstr) + +class ParserError(ValueError): + """Exception subclass used for any failure to parse a datetime string. + + This is a subclass of :py:exc:`ValueError`, and should be raised any time + earlier versions of ``dateutil`` would have raised ``ValueError``. + + .. versionadded:: 2.8.1 + """ + def __str__(self): + try: + return self.args[0] % self.args[1:] + except (TypeError, IndexError): + return super(ParserError, self).__str__() + + def __repr__(self): + args = ", ".join("'%s'" % arg for arg in self.args) + return "%s(%s)" % (self.__class__.__name__, args) + + class UnknownTimezoneWarning(RuntimeWarning): - """Raised when the parser finds a timezone it cannot parse into a tzinfo""" + """Raised when the parser finds a timezone it cannot parse into a tzinfo. + + .. versionadded:: 2.7.0 + """ # vim:ts=4:sw=4:et diff --git a/libs/dateutil/parser/isoparser.py b/libs/dateutil/parser/isoparser.py index cd27f93d9..5d7bee380 100644 --- a/libs/dateutil/parser/isoparser.py +++ b/libs/dateutil/parser/isoparser.py @@ -88,10 +88,12 @@ class isoparser(object): - ``hh`` - ``hh:mm`` or ``hhmm`` - ``hh:mm:ss`` or ``hhmmss`` - - ``hh:mm:ss.sss`` or ``hh:mm:ss.ssssss`` (3-6 sub-second digits) + - ``hh:mm:ss.ssssss`` (Up to 6 sub-second digits) Midnight is a special case for `hh`, as the standard supports both - 00:00 and 24:00 as a representation. + 00:00 and 24:00 as a representation. The decimal separator can be + either a dot or a comma. + .. caution:: @@ -137,6 +139,10 @@ class isoparser(object): else: raise ValueError('String contains unknown ISO components') + if len(components) > 3 and components[3] == 24: + components[3] = 0 + return datetime(*components) + timedelta(days=1) + return datetime(*components) @_takes_ascii @@ -153,7 +159,7 @@ class isoparser(object): components, pos = self._parse_isodate(datestr) if pos < len(datestr): raise ValueError('String contains unknown ISO ' + - 'components: {}'.format(datestr)) + 'components: {!r}'.format(datestr.decode('ascii'))) return date(*components) @_takes_ascii @@ -167,7 +173,10 @@ class isoparser(object): :return: Returns a :class:`datetime.time` object """ - return time(*self._parse_isotime(timestr)) + components = self._parse_isotime(timestr) + if components[0] == 24: + components[0] = 0 + return time(*components) @_takes_ascii def parse_tzstr(self, tzstr, zero_as_utc=True): @@ -190,10 +199,9 @@ class isoparser(object): return self._parse_tzstr(tzstr, zero_as_utc=zero_as_utc) # Constants - _MICROSECOND_END_REGEX = re.compile(b'[-+Z]+') _DATE_SEP = b'-' _TIME_SEP = b':' - _MICRO_SEP = b'.' + _FRACTION_REGEX = re.compile(b'[\\.,]([0-9]+)') def _parse_isodate(self, dt_str): try: @@ -325,39 +333,42 @@ class isoparser(object): pos = 0 comp = -1 - if len(timestr) < 2: + if len_str < 2: raise ValueError('ISO time too short') - has_sep = len_str >= 3 and timestr[2:3] == self._TIME_SEP + has_sep = False while pos < len_str and comp < 5: comp += 1 - if timestr[pos:pos + 1] in b'-+Z': + if timestr[pos:pos + 1] in b'-+Zz': # Detect time zone boundary components[-1] = self._parse_tzstr(timestr[pos:]) pos = len_str break + if comp == 1 and timestr[pos:pos+1] == self._TIME_SEP: + has_sep = True + pos += 1 + elif comp == 2 and has_sep: + if timestr[pos:pos+1] != self._TIME_SEP: + raise ValueError('Inconsistent use of colon separator') + pos += 1 + if comp < 3: # Hour, minute, second components[comp] = int(timestr[pos:pos + 2]) pos += 2 - if (has_sep and pos < len_str and - timestr[pos:pos + 1] == self._TIME_SEP): - pos += 1 if comp == 3: - # Microsecond - if timestr[pos:pos + 1] != self._MICRO_SEP: + # Fraction of a second + frac = self._FRACTION_REGEX.match(timestr[pos:]) + if not frac: continue - pos += 1 - us_str = self._MICROSECOND_END_REGEX.split(timestr[pos:pos + 6], - 1)[0] - + us_str = frac.group(1)[:6] # Truncate to microseconds components[comp] = int(us_str) * 10**(6 - len(us_str)) - pos += len(us_str) + pos += len(frac.group()) if pos < len_str: raise ValueError('Unused components in ISO string') @@ -366,13 +377,12 @@ class isoparser(object): # Standard supports 00:00 and 24:00 as representations of midnight if any(component != 0 for component in components[1:4]): raise ValueError('Hour may only be 24 at 24:00:00.000') - components[0] = 0 return components def _parse_tzstr(self, tzstr, zero_as_utc=True): - if tzstr == b'Z': - return tz.tzutc() + if tzstr == b'Z' or tzstr == b'z': + return tz.UTC if len(tzstr) not in {3, 5, 6}: raise ValueError('Time zone offset must be 1, 3, 5 or 6 characters') @@ -391,7 +401,7 @@ class isoparser(object): minutes = int(tzstr[(4 if tzstr[3:4] == self._TIME_SEP else 3):]) if zero_as_utc and hours == 0 and minutes == 0: - return tz.tzutc() + return tz.UTC else: if minutes > 59: raise ValueError('Invalid minutes in time zone offset') diff --git a/libs/dateutil/relativedelta.py b/libs/dateutil/relativedelta.py index 7e3bd12ac..a9e85f7e6 100644 --- a/libs/dateutil/relativedelta.py +++ b/libs/dateutil/relativedelta.py @@ -10,16 +10,20 @@ from warnings import warn from ._common import weekday -MO, TU, WE, TH, FR, SA, SU = weekdays = tuple([weekday(x) for x in range(7)]) +MO, TU, WE, TH, FR, SA, SU = weekdays = tuple(weekday(x) for x in range(7)) __all__ = ["relativedelta", "MO", "TU", "WE", "TH", "FR", "SA", "SU"] class relativedelta(object): """ - The relativedelta type is based on the specification of the excellent - work done by M.-A. Lemburg in his - `mx.DateTime `_ extension. + The relativedelta type is designed to be applied to an existing datetime and + can replace specific components of that datetime, or represents an interval + of time. + + It is based on the specification of the excellent work done by M.-A. Lemburg + in his + `mx.DateTime `_ extension. However, notice that this type does *NOT* implement the same algorithm as his work. Do *NOT* expect it to behave like mx.DateTime's counterpart. @@ -34,22 +38,26 @@ class relativedelta(object): year, month, day, hour, minute, second, microsecond: Absolute information (argument is singular); adding or subtracting a - relativedelta with absolute information does not perform an aritmetic + relativedelta with absolute information does not perform an arithmetic operation, but rather REPLACES the corresponding value in the original datetime with the value(s) in relativedelta. years, months, weeks, days, hours, minutes, seconds, microseconds: Relative information, may be negative (argument is plural); adding or subtracting a relativedelta with relative information performs - the corresponding aritmetic operation on the original datetime value + the corresponding arithmetic operation on the original datetime value with the information in the relativedelta. - weekday: - One of the weekday instances (MO, TU, etc). These instances may - receive a parameter N, specifying the Nth weekday, which could - be positive or negative (like MO(+1) or MO(-2). Not specifying - it is the same as specifying +1. You can also use an integer, - where 0=MO. + weekday: + One of the weekday instances (MO, TU, etc) available in the + relativedelta module. These instances may receive a parameter N, + specifying the Nth weekday, which could be positive or negative + (like MO(+1) or MO(-2)). Not specifying it is the same as specifying + +1. You can also use an integer, where 0=MO. This argument is always + relative e.g. if the calculated date is already Monday, using MO(1) + or MO(-1) won't change the day. To effectively make it absolute, use + it in combination with the day argument (e.g. day=1, MO(1) for first + Monday of the month). leapdays: Will add given days to the date found, if year is a leap @@ -59,33 +67,39 @@ class relativedelta(object): Set the yearday or the non-leap year day (jump leap days). These are converted to day/month/leapdays information. - Here is the behavior of operations with relativedelta: + There are relative and absolute forms of the keyword + arguments. The plural is relative, and the singular is + absolute. For each argument in the order below, the absolute form + is applied first (by setting each attribute to that value) and + then the relative form (by adding the value to the attribute). - 1. Calculate the absolute year, using the 'year' argument, or the - original datetime year, if the argument is not present. + The order of attributes considered when this relativedelta is + added to a datetime is: - 2. Add the relative 'years' argument to the absolute year. + 1. Year + 2. Month + 3. Day + 4. Hours + 5. Minutes + 6. Seconds + 7. Microseconds - 3. Do steps 1 and 2 for month/months. + Finally, weekday is applied, using the rule described above. - 4. Calculate the absolute day, using the 'day' argument, or the - original datetime day, if the argument is not present. Then, - subtract from the day until it fits in the year and month - found after their operations. + For example - 5. Add the relative 'days' argument to the absolute day. Notice - that the 'weeks' argument is multiplied by 7 and added to - 'days'. + >>> from datetime import datetime + >>> from dateutil.relativedelta import relativedelta, MO + >>> dt = datetime(2018, 4, 9, 13, 37, 0) + >>> delta = relativedelta(hours=25, day=1, weekday=MO(1)) + >>> dt + delta + datetime.datetime(2018, 4, 2, 14, 37) - 6. Do steps 1 and 2 for hour/hours, minute/minutes, second/seconds, - microsecond/microseconds. + First, the day is set to 1 (the first of the month), then 25 hours + are added, to get to the 2nd day and 14th hour, finally the + weekday is applied, but since the 2nd is already a Monday there is + no effect. - 7. If the 'weekday' argument is present, calculate the weekday, - with the given (wday, nth) tuple. wday is the index of the - weekday (0-6, 0=Mon), and nth is the number of weeks to add - forward or backward, depending on its signal. Notice that if - the calculated date is already Monday, for example, using - (0, 1) or (0, -1) won't change the day. """ def __init__(self, dt1=None, dt2=None, @@ -95,11 +109,6 @@ class relativedelta(object): yearday=None, nlyearday=None, hour=None, minute=None, second=None, microsecond=None): - # Check for non-integer values in integer-only quantities - if any(x is not None and x != int(x) for x in (years, months)): - raise ValueError("Non-integer years and months are " - "ambiguous and not currently supported.") - if dt1 and dt2: # datetime is a subclass of date. So both must be date if not (isinstance(dt1, datetime.date) and @@ -159,9 +168,14 @@ class relativedelta(object): self.seconds = delta.seconds + delta.days * 86400 self.microseconds = delta.microseconds else: + # Check for non-integer values in integer-only quantities + if any(x is not None and x != int(x) for x in (years, months)): + raise ValueError("Non-integer years and months are " + "ambiguous and not currently supported.") + # Relative information - self.years = years - self.months = months + self.years = int(years) + self.months = int(months) self.days = days + weeks * 7 self.leapdays = leapdays self.hours = hours @@ -186,7 +200,6 @@ class relativedelta(object): "This is not a well-defined condition and will raise " + "errors in future versions.", DeprecationWarning) - if isinstance(weekday, integer_types): self.weekday = weekdays[weekday] else: @@ -250,7 +263,8 @@ class relativedelta(object): @property def weeks(self): - return self.days // 7 + return int(self.days / 7.0) + @weeks.setter def weeks(self, value): self.days = self.days - (self.weeks * 7) + value * 7 @@ -271,7 +285,7 @@ class relativedelta(object): values for the relative attributes. >>> relativedelta(days=1.5, hours=2).normalized() - relativedelta(days=1, hours=14) + relativedelta(days=+1, hours=+14) :return: Returns a :class:`dateutil.relativedelta.relativedelta` object. @@ -311,14 +325,22 @@ class relativedelta(object): microseconds=(other.microseconds + self.microseconds), leapdays=other.leapdays or self.leapdays, - year=other.year or self.year, - month=other.month or self.month, - day=other.day or self.day, - weekday=other.weekday or self.weekday, - hour=other.hour or self.hour, - minute=other.minute or self.minute, - second=other.second or self.second, - microsecond=(other.microsecond or + year=(other.year if other.year is not None + else self.year), + month=(other.month if other.month is not None + else self.month), + day=(other.day if other.day is not None + else self.day), + weekday=(other.weekday if other.weekday is not None + else self.weekday), + hour=(other.hour if other.hour is not None + else self.hour), + minute=(other.minute if other.minute is not None + else self.minute), + second=(other.second if other.second is not None + else self.second), + microsecond=(other.microsecond if other.microsecond + is not None else self.microsecond)) if isinstance(other, datetime.timedelta): return self.__class__(years=self.years, @@ -396,14 +418,41 @@ class relativedelta(object): seconds=self.seconds - other.seconds, microseconds=self.microseconds - other.microseconds, leapdays=self.leapdays or other.leapdays, - year=self.year or other.year, - month=self.month or other.month, - day=self.day or other.day, - weekday=self.weekday or other.weekday, - hour=self.hour or other.hour, - minute=self.minute or other.minute, - second=self.second or other.second, - microsecond=self.microsecond or other.microsecond) + year=(self.year if self.year is not None + else other.year), + month=(self.month if self.month is not None else + other.month), + day=(self.day if self.day is not None else + other.day), + weekday=(self.weekday if self.weekday is not None else + other.weekday), + hour=(self.hour if self.hour is not None else + other.hour), + minute=(self.minute if self.minute is not None else + other.minute), + second=(self.second if self.second is not None else + other.second), + microsecond=(self.microsecond if self.microsecond + is not None else + other.microsecond)) + + def __abs__(self): + return self.__class__(years=abs(self.years), + months=abs(self.months), + days=abs(self.days), + hours=abs(self.hours), + minutes=abs(self.minutes), + seconds=abs(self.seconds), + microseconds=abs(self.microseconds), + leapdays=self.leapdays, + year=self.year, + month=self.month, + day=self.day, + weekday=self.weekday, + hour=self.hour, + minute=self.minute, + second=self.second, + microsecond=self.microsecond) def __neg__(self): return self.__class__(years=-self.years, @@ -495,7 +544,25 @@ class relativedelta(object): self.second == other.second and self.microsecond == other.microsecond) - __hash__ = None + def __hash__(self): + return hash(( + self.weekday, + self.years, + self.months, + self.days, + self.hours, + self.minutes, + self.seconds, + self.microseconds, + self.leapdays, + self.year, + self.month, + self.day, + self.hour, + self.minute, + self.second, + self.microsecond, + )) def __ne__(self, other): return not self.__eq__(other) @@ -525,6 +592,7 @@ class relativedelta(object): return "{classname}({attrs})".format(classname=self.__class__.__name__, attrs=", ".join(l)) + def _sign(x): return int(copysign(1, x)) diff --git a/libs/dateutil/rrule.py b/libs/dateutil/rrule.py index da94351b9..b3203393c 100644 --- a/libs/dateutil/rrule.py +++ b/libs/dateutil/rrule.py @@ -2,28 +2,30 @@ """ The rrule module offers a small, complete, and very fast, implementation of the recurrence rules documented in the -`iCalendar RFC `_, +`iCalendar RFC `_, including support for caching of results. """ -import itertools -import datetime import calendar +import datetime +import heapq +import itertools +import re import sys +from functools import wraps +# For warning about deprecation of until and count +from warnings import warn + +from six import advance_iterator, integer_types + +from six.moves import _thread, range + +from ._common import weekday as weekdaybase try: from math import gcd except ImportError: from fractions import gcd -from six import advance_iterator, integer_types -from six.moves import _thread, range -import heapq - -from ._common import weekday as weekdaybase - -# For warning about deprecation of until and count -from warnings import warn - __all__ = ["rrule", "rruleset", "rrulestr", "YEARLY", "MONTHLY", "WEEKLY", "DAILY", "HOURLY", "MINUTELY", "SECONDLY", @@ -46,7 +48,7 @@ del M29, M30, M31, M365MASK[59], MDAY365MASK[59], NMDAY365MASK[31] MDAY365MASK = tuple(MDAY365MASK) M365MASK = tuple(M365MASK) -FREQNAMES = ['YEARLY','MONTHLY','WEEKLY','DAILY','HOURLY','MINUTELY','SECONDLY'] +FREQNAMES = ['YEARLY', 'MONTHLY', 'WEEKLY', 'DAILY', 'HOURLY', 'MINUTELY', 'SECONDLY'] (YEARLY, MONTHLY, @@ -60,6 +62,7 @@ FREQNAMES = ['YEARLY','MONTHLY','WEEKLY','DAILY','HOURLY','MINUTELY','SECONDLY'] easter = None parser = None + class weekday(weekdaybase): """ This version of weekday does not allow n = 0. @@ -70,7 +73,8 @@ class weekday(weekdaybase): super(weekday, self).__init__(wkday, n) -MO, TU, WE, TH, FR, SA, SU = weekdays = tuple([weekday(x) for x in range(7)]) + +MO, TU, WE, TH, FR, SA, SU = weekdays = tuple(weekday(x) for x in range(7)) def _invalidates_cache(f): @@ -78,6 +82,7 @@ def _invalidates_cache(f): Decorator for rruleset methods which may invalidate the cached length. """ + @wraps(f) def inner_func(self, *args, **kwargs): rv = f(self, *args, **kwargs) self._invalidate_cache() @@ -174,7 +179,7 @@ class rrulebase(object): return False return False - # __len__() introduces a large performance penality. + # __len__() introduces a large performance penalty. def count(self): """ Returns the number of recurrences in this set. It will have go trough the whole recurrence, if this hasn't been done before. """ @@ -256,13 +261,13 @@ class rrulebase(object): n = 0 for d in gen: if comp(d, dt): - yield d - if count is not None: n += 1 - if n >= count: + if n > count: break + yield d + def between(self, after, before, inc=False, count=1): """ Returns all the occurrences of the rrule between after and before. The inc keyword defines what happens if after and/or before are @@ -333,10 +338,6 @@ class rrule(rrulebase): Additionally, it supports the following keyword arguments: - :param cache: - If given, it must be a boolean value specifying to enable or disable - caching of results. If you will use the same rrule instance multiple - times, enabling caching will improve the performance considerably. :param dtstart: The recurrence start. Besides being the base for the recurrence, missing parameters in the final recurrence instances will also be @@ -353,20 +354,26 @@ class rrule(rrulebase): from calendar.firstweekday(), and may be modified by calendar.setfirstweekday(). :param count: - How many occurrences will be generated. + If given, this determines how many occurrences will be generated. .. note:: - As of version 2.5.0, the use of the ``until`` keyword together - with the ``count`` keyword is deprecated per RFC-2445 Sec. 4.3.10. + As of version 2.5.0, the use of the keyword ``until`` in conjunction + with ``count`` is deprecated, to make sure ``dateutil`` is fully + compliant with `RFC-5545 Sec. 3.3.10 `_. Therefore, ``until`` and ``count`` + **must not** occur in the same call to ``rrule``. :param until: - If given, this must be a datetime instance, that will specify the + If given, this must be a datetime instance specifying the upper-bound limit of the recurrence. The last recurrence in the rule is the greatest datetime that is less than or equal to the value specified in the ``until`` parameter. - + .. note:: - As of version 2.5.0, the use of the ``until`` keyword together - with the ``count`` keyword is deprecated per RFC-2445 Sec. 4.3.10. + As of version 2.5.0, the use of the keyword ``until`` in conjunction + with ``count`` is deprecated, to make sure ``dateutil`` is fully + compliant with `RFC-5545 Sec. 3.3.10 `_. Therefore, ``until`` and ``count`` + **must not** occur in the same call to ``rrule``. :param bysetpos: If given, it must be either an integer, or a sequence of integers, positive or negative. Each given integer will specify an occurrence @@ -383,6 +390,11 @@ class rrule(rrulebase): :param byyearday: If given, it must be either an integer, or a sequence of integers, meaning the year days to apply the recurrence to. + :param byeaster: + If given, it must be either an integer, or a sequence of integers, + positive or negative. Each integer will define an offset from the + Easter Sunday. Passing the offset 0 to byeaster will yield the Easter + Sunday itself. This is an extension to the RFC specification. :param byweekno: If given, it must be either an integer, or a sequence of integers, meaning the week numbers to apply the recurrence to. Week numbers @@ -408,11 +420,10 @@ class rrule(rrulebase): :param bysecond: If given, it must be either an integer, or a sequence of integers, meaning the seconds to apply the recurrence to. - :param byeaster: - If given, it must be either an integer, or a sequence of integers, - positive or negative. Each integer will define an offset from the - Easter Sunday. Passing the offset 0 to byeaster will yield the Easter - Sunday itself. This is an extension to the RFC specification. + :param cache: + If given, it must be a boolean value specifying to enable or disable + caching of results. If you will use the same rrule instance multiple + times, enabling caching will improve the performance considerably. """ def __init__(self, freq, dtstart=None, interval=1, wkst=None, count=None, until=None, bysetpos=None, @@ -423,7 +434,10 @@ class rrule(rrulebase): super(rrule, self).__init__(cache) global easter if not dtstart: - dtstart = datetime.datetime.now().replace(microsecond=0) + if until and until.tzinfo: + dtstart = datetime.datetime.now(tz=until.tzinfo).replace(microsecond=0) + else: + dtstart = datetime.datetime.now().replace(microsecond=0) elif not isinstance(dtstart, datetime.datetime): dtstart = datetime.datetime.fromordinal(dtstart.toordinal()) else: @@ -444,8 +458,22 @@ class rrule(rrulebase): until = datetime.datetime.fromordinal(until.toordinal()) self._until = until - if count and until: - warn("Using both 'count' and 'until' is inconsistent with RFC 2445" + if self._dtstart and self._until: + if (self._dtstart.tzinfo is not None) != (self._until.tzinfo is not None): + # According to RFC5545 Section 3.3.10: + # https://tools.ietf.org/html/rfc5545#section-3.3.10 + # + # > If the "DTSTART" property is specified as a date with UTC + # > time or a date with local time and time zone reference, + # > then the UNTIL rule part MUST be specified as a date with + # > UTC time. + raise ValueError( + 'RRULE UNTIL values must be specified in UTC when DTSTART ' + 'is timezone-aware' + ) + + if count is not None and until: + warn("Using both 'count' and 'until' is inconsistent with RFC 5545" " and has been deprecated in dateutil. Future versions will " "raise an error.", DeprecationWarning) @@ -533,8 +561,8 @@ class rrule(rrulebase): bymonthday = set(bymonthday) # Ensure it's unique - self._bymonthday = tuple(sorted([x for x in bymonthday if x > 0])) - self._bynmonthday = tuple(sorted([x for x in bymonthday if x < 0])) + self._bymonthday = tuple(sorted(x for x in bymonthday if x > 0)) + self._bynmonthday = tuple(sorted(x for x in bymonthday if x < 0)) # Storing positive numbers first, then negative numbers if 'bymonthday' not in self._original_rule: @@ -582,13 +610,13 @@ class rrule(rrulebase): self._byweekday = tuple(sorted(self._byweekday)) orig_byweekday = [weekday(x) for x in self._byweekday] else: - orig_byweekday = tuple() + orig_byweekday = () if self._bynweekday is not None: self._bynweekday = tuple(sorted(self._bynweekday)) orig_bynweekday = [weekday(*x) for x in self._bynweekday] else: - orig_bynweekday = tuple() + orig_bynweekday = () if 'byweekday' not in self._original_rule: self._original_rule['byweekday'] = tuple(itertools.chain( @@ -597,7 +625,7 @@ class rrule(rrulebase): # byhour if byhour is None: if freq < HOURLY: - self._byhour = set((dtstart.hour,)) + self._byhour = {dtstart.hour} else: self._byhour = None else: @@ -617,7 +645,7 @@ class rrule(rrulebase): # byminute if byminute is None: if freq < MINUTELY: - self._byminute = set((dtstart.minute,)) + self._byminute = {dtstart.minute} else: self._byminute = None else: @@ -672,7 +700,7 @@ class rrule(rrulebase): def __str__(self): """ Output a string that would generate this RRULE if passed to rrulestr. - This is mostly compatible with RFC2445, except for the + This is mostly compatible with RFC5545, except for the dateutil-specific extension BYEASTER. """ @@ -689,7 +717,7 @@ class rrule(rrulebase): if self._wkst: parts.append('WKST=' + repr(weekday(self._wkst))[0:2]) - if self._count: + if self._count is not None: parts.append('COUNT=' + str(self._count)) if self._until: @@ -697,7 +725,7 @@ class rrule(rrulebase): if self._original_rule.get('byweekday') is not None: # The str() method on weekday objects doesn't generate - # RFC2445-compliant strings, so we should modify that. + # RFC5545-compliant strings, so we should modify that. original_rule = dict(self._original_rule) wday_strings = [] for wday in original_rule['byweekday']: @@ -728,7 +756,7 @@ class rrule(rrulebase): parts.append(partfmt.format(name=name, vals=(','.join(str(v) for v in value)))) - output.append(';'.join(parts)) + output.append('RRULE:' + ';'.join(parts)) return '\n'.join(output) def replace(self, **kwargs): @@ -745,7 +773,6 @@ class rrule(rrulebase): new_kwargs.update(kwargs) return rrule(**new_kwargs) - def _iter(self): year, month, day, hour, minute, second, weekday, yearday, _ = \ self._dtstart.timetuple() @@ -844,13 +871,13 @@ class rrule(rrulebase): self._len = total return elif res >= self._dtstart: - total += 1 - yield res - if count: + if count is not None: count -= 1 - if not count: + if count < 0: self._len = total return + total += 1 + yield res else: for i in dayset[start:end]: if i is not None: @@ -861,14 +888,15 @@ class rrule(rrulebase): self._len = total return elif res >= self._dtstart: - total += 1 - yield res - if count: + if count is not None: count -= 1 - if not count: + if count < 0: self._len = total return + total += 1 + yield res + # Handle frequency and interval fixday = False if freq == YEARLY: @@ -1385,7 +1413,52 @@ class rruleset(rrulebase): self._len = total + + class _rrulestr(object): + """ Parses a string representation of a recurrence rule or set of + recurrence rules. + + :param s: + Required, a string defining one or more recurrence rules. + + :param dtstart: + If given, used as the default recurrence start if not specified in the + rule string. + + :param cache: + If set ``True`` caching of results will be enabled, improving + performance of multiple queries considerably. + + :param unfold: + If set ``True`` indicates that a rule string is split over more + than one line and should be joined before processing. + + :param forceset: + If set ``True`` forces a :class:`dateutil.rrule.rruleset` to + be returned. + + :param compatible: + If set ``True`` forces ``unfold`` and ``forceset`` to be ``True``. + + :param ignoretz: + If set ``True``, time zones in parsed strings are ignored and a naive + :class:`datetime.datetime` object is returned. + + :param tzids: + If given, a callable or mapping used to retrieve a + :class:`datetime.tzinfo` from a string representation. + Defaults to :func:`dateutil.tz.gettz`. + + :param tzinfos: + Additional time zone names / aliases which may be present in a string + representation. See :func:`dateutil.parser.parse` for more + information. + + :return: + Returns a :class:`dateutil.rrule.rruleset` or + :class:`dateutil.rrule.rrule` + """ _freq_map = {"YEARLY": YEARLY, "MONTHLY": MONTHLY, @@ -1487,6 +1560,58 @@ class _rrulestr(object): raise ValueError("invalid '%s': %s" % (name, value)) return rrule(dtstart=dtstart, cache=cache, **rrkwargs) + def _parse_date_value(self, date_value, parms, rule_tzids, + ignoretz, tzids, tzinfos): + global parser + if not parser: + from dateutil import parser + + datevals = [] + value_found = False + TZID = None + + for parm in parms: + if parm.startswith("TZID="): + try: + tzkey = rule_tzids[parm.split('TZID=')[-1]] + except KeyError: + continue + if tzids is None: + from . import tz + tzlookup = tz.gettz + elif callable(tzids): + tzlookup = tzids + else: + tzlookup = getattr(tzids, 'get', None) + if tzlookup is None: + msg = ('tzids must be a callable, mapping, or None, ' + 'not %s' % tzids) + raise ValueError(msg) + + TZID = tzlookup(tzkey) + continue + + # RFC 5445 3.8.2.4: The VALUE parameter is optional, but may be found + # only once. + if parm not in {"VALUE=DATE-TIME", "VALUE=DATE"}: + raise ValueError("unsupported parm: " + parm) + else: + if value_found: + msg = ("Duplicate value parameter found in: " + parm) + raise ValueError(msg) + value_found = True + + for datestr in date_value.split(','): + date = parser.parse(datestr, ignoretz=ignoretz, tzinfos=tzinfos) + if TZID is not None: + if date.tzinfo is None: + date = date.replace(tzinfo=TZID) + else: + raise ValueError('DTSTART/EXDATE specifies multiple timezone') + datevals.append(date) + + return datevals + def _parse_rfc(self, s, dtstart=None, cache=False, @@ -1494,11 +1619,17 @@ class _rrulestr(object): forceset=False, compatible=False, ignoretz=False, + tzids=None, tzinfos=None): global parser if compatible: forceset = True unfold = True + + TZID_NAMES = dict(map( + lambda x: (x.upper(), x), + re.findall('TZID=(?P[^:]+):', s) + )) s = s.upper() if not s.strip(): raise ValueError("empty string") @@ -1553,17 +1684,18 @@ class _rrulestr(object): raise ValueError("unsupported EXRULE parm: "+parm) exrulevals.append(value) elif name == "EXDATE": - for parm in parms: - if parm != "VALUE=DATE-TIME": - raise ValueError("unsupported RDATE parm: "+parm) - exdatevals.append(value) + exdatevals.extend( + self._parse_date_value(value, parms, + TZID_NAMES, ignoretz, + tzids, tzinfos) + ) elif name == "DTSTART": - for parm in parms: - raise ValueError("unsupported DTSTART parm: "+parm) - if not parser: - from dateutil import parser - dtstart = parser.parse(value, ignoretz=ignoretz, - tzinfos=tzinfos) + dtvals = self._parse_date_value(value, parms, TZID_NAMES, + ignoretz, tzids, tzinfos) + if len(dtvals) != 1: + raise ValueError("Multiple DTSTART values specified:" + + value) + dtstart = dtvals[0] else: raise ValueError("unsupported property: "+name) if (forceset or len(rrulevals) > 1 or rdatevals @@ -1585,10 +1717,7 @@ class _rrulestr(object): ignoretz=ignoretz, tzinfos=tzinfos)) for value in exdatevals: - for datestr in value.split(','): - rset.exdate(parser.parse(datestr, - ignoretz=ignoretz, - tzinfos=tzinfos)) + rset.exdate(value) if compatible and dtstart: rset.rdate(dtstart) return rset @@ -1602,6 +1731,7 @@ class _rrulestr(object): def __call__(self, s, **kwargs): return self._parse_rfc(s, **kwargs) + rrulestr = _rrulestr() # vim:ts=4:sw=4:et diff --git a/libs/dateutil/test/_common.py b/libs/dateutil/test/_common.py index f77b53e4f..b8d204737 100644 --- a/libs/dateutil/test/_common.py +++ b/libs/dateutil/test/_common.py @@ -1,64 +1,12 @@ from __future__ import unicode_literals -try: - import unittest2 as unittest -except ImportError: - import unittest - import os -import datetime import time import subprocess import warnings import tempfile import pickle - -class WarningTestMixin(object): - # Based on https://stackoverflow.com/a/12935176/467366 - class _AssertWarnsContext(warnings.catch_warnings): - def __init__(self, expected_warnings, parent, **kwargs): - super(WarningTestMixin._AssertWarnsContext, self).__init__(**kwargs) - - self.parent = parent - try: - self.expected_warnings = list(expected_warnings) - except TypeError: - self.expected_warnings = [expected_warnings] - - self._warning_log = [] - - def __enter__(self, *args, **kwargs): - rv = super(WarningTestMixin._AssertWarnsContext, self).__enter__(*args, **kwargs) - - if self._showwarning is not self._module.showwarning: - super_showwarning = self._module.showwarning - else: - super_showwarning = None - - def showwarning(*args, **kwargs): - if super_showwarning is not None: - super_showwarning(*args, **kwargs) - - self._warning_log.append(warnings.WarningMessage(*args, **kwargs)) - - self._module.showwarning = showwarning - return rv - - def __exit__(self, *args, **kwargs): - super(WarningTestMixin._AssertWarnsContext, self).__exit__(self, *args, **kwargs) - - self.parent.assertTrue(any(issubclass(item.category, warning) - for warning in self.expected_warnings - for item in self._warning_log)) - - def assertWarns(self, warning, callable=None, *args, **kwargs): - warnings.simplefilter('always') - context = self.__class__._AssertWarnsContext(warning, self) - if callable is None: - return context - else: - with context: - callable(*args, **kwargs) +import pytest class PicklableMixin(object): @@ -81,7 +29,7 @@ class PicklableMixin(object): return nobj - def assertPicklable(self, obj, asfile=False, + def assertPicklable(self, obj, singleton=False, asfile=False, dump_kwargs=None, load_kwargs=None): """ Assert that an object can be pickled and unpickled. This assertion @@ -93,7 +41,8 @@ class PicklableMixin(object): load_kwargs = load_kwargs or {} nobj = get_nobj(obj, dump_kwargs, load_kwargs) - self.assertIsNot(obj, nobj) + if not singleton: + self.assertIsNot(obj, nobj) self.assertEqual(obj, nobj) @@ -138,7 +87,11 @@ class TZContextBase(object): def __enter__(self): if not self.tz_change_allowed(): - raise ValueError(self.tz_change_disallowed_message()) + msg = self.tz_change_disallowed_message() + pytest.skip(msg) + + # If this is used outside of a test suite, we still want an error. + raise ValueError(msg) # pragma: no cover self._old_tz = self.get_current_tz() self.set_current_tz(self.tzval) @@ -192,7 +145,7 @@ class TZWinContext(TZContextBase): p = subprocess.Popen(['tzutil', '/g'], stdout=subprocess.PIPE) ctzname, err = p.communicate() - ctzname = ctzname.decode() # Popen returns + ctzname = ctzname.decode() # Popen returns if p.returncode: raise OSError('Failed to get current time zone: ' + err) @@ -209,17 +162,6 @@ class TZWinContext(TZContextBase): (err or 'Unknown error.')) -### -# Compatibility functions - -def _total_seconds(td): - # Python 2.6 doesn't have a total_seconds() method on timedelta objects - return ((td.seconds + td.days * 86400) * 1000000 + - td.microseconds) // 1000000 - -total_seconds = getattr(datetime.timedelta, 'total_seconds', _total_seconds) - - ### # Utility classes class NotAValueClass(object): @@ -245,6 +187,7 @@ class NotAValueClass(object): __le__ = __rle__ = _op __ge__ = __rge__ = _op + NotAValue = NotAValueClass() @@ -278,11 +221,13 @@ class ComparesEqualClass(object): __rlt__ = __lt__ __rgt__ = __gt__ + ComparesEqual = ComparesEqualClass() + class UnsetTzClass(object): """ Sentinel class for unset time zone variable """ pass -UnsetTz = UnsetTzClass() +UnsetTz = UnsetTzClass() diff --git a/libs/dateutil/test/conftest.py b/libs/dateutil/test/conftest.py new file mode 100644 index 000000000..78ed70acb --- /dev/null +++ b/libs/dateutil/test/conftest.py @@ -0,0 +1,41 @@ +import os +import pytest + + +# Configure pytest to ignore xfailing tests +# See: https://stackoverflow.com/a/53198349/467366 +def pytest_collection_modifyitems(items): + for item in items: + marker_getter = getattr(item, 'get_closest_marker', None) + + # Python 3.3 support + if marker_getter is None: + marker_getter = item.get_marker + + marker = marker_getter('xfail') + + # Need to query the args because conditional xfail tests still have + # the xfail mark even if they are not expected to fail + if marker and (not marker.args or marker.args[0]): + item.add_marker(pytest.mark.no_cover) + + +def set_tzpath(): + """ + Sets the TZPATH variable if it's specified in an environment variable. + """ + tzpath = os.environ.get('DATEUTIL_TZPATH', None) + + if tzpath is None: + return + + path_components = tzpath.split(':') + + print("Setting TZPATH to {}".format(path_components)) + + from dateutil import tz + tz.TZPATHS.clear() + tz.TZPATHS.extend(path_components) + + +set_tzpath() diff --git a/libs/dateutil/test/property/test_isoparse_prop.py b/libs/dateutil/test/property/test_isoparse_prop.py new file mode 100644 index 000000000..f8e288f3d --- /dev/null +++ b/libs/dateutil/test/property/test_isoparse_prop.py @@ -0,0 +1,27 @@ +from hypothesis import given, assume +from hypothesis import strategies as st + +from dateutil import tz +from dateutil.parser import isoparse + +import pytest + +# Strategies +TIME_ZONE_STRATEGY = st.sampled_from([None, tz.UTC] + + [tz.gettz(zname) for zname in ('US/Eastern', 'US/Pacific', + 'Australia/Sydney', 'Europe/London')]) +ASCII_STRATEGY = st.characters(max_codepoint=127) + + +@pytest.mark.isoparser +@given(dt=st.datetimes(timezones=TIME_ZONE_STRATEGY), sep=ASCII_STRATEGY) +def test_timespec_auto(dt, sep): + if dt.tzinfo is not None: + # Assume offset has no sub-second components + assume(dt.utcoffset().total_seconds() % 60 == 0) + + sep = str(sep) # Python 2.7 requires bytes + dtstr = dt.isoformat(sep=sep) + dt_rt = isoparse(dtstr) + + assert dt_rt == dt diff --git a/libs/dateutil/test/property/test_parser_prop.py b/libs/dateutil/test/property/test_parser_prop.py new file mode 100644 index 000000000..fdfd171e8 --- /dev/null +++ b/libs/dateutil/test/property/test_parser_prop.py @@ -0,0 +1,22 @@ +from hypothesis.strategies import integers +from hypothesis import given + +import pytest + +from dateutil.parser import parserinfo + + +@pytest.mark.parserinfo +@given(integers(min_value=100, max_value=9999)) +def test_convertyear(n): + assert n == parserinfo().convertyear(n) + + +@pytest.mark.parserinfo +@given(integers(min_value=-50, + max_value=49)) +def test_convertyear_no_specified_century(n): + p = parserinfo() + new_year = p._year + n + result = p.convertyear(new_year % 100, century_specified=False) + assert result == new_year diff --git a/libs/dateutil/test/property/test_tz_prop.py b/libs/dateutil/test/property/test_tz_prop.py new file mode 100644 index 000000000..ec6d271dc --- /dev/null +++ b/libs/dateutil/test/property/test_tz_prop.py @@ -0,0 +1,35 @@ +from datetime import datetime, timedelta + +import pytest +import six +from hypothesis import assume, given +from hypothesis import strategies as st + +from dateutil import tz as tz + +EPOCHALYPSE = datetime.fromtimestamp(2147483647) +NEGATIVE_EPOCHALYPSE = datetime.fromtimestamp(0) - timedelta(seconds=2147483648) + + +@pytest.mark.gettz +@pytest.mark.parametrize("gettz_arg", [None, ""]) +# TODO: Remove bounds when GH #590 is resolved +@given( + dt=st.datetimes( + min_value=NEGATIVE_EPOCHALYPSE, max_value=EPOCHALYPSE, timezones=st.just(tz.UTC), + ) +) +def test_gettz_returns_local(gettz_arg, dt): + act_tz = tz.gettz(gettz_arg) + if isinstance(act_tz, tz.tzlocal): + return + + dt_act = dt.astimezone(tz.gettz(gettz_arg)) + if six.PY2: + dt_exp = dt.astimezone(tz.tzlocal()) + else: + dt_exp = dt.astimezone() + + assert dt_act == dt_exp + assert dt_act.tzname() == dt_exp.tzname() + assert dt_act.utcoffset() == dt_exp.utcoffset() diff --git a/libs/dateutil/test/test_easter.py b/libs/dateutil/test/test_easter.py index b45d7fe89..cf2ec7f28 100644 --- a/libs/dateutil/test/test_easter.py +++ b/libs/dateutil/test/test_easter.py @@ -2,11 +2,7 @@ from dateutil.easter import easter from dateutil.easter import EASTER_WESTERN, EASTER_ORTHODOX, EASTER_JULIAN from datetime import date - -try: - import unittest2 as unittest -except ImportError: - import unittest +import pytest # List of easters between 1990 and 2050 western_easter_dates = [ @@ -77,23 +73,21 @@ julian_easter_dates = [ ] -class EasterTest(unittest.TestCase): - def testEasterWestern(self): - for easter_date in western_easter_dates: - self.assertEqual(easter_date, - easter(easter_date.year, EASTER_WESTERN)) +@pytest.mark.parametrize("easter_date", western_easter_dates) +def test_easter_western(easter_date): + assert easter_date == easter(easter_date.year, EASTER_WESTERN) - def testEasterOrthodox(self): - for easter_date in orthodox_easter_dates: - self.assertEqual(easter_date, - easter(easter_date.year, EASTER_ORTHODOX)) - def testEasterJulian(self): - for easter_date in julian_easter_dates: - self.assertEqual(easter_date, - easter(easter_date.year, EASTER_JULIAN)) +@pytest.mark.parametrize("easter_date", orthodox_easter_dates) +def test_easter_orthodox(easter_date): + assert easter_date == easter(easter_date.year, EASTER_ORTHODOX) - def testEasterBadMethod(self): - # Invalid methods raise ValueError - with self.assertRaises(ValueError): - easter(1975, 4) + +@pytest.mark.parametrize("easter_date", julian_easter_dates) +def test_easter_julian(easter_date): + assert easter_date == easter(easter_date.year, EASTER_JULIAN) + + +def test_easter_bad_method(): + with pytest.raises(ValueError): + easter(1975, 4) diff --git a/libs/dateutil/test/test_import_star.py b/libs/dateutil/test/test_import_star.py new file mode 100644 index 000000000..2fb709812 --- /dev/null +++ b/libs/dateutil/test/test_import_star.py @@ -0,0 +1,33 @@ +"""Test for the "import *" functionality. + +As import * can be only done at module level, it has been added in a separate file +""" +import pytest + +prev_locals = list(locals()) +from dateutil import * +new_locals = {name:value for name,value in locals().items() + if name not in prev_locals} +new_locals.pop('prev_locals') + + +@pytest.mark.import_star +def test_imported_modules(): + """ Test that `from dateutil import *` adds modules in __all__ locally """ + import dateutil.easter + import dateutil.parser + import dateutil.relativedelta + import dateutil.rrule + import dateutil.tz + import dateutil.utils + import dateutil.zoneinfo + + assert dateutil.easter == new_locals.pop("easter") + assert dateutil.parser == new_locals.pop("parser") + assert dateutil.relativedelta == new_locals.pop("relativedelta") + assert dateutil.rrule == new_locals.pop("rrule") + assert dateutil.tz == new_locals.pop("tz") + assert dateutil.utils == new_locals.pop("utils") + assert dateutil.zoneinfo == new_locals.pop("zoneinfo") + + assert not new_locals diff --git a/libs/dateutil/test/test_imports.py b/libs/dateutil/test/test_imports.py index 1d8ac171e..60b86005c 100644 --- a/libs/dateutil/test/test_imports.py +++ b/libs/dateutil/test/test_imports.py @@ -1,149 +1,176 @@ import sys - -try: - import unittest2 as unittest -except ImportError: - import unittest +import pytest -class ImportEasterTest(unittest.TestCase): - """ Test that dateutil.easter-related imports work properly """ - - def testEasterDirect(self): - import dateutil.easter - - def testEasterFrom(self): - from dateutil import easter - - def testEasterStar(self): - from dateutil.easter import easter +HOST_IS_WINDOWS = sys.platform.startswith('win') -class ImportParserTest(unittest.TestCase): - """ Test that dateutil.parser-related imports work properly """ - def testParserDirect(self): - import dateutil.parser - - def testParserFrom(self): - from dateutil import parser - - def testParserAll(self): - # All interface - from dateutil.parser import parse - from dateutil.parser import parserinfo - - # Other public classes - from dateutil.parser import parser - - for var in (parse, parserinfo, parser): - self.assertIsNot(var, None) +def test_import_version_str(): + """ Test that dateutil.__version__ can be imported""" + from dateutil import __version__ -class ImportRelativeDeltaTest(unittest.TestCase): - """ Test that dateutil.relativedelta-related imports work properly """ - def testRelativeDeltaDirect(self): - import dateutil.relativedelta - - def testRelativeDeltaFrom(self): - from dateutil import relativedelta - - def testRelativeDeltaAll(self): - from dateutil.relativedelta import relativedelta - from dateutil.relativedelta import MO, TU, WE, TH, FR, SA, SU - - for var in (relativedelta, MO, TU, WE, TH, FR, SA, SU): - self.assertIsNot(var, None) - - # In the public interface but not in all - from dateutil.relativedelta import weekday - self.assertIsNot(weekday, None) +def test_import_version_root(): + import dateutil + assert hasattr(dateutil, '__version__') -class ImportRRuleTest(unittest.TestCase): - """ Test that dateutil.rrule related imports work properly """ - def testRRuleDirect(self): - import dateutil.rrule - - def testRRuleFrom(self): - from dateutil import rrule - - def testRRuleAll(self): - from dateutil.rrule import rrule - from dateutil.rrule import rruleset - from dateutil.rrule import rrulestr - from dateutil.rrule import YEARLY, MONTHLY, WEEKLY, DAILY - from dateutil.rrule import HOURLY, MINUTELY, SECONDLY - from dateutil.rrule import MO, TU, WE, TH, FR, SA, SU - - rr_all = (rrule, rruleset, rrulestr, - YEARLY, MONTHLY, WEEKLY, DAILY, - HOURLY, MINUTELY, SECONDLY, - MO, TU, WE, TH, FR, SA, SU) - - for var in rr_all: - self.assertIsNot(var, None) - - # In the public interface but not in all - from dateutil.rrule import weekday - self.assertIsNot(weekday, None) +# Test that dateutil.easter-related imports work properly +def test_import_easter_direct(): + import dateutil.easter -class ImportTZTest(unittest.TestCase): - """ Test that dateutil.tz related imports work properly """ - def testTzDirect(self): - import dateutil.tz - - def testTzFrom(self): - from dateutil import tz - - def testTzAll(self): - from dateutil.tz import tzutc - from dateutil.tz import tzoffset - from dateutil.tz import tzlocal - from dateutil.tz import tzfile - from dateutil.tz import tzrange - from dateutil.tz import tzstr - from dateutil.tz import tzical - from dateutil.tz import gettz - from dateutil.tz import tzwin - from dateutil.tz import tzwinlocal - - tz_all = ["tzutc", "tzoffset", "tzlocal", "tzfile", "tzrange", - "tzstr", "tzical", "gettz"] - - tz_all += ["tzwin", "tzwinlocal"] if sys.platform.startswith("win") else [] - lvars = locals() - - for var in tz_all: - self.assertIsNot(lvars[var], None) +def test_import_easter_from(): + from dateutil import easter -@unittest.skipUnless(sys.platform.startswith('win'), "Requires Windows") -class ImportTZWinTest(unittest.TestCase): - """ Test that dateutil.tzwin related imports work properly """ - def testTzwinDirect(self): - import dateutil.tzwin - - def testTzwinFrom(self): - from dateutil import tzwin - - def testTzwinStar(self): - tzwin_all = ["tzwin", "tzwinlocal"] +def test_import_easter_start(): + from dateutil.easter import easter -class ImportZoneInfoTest(unittest.TestCase): - def testZoneinfoDirect(self): - import dateutil.zoneinfo +# Test that dateutil.parser-related imports work properly +def test_import_parser_direct(): + import dateutil.parser - def testZoneinfoFrom(self): - from dateutil import zoneinfo - def testZoneinfoStar(self): - from dateutil.zoneinfo import gettz - from dateutil.zoneinfo import gettz_db_metadata - from dateutil.zoneinfo import rebuild +def test_import_parser_from(): + from dateutil import parser - zi_all = (gettz, gettz_db_metadata, rebuild) - for var in zi_all: - self.assertIsNot(var, None) +def test_import_parser_all(): + # All interface + from dateutil.parser import parse + from dateutil.parser import parserinfo + + # Other public classes + from dateutil.parser import parser + + for var in (parse, parserinfo, parser): + assert var is not None + + +# Test that dateutil.relativedelta-related imports work properly +def test_import_relative_delta_direct(): + import dateutil.relativedelta + + +def test_import_relative_delta_from(): + from dateutil import relativedelta + +def test_import_relative_delta_all(): + from dateutil.relativedelta import relativedelta + from dateutil.relativedelta import MO, TU, WE, TH, FR, SA, SU + + for var in (relativedelta, MO, TU, WE, TH, FR, SA, SU): + assert var is not None + + # In the public interface but not in all + from dateutil.relativedelta import weekday + assert weekday is not None + + +# Test that dateutil.rrule related imports work properly +def test_import_rrule_direct(): + import dateutil.rrule + + +def test_import_rrule_from(): + from dateutil import rrule + + +def test_import_rrule_all(): + from dateutil.rrule import rrule + from dateutil.rrule import rruleset + from dateutil.rrule import rrulestr + from dateutil.rrule import YEARLY, MONTHLY, WEEKLY, DAILY + from dateutil.rrule import HOURLY, MINUTELY, SECONDLY + from dateutil.rrule import MO, TU, WE, TH, FR, SA, SU + + rr_all = (rrule, rruleset, rrulestr, + YEARLY, MONTHLY, WEEKLY, DAILY, + HOURLY, MINUTELY, SECONDLY, + MO, TU, WE, TH, FR, SA, SU) + + for var in rr_all: + assert var is not None + + # In the public interface but not in all + from dateutil.rrule import weekday + assert weekday is not None + + +# Test that dateutil.tz related imports work properly +def test_import_tztest_direct(): + import dateutil.tz + + +def test_import_tz_from(): + from dateutil import tz + + +def test_import_tz_all(): + from dateutil.tz import tzutc + from dateutil.tz import tzoffset + from dateutil.tz import tzlocal + from dateutil.tz import tzfile + from dateutil.tz import tzrange + from dateutil.tz import tzstr + from dateutil.tz import tzical + from dateutil.tz import gettz + from dateutil.tz import tzwin + from dateutil.tz import tzwinlocal + from dateutil.tz import UTC + from dateutil.tz import datetime_ambiguous + from dateutil.tz import datetime_exists + from dateutil.tz import resolve_imaginary + + tz_all = ["tzutc", "tzoffset", "tzlocal", "tzfile", "tzrange", + "tzstr", "tzical", "gettz", "datetime_ambiguous", + "datetime_exists", "resolve_imaginary", "UTC"] + + tz_all += ["tzwin", "tzwinlocal"] if sys.platform.startswith("win") else [] + lvars = locals() + + for var in tz_all: + assert lvars[var] is not None + +# Test that dateutil.tzwin related imports work properly +@pytest.mark.skipif(not HOST_IS_WINDOWS, reason="Requires Windows") +def test_import_tz_windows_direct(): + import dateutil.tzwin + + +@pytest.mark.skipif(not HOST_IS_WINDOWS, reason="Requires Windows") +def test_import_tz_windows_from(): + from dateutil import tzwin + + +@pytest.mark.skipif(not HOST_IS_WINDOWS, reason="Requires Windows") +def test_import_tz_windows_star(): + from dateutil.tzwin import tzwin + from dateutil.tzwin import tzwinlocal + + tzwin_all = [tzwin, tzwinlocal] + + for var in tzwin_all: + assert var is not None + + +# Test imports of Zone Info +def test_import_zone_info_direct(): + import dateutil.zoneinfo + + +def test_import_zone_info_from(): + from dateutil import zoneinfo + + +def test_import_zone_info_star(): + from dateutil.zoneinfo import gettz + from dateutil.zoneinfo import gettz_db_metadata + from dateutil.zoneinfo import rebuild + + zi_all = (gettz, gettz_db_metadata, rebuild) + + for var in zi_all: + assert var is not None diff --git a/libs/dateutil/test/test_internals.py b/libs/dateutil/test/test_internals.py new file mode 100644 index 000000000..530813147 --- /dev/null +++ b/libs/dateutil/test/test_internals.py @@ -0,0 +1,91 @@ +# -*- coding: utf-8 -*- +""" +Tests for implementation details, not necessarily part of the user-facing +API. + +The motivating case for these tests is #483, where we want to smoke-test +code that may be difficult to reach through the standard API calls. +""" + +import sys +import pytest + +from dateutil.parser._parser import _ymd +from dateutil import tz + +IS_PY32 = sys.version_info[0:2] == (3, 2) + + +@pytest.mark.smoke +def test_YMD_could_be_day(): + ymd = _ymd('foo bar 124 baz') + + ymd.append(2, 'M') + assert ymd.has_month + assert not ymd.has_year + assert ymd.could_be_day(4) + assert not ymd.could_be_day(-6) + assert not ymd.could_be_day(32) + + # Assumes leap year + assert ymd.could_be_day(29) + + ymd.append(1999) + assert ymd.has_year + assert not ymd.could_be_day(29) + + ymd.append(16, 'D') + assert ymd.has_day + assert not ymd.could_be_day(1) + + ymd = _ymd('foo bar 124 baz') + ymd.append(1999) + assert ymd.could_be_day(31) + + +### +# Test that private interfaces in _parser are deprecated properly +@pytest.mark.skipif(IS_PY32, reason='pytest.warns not supported on Python 3.2') +def test_parser_private_warns(): + from dateutil.parser import _timelex, _tzparser + from dateutil.parser import _parsetz + + with pytest.warns(DeprecationWarning): + _tzparser() + + with pytest.warns(DeprecationWarning): + _timelex('2014-03-03') + + with pytest.warns(DeprecationWarning): + _parsetz('+05:00') + + +@pytest.mark.skipif(IS_PY32, reason='pytest.warns not supported on Python 3.2') +def test_parser_parser_private_not_warns(): + from dateutil.parser._parser import _timelex, _tzparser + from dateutil.parser._parser import _parsetz + + with pytest.warns(None) as recorder: + _tzparser() + assert len(recorder) == 0 + + with pytest.warns(None) as recorder: + _timelex('2014-03-03') + + assert len(recorder) == 0 + + with pytest.warns(None) as recorder: + _parsetz('+05:00') + assert len(recorder) == 0 + + +@pytest.mark.tzstr +def test_tzstr_internal_timedeltas(): + with pytest.warns(tz.DeprecatedTzFormatWarning): + tz1 = tz.tzstr("EST5EDT,5,4,0,7200,11,-3,0,7200") + + with pytest.warns(tz.DeprecatedTzFormatWarning): + tz2 = tz.tzstr("EST5EDT,4,1,0,7200,10,-1,0,7200") + + assert tz1._start_delta != tz2._start_delta + assert tz1._end_delta != tz2._end_delta diff --git a/libs/dateutil/test/test_isoparser.py b/libs/dateutil/test/test_isoparser.py new file mode 100644 index 000000000..35899ab9b --- /dev/null +++ b/libs/dateutil/test/test_isoparser.py @@ -0,0 +1,509 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from datetime import datetime, timedelta, date, time +import itertools as it + +from dateutil import tz +from dateutil.tz import UTC +from dateutil.parser import isoparser, isoparse + +import pytest +import six + + +def _generate_tzoffsets(limited): + def _mkoffset(hmtuple, fmt): + h, m = hmtuple + m_td = (-1 if h < 0 else 1) * m + + tzo = tz.tzoffset(None, timedelta(hours=h, minutes=m_td)) + return tzo, fmt.format(h, m) + + out = [] + if not limited: + # The subset that's just hours + hm_out_h = [(h, 0) for h in (-23, -5, 0, 5, 23)] + out.extend([_mkoffset(hm, '{:+03d}') for hm in hm_out_h]) + + # Ones that have hours and minutes + hm_out = [] + hm_out_h + hm_out += [(-12, 15), (11, 30), (10, 2), (5, 15), (-5, 30)] + else: + hm_out = [(-5, -0)] + + fmts = ['{:+03d}:{:02d}', '{:+03d}{:02d}'] + out += [_mkoffset(hm, fmt) for hm in hm_out for fmt in fmts] + + # Also add in UTC and naive + out.append((UTC, 'Z')) + out.append((None, '')) + + return out + +FULL_TZOFFSETS = _generate_tzoffsets(False) +FULL_TZOFFSETS_AWARE = [x for x in FULL_TZOFFSETS if x[1]] +TZOFFSETS = _generate_tzoffsets(True) + +DATES = [datetime(1996, 1, 1), datetime(2017, 1, 1)] +@pytest.mark.parametrize('dt', tuple(DATES)) +def test_year_only(dt): + dtstr = dt.strftime('%Y') + + assert isoparse(dtstr) == dt + +DATES += [datetime(2000, 2, 1), datetime(2017, 4, 1)] +@pytest.mark.parametrize('dt', tuple(DATES)) +def test_year_month(dt): + fmt = '%Y-%m' + dtstr = dt.strftime(fmt) + + assert isoparse(dtstr) == dt + +DATES += [datetime(2016, 2, 29), datetime(2018, 3, 15)] +YMD_FMTS = ('%Y%m%d', '%Y-%m-%d') +@pytest.mark.parametrize('dt', tuple(DATES)) +@pytest.mark.parametrize('fmt', YMD_FMTS) +def test_year_month_day(dt, fmt): + dtstr = dt.strftime(fmt) + + assert isoparse(dtstr) == dt + +def _isoparse_date_and_time(dt, date_fmt, time_fmt, tzoffset, + microsecond_precision=None): + tzi, offset_str = tzoffset + fmt = date_fmt + 'T' + time_fmt + dt = dt.replace(tzinfo=tzi) + dtstr = dt.strftime(fmt) + + if microsecond_precision is not None: + if not fmt.endswith('%f'): # pragma: nocover + raise ValueError('Time format has no microseconds!') + + if microsecond_precision != 6: + dtstr = dtstr[:-(6 - microsecond_precision)] + elif microsecond_precision > 6: # pragma: nocover + raise ValueError('Precision must be 1-6') + + dtstr += offset_str + + assert isoparse(dtstr) == dt + +DATETIMES = [datetime(1998, 4, 16, 12), + datetime(2019, 11, 18, 23), + datetime(2014, 12, 16, 4)] +@pytest.mark.parametrize('dt', tuple(DATETIMES)) +@pytest.mark.parametrize('date_fmt', YMD_FMTS) +@pytest.mark.parametrize('tzoffset', TZOFFSETS) +def test_ymd_h(dt, date_fmt, tzoffset): + _isoparse_date_and_time(dt, date_fmt, '%H', tzoffset) + +DATETIMES = [datetime(2012, 1, 6, 9, 37)] +@pytest.mark.parametrize('dt', tuple(DATETIMES)) +@pytest.mark.parametrize('date_fmt', YMD_FMTS) +@pytest.mark.parametrize('time_fmt', ('%H%M', '%H:%M')) +@pytest.mark.parametrize('tzoffset', TZOFFSETS) +def test_ymd_hm(dt, date_fmt, time_fmt, tzoffset): + _isoparse_date_and_time(dt, date_fmt, time_fmt, tzoffset) + +DATETIMES = [datetime(2003, 9, 2, 22, 14, 2), + datetime(2003, 8, 8, 14, 9, 14), + datetime(2003, 4, 7, 6, 14, 59)] +HMS_FMTS = ('%H%M%S', '%H:%M:%S') +@pytest.mark.parametrize('dt', tuple(DATETIMES)) +@pytest.mark.parametrize('date_fmt', YMD_FMTS) +@pytest.mark.parametrize('time_fmt', HMS_FMTS) +@pytest.mark.parametrize('tzoffset', TZOFFSETS) +def test_ymd_hms(dt, date_fmt, time_fmt, tzoffset): + _isoparse_date_and_time(dt, date_fmt, time_fmt, tzoffset) + +DATETIMES = [datetime(2017, 11, 27, 6, 14, 30, 123456)] +@pytest.mark.parametrize('dt', tuple(DATETIMES)) +@pytest.mark.parametrize('date_fmt', YMD_FMTS) +@pytest.mark.parametrize('time_fmt', (x + sep + '%f' for x in HMS_FMTS + for sep in '.,')) +@pytest.mark.parametrize('tzoffset', TZOFFSETS) +@pytest.mark.parametrize('precision', list(range(3, 7))) +def test_ymd_hms_micro(dt, date_fmt, time_fmt, tzoffset, precision): + # Truncate the microseconds to the desired precision for the representation + dt = dt.replace(microsecond=int(round(dt.microsecond, precision-6))) + + _isoparse_date_and_time(dt, date_fmt, time_fmt, tzoffset, precision) + +### +# Truncation of extra digits beyond microsecond precision +@pytest.mark.parametrize('dt_str', [ + '2018-07-03T14:07:00.123456000001', + '2018-07-03T14:07:00.123456999999', +]) +def test_extra_subsecond_digits(dt_str): + assert isoparse(dt_str) == datetime(2018, 7, 3, 14, 7, 0, 123456) + +@pytest.mark.parametrize('tzoffset', FULL_TZOFFSETS) +def test_full_tzoffsets(tzoffset): + dt = datetime(2017, 11, 27, 6, 14, 30, 123456) + date_fmt = '%Y-%m-%d' + time_fmt = '%H:%M:%S.%f' + + _isoparse_date_and_time(dt, date_fmt, time_fmt, tzoffset) + +@pytest.mark.parametrize('dt_str', [ + '2014-04-11T00', + '2014-04-10T24', + '2014-04-11T00:00', + '2014-04-10T24:00', + '2014-04-11T00:00:00', + '2014-04-10T24:00:00', + '2014-04-11T00:00:00.000', + '2014-04-10T24:00:00.000', + '2014-04-11T00:00:00.000000', + '2014-04-10T24:00:00.000000'] +) +def test_datetime_midnight(dt_str): + assert isoparse(dt_str) == datetime(2014, 4, 11, 0, 0, 0, 0) + +@pytest.mark.parametrize('datestr', [ + '2014-01-01', + '20140101', +]) +@pytest.mark.parametrize('sep', [' ', 'a', 'T', '_', '-']) +def test_isoparse_sep_none(datestr, sep): + isostr = datestr + sep + '14:33:09' + assert isoparse(isostr) == datetime(2014, 1, 1, 14, 33, 9) + +## +# Uncommon date formats +TIME_ARGS = ('time_args', + ((None, time(0), None), ) + tuple(('%H:%M:%S.%f', _t, _tz) + for _t, _tz in it.product([time(0), time(9, 30), time(14, 47)], + TZOFFSETS))) + +@pytest.mark.parametrize('isocal,dt_expected',[ + ((2017, 10), datetime(2017, 3, 6)), + ((2020, 1), datetime(2019, 12, 30)), # ISO year != Cal year + ((2004, 53), datetime(2004, 12, 27)), # Only half the week is in 2014 +]) +def test_isoweek(isocal, dt_expected): + # TODO: Figure out how to parametrize this on formats, too + for fmt in ('{:04d}-W{:02d}', '{:04d}W{:02d}'): + dtstr = fmt.format(*isocal) + assert isoparse(dtstr) == dt_expected + +@pytest.mark.parametrize('isocal,dt_expected',[ + ((2016, 13, 7), datetime(2016, 4, 3)), + ((2004, 53, 7), datetime(2005, 1, 2)), # ISO year != Cal year + ((2009, 1, 2), datetime(2008, 12, 30)), # ISO year < Cal year + ((2009, 53, 6), datetime(2010, 1, 2)) # ISO year > Cal year +]) +def test_isoweek_day(isocal, dt_expected): + # TODO: Figure out how to parametrize this on formats, too + for fmt in ('{:04d}-W{:02d}-{:d}', '{:04d}W{:02d}{:d}'): + dtstr = fmt.format(*isocal) + assert isoparse(dtstr) == dt_expected + +@pytest.mark.parametrize('isoord,dt_expected', [ + ((2004, 1), datetime(2004, 1, 1)), + ((2016, 60), datetime(2016, 2, 29)), + ((2017, 60), datetime(2017, 3, 1)), + ((2016, 366), datetime(2016, 12, 31)), + ((2017, 365), datetime(2017, 12, 31)) +]) +def test_iso_ordinal(isoord, dt_expected): + for fmt in ('{:04d}-{:03d}', '{:04d}{:03d}'): + dtstr = fmt.format(*isoord) + + assert isoparse(dtstr) == dt_expected + + +### +# Acceptance of bytes +@pytest.mark.parametrize('isostr,dt', [ + (b'2014', datetime(2014, 1, 1)), + (b'20140204', datetime(2014, 2, 4)), + (b'2014-02-04', datetime(2014, 2, 4)), + (b'2014-02-04T12', datetime(2014, 2, 4, 12)), + (b'2014-02-04T12:30', datetime(2014, 2, 4, 12, 30)), + (b'2014-02-04T12:30:15', datetime(2014, 2, 4, 12, 30, 15)), + (b'2014-02-04T12:30:15.224', datetime(2014, 2, 4, 12, 30, 15, 224000)), + (b'20140204T123015.224', datetime(2014, 2, 4, 12, 30, 15, 224000)), + (b'2014-02-04T12:30:15.224Z', datetime(2014, 2, 4, 12, 30, 15, 224000, + UTC)), + (b'2014-02-04T12:30:15.224z', datetime(2014, 2, 4, 12, 30, 15, 224000, + UTC)), + (b'2014-02-04T12:30:15.224+05:00', + datetime(2014, 2, 4, 12, 30, 15, 224000, + tzinfo=tz.tzoffset(None, timedelta(hours=5))))]) +def test_bytes(isostr, dt): + assert isoparse(isostr) == dt + + +### +# Invalid ISO strings +@pytest.mark.parametrize('isostr,exception', [ + ('201', ValueError), # ISO string too short + ('2012-0425', ValueError), # Inconsistent date separators + ('201204-25', ValueError), # Inconsistent date separators + ('20120425T0120:00', ValueError), # Inconsistent time separators + ('20120425T01:2000', ValueError), # Inconsistent time separators + ('14:3015', ValueError), # Inconsistent time separator + ('20120425T012500-334', ValueError), # Wrong microsecond separator + ('2001-1', ValueError), # YYYY-M not valid + ('2012-04-9', ValueError), # YYYY-MM-D not valid + ('201204', ValueError), # YYYYMM not valid + ('20120411T03:30+', ValueError), # Time zone too short + ('20120411T03:30+1234567', ValueError), # Time zone too long + ('20120411T03:30-25:40', ValueError), # Time zone invalid + ('2012-1a', ValueError), # Invalid month + ('20120411T03:30+00:60', ValueError), # Time zone invalid minutes + ('20120411T03:30+00:61', ValueError), # Time zone invalid minutes + ('20120411T033030.123456012:00', # No sign in time zone + ValueError), + ('2012-W00', ValueError), # Invalid ISO week + ('2012-W55', ValueError), # Invalid ISO week + ('2012-W01-0', ValueError), # Invalid ISO week day + ('2012-W01-8', ValueError), # Invalid ISO week day + ('2013-000', ValueError), # Invalid ordinal day + ('2013-366', ValueError), # Invalid ordinal day + ('2013366', ValueError), # Invalid ordinal day + ('2014-03-12Т12:30:14', ValueError), # Cyrillic T + ('2014-04-21T24:00:01', ValueError), # Invalid use of 24 for midnight + ('2014_W01-1', ValueError), # Invalid separator + ('2014W01-1', ValueError), # Inconsistent use of dashes + ('2014-W011', ValueError), # Inconsistent use of dashes + +]) +def test_iso_raises(isostr, exception): + with pytest.raises(exception): + isoparse(isostr) + + +@pytest.mark.parametrize('sep_act, valid_sep, exception', [ + ('T', 'C', ValueError), + ('C', 'T', ValueError), +]) +def test_iso_with_sep_raises(sep_act, valid_sep, exception): + parser = isoparser(sep=valid_sep) + isostr = '2012-04-25' + sep_act + '01:25:00' + with pytest.raises(exception): + parser.isoparse(isostr) + + +### +# Test ISOParser constructor +@pytest.mark.parametrize('sep', [' ', '9', 'ðŸ›']) +def test_isoparser_invalid_sep(sep): + with pytest.raises(ValueError): + isoparser(sep=sep) + + +# This only fails on Python 3 +@pytest.mark.xfail(not six.PY2, reason="Fails on Python 3 only") +def test_isoparser_byte_sep(): + dt = datetime(2017, 12, 6, 12, 30, 45) + dt_str = dt.isoformat(sep=str('T')) + + dt_rt = isoparser(sep=b'T').isoparse(dt_str) + + assert dt == dt_rt + + +### +# Test parse_tzstr +@pytest.mark.parametrize('tzoffset', FULL_TZOFFSETS) +def test_parse_tzstr(tzoffset): + dt = datetime(2017, 11, 27, 6, 14, 30, 123456) + date_fmt = '%Y-%m-%d' + time_fmt = '%H:%M:%S.%f' + + _isoparse_date_and_time(dt, date_fmt, time_fmt, tzoffset) + + +@pytest.mark.parametrize('tzstr', [ + '-00:00', '+00:00', '+00', '-00', '+0000', '-0000' +]) +@pytest.mark.parametrize('zero_as_utc', [True, False]) +def test_parse_tzstr_zero_as_utc(tzstr, zero_as_utc): + tzi = isoparser().parse_tzstr(tzstr, zero_as_utc=zero_as_utc) + assert tzi == UTC + assert (type(tzi) == tz.tzutc) == zero_as_utc + + +@pytest.mark.parametrize('tzstr,exception', [ + ('00:00', ValueError), # No sign + ('05:00', ValueError), # No sign + ('_00:00', ValueError), # Invalid sign + ('+25:00', ValueError), # Offset too large + ('00:0000', ValueError), # String too long +]) +def test_parse_tzstr_fails(tzstr, exception): + with pytest.raises(exception): + isoparser().parse_tzstr(tzstr) + +### +# Test parse_isodate +def __make_date_examples(): + dates_no_day = [ + date(1999, 12, 1), + date(2016, 2, 1) + ] + + if not six.PY2: + # strftime does not support dates before 1900 in Python 2 + dates_no_day.append(date(1000, 11, 1)) + + # Only one supported format for dates with no day + o = zip(dates_no_day, it.repeat('%Y-%m')) + + dates_w_day = [ + date(1969, 12, 31), + date(1900, 1, 1), + date(2016, 2, 29), + date(2017, 11, 14) + ] + + dates_w_day_fmts = ('%Y%m%d', '%Y-%m-%d') + o = it.chain(o, it.product(dates_w_day, dates_w_day_fmts)) + + return list(o) + + +@pytest.mark.parametrize('d,dt_fmt', __make_date_examples()) +@pytest.mark.parametrize('as_bytes', [True, False]) +def test_parse_isodate(d, dt_fmt, as_bytes): + d_str = d.strftime(dt_fmt) + if isinstance(d_str, six.text_type) and as_bytes: + d_str = d_str.encode('ascii') + elif isinstance(d_str, bytes) and not as_bytes: + d_str = d_str.decode('ascii') + + iparser = isoparser() + assert iparser.parse_isodate(d_str) == d + + +@pytest.mark.parametrize('isostr,exception', [ + ('243', ValueError), # ISO string too short + ('2014-0423', ValueError), # Inconsistent date separators + ('201404-23', ValueError), # Inconsistent date separators + ('2014æ—¥03月14', ValueError), # Not ASCII + ('2013-02-29', ValueError), # Not a leap year + ('2014/12/03', ValueError), # Wrong separators + ('2014-04-19T', ValueError), # Unknown components + ('201202', ValueError), # Invalid format +]) +def test_isodate_raises(isostr, exception): + with pytest.raises(exception): + isoparser().parse_isodate(isostr) + + +def test_parse_isodate_error_text(): + with pytest.raises(ValueError) as excinfo: + isoparser().parse_isodate('2014-0423') + + # ensure the error message does not contain b' prefixes + if six.PY2: + expected_error = "String contains unknown ISO components: u'2014-0423'" + else: + expected_error = "String contains unknown ISO components: '2014-0423'" + assert expected_error == str(excinfo.value) + + +### +# Test parse_isotime +def __make_time_examples(): + outputs = [] + + # HH + time_h = [time(0), time(8), time(22)] + time_h_fmts = ['%H'] + + outputs.append(it.product(time_h, time_h_fmts)) + + # HHMM / HH:MM + time_hm = [time(0, 0), time(0, 30), time(8, 47), time(16, 1)] + time_hm_fmts = ['%H%M', '%H:%M'] + + outputs.append(it.product(time_hm, time_hm_fmts)) + + # HHMMSS / HH:MM:SS + time_hms = [time(0, 0, 0), time(0, 15, 30), + time(8, 2, 16), time(12, 0), time(16, 2), time(20, 45)] + + time_hms_fmts = ['%H%M%S', '%H:%M:%S'] + + outputs.append(it.product(time_hms, time_hms_fmts)) + + # HHMMSS.ffffff / HH:MM:SS.ffffff + time_hmsu = [time(0, 0, 0, 0), time(4, 15, 3, 247993), + time(14, 21, 59, 948730), + time(23, 59, 59, 999999)] + + time_hmsu_fmts = ['%H%M%S.%f', '%H:%M:%S.%f'] + + outputs.append(it.product(time_hmsu, time_hmsu_fmts)) + + outputs = list(map(list, outputs)) + + # Time zones + ex_naive = list(it.chain.from_iterable(x[0:2] for x in outputs)) + o = it.product(ex_naive, TZOFFSETS) # ((time, fmt), (tzinfo, offsetstr)) + o = ((t.replace(tzinfo=tzi), fmt + off_str) + for (t, fmt), (tzi, off_str) in o) + + outputs.append(o) + + return list(it.chain.from_iterable(outputs)) + + +@pytest.mark.parametrize('time_val,time_fmt', __make_time_examples()) +@pytest.mark.parametrize('as_bytes', [True, False]) +def test_isotime(time_val, time_fmt, as_bytes): + tstr = time_val.strftime(time_fmt) + if isinstance(tstr, six.text_type) and as_bytes: + tstr = tstr.encode('ascii') + elif isinstance(tstr, bytes) and not as_bytes: + tstr = tstr.decode('ascii') + + iparser = isoparser() + + assert iparser.parse_isotime(tstr) == time_val + + +@pytest.mark.parametrize('isostr', [ + '24:00', + '2400', + '24:00:00', + '240000', + '24:00:00.000', + '24:00:00,000', + '24:00:00.000000', + '24:00:00,000000', +]) +def test_isotime_midnight(isostr): + iparser = isoparser() + assert iparser.parse_isotime(isostr) == time(0, 0, 0, 0) + + +@pytest.mark.parametrize('isostr,exception', [ + ('3', ValueError), # ISO string too short + ('14時30分15秒', ValueError), # Not ASCII + ('14_30_15', ValueError), # Invalid separators + ('1430:15', ValueError), # Inconsistent separator use + ('25', ValueError), # Invalid hours + ('25:15', ValueError), # Invalid hours + ('14:60', ValueError), # Invalid minutes + ('14:59:61', ValueError), # Invalid seconds + ('14:30:15.34468305:00', ValueError), # No sign in time zone + ('14:30:15+', ValueError), # Time zone too short + ('14:30:15+1234567', ValueError), # Time zone invalid + ('14:59:59+25:00', ValueError), # Invalid tz hours + ('14:59:59+12:62', ValueError), # Invalid tz minutes + ('14:59:30_344583', ValueError), # Invalid microsecond separator + ('24:01', ValueError), # 24 used for non-midnight time + ('24:00:01', ValueError), # 24 used for non-midnight time + ('24:00:00.001', ValueError), # 24 used for non-midnight time + ('24:00:00.000001', ValueError), # 24 used for non-midnight time +]) +def test_isotime_raises(isostr, exception): + iparser = isoparser() + with pytest.raises(exception): + iparser.parse_isotime(isostr) diff --git a/libs/dateutil/test/test_parser.py b/libs/dateutil/test/test_parser.py index 1115bbf65..08a34dafb 100644 --- a/libs/dateutil/test/test_parser.py +++ b/libs/dateutil/test/test_parser.py @@ -1,46 +1,310 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals -from ._common import unittest -from datetime import datetime, timedelta, date +import itertools +from datetime import datetime, timedelta +import unittest +import sys +from dateutil import tz from dateutil.tz import tzoffset -from dateutil.parser import * +from dateutil.parser import parse, parserinfo +from dateutil.parser import ParserError +from dateutil.parser import UnknownTimezoneWarning -import six -from six import assertRaisesRegex, PY3 -from six.moves import StringIO +from ._common import TZEnvContext -class ParserTest(unittest.TestCase): +from six import assertRaisesRegex, PY2 +from io import StringIO - def setUp(self): - self.tzinfos = {"BRST": -10800} - self.brsttz = tzoffset("BRST", -10800) - self.default = datetime(2003, 9, 25) +import pytest - # Parser should be able to handle bytestring and unicode - base_str = '2014-05-01 08:00:00' - try: - # Python 2.x - self.uni_str = unicode(base_str) - self.str_str = str(base_str) - except NameError: - self.uni_str = str(base_str) - self.str_str = bytes(base_str.encode()) +# Platform info +IS_WIN = sys.platform.startswith('win') - def testEmptyString(self): - with self.assertRaises(ValueError): +PLATFORM_HAS_DASH_D = False +try: + if datetime.now().strftime('%-d'): + PLATFORM_HAS_DASH_D = True +except ValueError: + pass + + +@pytest.fixture(params=[True, False]) +def fuzzy(request): + """Fixture to pass fuzzy=True or fuzzy=False to parse""" + return request.param + + +# Parser test cases using no keyword arguments. Format: (parsable_text, expected_datetime, assertion_message) +PARSER_TEST_CASES = [ + ("Thu Sep 25 10:36:28 2003", datetime(2003, 9, 25, 10, 36, 28), "date command format strip"), + ("Thu Sep 25 2003", datetime(2003, 9, 25), "date command format strip"), + ("2003-09-25T10:49:41", datetime(2003, 9, 25, 10, 49, 41), "iso format strip"), + ("2003-09-25T10:49", datetime(2003, 9, 25, 10, 49), "iso format strip"), + ("2003-09-25T10", datetime(2003, 9, 25, 10), "iso format strip"), + ("2003-09-25", datetime(2003, 9, 25), "iso format strip"), + ("20030925T104941", datetime(2003, 9, 25, 10, 49, 41), "iso stripped format strip"), + ("20030925T1049", datetime(2003, 9, 25, 10, 49, 0), "iso stripped format strip"), + ("20030925T10", datetime(2003, 9, 25, 10), "iso stripped format strip"), + ("20030925", datetime(2003, 9, 25), "iso stripped format strip"), + ("2003-09-25 10:49:41,502", datetime(2003, 9, 25, 10, 49, 41, 502000), "python logger format"), + ("199709020908", datetime(1997, 9, 2, 9, 8), "no separator"), + ("19970902090807", datetime(1997, 9, 2, 9, 8, 7), "no separator"), + ("09-25-2003", datetime(2003, 9, 25), "date with dash"), + ("25-09-2003", datetime(2003, 9, 25), "date with dash"), + ("10-09-2003", datetime(2003, 10, 9), "date with dash"), + ("10-09-03", datetime(2003, 10, 9), "date with dash"), + ("2003.09.25", datetime(2003, 9, 25), "date with dot"), + ("09.25.2003", datetime(2003, 9, 25), "date with dot"), + ("25.09.2003", datetime(2003, 9, 25), "date with dot"), + ("10.09.2003", datetime(2003, 10, 9), "date with dot"), + ("10.09.03", datetime(2003, 10, 9), "date with dot"), + ("2003/09/25", datetime(2003, 9, 25), "date with slash"), + ("09/25/2003", datetime(2003, 9, 25), "date with slash"), + ("25/09/2003", datetime(2003, 9, 25), "date with slash"), + ("10/09/2003", datetime(2003, 10, 9), "date with slash"), + ("10/09/03", datetime(2003, 10, 9), "date with slash"), + ("2003 09 25", datetime(2003, 9, 25), "date with space"), + ("09 25 2003", datetime(2003, 9, 25), "date with space"), + ("25 09 2003", datetime(2003, 9, 25), "date with space"), + ("10 09 2003", datetime(2003, 10, 9), "date with space"), + ("10 09 03", datetime(2003, 10, 9), "date with space"), + ("25 09 03", datetime(2003, 9, 25), "date with space"), + ("03 25 Sep", datetime(2003, 9, 25), "strangely ordered date"), + ("25 03 Sep", datetime(2025, 9, 3), "strangely ordered date"), + (" July 4 , 1976 12:01:02 am ", datetime(1976, 7, 4, 0, 1, 2), "extra space"), + ("Wed, July 10, '96", datetime(1996, 7, 10, 0, 0), "random format"), + ("1996.July.10 AD 12:08 PM", datetime(1996, 7, 10, 12, 8), "random format"), + ("July 4, 1976", datetime(1976, 7, 4), "random format"), + ("7 4 1976", datetime(1976, 7, 4), "random format"), + ("4 jul 1976", datetime(1976, 7, 4), "random format"), + ("4 Jul 1976", datetime(1976, 7, 4), "'%-d %b %Y' format"), + ("7-4-76", datetime(1976, 7, 4), "random format"), + ("19760704", datetime(1976, 7, 4), "random format"), + ("0:01:02 on July 4, 1976", datetime(1976, 7, 4, 0, 1, 2), "random format"), + ("July 4, 1976 12:01:02 am", datetime(1976, 7, 4, 0, 1, 2), "random format"), + ("Mon Jan 2 04:24:27 1995", datetime(1995, 1, 2, 4, 24, 27), "random format"), + ("04.04.95 00:22", datetime(1995, 4, 4, 0, 22), "random format"), + ("Jan 1 1999 11:23:34.578", datetime(1999, 1, 1, 11, 23, 34, 578000), "random format"), + ("950404 122212", datetime(1995, 4, 4, 12, 22, 12), "random format"), + ("3rd of May 2001", datetime(2001, 5, 3), "random format"), + ("5th of March 2001", datetime(2001, 3, 5), "random format"), + ("1st of May 2003", datetime(2003, 5, 1), "random format"), + ('0099-01-01T00:00:00', datetime(99, 1, 1, 0, 0), "99 ad"), + ('0031-01-01T00:00:00', datetime(31, 1, 1, 0, 0), "31 ad"), + ("20080227T21:26:01.123456789", datetime(2008, 2, 27, 21, 26, 1, 123456), "high precision seconds"), + ('13NOV2017', datetime(2017, 11, 13), "dBY (See GH360)"), + ('0003-03-04', datetime(3, 3, 4), "pre 12 year same month (See GH PR #293)"), + ('December.0031.30', datetime(31, 12, 30), "BYd corner case (GH#687)"), + + # Cases with legacy h/m/s format, candidates for deprecation (GH#886) + ("2016-12-21 04.2h", datetime(2016, 12, 21, 4, 12), "Fractional Hours"), +] +# Check that we don't have any duplicates +assert len(set([x[0] for x in PARSER_TEST_CASES])) == len(PARSER_TEST_CASES) + + +@pytest.mark.parametrize("parsable_text,expected_datetime,assertion_message", PARSER_TEST_CASES) +def test_parser(parsable_text, expected_datetime, assertion_message): + assert parse(parsable_text) == expected_datetime, assertion_message + + +# Parser test cases using datetime(2003, 9, 25) as a default. +# Format: (parsable_text, expected_datetime, assertion_message) +PARSER_DEFAULT_TEST_CASES = [ + ("Thu Sep 25 10:36:28", datetime(2003, 9, 25, 10, 36, 28), "date command format strip"), + ("Thu Sep 10:36:28", datetime(2003, 9, 25, 10, 36, 28), "date command format strip"), + ("Thu 10:36:28", datetime(2003, 9, 25, 10, 36, 28), "date command format strip"), + ("Sep 10:36:28", datetime(2003, 9, 25, 10, 36, 28), "date command format strip"), + ("10:36:28", datetime(2003, 9, 25, 10, 36, 28), "date command format strip"), + ("10:36", datetime(2003, 9, 25, 10, 36), "date command format strip"), + ("Sep 2003", datetime(2003, 9, 25), "date command format strip"), + ("Sep", datetime(2003, 9, 25), "date command format strip"), + ("2003", datetime(2003, 9, 25), "date command format strip"), + ("10h36m28.5s", datetime(2003, 9, 25, 10, 36, 28, 500000), "hour with letters"), + ("10h36m28s", datetime(2003, 9, 25, 10, 36, 28), "hour with letters strip"), + ("10h36m", datetime(2003, 9, 25, 10, 36), "hour with letters strip"), + ("10h", datetime(2003, 9, 25, 10), "hour with letters strip"), + ("10 h 36", datetime(2003, 9, 25, 10, 36), "hour with letters strip"), + ("10 h 36.5", datetime(2003, 9, 25, 10, 36, 30), "hour with letter strip"), + ("36 m 5", datetime(2003, 9, 25, 0, 36, 5), "hour with letters spaces"), + ("36 m 5 s", datetime(2003, 9, 25, 0, 36, 5), "minute with letters spaces"), + ("36 m 05", datetime(2003, 9, 25, 0, 36, 5), "minute with letters spaces"), + ("36 m 05 s", datetime(2003, 9, 25, 0, 36, 5), "minutes with letters spaces"), + ("10h am", datetime(2003, 9, 25, 10), "hour am pm"), + ("10h pm", datetime(2003, 9, 25, 22), "hour am pm"), + ("10am", datetime(2003, 9, 25, 10), "hour am pm"), + ("10pm", datetime(2003, 9, 25, 22), "hour am pm"), + ("10:00 am", datetime(2003, 9, 25, 10), "hour am pm"), + ("10:00 pm", datetime(2003, 9, 25, 22), "hour am pm"), + ("10:00am", datetime(2003, 9, 25, 10), "hour am pm"), + ("10:00pm", datetime(2003, 9, 25, 22), "hour am pm"), + ("10:00a.m", datetime(2003, 9, 25, 10), "hour am pm"), + ("10:00p.m", datetime(2003, 9, 25, 22), "hour am pm"), + ("10:00a.m.", datetime(2003, 9, 25, 10), "hour am pm"), + ("10:00p.m.", datetime(2003, 9, 25, 22), "hour am pm"), + ("Wed", datetime(2003, 10, 1), "weekday alone"), + ("Wednesday", datetime(2003, 10, 1), "long weekday"), + ("October", datetime(2003, 10, 25), "long month"), + ("31-Dec-00", datetime(2000, 12, 31), "zero year"), + ("0:01:02", datetime(2003, 9, 25, 0, 1, 2), "random format"), + ("12h 01m02s am", datetime(2003, 9, 25, 0, 1, 2), "random format"), + ("12:08 PM", datetime(2003, 9, 25, 12, 8), "random format"), + ("01h02m03", datetime(2003, 9, 25, 1, 2, 3), "random format"), + ("01h02", datetime(2003, 9, 25, 1, 2), "random format"), + ("01h02s", datetime(2003, 9, 25, 1, 0, 2), "random format"), + ("01m02", datetime(2003, 9, 25, 0, 1, 2), "random format"), + ("01m02h", datetime(2003, 9, 25, 2, 1), "random format"), + ("2004 10 Apr 11h30m", datetime(2004, 4, 10, 11, 30), "random format") +] +# Check that we don't have any duplicates +assert len(set([x[0] for x in PARSER_DEFAULT_TEST_CASES])) == len(PARSER_DEFAULT_TEST_CASES) + + +@pytest.mark.parametrize("parsable_text,expected_datetime,assertion_message", PARSER_DEFAULT_TEST_CASES) +def test_parser_default(parsable_text, expected_datetime, assertion_message): + assert parse(parsable_text, default=datetime(2003, 9, 25)) == expected_datetime, assertion_message + + +@pytest.mark.parametrize('sep', ['-', '.', '/', ' ']) +def test_parse_dayfirst(sep): + expected = datetime(2003, 9, 10) + fmt = sep.join(['%d', '%m', '%Y']) + dstr = expected.strftime(fmt) + result = parse(dstr, dayfirst=True) + assert result == expected + + +@pytest.mark.parametrize('sep', ['-', '.', '/', ' ']) +def test_parse_yearfirst(sep): + expected = datetime(2010, 9, 3) + fmt = sep.join(['%Y', '%m', '%d']) + dstr = expected.strftime(fmt) + result = parse(dstr, yearfirst=True) + assert result == expected + + +@pytest.mark.parametrize('dstr,expected', [ + ("Thu Sep 25 10:36:28 BRST 2003", datetime(2003, 9, 25, 10, 36, 28)), + ("1996.07.10 AD at 15:08:56 PDT", datetime(1996, 7, 10, 15, 8, 56)), + ("Tuesday, April 12, 1952 AD 3:30:42pm PST", + datetime(1952, 4, 12, 15, 30, 42)), + ("November 5, 1994, 8:15:30 am EST", datetime(1994, 11, 5, 8, 15, 30)), + ("1994-11-05T08:15:30-05:00", datetime(1994, 11, 5, 8, 15, 30)), + ("1994-11-05T08:15:30Z", datetime(1994, 11, 5, 8, 15, 30)), + ("1976-07-04T00:01:02Z", datetime(1976, 7, 4, 0, 1, 2)), + ("1986-07-05T08:15:30z", datetime(1986, 7, 5, 8, 15, 30)), + ("Tue Apr 4 00:22:12 PDT 1995", datetime(1995, 4, 4, 0, 22, 12)), +]) +def test_parse_ignoretz(dstr, expected): + result = parse(dstr, ignoretz=True) + assert result == expected + + +_brsttz = tzoffset("BRST", -10800) + + +@pytest.mark.parametrize('dstr,expected', [ + ("20030925T104941-0300", + datetime(2003, 9, 25, 10, 49, 41, tzinfo=_brsttz)), + ("Thu, 25 Sep 2003 10:49:41 -0300", + datetime(2003, 9, 25, 10, 49, 41, tzinfo=_brsttz)), + ("2003-09-25T10:49:41.5-03:00", + datetime(2003, 9, 25, 10, 49, 41, 500000, tzinfo=_brsttz)), + ("2003-09-25T10:49:41-03:00", + datetime(2003, 9, 25, 10, 49, 41, tzinfo=_brsttz)), + ("20030925T104941.5-0300", + datetime(2003, 9, 25, 10, 49, 41, 500000, tzinfo=_brsttz)), +]) +def test_parse_with_tzoffset(dstr, expected): + # In these cases, we are _not_ passing a tzinfos arg + result = parse(dstr) + assert result == expected + + +class TestFormat(object): + + def test_ybd(self): + # If we have a 4-digit year, a non-numeric month (abbreviated or not), + # and a day (1 or 2 digits), then there is no ambiguity as to which + # token is a year/month/day. This holds regardless of what order the + # terms are in and for each of the separators below. + + seps = ['-', ' ', '/', '.'] + + year_tokens = ['%Y'] + month_tokens = ['%b', '%B'] + day_tokens = ['%d'] + if PLATFORM_HAS_DASH_D: + day_tokens.append('%-d') + + prods = itertools.product(year_tokens, month_tokens, day_tokens) + perms = [y for x in prods for y in itertools.permutations(x)] + unambig_fmts = [sep.join(perm) for sep in seps for perm in perms] + + actual = datetime(2003, 9, 25) + + for fmt in unambig_fmts: + dstr = actual.strftime(fmt) + res = parse(dstr) + assert res == actual + + # TODO: some redundancy with PARSER_TEST_CASES cases + @pytest.mark.parametrize("fmt,dstr", [ + ("%a %b %d %Y", "Thu Sep 25 2003"), + ("%b %d %Y", "Sep 25 2003"), + ("%Y-%m-%d", "2003-09-25"), + ("%Y%m%d", "20030925"), + ("%Y-%b-%d", "2003-Sep-25"), + ("%d-%b-%Y", "25-Sep-2003"), + ("%b-%d-%Y", "Sep-25-2003"), + ("%m-%d-%Y", "09-25-2003"), + ("%d-%m-%Y", "25-09-2003"), + ("%Y.%m.%d", "2003.09.25"), + ("%Y.%b.%d", "2003.Sep.25"), + ("%d.%b.%Y", "25.Sep.2003"), + ("%b.%d.%Y", "Sep.25.2003"), + ("%m.%d.%Y", "09.25.2003"), + ("%d.%m.%Y", "25.09.2003"), + ("%Y/%m/%d", "2003/09/25"), + ("%Y/%b/%d", "2003/Sep/25"), + ("%d/%b/%Y", "25/Sep/2003"), + ("%b/%d/%Y", "Sep/25/2003"), + ("%m/%d/%Y", "09/25/2003"), + ("%d/%m/%Y", "25/09/2003"), + ("%Y %m %d", "2003 09 25"), + ("%Y %b %d", "2003 Sep 25"), + ("%d %b %Y", "25 Sep 2003"), + ("%m %d %Y", "09 25 2003"), + ("%d %m %Y", "25 09 2003"), + ("%y %d %b", "03 25 Sep",), + ]) + def test_strftime_formats_2003Sep25(self, fmt, dstr): + expected = datetime(2003, 9, 25) + + # First check that the format strings behave as expected + # (not strictly necessary, but nice to have) + assert expected.strftime(fmt) == dstr + + res = parse(dstr) + assert res == expected + + +class TestInputTypes(object): + def test_empty_string_invalid(self): + with pytest.raises(ParserError): parse('') - def testNone(self): - with self.assertRaises(TypeError): + def test_none_invalid(self): + with pytest.raises(TypeError): parse(None) - def testInvalidType(self): - with self.assertRaises(TypeError): + def test_int_invalid(self): + with pytest.raises(TypeError): parse(13) - def testDuckTyping(self): + def test_duck_typing(self): # We want to support arbitrary classes that implement the stream # interface. @@ -51,25 +315,112 @@ class ParserTest(unittest.TestCase): def read(self, *args, **kwargs): return self.stream.read(*args, **kwargs) - dstr = StringPassThrough(StringIO('2014 January 19')) - self.assertEqual(parse(dstr), datetime(2014, 1, 19)) + res = parse(dstr) + expected = datetime(2014, 1, 19) + assert res == expected - def testParseStream(self): + def test_parse_stream(self): dstr = StringIO('2014 January 19') - self.assertEqual(parse(dstr), datetime(2014, 1, 19)) + res = parse(dstr) + expected = datetime(2014, 1, 19) + assert res == expected - def testParseStr(self): - self.assertEqual(parse(self.str_str), - parse(self.uni_str)) + def test_parse_str(self): + # Parser should be able to handle bytestring and unicode + uni_str = '2014-05-01 08:00:00' + bytes_str = uni_str.encode() + + res = parse(bytes_str) + expected = parse(uni_str) + assert res == expected + + def test_parse_bytes(self): + res = parse(b'2014 January 19') + expected = datetime(2014, 1, 19) + assert res == expected + + def test_parse_bytearray(self): + # GH#417 + res = parse(bytearray(b'2014 January 19')) + expected = datetime(2014, 1, 19) + assert res == expected + + +class TestTzinfoInputTypes(object): + def assert_equal_same_tz(self, dt1, dt2): + assert dt1 == dt2 + assert dt1.tzinfo is dt2.tzinfo + + def test_tzinfo_dict_could_return_none(self): + dstr = "2017-02-03 12:40 BRST" + result = parse(dstr, tzinfos={"BRST": None}) + expected = datetime(2017, 2, 3, 12, 40) + self.assert_equal_same_tz(result, expected) + + def test_tzinfos_callable_could_return_none(self): + dstr = "2017-02-03 12:40 BRST" + result = parse(dstr, tzinfos=lambda *args: None) + expected = datetime(2017, 2, 3, 12, 40) + self.assert_equal_same_tz(result, expected) + + def test_invalid_tzinfo_input(self): + dstr = "2014 January 19 09:00 UTC" + # Pass an absurd tzinfos object + tzinfos = {"UTC": ValueError} + with pytest.raises(TypeError): + parse(dstr, tzinfos=tzinfos) + + def test_valid_tzinfo_tzinfo_input(self): + dstr = "2014 January 19 09:00 UTC" + tzinfos = {"UTC": tz.UTC} + expected = datetime(2014, 1, 19, 9, tzinfo=tz.UTC) + res = parse(dstr, tzinfos=tzinfos) + self.assert_equal_same_tz(res, expected) + + def test_valid_tzinfo_unicode_input(self): + dstr = "2014 January 19 09:00 UTC" + tzinfos = {u"UTC": u"UTC+0"} + expected = datetime(2014, 1, 19, 9, tzinfo=tz.tzstr("UTC+0")) + res = parse(dstr, tzinfos=tzinfos) + self.assert_equal_same_tz(res, expected) + + def test_valid_tzinfo_callable_input(self): + dstr = "2014 January 19 09:00 UTC" + + def tzinfos(*args, **kwargs): + return u"UTC+0" + + expected = datetime(2014, 1, 19, 9, tzinfo=tz.tzstr("UTC+0")) + res = parse(dstr, tzinfos=tzinfos) + self.assert_equal_same_tz(res, expected) + + def test_valid_tzinfo_int_input(self): + dstr = "2014 January 19 09:00 UTC" + tzinfos = {u"UTC": -28800} + expected = datetime(2014, 1, 19, 9, tzinfo=tz.tzoffset(u"UTC", -28800)) + res = parse(dstr, tzinfos=tzinfos) + self.assert_equal_same_tz(res, expected) + + +class ParserTest(unittest.TestCase): + + @classmethod + def setup_class(cls): + cls.tzinfos = {"BRST": -10800} + cls.brsttz = tzoffset("BRST", -10800) + cls.default = datetime(2003, 9, 25) + + # Parser should be able to handle bytestring and unicode + cls.uni_str = '2014-05-01 08:00:00' + cls.str_str = cls.uni_str.encode() def testParserParseStr(self): from dateutil.parser import parser - self.assertEqual(parser().parse(self.str_str), - parser().parse(self.uni_str)) + assert parser().parse(self.str_str) == parser().parse(self.uni_str) def testParseUnicodeWords(self): @@ -87,9 +438,9 @@ class ParserTest(unittest.TestCase): ("ноÑ", "ÐоÑбрь"), ("дек", "Декабрь")] - self.assertEqual(parse('10 СентÑбрь 2015 10:20', - parserinfo=rus_parserinfo()), - datetime(2015, 9, 10, 10, 20)) + expected = datetime(2015, 9, 10, 10, 20) + res = parse('10 СентÑбрь 2015 10:20', parserinfo=rus_parserinfo()) + assert res == expected def testParseWithNulls(self): # This relies on the from __future__ import unicode_literals, because @@ -97,8 +448,7 @@ class ParserTest(unittest.TestCase): # May want to switch to u'...' if we ever drop Python 3.2 support. pstring = '\x00\x00August 29, 1924' - self.assertEqual(parse(pstring), - datetime(1924, 8, 29)) + assert parse(pstring) == datetime(1924, 8, 29) def testDateCommandFormat(self): self.assertEqual(parse("Thu Sep 25 10:36:28 BRST 2003", @@ -106,13 +456,6 @@ class ParserTest(unittest.TestCase): datetime(2003, 9, 25, 10, 36, 28, tzinfo=self.brsttz)) - def testDateCommandFormatUnicode(self): - self.assertEqual(parse("Thu Sep 25 10:36:28 BRST 2003", - tzinfos=self.tzinfos), - datetime(2003, 9, 25, 10, 36, 28, - tzinfo=self.brsttz)) - - def testDateCommandFormatReversed(self): self.assertEqual(parse("2003 10:36:28 BRST 25 Sep Thu", tzinfos=self.tzinfos), @@ -120,405 +463,34 @@ class ParserTest(unittest.TestCase): tzinfo=self.brsttz)) def testDateCommandFormatWithLong(self): - if not PY3: + if PY2: self.assertEqual(parse("Thu Sep 25 10:36:28 BRST 2003", tzinfos={"BRST": long(-10800)}), datetime(2003, 9, 25, 10, 36, 28, tzinfo=self.brsttz)) - def testDateCommandFormatIgnoreTz(self): - self.assertEqual(parse("Thu Sep 25 10:36:28 BRST 2003", - ignoretz=True), - datetime(2003, 9, 25, 10, 36, 28)) - - def testDateCommandFormatStrip1(self): - self.assertEqual(parse("Thu Sep 25 10:36:28 2003"), - datetime(2003, 9, 25, 10, 36, 28)) - - def testDateCommandFormatStrip2(self): - self.assertEqual(parse("Thu Sep 25 10:36:28", default=self.default), - datetime(2003, 9, 25, 10, 36, 28)) - - def testDateCommandFormatStrip3(self): - self.assertEqual(parse("Thu Sep 10:36:28", default=self.default), - datetime(2003, 9, 25, 10, 36, 28)) - - def testDateCommandFormatStrip4(self): - self.assertEqual(parse("Thu 10:36:28", default=self.default), - datetime(2003, 9, 25, 10, 36, 28)) - - def testDateCommandFormatStrip5(self): - self.assertEqual(parse("Sep 10:36:28", default=self.default), - datetime(2003, 9, 25, 10, 36, 28)) - - def testDateCommandFormatStrip6(self): - self.assertEqual(parse("10:36:28", default=self.default), - datetime(2003, 9, 25, 10, 36, 28)) - - def testDateCommandFormatStrip7(self): - self.assertEqual(parse("10:36", default=self.default), - datetime(2003, 9, 25, 10, 36)) - - def testDateCommandFormatStrip8(self): - self.assertEqual(parse("Thu Sep 25 2003"), - datetime(2003, 9, 25)) - - def testDateCommandFormatStrip9(self): - self.assertEqual(parse("Sep 25 2003"), - datetime(2003, 9, 25)) - - def testDateCommandFormatStrip10(self): - self.assertEqual(parse("Sep 2003", default=self.default), - datetime(2003, 9, 25)) - - def testDateCommandFormatStrip11(self): - self.assertEqual(parse("Sep", default=self.default), - datetime(2003, 9, 25)) - - def testDateCommandFormatStrip12(self): - self.assertEqual(parse("2003", default=self.default), - datetime(2003, 9, 25)) - - def testDateRCommandFormat(self): - self.assertEqual(parse("Thu, 25 Sep 2003 10:49:41 -0300"), - datetime(2003, 9, 25, 10, 49, 41, - tzinfo=self.brsttz)) - - def testISOFormat(self): - self.assertEqual(parse("2003-09-25T10:49:41.5-03:00"), - datetime(2003, 9, 25, 10, 49, 41, 500000, - tzinfo=self.brsttz)) - - def testISOFormatStrip1(self): - self.assertEqual(parse("2003-09-25T10:49:41-03:00"), - datetime(2003, 9, 25, 10, 49, 41, - tzinfo=self.brsttz)) def testISOFormatStrip2(self): - self.assertEqual(parse("2003-09-25T10:49:41"), - datetime(2003, 9, 25, 10, 49, 41)) - - def testISOFormatStrip3(self): - self.assertEqual(parse("2003-09-25T10:49"), - datetime(2003, 9, 25, 10, 49)) - - def testISOFormatStrip4(self): - self.assertEqual(parse("2003-09-25T10"), - datetime(2003, 9, 25, 10)) - - def testISOFormatStrip5(self): - self.assertEqual(parse("2003-09-25"), - datetime(2003, 9, 25)) - - def testISOStrippedFormat(self): - self.assertEqual(parse("20030925T104941.5-0300"), - datetime(2003, 9, 25, 10, 49, 41, 500000, - tzinfo=self.brsttz)) - - def testISOStrippedFormatStrip1(self): - self.assertEqual(parse("20030925T104941-0300"), + self.assertEqual(parse("2003-09-25T10:49:41+03:00"), datetime(2003, 9, 25, 10, 49, 41, - tzinfo=self.brsttz)) + tzinfo=tzoffset(None, 10800))) def testISOStrippedFormatStrip2(self): - self.assertEqual(parse("20030925T104941"), - datetime(2003, 9, 25, 10, 49, 41)) - - def testISOStrippedFormatStrip3(self): - self.assertEqual(parse("20030925T1049"), - datetime(2003, 9, 25, 10, 49, 0)) - - def testISOStrippedFormatStrip4(self): - self.assertEqual(parse("20030925T10"), - datetime(2003, 9, 25, 10)) - - def testISOStrippedFormatStrip5(self): - self.assertEqual(parse("20030925"), - datetime(2003, 9, 25)) - - def testPythonLoggerFormat(self): - self.assertEqual(parse("2003-09-25 10:49:41,502"), - datetime(2003, 9, 25, 10, 49, 41, 502000)) - - def testNoSeparator1(self): - self.assertEqual(parse("199709020908"), - datetime(1997, 9, 2, 9, 8)) - - def testNoSeparator2(self): - self.assertEqual(parse("19970902090807"), - datetime(1997, 9, 2, 9, 8, 7)) - - def testDateWithDash1(self): - self.assertEqual(parse("2003-09-25"), - datetime(2003, 9, 25)) - - def testDateWithDash2(self): - self.assertEqual(parse("2003-Sep-25"), - datetime(2003, 9, 25)) - - def testDateWithDash3(self): - self.assertEqual(parse("25-Sep-2003"), - datetime(2003, 9, 25)) - - def testDateWithDash4(self): - self.assertEqual(parse("25-Sep-2003"), - datetime(2003, 9, 25)) - - def testDateWithDash5(self): - self.assertEqual(parse("Sep-25-2003"), - datetime(2003, 9, 25)) - - def testDateWithDash6(self): - self.assertEqual(parse("09-25-2003"), - datetime(2003, 9, 25)) - - def testDateWithDash7(self): - self.assertEqual(parse("25-09-2003"), - datetime(2003, 9, 25)) - - def testDateWithDash8(self): - self.assertEqual(parse("10-09-2003", dayfirst=True), - datetime(2003, 9, 10)) - - def testDateWithDash9(self): - self.assertEqual(parse("10-09-2003"), - datetime(2003, 10, 9)) - - def testDateWithDash10(self): - self.assertEqual(parse("10-09-03"), - datetime(2003, 10, 9)) - - def testDateWithDash11(self): - self.assertEqual(parse("10-09-03", yearfirst=True), - datetime(2010, 9, 3)) - - def testDateWithDot1(self): - self.assertEqual(parse("2003.09.25"), - datetime(2003, 9, 25)) - - def testDateWithDot2(self): - self.assertEqual(parse("2003.Sep.25"), - datetime(2003, 9, 25)) - - def testDateWithDot3(self): - self.assertEqual(parse("25.Sep.2003"), - datetime(2003, 9, 25)) - - def testDateWithDot4(self): - self.assertEqual(parse("25.Sep.2003"), - datetime(2003, 9, 25)) - - def testDateWithDot5(self): - self.assertEqual(parse("Sep.25.2003"), - datetime(2003, 9, 25)) - - def testDateWithDot6(self): - self.assertEqual(parse("09.25.2003"), - datetime(2003, 9, 25)) - - def testDateWithDot7(self): - self.assertEqual(parse("25.09.2003"), - datetime(2003, 9, 25)) - - def testDateWithDot8(self): - self.assertEqual(parse("10.09.2003", dayfirst=True), - datetime(2003, 9, 10)) - - def testDateWithDot9(self): - self.assertEqual(parse("10.09.2003"), - datetime(2003, 10, 9)) - - def testDateWithDot10(self): - self.assertEqual(parse("10.09.03"), - datetime(2003, 10, 9)) - - def testDateWithDot11(self): - self.assertEqual(parse("10.09.03", yearfirst=True), - datetime(2010, 9, 3)) - - def testDateWithSlash1(self): - self.assertEqual(parse("2003/09/25"), - datetime(2003, 9, 25)) - - def testDateWithSlash2(self): - self.assertEqual(parse("2003/Sep/25"), - datetime(2003, 9, 25)) - - def testDateWithSlash3(self): - self.assertEqual(parse("25/Sep/2003"), - datetime(2003, 9, 25)) - - def testDateWithSlash4(self): - self.assertEqual(parse("25/Sep/2003"), - datetime(2003, 9, 25)) - - def testDateWithSlash5(self): - self.assertEqual(parse("Sep/25/2003"), - datetime(2003, 9, 25)) - - def testDateWithSlash6(self): - self.assertEqual(parse("09/25/2003"), - datetime(2003, 9, 25)) - - def testDateWithSlash7(self): - self.assertEqual(parse("25/09/2003"), - datetime(2003, 9, 25)) - - def testDateWithSlash8(self): - self.assertEqual(parse("10/09/2003", dayfirst=True), - datetime(2003, 9, 10)) - - def testDateWithSlash9(self): - self.assertEqual(parse("10/09/2003"), - datetime(2003, 10, 9)) - - def testDateWithSlash10(self): - self.assertEqual(parse("10/09/03"), - datetime(2003, 10, 9)) - - def testDateWithSlash11(self): - self.assertEqual(parse("10/09/03", yearfirst=True), - datetime(2010, 9, 3)) - - def testDateWithSpace1(self): - self.assertEqual(parse("2003 09 25"), - datetime(2003, 9, 25)) - - def testDateWithSpace2(self): - self.assertEqual(parse("2003 Sep 25"), - datetime(2003, 9, 25)) - - def testDateWithSpace3(self): - self.assertEqual(parse("25 Sep 2003"), - datetime(2003, 9, 25)) - - def testDateWithSpace4(self): - self.assertEqual(parse("25 Sep 2003"), - datetime(2003, 9, 25)) - - def testDateWithSpace5(self): - self.assertEqual(parse("Sep 25 2003"), - datetime(2003, 9, 25)) - - def testDateWithSpace6(self): - self.assertEqual(parse("09 25 2003"), - datetime(2003, 9, 25)) - - def testDateWithSpace7(self): - self.assertEqual(parse("25 09 2003"), - datetime(2003, 9, 25)) - - def testDateWithSpace8(self): - self.assertEqual(parse("10 09 2003", dayfirst=True), - datetime(2003, 9, 10)) - - def testDateWithSpace9(self): - self.assertEqual(parse("10 09 2003"), - datetime(2003, 10, 9)) - - def testDateWithSpace10(self): - self.assertEqual(parse("10 09 03"), - datetime(2003, 10, 9)) - - def testDateWithSpace11(self): - self.assertEqual(parse("10 09 03", yearfirst=True), - datetime(2010, 9, 3)) - - def testDateWithSpace12(self): - self.assertEqual(parse("25 09 03"), - datetime(2003, 9, 25)) - - def testStrangelyOrderedDate1(self): - self.assertEqual(parse("03 25 Sep"), - datetime(2003, 9, 25)) - - def testStrangelyOrderedDate2(self): - self.assertEqual(parse("2003 25 Sep"), - datetime(2003, 9, 25)) - - def testStrangelyOrderedDate3(self): - self.assertEqual(parse("25 03 Sep"), - datetime(2025, 9, 3)) - - def testHourWithLetters(self): - self.assertEqual(parse("10h36m28.5s", default=self.default), - datetime(2003, 9, 25, 10, 36, 28, 500000)) - - def testHourWithLettersStrip1(self): - self.assertEqual(parse("10h36m28s", default=self.default), - datetime(2003, 9, 25, 10, 36, 28)) - - def testHourWithLettersStrip2(self): - self.assertEqual(parse("10h36m", default=self.default), - datetime(2003, 9, 25, 10, 36)) - - def testHourWithLettersStrip3(self): - self.assertEqual(parse("10h", default=self.default), - datetime(2003, 9, 25, 10)) - - def testHourWithLettersStrip4(self): - self.assertEqual(parse("10 h 36", default=self.default), - datetime(2003, 9, 25, 10, 36)) + self.assertEqual(parse("20030925T104941+0300"), + datetime(2003, 9, 25, 10, 49, 41, + tzinfo=tzoffset(None, 10800))) def testAMPMNoHour(self): - with self.assertRaises(ValueError): + with pytest.raises(ParserError): parse("AM") - with self.assertRaises(ValueError): + with pytest.raises(ParserError): parse("Jan 20, 2015 PM") - def testHourAmPm1(self): - self.assertEqual(parse("10h am", default=self.default), - datetime(2003, 9, 25, 10)) - - def testHourAmPm2(self): - self.assertEqual(parse("10h pm", default=self.default), - datetime(2003, 9, 25, 22)) - - def testHourAmPm3(self): - self.assertEqual(parse("10am", default=self.default), - datetime(2003, 9, 25, 10)) - - def testHourAmPm4(self): - self.assertEqual(parse("10pm", default=self.default), - datetime(2003, 9, 25, 22)) - - def testHourAmPm5(self): - self.assertEqual(parse("10:00 am", default=self.default), - datetime(2003, 9, 25, 10)) - - def testHourAmPm6(self): - self.assertEqual(parse("10:00 pm", default=self.default), - datetime(2003, 9, 25, 22)) - - def testHourAmPm7(self): - self.assertEqual(parse("10:00am", default=self.default), - datetime(2003, 9, 25, 10)) - - def testHourAmPm8(self): - self.assertEqual(parse("10:00pm", default=self.default), - datetime(2003, 9, 25, 22)) - - def testHourAmPm9(self): - self.assertEqual(parse("10:00a.m", default=self.default), - datetime(2003, 9, 25, 10)) - - def testHourAmPm10(self): - self.assertEqual(parse("10:00p.m", default=self.default), - datetime(2003, 9, 25, 22)) - - def testHourAmPm11(self): - self.assertEqual(parse("10:00a.m.", default=self.default), - datetime(2003, 9, 25, 10)) - - def testHourAmPm12(self): - self.assertEqual(parse("10:00p.m.", default=self.default), - datetime(2003, 9, 25, 22)) - def testAMPMRange(self): - with self.assertRaises(ValueError): + with pytest.raises(ParserError): parse("13:44 AM") - with self.assertRaises(ValueError): + with pytest.raises(ParserError): parse("January 25, 1921 23:13 PM") def testPertain(self): @@ -527,22 +499,6 @@ class ParserTest(unittest.TestCase): self.assertEqual(parse("Sep of 03", default=self.default), datetime(2003, 9, 25)) - def testWeekdayAlone(self): - self.assertEqual(parse("Wed", default=self.default), - datetime(2003, 10, 1)) - - def testLongWeekday(self): - self.assertEqual(parse("Wednesday", default=self.default), - datetime(2003, 10, 1)) - - def testLongMonth(self): - self.assertEqual(parse("October", default=self.default), - datetime(2003, 10, 25)) - - def testZeroYear(self): - self.assertEqual(parse("31-Dec-00", default=self.default), - datetime(2000, 12, 31)) - def testFuzzy(self): s = "Today is 25 of September of 2003, exactly " \ "at 10:49:41 with timezone -03:00." @@ -551,14 +507,19 @@ class ParserTest(unittest.TestCase): tzinfo=self.brsttz)) def testFuzzyWithTokens(self): - s = "Today is 25 of September of 2003, exactly " \ + s1 = "Today is 25 of September of 2003, exactly " \ "at 10:49:41 with timezone -03:00." - self.assertEqual(parse(s, fuzzy_with_tokens=True), + self.assertEqual(parse(s1, fuzzy_with_tokens=True), (datetime(2003, 9, 25, 10, 49, 41, tzinfo=self.brsttz), ('Today is ', 'of ', ', exactly at ', ' with timezone ', '.'))) + s2 = "http://biz.yahoo.com/ipo/p/600221.html" + self.assertEqual(parse(s2, fuzzy_with_tokens=True), + (datetime(2060, 2, 21, 0, 0, 0), + ('http://biz.yahoo.com/ipo/p/', '.html'))) + def testFuzzyAMPMProblem(self): # Sometimes fuzzy parsing results in AM/PM flag being set without # hours - if it's fuzzy it should ignore that. @@ -576,186 +537,44 @@ class ParserTest(unittest.TestCase): def testFuzzyIgnoreAMPM(self): s1 = "Jan 29, 1945 14:45 AM I going to see you there?" - - self.assertEqual(parse(s1, fuzzy=True), datetime(1945, 1, 29, 14, 45)) - - def testExtraSpace(self): - self.assertEqual(parse(" July 4 , 1976 12:01:02 am "), - datetime(1976, 7, 4, 0, 1, 2)) - - def testRandomFormat1(self): - self.assertEqual(parse("Wed, July 10, '96"), - datetime(1996, 7, 10, 0, 0)) - - def testRandomFormat2(self): - self.assertEqual(parse("1996.07.10 AD at 15:08:56 PDT", - ignoretz=True), - datetime(1996, 7, 10, 15, 8, 56)) - - def testRandomFormat3(self): - self.assertEqual(parse("1996.July.10 AD 12:08 PM"), - datetime(1996, 7, 10, 12, 8)) - - def testRandomFormat4(self): - self.assertEqual(parse("Tuesday, April 12, 1952 AD 3:30:42pm PST", - ignoretz=True), - datetime(1952, 4, 12, 15, 30, 42)) - - def testRandomFormat5(self): - self.assertEqual(parse("November 5, 1994, 8:15:30 am EST", - ignoretz=True), - datetime(1994, 11, 5, 8, 15, 30)) - - def testRandomFormat6(self): - self.assertEqual(parse("1994-11-05T08:15:30-05:00", - ignoretz=True), - datetime(1994, 11, 5, 8, 15, 30)) - - def testRandomFormat7(self): - self.assertEqual(parse("1994-11-05T08:15:30Z", - ignoretz=True), - datetime(1994, 11, 5, 8, 15, 30)) - - def testRandomFormat8(self): - self.assertEqual(parse("July 4, 1976"), datetime(1976, 7, 4)) - - def testRandomFormat9(self): - self.assertEqual(parse("7 4 1976"), datetime(1976, 7, 4)) - - def testRandomFormat10(self): - self.assertEqual(parse("4 jul 1976"), datetime(1976, 7, 4)) - - def testRandomFormat11(self): - self.assertEqual(parse("7-4-76"), datetime(1976, 7, 4)) - - def testRandomFormat12(self): - self.assertEqual(parse("19760704"), datetime(1976, 7, 4)) - - def testRandomFormat13(self): - self.assertEqual(parse("0:01:02", default=self.default), - datetime(2003, 9, 25, 0, 1, 2)) - - def testRandomFormat14(self): - self.assertEqual(parse("12h 01m02s am", default=self.default), - datetime(2003, 9, 25, 0, 1, 2)) - - def testRandomFormat15(self): - self.assertEqual(parse("0:01:02 on July 4, 1976"), - datetime(1976, 7, 4, 0, 1, 2)) - - def testRandomFormat16(self): - self.assertEqual(parse("0:01:02 on July 4, 1976"), - datetime(1976, 7, 4, 0, 1, 2)) - - def testRandomFormat17(self): - self.assertEqual(parse("1976-07-04T00:01:02Z", ignoretz=True), - datetime(1976, 7, 4, 0, 1, 2)) - - def testRandomFormat18(self): - self.assertEqual(parse("July 4, 1976 12:01:02 am"), - datetime(1976, 7, 4, 0, 1, 2)) - - def testRandomFormat19(self): - self.assertEqual(parse("Mon Jan 2 04:24:27 1995"), - datetime(1995, 1, 2, 4, 24, 27)) - - def testRandomFormat20(self): - self.assertEqual(parse("Tue Apr 4 00:22:12 PDT 1995", ignoretz=True), - datetime(1995, 4, 4, 0, 22, 12)) - - def testRandomFormat21(self): - self.assertEqual(parse("04.04.95 00:22"), - datetime(1995, 4, 4, 0, 22)) - - def testRandomFormat22(self): - self.assertEqual(parse("Jan 1 1999 11:23:34.578"), - datetime(1999, 1, 1, 11, 23, 34, 578000)) - - def testRandomFormat23(self): - self.assertEqual(parse("950404 122212"), - datetime(1995, 4, 4, 12, 22, 12)) + with pytest.warns(UnknownTimezoneWarning): + res = parse(s1, fuzzy=True) + self.assertEqual(res, datetime(1945, 1, 29, 14, 45)) def testRandomFormat24(self): self.assertEqual(parse("0:00 PM, PST", default=self.default, ignoretz=True), datetime(2003, 9, 25, 12, 0)) - def testRandomFormat25(self): - self.assertEqual(parse("12:08 PM", default=self.default), - datetime(2003, 9, 25, 12, 8)) - def testRandomFormat26(self): - self.assertEqual(parse("5:50 A.M. on June 13, 1990"), - datetime(1990, 6, 13, 5, 50)) + with pytest.warns(UnknownTimezoneWarning): + res = parse("5:50 A.M. on June 13, 1990") - def testRandomFormat27(self): - self.assertEqual(parse("3rd of May 2001"), datetime(2001, 5, 3)) - - def testRandomFormat28(self): - self.assertEqual(parse("5th of March 2001"), datetime(2001, 3, 5)) - - def testRandomFormat29(self): - self.assertEqual(parse("1st of May 2003"), datetime(2003, 5, 1)) - - def testRandomFormat30(self): - self.assertEqual(parse("01h02m03", default=self.default), - datetime(2003, 9, 25, 1, 2, 3)) - - def testRandomFormat31(self): - self.assertEqual(parse("01h02", default=self.default), - datetime(2003, 9, 25, 1, 2)) - - def testRandomFormat32(self): - self.assertEqual(parse("01h02s", default=self.default), - datetime(2003, 9, 25, 1, 0, 2)) - - def testRandomFormat33(self): - self.assertEqual(parse("01m02", default=self.default), - datetime(2003, 9, 25, 0, 1, 2)) - - def testRandomFormat34(self): - self.assertEqual(parse("01m02h", default=self.default), - datetime(2003, 9, 25, 2, 1)) - - def testRandomFormat35(self): - self.assertEqual(parse("2004 10 Apr 11h30m", default=self.default), - datetime(2004, 4, 10, 11, 30)) - - def test_99_ad(self): - self.assertEqual(parse('0099-01-01T00:00:00'), - datetime(99, 1, 1, 0, 0)) - - def test_31_ad(self): - self.assertEqual(parse('0031-01-01T00:00:00'), - datetime(31, 1, 1, 0, 0)) - - def testInvalidDay(self): - with self.assertRaises(ValueError): - parse("Feb 30, 2007") + self.assertEqual(res, datetime(1990, 6, 13, 5, 50)) def testUnspecifiedDayFallback(self): # Test that for an unspecified day, the fallback behavior is correct. self.assertEqual(parse("April 2009", default=datetime(2010, 1, 31)), datetime(2009, 4, 30)) - def testUnspecifiedDayFallbackFebNoLeapYear(self): + def testUnspecifiedDayFallbackFebNoLeapYear(self): self.assertEqual(parse("Feb 2007", default=datetime(2010, 1, 31)), datetime(2007, 2, 28)) - def testUnspecifiedDayFallbackFebLeapYear(self): + def testUnspecifiedDayFallbackFebLeapYear(self): self.assertEqual(parse("Feb 2008", default=datetime(2010, 1, 31)), datetime(2008, 2, 29)) def testErrorType01(self): - self.assertRaises(ValueError, - parse, 'shouldfail') + with pytest.raises(ParserError): + parse('shouldfail') def testCorrectErrorOnFuzzyWithTokens(self): - assertRaisesRegex(self, ValueError, 'Unknown string format', + assertRaisesRegex(self, ParserError, 'Unknown string format', parse, '04/04/32/423', fuzzy_with_tokens=True) - assertRaisesRegex(self, ValueError, 'Unknown string format', + assertRaisesRegex(self, ParserError, 'Unknown string format', parse, '04/04/04 +32423', fuzzy_with_tokens=True) - assertRaisesRegex(self, ValueError, 'Unknown string format', + assertRaisesRegex(self, ParserError, 'Unknown string format', parse, '04/04/0d4', fuzzy_with_tokens=True) def testIncreasingCTime(self): @@ -766,22 +585,22 @@ class ParserTest(unittest.TestCase): delta = timedelta(days=365+31+1, seconds=1+60+60*60) dt = datetime(1900, 1, 1, 0, 0, 0, 0) for i in range(200): - self.assertEqual(parse(dt.ctime()), dt) + assert parse(dt.ctime()) == dt dt += delta def testIncreasingISOFormat(self): delta = timedelta(days=365+31+1, seconds=1+60+60*60) dt = datetime(1900, 1, 1, 0, 0, 0, 0) for i in range(200): - self.assertEqual(parse(dt.isoformat()), dt) + assert parse(dt.isoformat()) == dt dt += delta def testMicrosecondsPrecisionError(self): # Skip found out that sad precision problem. :-( dt1 = parse("00:11:25.01") dt2 = parse("00:12:10.01") - self.assertEqual(dt1.microsecond, 10000) - self.assertEqual(dt2.microsecond, 10000) + assert dt1.microsecond == 10000 + assert dt2.microsecond == 10000 def testMicrosecondPrecisionErrorReturns(self): # One more precision issue, discovered by Eric Brown. This should @@ -791,11 +610,7 @@ class ParserTest(unittest.TestCase): 1001, 1000, 999, 998, 101, 100, 99, 98]: dt = datetime(2008, 2, 27, 21, 26, 1, ms) - self.assertEqual(parse(dt.isoformat()), dt) - - def testHighPrecisionSeconds(self): - self.assertEqual(parse("20080227T21:26:01.123456789"), - datetime(2008, 2, 27, 21, 26, 1, 123456)) + assert parse(dt.isoformat()) == dt def testCustomParserInfo(self): # Custom parser info wasn't working, as Michael Elsdörfer discovered. @@ -806,7 +621,26 @@ class ParserTest(unittest.TestCase): MONTHS[0] = ("Foo", "Foo") myparser = parser(myparserinfo()) dt = myparser.parse("01/Foo/2007") - self.assertEqual(dt, datetime(2007, 1, 1)) + assert dt == datetime(2007, 1, 1) + + def testCustomParserShortDaynames(self): + # Horacio Hoyos discovered that day names shorter than 3 characters, + # for example two letter German day name abbreviations, don't work: + # https://github.com/dateutil/dateutil/issues/343 + from dateutil.parser import parserinfo, parser + + class GermanParserInfo(parserinfo): + WEEKDAYS = [("Mo", "Montag"), + ("Di", "Dienstag"), + ("Mi", "Mittwoch"), + ("Do", "Donnerstag"), + ("Fr", "Freitag"), + ("Sa", "Samstag"), + ("So", "Sonntag")] + + myparser = parser(GermanParserInfo()) + dt = myparser.parse("Sa 21. Jan 2017") + self.assertEqual(dt, datetime(2017, 1, 21)) def testNoYearFirstNoDayFirst(self): dtstr = '090107' @@ -851,7 +685,7 @@ class ParserTest(unittest.TestCase): def testUnambiguousDayFirst(self): dtstr = '2015 09 25' - self.assertEqual(parse(dtstr, dayfirst=True), + self.assertEqual(parse(dtstr, dayfirst=True), datetime(2015, 9, 25)) def testUnambiguousDayFirstYearFirst(self): @@ -859,3 +693,272 @@ class ParserTest(unittest.TestCase): self.assertEqual(parse(dtstr, dayfirst=True, yearfirst=True), datetime(2015, 9, 25)) + def test_mstridx(self): + # See GH408 + dtstr = '2015-15-May' + self.assertEqual(parse(dtstr), + datetime(2015, 5, 15)) + + def test_idx_check(self): + dtstr = '2017-07-17 06:15:' + # Pre-PR, the trailing colon will cause an IndexError at 824-825 + # when checking `i < len_l` and then accessing `l[i+1]` + res = parse(dtstr, fuzzy=True) + assert res == datetime(2017, 7, 17, 6, 15) + + def test_hmBY(self): + # See GH#483 + dtstr = '02:17NOV2017' + res = parse(dtstr, default=self.default) + assert res == datetime(2017, 11, self.default.day, 2, 17) + + def test_validate_hour(self): + # See GH353 + invalid = "201A-01-01T23:58:39.239769+03:00" + with pytest.raises(ParserError): + parse(invalid) + + def test_era_trailing_year(self): + dstr = 'AD2001' + res = parse(dstr) + assert res.year == 2001, res + + def test_includes_timestr(self): + timestr = "2020-13-97T44:61:83" + + try: + parse(timestr) + except ParserError as e: + assert e.args[1] == timestr + else: + pytest.fail("Failed to raise ParserError") + + +class TestOutOfBounds(object): + + def test_no_year_zero(self): + with pytest.raises(ParserError): + parse("0000 Jun 20") + + def test_out_of_bound_day(self): + with pytest.raises(ParserError): + parse("Feb 30, 2007") + + def test_illegal_month_error(self): + with pytest.raises(ParserError): + parse("0-100") + + def test_day_sanity(self, fuzzy): + dstr = "2014-15-25" + with pytest.raises(ParserError): + parse(dstr, fuzzy=fuzzy) + + def test_minute_sanity(self, fuzzy): + dstr = "2014-02-28 22:64" + with pytest.raises(ParserError): + parse(dstr, fuzzy=fuzzy) + + def test_hour_sanity(self, fuzzy): + dstr = "2014-02-28 25:16 PM" + with pytest.raises(ParserError): + parse(dstr, fuzzy=fuzzy) + + def test_second_sanity(self, fuzzy): + dstr = "2014-02-28 22:14:64" + with pytest.raises(ParserError): + parse(dstr, fuzzy=fuzzy) + + +class TestParseUnimplementedCases(object): + @pytest.mark.xfail + def test_somewhat_ambiguous_string(self): + # Ref: github issue #487 + # The parser is choosing the wrong part for hour + # causing datetime to raise an exception. + dtstr = '1237 PM BRST Mon Oct 30 2017' + res = parse(dtstr, tzinfo=self.tzinfos) + assert res == datetime(2017, 10, 30, 12, 37, tzinfo=self.tzinfos) + + @pytest.mark.xfail + def test_YmdH_M_S(self): + # found in nasdaq's ftp data + dstr = '1991041310:19:24' + expected = datetime(1991, 4, 13, 10, 19, 24) + res = parse(dstr) + assert res == expected, (res, expected) + + @pytest.mark.xfail + def test_first_century(self): + dstr = '0031 Nov 03' + expected = datetime(31, 11, 3) + res = parse(dstr) + assert res == expected, res + + @pytest.mark.xfail + def test_era_trailing_year_with_dots(self): + dstr = 'A.D.2001' + res = parse(dstr) + assert res.year == 2001, res + + @pytest.mark.xfail + def test_ad_nospace(self): + expected = datetime(6, 5, 19) + for dstr in [' 6AD May 19', ' 06AD May 19', + ' 006AD May 19', ' 0006AD May 19']: + res = parse(dstr) + assert res == expected, (dstr, res) + + @pytest.mark.xfail + def test_four_letter_day(self): + dstr = 'Frid Dec 30, 2016' + expected = datetime(2016, 12, 30) + res = parse(dstr) + assert res == expected + + @pytest.mark.xfail + def test_non_date_number(self): + dstr = '1,700' + with pytest.raises(ParserError): + parse(dstr) + + @pytest.mark.xfail + def test_on_era(self): + # This could be classified as an "eras" test, but the relevant part + # about this is the ` on ` + dstr = '2:15 PM on January 2nd 1973 A.D.' + expected = datetime(1973, 1, 2, 14, 15) + res = parse(dstr) + assert res == expected + + @pytest.mark.xfail + def test_extraneous_year(self): + # This was found in the wild at insidertrading.org + dstr = "2011 MARTIN CHILDREN'S IRREVOCABLE TRUST u/a/d NOVEMBER 7, 2012" + res = parse(dstr, fuzzy_with_tokens=True) + expected = datetime(2012, 11, 7) + assert res == expected + + @pytest.mark.xfail + def test_extraneous_year_tokens(self): + # This was found in the wild at insidertrading.org + # Unlike in the case above, identifying the first "2012" as the year + # would not be a problem, but inferring that the latter 2012 is hhmm + # is a problem. + dstr = "2012 MARTIN CHILDREN'S IRREVOCABLE TRUST u/a/d NOVEMBER 7, 2012" + expected = datetime(2012, 11, 7) + (res, tokens) = parse(dstr, fuzzy_with_tokens=True) + assert res == expected + assert tokens == ("2012 MARTIN CHILDREN'S IRREVOCABLE TRUST u/a/d ",) + + @pytest.mark.xfail + def test_extraneous_year2(self): + # This was found in the wild at insidertrading.org + dstr = ("Berylson Amy Smith 1998 Grantor Retained Annuity Trust " + "u/d/t November 2, 1998 f/b/o Jennifer L Berylson") + res = parse(dstr, fuzzy_with_tokens=True) + expected = datetime(1998, 11, 2) + assert res == expected + + @pytest.mark.xfail + def test_extraneous_year3(self): + # This was found in the wild at insidertrading.org + dstr = "SMITH R & WEISS D 94 CHILD TR FBO M W SMITH UDT 12/1/1994" + res = parse(dstr, fuzzy_with_tokens=True) + expected = datetime(1994, 12, 1) + assert res == expected + + @pytest.mark.xfail + def test_unambiguous_YYYYMM(self): + # 171206 can be parsed as YYMMDD. However, 201712 cannot be parsed + # as instance of YYMMDD and parser could fallback to YYYYMM format. + dstr = "201712" + res = parse(dstr) + expected = datetime(2017, 12, 1) + assert res == expected + + @pytest.mark.xfail + def test_extraneous_numerical_content(self): + # ref: https://github.com/dateutil/dateutil/issues/1029 + # parser interprets price and percentage as parts of the date + dstr = "£14.99 (25% off, until April 20)" + res = parse(dstr, fuzzy=True, default=datetime(2000, 1, 1)) + expected = datetime(2000, 4, 20) + assert res == expected + + +@pytest.mark.skipif(IS_WIN, reason="Windows does not use TZ var") +class TestTZVar(object): + def test_parse_unambiguous_nonexistent_local(self): + # When dates are specified "EST" even when they should be "EDT" in the + # local time zone, we should still assign the local time zone + with TZEnvContext('EST+5EDT,M3.2.0/2,M11.1.0/2'): + dt_exp = datetime(2011, 8, 1, 12, 30, tzinfo=tz.tzlocal()) + dt = parse('2011-08-01T12:30 EST') + + assert dt.tzname() == 'EDT' + assert dt == dt_exp + + def test_tzlocal_in_gmt(self): + # GH #318 + with TZEnvContext('GMT0BST,M3.5.0,M10.5.0'): + # This is an imaginary datetime in tz.tzlocal() but should still + # parse using the GMT-as-alias-for-UTC rule + dt = parse('2004-05-01T12:00 GMT') + dt_exp = datetime(2004, 5, 1, 12, tzinfo=tz.UTC) + + assert dt == dt_exp + + def test_tzlocal_parse_fold(self): + # One manifestion of GH #318 + with TZEnvContext('EST+5EDT,M3.2.0/2,M11.1.0/2'): + dt_exp = datetime(2011, 11, 6, 1, 30, tzinfo=tz.tzlocal()) + dt_exp = tz.enfold(dt_exp, fold=1) + dt = parse('2011-11-06T01:30 EST') + + # Because this is ambiguous, until `tz.tzlocal() is tz.tzlocal()` + # we'll just check the attributes we care about rather than + # dt == dt_exp + assert dt.tzname() == dt_exp.tzname() + assert dt.replace(tzinfo=None) == dt_exp.replace(tzinfo=None) + assert getattr(dt, 'fold') == getattr(dt_exp, 'fold') + assert dt.astimezone(tz.UTC) == dt_exp.astimezone(tz.UTC) + + +def test_parse_tzinfos_fold(): + NYC = tz.gettz('America/New_York') + tzinfos = {'EST': NYC, 'EDT': NYC} + + dt_exp = tz.enfold(datetime(2011, 11, 6, 1, 30, tzinfo=NYC), fold=1) + dt = parse('2011-11-06T01:30 EST', tzinfos=tzinfos) + + assert dt == dt_exp + assert dt.tzinfo is dt_exp.tzinfo + assert getattr(dt, 'fold') == getattr(dt_exp, 'fold') + assert dt.astimezone(tz.UTC) == dt_exp.astimezone(tz.UTC) + + +@pytest.mark.parametrize('dtstr,dt', [ + ('5.6h', datetime(2003, 9, 25, 5, 36)), + ('5.6m', datetime(2003, 9, 25, 0, 5, 36)), + # '5.6s' never had a rounding problem, test added for completeness + ('5.6s', datetime(2003, 9, 25, 0, 0, 5, 600000)) +]) +def test_rounding_floatlike_strings(dtstr, dt): + assert parse(dtstr, default=datetime(2003, 9, 25)) == dt + + +@pytest.mark.parametrize('value', ['1: test', 'Nan']) +def test_decimal_error(value): + # GH 632, GH 662 - decimal.Decimal raises some non-ParserError exception + # when constructed with an invalid value + with pytest.raises(ParserError): + parse(value) + +def test_parsererror_repr(): + # GH 991 — the __repr__ was not properly indented and so was never defined. + # This tests the current behavior of the ParserError __repr__, but the + # precise format is not guaranteed to be stable and may change even in + # minor versions. This test exists to avoid regressions. + s = repr(ParserError("Problem with string: %s", "2019-01-01")) + + assert s == "ParserError('Problem with string: %s', '2019-01-01')" diff --git a/libs/dateutil/test/test_relativedelta.py b/libs/dateutil/test/test_relativedelta.py index 9e1ca7c1a..1e5d17044 100644 --- a/libs/dateutil/test/test_relativedelta.py +++ b/libs/dateutil/test/test_relativedelta.py @@ -1,14 +1,17 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals -from ._common import unittest, WarningTestMixin, NotAValue +from ._common import NotAValue import calendar from datetime import datetime, date, timedelta +import unittest -from dateutil.relativedelta import * +import pytest + +from dateutil.relativedelta import relativedelta, MO, TU, WE, FR, SU -class RelativeDeltaTest(WarningTestMixin, unittest.TestCase): +class RelativeDeltaTest(unittest.TestCase): now = datetime(2003, 9, 17, 20, 54, 47, 282310) today = date(2003, 9, 17) @@ -116,14 +119,30 @@ class RelativeDeltaTest(WarningTestMixin, unittest.TestCase): self.assertEqual(self.today+relativedelta(day=31, weekday=FR(-1)), date(2003, 9, 26)) + def testLastDayOfFebruary(self): + self.assertEqual(date(2021, 2, 1) + relativedelta(day=31), + date(2021, 2, 28)) + + def testLastDayOfFebruaryLeapYear(self): + self.assertEqual(date(2020, 2, 1) + relativedelta(day=31), + date(2020, 2, 29)) + def testNextWednesdayIsToday(self): self.assertEqual(self.today+relativedelta(weekday=WE), date(2003, 9, 17)) - def testNextWenesdayNotToday(self): + def testNextWednesdayNotToday(self): self.assertEqual(self.today+relativedelta(days=+1, weekday=WE), date(2003, 9, 24)) + def testAddMoreThan12Months(self): + self.assertEqual(date(2003, 12, 1) + relativedelta(months=+13), + date(2005, 1, 1)) + + def testAddNegativeMonths(self): + self.assertEqual(date(2003, 1, 1) + relativedelta(months=-2), + date(2002, 11, 1)) + def test15thISOYearWeek(self): self.assertEqual(date(2003, 1, 1) + relativedelta(day=4, weeks=+14, weekday=MO(-1)), @@ -180,6 +199,12 @@ class RelativeDeltaTest(WarningTestMixin, unittest.TestCase): relativedelta(years=1, months=2, days=13, hours=4, minutes=5, microseconds=6)) + def testAbsoluteAddition(self): + self.assertEqual(relativedelta() + relativedelta(day=0, hour=0), + relativedelta(day=0, hour=0)) + self.assertEqual(relativedelta(day=0, hour=0) + relativedelta(), + relativedelta(day=0, hour=0)) + def testAdditionToDatetime(self): self.assertEqual(datetime(2000, 1, 1) + relativedelta(days=1), datetime(2000, 1, 2)) @@ -196,6 +221,31 @@ class RelativeDeltaTest(WarningTestMixin, unittest.TestCase): # For unsupported types that define their own comparators, etc. self.assertIs(relativedelta(days=1) + NotAValue, NotAValue) + def testAdditionFloatValue(self): + self.assertEqual(datetime(2000, 1, 1) + relativedelta(days=float(1)), + datetime(2000, 1, 2)) + self.assertEqual(datetime(2000, 1, 1) + relativedelta(months=float(1)), + datetime(2000, 2, 1)) + self.assertEqual(datetime(2000, 1, 1) + relativedelta(years=float(1)), + datetime(2001, 1, 1)) + + def testAdditionFloatFractionals(self): + self.assertEqual(datetime(2000, 1, 1, 0) + + relativedelta(days=float(0.5)), + datetime(2000, 1, 1, 12)) + self.assertEqual(datetime(2000, 1, 1, 0, 0) + + relativedelta(hours=float(0.5)), + datetime(2000, 1, 1, 0, 30)) + self.assertEqual(datetime(2000, 1, 1, 0, 0, 0) + + relativedelta(minutes=float(0.5)), + datetime(2000, 1, 1, 0, 0, 30)) + self.assertEqual(datetime(2000, 1, 1, 0, 0, 0, 0) + + relativedelta(seconds=float(0.5)), + datetime(2000, 1, 1, 0, 0, 0, 500000)) + self.assertEqual(datetime(2000, 1, 1, 0, 0, 0, 0) + + relativedelta(microseconds=float(500000.25)), + datetime(2000, 1, 1, 0, 0, 0, 500000)) + def testSubtraction(self): self.assertEqual(relativedelta(days=10) - relativedelta(years=1, months=2, days=3, hours=4, @@ -238,6 +288,20 @@ class RelativeDeltaTest(WarningTestMixin, unittest.TestCase): self.assertFalse(relativedelta(days=0)) self.assertTrue(relativedelta(days=1)) + def testAbsoluteValueNegative(self): + rd_base = relativedelta(years=-1, months=-5, days=-2, hours=-3, + minutes=-5, seconds=-2, microseconds=-12) + rd_expected = relativedelta(years=1, months=5, days=2, hours=3, + minutes=5, seconds=2, microseconds=12) + self.assertEqual(abs(rd_base), rd_expected) + + def testAbsoluteValuePositive(self): + rd_base = relativedelta(years=1, months=5, days=2, hours=3, + minutes=5, seconds=2, microseconds=12) + rd_expected = rd_base + + self.assertEqual(abs(rd_base), rd_expected) + def testComparison(self): d1 = relativedelta(years=1, months=1, days=1, leapdays=0, hours=1, minutes=1, seconds=1, microseconds=1) @@ -304,28 +368,38 @@ class RelativeDeltaTest(WarningTestMixin, unittest.TestCase): with self.assertRaises(ValueError): relativedelta(months=1.5) + def testRelativeDeltaInvalidDatetimeObject(self): + with self.assertRaises(TypeError): + relativedelta(dt1='2018-01-01', dt2='2018-01-02') + + with self.assertRaises(TypeError): + relativedelta(dt1=datetime(2018, 1, 1), dt2='2018-01-02') + + with self.assertRaises(TypeError): + relativedelta(dt1='2018-01-01', dt2=datetime(2018, 1, 2)) + def testRelativeDeltaFractionalAbsolutes(self): # Fractional absolute values will soon be unsupported, # check for the deprecation warning. - with self.assertWarns(DeprecationWarning): + with pytest.warns(DeprecationWarning): relativedelta(year=2.86) - with self.assertWarns(DeprecationWarning): + with pytest.warns(DeprecationWarning): relativedelta(month=1.29) - with self.assertWarns(DeprecationWarning): + with pytest.warns(DeprecationWarning): relativedelta(day=0.44) - with self.assertWarns(DeprecationWarning): + with pytest.warns(DeprecationWarning): relativedelta(hour=23.98) - with self.assertWarns(DeprecationWarning): + with pytest.warns(DeprecationWarning): relativedelta(minute=45.21) - with self.assertWarns(DeprecationWarning): + with pytest.warns(DeprecationWarning): relativedelta(second=13.2) - with self.assertWarns(DeprecationWarning): + with pytest.warns(DeprecationWarning): relativedelta(microsecond=157221.93) def testRelativeDeltaFractionalRepr(self): @@ -410,13 +484,13 @@ class RelativeDeltaTest(WarningTestMixin, unittest.TestCase): self.assertEqual(rd1.normalized(), relativedelta(days=2, hours=18)) - # Equvalent to (days=1, hours=11, minutes=31, seconds=12) + # Equivalent to (days=1, hours=11, minutes=31, seconds=12) rd2 = relativedelta(days=1.48) self.assertEqual(rd2.normalized(), relativedelta(days=1, hours=11, minutes=31, seconds=12)) - def testRelativeDeltaNormalizeFractionalDays(self): + def testRelativeDeltaNormalizeFractionalDays2(self): # Equivalent to (hours=1, minutes=30) rd1 = relativedelta(hours=1.5) @@ -447,7 +521,7 @@ class RelativeDeltaTest(WarningTestMixin, unittest.TestCase): self.assertEqual(rd1.normalized(), relativedelta(seconds=45, microseconds=25000)) - def testRelativeDeltaFractionalPositiveOverflow(self): + def testRelativeDeltaFractionalPositiveOverflow2(self): # Equivalent to (days=1, hours=14) rd1 = relativedelta(days=1.5, hours=2) self.assertEqual(rd1.normalized(), @@ -569,3 +643,64 @@ class RelativeDeltaTest(WarningTestMixin, unittest.TestCase): ) self.assertEqual(expected, rd + td) + + def testHashable(self): + try: + {relativedelta(minute=1): 'test'} + except: + self.fail("relativedelta() failed to hash!") + + +class RelativeDeltaWeeksPropertyGetterTest(unittest.TestCase): + """Test the weeks property getter""" + + def test_one_day(self): + rd = relativedelta(days=1) + self.assertEqual(rd.days, 1) + self.assertEqual(rd.weeks, 0) + + def test_minus_one_day(self): + rd = relativedelta(days=-1) + self.assertEqual(rd.days, -1) + self.assertEqual(rd.weeks, 0) + + def test_height_days(self): + rd = relativedelta(days=8) + self.assertEqual(rd.days, 8) + self.assertEqual(rd.weeks, 1) + + def test_minus_height_days(self): + rd = relativedelta(days=-8) + self.assertEqual(rd.days, -8) + self.assertEqual(rd.weeks, -1) + + +class RelativeDeltaWeeksPropertySetterTest(unittest.TestCase): + """Test the weeks setter which makes a "smart" update of the days attribute""" + + def test_one_day_set_one_week(self): + rd = relativedelta(days=1) + rd.weeks = 1 # add 7 days + self.assertEqual(rd.days, 8) + self.assertEqual(rd.weeks, 1) + + def test_minus_one_day_set_one_week(self): + rd = relativedelta(days=-1) + rd.weeks = 1 # add 7 days + self.assertEqual(rd.days, 6) + self.assertEqual(rd.weeks, 0) + + def test_height_days_set_minus_one_week(self): + rd = relativedelta(days=8) + rd.weeks = -1 # change from 1 week, 1 day to -1 week, 1 day + self.assertEqual(rd.days, -6) + self.assertEqual(rd.weeks, 0) + + def test_minus_height_days_set_minus_one_week(self): + rd = relativedelta(days=-8) + rd.weeks = -1 # does not change anything + self.assertEqual(rd.days, -8) + self.assertEqual(rd.weeks, -1) + + +# vim:ts=4:sw=4:et diff --git a/libs/dateutil/test/test_rrule.py b/libs/dateutil/test/test_rrule.py index 2a1e6e8b4..52673ecc2 100644 --- a/libs/dateutil/test/test_rrule.py +++ b/libs/dateutil/test/test_rrule.py @@ -1,15 +1,25 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals -from ._common import WarningTestMixin, unittest -import calendar from datetime import datetime, date -from six import PY3 +import unittest +from six import PY2 -from dateutil.rrule import * +from dateutil import tz +from dateutil.rrule import ( + rrule, rruleset, rrulestr, + YEARLY, MONTHLY, WEEKLY, DAILY, + HOURLY, MINUTELY, SECONDLY, + MO, TU, WE, TH, FR, SA, SU +) + +from freezegun import freeze_time + +import pytest -class RRuleTest(WarningTestMixin, unittest.TestCase): +@pytest.mark.rrule +class RRuleTest(unittest.TestCase): def _rrulestr_reverse_test(self, rule): """ Call with an `rrule` and it will test that `str(rrule)` generates a @@ -21,6 +31,20 @@ class RRuleTest(WarningTestMixin, unittest.TestCase): self.assertEqual(list(rule), list(rrulestr_rrule)) + def testStrAppendRRULEToken(self): + # `_rrulestr_reverse_test` does not check if the "RRULE:" prefix + # property is appended properly, so give it a dedicated test + self.assertEqual(str(rrule(YEARLY, + count=5, + dtstart=datetime(1997, 9, 2, 9, 0))), + "DTSTART:19970902T090000\n" + "RRULE:FREQ=YEARLY;COUNT=5") + + rr_str = ( + 'DTSTART:19970105T083000\nRRULE:FREQ=YEARLY;INTERVAL=2' + ) + self.assertEqual(str(rrulestr(rr_str)), rr_str) + def testYearly(self): self.assertEqual(list(rrule(YEARLY, count=3, @@ -2259,7 +2283,7 @@ class RRuleTest(WarningTestMixin, unittest.TestCase): datetime(2010, 3, 22, 14, 1)]) def testLongIntegers(self): - if not PY3: # There is no longs in python3 + if PY2: # There are no longs in python3 self.assertEqual(list(rrule(MINUTELY, count=long(2), interval=long(2), @@ -2344,10 +2368,10 @@ class RRuleTest(WarningTestMixin, unittest.TestCase): def testBadUntilCountRRule(self): """ - See rfc-2445 4.3.10 - This checks for the deprecation warning, and will + See rfc-5545 3.3.10 - This checks for the deprecation warning, and will eventually check for an error. """ - with self.assertWarns(DeprecationWarning): + with pytest.warns(DeprecationWarning): rrule(DAILY, dtstart=datetime(1997, 9, 2, 9, 0), count=3, until=datetime(1997, 9, 4, 9, 0)) @@ -2472,6 +2496,12 @@ class RRuleTest(WarningTestMixin, unittest.TestCase): dtstart=datetime(1997, 9, 2, 9, 0)).count(), 3) + def testCountZero(self): + self.assertEqual(rrule(YEARLY, + count=0, + dtstart=datetime(1997, 9, 2, 9, 0)).count(), + 0) + def testContains(self): rr = rrule(DAILY, count=3, dtstart=datetime(1997, 9, 2, 9, 0)) self.assertEqual(datetime(1997, 9, 3, 9, 0) in rr, True) @@ -2644,6 +2674,70 @@ class RRuleTest(WarningTestMixin, unittest.TestCase): datetime(1998, 9, 2, 9, 0), datetime(1999, 9, 2, 9, 0)]) + def testStrWithTZID(self): + NYC = tz.gettz('America/New_York') + self.assertEqual(list(rrulestr( + "DTSTART;TZID=America/New_York:19970902T090000\n" + "RRULE:FREQ=YEARLY;COUNT=3\n" + )), + [datetime(1997, 9, 2, 9, 0, tzinfo=NYC), + datetime(1998, 9, 2, 9, 0, tzinfo=NYC), + datetime(1999, 9, 2, 9, 0, tzinfo=NYC)]) + + def testStrWithTZIDMapping(self): + rrstr = ("DTSTART;TZID=Eastern:19970902T090000\n" + + "RRULE:FREQ=YEARLY;COUNT=3") + + NYC = tz.gettz('America/New_York') + rr = rrulestr(rrstr, tzids={'Eastern': NYC}) + exp = [datetime(1997, 9, 2, 9, 0, tzinfo=NYC), + datetime(1998, 9, 2, 9, 0, tzinfo=NYC), + datetime(1999, 9, 2, 9, 0, tzinfo=NYC)] + + self.assertEqual(list(rr), exp) + + def testStrWithTZIDCallable(self): + rrstr = ('DTSTART;TZID=UTC+04:19970902T090000\n' + + 'RRULE:FREQ=YEARLY;COUNT=3') + + TZ = tz.tzstr('UTC+04') + def parse_tzstr(tzstr): + if tzstr is None: + raise ValueError('Invalid tzstr') + + return tz.tzstr(tzstr) + + rr = rrulestr(rrstr, tzids=parse_tzstr) + + exp = [datetime(1997, 9, 2, 9, 0, tzinfo=TZ), + datetime(1998, 9, 2, 9, 0, tzinfo=TZ), + datetime(1999, 9, 2, 9, 0, tzinfo=TZ),] + + self.assertEqual(list(rr), exp) + + def testStrWithTZIDCallableFailure(self): + rrstr = ('DTSTART;TZID=America/New_York:19970902T090000\n' + + 'RRULE:FREQ=YEARLY;COUNT=3') + + class TzInfoError(Exception): + pass + + def tzinfos(tzstr): + if tzstr == 'America/New_York': + raise TzInfoError('Invalid!') + return None + + with self.assertRaises(TzInfoError): + rrulestr(rrstr, tzids=tzinfos) + + def testStrWithConflictingTZID(self): + # RFC 5545 Section 3.3.5, FORM #2: DATE WITH UTC TIME + # https://tools.ietf.org/html/rfc5545#section-3.3.5 + # The "TZID" property parameter MUST NOT be applied to DATE-TIME + with self.assertRaises(ValueError): + rrulestr("DTSTART;TZID=America/New_York:19970902T090000Z\n"+ + "RRULE:FREQ=YEARLY;COUNT=3\n") + def testStrType(self): self.assertEqual(isinstance(rrulestr( "DTSTART:19970902T090000\n" @@ -2758,6 +2852,74 @@ class RRuleTest(WarningTestMixin, unittest.TestCase): datetime(1997, 9, 9, 9, 0), datetime(1997, 9, 16, 9, 0)]) + def testStrSetExDateMultiple(self): + rrstr = ("DTSTART:19970902T090000\n" + "RRULE:FREQ=YEARLY;COUNT=6;BYDAY=TU,TH\n" + "EXDATE:19970904T090000,19970911T090000,19970918T090000\n") + + rr = rrulestr(rrstr) + assert list(rr) == [datetime(1997, 9, 2, 9, 0), + datetime(1997, 9, 9, 9, 0), + datetime(1997, 9, 16, 9, 0)] + + def testStrSetExDateWithTZID(self): + BXL = tz.gettz('Europe/Brussels') + rr = rrulestr("DTSTART;TZID=Europe/Brussels:19970902T090000\n" + "RRULE:FREQ=YEARLY;COUNT=6;BYDAY=TU,TH\n" + "EXDATE;TZID=Europe/Brussels:19970904T090000\n" + "EXDATE;TZID=Europe/Brussels:19970911T090000\n" + "EXDATE;TZID=Europe/Brussels:19970918T090000\n") + + assert list(rr) == [datetime(1997, 9, 2, 9, 0, tzinfo=BXL), + datetime(1997, 9, 9, 9, 0, tzinfo=BXL), + datetime(1997, 9, 16, 9, 0, tzinfo=BXL)] + + def testStrSetExDateValueDateTimeNoTZID(self): + rrstr = '\n'.join([ + "DTSTART:19970902T090000", + "RRULE:FREQ=YEARLY;COUNT=4;BYDAY=TU,TH", + "EXDATE;VALUE=DATE-TIME:19970902T090000", + "EXDATE;VALUE=DATE-TIME:19970909T090000", + ]) + + rr = rrulestr(rrstr) + assert list(rr) == [datetime(1997, 9, 4, 9), datetime(1997, 9, 11, 9)] + + def testStrSetExDateValueMixDateTimeNoTZID(self): + rrstr = '\n'.join([ + "DTSTART:19970902T090000", + "RRULE:FREQ=YEARLY;COUNT=4;BYDAY=TU,TH", + "EXDATE;VALUE=DATE-TIME:19970902T090000", + "EXDATE:19970909T090000", + ]) + + rr = rrulestr(rrstr) + assert list(rr) == [datetime(1997, 9, 4, 9), datetime(1997, 9, 11, 9)] + + def testStrSetExDateValueDateTimeWithTZID(self): + BXL = tz.gettz('Europe/Brussels') + rrstr = '\n'.join([ + "DTSTART;VALUE=DATE-TIME;TZID=Europe/Brussels:19970902T090000", + "RRULE:FREQ=YEARLY;COUNT=4;BYDAY=TU,TH", + "EXDATE;VALUE=DATE-TIME;TZID=Europe/Brussels:19970902T090000", + "EXDATE;VALUE=DATE-TIME;TZID=Europe/Brussels:19970909T090000", + ]) + + rr = rrulestr(rrstr) + assert list(rr) == [datetime(1997, 9, 4, 9, tzinfo=BXL), + datetime(1997, 9, 11, 9, tzinfo=BXL)] + + def testStrSetExDateValueDate(self): + rrstr = '\n'.join([ + "DTSTART;VALUE=DATE:19970902", + "RRULE:FREQ=YEARLY;COUNT=4;BYDAY=TU,TH", + "EXDATE;VALUE=DATE:19970902", + "EXDATE;VALUE=DATE:19970909", + ]) + + rr = rrulestr(rrstr) + assert list(rr) == [datetime(1997, 9, 4), datetime(1997, 9, 11)] + def testStrSetDateAndExDate(self): self.assertEqual(list(rrulestr( "DTSTART:19970902T090000\n" @@ -2812,7 +2974,7 @@ class RRuleTest(WarningTestMixin, unittest.TestCase): def testStrUntil(self): self.assertEqual(list(rrulestr( - "DTSTART:19970902T090000\n" + "DTSTART:19970902T090000\n" "RRULE:FREQ=YEARLY;" "UNTIL=19990101T000000;BYDAY=1TU,-1TH\n" )), @@ -2820,12 +2982,45 @@ class RRuleTest(WarningTestMixin, unittest.TestCase): datetime(1998, 1, 6, 9, 0), datetime(1998, 12, 31, 9, 0)]) + def testStrValueDatetime(self): + rr = rrulestr("DTSTART;VALUE=DATE-TIME:19970902T090000\n" + "RRULE:FREQ=YEARLY;COUNT=2") + + self.assertEqual(list(rr), [datetime(1997, 9, 2, 9, 0, 0), + datetime(1998, 9, 2, 9, 0, 0)]) + + def testStrValueDate(self): + rr = rrulestr("DTSTART;VALUE=DATE:19970902\n" + "RRULE:FREQ=YEARLY;COUNT=2") + + self.assertEqual(list(rr), [datetime(1997, 9, 2, 0, 0, 0), + datetime(1998, 9, 2, 0, 0, 0)]) + + def testStrMultipleDTStartComma(self): + with pytest.raises(ValueError): + rr = rrulestr("DTSTART:19970101T000000,19970202T000000\n" + "RRULE:FREQ=YEARLY;COUNT=1") + def testStrInvalidUntil(self): with self.assertRaises(ValueError): list(rrulestr("DTSTART:19970902T090000\n" "RRULE:FREQ=YEARLY;" "UNTIL=TheCowsComeHome;BYDAY=1TU,-1TH\n")) + def testStrUntilMustBeUTC(self): + with self.assertRaises(ValueError): + list(rrulestr("DTSTART;TZID=America/New_York:19970902T090000\n" + "RRULE:FREQ=YEARLY;" + "UNTIL=19990101T000000;BYDAY=1TU,-1TH\n")) + + def testStrUntilWithTZ(self): + NYC = tz.gettz('America/New_York') + rr = list(rrulestr("DTSTART;TZID=America/New_York:19970101T000000\n" + "RRULE:FREQ=YEARLY;" + "UNTIL=19990101T000000Z\n")) + self.assertEqual(list(rr), [datetime(1997, 1, 1, 0, 0, 0, tzinfo=NYC), + datetime(1998, 1, 1, 0, 0, 0, tzinfo=NYC)]) + def testStrEmptyByDay(self): with self.assertRaises(ValueError): list(rrulestr("DTSTART:19970902T090000\n" @@ -2864,12 +3059,6 @@ class RRuleTest(WarningTestMixin, unittest.TestCase): dtstart=datetime(1997, 9, 2, 9, 0)) self._rrulestr_reverse_test(rule) - def testToStrYearlyByMonth(self): - rule = rrule(YEARLY, count=3, bymonth=(1, 3), - dtstart=datetime(1997, 9, 2, 9, 0)) - - self._rrulestr_reverse_test(rule) - def testToStrYearlyByMonth(self): self._rrulestr_reverse_test(rrule(YEARLY, count=3, @@ -4389,7 +4578,7 @@ class RRuleTest(WarningTestMixin, unittest.TestCase): dtstart=datetime(1997, 9, 2, 9, 0))) def testToStrLongIntegers(self): - if not PY3: # There is no longs in python3 + if PY2: # There are no longs in python3 self._rrulestr_reverse_test(rrule(MINUTELY, count=long(2), interval=long(2), @@ -4399,7 +4588,7 @@ class RRuleTest(WarningTestMixin, unittest.TestCase): byminute=long(6), bysecond=long(6), dtstart=datetime(1997, 9, 2, 9, 0))) - + self._rrulestr_reverse_test(rrule(YEARLY, count=long(2), bymonthday=long(5), @@ -4426,6 +4615,31 @@ class RRuleTest(WarningTestMixin, unittest.TestCase): [datetime(1997, 1, 6)]) +@pytest.mark.rrule +@freeze_time(datetime(2018, 3, 6, 5, 36, tzinfo=tz.UTC)) +def test_generated_aware_dtstart(): + dtstart_exp = datetime(2018, 3, 6, 5, 36, tzinfo=tz.UTC) + UNTIL = datetime(2018, 3, 6, 8, 0, tzinfo=tz.UTC) + + rule_without_dtstart = rrule(freq=HOURLY, until=UNTIL) + rule_with_dtstart = rrule(freq=HOURLY, dtstart=dtstart_exp, until=UNTIL) + assert list(rule_without_dtstart) == list(rule_with_dtstart) + + +@pytest.mark.rrule +@pytest.mark.rrulestr +@pytest.mark.xfail(reason="rrulestr loses time zone, gh issue #637") +@freeze_time(datetime(2018, 3, 6, 5, 36, tzinfo=tz.UTC)) +def test_generated_aware_dtstart_rrulestr(): + rrule_without_dtstart = rrule(freq=HOURLY, + until=datetime(2018, 3, 6, 8, 0, + tzinfo=tz.UTC)) + rrule_r = rrulestr(str(rrule_without_dtstart)) + + assert list(rrule_r) == list(rrule_without_dtstart) + + +@pytest.mark.rruleset class RRuleSetTest(unittest.TestCase): def testSet(self): rrset = rruleset() @@ -4641,7 +4855,7 @@ class RRuleSetTest(unittest.TestCase): class WeekdayTest(unittest.TestCase): def testInvalidNthWeekday(self): with self.assertRaises(ValueError): - zeroth_friday = FR(0) + FR(0) def testWeekdayCallable(self): # Calling a weekday instance generates a new weekday instance with the @@ -4672,7 +4886,7 @@ class WeekdayTest(unittest.TestCase): self.n = n MO_Basic = BasicWeekday(0) - + self.assertNotEqual(MO, MO_Basic) self.assertNotEqual(MO(1), MO_Basic) @@ -4698,4 +4912,3 @@ class WeekdayTest(unittest.TestCase): for repstr, wday in zip(with_n_reprs, with_n_wdays): self.assertEqual(repr(wday), repstr) - diff --git a/libs/dateutil/test/test_tz.py b/libs/dateutil/test/test_tz.py index 4ca203661..e5e4772d9 100644 --- a/libs/dateutil/test/test_tz.py +++ b/libs/dateutil/test/test_tz.py @@ -1,29 +1,30 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals -from ._common import unittest, PicklableMixin -from ._common import total_seconds +from ._common import PicklableMixin from ._common import TZEnvContext, TZWinContext -from ._common import WarningTestMixin from ._common import ComparesEqual from datetime import datetime, timedelta from datetime import time as dt_time from datetime import tzinfo -from six import BytesIO, StringIO +from six import PY2 +from io import BytesIO, StringIO +import unittest -import os -import subprocess import sys import base64 import copy -import itertools +import gc +import weakref from functools import partial IS_WIN = sys.platform.startswith('win') +import pytest + # dateutil imports -from dateutil.relativedelta import relativedelta, SU +from dateutil.relativedelta import relativedelta, SU, TH from dateutil.parser import parse from dateutil import tz as tz from dateutil import zoneinfo @@ -152,6 +153,19 @@ END:DAYLIGHT END:VTIMEZONE """ +EST_TUPLE = ('EST', timedelta(hours=-5), timedelta(hours=0)) +EDT_TUPLE = ('EDT', timedelta(hours=-4), timedelta(hours=1)) + +SUPPORTS_SUB_MINUTE_OFFSETS = sys.version_info >= (3, 6) + + +### +# Helper functions +def get_timezone_tuple(dt): + """Retrieve a (tzname, utcoffset, dst) tuple for a given DST""" + return dt.tzname(), dt.utcoffset(), dt.dst() + + ### # Mix-ins class context_passthrough(object): @@ -181,15 +195,13 @@ class TzFoldMixin(object): tzname = self._get_tzname('Australia/Sydney') with self._gettz_context(tzname): - SYD0 = self.gettz(tzname) - SYD1 = self.gettz(tzname) + SYD = self.gettz(tzname) - t0_u = datetime(2012, 3, 31, 15, 30, tzinfo=tz.tzutc()) # AEST - t1_u = datetime(2012, 3, 31, 16, 30, tzinfo=tz.tzutc()) # AEDT + t0_u = datetime(2012, 3, 31, 15, 30, tzinfo=tz.UTC) # AEST + t1_u = datetime(2012, 3, 31, 16, 30, tzinfo=tz.UTC) # AEDT - # Using fresh tzfiles - t0_syd0 = t0_u.astimezone(SYD0) - t1_syd1 = t1_u.astimezone(SYD1) + t0_syd0 = t0_u.astimezone(SYD) + t1_syd1 = t1_u.astimezone(SYD) self.assertEqual(t0_syd0.replace(tzinfo=None), datetime(2012, 4, 1, 2, 30)) @@ -200,21 +212,18 @@ class TzFoldMixin(object): self.assertEqual(t0_syd0.utcoffset(), timedelta(hours=11)) self.assertEqual(t1_syd1.utcoffset(), timedelta(hours=10)) - def testGapPositiveUTCOffset(self): # Test that we don't have a problem around gaps. tzname = self._get_tzname('Australia/Sydney') with self._gettz_context(tzname): - SYD0 = self.gettz(tzname) - SYD1 = self.gettz(tzname) + SYD = self.gettz(tzname) - t0_u = datetime(2012, 10, 6, 15, 30, tzinfo=tz.tzutc()) # AEST - t1_u = datetime(2012, 10, 6, 16, 30, tzinfo=tz.tzutc()) # AEDT + t0_u = datetime(2012, 10, 6, 15, 30, tzinfo=tz.UTC) # AEST + t1_u = datetime(2012, 10, 6, 16, 30, tzinfo=tz.UTC) # AEDT - # Using fresh tzfiles - t0 = t0_u.astimezone(SYD0) - t1 = t1_u.astimezone(SYD1) + t0 = t0_u.astimezone(SYD) + t1 = t1_u.astimezone(SYD) self.assertEqual(t0.replace(tzinfo=None), datetime(2012, 10, 7, 1, 30)) @@ -230,41 +239,36 @@ class TzFoldMixin(object): tzname = self._get_tzname('America/Toronto') with self._gettz_context(tzname): - # Calling fromutc() alters the tzfile object - TOR0 = self.gettz(tzname) - TOR1 = self.gettz(tzname) + TOR = self.gettz(tzname) - t0_u = datetime(2011, 11, 6, 5, 30, tzinfo=tz.tzutc()) - t1_u = datetime(2011, 11, 6, 6, 30, tzinfo=tz.tzutc()) + t0_u = datetime(2011, 11, 6, 5, 30, tzinfo=tz.UTC) + t1_u = datetime(2011, 11, 6, 6, 30, tzinfo=tz.UTC) - # Using fresh tzfiles - t0_tor0 = t0_u.astimezone(TOR0) - t1_tor1 = t1_u.astimezone(TOR1) + t0_tor = t0_u.astimezone(TOR) + t1_tor = t1_u.astimezone(TOR) - self.assertEqual(t0_tor0.replace(tzinfo=None), + self.assertEqual(t0_tor.replace(tzinfo=None), datetime(2011, 11, 6, 1, 30)) - self.assertEqual(t1_tor1.replace(tzinfo=None), + self.assertEqual(t1_tor.replace(tzinfo=None), datetime(2011, 11, 6, 1, 30)) - self.assertEqual(t0_tor0.utcoffset(), timedelta(hours=-4.0)) - self.assertEqual(t1_tor1.utcoffset(), timedelta(hours=-5.0)) + self.assertNotEqual(t0_tor.tzname(), t1_tor.tzname()) + self.assertEqual(t0_tor.utcoffset(), timedelta(hours=-4.0)) + self.assertEqual(t1_tor.utcoffset(), timedelta(hours=-5.0)) def testGapNegativeUTCOffset(self): # Test that we don't have a problem around gaps. tzname = self._get_tzname('America/Toronto') with self._gettz_context(tzname): - # Calling fromutc() alters the tzfile object - TOR0 = self.gettz(tzname) - TOR1 = self.gettz(tzname) + TOR = self.gettz(tzname) - t0_u = datetime(2011, 3, 13, 6, 30, tzinfo=tz.tzutc()) - t1_u = datetime(2011, 3, 13, 7, 30, tzinfo=tz.tzutc()) + t0_u = datetime(2011, 3, 13, 6, 30, tzinfo=tz.UTC) + t1_u = datetime(2011, 3, 13, 7, 30, tzinfo=tz.UTC) - # Using fresh tzfiles - t0 = t0_u.astimezone(TOR0) - t1 = t1_u.astimezone(TOR1) + t0 = t0_u.astimezone(TOR) + t1 = t1_u.astimezone(TOR) self.assertEqual(t0.replace(tzinfo=None), datetime(2011, 3, 13, 1, 30)) @@ -276,22 +280,39 @@ class TzFoldMixin(object): self.assertEqual(t0.utcoffset(), timedelta(hours=-5.0)) self.assertEqual(t1.utcoffset(), timedelta(hours=-4.0)) + def testFoldLondon(self): + tzname = self._get_tzname('Europe/London') + + with self._gettz_context(tzname): + LON = self.gettz(tzname) + UTC = tz.UTC + + t0_u = datetime(2013, 10, 27, 0, 30, tzinfo=UTC) # BST + t1_u = datetime(2013, 10, 27, 1, 30, tzinfo=UTC) # GMT + + t0 = t0_u.astimezone(LON) + t1 = t1_u.astimezone(LON) + + self.assertEqual(t0.replace(tzinfo=None), + datetime(2013, 10, 27, 1, 30)) + + self.assertEqual(t1.replace(tzinfo=None), + datetime(2013, 10, 27, 1, 30)) + + self.assertEqual(t0.utcoffset(), timedelta(hours=1)) + self.assertEqual(t1.utcoffset(), timedelta(hours=0)) + def testFoldIndependence(self): tzname = self._get_tzname('America/New_York') with self._gettz_context(tzname): NYC = self.gettz(tzname) - UTC = tz.tzutc() + UTC = tz.UTC hour = timedelta(hours=1) # Firmly 2015-11-01 0:30 EDT-4 pre_dst = datetime(2015, 11, 1, 0, 30, tzinfo=NYC) - # Currently, there's no way around the fact that this resolves to an - # ambiguous date, which defaults to EST. I'm not hard-coding in the - # answer, though, because the preferred behavior would be that this - # results in a time on the EDT side. - # Ambiguous between 2015-11-01 1:30 EDT-4 and 2015-11-01 1:30 EST-5 in_dst = pre_dst + hour in_dst_tzname_0 = in_dst.tzname() # Stash the tzname - EDT @@ -309,6 +330,25 @@ class TzFoldMixin(object): # Now check to make sure in_dst's tzname hasn't changed self.assertEqual(in_dst_tzname_0, in_dst.tzname()) + def testInZoneFoldEquality(self): + # Two datetimes in the same zone are considered to be equal if their + # wall times are equal, even if they have different absolute times. + + tzname = self._get_tzname('America/New_York') + + with self._gettz_context(tzname): + NYC = self.gettz(tzname) + UTC = tz.UTC + + dt0 = datetime(2011, 11, 6, 1, 30, tzinfo=NYC) + dt1 = tz.enfold(dt0, fold=1) + + # Make sure these actually represent different times + self.assertNotEqual(dt0.astimezone(UTC), dt1.astimezone(UTC)) + + # Test that they compare equal + self.assertEqual(dt0, dt1) + def _test_ambiguous_time(self, dt, tzid, ambiguous): # This is a test to check that the individual is_ambiguous values # on the _tzinfo subclasses work. @@ -384,8 +424,7 @@ class TzFoldMixin(object): SYD0 = self.gettz(tzname) SYD1 = self.gettz(tzname) - t0_u = datetime(2012, 3, 31, 14, 30, tzinfo=tz.tzutc()) # AEST - t1_u = datetime(2012, 3, 31, 16, 30, tzinfo=tz.tzutc()) # AEDT + t0_u = datetime(2012, 3, 31, 14, 30, tzinfo=tz.UTC) # AEST t0_syd0 = t0_u.astimezone(SYD0) t0_syd1 = t0_u.astimezone(SYD1) @@ -414,13 +453,13 @@ class TzWinFoldMixin(object): if gap: t_n = dston - timedelta(minutes=30) - t0_u = t_n.replace(tzinfo=tzi).astimezone(tz.tzutc()) + t0_u = t_n.replace(tzinfo=tzi).astimezone(tz.UTC) t1_u = t0_u + timedelta(hours=1) else: # Get 1 hour before the first ambiguous date t_n = dstoff - timedelta(minutes=30) - t0_u = t_n.replace(tzinfo=tzi).astimezone(tz.tzutc()) + t0_u = t_n.replace(tzinfo=tzi).astimezone(tz.UTC) t_n += timedelta(hours=1) # Naive ambiguous date t0_u = t0_u + timedelta(hours=1) # First ambiguous date t1_u = t0_u + timedelta(hours=1) # Second ambiguous date @@ -435,33 +474,22 @@ class TzWinFoldMixin(object): with self.context(tzname): # Calling fromutc() alters the tzfile object SYD = self.tzclass(*args) - SYD0 = self.tzclass(*args) - SYD1 = self.tzclass(*args) - - self.assertIsNot(SYD0, SYD1) # Get the transition time in UTC from the object, because # Windows doesn't store historical info - t_n, t0_u, t1_u = self.get_utc_transitions(SYD0, 2012, False) + t_n, t0_u, t1_u = self.get_utc_transitions(SYD, 2012, False) # Using fresh tzfiles - t0_syd0 = t0_u.astimezone(SYD0) - t1_syd1 = t1_u.astimezone(SYD1) + t0_syd = t0_u.astimezone(SYD) + t1_syd = t1_u.astimezone(SYD) - self.assertEqual(t0_syd0.replace(tzinfo=None), t_n) + self.assertEqual(t0_syd.replace(tzinfo=None), t_n) - self.assertEqual(t1_syd1.replace(tzinfo=None), t_n) + self.assertEqual(t1_syd.replace(tzinfo=None), t_n) - self.assertNotEqual(t0_syd0, t1_syd1) - self.assertEqual(t0_syd0.utcoffset(), timedelta(hours=11)) - self.assertEqual(t1_syd1.utcoffset(), timedelta(hours=10)) - - # Re-using them across (make sure there's no cache problem) - t0_syd1 = t0_u.astimezone(SYD1) - t1_syd0 = t1_u.astimezone(SYD0) - - self.assertEqual(t0_syd0, t0_syd1) - self.assertEqual(t1_syd1, t1_syd0) + self.assertEqual(t0_syd.utcoffset(), timedelta(hours=11)) + self.assertEqual(t1_syd.utcoffset(), timedelta(hours=10)) + self.assertNotEqual(t0_syd.tzname(), t1_syd.tzname()) def testGapPositiveUTCOffset(self): # Test that we don't have a problem around gaps. @@ -469,18 +497,12 @@ class TzWinFoldMixin(object): args = self.get_args(tzname) with self.context(tzname): - # Calling fromutc() alters the tzfile object SYD = self.tzclass(*args) - SYD0 = self.tzclass(*args) - SYD1 = self.tzclass(*args) - - self.assertIsNot(SYD0, SYD1) t_n, t0_u, t1_u = self.get_utc_transitions(SYD, 2012, True) - # Using fresh tzfiles - t0 = t0_u.astimezone(SYD0) - t1 = t1_u.astimezone(SYD1) + t0 = t0_u.astimezone(SYD) + t1 = t1_u.astimezone(SYD) self.assertEqual(t0.replace(tzinfo=None), t_n) @@ -494,48 +516,33 @@ class TzWinFoldMixin(object): tzname = 'Eastern Standard Time' args = self.get_args(tzname) - # Calling fromutc() alters the tzfile object with self.context(tzname): TOR = self.tzclass(*args) - TOR0 = self.tzclass(*args) - TOR1 = self.tzclass(*args) t_n, t0_u, t1_u = self.get_utc_transitions(TOR, 2011, False) - # Using fresh tzfiles - t0_tor0 = t0_u.astimezone(TOR0) - t1_tor1 = t1_u.astimezone(TOR1) + t0_tor = t0_u.astimezone(TOR) + t1_tor = t1_u.astimezone(TOR) - self.assertEqual(t0_tor0.replace(tzinfo=None), t_n) - self.assertEqual(t1_tor1.replace(tzinfo=None), t_n) + self.assertEqual(t0_tor.replace(tzinfo=None), t_n) + self.assertEqual(t1_tor.replace(tzinfo=None), t_n) - self.assertNotEqual(t0_tor0.tzname(), t1_tor1.tzname()) - self.assertEqual(t0_tor0.utcoffset(), timedelta(hours=-4.0)) - self.assertEqual(t1_tor1.utcoffset(), timedelta(hours=-5.0)) - - # Re-using them across (make sure there's no cache problem) - t0_tor1 = t0_u.astimezone(TOR1) - t1_tor0 = t1_u.astimezone(TOR0) - - self.assertEqual(t0_tor0, t0_tor1) - self.assertEqual(t1_tor1, t1_tor0) + self.assertNotEqual(t0_tor.tzname(), t1_tor.tzname()) + self.assertEqual(t0_tor.utcoffset(), timedelta(hours=-4.0)) + self.assertEqual(t1_tor.utcoffset(), timedelta(hours=-5.0)) def testGapNegativeUTCOffset(self): # Test that we don't have a problem around gaps. tzname = 'Eastern Standard Time' args = self.get_args(tzname) - # Calling fromutc() alters the tzfile object with self.context(tzname): TOR = self.tzclass(*args) - TOR0 = self.tzclass(*args) - TOR1 = self.tzclass(*args) t_n, t0_u, t1_u = self.get_utc_transitions(TOR, 2011, True) - # Using fresh tzfiles - t0 = t0_u.astimezone(TOR0) - t1 = t1_u.astimezone(TOR1) + t0 = t0_u.astimezone(TOR) + t1 = t1_u.astimezone(TOR) self.assertEqual(t0.replace(tzinfo=None), t_n) @@ -543,7 +550,7 @@ class TzWinFoldMixin(object): self.assertEqual(t1.replace(tzinfo=None), t_n + timedelta(hours=2)) - self.assertNotEqual(t0, t1) + self.assertNotEqual(t0.tzname(), t1.tzname()) self.assertEqual(t0.utcoffset(), timedelta(hours=-5.0)) self.assertEqual(t1.utcoffset(), timedelta(hours=-4.0)) @@ -553,7 +560,7 @@ class TzWinFoldMixin(object): with self.context(tzname): NYC = self.tzclass(*args) - UTC = tz.tzutc() + UTC = tz.UTC hour = timedelta(hours=1) # Firmly 2015-11-01 0:30 EDT-4 @@ -580,10 +587,36 @@ class TzWinFoldMixin(object): # Now check to make sure in_dst's tzname hasn't changed self.assertEqual(in_dst_tzname_0, in_dst.tzname()) + def testInZoneFoldEquality(self): + # Two datetimes in the same zone are considered to be equal if their + # wall times are equal, even if they have different absolute times. + tzname = 'Eastern Standard Time' + args = self.get_args(tzname) + + with self.context(tzname): + NYC = self.tzclass(*args) + UTC = tz.UTC + + t_n, t0_u, t1_u = self.get_utc_transitions(NYC, 2011, False) + + dt0 = t_n.replace(tzinfo=NYC) + dt1 = tz.enfold(dt0, fold=1) + + # Make sure these actually represent different times + self.assertNotEqual(dt0.astimezone(UTC), dt1.astimezone(UTC)) + + # Test that they compare equal + self.assertEqual(dt0, dt1) ### # Test Cases class TzUTCTest(unittest.TestCase): + def testSingleton(self): + UTC_0 = tz.tzutc() + UTC_1 = tz.tzutc() + + self.assertIs(UTC_0, UTC_1) + def testOffset(self): ct = datetime(2009, 4, 1, 12, 11, 13, tzinfo=tz.tzutc()) @@ -602,7 +635,6 @@ class TzUTCTest(unittest.TestCase): UTC0 = tz.tzutc() UTC1 = tz.tzutc() - self.assertIsNot(UTC0, UTC1) self.assertEqual(UTC0, UTC1) def testInequality(self): @@ -636,6 +668,7 @@ class TzUTCTest(unittest.TestCase): self.assertFalse(tz.datetime_ambiguous(dt)) +@pytest.mark.tzoffset class TzOffsetTest(unittest.TestCase): def testTimedeltaOffset(self): est = tz.tzoffset('EST', timedelta(hours=-5)) @@ -659,7 +692,6 @@ class TzOffsetTest(unittest.TestCase): tzo = tz.tzoffset(tname, -5 * 3600) self.assertEqual(repr(tzo), "tzoffset(" + repr(tname) + ", -18000)") - def testEquality(self): utc = tz.tzoffset('UTC', 0) gmt = tz.tzoffset('GMT', 0) @@ -667,7 +699,7 @@ class TzOffsetTest(unittest.TestCase): self.assertEqual(utc, gmt) def testUTCEquality(self): - utc = tz.tzutc() + utc = tz.UTC o_utc = tz.tzoffset('UTC', 0) self.assertEqual(utc, o_utc) @@ -691,7 +723,73 @@ class TzOffsetTest(unittest.TestCase): self.assertFalse(tz.datetime_ambiguous(dt)) + def testTzOffsetInstance(self): + tz1 = tz.tzoffset.instance('EST', timedelta(hours=-5)) + tz2 = tz.tzoffset.instance('EST', timedelta(hours=-5)) + assert tz1 is not tz2 + + def testTzOffsetSingletonDifferent(self): + tz1 = tz.tzoffset('EST', timedelta(hours=-5)) + tz2 = tz.tzoffset('EST', -18000) + + assert tz1 is tz2 + + +@pytest.mark.smoke +@pytest.mark.tzoffset +def test_tzoffset_weakref(): + UTC1 = tz.tzoffset('UTC', 0) + UTC_ref = weakref.ref(tz.tzoffset('UTC', 0)) + UTC1 is UTC_ref() + del UTC1 + gc.collect() + + assert UTC_ref() is not None # Should be in the strong cache + assert UTC_ref() is tz.tzoffset('UTC', 0) + + # Fill the strong cache with other items + for offset in range(5,15): + tz.tzoffset('RandomZone', offset) + + gc.collect() + assert UTC_ref() is None + assert UTC_ref() is not tz.tzoffset('UTC', 0) + + +@pytest.mark.tzoffset +@pytest.mark.parametrize('args', [ + ('UTC', 0), + ('EST', -18000), + ('EST', timedelta(hours=-5)), + (None, timedelta(hours=3)), +]) +def test_tzoffset_singleton(args): + tz1 = tz.tzoffset(*args) + tz2 = tz.tzoffset(*args) + + assert tz1 is tz2 + + +@pytest.mark.tzoffset +@pytest.mark.skipif(not SUPPORTS_SUB_MINUTE_OFFSETS, + reason='Sub-minute offsets not supported') +def test_tzoffset_sub_minute(): + delta = timedelta(hours=12, seconds=30) + test_datetime = datetime(2000, 1, 1, tzinfo=tz.tzoffset(None, delta)) + assert test_datetime.utcoffset() == delta + + +@pytest.mark.tzoffset +@pytest.mark.skipif(SUPPORTS_SUB_MINUTE_OFFSETS, + reason='Sub-minute offsets supported') +def test_tzoffset_sub_minute_rounding(): + delta = timedelta(hours=12, seconds=30) + test_date = datetime(2000, 1, 1, tzinfo=tz.tzoffset(None, delta)) + assert test_date.utcoffset() == timedelta(hours=12, minutes=1) + + +@pytest.mark.tzlocal class TzLocalTest(unittest.TestCase): def testEquality(self): tz1 = tz.tzlocal() @@ -703,8 +801,8 @@ class TzLocalTest(unittest.TestCase): def testInequalityFixedOffset(self): tzl = tz.tzlocal() - tzos = tz.tzoffset('LST', total_seconds(tzl._std_offset)) - tzod = tz.tzoffset('LDT', total_seconds(tzl._std_offset)) + tzos = tz.tzoffset('LST', tzl._std_offset.total_seconds()) + tzod = tz.tzoffset('LDT', tzl._std_offset.total_seconds()) self.assertFalse(tzl == tzos) self.assertFalse(tzl == tzod) @@ -713,12 +811,15 @@ class TzLocalTest(unittest.TestCase): def testInequalityInvalid(self): tzl = tz.tzlocal() - UTC = tz.tzutc() self.assertTrue(tzl != 1) - self.assertTrue(tzl != tz.tzutc()) self.assertFalse(tzl == 1) - self.assertFalse(tzl == UTC) + + # TODO: Use some sort of universal local mocking so that it's clear + # that we're expecting tzlocal to *not* be Pacific/Kiritimati + LINT = tz.gettz('Pacific/Kiritimati') + self.assertTrue(tzl != LINT) + self.assertFalse(tzl == LINT) def testInequalityUnsupported(self): tzl = tz.tzlocal() @@ -732,9 +833,24 @@ class TzLocalTest(unittest.TestCase): self.assertEqual(repr(tzl), 'tzlocal()') +@pytest.mark.parametrize('args,kwargs', [ + (('EST', -18000), {}), + (('EST', timedelta(hours=-5)), {}), + (('EST',), {'offset': -18000}), + (('EST',), {'offset': timedelta(hours=-5)}), + (tuple(), {'name': 'EST', 'offset': -18000}) +]) +def test_tzoffset_is(args, kwargs): + tz_ref = tz.tzoffset('EST', -18000) + assert tz.tzoffset(*args, **kwargs) is tz_ref + + +def test_tzoffset_is_not(): + assert tz.tzoffset('EDT', -14400) is not tz.tzoffset('EST', -18000) + + +@pytest.mark.tzlocal @unittest.skipIf(IS_WIN, "requires Unix") -@unittest.skipUnless(TZEnvContext.tz_change_allowed(), - TZEnvContext.tz_change_disallowed_message()) class TzLocalNixTest(unittest.TestCase, TzFoldMixin): # This is a set of tests for `tzlocal()` on *nix systems @@ -745,6 +861,9 @@ class TzLocalNixTest(unittest.TestCase, TzFoldMixin): # POSIX string for AEST/AEDT (valid >= 2008) TZ_AEST = 'AEST-10AEDT,M10.1.0/2,M4.1.0/3' + # POSIX string for BST/GMT + TZ_LON = 'GMT0BST,M3.5.0,M10.5.0' + # POSIX string for UTC UTC = 'UTC' @@ -755,7 +874,8 @@ class TzLocalNixTest(unittest.TestCase, TzFoldMixin): def _gettz_context(self, tzname): tzname_map = {'Australia/Sydney': self.TZ_AEST, 'America/Toronto': self.TZ_EST, - 'America/New_York': self.TZ_EST} + 'America/New_York': self.TZ_EST, + 'Europe/London': self.TZ_LON} return TZEnvContext(tzname_map.get(tzname, tzname)) @@ -830,7 +950,76 @@ class TzLocalNixTest(unittest.TestCase, TzFoldMixin): self.assertIs(dt_time(13, 20, tzinfo=tz.tzlocal()).dst(), None) + def testUTCEquality(self): + with TZEnvContext(self.UTC): + assert tz.tzlocal() == tz.UTC + +# TODO: Maybe a better hack than this? +def mark_tzlocal_nix(f): + marks = [ + pytest.mark.tzlocal, + pytest.mark.skipif(IS_WIN, reason='requires Unix'), + ] + + for mark in reversed(marks): + f = mark(f) + + return f + + +@mark_tzlocal_nix +@pytest.mark.parametrize('tzvar', ['UTC', 'GMT0', 'UTC0']) +def test_tzlocal_utc_equal(tzvar): + with TZEnvContext(tzvar): + assert tz.tzlocal() == tz.UTC + + +@mark_tzlocal_nix +@pytest.mark.parametrize('tzvar', [ + 'Europe/London', 'America/New_York', + 'GMT0BST', 'EST5EDT']) +def test_tzlocal_utc_unequal(tzvar): + with TZEnvContext(tzvar): + assert tz.tzlocal() != tz.UTC + + +@mark_tzlocal_nix +def test_tzlocal_local_time_trim_colon(): + with TZEnvContext(':/etc/localtime'): + assert tz.gettz() is not None + + +@mark_tzlocal_nix +@pytest.mark.parametrize('tzvar, tzoff', [ + ('EST5', tz.tzoffset('EST', -18000)), + ('GMT0', tz.tzoffset('GMT', 0)), + ('YAKT-9', tz.tzoffset('YAKT', timedelta(hours=9))), + ('JST-9', tz.tzoffset('JST', timedelta(hours=9))), +]) +def test_tzlocal_offset_equal(tzvar, tzoff): + with TZEnvContext(tzvar): + # Including both to test both __eq__ and __ne__ + assert tz.tzlocal() == tzoff + assert not (tz.tzlocal() != tzoff) + + +@mark_tzlocal_nix +@pytest.mark.parametrize('tzvar, tzoff', [ + ('EST5EDT', tz.tzoffset('EST', -18000)), + ('GMT0BST', tz.tzoffset('GMT', 0)), + ('EST5', tz.tzoffset('EST', -14400)), + ('YAKT-9', tz.tzoffset('JST', timedelta(hours=9))), + ('JST-9', tz.tzoffset('YAKT', timedelta(hours=9))), +]) +def test_tzlocal_offset_unequal(tzvar, tzoff): + with TZEnvContext(tzvar): + # Including both to test both __eq__ and __ne__ + assert tz.tzlocal() != tzoff + assert not (tz.tzlocal() == tzoff) + + +@pytest.mark.gettz class GettzTest(unittest.TestCase, TzFoldMixin): gettz = staticmethod(tz.gettz) @@ -877,8 +1066,134 @@ class GettzTest(unittest.TestCase, TzFoldMixin): self.assertEqual(t_west.utcoffset(), timedelta(hours=1)) self.assertEqual(t_west.dst(), timedelta(hours=1)) + def testGettzCacheTzFile(self): + NYC1 = tz.gettz('America/New_York') + NYC2 = tz.gettz('America/New_York') -class ZoneInfoGettzTest(GettzTest, WarningTestMixin): + assert NYC1 is NYC2 + + def testGettzCacheTzLocal(self): + local1 = tz.gettz() + local2 = tz.gettz() + + assert local1 is not local2 + + +@pytest.mark.gettz +def test_gettz_same_result_for_none_and_empty_string(): + local_from_none = tz.gettz() + local_from_empty_string = tz.gettz("") + assert local_from_none is not None + assert local_from_empty_string is not None + assert local_from_none == local_from_empty_string + + +@pytest.mark.gettz +@pytest.mark.parametrize('badzone', [ + 'Fake.Region/Abcdefghijklmnop', # Violates several tz project name rules +]) +def test_gettz_badzone(badzone): + # Make sure passing a bad TZ string to gettz returns None (GH #800) + tzi = tz.gettz(badzone) + assert tzi is None + + +@pytest.mark.gettz +def test_gettz_badzone_unicode(): + # Make sure a unicode string can be passed to TZ (GH #802) + # When fixed, combine this with test_gettz_badzone + tzi = tz.gettz('ðŸ¼') + assert tzi is None + + +@pytest.mark.gettz +@pytest.mark.parametrize( + "badzone,exc_reason", + [ + pytest.param( + b"America/New_York", + ".*should be str, not bytes.*", + id="bytes on Python 3", + marks=[ + pytest.mark.skipif( + PY2, reason="bytes arguments accepted in Python 2" + ) + ], + ), + pytest.param( + object(), + None, + id="no startswith()", + marks=[ + pytest.mark.xfail(reason="AttributeError instead of TypeError", + raises=AttributeError), + ], + ), + ], +) +def test_gettz_zone_wrong_type(badzone, exc_reason): + with pytest.raises(TypeError, match=exc_reason): + tz.gettz(badzone) + + +@pytest.mark.gettz +@pytest.mark.xfail(IS_WIN, reason='zoneinfo separately cached') +def test_gettz_cache_clear(): + NYC1 = tz.gettz('America/New_York') + tz.gettz.cache_clear() + + NYC2 = tz.gettz('America/New_York') + + assert NYC1 is not NYC2 + +@pytest.mark.gettz +@pytest.mark.xfail(IS_WIN, reason='zoneinfo separately cached') +def test_gettz_set_cache_size(): + tz.gettz.cache_clear() + tz.gettz.set_cache_size(3) + + MONACO_ref = weakref.ref(tz.gettz('Europe/Monaco')) + EASTER_ref = weakref.ref(tz.gettz('Pacific/Easter')) + CURRIE_ref = weakref.ref(tz.gettz('Australia/Currie')) + + gc.collect() + + assert MONACO_ref() is not None + assert EASTER_ref() is not None + assert CURRIE_ref() is not None + + tz.gettz.set_cache_size(2) + gc.collect() + + assert MONACO_ref() is None + +@pytest.mark.xfail(IS_WIN, reason="Windows does not use system zoneinfo") +@pytest.mark.smoke +@pytest.mark.gettz +def test_gettz_weakref(): + tz.gettz.cache_clear() + tz.gettz.set_cache_size(2) + NYC1 = tz.gettz('America/New_York') + NYC_ref = weakref.ref(tz.gettz('America/New_York')) + + assert NYC1 is NYC_ref() + + del NYC1 + gc.collect() + + assert NYC_ref() is not None # Should still be in the strong cache + assert tz.gettz('America/New_York') is NYC_ref() + + # Populate strong cache with other timezones + tz.gettz('Europe/Monaco') + tz.gettz('Pacific/Easter') + tz.gettz('Australia/Currie') + + gc.collect() + assert NYC_ref() is None # Should have been pushed out + assert tz.gettz('America/New_York') is not NYC_ref() + +class ZoneInfoGettzTest(GettzTest): def gettz(self, name): zoneinfo_file = zoneinfo.get_zonefile_instance() return zoneinfo_file.get(name) @@ -938,12 +1253,12 @@ class ZoneInfoGettzTest(GettzTest, WarningTestMixin): self.assertIs(zif_1, zif_2) def testZoneInfoDeprecated(self): - with self.assertWarns(DeprecationWarning): - tzi = zoneinfo.gettz('US/Eastern') + with pytest.warns(DeprecationWarning): + zoneinfo.gettz('US/Eastern') def testZoneInfoMetadataDeprecated(self): - with self.assertWarns(DeprecationWarning): - tzdb_md = zoneinfo.gettz_db_metadata() + with pytest.warns(DeprecationWarning): + zoneinfo.gettz_db_metadata() class TZRangeTest(unittest.TestCase, TzFoldMixin): @@ -960,13 +1275,21 @@ class TZRangeTest(unittest.TestCase, TzFoldMixin): weekday=SU(+1)), end=relativedelta(month=4, day=1, hour=2, weekday=SU(+1))) + + TZ_LON = tz.tzrange('GMT', timedelta(hours=0), + 'BST', timedelta(hours=1), + start=relativedelta(month=3, day=31, weekday=SU(-1), + hours=2), + end=relativedelta(month=10, day=31, weekday=SU(-1), + hours=1)) # POSIX string for UTC UTC = 'UTC' def gettz(self, tzname): tzname_map = {'Australia/Sydney': self.TZ_AEST, 'America/Toronto': self.TZ_EST, - 'America/New_York': self.TZ_EST} + 'America/New_York': self.TZ_EST, + 'Europe/London': self.TZ_LON} return tzname_map[tzname] @@ -1027,7 +1350,7 @@ class TZRangeTest(unittest.TestCase, TzFoldMixin): def testBrokenIsDstHandling(self): # tzrange._isdst() was using a date() rather than a datetime(). # Issue reported by Lennart Regebro. - dt = datetime(2007, 8, 6, 4, 10, tzinfo=tz.tzutc()) + dt = datetime(2007, 8, 6, 4, 10, tzinfo=tz.UTC) self.assertEqual(dt.astimezone(tz=tz.gettz("GMT+2")), datetime(2007, 8, 6, 6, 10, tzinfo=tz.tzstr("GMT+2"))) @@ -1080,6 +1403,7 @@ class TZRangeTest(unittest.TestCase, TzFoldMixin): self.assertFalse(TZR != ComparesEqual) +@pytest.mark.tzstr class TZStrTest(unittest.TestCase, TzFoldMixin): # POSIX string indicating change to summer time on the 2nd Sunday in March # at 2AM, and ending the 1st Sunday in November at 2AM. (valid >= 2007) @@ -1088,106 +1412,18 @@ class TZStrTest(unittest.TestCase, TzFoldMixin): # POSIX string for AEST/AEDT (valid >= 2008) TZ_AEST = 'AEST-10AEDT,M10.1.0/2,M4.1.0/3' + # POSIX string for GMT/BST + TZ_LON = 'GMT0BST,M3.5.0,M10.5.0' + def gettz(self, tzname): # Actual time zone changes are handled by the _gettz_context function tzname_map = {'Australia/Sydney': self.TZ_AEST, 'America/Toronto': self.TZ_EST, - 'America/New_York': self.TZ_EST} + 'America/New_York': self.TZ_EST, + 'Europe/London': self.TZ_LON} return tz.tzstr(tzname_map[tzname]) - def testStrStart1(self): - self.assertEqual(datetime(2003, 4, 6, 1, 59, - tzinfo=tz.tzstr("EST5EDT")).tzname(), "EST") - self.assertEqual(datetime(2003, 4, 6, 2, 00, - tzinfo=tz.tzstr("EST5EDT")).tzname(), "EDT") - - def testStrEnd1(self): - self.assertEqual(datetime(2003, 10, 26, 0, 59, - tzinfo=tz.tzstr("EST5EDT")).tzname(), "EDT") - - end = tz.enfold(datetime(2003, 10, 26, 1, 00, - tzinfo=tz.tzstr("EST5EDT")), fold=1) - self.assertEqual(end.tzname(), "EST") - - def testStrStart2(self): - s = "EST5EDT,4,0,6,7200,10,0,26,7200,3600" - self.assertEqual(datetime(2003, 4, 6, 1, 59, - tzinfo=tz.tzstr(s)).tzname(), "EST") - self.assertEqual(datetime(2003, 4, 6, 2, 00, - tzinfo=tz.tzstr(s)).tzname(), "EDT") - - def testStrEnd2(self): - s = "EST5EDT,4,0,6,7200,10,0,26,7200,3600" - self.assertEqual(datetime(2003, 10, 26, 0, 59, - tzinfo=tz.tzstr(s)).tzname(), "EDT") - - end = tz.enfold(datetime(2003, 10, 26, 1, 00, - tzinfo=tz.tzstr(s)), fold=1) - self.assertEqual(end.tzname(), "EST") - - def testStrStart3(self): - s = "EST5EDT,4,1,0,7200,10,-1,0,7200,3600" - self.assertEqual(datetime(2003, 4, 6, 1, 59, - tzinfo=tz.tzstr(s)).tzname(), "EST") - self.assertEqual(datetime(2003, 4, 6, 2, 00, - tzinfo=tz.tzstr(s)).tzname(), "EDT") - - def testStrEnd3(self): - s = "EST5EDT,4,1,0,7200,10,-1,0,7200,3600" - self.assertEqual(datetime(2003, 10, 26, 0, 59, - tzinfo=tz.tzstr(s)).tzname(), "EDT") - - end = tz.enfold(datetime(2003, 10, 26, 1, 00, - tzinfo=tz.tzstr(s)), fold=1) - self.assertEqual(end.tzname(), "EST") - - def testStrStart4(self): - s = "EST5EDT4,M4.1.0/02:00:00,M10-5-0/02:00" - self.assertEqual(datetime(2003, 4, 6, 1, 59, - tzinfo=tz.tzstr(s)).tzname(), "EST") - self.assertEqual(datetime(2003, 4, 6, 2, 00, - tzinfo=tz.tzstr(s)).tzname(), "EDT") - - def testStrEnd4(self): - s = "EST5EDT4,M4.1.0/02:00:00,M10-5-0/02:00" - self.assertEqual(datetime(2003, 10, 26, 0, 59, - tzinfo=tz.tzstr(s)).tzname(), "EDT") - end = tz.enfold(datetime(2003, 10, 26, 1, 00, tzinfo=tz.tzstr(s)), - fold=1) - self.assertEqual(end.tzname(), "EST") - - def testStrStart5(self): - s = "EST5EDT4,95/02:00:00,298/02:00" - self.assertEqual(datetime(2003, 4, 6, 1, 59, - tzinfo=tz.tzstr(s)).tzname(), "EST") - self.assertEqual(datetime(2003, 4, 6, 2, 00, - tzinfo=tz.tzstr(s)).tzname(), "EDT") - - def testStrEnd5(self): - s = "EST5EDT4,95/02:00:00,298/02" - self.assertEqual(datetime(2003, 10, 26, 0, 59, - tzinfo=tz.tzstr(s)).tzname(), "EDT") - end = tz.enfold(datetime(2003, 10, 26, 1, 00, - tzinfo=tz.tzstr(s)), fold=1) - self.assertEqual(end.tzname(), "EST") - - def testStrStart6(self): - s = "EST5EDT4,J96/02:00:00,J299/02:00" - self.assertEqual(datetime(2003, 4, 6, 1, 59, - tzinfo=tz.tzstr(s)).tzname(), "EST") - self.assertEqual(datetime(2003, 4, 6, 2, 00, - tzinfo=tz.tzstr(s)).tzname(), "EDT") - - def testStrEnd6(self): - s = "EST5EDT4,J96/02:00:00,J299/02" - self.assertEqual(datetime(2003, 10, 26, 0, 59, - tzinfo=tz.tzstr(s)).tzname(), "EDT") - - end = tz.enfold(datetime(2003, 10, 26, 1, 00, - tzinfo=tz.tzstr(s)), fold=1) - self.assertEqual(end.tzname(), "EST") - def testStrStr(self): # Test that tz.tzstr() won't throw an error if given a str instead # of a unicode literal. @@ -1196,15 +1432,6 @@ class TZStrTest(unittest.TestCase, TzFoldMixin): self.assertEqual(datetime(2003, 4, 6, 2, 00, tzinfo=tz.tzstr(str("EST5EDT"))).tzname(), "EDT") - def testStrCmp1(self): - self.assertEqual(tz.tzstr("EST5EDT"), - tz.tzstr("EST5EDT4,M4.1.0/02:00:00,M10-5-0/02:00")) - - def testStrCmp2(self): - # TODO: This is parsing the default arguments. - self.assertEqual(tz.tzstr("EST5EDT"), - tz.tzstr("EST5EDT,4,1,0,7200,10,-1,0,7200,3600")) - def testStrInequality(self): TZS1 = tz.tzstr('EST5EDT4') @@ -1262,10 +1489,217 @@ class TZStrTest(unittest.TestCase, TzFoldMixin): with self.assertRaises(ValueError): tz.tzstr('InvalidString;439999') + def testTzStrSingleton(self): + tz1 = tz.tzstr('EST5EDT') + tz2 = tz.tzstr('CST4CST') + tz3 = tz.tzstr('EST5EDT') + + self.assertIsNot(tz1, tz2) + self.assertIs(tz1, tz3) + + def testTzStrSingletonPosix(self): + tz_t1 = tz.tzstr('GMT+3', posix_offset=True) + tz_f1 = tz.tzstr('GMT+3', posix_offset=False) + + tz_t2 = tz.tzstr('GMT+3', posix_offset=True) + tz_f2 = tz.tzstr('GMT+3', posix_offset=False) + + self.assertIs(tz_t1, tz_t2) + self.assertIsNot(tz_t1, tz_f1) + + self.assertIs(tz_f1, tz_f2) + + def testTzStrInstance(self): + tz1 = tz.tzstr('EST5EDT') + tz2 = tz.tzstr.instance('EST5EDT') + tz3 = tz.tzstr.instance('EST5EDT') + + assert tz1 is not tz2 + assert tz2 is not tz3 + + # Ensure that these still are all the same zone + assert tz1 == tz2 == tz3 + + +@pytest.mark.smoke +@pytest.mark.tzstr +def test_tzstr_weakref(): + tz_t1 = tz.tzstr('EST5EDT') + tz_t2_ref = weakref.ref(tz.tzstr('EST5EDT')) + assert tz_t1 is tz_t2_ref() + + del tz_t1 + gc.collect() + + assert tz_t2_ref() is not None + assert tz.tzstr('EST5EDT') is tz_t2_ref() + + for offset in range(5,15): + tz.tzstr('GMT+{}'.format(offset)) + gc.collect() + + assert tz_t2_ref() is None + assert tz.tzstr('EST5EDT') is not tz_t2_ref() + + +@pytest.mark.tzstr +@pytest.mark.parametrize('tz_str,expected', [ + # From https://www.gnu.org/software/libc/manual/html_node/TZ-Variable.html + ('', tz.tzrange(None)), # TODO: Should change this so tz.tzrange('') works + ('EST+5EDT,M3.2.0/2,M11.1.0/12', + tz.tzrange('EST', -18000, 'EDT', -14400, + start=relativedelta(month=3, day=1, weekday=SU(2), hours=2), + end=relativedelta(month=11, day=1, weekday=SU(1), hours=11))), + ('WART4WARST,J1/0,J365/25', # This is DST all year, Western Argentina Summer Time + tz.tzrange('WART', timedelta(hours=-4), 'WARST', + start=relativedelta(month=1, day=1, hours=0), + end=relativedelta(month=12, day=31, days=1))), + ('IST-2IDT,M3.4.4/26,M10.5.0', # Israel Standard / Daylight Time + tz.tzrange('IST', timedelta(hours=2), 'IDT', + start=relativedelta(month=3, day=1, weekday=TH(4), days=1, hours=2), + end=relativedelta(month=10, day=31, weekday=SU(-1), hours=1))), + ('WGT3WGST,M3.5.0/2,M10.5.0/1', + tz.tzrange('WGT', timedelta(hours=-3), 'WGST', + start=relativedelta(month=3, day=31, weekday=SU(-1), hours=2), + end=relativedelta(month=10, day=31, weekday=SU(-1), hours=0))), + + # Different offset specifications + ('WGT0300WGST', + tz.tzrange('WGT', timedelta(hours=-3), 'WGST')), + ('WGT03:00WGST', + tz.tzrange('WGT', timedelta(hours=-3), 'WGST')), + ('AEST-1100AEDT', + tz.tzrange('AEST', timedelta(hours=11), 'AEDT')), + ('AEST-11:00AEDT', + tz.tzrange('AEST', timedelta(hours=11), 'AEDT')), + + # Different time formats + ('EST5EDT,M3.2.0/4:00,M11.1.0/3:00', + tz.tzrange('EST', timedelta(hours=-5), 'EDT', + start=relativedelta(month=3, day=1, weekday=SU(2), hours=4), + end=relativedelta(month=11, day=1, weekday=SU(1), hours=2))), + ('EST5EDT,M3.2.0/04:00,M11.1.0/03:00', + tz.tzrange('EST', timedelta(hours=-5), 'EDT', + start=relativedelta(month=3, day=1, weekday=SU(2), hours=4), + end=relativedelta(month=11, day=1, weekday=SU(1), hours=2))), + ('EST5EDT,M3.2.0/0400,M11.1.0/0300', + tz.tzrange('EST', timedelta(hours=-5), 'EDT', + start=relativedelta(month=3, day=1, weekday=SU(2), hours=4), + end=relativedelta(month=11, day=1, weekday=SU(1), hours=2))), +]) +def test_valid_GNU_tzstr(tz_str, expected): + tzi = tz.tzstr(tz_str) + + assert tzi == expected + + +@pytest.mark.tzstr +@pytest.mark.parametrize('tz_str, expected', [ + ('EST5EDT,5,4,0,7200,11,3,0,7200', + tz.tzrange('EST', timedelta(hours=-5), 'EDT', + start=relativedelta(month=5, day=1, weekday=SU(+4), hours=+2), + end=relativedelta(month=11, day=1, weekday=SU(+3), hours=+1))), + ('EST5EDT,5,-4,0,7200,11,3,0,7200', + tz.tzrange('EST', timedelta(hours=-5), 'EDT', + start=relativedelta(hours=+2, month=5, day=31, weekday=SU(-4)), + end=relativedelta(hours=+1, month=11, day=1, weekday=SU(+3)))), + ('EST5EDT,5,4,0,7200,11,-3,0,7200', + tz.tzrange('EST', timedelta(hours=-5), 'EDT', + start=relativedelta(hours=+2, month=5, day=1, weekday=SU(+4)), + end=relativedelta(hours=+1, month=11, day=31, weekday=SU(-3)))), + ('EST5EDT,5,4,0,7200,11,-3,0,7200,3600', + tz.tzrange('EST', timedelta(hours=-5), 'EDT', + start=relativedelta(hours=+2, month=5, day=1, weekday=SU(+4)), + end=relativedelta(hours=+1, month=11, day=31, weekday=SU(-3)))), + ('EST5EDT,5,4,0,7200,11,-3,0,7200,3600', + tz.tzrange('EST', timedelta(hours=-5), 'EDT', + start=relativedelta(hours=+2, month=5, day=1, weekday=SU(+4)), + end=relativedelta(hours=+1, month=11, day=31, weekday=SU(-3)))), + ('EST5EDT,5,4,0,7200,11,-3,0,7200,-3600', + tz.tzrange('EST', timedelta(hours=-5), 'EDT', timedelta(hours=-6), + start=relativedelta(hours=+2, month=5, day=1, weekday=SU(+4)), + end=relativedelta(hours=+3, month=11, day=31, weekday=SU(-3)))), + ('EST5EDT,5,4,0,7200,11,-3,0,7200,+7200', + tz.tzrange('EST', timedelta(hours=-5), 'EDT', timedelta(hours=-3), + start=relativedelta(hours=+2, month=5, day=1, weekday=SU(+4)), + end=relativedelta(hours=0, month=11, day=31, weekday=SU(-3)))), + ('EST5EDT,5,4,0,7200,11,-3,0,7200,+3600', + tz.tzrange('EST', timedelta(hours=-5), 'EDT', + start=relativedelta(hours=+2, month=5, day=1, weekday=SU(+4)), + end=relativedelta(hours=+1, month=11, day=31, weekday=SU(-3)))), +]) +def test_valid_dateutil_format(tz_str, expected): + # This tests the dateutil-specific format that is used widely in the tests + # and examples. It is unclear where this format originated from. + with pytest.warns(tz.DeprecatedTzFormatWarning): + tzi = tz.tzstr.instance(tz_str) + + assert tzi == expected + + +@pytest.mark.tzstr +@pytest.mark.parametrize('tz_str', [ + 'hdfiughdfuig,dfughdfuigpu87ñ::', + ',dfughdfuigpu87ñ::', + '-1:WART4WARST,J1,J365/25', + 'WART4WARST,J1,J365/-25', + 'IST-2IDT,M3.4.-1/26,M10.5.0', + 'IST-2IDT,M3,2000,1/26,M10,5,0' +]) +def test_invalid_GNU_tzstr(tz_str): + with pytest.raises(ValueError): + tz.tzstr(tz_str) + + +# Different representations of the same default rule set +DEFAULT_TZSTR_RULES_EQUIV_2003 = [ + 'EST5EDT', + 'EST5EDT4,M4.1.0/02:00:00,M10-5-0/02:00', + 'EST5EDT4,95/02:00:00,298/02:00', + 'EST5EDT4,J96/02:00:00,J299/02:00', + 'EST5EDT4,J96/02:00:00,J299/02' +] + + +@pytest.mark.tzstr +@pytest.mark.parametrize('tz_str', DEFAULT_TZSTR_RULES_EQUIV_2003) +def test_tzstr_default_start(tz_str): + tzi = tz.tzstr(tz_str) + dt_std = datetime(2003, 4, 6, 1, 59, tzinfo=tzi) + dt_dst = datetime(2003, 4, 6, 2, 00, tzinfo=tzi) + + assert get_timezone_tuple(dt_std) == EST_TUPLE + assert get_timezone_tuple(dt_dst) == EDT_TUPLE + + +@pytest.mark.tzstr +@pytest.mark.parametrize('tz_str', DEFAULT_TZSTR_RULES_EQUIV_2003) +def test_tzstr_default_end(tz_str): + tzi = tz.tzstr(tz_str) + dt_dst = datetime(2003, 10, 26, 0, 59, tzinfo=tzi) + dt_dst_ambig = datetime(2003, 10, 26, 1, 00, tzinfo=tzi) + dt_std_ambig = tz.enfold(dt_dst_ambig, fold=1) + dt_std = datetime(2003, 10, 26, 2, 00, tzinfo=tzi) + + assert get_timezone_tuple(dt_dst) == EDT_TUPLE + assert get_timezone_tuple(dt_dst_ambig) == EDT_TUPLE + assert get_timezone_tuple(dt_std_ambig) == EST_TUPLE + assert get_timezone_tuple(dt_std) == EST_TUPLE + + +@pytest.mark.tzstr +@pytest.mark.parametrize('tzstr_1', ['EST5EDT', + 'EST5EDT4,M4.1.0/02:00:00,M10-5-0/02:00']) +@pytest.mark.parametrize('tzstr_2', ['EST5EDT', + 'EST5EDT4,M4.1.0/02:00:00,M10-5-0/02:00']) +def test_tzstr_default_cmp(tzstr_1, tzstr_2): + tz1 = tz.tzstr(tzstr_1) + tz2 = tz.tzstr(tzstr_2) + + assert tz1 == tz2 class TZICalTest(unittest.TestCase, TzFoldMixin): - - def gettz(self, tzname): + def _gettz_str_tuple(self, tzname): TZ_EST = ( 'BEGIN:VTIMEZONE', 'TZID:US-Eastern', @@ -1284,7 +1718,27 @@ class TZICalTest(unittest.TestCase, TzFoldMixin): 'TZNAME:EDT', 'END:DAYLIGHT', 'END:VTIMEZONE' - ) + ) + + TZ_PST = ( + 'BEGIN:VTIMEZONE', + 'TZID:US-Pacific', + 'BEGIN:STANDARD', + 'DTSTART:19971029T020000', + 'RRULE:FREQ=YEARLY;BYDAY=+1SU;BYMONTH=11', + 'TZOFFSETFROM:-0700', + 'TZOFFSETTO:-0800', + 'TZNAME:PST', + 'END:STANDARD', + 'BEGIN:DAYLIGHT', + 'DTSTART:19980301T020000', + 'RRULE:FREQ=YEARLY;BYDAY=+2SU;BYMONTH=03', + 'TZOFFSETFROM:-0800', + 'TZOFFSETTO:-0700', + 'TZNAME:PDT', + 'END:DAYLIGHT', + 'END:VTIMEZONE' + ) TZ_AEST = ( 'BEGIN:VTIMEZONE', @@ -1304,13 +1758,57 @@ class TZICalTest(unittest.TestCase, TzFoldMixin): 'TZNAME:AEDT', 'END:DAYLIGHT', 'END:VTIMEZONE' - ) + ) + + TZ_LON = ( + 'BEGIN:VTIMEZONE', + 'TZID:Europe-London', + 'BEGIN:STANDARD', + 'DTSTART:19810301T030000', + 'RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10;BYHOUR=02', + 'TZOFFSETFROM:+0100', + 'TZOFFSETTO:+0000', + 'TZNAME:GMT', + 'END:STANDARD', + 'BEGIN:DAYLIGHT', + 'DTSTART:19961001T030000', + 'RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=03;BYHOUR=01', + 'TZOFFSETFROM:+0000', + 'TZOFFSETTO:+0100', + 'TZNAME:BST', + 'END:DAYLIGHT', + 'END:VTIMEZONE' + ) tzname_map = {'Australia/Sydney': TZ_AEST, 'America/Toronto': TZ_EST, - 'America/New_York': TZ_EST} + 'America/New_York': TZ_EST, + 'America/Los_Angeles': TZ_PST, + 'Europe/London': TZ_LON} - tzc = tz.tzical(StringIO('\n'.join(tzname_map[tzname]))).get() + return tzname_map[tzname] + + def _gettz_str(self, tzname): + return '\n'.join(self._gettz_str_tuple(tzname)) + + def _tzstr_dtstart_with_params(self, tzname, param_str): + # Adds parameters to the DTSTART values of a given tzstr + tz_str_tuple = self._gettz_str_tuple(tzname) + + out_tz = [] + for line in tz_str_tuple: + if line.startswith('DTSTART'): + name, value = line.split(':', 1) + line = name + ';' + param_str + ':' + value + + out_tz.append(line) + + return '\n'.join(out_tz) + + def gettz(self, tzname): + tz_str = self._gettz_str(tzname) + + tzc = tz.tzical(StringIO(tz_str)).get() return tzc @@ -1321,16 +1819,15 @@ class TZICalTest(unittest.TestCase, TzFoldMixin): self.assertEqual(repr(tzc), "tzical(" + repr(instr.name) + ")") - # Test performance def _test_us_zone(self, tzc, func, values, start): if start: - dt1 = datetime(2003, 4, 6, 1, 59) - dt2 = datetime(2003, 4, 6, 2, 00) + dt1 = datetime(2003, 3, 9, 1, 59) + dt2 = datetime(2003, 3, 9, 2, 00) fold = [0, 0] else: - dt1 = datetime(2003, 10, 26, 0, 59) - dt2 = datetime(2003, 10, 26, 1, 00) + dt1 = datetime(2003, 11, 2, 0, 59) + dt2 = datetime(2003, 11, 2, 1, 00) fold = [0, 1] dts = (tz.enfold(dt.replace(tzinfo=tzc), fold=f) @@ -1340,17 +1837,20 @@ class TZICalTest(unittest.TestCase, TzFoldMixin): self.assertEqual(func(dt), value) def _test_multi_zones(self, tzstrs, tzids, func, values, start): - tzic = tz.tzical(StringIO(''.join(tzstrs))) + tzic = tz.tzical(StringIO('\n'.join(tzstrs))) for tzid, vals in zip(tzids, values): tzc = tzic.get(tzid) self._test_us_zone(tzc, func, vals, start) def _prepare_EST(self): - return tz.tzical(StringIO(TZICAL_EST5EDT)).get() + tz_str = self._gettz_str('America/New_York') + return tz.tzical(StringIO(tz_str)).get() + + def _testEST(self, start, test_type, tzc=None): + if tzc is None: + tzc = self._prepare_EST() - def _testEST(self, start, test_type): - tzc = self._prepare_EST() argdict = { 'name': (datetime.tzname, ('EST', 'EDT')), 'offset': (datetime.utcoffset, (timedelta(hours=-5), @@ -1384,8 +1884,22 @@ class TZICalTest(unittest.TestCase, TzFoldMixin): def testESTEndDST(self): self._testEST(start=False, test_type='dst') + def testESTValueDatetime(self): + # Violating one-test-per-test rule because we're not set up to do + # parameterized tests and the manual proliferation is getting a bit + # out of hand. + tz_str = self._tzstr_dtstart_with_params('America/New_York', + 'VALUE=DATE-TIME') + + tzc = tz.tzical(StringIO(tz_str)).get() + + for start in (True, False): + for test_type in ('name', 'offset', 'dst'): + self._testEST(start=start, test_type=test_type, tzc=tzc) + def _testMultizone(self, start, test_type): - tzstrs = (TZICAL_EST5EDT, TZICAL_PST8PDT) + tzstrs = (self._gettz_str('America/New_York'), + self._gettz_str('America/Los_Angeles')) tzids = ('US-Eastern', 'US-Pacific') argdict = { @@ -1427,7 +1941,9 @@ class TZICalTest(unittest.TestCase, TzFoldMixin): self._testMultizone(start=False, test_type='dst') def testMultiZoneKeys(self): - tzic = tz.tzical(StringIO(''.join((TZICAL_EST5EDT, TZICAL_PST8PDT)))) + est_str = self._gettz_str('America/New_York') + pst_str = self._gettz_str('America/Los_Angeles') + tzic = tz.tzical(StringIO('\n'.join((est_str, pst_str)))) # Sort keys because they are in a random order, being dictionary keys keys = sorted(tzic.keys()) @@ -1445,6 +1961,24 @@ class TZICalTest(unittest.TestCase, TzFoldMixin): with self.assertRaises(ValueError): tzic.get() + def testDtstartDate(self): + tz_str = self._tzstr_dtstart_with_params('America/New_York', + 'VALUE=DATE') + with self.assertRaises(ValueError): + tz.tzical(StringIO(tz_str)) + + def testDtstartTzid(self): + tz_str = self._tzstr_dtstart_with_params('America/New_York', + 'TZID=UTC') + with self.assertRaises(ValueError): + tz.tzical(StringIO(tz_str)) + + def testDtstartBadParam(self): + tz_str = self._tzstr_dtstart_with_params('America/New_York', + 'FOO=BAR') + with self.assertRaises(ValueError): + tz.tzical(StringIO(tz_str)) + # Test Parsing def testGap(self): tzic = tz.tzical(StringIO('\n'.join((TZICAL_EST5EDT, TZICAL_PST8PDT)))) @@ -1484,11 +2018,13 @@ class TZTest(unittest.TestCase): with self.assertRaises(ValueError): tz.tzfile(BytesIO(b'BadFile')) - def testRoundNonFullMinutes(self): - # This timezone has an offset of 5992 seconds in 1900-01-01. - tzc = tz.tzfile(BytesIO(base64.b64decode(EUROPE_HELSINKI))) - self.assertEqual(str(datetime(1900, 1, 1, 0, 0, tzinfo=tzc)), - "1900-01-01 00:00:00+01:40") + def testFilestreamWithNameRepr(self): + # If fileobj is a filestream with a "name" attribute this name should + # be reflected in the tz object's repr + fileobj = BytesIO(base64.b64decode(TZFILE_EST5EDT)) + fileobj.name = 'foo' + tzc = tz.tzfile(fileobj) + self.assertEqual(repr(tzc), 'tzfile(' + repr('foo') + ')') def testLeapCountDecodesProperly(self): # This timezone has leapcnt, and failed to decode until @@ -1501,7 +2037,7 @@ class TZTest(unittest.TestCase): # work NEW_YORK must be in TZif version 1 format i.e. no more data # after TZif v1 header + data has been read fileobj = BytesIO(base64.b64decode(NEW_YORK)) - tzc = tz.tzfile(fileobj) + tz.tzfile(fileobj) # we expect no remaining file content now, i.e. zero-length; if there's # still data we haven't read the file format correctly remaining_tzfile_content = fileobj.read() @@ -1532,15 +2068,13 @@ class TZTest(unittest.TestCase): def testGMTOffset(self): # GMT and UTC offsets have inverted signal when compared to the # usual TZ variable handling. - dt = datetime(2007, 8, 6, 4, 10, tzinfo=tz.tzutc()) + dt = datetime(2007, 8, 6, 4, 10, tzinfo=tz.UTC) self.assertEqual(dt.astimezone(tz=tz.tzstr("GMT+2")), datetime(2007, 8, 6, 6, 10, tzinfo=tz.tzstr("GMT+2"))) self.assertEqual(dt.astimezone(tz=tz.gettz("UTC-2")), datetime(2007, 8, 6, 2, 10, tzinfo=tz.tzstr("UTC-2"))) @unittest.skipIf(IS_WIN, "requires Unix") - @unittest.skipUnless(TZEnvContext.tz_change_allowed(), - TZEnvContext.tz_change_disallowed_message()) def testTZSetDoesntCorrupt(self): # if we start in non-UTC then tzset UTC make sure parse doesn't get # confused @@ -1550,6 +2084,40 @@ class TZTest(unittest.TestCase): self.assertEqual(str(dt), '2014-07-20 12:34:56+00:00') +@pytest.mark.tzfile +@pytest.mark.skipif(not SUPPORTS_SUB_MINUTE_OFFSETS, + reason='Sub-minute offsets not supported') +def test_tzfile_sub_minute_offset(): + # If user running python 3.6 or newer, exact offset is used + tzc = tz.tzfile(BytesIO(base64.b64decode(EUROPE_HELSINKI))) + offset = timedelta(hours=1, minutes=39, seconds=52) + assert datetime(1900, 1, 1, 0, 0, tzinfo=tzc).utcoffset() == offset + + +@pytest.mark.tzfile +@pytest.mark.skipif(SUPPORTS_SUB_MINUTE_OFFSETS, + reason='Sub-minute offsets supported.') +def test_sub_minute_rounding_tzfile(): + # This timezone has an offset of 5992 seconds in 1900-01-01. + # For python version pre-3.6, this will be rounded + tzc = tz.tzfile(BytesIO(base64.b64decode(EUROPE_HELSINKI))) + offset = timedelta(hours=1, minutes=40) + assert datetime(1900, 1, 1, 0, 0, tzinfo=tzc).utcoffset() == offset + + +@pytest.mark.tzfile +def test_samoa_transition(): + # utcoffset() was erroneously returning +14:00 an hour early (GH #812) + APIA = tz.gettz('Pacific/Apia') + dt = datetime(2011, 12, 29, 23, 59, tzinfo=APIA) + assert dt.utcoffset() == timedelta(hours=-10) + + # Make sure the transition actually works, too + dt_after = (dt.astimezone(tz.UTC) + timedelta(minutes=1)).astimezone(APIA) + assert dt_after == datetime(2011, 12, 31, tzinfo=APIA) + assert dt_after.utcoffset() == timedelta(hours=14) + + @unittest.skipUnless(IS_WIN, "Requires Windows") class TzWinTest(unittest.TestCase, TzWinFoldMixin): def setUp(self): @@ -1639,7 +2207,7 @@ class TzWinTest(unittest.TestCase, TzWinFoldMixin): def testTzWinEqualityInvalid(self): # Compare to objects that do not implement comparison with this # (should default to False) - UTC = tz.tzutc() + UTC = tz.UTC EST = tz.tzwin('Eastern Standard Time') self.assertFalse(EST == UTC) @@ -1689,8 +2257,6 @@ class TzWinTest(unittest.TestCase, TzWinFoldMixin): @unittest.skipUnless(IS_WIN, "Requires Windows") -@unittest.skipUnless(TZWinContext.tz_change_allowed(), - TZWinContext.tz_change_disallowed_message()) class TzWinLocalTest(unittest.TestCase, TzWinFoldMixin): def setUp(self): @@ -1698,12 +2264,12 @@ class TzWinLocalTest(unittest.TestCase, TzWinFoldMixin): self.context = TZWinContext def get_args(self, tzname): - return tuple() + return () def testLocal(self): # Not sure how to pin a local time zone, so for now we're just going # to run this and make sure it doesn't raise an error - # See Github Issue #135: https://github.com/dateutil/dateutil/issues/135 + # See GitHub Issue #135: https://github.com/dateutil/dateutil/issues/135 datetime.now(tzwin.tzwinlocal()) def testTzwinLocalUTCOffset(self): @@ -1816,17 +2382,18 @@ class TzPickleTest(PicklableMixin, unittest.TestCase): asfile=self._asfile) def testPickleTzUTC(self): - self.assertPicklable(tz.tzutc()) + self.assertPicklable(tz.tzutc(), singleton=True) def testPickleTzOffsetZero(self): - self.assertPicklable(tz.tzoffset('UTC', 0)) + self.assertPicklable(tz.tzoffset('UTC', 0), singleton=True) def testPickleTzOffsetPos(self): - self.assertPicklable(tz.tzoffset('UTC+1', 3600)) + self.assertPicklable(tz.tzoffset('UTC+1', 3600), singleton=True) def testPickleTzOffsetNeg(self): - self.assertPicklable(tz.tzoffset('UTC-1', -3600)) + self.assertPicklable(tz.tzoffset('UTC-1', -3600), singleton=True) + @pytest.mark.tzlocal def testPickleTzLocal(self): self.assertPicklable(tz.tzlocal()) @@ -2096,19 +2663,149 @@ class DatetimeExistsTest(unittest.TestCase): self.assertFalse(tz.datetime_exists(dt, tz=AEST)) -class EnfoldTest(unittest.TestCase): - def testEnterFoldDefault(self): +class TestEnfold: + def test_enter_fold_default(self): dt = tz.enfold(datetime(2020, 1, 19, 3, 32)) - self.assertEqual(dt.fold, 1) + assert dt.fold == 1 - def testEnterFold(self): + def test_enter_fold(self): dt = tz.enfold(datetime(2020, 1, 19, 3, 32), fold=1) - self.assertEqual(dt.fold, 1) + assert dt.fold == 1 - def testExitFold(self): + def test_exit_fold(self): dt = tz.enfold(datetime(2020, 1, 19, 3, 32), fold=0) # Before Python 3.6, dt.fold won't exist if fold is 0. - self.assertEqual(getattr(dt, 'fold', 0), 0) + assert getattr(dt, 'fold', 0) == 0 + + def test_defold(self): + dt = tz.enfold(datetime(2020, 1, 19, 3, 32), fold=1) + + dt2 = tz.enfold(dt, fold=0) + + assert getattr(dt2, 'fold', 0) == 0 + + def test_fold_replace_args(self): + # This test can be dropped when Python < 3.6 is dropped, since it + # is mainly to cover the `replace` method on _DatetimeWithFold + dt = tz.enfold(datetime(1950, 1, 2, 12, 30, 15, 8), fold=1) + + dt2 = dt.replace(1952, 2, 3, 13, 31, 16, 9) + assert dt2 == tz.enfold(datetime(1952, 2, 3, 13, 31, 16, 9), fold=1) + assert dt2.fold == 1 + + def test_fold_replace_exception_duplicate_args(self): + dt = tz.enfold(datetime(1999, 1, 3), fold=1) + + with pytest.raises(TypeError): + dt.replace(1950, year=2000) + + +@pytest.mark.tz_resolve_imaginary +class ImaginaryDateTest(unittest.TestCase): + def testCanberraForward(self): + tzi = tz.gettz('Australia/Canberra') + dt = datetime(2018, 10, 7, 2, 30, tzinfo=tzi) + dt_act = tz.resolve_imaginary(dt) + dt_exp = datetime(2018, 10, 7, 3, 30, tzinfo=tzi) + self.assertEqual(dt_act, dt_exp) + + def testLondonForward(self): + tzi = tz.gettz('Europe/London') + dt = datetime(2018, 3, 25, 1, 30, tzinfo=tzi) + dt_act = tz.resolve_imaginary(dt) + dt_exp = datetime(2018, 3, 25, 2, 30, tzinfo=tzi) + self.assertEqual(dt_act, dt_exp) + + def testKeivForward(self): + tzi = tz.gettz('Europe/Kiev') + dt = datetime(2018, 3, 25, 3, 30, tzinfo=tzi) + dt_act = tz.resolve_imaginary(dt) + dt_exp = datetime(2018, 3, 25, 4, 30, tzinfo=tzi) + self.assertEqual(dt_act, dt_exp) + + +@pytest.mark.tz_resolve_imaginary +@pytest.mark.parametrize('dt', [ + datetime(2017, 11, 5, 1, 30, tzinfo=tz.gettz('America/New_York')), + datetime(2018, 10, 28, 1, 30, tzinfo=tz.gettz('Europe/London')), + datetime(2017, 4, 2, 2, 30, tzinfo=tz.gettz('Australia/Sydney')), +]) +def test_resolve_imaginary_ambiguous(dt): + assert tz.resolve_imaginary(dt) is dt + + dt_f = tz.enfold(dt) + assert dt is not dt_f + assert tz.resolve_imaginary(dt_f) is dt_f + + +@pytest.mark.tz_resolve_imaginary +@pytest.mark.parametrize('dt', [ + datetime(2017, 6, 2, 12, 30, tzinfo=tz.gettz('America/New_York')), + datetime(2018, 4, 2, 9, 30, tzinfo=tz.gettz('Europe/London')), + datetime(2017, 2, 2, 16, 30, tzinfo=tz.gettz('Australia/Sydney')), + datetime(2017, 12, 2, 12, 30, tzinfo=tz.gettz('America/New_York')), + datetime(2018, 12, 2, 9, 30, tzinfo=tz.gettz('Europe/London')), + datetime(2017, 6, 2, 16, 30, tzinfo=tz.gettz('Australia/Sydney')), + datetime(2025, 9, 25, 1, 17, tzinfo=tz.UTC), + datetime(2025, 9, 25, 1, 17, tzinfo=tz.tzoffset('EST', -18000)), + datetime(2019, 3, 4, tzinfo=None) +]) +def test_resolve_imaginary_existing(dt): + assert tz.resolve_imaginary(dt) is dt + + +def __get_kiritimati_resolve_imaginary_test(): + # In the 2018d release of the IANA database, the Kiritimati "imaginary day" + # data was corrected, so if the system zoneinfo is older than 2018d, the + # Kiritimati test will fail. + + tzi = tz.gettz('Pacific/Kiritimati') + new_version = False + if not tz.datetime_exists(datetime(1995, 1, 1, 12, 30), tzi): + zif = zoneinfo.get_zonefile_instance() + if zif.metadata is not None: + new_version = zif.metadata['tzversion'] >= '2018d' + + if new_version: + tzi = zif.get('Pacific/Kiritimati') + else: + new_version = True + + if new_version: + dates = (datetime(1994, 12, 31, 12, 30), datetime(1995, 1, 1, 12, 30)) + else: + dates = (datetime(1995, 1, 1, 12, 30), datetime(1995, 1, 2, 12, 30)) + + return (tzi, ) + dates + + +resolve_imaginary_tests = [ + (tz.gettz('Europe/London'), + datetime(2018, 3, 25, 1, 30), datetime(2018, 3, 25, 2, 30)), + (tz.gettz('America/New_York'), + datetime(2017, 3, 12, 2, 30), datetime(2017, 3, 12, 3, 30)), + (tz.gettz('Australia/Sydney'), + datetime(2014, 10, 5, 2, 0), datetime(2014, 10, 5, 3, 0)), + __get_kiritimati_resolve_imaginary_test(), +] + + +if SUPPORTS_SUB_MINUTE_OFFSETS: + resolve_imaginary_tests.append( + (tz.gettz('Africa/Monrovia'), + datetime(1972, 1, 7, 0, 30), datetime(1972, 1, 7, 1, 14, 30))) + + +@pytest.mark.tz_resolve_imaginary +@pytest.mark.parametrize('tzi, dt, dt_exp', resolve_imaginary_tests) +def test_resolve_imaginary(tzi, dt, dt_exp): + dt = dt.replace(tzinfo=tzi) + dt_exp = dt_exp.replace(tzinfo=tzi) + + dt_r = tz.resolve_imaginary(dt) + assert dt_r == dt_exp + assert dt_r.tzname() == dt_exp.tzname() + assert dt_r.utcoffset() == dt_exp.utcoffset() diff --git a/libs/dateutil/test/test_utils.py b/libs/dateutil/test/test_utils.py new file mode 100644 index 000000000..fe1bfdcb8 --- /dev/null +++ b/libs/dateutil/test/test_utils.py @@ -0,0 +1,52 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals +from datetime import timedelta, datetime + +from dateutil import tz +from dateutil import utils +from dateutil.tz import UTC +from dateutil.utils import within_delta + +from freezegun import freeze_time + +NYC = tz.gettz("America/New_York") + + +@freeze_time(datetime(2014, 12, 15, 1, 21, 33, 4003)) +def test_utils_today(): + assert utils.today() == datetime(2014, 12, 15, 0, 0, 0) + + +@freeze_time(datetime(2014, 12, 15, 12), tz_offset=5) +def test_utils_today_tz_info(): + assert utils.today(NYC) == datetime(2014, 12, 15, 0, 0, 0, tzinfo=NYC) + + +@freeze_time(datetime(2014, 12, 15, 23), tz_offset=5) +def test_utils_today_tz_info_different_day(): + assert utils.today(UTC) == datetime(2014, 12, 16, 0, 0, 0, tzinfo=UTC) + + +def test_utils_default_tz_info_naive(): + dt = datetime(2014, 9, 14, 9, 30) + assert utils.default_tzinfo(dt, NYC).tzinfo is NYC + + +def test_utils_default_tz_info_aware(): + dt = datetime(2014, 9, 14, 9, 30, tzinfo=UTC) + assert utils.default_tzinfo(dt, NYC).tzinfo is UTC + + +def test_utils_within_delta(): + d1 = datetime(2016, 1, 1, 12, 14, 1, 9) + d2 = d1.replace(microsecond=15) + + assert within_delta(d1, d2, timedelta(seconds=1)) + assert not within_delta(d1, d2, timedelta(microseconds=1)) + + +def test_utils_within_delta_with_negative_delta(): + d1 = datetime(2016, 1, 1) + d2 = datetime(2015, 12, 31) + + assert within_delta(d2, d1, timedelta(days=-1)) diff --git a/libs/dateutil/tz/__init__.py b/libs/dateutil/tz/__init__.py index 1cba7b9e9..af1352c47 100644 --- a/libs/dateutil/tz/__init__.py +++ b/libs/dateutil/tz/__init__.py @@ -1,4 +1,12 @@ +# -*- coding: utf-8 -*- from .tz import * +from .tz import __doc__ __all__ = ["tzutc", "tzoffset", "tzlocal", "tzfile", "tzrange", - "tzstr", "tzical", "tzwin", "tzwinlocal", "gettz"] + "tzstr", "tzical", "tzwin", "tzwinlocal", "gettz", + "enfold", "datetime_ambiguous", "datetime_exists", + "resolve_imaginary", "UTC", "DeprecatedTzFormatWarning"] + + +class DeprecatedTzFormatWarning(Warning): + """Warning raised when time zones are parsed from deprecated formats.""" diff --git a/libs/dateutil/tz/_common.py b/libs/dateutil/tz/_common.py index 212e8ce95..e6ac11831 100644 --- a/libs/dateutil/tz/_common.py +++ b/libs/dateutil/tz/_common.py @@ -1,8 +1,9 @@ -from six import PY3 -from six.moves import _thread +from six import PY2 + +from functools import wraps from datetime import datetime, timedelta, tzinfo -import copy + ZERO = timedelta(0) @@ -15,14 +16,18 @@ def tzname_in_python2(namefunc): tzname() API changed in Python 3. It used to return bytes, but was changed to unicode strings """ - def adjust_encoding(*args, **kwargs): - name = namefunc(*args, **kwargs) - if name is not None and not PY3: - name = name.encode() + if PY2: + @wraps(namefunc) + def adjust_encoding(*args, **kwargs): + name = namefunc(*args, **kwargs) + if name is not None: + name = name.encode() - return name + return name - return adjust_encoding + return adjust_encoding + else: + return namefunc # The following is adapted from Alexander Belopolsky's tz library @@ -45,7 +50,7 @@ if hasattr(datetime, 'fold'): subclass of :py:class:`datetime.datetime` with the ``fold`` attribute added, if ``fold`` is 1. - ..versionadded:: 2.6.0 + .. versionadded:: 2.6.0 """ return dt.replace(fold=fold) @@ -56,10 +61,40 @@ else: Python versions before 3.6. It is used only for dates in a fold, so the ``fold`` attribute is fixed at ``1``. - ..versionadded:: 2.6.0 + .. versionadded:: 2.6.0 """ __slots__ = () + def replace(self, *args, **kwargs): + """ + Return a datetime with the same attributes, except for those + attributes given new values by whichever keyword arguments are + specified. Note that tzinfo=None can be specified to create a naive + datetime from an aware datetime with no conversion of date and time + data. + + This is reimplemented in ``_DatetimeWithFold`` because pypy3 will + return a ``datetime.datetime`` even if ``fold`` is unchanged. + """ + argnames = ( + 'year', 'month', 'day', 'hour', 'minute', 'second', + 'microsecond', 'tzinfo' + ) + + for arg, argname in zip(args, argnames): + if argname in kwargs: + raise TypeError('Duplicate argument: {}'.format(argname)) + + kwargs[argname] = arg + + for argname in argnames: + if argname not in kwargs: + kwargs[argname] = getattr(self, argname) + + dt_class = self.__class__ if kwargs.get('fold', 1) else datetime + + return dt_class(**kwargs) + @property def fold(self): return 1 @@ -80,7 +115,7 @@ else: subclass of :py:class:`datetime.datetime` with the ``fold`` attribute added, if ``fold`` is 1. - ..versionadded:: 2.6.0 + .. versionadded:: 2.6.0 """ if getattr(dt, 'fold', 0) == fold: return dt @@ -94,6 +129,23 @@ else: return datetime(*args) +def _validate_fromutc_inputs(f): + """ + The CPython version of ``fromutc`` checks that the input is a ``datetime`` + object and that ``self`` is attached as its ``tzinfo``. + """ + @wraps(f) + def fromutc(self, dt): + if not isinstance(dt, datetime): + raise TypeError("fromutc() requires a datetime argument") + if dt.tzinfo is not self: + raise ValueError("dt.tzinfo is not self") + + return f(self, dt) + + return fromutc + + class _tzinfo(tzinfo): """ Base class for all ``dateutil`` ``tzinfo`` objects. @@ -111,7 +163,7 @@ class _tzinfo(tzinfo): :return: Returns ``True`` if ambiguous, ``False`` otherwise. - ..versionadded:: 2.6.0 + .. versionadded:: 2.6.0 """ dt = dt.replace(tzinfo=self) @@ -121,7 +173,7 @@ class _tzinfo(tzinfo): same_offset = wall_0.utcoffset() == wall_1.utcoffset() same_dt = wall_0.replace(tzinfo=None) == wall_1.replace(tzinfo=None) - + return same_dt and not same_offset def _fold_status(self, dt_utc, dt_wall): @@ -160,18 +212,13 @@ class _tzinfo(tzinfo): Since this is the one time that we *know* we have an unambiguous datetime object, we take this opportunity to determine whether the datetime is ambiguous and in a "fold" state (e.g. if it's the first - occurence, chronologically, of the ambiguous datetime). + occurrence, chronologically, of the ambiguous datetime). :param dt: - A timezone-aware :class:`datetime.dateime` object. + A timezone-aware :class:`datetime.datetime` object. """ # Re-implement the algorithm from Python's datetime.py - if not isinstance(dt, datetime): - raise TypeError("fromutc() requires a datetime argument") - if dt.tzinfo is not self: - raise ValueError("dt.tzinfo is not self") - dtoff = dt.utcoffset() if dtoff is None: raise ValueError("fromutc() requires a non-None utcoffset() " @@ -184,16 +231,17 @@ class _tzinfo(tzinfo): if dtdst is None: raise ValueError("fromutc() requires a non-None dst() result") delta = dtoff - dtdst - if delta: - dt += delta - # Set fold=1 so we can default to being in the fold for - # ambiguous dates. - dtdst = enfold(dt, fold=1).dst() - if dtdst is None: - raise ValueError("fromutc(): dt.dst gave inconsistent " - "results; cannot convert") + + dt += delta + # Set fold=1 so we can default to being in the fold for + # ambiguous dates. + dtdst = enfold(dt, fold=1).dst() + if dtdst is None: + raise ValueError("fromutc(): dt.dst gave inconsistent " + "results; cannot convert") return dt + dtdst + @_validate_fromutc_inputs def fromutc(self, dt): """ Given a timezone-aware datetime in a given timezone, calculates a @@ -202,10 +250,10 @@ class _tzinfo(tzinfo): Since this is the one time that we *know* we have an unambiguous datetime object, we take this opportunity to determine whether the datetime is ambiguous and in a "fold" state (e.g. if it's the first - occurance, chronologically, of the ambiguous datetime). + occurrence, chronologically, of the ambiguous datetime). :param dt: - A timezone-aware :class:`datetime.dateime` object. + A timezone-aware :class:`datetime.datetime` object. """ dt_wall = self._fromutc(dt) @@ -236,7 +284,7 @@ class tzrangebase(_tzinfo): abbreviations in DST and STD, respectively. * ``_hasdst``: Whether or not the zone has DST. - ..versionadded:: 2.6.0 + .. versionadded:: 2.6.0 """ def __init__(self): raise NotImplementedError('tzrangebase is an abstract base class') @@ -290,7 +338,6 @@ class tzrangebase(_tzinfo): utc_transitions = (dston, dstoff) dt_utc = dt.replace(tzinfo=None) - isdst = self._naive_isdst(dt_utc, utc_transitions) if isdst: @@ -360,7 +407,7 @@ class tzrangebase(_tzinfo): @property def _dst_base_offset(self): return self._dst_offset - self._std_offset - + __hash__ = None def __ne__(self, other): @@ -370,11 +417,3 @@ class tzrangebase(_tzinfo): return "%s(...)" % self.__class__.__name__ __reduce__ = object.__reduce__ - - -def _total_seconds(td): - # Python 2.6 doesn't have a total_seconds() method on timedelta objects - return ((td.seconds + td.days * 86400) * 1000000 + - td.microseconds) // 1000000 - -_total_seconds = getattr(timedelta, 'total_seconds', _total_seconds) diff --git a/libs/dateutil/tz/_factories.py b/libs/dateutil/tz/_factories.py index de2e0c1de..f8a65891a 100644 --- a/libs/dateutil/tz/_factories.py +++ b/libs/dateutil/tz/_factories.py @@ -1,4 +1,8 @@ from datetime import timedelta +import weakref +from collections import OrderedDict + +from six.moves import _thread class _TzSingleton(type): @@ -11,6 +15,7 @@ class _TzSingleton(type): cls.__instance = super(_TzSingleton, cls).__call__() return cls.__instance + class _TzFactory(type): def instance(cls, *args, **kwargs): """Alternate constructor that returns a fresh instance""" @@ -19,7 +24,11 @@ class _TzFactory(type): class _TzOffsetFactory(_TzFactory): def __init__(cls, *args, **kwargs): - cls.__instances = {} + cls.__instances = weakref.WeakValueDictionary() + cls.__strong_cache = OrderedDict() + cls.__strong_cache_size = 8 + + cls._cache_lock = _thread.allocate_lock() def __call__(cls, name, offset): if isinstance(offset, timedelta): @@ -31,12 +40,25 @@ class _TzOffsetFactory(_TzFactory): if instance is None: instance = cls.__instances.setdefault(key, cls.instance(name, offset)) + + # This lock may not be necessary in Python 3. See GH issue #901 + with cls._cache_lock: + cls.__strong_cache[key] = cls.__strong_cache.pop(key, instance) + + # Remove an item if the strong cache is overpopulated + if len(cls.__strong_cache) > cls.__strong_cache_size: + cls.__strong_cache.popitem(last=False) + return instance class _TzStrFactory(_TzFactory): def __init__(cls, *args, **kwargs): - cls.__instances = {} + cls.__instances = weakref.WeakValueDictionary() + cls.__strong_cache = OrderedDict() + cls.__strong_cache_size = 8 + + cls.__cache_lock = _thread.allocate_lock() def __call__(cls, s, posix_offset=False): key = (s, posix_offset) @@ -45,5 +67,14 @@ class _TzStrFactory(_TzFactory): if instance is None: instance = cls.__instances.setdefault(key, cls.instance(s, posix_offset)) + + # This lock may not be necessary in Python 3. See GH issue #901 + with cls.__cache_lock: + cls.__strong_cache[key] = cls.__strong_cache.pop(key, instance) + + # Remove an item if the strong cache is overpopulated + if len(cls.__strong_cache) > cls.__strong_cache_size: + cls.__strong_cache.popitem(last=False) + return instance diff --git a/libs/dateutil/tz/tz.py b/libs/dateutil/tz/tz.py index 6bee29168..c67f56d46 100644 --- a/libs/dateutil/tz/tz.py +++ b/libs/dateutil/tz/tz.py @@ -1,10 +1,10 @@ # -*- coding: utf-8 -*- """ This module offers timezone implementations subclassing the abstract -:py:`datetime.tzinfo` type. There are classes to handle tzfile format files -(usually are in :file:`/etc/localtime`, :file:`/usr/share/zoneinfo`, etc), TZ -environment string (in all known formats), given ranges (with help from -relative deltas), local machine timezone, fixed offset timezone, and UTC +:py:class:`datetime.tzinfo` type. There are classes to handle tzfile format +files (usually are in :file:`/etc/localtime`, :file:`/usr/share/zoneinfo`, +etc), TZ environment string (in all known formats), given ranges (with help +from relative deltas), local machine timezone, fixed offset timezone, and UTC timezone. """ import datetime @@ -13,28 +13,63 @@ import time import sys import os import bisect -import copy +import weakref +from collections import OrderedDict -from operator import itemgetter - -from contextlib import contextmanager - -from six import string_types, PY3 -from ._common import tzname_in_python2, _tzinfo, _total_seconds +import six +from six import string_types +from six.moves import _thread +from ._common import tzname_in_python2, _tzinfo from ._common import tzrangebase, enfold +from ._common import _validate_fromutc_inputs +from ._factories import _TzSingleton, _TzOffsetFactory +from ._factories import _TzStrFactory try: from .win import tzwin, tzwinlocal except ImportError: tzwin = tzwinlocal = None +# For warning about rounding tzinfo +from warnings import warn + ZERO = datetime.timedelta(0) EPOCH = datetime.datetime.utcfromtimestamp(0) EPOCHORDINAL = EPOCH.toordinal() + +@six.add_metaclass(_TzSingleton) class tzutc(datetime.tzinfo): """ This is a tzinfo object that represents the UTC time zone. + + **Examples:** + + .. doctest:: + + >>> from datetime import * + >>> from dateutil.tz import * + + >>> datetime.now() + datetime.datetime(2003, 9, 27, 9, 40, 1, 521290) + + >>> datetime.now(tzutc()) + datetime.datetime(2003, 9, 27, 12, 40, 12, 156379, tzinfo=tzutc()) + + >>> datetime.now(tzutc()).tzname() + 'UTC' + + .. versionchanged:: 2.7.0 + ``tzutc()`` is now a singleton, so the result of ``tzutc()`` will + always return the same object. + + .. doctest:: + + >>> from dateutil.tz import tzutc, UTC + >>> tzutc() is tzutc() + True + >>> tzutc() is UTC + True """ def utcoffset(self, dt): return ZERO @@ -62,6 +97,14 @@ class tzutc(datetime.tzinfo): """ return False + @_validate_fromutc_inputs + def fromutc(self, dt): + """ + Fast track version of fromutc() returns the original ``dt`` object for + any valid :py:class:`datetime.datetime` object. + """ + return dt + def __eq__(self, other): if not isinstance(other, (tzutc, tzoffset)): return NotImplemented @@ -80,26 +123,33 @@ class tzutc(datetime.tzinfo): __reduce__ = object.__reduce__ +#: Convenience constant providing a :class:`tzutc()` instance +#: +#: .. versionadded:: 2.7.0 +UTC = tzutc() + + +@six.add_metaclass(_TzOffsetFactory) class tzoffset(datetime.tzinfo): """ A simple class for representing a fixed offset from UTC. :param name: The timezone name, to be returned when ``tzname()`` is called. - :param offset: The time zone offset in seconds, or (since version 2.6.0, represented - as a :py:class:`datetime.timedelta` object. + as a :py:class:`datetime.timedelta` object). """ def __init__(self, name, offset): self._name = name - + try: # Allow a timedelta - offset = _total_seconds(offset) + offset = offset.total_seconds() except (TypeError, AttributeError): pass - self._offset = datetime.timedelta(seconds=offset) + + self._offset = datetime.timedelta(seconds=_get_supported_offset(offset)) def utcoffset(self, dt): return self._offset @@ -107,6 +157,14 @@ class tzoffset(datetime.tzinfo): def dst(self, dt): return ZERO + @tzname_in_python2 + def tzname(self, dt): + return self._name + + @_validate_fromutc_inputs + def fromutc(self, dt): + return dt + self._offset + def is_ambiguous(self, dt): """ Whether or not the "wall time" of a given datetime is ambiguous in this @@ -114,8 +172,6 @@ class tzoffset(datetime.tzinfo): :param dt: A :py:class:`datetime.datetime`, naive or time zone aware. - - :return: Returns ``True`` if ambiguous, ``False`` otherwise. @@ -123,10 +179,6 @@ class tzoffset(datetime.tzinfo): """ return False - @tzname_in_python2 - def tzname(self, dt): - return self._name - def __eq__(self, other): if not isinstance(other, tzoffset): return NotImplemented @@ -141,7 +193,7 @@ class tzoffset(datetime.tzinfo): def __repr__(self): return "%s(%s, %s)" % (self.__class__.__name__, repr(self._name), - int(_total_seconds(self._offset))) + int(self._offset.total_seconds())) __reduce__ = object.__reduce__ @@ -161,6 +213,7 @@ class tzlocal(_tzinfo): self._dst_saved = self._dst_offset - self._std_offset self._hasdst = bool(self._dst_saved) + self._tznames = tuple(time.tzname) def utcoffset(self, dt): if dt is None and self._hasdst: @@ -182,7 +235,7 @@ class tzlocal(_tzinfo): @tzname_in_python2 def tzname(self, dt): - return time.tzname[self._isdst(dt)] + return self._tznames[self._isdst(dt)] def is_ambiguous(self, dt): """ @@ -247,12 +300,20 @@ class tzlocal(_tzinfo): return dstval def __eq__(self, other): - if not isinstance(other, tzlocal): + if isinstance(other, tzlocal): + return (self._std_offset == other._std_offset and + self._dst_offset == other._dst_offset) + elif isinstance(other, tzutc): + return (not self._hasdst and + self._tznames[0] in {'UTC', 'GMT'} and + self._std_offset == ZERO) + elif isinstance(other, tzoffset): + return (not self._hasdst and + self._tznames[0] == other._name and + self._std_offset == other._offset) + else: return NotImplemented - return (self._std_offset == other._std_offset and - self._dst_offset == other._dst_offset) - __hash__ = None def __ne__(self, other): @@ -314,7 +375,7 @@ class _tzfile(object): Lightweight class for holding the relevant transition and time zone information read from binary tzfiles. """ - attrs = ['trans_list', 'trans_idx', 'ttinfo_list', + attrs = ['trans_list', 'trans_list_utc', 'trans_idx', 'ttinfo_list', 'ttinfo_std', 'ttinfo_dst', 'ttinfo_before', 'ttinfo_first'] def __init__(self, **kwargs): @@ -337,11 +398,61 @@ class tzfile(_tzinfo): and ``fileobj`` is a file stream, this parameter will be set either to ``fileobj``'s ``name`` attribute or to ``repr(fileobj)``. - See `Sources for Time Zone and Daylight Saving Time Data - `_ for more information. Time zone - files can be compiled from the `IANA Time Zone database files + See `Sources for Time Zone and Daylight Saving Time Data + `_ for more information. + Time zone files can be compiled from the `IANA Time Zone database files `_ with the `zic time zone compiler `_ + + .. note:: + + Only construct a ``tzfile`` directly if you have a specific timezone + file on disk that you want to read into a Python ``tzinfo`` object. + If you want to get a ``tzfile`` representing a specific IANA zone, + (e.g. ``'America/New_York'``), you should call + :func:`dateutil.tz.gettz` with the zone identifier. + + + **Examples:** + + Using the US Eastern time zone as an example, we can see that a ``tzfile`` + provides time zone information for the standard Daylight Saving offsets: + + .. testsetup:: tzfile + + from dateutil.tz import gettz + from datetime import datetime + + .. doctest:: tzfile + + >>> NYC = gettz('America/New_York') + >>> NYC + tzfile('/usr/share/zoneinfo/America/New_York') + + >>> print(datetime(2016, 1, 3, tzinfo=NYC)) # EST + 2016-01-03 00:00:00-05:00 + + >>> print(datetime(2016, 7, 7, tzinfo=NYC)) # EDT + 2016-07-07 00:00:00-04:00 + + + The ``tzfile`` structure contains a fully history of the time zone, + so historical dates will also have the right offsets. For example, before + the adoption of the UTC standards, New York used local solar mean time: + + .. doctest:: tzfile + + >>> print(datetime(1901, 4, 12, tzinfo=NYC)) # LMT + 1901-04-12 00:00:00-04:56 + + And during World War II, New York was on "Eastern War Time", which was a + state of permanent daylight saving time: + + .. doctest:: tzfile + + >>> print(datetime(1944, 2, 7, tzinfo=NYC)) # EWT + 1944-02-07 00:00:00-04:00 + """ def __init__(self, fileobj, filename=None): @@ -361,7 +472,7 @@ class tzfile(_tzinfo): if fileobj is not None: if not file_opened_here: - fileobj = _ContextWrapper(fileobj) + fileobj = _nullcontext(fileobj) with fileobj as file_stream: tzobj = self._read_tzfile(file_stream) @@ -424,10 +535,10 @@ class tzfile(_tzinfo): # change. if timecnt: - out.trans_list = list(struct.unpack(">%dl" % timecnt, - fileobj.read(timecnt*4))) + out.trans_list_utc = list(struct.unpack(">%dl" % timecnt, + fileobj.read(timecnt*4))) else: - out.trans_list = [] + out.trans_list_utc = [] # Next come tzh_timecnt one-byte values of type unsigned # char; each one tells which of the different types of @@ -438,7 +549,7 @@ class tzfile(_tzinfo): if timecnt: out.trans_idx = struct.unpack(">%dB" % timecnt, - fileobj.read(timecnt)) + fileobj.read(timecnt)) else: out.trans_idx = [] @@ -469,10 +580,9 @@ class tzfile(_tzinfo): # The pairs of values are sorted in ascending order # by time. - # Not used, for now (but read anyway for correct file position) + # Not used, for now (but seek for correct file position) if leapcnt: - leap = struct.unpack(">%dl" % (leapcnt*2), - fileobj.read(leapcnt*8)) + fileobj.seek(leapcnt * 8, os.SEEK_CUR) # Then there are tzh_ttisstdcnt standard/wall # indicators, each stored as a one-byte value; @@ -502,10 +612,7 @@ class tzfile(_tzinfo): out.ttinfo_list = [] for i in range(typecnt): gmtoff, isdst, abbrind = ttinfo[i] - # Round to full-minutes if that's not the case. Python's - # datetime doesn't accept sub-minute timezones. Check - # http://python.org/sf/1447945 for some information. - gmtoff = 60 * ((gmtoff + 30) // 60) + gmtoff = _get_supported_offset(gmtoff) tti = _ttinfo() tti.offset = gmtoff tti.dstoffset = datetime.timedelta(0) @@ -527,7 +634,7 @@ class tzfile(_tzinfo): out.ttinfo_dst = None out.ttinfo_before = None if out.ttinfo_list: - if not out.trans_list: + if not out.trans_list_utc: out.ttinfo_std = out.ttinfo_first = out.ttinfo_list[0] else: for i in range(timecnt-1, -1, -1): @@ -557,43 +664,52 @@ class tzfile(_tzinfo): # isgmt are off, so it should be in wall time. OTOH, it's # always in gmt time. Let me know if you have comments # about this. - laststdoffset = None + lastdst = None + lastoffset = None + lastdstoffset = None + lastbaseoffset = None + out.trans_list = [] + for i, tti in enumerate(out.trans_idx): - if not tti.isdst: - offset = tti.offset - laststdoffset = offset - else: - if laststdoffset is not None: - # Store the DST offset as well and update it in the list - tti.dstoffset = tti.offset - laststdoffset - out.trans_idx[i] = tti + offset = tti.offset + dstoffset = 0 - offset = laststdoffset or 0 + if lastdst is not None: + if tti.isdst: + if not lastdst: + dstoffset = offset - lastoffset - out.trans_list[i] += offset + if not dstoffset and lastdstoffset: + dstoffset = lastdstoffset - # In case we missed any DST offsets on the way in for some reason, make - # a second pass over the list, looking for the /next/ DST offset. - laststdoffset = None - for i in reversed(range(len(out.trans_idx))): - tti = out.trans_idx[i] - if tti.isdst: - if not (tti.dstoffset or laststdoffset is None): - tti.dstoffset = tti.offset - laststdoffset - else: - laststdoffset = tti.offset + tti.dstoffset = datetime.timedelta(seconds=dstoffset) + lastdstoffset = dstoffset - if not isinstance(tti.dstoffset, datetime.timedelta): - tti.dstoffset = datetime.timedelta(seconds=tti.dstoffset) - - out.trans_idx[i] = tti + # If a time zone changes its base offset during a DST transition, + # then you need to adjust by the previous base offset to get the + # transition time in local time. Otherwise you use the current + # base offset. Ideally, I would have some mathematical proof of + # why this is true, but I haven't really thought about it enough. + baseoffset = offset - dstoffset + adjustment = baseoffset + if (lastbaseoffset is not None and baseoffset != lastbaseoffset + and tti.isdst != lastdst): + # The base DST has changed + adjustment = lastbaseoffset + + lastdst = tti.isdst + lastoffset = offset + lastbaseoffset = baseoffset + + out.trans_list.append(out.trans_list_utc[i] + adjustment) out.trans_idx = tuple(out.trans_idx) out.trans_list = tuple(out.trans_list) + out.trans_list_utc = tuple(out.trans_list_utc) return out - def _find_last_transition(self, dt): + def _find_last_transition(self, dt, in_utc=False): # If there's no list, there are no transitions to find if not self._trans_list: return None @@ -602,14 +718,15 @@ class tzfile(_tzinfo): # Find where the timestamp fits in the transition list - if the # timestamp is a transition time, it's part of the "after" period. - idx = bisect.bisect_right(self._trans_list, timestamp) + trans_list = self._trans_list_utc if in_utc else self._trans_list + idx = bisect.bisect_right(trans_list, timestamp) # We want to know when the previous transition was, so subtract off 1 return idx - 1 def _get_ttinfo(self, idx): # For no list or after the last transition, default to _ttinfo_std - if idx is None or (idx + 1) == len(self._trans_list): + if idx is None or (idx + 1) >= len(self._trans_list): return self._ttinfo_std # If there is a list and the time is before it, return _ttinfo_before @@ -623,6 +740,42 @@ class tzfile(_tzinfo): return self._get_ttinfo(idx) + def fromutc(self, dt): + """ + The ``tzfile`` implementation of :py:func:`datetime.tzinfo.fromutc`. + + :param dt: + A :py:class:`datetime.datetime` object. + + :raises TypeError: + Raised if ``dt`` is not a :py:class:`datetime.datetime` object. + + :raises ValueError: + Raised if this is called with a ``dt`` which does not have this + ``tzinfo`` attached. + + :return: + Returns a :py:class:`datetime.datetime` object representing the + wall time in ``self``'s time zone. + """ + # These isinstance checks are in datetime.tzinfo, so we'll preserve + # them, even if we don't care about duck typing. + if not isinstance(dt, datetime.datetime): + raise TypeError("fromutc() requires a datetime argument") + + if dt.tzinfo is not self: + raise ValueError("dt.tzinfo is not self") + + # First treat UTC as wall time and get the transition we're in. + idx = self._find_last_transition(dt, in_utc=True) + tti = self._get_ttinfo(idx) + + dt_out = dt + datetime.timedelta(seconds=tti.offset) + + fold = self.is_ambiguous(dt_out, idx=idx) + + return enfold(dt_out, fold=int(fold)) + def is_ambiguous(self, dt, idx=None): """ Whether or not the "wall time" of a given datetime is ambiguous in this @@ -660,7 +813,7 @@ class tzfile(_tzinfo): if idx is None or idx == 0: return idx - # Get the current datetime as a timestamp + # If it's ambiguous and we're in a fold, shift to a different index. idx_offset = int(not _fold and self.is_ambiguous(dt, idx)) return idx - idx_offset @@ -680,7 +833,7 @@ class tzfile(_tzinfo): if not self._ttinfo_dst: return ZERO - + tti = self._find_ttinfo(dt) if not tti.isdst: @@ -753,8 +906,9 @@ class tzrange(tzrangebase): :param start: A :class:`relativedelta.relativedelta` object or equivalent specifying - the time and time of year that daylight savings time starts. To specify, - for example, that DST starts at 2AM on the 2nd Sunday in March, pass: + the time and time of year that daylight savings time starts. To + specify, for example, that DST starts at 2AM on the 2nd Sunday in + March, pass: ``relativedelta(hours=2, month=3, day=1, weekday=SU(+2))`` @@ -762,12 +916,12 @@ class tzrange(tzrangebase): value is 2 AM on the first Sunday in April. :param end: - A :class:`relativedelta.relativedelta` object or equivalent representing - the time and time of year that daylight savings time ends, with the - same specification method as in ``start``. One note is that this should - point to the first time in the *standard* zone, so if a transition - occurs at 2AM in the DST zone and the clocks are set back 1 hour to 1AM, - set the `hours` parameter to +1. + A :class:`relativedelta.relativedelta` object or equivalent + representing the time and time of year that daylight savings time + ends, with the same specification method as in ``start``. One note is + that this should point to the first time in the *standard* zone, so if + a transition occurs at 2AM in the DST zone and the clocks are set back + 1 hour to 1AM, set the ``hours`` parameter to +1. **Examples:** @@ -803,12 +957,12 @@ class tzrange(tzrangebase): self._dst_abbr = dstabbr try: - stdoffset = _total_seconds(stdoffset) + stdoffset = stdoffset.total_seconds() except (TypeError, AttributeError): pass try: - dstoffset = _total_seconds(dstoffset) + dstoffset = dstoffset.total_seconds() except (TypeError, AttributeError): pass @@ -879,6 +1033,7 @@ class tzrange(tzrangebase): return self._dst_base_offset_ +@six.add_metaclass(_TzStrFactory) class tzstr(tzrange): """ ``tzstr`` objects are time zone objects specified by a time-zone string as @@ -897,25 +1052,38 @@ class tzstr(tzrange): :param s: A time zone string in ``TZ`` variable format. This can be a - :class:`bytes` (2.x: :class:`str`), :class:`str` (2.x: :class:`unicode`) - or a stream emitting unicode characters (e.g. :class:`StringIO`). + :class:`bytes` (2.x: :class:`str`), :class:`str` (2.x: + :class:`unicode`) or a stream emitting unicode characters + (e.g. :class:`StringIO`). :param posix_offset: Optional. If set to ``True``, interpret strings such as ``GMT+3`` or ``UTC+3`` as being 3 hours *behind* UTC rather than ahead, per the POSIX standard. + .. caution:: + + Prior to version 2.7.0, this function also supported time zones + in the format: + + * ``EST5EDT,4,0,6,7200,10,0,26,7200,3600`` + * ``EST5EDT,4,1,0,7200,10,-1,0,7200,3600`` + + This format is non-standard and has been deprecated; this function + will raise a :class:`DeprecatedTZFormatWarning` until + support is removed in a future version. + .. _`GNU C Library: TZ Variable`: https://www.gnu.org/software/libc/manual/html_node/TZ-Variable.html """ def __init__(self, s, posix_offset=False): global parser - from dateutil import parser + from dateutil.parser import _parser as parser self._s = s res = parser._parsetz(s) - if res is None: + if res is None or res.any_unused_tokens: raise ValueError("unknown string format") # Here we break the compatibility with the TZ variable handling. @@ -1004,6 +1172,7 @@ class _tzicalvtz(_tzinfo): self._comps = comps self._cachedate = [] self._cachecomp = [] + self._cache_lock = _thread.allocate_lock() def _find_comp(self, dt): if len(self._comps) == 1: @@ -1012,11 +1181,12 @@ class _tzicalvtz(_tzinfo): dt = dt.replace(tzinfo=None) try: - return self._cachecomp[self._cachedate.index((dt, self._fold(dt)))] + with self._cache_lock: + return self._cachecomp[self._cachedate.index( + (dt, self._fold(dt)))] except ValueError: pass - lastcompdt = None lastcomp = None @@ -1039,12 +1209,13 @@ class _tzicalvtz(_tzinfo): else: lastcomp = comp[0] - self._cachedate.insert(0, (dt, self._fold(dt))) - self._cachecomp.insert(0, lastcomp) + with self._cache_lock: + self._cachedate.insert(0, (dt, self._fold(dt))) + self._cachecomp.insert(0, lastcomp) - if len(self._cachedate) > 10: - self._cachedate.pop() - self._cachecomp.pop() + if len(self._cachedate) > 10: + self._cachedate.pop() + self._cachecomp.pop() return lastcomp @@ -1082,13 +1253,13 @@ class _tzicalvtz(_tzinfo): class tzical(object): """ This object is designed to parse an iCalendar-style ``VTIMEZONE`` structure - as set out in `RFC 2445`_ Section 4.6.5 into one or more `tzinfo` objects. + as set out in `RFC 5545`_ Section 4.6.5 into one or more `tzinfo` objects. :param `fileobj`: A file or stream in iCalendar format, which should be UTF-8 encoded with CRLF endings. - .. _`RFC 2445`: https://www.ietf.org/rfc/rfc2445.txt + .. _`RFC 5545`: https://tools.ietf.org/html/rfc5545 """ def __init__(self, fileobj): global rrule @@ -1098,10 +1269,9 @@ class tzical(object): self._s = fileobj # ical should be encoded in UTF-8 with CRLF fileobj = open(fileobj, 'r') - file_opened_here = True else: self._s = getattr(fileobj, 'name', repr(fileobj)) - fileobj = _ContextWrapper(fileobj) + fileobj = _nullcontext(fileobj) self._vtz = {} @@ -1237,6 +1407,13 @@ class tzical(object): raise ValueError("invalid component end: "+value) elif comptype: if name == "DTSTART": + # DTSTART in VTIMEZONE takes a subset of valid RRULE + # values under RFC 5545. + for parm in parms: + if parm != 'VALUE=DATE-TIME': + msg = ('Unsupported DTSTART param in ' + + 'VTIMEZONE: ' + parm) + raise ValueError(msg) rrulelines.append(line) founddtstart = True elif name in ("RRULE", "RDATE", "EXRULE", "EXDATE"): @@ -1278,6 +1455,7 @@ class tzical(object): def __repr__(self): return "%s(%s)" % (self.__class__.__name__, repr(self._s)) + if sys.platform != "win32": TZFILES = ["/etc/localtime", "localtime"] TZPATHS = ["/usr/share/zoneinfo", @@ -1289,78 +1467,217 @@ else: TZPATHS = [] -def gettz(name=None): - tz = None - if not name: - try: - name = os.environ["TZ"] - except KeyError: - pass - if name is None or name == ":": - for filepath in TZFILES: - if not os.path.isabs(filepath): - filename = filepath - for path in TZPATHS: - filepath = os.path.join(path, filename) - if os.path.isfile(filepath): - break - else: - continue - if os.path.isfile(filepath): - try: - tz = tzfile(filepath) - break - except (IOError, OSError, ValueError): - pass - else: - tz = tzlocal() - else: - if name.startswith(":"): - name = name[:-1] - if os.path.isabs(name): - if os.path.isfile(name): - tz = tzfile(name) - else: - tz = None - else: - for path in TZPATHS: - filepath = os.path.join(path, name) - if not os.path.isfile(filepath): - filepath = filepath.replace(' ', '_') - if not os.path.isfile(filepath): - continue - try: - tz = tzfile(filepath) - break - except (IOError, OSError, ValueError): - pass - else: - tz = None - if tzwin is not None: - try: - tz = tzwin(name) - except WindowsError: - tz = None +def __get_gettz(): + tzlocal_classes = (tzlocal,) + if tzwinlocal is not None: + tzlocal_classes += (tzwinlocal,) - if not tz: - from dateutil.zoneinfo import get_zonefile_instance - tz = get_zonefile_instance().get(name) + class GettzFunc(object): + """ + Retrieve a time zone object from a string representation - if not tz: - for c in name: - # name must have at least one offset to be a tzstr - if c in "0123456789": - try: - tz = tzstr(name) - except ValueError: - pass - break + This function is intended to retrieve the :py:class:`tzinfo` subclass + that best represents the time zone that would be used if a POSIX + `TZ variable`_ were set to the same value. + + If no argument or an empty string is passed to ``gettz``, local time + is returned: + + .. code-block:: python3 + + >>> gettz() + tzfile('/etc/localtime') + + This function is also the preferred way to map IANA tz database keys + to :class:`tzfile` objects: + + .. code-block:: python3 + + >>> gettz('Pacific/Kiritimati') + tzfile('/usr/share/zoneinfo/Pacific/Kiritimati') + + On Windows, the standard is extended to include the Windows-specific + zone names provided by the operating system: + + .. code-block:: python3 + + >>> gettz('Egypt Standard Time') + tzwin('Egypt Standard Time') + + Passing a GNU ``TZ`` style string time zone specification returns a + :class:`tzstr` object: + + .. code-block:: python3 + + >>> gettz('AEST-10AEDT-11,M10.1.0/2,M4.1.0/3') + tzstr('AEST-10AEDT-11,M10.1.0/2,M4.1.0/3') + + :param name: + A time zone name (IANA, or, on Windows, Windows keys), location of + a ``tzfile(5)`` zoneinfo file or ``TZ`` variable style time zone + specifier. An empty string, no argument or ``None`` is interpreted + as local time. + + :return: + Returns an instance of one of ``dateutil``'s :py:class:`tzinfo` + subclasses. + + .. versionchanged:: 2.7.0 + + After version 2.7.0, any two calls to ``gettz`` using the same + input strings will return the same object: + + .. code-block:: python3 + + >>> tz.gettz('America/Chicago') is tz.gettz('America/Chicago') + True + + In addition to improving performance, this ensures that + `"same zone" semantics`_ are used for datetimes in the same zone. + + + .. _`TZ variable`: + https://www.gnu.org/software/libc/manual/html_node/TZ-Variable.html + + .. _`"same zone" semantics`: + https://blog.ganssle.io/articles/2018/02/aware-datetime-arithmetic.html + """ + def __init__(self): + + self.__instances = weakref.WeakValueDictionary() + self.__strong_cache_size = 8 + self.__strong_cache = OrderedDict() + self._cache_lock = _thread.allocate_lock() + + def __call__(self, name=None): + with self._cache_lock: + rv = self.__instances.get(name, None) + + if rv is None: + rv = self.nocache(name=name) + if not (name is None + or isinstance(rv, tzlocal_classes) + or rv is None): + # tzlocal is slightly more complicated than the other + # time zone providers because it depends on environment + # at construction time, so don't cache that. + # + # We also cannot store weak references to None, so we + # will also not store that. + self.__instances[name] = rv else: - if name in ("GMT", "UTC"): - tz = tzutc() - elif name in time.tzname: - tz = tzlocal() - return tz + # No need for strong caching, return immediately + return rv + + self.__strong_cache[name] = self.__strong_cache.pop(name, rv) + + if len(self.__strong_cache) > self.__strong_cache_size: + self.__strong_cache.popitem(last=False) + + return rv + + def set_cache_size(self, size): + with self._cache_lock: + self.__strong_cache_size = size + while len(self.__strong_cache) > size: + self.__strong_cache.popitem(last=False) + + def cache_clear(self): + with self._cache_lock: + self.__instances = weakref.WeakValueDictionary() + self.__strong_cache.clear() + + @staticmethod + def nocache(name=None): + """A non-cached version of gettz""" + tz = None + if not name: + try: + name = os.environ["TZ"] + except KeyError: + pass + if name is None or name in ("", ":"): + for filepath in TZFILES: + if not os.path.isabs(filepath): + filename = filepath + for path in TZPATHS: + filepath = os.path.join(path, filename) + if os.path.isfile(filepath): + break + else: + continue + if os.path.isfile(filepath): + try: + tz = tzfile(filepath) + break + except (IOError, OSError, ValueError): + pass + else: + tz = tzlocal() + else: + try: + if name.startswith(":"): + name = name[1:] + except TypeError as e: + if isinstance(name, bytes): + new_msg = "gettz argument should be str, not bytes" + six.raise_from(TypeError(new_msg), e) + else: + raise + if os.path.isabs(name): + if os.path.isfile(name): + tz = tzfile(name) + else: + tz = None + else: + for path in TZPATHS: + filepath = os.path.join(path, name) + if not os.path.isfile(filepath): + filepath = filepath.replace(' ', '_') + if not os.path.isfile(filepath): + continue + try: + tz = tzfile(filepath) + break + except (IOError, OSError, ValueError): + pass + else: + tz = None + if tzwin is not None: + try: + tz = tzwin(name) + except (WindowsError, UnicodeEncodeError): + # UnicodeEncodeError is for Python 2.7 compat + tz = None + + if not tz: + from dateutil.zoneinfo import get_zonefile_instance + tz = get_zonefile_instance().get(name) + + if not tz: + for c in name: + # name is not a tzstr unless it has at least + # one offset. For short values of "name", an + # explicit for loop seems to be the fastest way + # To determine if a string contains a digit + if c in "0123456789": + try: + tz = tzstr(name) + except ValueError: + pass + break + else: + if name in ("GMT", "UTC"): + tz = UTC + elif name in time.tzname: + tz = tzlocal() + return tz + + return GettzFunc() + + +gettz = __get_gettz() +del __get_gettz def datetime_exists(dt, tz=None): @@ -1375,9 +1692,12 @@ def datetime_exists(dt, tz=None): :param tz: A :class:`datetime.tzinfo` with support for the ``fold`` attribute. If ``None`` or not provided, the datetime's own time zone will be used. - + :return: - Returns a boolean value whether or not the "wall time" exists in ``tz``. + Returns a boolean value whether or not the "wall time" exists in + ``tz``. + + .. versionadded:: 2.7.0 """ if tz is None: if dt.tzinfo is None: @@ -1388,7 +1708,7 @@ def datetime_exists(dt, tz=None): # This is essentially a test of whether or not the datetime can survive # a round trip to UTC. - dt_rt = dt.replace(tzinfo=tz).astimezone(tzutc()).astimezone(tz) + dt_rt = dt.replace(tzinfo=tz).astimezone(UTC).astimezone(tz) dt_rt = dt_rt.replace(tzinfo=None) return dt == dt_rt @@ -1407,7 +1727,7 @@ def datetime_ambiguous(dt, tz=None): :param tz: A :class:`datetime.tzinfo` with support for the ``fold`` attribute. If ``None`` or not provided, the datetime's own time zone will be used. - + :return: Returns a boolean value whether or not the "wall time" is ambiguous in ``tz``. @@ -1425,7 +1745,7 @@ def datetime_ambiguous(dt, tz=None): if is_ambiguous_fn is not None: try: return tz.is_ambiguous(dt) - except: + except Exception: pass # If it doesn't come out and tell us it's ambiguous, we'll just check if @@ -1440,25 +1760,90 @@ def datetime_ambiguous(dt, tz=None): return not (same_offset and same_dst) +def resolve_imaginary(dt): + """ + Given a datetime that may be imaginary, return an existing datetime. + + This function assumes that an imaginary datetime represents what the + wall time would be in a zone had the offset transition not occurred, so + it will always fall forward by the transition's change in offset. + + .. doctest:: + + >>> from dateutil import tz + >>> from datetime import datetime + >>> NYC = tz.gettz('America/New_York') + >>> print(tz.resolve_imaginary(datetime(2017, 3, 12, 2, 30, tzinfo=NYC))) + 2017-03-12 03:30:00-04:00 + + >>> KIR = tz.gettz('Pacific/Kiritimati') + >>> print(tz.resolve_imaginary(datetime(1995, 1, 1, 12, 30, tzinfo=KIR))) + 1995-01-02 12:30:00+14:00 + + As a note, :func:`datetime.astimezone` is guaranteed to produce a valid, + existing datetime, so a round-trip to and from UTC is sufficient to get + an extant datetime, however, this generally "falls back" to an earlier time + rather than falling forward to the STD side (though no guarantees are made + about this behavior). + + :param dt: + A :class:`datetime.datetime` which may or may not exist. + + :return: + Returns an existing :class:`datetime.datetime`. If ``dt`` was not + imaginary, the datetime returned is guaranteed to be the same object + passed to the function. + + .. versionadded:: 2.7.0 + """ + if dt.tzinfo is not None and not datetime_exists(dt): + + curr_offset = (dt + datetime.timedelta(hours=24)).utcoffset() + old_offset = (dt - datetime.timedelta(hours=24)).utcoffset() + + dt += curr_offset - old_offset + + return dt + + def _datetime_to_timestamp(dt): """ - Convert a :class:`datetime.datetime` object to an epoch timestamp in seconds - since January 1, 1970, ignoring the time zone. + Convert a :class:`datetime.datetime` object to an epoch timestamp in + seconds since January 1, 1970, ignoring the time zone. """ - return _total_seconds((dt.replace(tzinfo=None) - EPOCH)) + return (dt.replace(tzinfo=None) - EPOCH).total_seconds() -class _ContextWrapper(object): - """ - Class for wrapping contexts so that they are passed through in a - with statement. - """ - def __init__(self, context): - self.context = context - def __enter__(self): - return self.context +if sys.version_info >= (3, 6): + def _get_supported_offset(second_offset): + return second_offset +else: + def _get_supported_offset(second_offset): + # For python pre-3.6, round to full-minutes if that's not the case. + # Python's datetime doesn't accept sub-minute timezones. Check + # http://python.org/sf/1447945 or https://bugs.python.org/issue5288 + # for some information. + old_offset = second_offset + calculated_offset = 60 * ((second_offset + 30) // 60) + return calculated_offset - def __exit__(*args, **kwargs): - pass + +try: + # Python 3.7 feature + from contextlib import nullcontext as _nullcontext +except ImportError: + class _nullcontext(object): + """ + Class for wrapping contexts so that they are passed through in a + with statement. + """ + def __init__(self, context): + self.context = context + + def __enter__(self): + return self.context + + def __exit__(*args, **kwargs): + pass # vim:ts=4:sw=4:et diff --git a/libs/dateutil/tz/win.py b/libs/dateutil/tz/win.py index 9f4e5519f..cde07ba79 100644 --- a/libs/dateutil/tz/win.py +++ b/libs/dateutil/tz/win.py @@ -1,3 +1,11 @@ +# -*- coding: utf-8 -*- +""" +This module provides an interface to the native time zone data on Windows, +including :py:class:`datetime.tzinfo` implementations. + +Attempting to import this module on a non-Windows platform will raise an +:py:obj:`ImportError`. +""" # This code was originally contributed by Jeffrey Harris. import datetime import struct @@ -12,7 +20,6 @@ except ValueError: # ValueError is raised on non-Windows systems for some horrible reason. raise ImportError("Running tzwin on non-Windows system") -from ._common import tzname_in_python2, _tzinfo from ._common import tzrangebase __all__ = ["tzwin", "tzwinlocal", "tzres"] @@ -34,12 +41,13 @@ def _settzkeyname(): handle.Close() return TZKEYNAME + TZKEYNAME = _settzkeyname() class tzres(object): """ - Class for accessing `tzres.dll`, which contains timezone name related + Class for accessing ``tzres.dll``, which contains timezone name related resources. .. versionadded:: 2.5.0 @@ -49,7 +57,7 @@ class tzres(object): def __init__(self, tzres_loc='tzres.dll'): # Load the user32 DLL so we can load strings from tzres user32 = ctypes.WinDLL('user32') - + # Specify the LoadStringW function user32.LoadStringW.argtypes = (wintypes.HINSTANCE, wintypes.UINT, @@ -63,7 +71,7 @@ class tzres(object): def load_name(self, offset): """ Load a timezone name from a DLL offset (integer). - + >>> from dateutil.tzwin import tzres >>> tzr = tzres() >>> print(tzr.load_name(112)) @@ -72,9 +80,10 @@ class tzres(object): :param offset: A positive integer value referring to a string from the tzres dll. - ..note: + .. note:: + Offsets found in the registry are generally of the form - `@tzres.dll,-114`. The offset in this case if 114, not -114. + ``@tzres.dll,-114``. The offset in this case is 114, not -114. """ resource = self.p_wchar() @@ -146,6 +155,9 @@ class tzwinbase(tzrangebase): return result def display(self): + """ + Return the display name of the time zone. + """ return self._display def transitions(self, year): @@ -188,13 +200,23 @@ class tzwinbase(tzrangebase): class tzwin(tzwinbase): + """ + Time zone object created from the zone info in the Windows registry + + These are similar to :py:class:`dateutil.tz.tzrange` objects in that + the time zone data is provided in the format of a single offset rule + for either 0 or 2 time zone transitions per year. + + :param: name + The name of a Windows time zone key, e.g. "Eastern Standard Time". + The full list of keys can be retrieved with :func:`tzwin.list`. + """ def __init__(self, name): self._name = name - # multiple contexts only possible in 2.7 and 3.1, we still support 2.6 with winreg.ConnectRegistry(None, winreg.HKEY_LOCAL_MACHINE) as handle: - tzkeyname = text_type("{kn}\{name}").format(kn=TZKEYNAME, name=name) + tzkeyname = text_type("{kn}\\{name}").format(kn=TZKEYNAME, name=name) with winreg.OpenKey(handle, tzkeyname) as tzkey: keydict = valuestodict(tzkey) @@ -235,6 +257,22 @@ class tzwin(tzwinbase): class tzwinlocal(tzwinbase): + """ + Class representing the local time zone information in the Windows registry + + While :class:`dateutil.tz.tzlocal` makes system calls (via the :mod:`time` + module) to retrieve time zone information, ``tzwinlocal`` retrieves the + rules directly from the Windows registry and creates an object like + :class:`dateutil.tz.tzwin`. + + Because Windows does not have an equivalent of :func:`time.tzset`, on + Windows, :class:`dateutil.tz.tzlocal` instances will always reflect the + time zone settings *at the time that the process was started*, meaning + changes to the machine's time zone settings during the run of a program + on Windows will **not** be reflected by :class:`dateutil.tz.tzlocal`. + Because ``tzwinlocal`` reads the registry directly, it is unaffected by + this issue. + """ def __init__(self): with winreg.ConnectRegistry(None, winreg.HKEY_LOCAL_MACHINE) as handle: with winreg.OpenKey(handle, TZLOCALKEYNAME) as tzlocalkey: @@ -244,7 +282,7 @@ class tzwinlocal(tzwinbase): self._dst_abbr = keydict["DaylightName"] try: - tzkeyname = text_type('{kn}\{sn}').format(kn=TZKEYNAME, + tzkeyname = text_type('{kn}\\{sn}').format(kn=TZKEYNAME, sn=self._std_abbr) with winreg.OpenKey(handle, tzkeyname) as tzkey: _keydict = valuestodict(tzkey) @@ -266,7 +304,7 @@ class tzwinlocal(tzwinbase): self._stdweeknumber, # Last = 5 self._stdhour, self._stdminute) = tup[1:5] - + self._stddayofweek = tup[7] tup = struct.unpack("=8h", keydict["DaylightStart"]) diff --git a/libs/dateutil/tzwin.py b/libs/dateutil/tzwin.py index 55cd91028..cebc673e4 100644 --- a/libs/dateutil/tzwin.py +++ b/libs/dateutil/tzwin.py @@ -1,2 +1,2 @@ # tzwin has moved to dateutil.tz.win -from .tz.win import * \ No newline at end of file +from .tz.win import * diff --git a/libs/dateutil/utils.py b/libs/dateutil/utils.py index ebcce6aa2..dd2d245a0 100644 --- a/libs/dateutil/utils.py +++ b/libs/dateutil/utils.py @@ -28,7 +28,7 @@ def today(tzinfo=None): def default_tzinfo(dt, tzinfo): """ - Sets the the ``tzinfo`` parameter on naive datetimes only + Sets the ``tzinfo`` parameter on naive datetimes only This is useful for example when you are provided a datetime that may have either an implicit or explicit time zone, such as when parsing a time zone @@ -63,7 +63,7 @@ def default_tzinfo(dt, tzinfo): def within_delta(dt1, dt2, delta): """ - Useful for comparing two datetimes that may a negilible difference + Useful for comparing two datetimes that may have a negligible difference to be considered equal. """ delta = abs(delta) diff --git a/libs/dateutil/zoneinfo/__init__.py b/libs/dateutil/zoneinfo/__init__.py index 7145e05cf..34f11ad66 100644 --- a/libs/dateutil/zoneinfo/__init__.py +++ b/libs/dateutil/zoneinfo/__init__.py @@ -1,32 +1,20 @@ # -*- coding: utf-8 -*- -import logging -import os import warnings -import tempfile -import shutil import json from tarfile import TarFile from pkgutil import get_data from io import BytesIO -from contextlib import closing -from dateutil.tz import tzfile +from dateutil.tz import tzfile as _tzfile -__all__ = ["get_zonefile_instance", "gettz", "gettz_db_metadata", "rebuild"] +__all__ = ["get_zonefile_instance", "gettz", "gettz_db_metadata"] ZONEFILENAME = "dateutil-zoneinfo.tar.gz" METADATA_FN = 'METADATA' -# python2.6 compatability. Note that TarFile.__exit__ != TarFile.close, but -# it's close enough for python2.6 -tar_open = TarFile.open -if not hasattr(TarFile, '__exit__'): - def tar_open(*args, **kwargs): - return closing(TarFile.open(*args, **kwargs)) - -class tzfile(tzfile): +class tzfile(_tzfile): def __reduce__(self): return (gettz, (self._filename,)) @@ -42,23 +30,15 @@ def getzoneinfofile_stream(): class ZoneInfoFile(object): def __init__(self, zonefile_stream=None): if zonefile_stream is not None: - with tar_open(fileobj=zonefile_stream, mode='r') as tf: - # dict comprehension does not work on python2.6 - # TODO: get back to the nicer syntax when we ditch python2.6 - # self.zones = {zf.name: tzfile(tf.extractfile(zf), - # filename = zf.name) - # for zf in tf.getmembers() if zf.isfile()} - self.zones = dict((zf.name, tzfile(tf.extractfile(zf), - filename=zf.name)) - for zf in tf.getmembers() - if zf.isfile() and zf.name != METADATA_FN) + with TarFile.open(fileobj=zonefile_stream) as tf: + self.zones = {zf.name: tzfile(tf.extractfile(zf), filename=zf.name) + for zf in tf.getmembers() + if zf.isfile() and zf.name != METADATA_FN} # deal with links: They'll point to their parent object. Less # waste of memory - # links = {zl.name: self.zones[zl.linkname] - # for zl in tf.getmembers() if zl.islnk() or zl.issym()} - links = dict((zl.name, self.zones[zl.linkname]) - for zl in tf.getmembers() if - zl.islnk() or zl.issym()) + links = {zl.name: self.zones[zl.linkname] + for zl in tf.getmembers() if + zl.islnk() or zl.issym()} self.zones.update(links) try: metadata_json = tf.extractfile(tf.getmember(METADATA_FN)) @@ -68,14 +48,14 @@ class ZoneInfoFile(object): # no metadata in tar file self.metadata = None else: - self.zones = dict() + self.zones = {} self.metadata = None def get(self, name, default=None): """ Wrapper for :func:`ZoneInfoFile.zones.get`. This is a convenience method for retrieving zones from the zone dictionary. - + :param name: The name of the zone to retrieve. (Generally IANA zone names) @@ -94,7 +74,8 @@ class ZoneInfoFile(object): # timezone. Ugly, but adheres to the api. # # TODO: Remove after deprecation period. -_CLASS_ZONE_INSTANCE = list() +_CLASS_ZONE_INSTANCE = [] + def get_zonefile_instance(new_instance=False): """ @@ -124,6 +105,7 @@ def get_zonefile_instance(new_instance=False): return zif + def gettz(name): """ This retrieves a time zone from the local zoneinfo tarball that is packaged @@ -183,5 +165,3 @@ def gettz_db_metadata(): if len(_CLASS_ZONE_INSTANCE) == 0: _CLASS_ZONE_INSTANCE.append(ZoneInfoFile(getzoneinfofile_stream())) return _CLASS_ZONE_INSTANCE[0].metadata - - diff --git a/libs/dateutil/zoneinfo/dateutil-zoneinfo.tar.gz b/libs/dateutil/zoneinfo/dateutil-zoneinfo.tar.gz index 1d15597b4630ade143d8408477858d1ba18dc707..524c48e12db7dfe159f282e4664f71ce249e1970 100644 GIT binary patch literal 174394 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 z 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 z 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

oF5%wc?0lVIdiCvTMwKQ}m1M4pzSU2C#ISwFq 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
oHJaZa%&cRgbKqQScY`o5It4F}rqGD>-kDeG zYp~(1B-G_BGy5fGB4?rVBF7iT0);I+%(%Q7ZSx_9V&>ENu8yEjQ5?{=AZ2I}>Z+vS z>WJhy1xQp+`+1Qi4*ovkZ>A?FrqRUf8*s|{4~&T4(rJu{%LUn>fQek9pn#J$w>Dto zBK8Q~q_2N;0vtcP1BDGHF&?k*L4}eT5q_FapqPRAHPmIPAmh??rFvWVL32;r$S6#j zKPm^9*|b1iKFn=6S(&S#9f)(0Yztw8oh+RM0OhTIZ9s$f%b*}Mcf;`s;x&v1iW#90 zMqQrcS6?25o3;+d!Hvg3h}?OB4+n7XtgcaSo5#x=@XCpk6-j*HJtMG`{wOFwe~c+8 zU`7;Jzh(?F?7x5_eXyXwO2f?Y3Gmum8j6YAz_iC51O*U6+iVQIg8~k+m;zv?SMLIV zoQa9cs~oj}lVhRm&l{tR>Ow;d(%4_N6t~vKQPGpjD-=lz+xNobW@ygtJXh4dgmwUT zK_wUJ1Qa^8-zstkDPw@h`J^1B(^2vA8N#iew3_LPLKh}^3 zG$(Jo=9qu3a22YTmtgd&8Y5^5n6;=Mt$Y{yudf`fsSSXmRtuw1Wq>C#tY#4QEbs9& z?8oCRx{+6+2gIBcpuq<-@O;w&6)DFERqLz0|LVO-Tda9#ub#8QI44IIv`R2X@#xGc zb(;WT^#5ayMcO*R`ILyHFto&`2-)+4EngTf+y$>M?*H4d-Fp1JEyT2z#EYBsq^Aod6Typ3}}VeBgA zNd!%NNr;E81_e#<8chdZxV3rKnVEGhx#KEi_kAvV{Jmr3<*tF&@`Lve^nW%VP`Dhn z${<8y6OZgH`6x`?Zhg?4BeFGS6Kya}_*2`_U6^orCyi9ThneX_TmTzO76iTz#=R+& z4NeUqa$1hzwlG-O`89jbkn;L+;}?~H4x>Qk}Ii<HW&~oE!54fyg9WyiRO|X5&PXa$URgK|d|Zp2d`>p`O_!7Or0nA? zKVA~umPKXhu#%E%G<{j+aP$uice%>+*?w6Ot7B2`X$|rC+}hoa*Qcg7%IV!RJ!aBc zUYh$}UtlG_16#GreA2r7^PL*u!{t=-HjXQ=lj?Lvc^=HWSL+?`S(aahmPXem)%_)| zICclM#aC+%|NhEJ8gjQSalz{UGxQ^??7pdM^6))F?l|8*9bdIas@+sC*StPFzHh3P zY?seBf~%`s&;%ZcPtLdk;#yc$t%MMY3`qsQat@d)8?8EyGmP0Dk2dOd9A_D`K0df< zPvU%SG*-AH1$0{FzPel_s zM=*CM_oyh8HSqB!3I{%XRsabvG_-*$=$TnTdEhq8Bvfz49u44DVK%IkHxAjG{Y~e& zPMjlV|IC$0U)MVtp}$RXulp4+_W;>XHc}fQ-cf~Xp?fyvGmGXI^DvWlq3>Aj`o-og zyEQOhDCZwDRIl$kxj3#m_&Qp~9JmKO3(EU|k}scr$o$7R)Cga>EkD48pCx_`NK%=A+##@C;pnN^|FASjiva5nz%%QyQZ=XxV%Sn;ix07!00t zHV^j#((W&&Pt{9B{r$Qvjr=2cT`vf;E6>F-Ev%!JFY=CpGlZ&63LR|KmICj^5I#-@(Gc3?EwB}jt1OyS z-etuRW={5w^y1pb&BYY5G;_9y?ij=VdB3I$7p*lHTQklIpjPP3@-i~??Hwqz9XaA) z2=|SWmUidd;j6rHeVL(G|7ABC0>AA?C0<#MZe==;{z|VQ9j(WJi83%XtQE3?G=@On*|Oc$e&l|rH6Ak|B9ay%sL#>>b_>V;k{p6??79U z%9k*;@~}iq=TT{|Ud#R=OUMWuT6M#+$HcMv-ui zhqFH^_Gl2B4yTa8^UlD+L8Qv!8a3U;H)e@|8VjX9!PwNFWJ3JC^>*PRZFgxT;X(pl zGZX1%b3uh4=6ouxFVpeTFYng~Ge1zDiY^JT zWV^zRTJy{`wFBQl&0jh;pTb1wYyG|l%K=6QObNNGK9J%qR2+qv$)75lNhLP@6>Vs6 zpfnmFpTvJ6-uruVPAH9LkZvW^`IqJMWXWn$eMvV7V>)rkZ(=!*a9iyk+T(IG*y=2`(N1!x6EQ4cu$x>%5Z z4Ms#UusU%wo8K0?4;QSkkt6NK{RTgb?Z z0E{Z3a!5@8s+8FhB5u_Im*R>R)DCRx-m2aXoW=I61F%yBHX?dLOo>6QUa+aBZ2PKX zOTAPx&A)s8V3&UT<1Pg%eSYI|rY;vhPNMU?Oa9#H$1qKsXF@XqY46=NV7!3-to|0n zz?!2sOj%_Rb4vBsdi_Jv-oyK1>Bo!xO%Snyi7DrYMd-7~{Jk1R;s0l3_rUP4NYmra znU*&nOpiWUb>&6yFqb|sTs53}uy<h8tZ7MOmI@*w>aY7JLXE}r#k`_T~ zrVK?p?Z1^JGpf$Z_OhW5TFjcbdhI=kI){3dL@*`6(?r_d3h+PUE9~YCm;0i_IM)f$-Slm8IJ>at-4B)JCgp z7Dcn_J!9q00^#3&^ivObCGmgix;wwzL%u>L8crS2M=h}i&ikGbovSGp`oi)~R&6}L zM~6z!nEkUJeu41Cnt1-z>UjRs(D%`~g^w-oNINoF*d~A-dtw^7I#BGsYZCvdgD8#M z#De9WVK-2|Lf;SNo-rZJLA$mMe7pmZxN-UKG$#j)|^*f9WHxjRdchkU$@l$Hhq)aJ_V|xan$?P$QVqy;=AlOYpo+h1A?gzY^{Jr57bg(Xn=5-h9nv=o%qF<0L*JU2rsRCX&4 zKAFw6>HHPh*A1`q)dv?~iGcZd&Zh+3x{p#jvd=H-|Nbfzn+#JfC}GOe5#B!5T`9(% zqI^1MKWpXpyDgta!Au(W%eu*Ek; z`iJO`!h#ls617ML1Q)`m8wF%ulS2(^dX+u||**UkxoztcHRVvK1nRvgH8`)^HP^ztZ?bneq2XN^~<3CsO~u z&M8bsX?%t&f0Hr)PfN5({wh5e=ao+Kr+@+#m8UuJ{IQ=Xlq1FG!@@rdSukA(C$t6$ZAO7d-QFG0IJiqy-Z`VJsbn`9X5z_!o$cSnE#9Q-pL{qnY ziv0Jt&%6F#wu!`tKjxcvZF16-u{;`PdA)4*1;`?@)Rn*~PJUUnry;*=50%Y+RR5Z1 m-$#4a)}#Oa^vdq}C+?{-(M1S9@p8ZNUwihHxL5`caRvZD>`q(& literal 139671 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

aF ziyQ>dF$V!TPzqxV0#8wc03|4E$+rPxFjB+|?Ew%%e1NclQVj7yd{tM1*=}OfA2nGL zUiB6{Tsc1%@lOH(Eqo0OJYnq@{Wm7UQGmFai*fKh4L82Ievz z*H$hIw)@>qkm&+`2)>yBhS?N)cwsxTa>hCoXAW!v3nJmpK27v<4j;rj9x}rXqg@@n zV6Cf$5*j9qZ($;2#5ci&%P<7%bBHV^CZBu0r4y@J{q2yMHAKU}*o3ihvEImgBb1@h zxZ>RL-Omv&m8JehaBU7x^;5T84DC?;(fdT_+`l1U1UC!qLRY5@oL@K6ZdN(&eRg5* zJ=zerJsL|6r&fya1ntNG4es`)+vAl{M`^c*e>2BK`<=jCg@;jx?<{wvoTF!lbyZc< zkGYBScHFDxRqN&C`}arN_tYgR9Rhp9TpSB!*TO0EP?3O)Nyrs~w?sFw<$AD7M*OE? zO6HqAH2$nnd=jDrxDmwJ;O;rBev>)m3j(M}EhN--(j#hKakSjyVI$K9F-6Y-$XQ^JUhOWRF*{5r|MIEelrIuZvMA zZ3|YgVTR#!ge%x<}1fM!&Tf=vY|n?U;&2rB+|wqD1GL0;e;*-H9B}jtPFMVgbLQa=t@=LiO~y zl7HF1L_3YGoF5uV^>aUq6$8>T$DWPt!~&|OU%MCnRMlfey%rxwE3UE(S#&TBX`L{~ zo$%`nM~SnNa%$@jpUGqm%i*ixsIpe9cvo+2BKU%<-uS(mU2T5K^=2>$;><@#nmQ? zyj2Qc?~yy}0lB=w zer}v~cuY8Skd>f*$9qZpuKab6Rb%NJ%X0DB!V!}c*%9G+ZnAjAs`RuP3w(zJw_S%M zW22iOKBrN_gL+Tnq?e78Ma$7J?<3OfY=m}GX0DE09aC|xHstR&k-=nPJOToKY{PE# zCA3XBn5mq~R7pFEL<65Or+T^d`}tgzQhDk>3r(T$=uh$mDW&p*^+n2Hz4w3hoB!1d z{a2&{7B&A@bopPA=zm43V9^6u+P8AG7U|`%Nybec;+$_wpkau9K=?x+k2Ca(EouF_!pAh(LPpN8FPIbT z=yQ@Sq1LG((R4=LydzyR37i`lJL_&A)9f^{D}=t4C7KKawf z%Sxz;sYt$8w2YBc0c=$BzeY7$>hM9rw4yHuMC^irM;a}BB-nWC-N~Fq>mcVaF=?c5 z2x?j}Ut|-1eQ!Oh@~R>`(iEi4nJl8$b3sTe`4ZVATBO)}!)o;URP}%`2tgsu zxGxx40AfX_H6?E0hS5J_@_TWL{nkM{~&llN=Q;%W?qFtmwG!=+G z2GMyDAX*$G#3=Z3FceJZd}&1Bhh8`Q*Kn;s|CO1JPaBRG;T2RAt)z(A3Wp*oF(p%N z`I$@Fd9V=x6PGuEVHOQLEfkkfJ?OA&*@kv-?nH&b87-K_Pyq>G+cDmSqw@-{dmqix zN+EiwvLQ65X@?rNS=k6pRlM#%#%PQWCPUm9WbxAfJCAGcN@}h*@8O_|V*yKeSsbou z{&AL{um_?umR+{DC}QVQm|>r@ z^iRV{9R^C6KAHLb?6nI2NJ8{nq#z3WIqXWUS3#ro{Sr~dmnZfH;i%ptM(#61wJnmW zL}7G|l5OWJMG|&)-RkPkUEMXP*otKA>?SxfpcCiMA&4@(wcy!PNbr)0Yz%3GtvnmQ zg<~%ymxYO9&ks)a8y|CM0FdN|ohiX}7cYYcK96`Tc#F+Ec*}L@?$Mes`d4qe+jjZL z8wkSvZB-W%Cp%1pYoBzr((wG|f55C{-?Bl~b4AmQV@0PK4JW;}rZnZ;!k7`>b$<0G zr3gAT96$`EUvF+E@wk}k#dAflizEs}l|#TDmuTE0jL2JZkJJoA9RO`aQYsX7K}s2&^YtZ`spEXJHqdOFX9=%t&I+DQMgY5K zlmF(jkZih2(a@&b|7Nw&e(?1IoJT%|6ngru11I3SMggtAVF5tD?gV}82ey*t*b!II zg8!nUK4Z*|104NNYrH}$>2yDf1drisTmUVh%N@8O62U*OAIKR5^hdC1&h#oNzim5q zorj5;Wa4{QKrXlfU_}qly5b#U&i7VSP}d18%c+*;;oTiTy(Ae&wN@&K^YZWgjpw9_UPE1ulsZkp%S>Wz=W53iK% zOKSB~@-{KLXzYEc0;Uoz8+yIIJjUP8k^#jpHhE(bek6vTD-rjo+`f{wE}gea3L?l> zF+VC=>3v6;8O5c715W$dxWe(v-pIx{ch^O?y=hQAXWZUtdbt26q~oCME%iai)EAQd zHcTO@(eWB(M^sW}m&p9G?B(6KpybC@lGftO&~Kk3S01;&t!O@e6O}~&CTj61s{L0) zbbHn;p|6R*N=y}n>_=JKKW8bKp@!KcPyShQAH#&{h)Xas1f{-7bJD$N#SFUtKsEgp zl@2efFVq^4!U&P4+N@Vn^4Z1ZK4@g!{7Z>xe)(f#vpR{oZ)0SuH`yozliY@`m@}4v zA+LZ%>6*Cs1}%j-9Lv8uFd!V^5(s-jea=!+oe1hK~>Qw+1sUHdZJzIURbyx6OFzOmA zHXNJO?Pdx>cTo~sp?eWq z^{K*|-YcmcJ?ZAH??mAe8@uyo~rCWH$fpr;t=r8m!v8 zm&8_AP>IC=TLYYIdppG%Qx&jcvi|w9Hdf^?!0UI+|NA*p{4TBGr8r+&Sq9({!Y)G~OznLsP#&LJvQg@Vi3)(NnGA+DCTMa1s%KE{6+w3HCGBwt9wr{v^eKdU6~f6u$BK&< z)H(q-bUi!*NcC51Mn&+uNJp5n@jSoT zYNG$}bB^x}e#hr~R81!=Y~^|qA|SEFW?2E=lO`3`_lN&MlojpfYtDJBQlA0a^X@fg z*ez*+9ds;POCNOV_5|9ejs zkAQY!&X8v|*Y~j#x%K5*2?Xzz`BEQ-sU!y?aW%r5X+l#(>~$nk1T=P3axa9xogsm^ zFkGTeMC?3d(m-_fKz!{nlt32USFfbilM!CDBcjBVG6tfMN<mog&A2p3{} z{1;4!_@){`#}u);6UinBb-f@pkW8-vTa)N*W}vGrqJ?)VxDkkL$H}p;tr5x9Qzf1$ zoc=N(?eNcx*ezCR>ue!-%AnI8z?+0lsR{1ri!uh!neTZ%3*00qF@Qy!0wcp8Rg@K! zJQAP4wM|oZfZ8B?@>Kjes;iwaf5t@?${HZAV%=>N+!Zq}< zy#7u?Ai(65yu+?N$-I6Tz4!3+Q|}=pLxw~3G8B9g>e;$7&z4VxOI;dg#_8vU5)SU< z*ypAEYuF-nAUX=uYbnHfgM54ct4pN1I@xw1p4~b#=anLb_Lck#?JGp^BCY!zY5z-K zb1c*DYp`8FclAM}(5u;Qz??^e5WN_ej)$#&{=o$CdCe39Yjzh(yakINR!~wM`$)C* z`f-9l;z_*z$;9vuO7tMUm)8oYUFAw5fhE~@P{qsjx|q*}nx_yon~n$74|I#q%q8Gg zez-{@xJwOYqm|=f$@N>OO?5nB1eTR|?*RUD3C!6uK07Cz^Wp%*A6iKVtb^TGnbO?4JZdj>czb^y{+H`&EwP3 zI}6!1`e}qk)BeqWo(&!7i;V4QA0CQ#k{|0ihr!vR+Oz1eD_pnjAslMBj>SJ9;M77j zEvpfnWjPpbJxRi$dXBLdJpWFMc=sW_H7qhTu2@nxp8CZ+HR9R1^0ATOGkmNYmD6R3 z9g+P@Q+S_>{mtjDsi>Fw*L6Coq+%5_NqzD4kGcD~Aw+)-L|st~%9+Ol>wUs{DEx6z z?XQ;vN8dT@a4%JZX~+qgyBvC?@oI62yhh*Q?7Uq{=(T7tYl~S37a#!(61kTWN=+N+ z4Ei0Ul7kn*`+2M=DOl4T&%p|{xBxWt!UeR=&*_dV17;0fS#S{-!AwUlLuAKD3-g8# z1-V@xK>tdc1vHMO^<;;hBn+4MRJ*43K9!G(s=k){>AjnKkgmtSe_J1*U*Wax54lx4 zGNOZS%ILNbLyJG~D@OkNDsi}WLk{v6(<${0*B?vkXB%AiCKp7f6IXRU2~vk2ik3$t zwi21e+J_fK+7&lyv*YQ1gjTBI6({{U!I1Of6|BxG!j;qheP*`twt*6u_BpwYxuJ&Z zzjrJo+hmN{ZOEFTj-(fKQau7Sf7L~sBQlGM05b@@`>W3?(mkf$IZ4Yv%>4+A2|EZ- zd%s_>M;$2sn);fECKk`Gk2w}kOi1pX_>IJUuU1Xn0R1+^x1EPC5+e}BdK(gQLzn+B z0l}WRhelrj?>hnP>?^Ly^&J`Pt$9%DHh^zS;qWLt3*=OI;`SgsV3AXf)f%XZN_6ZrZk;?jcY673K-<4X}2Gx$!c zT!HW7uv|NWX)j=^7Z7G zUm0zl*BSseHc{_$wY`04@UHm&&4C^ZlEh$lM92feioO0^|Vi5uLFfcKV7j~VCL)lB|1)v0V84xa+F=V{ht zR{ps~SMw@Wo!ap$t{4fZ%=_<9dz__cK6mKnUOhE>?&ACHm-y9!xg??fG2-`DsBi*t zIYE(q9>B5JYR4-lZup}1NQ-L?7BvTke_8@!1HO$k@}8<$cdCMHk668%4{U1dNUPp}VGthg3waM$7*T#6TmVx_niDNb;A zC@uwx6)(lzX>lp;THGCSFa5ty@B8~OH^1GP*}Ei{+r8bH*;xlN!KibuL!DJEx~7^U z9I;4bY?DKQ?G}72nm-QJ)L*XxC8ikhjXwvwuQ+%FVGoKM5 zrcP=Oj9hSMJcVTP%;kyg0fHuiC+2AO#rd0(Lf9M!H$C%8`g*#CAr229()Tq7FpRnF z)cs9rZfo#)z?pFw==z%Q)%O})2~3wgtPNfi_zt-kpPqz88#flS*$|Q&nPFhIQYr4UUZ0fQ^5H-^V0vsZ2^xpugOn8b^=$SP{{U=Q|16A$8q+V(+T|(SD%Ep+JH~ zU#F`N$4k5C*Dwa;D~1QEi#G|nTwaOf2VsNN6SR8EuTMTN*~&H@&re5a=C9vqR!XIU z{xE%frLL5U_=n}AkUG2q84mQn>SW}9)u~mj{H>*4u2-USt)pHp!(6J}LpZ}STc*uN zG{cfwrj0|UX?mX1?HM-D zO=33588&mR8McC`%p$PC4?2yy7`T#~x<=hP`iB}Bjk=%k;!n8!3?7`|oD0#GSPg+s z$>CwV8^2f}md$*I%?I9|XAmiy%?#TTye&XmqmE5dDOJ!mHIl_PwGn=%Xt;(Ai*16H z=M06~(bTP@3Tz;H^e1)$;v*!6HfA5247gvv1CwS#fa+u5^OrlYM#XvF!svQ#e6r}t z{d~_>ZoI7M*tKtRiL2Oj36oou)$Inx*maDil;f3Fk1f_#uWA?*d-dQ&CI3A${AV9) zG`3w|vNydaVbs}M8hcL0QF{uHCp9s)A@K@wg6L@r&#d*yH$c9*UFi+U|1m6=F}`ZsI$iZ>3-jnnH1XWzy?P5n|Pg|5(G>e~kDX&UjeYwz&@sEp?J)!!_34 z)FrWr$2`nteEDSkORBFF5;Ir|E-;;m-{C%ZWG21Nx+J~E5|#5y9V|7;G0>~vJxE50 z*63^t(g2X-HB7X*4xBc*55VuZ4mjbhR{txja~+IJDG~F_Jx^lDJ5a(=#t8R5;FDy z6J1^K&I`qM4;eDlVoE?-2(JDWj;e-sNw5CE zhP5)z5^pY-PV{Z#bb@k3Hl4&0!d;<0TpI2EYGGV?*-VJTCUWevB@J73PmFpveRxHG zy?imrzY2$s>;yfD^)Y)1^>m@McHF;)3{t1osM)+eaxShi*Ld8hV&OVyg4Kd?ypy4& zL+C_@FyLoQw@qQT=D(~kw_PB70O^7Ea8fB6=$``M4j-y2s_x1te*>S!6P#da)C!0H zhB|Y9Qi0LSGSxlkD5T@gWaXd~TL-jnx9z-ZEZ`<7x*r+>SXf zyzNu1B4`Q2a|gEZ;G!d1yQ^`?&b(Q|@kq`B|L21C3Ct+g2=rpVdCpAiEgH-b;go1q zMk>r$_#yh=0W#yJEYGf_3s-f|9J7d(j_(clqT8tq>E(h zbla2^8V?Uq!OPeYf)hHN#PMG3Fu;igPEv3(9!S*)3Z_cV9)Z&URkDmHa=0XekcJGy zLNJ_b$uLCrzjmhin6x7EF-b-CW0EAzn_m}j`VJ?Cm%H6nQA2c}=7j~x-uzN}`Q{fE zygC@p&Edpn9f?Gf>>Dr1pg72)X(huTggwXn>FsXW`P=OMBfs&xH4eKob#w0za>WF9 zV9FaW0RR)87^vkfFld;17_|=h|5=zQYr3HN-*-(`MqTuz`|sDPQGZy#xaF-*c;$cv z@v|x?z8ys__3SdNSkG?D7O{*0v1A%Or9;w`H(0_NF>u@%Sy%*r{w00FLPG5pa&@Kl zQ?=(M7PNV6Z%OH*O|jWE4DE>V*QPV%m!5vs@Kw5Hq_>_Amc0-52@E?om6N?c{7r8B zqhimsHQ7&j!yA%`4ZoPv_9_I+C3uF4;EEf0^A*?Gq{`N(!2^zlBZeLF(B0-Pzpfxm z4W(sxvK)LpXU3ZKcc$HyT|sv6oK=XJ9Pl(&F=Og9Rbf0SJ!L3qy|3PmYhYH%TQTC@ z1_KPvk~QmLrrku{K_c&nW!@+KpxR7Ld2ZT*={u`EV8WMq^>IIG{tk*Lr@=HBrNVr} zJrq>TGZeIHnt(zb7KBNQDHVuQ6~o4qB1t@x(9DVHjpfl0kfq5l^L|Bq{BwMcc;BZwa`~z6egBNT{si}V;}E;1Le} zMQr+cHBv!m9z_d9Fo2PqK*8qTVUwX30Cm2%H=r3)h938fVtorffzHLG@P|%s^2pJt87je1TxgVUn_pSLDJsxCn?MK{UGCD{nt8A3wH@YXCzdF<3 z-g&FvfyPV<8n>d{XPKhgXzhU+p@W-Bn}3I%?=o0kVoGiA~pxVF{OQV z*rPLA9=fv2b`@2M{U2;{%JR@B#qBKJZuN)xZtg|RduBMV#Klc+-k;W0^~mO3ZxohP zb<#cBpOFi>_tsu&3cxNC$BW%jiydLG`5E@QQB7BY>%12CtC+H-)Z?6%<8T^DxKGzLDP#10o|kQ4EIQ|G>Tw}29cvzzxDuknA?cdma`|Qg$5*WR^WcfB4E`nGsMt&%FG*# zTANI(-E15~W%V~KMTy8Kf*|s^y%q{2Yl)5R!qN+%r~!$XB6xQpKEWe;5HbI~1E8^W z;ngE2Ip^LZ!*hhdXYJJ7I)nn5vYlSTUB0z8d@mVh5I@41^}{Ww#ks@F-1fiJC6g3X zfp2O+R=lVRR9LQo@L2N+Ircz-OiVi5@a8Tgcoq@LUBuBD#BDPU=NI)LzZv|y-fUBb zN>9X>A5?1u>OtXxx?(v70sRG9?+`d_c2Xo5mNf%ZY|-7OP$brqQ~l9Hf>20{n&lm^ zTli6phWW&~T_@`A?yS!zi>yl5P5@;-?9W;+)+HY}>8RG87=!tN?y`sK>>ZT&>(oOl zNkC<=gu=o5E(F3~k~?b88cEA4Af%b7iH_8CrtY@&{rF+{W4^RyMM z^~QS8ee;d-!nT%;c@FojT+Tl#hpFXRB-n%vBmPRGIpyCC9Lp5p&hg<^Nkm(?Z6#&!gS2=m~Acd)aV(g^e>3>-cw2Lul<3SgE0&f2c5H*d75u1-Jg1quAu>|@Uf0-0(_FB z3=o3{lT_5af1G-7`o9YWo-?p|qVLaxmD>fatrpA-9g(V7Fgl`hO z@A^UeOX<=_iB?q@^5pH+ub-t;;^6C5FoLV*hw*dD-`9)bA%}-^-@Ux-Ldjh%%LK-U z4k`s^(MHC`wHhT)(TjFF>6#kJ=~HqOD!qBVblEj|6wa>vjnBRttDHTE>oNJ%M{T!{ z>auxUIsLMlq@TQvHh*_NhZ99)=|^8X!1tm5=>A<%#BJa2#OHaIScp0Av+i za}4MrB;r3Zl>R#mfruv%*r<@}k?Fe--vyxj{F+MjLQewA7TqNPPO1iRB;p*x z;wvU2XcrV4<&|E&hSLqH0x*KZKErum3hn#+P4+nptZsc)BvkuFbb&k_5y)C1d zCFu<#u=i;q)v1n$e{bEF!c=D+;=zSWlMSIZR(gER3fm zZvO_NnBJTPA#lF7INvuKyBLWtC>qvf8c(en5|3k~b|z%5*$bKvw-Ii|u&c$@GbwcgsRR=?Y!W0Cb7=nooS zO*Al>-8J>&#_gl9dZM&%R20dku2$Rof!8W$oNs#~<0o^KH?2Zxw)$?F6ldi!-Y6t} zN?~4?FdKSf8Y~yYhyVVku>uxvG;-$RH%{H}fSa1Ci{9fJJetCKV&w@!B_je%W<0HZ ze&1@{T-@#5{h%m75(nKYws9H!RNU^g{QmxMN7F}#@yrD0Et_+36q^cO_IIRB{x^Xo ze^7o-BQ$!Y-Sq5<$rrUgriPdFi^4M)ID+4aKT;#N>_O_3zw~Xn zIum$&@d**+H;K5cZ4axN&1w7KBl`D_GOKie@cWh7p<=b6*e_$0BXB@btfD~e&vWeh zb+Z|K&O&VcUt8^O|jPKWTILaIaHyk!gE@^<~mqp-&V=r z@x`j!KS3O13hLm|2$zsxDTf>AHCHiq3{Np*iSP%B$OZs&9ID8Lfh`Q%34|}2WuHEg zv`a4iyD6+%gFj-Mw*rSJmupw&JwX<+$`c9ZnLY}QOdeTRYn=_4x1yMiW7azphU9m> z%kP$fKQ1CZKPL@tTFJn*HRW5LWtCIsYw~q#Cf3wBf=ltw_q{s@`;O3y&bIqtXNJ-a z7ye7e>$xbCv(9VLzNgP#Sz1CIA(IcMZ|6l-k5UG8yqE6H8?*jKcZRP9o3X%L084~J zf%pa!I}Xh*|4K8Gxfza`4bMLEPaiF5lO3`sJQ*sv@J^92Uq7ga@8X^IOJDL&0N7_A zTAvO2vOaar8rXK1JKWj@dT1(%Qm}c#y>8rU){jlQx4MJof>Fj%F;{VjaY1ZfLG@{k z`ip&1q?Iblb_CNutIFe&*lH(WAt&)a#(;5(%=Ck=H`S;Yz}K(7&n971?f?0twZl8i zHjCye#rsY@-S>Hytv~$dZ0|dv*g74SY33_?TYA@B;r_n`P8l#EI>ueVU6Fw9%@e32 zdkv`bpg&X5UU(!jP#7joDEi4}#;%UBV0a^8_;z0^$(ahL`zB2BQgwiaPXz*p?Qq{!|Qcr*pX2={BQYElI6zOt(jY}0pc`=HZ%dEI88 z`tEV;8Gj)6yJ0{7HBbUF7O;g*=VvE}8=c$UyvK;l`CBOf%6__9YW0dIB76l6+ zdBqs?PECC~@~dkJm%-C7ebOf4QAJ34H|jZiR9|Q5J4SIid7p?pFQ%Kc+A-=o3hkr5Q&9IU z6Fv}p3IZb|L%u2a;yJFzJ0s;GBX@r5)$S8O=!PJ4%kO{W-z?EGBQTK-eXi0`pAYIG zCj3kl%disO#~Oq8Cj_Q8rN$$HE*K2H>SWO|>V}8 zl_SM;$G@+BY^0xTD))TCHw_x(XL7`(VC7pp&yO}-b}gut?+vn1m1zp2*&r8QloUjj z7g;StTMi}}8jQ9x~ORAX!j@;rDmQpr)}x zG|+}isyrVTYXz-~AC z0$JA*_LlMd68G9-lZohzl~Rm`LMic#O}3}rjW_a)UACwBALo$mX?G)xJmZw@>Hb&d zqH^Y@I^(8telr@2&WVI5fP@H{o@DHqbi0Z?v%Mm%PwG&HyxpC}>1yLslKt?3a39ztZ{$-UBTnX%hl0>fxhVblt% zkU@$X%kVsRwCg{x-;eC!$SA$T-ec$pL~~K+$2n$XnT`T?Ytm#hL)X61Z#d6uuHk0!0e%i7dRoEok>* zGI~+UHmkl*qxobKuV~U2>%_MojZKf&IPl9rN`+HW2lL0n>-pPT&e7>xcMH1`*xbQO zZxa>ZyUepy^)-Nj7zDe`gE?*cb~!X&&esE{KRSD0ov0UuN5=QRBc?x_ovuB4t{NzO zY2^8K?JpH9-78}TTR!QCdr5Jv)B36i`%|R(&lcurWtm@ZLQ7@LEKRX{EX;7MyVQFPfV?YW~)Sm8Cj@RGJETI9tEyv5lWLn9(|R|X+r@%8gv{CBeetrsv#dYjG5do>V!NJMoJ@kcq9z;n2Y>$M+` zt+q4Q(O)o*&YivCF#h$n?(NSXzp58^!tVL02g_^4%p@z2L)TZ7WSd}tt9}}4EN@7X zMY5|lYQ=SA9vQP*U_|l5Yl^C^Wics+CKg3ecYMy`>o*aXf<3MZd(Ku8y*^ew<=Q$~ zf;c(%augAkPJNMFK6Vn}AGKfm*d?64dwsg{YUDufAIkKvAk=`ygV19E$ z7QB!Z+I@7){GzT={?*88!a^4Uw-5QK=?#BM_Yr$)_faLe>k>8P=GXN8&|2PT+PJrM zf}0hBKV4;_^_R_v>zd~3)BE-HM>cb3KM*%2^|Mv|6O?}*+1y!$ z;ReV*Ffp_I9=jdIuMw#RTJ!-{i+7U1qbAU*lq)u7{vDX3qq`jEJL=KaLfY)_wRqR^ z!hQqNY*>YGh__w(L{?)shp_DT{BjoI$^O+Wg6J96EQ0U(r*YwcW`+tR!O})u$}Xf{ zL#4KhjL1hlT`UQ`uCskjGHy0sr#tNiX6PFxZvT0fEzp}De0mFrM;sd(In8@;i z?F~J=zuaGz;lZ%P(}2+g-fHxt!ewHWRx72(w`pX;VCSVn(GF&#hX;|ajJ*qZk5gjW z9nJAx8^aj1=E(-4ZPTnoF%3!ftpinfy8{7Bq6aX%xXaQua<_*42bP6$NLsC6MhwO(_L6xXPp>gj4IC%4EtxrV7pq)& z&b`f(d{q*2`ymwV8hFY8nmHHM++I)bKX5ks3pwr$%pMJD3TEk^5bA_iW?YM##c^;F z+7$X@?R{^bl@|~0>(r;aVc*#5Ad~%I%&~UsKosfTdk@P3Z=*idF+!XpgTPns&5e25 zQ6QcgygYR09^&HVc#-D7F2NRO!rir+c-gcDeq#p}_aPR3^-Y3aeP&;oJDptnSV^Ux zf=&FCu3uX#mVs90BwF)NSP;e+q#Ywqv2QQmx^Nez?^f-RyEc$}k1h-VfAFlC*pENA zR>><7#rUilt81%#C>^E~wRj#}DYa){kh0+5CTMXEO@CYTk&YY8a)@ZT@s^Z^TQvu1A#oJ3F&up;mhAbwm)b1)W?p^)W<3<;bg8p);?C4Z8-Eq^h{p? zxxWqMzTI7BuzCU)j&m?g+%v+r-mPUhedv?|sv%pQk551+cjFSUQcrxhDefpnLcN;6 zb$(MH09IwIMmXvKTI+63k!&>Lb}`6VuZmzr2hS};$rW3RXLDdB!!M!yt#T|6qq*)Wht_4_5iAD0FSSKJL$*F7lFr-;or+hJZY{KTxf&*GMdi1&#pW4i%d?xBsF!A*z*P&S zmq$qNndjQp4DF-MoIbIG=f@h7a-4s*c+an9>i5@+^L>dGyvmgRyVdUD#^8E5%5TRR zGh>l%Y4_J&s#g9L(jNB>mzKQWDi_q_8r@Ms#4M6kYX%+DQF0FzZ|!U6W0ln22);*t@KK+|-*_rV=@J?Ixo+k}tbIFYYGL}MCcNXA9cOx~CW7M_cobz$ z?Gju7f7xocW@P)sO(Bx^dvpHY4m+PmM}0CV>LxO(L)S(lVfHoyrpA=9+xVQvS_%|s z4Sj=W$Z{IGxkkY~=#+RYAtwGv^g{iy+`?rFrYQZv*Aem7>(S0V<6-|{WVPc7yXkc; zi#E}sNtIUOlL`GcP(KUXa?e&<0zShyb%z{JfJF3Xi2pHw?jpWyl1%5>9QuvFSI%nr z0W$kZd^uiPY|x?QBp}`}5#uxg=VdV&AtZ%vz?qOX8RJde=VwT-`eyYtgy^jEEp_;v z3HdhN8Kk|h(2WPv9bp>wTMy+3;PXyV8&|RhE^g+CiT^g)13#fC=O*j~Hvo3mUFYmu zHcOd@m44_i{<|h^-qmEBZ|7_G`E;;ejTmKyK49@0*a&S#k?rr;M=X_l(q*+60(#wC z-Mrh^feuH2*Kd|$WjSCH`bH0+hilJ3y;b+}o*_jTMIKKi2c7qPusL_}_huAv`%m#5 z=bH0W7h_YDRaZIU(}CaU(!25w**rcC$75T*4=wo}FE%inmFExOvV@W@%7aYMd8VpN zbf=$3SxSjB|VaYMYb4{_*wv@x%3t^Qx)(ZZEK~I&&I!yZ&u$TJu z)-spjPt|-gmTcN#u5qwbTmTD#U8Nqq>BwJ=vlNZkX^#@>yI0OHAt<>1*i=t+5_kFm zXVcbXQvR>0o@6BMG6K#t5jOS^d(>Wp2qXriv(KT`P;gMZVaXLP=q~>&Xef~i!Bz~0 z!_GXR!pS`Q*blT{b-pXce*loQ0IQg0k>|BD7T?8LQ;*xhak^DCY~;P$aKMU?c>8aZ z`+LRX$4)jK;=_8FbQ$r}T0G2T{5~M7$7}iJ6(BDwJyNTiWdA8o{No`x+tdx;(d?DT zWpD#9RG<0D0n!+o#aJ8Ld6ViOwF^4#tIn-ciE^sWdAgBqV9p8pshO8&YkrtLd63FB zanRvqk|pF9#eccNzIc>R=5PbFTMh3oYGl>fC##rn>XnZh+Gd>}dn~pQwzC5YluILj z2@Si;tsFu}UF(;laO{_hHVmY@b+R@*)f_%O5HxI3#@5%FH(Fakjwi?r#j5y-DW}Jt z#neQcro5J(#;1Q`J}lYev@aO8){K5$>K*6*(?iylAgtZOGs4=t?%mddKy>n>ySVA` zSTc4cK5(SK!lgMylyxXLDx3T|i1kzLm#XMv#ms9Dq@Mop+ph?qd^}J-HB-_6DC7k~ zh#W#lSiS-oDlQosE*Uy583ryHCN9}aTrw-6 zAz?)dWF)v`=`=PUC9JI_tnDSNoh7W@B&`f^Z}50THpE+;I8 zg`oaeqAI@6O^1WTp8!BDGRJyqUm=epR#r_44J4(yb;y9oMe@YZ+^$T$T2{d z>=g?PY4_a4L!Q^#GyNcY+@nA)M)U9NY+{PEhYu-y#|zdz57KKj`*W%Kaw9e$=Tw1d z7}ZiRrO=EcxX5P6(#>y9yye;JvvPav4fmYG(fn!Z{GeZZ4kaL2<=4{zpV*rtu1jt( zI8M$RF)K;eSu7!DsIzyhH4>o7%;C3>5v%i{ z->|Wv*Vp>86!=z1!LRIO?bVt7a8s9>m(DuL_;kp}(kQ2aP|;s$J7vSF20oQcPoj1d zk$;ymN018c?vyMM!pa9hMp#+`+*MiA$9a$Z!z$54_|Zr+a~bCMS8R{DCyU zl;U3z5lv1ikrZQ8AhH$|)IH&wOo|1^8~*?f93V`>0UjJs{{sZ0Jdu(JK^l{y$)vVGZ#1GI1;`3Au((d;Z<#-$f> zt$Y|vALw1oOpSS5Nl0@gMh?`sWcQSvZHGT~2HithLY4IZ&GO@F&as(&EPs8VB*AMG z2!|41nV4P!yvWZb@bPd^Tn0TBbxL{+?%Uk$Pr@DiIihmfg*c`8R$z6`Oq3o0?k|tu zxm-vYk@H&50qRhCAz{C6dq02w&km0e<(t+>KoNuKt`q#+Ss?4}2EW9qVhfva}l42sVAJN65mGFF9CxEtM=!1#>HDEXb1U{Io3;Z#e6R?~^P8^PXl|K65 znNd>heOJQT#)z)u<(CWW9KQ9vqV&iGN-lB%&Jh4p0$?;tnT%_1y@wtE-6ibysv=Dz zV;2x}qd(9A%f2f-iuRjlNc#j=I~Qb}6IOlo9mr3?xQ09(04FZX?#kCdTJZ4Q1Os?4 z?BwaTiCd*(*q!BMFZL{L{89p+^Eiw>+gJo-D2Ugua(V6M`7$&-e6dREjq~)bQ>Ai% zGymD^Y&=;Pod9RckrlY7?8C$(HD1#O@POZ031~32-$0WFzas-U2~V9>m+51`-5cZn zsAx!B;kD;H<$7ENiubOw7P_UaMhRJt#+Pbm$~E3sY_wm5h3?;a;&h~z_1uOflJrz0 zzI^}`a`J0PXF)R^z;MKt&nTda1|Qz^1GG7VvE4xb5=|=~Tk&QC;l${J0SsbbS#r}4S9foUD9F51BH=H?j z3Q1Mf`)5OvO7i+yXR16f)_pb@PuuWWJiQUc%~|Q|B|41;kKW*e+2%jb57)2Y&&>NR zXA1J%y&d7(ABus4H5%F<^7yO3e}9Sh;&6?DU!pKKT&``Zsh}!;u z*n?7}z`>$vuFDtGMt?Q*6nX~{*VWA~&)O$>*421+dNl(m=iw4S?4859fDR|1n?A#< zgSke#{W(u-9wzlvT@TAk%2*K3w00eY<>^&Fsj-t>bV}?4c-)TgLE@7;^wZYqjdfPc zy;a#=u=`?hEeWrSQj|1aZWs4hJJu^pBk8;N9s0M99J)!bAsD&88!0LlBRzFo7EE3a zI=>5W{-D+97w|=I9T{PR4&n4crgvz#Q+03x3z+*t`osq-jJU1>2wAtGfk{B9VEq15 zHP)IuDtNipDcvC=)vkr@4iua$G0Ps%>1)&InxLR%yh!awr&?D!M$+;q%w$sf_J?C z+U_XbzLSgju7i4}QdF?Wgs>s9A$Dh6%wj_EqB`VD@RxI_gP9{jTIFpt;Ijv{wWyeJ z00c}NRpC=~W3W8@JO_|o>^1o9mv{3(waykmvGUT$zd`K=R2>EkZ^K9YqRjcvVv7(J zfxpmIZeLs=5MJ1viPMsbBktq5LVK($HOzaW#u>f7B&V!N4{MSHolICd@49a5wK-=Y?p?l#yul=_bCa&sF07ZK7%qQ)3804<*0tSJ9 zacozB;5|gwq~ccXaabF^GJ$^B`TPqYniBlmsl7Pann9K1P6-kYUye@QrWCFCjUpjd z&_gNC|1vJ*en9*R;LS19QT&`Z27ZJL|5?3+5cS+a10J9UT+TD$e?J6zHIH#Dbkm;B z0#1*$X3jGu)l#OQynV>Zh>bVc0pz!LXCnCwHG`Q1JODMO&NK4F0fpi{Leq`iHXesS zT_L*iiVt&umpJn>|TW=$F!H?4e_Ev>5zSXwCtVDsu_2eR21k911?A^3B{d+1+#rVVBZYiM3TY@@mRO9Z zv5}UrmXG?zzJH%|^G#uq5O?tljg6s%wF%q?AwLHpzY-z89U*@dA%6uS{}>_v8KL_H zs2iIJ-w=fC^Hsqf5%(yFrWQ-01}Cad4%Dp*7a$<}ARuQUAh#kQFCid5A|R6^B8wC# z*rVefMbgwVOVqGN^)-OHJDBjvk&s0S73{HakFap~b0JP>A>2KznW?o;?1+wusH^2s^sd?G7ZCC`46 z6vuEkGLG%ox?dI`N+8p}qfv^1AIGBX!PS4?ezBSV^Bcm2iKPw1M1)Q7S;S0u7I zA{_YcisBpxE*n-FTDL|8rL=ibE#Xyo?ofx4(!pl0g1>z7Dq`_s=26@oW(V~v)(2va z^@=SnL5lqPR!QU!S%gTND!&ttmA^L_DccH}zcVR!ii);pjT(j?M>lbfXFNQ8GMe!? zmDKD^E|Gl}c-G}L9rq~+jD`?n8MG>X`?bnI_B1b2OdZK8bDlx2jNWd{Pd~uPkA?M$ z`Rzf9`P+wQaDmxcH4#0uLz(xf7o{LYz$)rpqcUERn21zLhZZdvw>-m?OR_S~51Zn| z#lT{X%+=zTWqrH`Q!~7B?+1BzA2{K?*;wX)pI;9zYzVa&{c)97W!X=T*XHokU8vok;{4aGXnBY-vF?&+07HY`@_@3vl z3EwyFp6LBfGRs{hlTr3LCmY}Dt~u~<_LZWaWo$j|ubc9E3TPGNw6*z-m`&OC3tO;L z-v}F*B9i74LrYbnAffE0*3S;UPizv-91_l463%ZjiE!nSkgW<|D^bMJd=mEl(09&+ zzlVejPqdkx5GA@95wIQ;u$~aGo)WO09Lh*{ae6#X~23#zgGEnba-v(LS&O=r3?Zj-t+3y zZKI$e%RNUVkW%5kQXxjq(4Y%>f%Z)&da8+NiDU?l8SG7{ z#4>qzKRhDdBOoG7eN0TCqQa$y+rC0Xr2bc=>Cc1PQrJv+5-yP_Br(KRn?EAzPklRQ zhMTHn^5SoP`$a>oCPAg1$%c=Qf=p8cFKT}0G%#;PEPK}$bBwY5Hd#)p27aYmr_%guqp?v`QF$ji3 z_zVPGHgO*S9=BCXA`-q_TM^sq;j+8ZPGoF(_OBeU9bEeHTUeLz@pwn^Yj@0)ZaI?K zR!koCk_7GYK3Xfp(ZN#yD^1-Gvi))|(M!UyGGtK?4pCxeWaE`yBOKi7psX_gjbu)+ zZlgtXIKb=i4DvA2X=~ompD{ZS?r#h&aw4Bkj{ zlnUnj9N4Z2pI`3xhQFSmUqTT30bS1rVAuWd<}C#(&#Ec5zhJ7!)h;P4nH?S)o=USj zG^J3*F&B>GI1~1bHqm#;>67pC>fGH!p6pq^O2@9Whw)A?M_aFeXDy$i0IZkSz%%sQ zt0KDoyQps@hl;kf@!yD|72clXD>4=*Yi)8T$-Oi;@CRtXtJKjrz!{!6jWrXvmOeRN zbS2iQjyKWiQI&Mf@kF@exo%0(wkF|}%Zf-5J(qUBE1r(XGvXUU9+%OfGW`X>NArw> zw*U}+8r6+l=A?NCn0M^eGTxJJ@psJK==+{-2FlFFZ(1ZXp1A}^>Aw6s&MM!qsqL_I zZo6P%>=(PSMPrCx2E$sj8W!p}U48cLOlFMag{XYIH>UZuM(gr~LenpR_o^ctoYa&u zbn5OaRgLBQ;YrHhS^?t|G*#`vPb_gkjUci;V(1W`rm}=UUo2JOz)VKq3_Ss|o`j`ld({xL2* z-DN`MgqFD#Oe2K#?xjrU@;6UauQK`Fi?!_GR%SkdG!Fu`fjcYGT2)BL4;zyUPtwgkOS# zXz^nm6C$H&3nXgL4T>1Q-vQ^HkobLnR=D^xVIU{hij}AGt>K&WU8uAP64W%aGjEU= zpbNHv*}Ry3YJXXLf4b4dxk03SDPXn?->h&QmF;T7M{S9K|+@7{KK(T3!4jShZB$?f3ORyqCEF#30VG~aQevx{_&t& z-2bl5zyCkI)O%K32hz5uot@>TAq+ouJXt2DP!H#IKGwN~x#5(tr zFV^m>5P9od$)*rEP=f=(P&m*Py?EKf`P%En#;bTyX%zus)^Q%R)26*wU69669yGAm z_AAb*A00bz1;%*M;IkC@T*X^tM@-UM?%A%;?v>oYg3 zB*o_xpillEroK8Lim2-w1PP@@I+pHk5SB)|rKO|=q(x$BDM2I!0YOr_n-!$Hk#3Og zW_P~vdA|4kzCZ4qdw=Jg-@UUecW36Fd*;a{>`@u8;(87k2f}a5w*jv*fh7;*N(X0R z?*rUs=^fJCEw@PL90+TI)xEgpffORAJ)~A`y=hjauHmJ(2+=M4hv{rx zz`5{&Q0gS@OzC}&Vj+0xj0h_Zl*-uiu~zL}{fyEmFvI}--sOM{V2?)VWL=6DOe|V^ zXFIuP^EOmmT>zm>b90Xs+W(-P#GM0qD&WGTqa{-F`(L17`YV8jcw@%>uqt%iqxzJ8 zC{Zz6t=ImQwn)iZA*n)pTHM+mNm#aj?=Gg0C$6a^sin{cup5p6YyvvTdTt&_p6A=Z z?KH6Dgcl|r&yftQR5~9E?iXM`B(ptT3@Wy;JIYPr=$$q+*5^)*9GN#x&^dj@k~Ofo zOhV^^ggTbSxBu8Xhdoz08w~+4Ujf-G+d#eJ&HJq#;1zGXc>-SXJLGZW^HyHqx&jJZ zyWR)Ti@Ndw*K5e@$p`Zj{dl+A^mM?a-|bcf`(444a~L*@#Y#3WU_h2ix((cYvk$)o zU(o{;|Nesb0X$#6h0C&m1ICC(^0?DLe@(uJE-mwG=+2zov$+43P!o=TrDanR3igpz zF-eM?d16{;Zhh`bEQqhwBh*i+%)YGf?7Oce zG5owwsV-u0m1zDb^?C+TM@lU@vX!h*F*kVm!j`{>%zX8ObGGGyWZX_3hwi89{^DutawVix*Tb#{bykThh^z5o^hxthAwXnlJoGNklfIY*iTC{*-HmRy9UfgqI zJ6WAFWveeXLoa38xrjl+-(v2f;H#;|n7sAKXq4&EE-ILwfYqf(;_#Di#YgvBp2y^N z>9EZwHv&DfN6+FW&3Q6}kl%iP_HQmN8kJFUyS^S!F@~?FQ8yRAz1m`-_88&f5BrT;=x&foNU)+qje(I@{$7w+mV%WW^3H2QVxqnW{DC!mO zHWleekZI?ucxdO(2<57nb>yf7G~wv8_vWY|#v<9hn$Yz3zVho8KgQE5K2FV1k=&9_ zZqCQi-&+8Icn}B}4`vst0D;$d`ak<4*)JdC>F?n%{M5^mOHOx`Pfj1mQ8~1cPo9sF zOJ*0&ReA1Fq{A~YyQ)y6!{>|YDw}-wiBNx!8+57yyXmDW((y^kQCV7(PQFtk|2bs` z0xP_^DnjB~`Q@Oy*>H|ZrZq^n63p(^3VKe0;}{&!%1;s1${*pBOBSC274o2`6sV{M zDHA}s3!bVQ)la=*@YK;ive%IZ;J%J!zx)FF!$kFpv&psdQ{2IxDYf&I}VI^L#+$ zwE-{Jf<+PH(){@p5X%GvC-gb~@Gvd{c3ZnbHv}Gj033_J({13%p?S%zc@CY^US+4) zT<|KGK-cd6o?Gns{EBbXN3tj9)lUoXCUUy9Ue|i*Xw0cy-+V|_HTgpB8Q=SgBSH`O;|IItQp3E5$PVk z#b|SQN0yf*67#AQ)6^_tkFbmv_^o*dGh7qQ>|lAmS~3n4MqeHP*Jf_wbb!YtU}v+l ze_=F-I*wHNY6rw0M<-$(9`9MrlD@Hd#!_;5m8xvxUU3(%LkHj|J;0fCL-(P6_8x~K z6>TT!^~RXJ2dU1X6&0txcV%f$NF&OjN7vywpHn~Y%bj|kPdc52O!u+tta%^r!|5;r z#s$q}AYHH~z2b{0SUZOXSl?mN;Orloz1@^JG6ZCHrvaYlU}ym|vgHa!Wc{$hW!7NO z{@0=hsgDVr!leBm&~VFQq7x#Q@v!*9(_mwpP-Y`t=`EYXa@%0v_LBKR1H0QyoW7hK zbD7HC8~ZHJK$r8)X7XV$-^<(AYZ^+5&1DZ8&Jf#S*S@eO+714<&C}Zcbv{8ven%+J zZM#3iO`_?{+p*-3NPV)i%MP+Lj|}TRP3)fX>Ff+$Mkr~Ea4+4LY;`%Y+g-AW>v2hr zRU!+6MaJlNY^+}fWm;DIm~vinKh+JLWr}Sld^T%P{G=odaH>J*134k5u)4)WAfQ1k z^A|9p`t7^G&^Rf&;(hNItW`1Kw_UgL(e)J%y}@knii~xuihlT2YIn~7d6Y^dP&bME z5mlKbE^lCJe}V}NTI=&yAfWoP2y@sT^qT%fVrH+pG{ALx;qRD;VV?eblzP=9yLB&8 zG^t1FDB0b){&IVv+^MI|WI4$d3J|gM--jRbo(h0By4dZy@FmlozaqEX2~JJrxX2Nu z$H(qtO=NbnqJK?e5$9B%+&w+Yc;cki58xAEM_%a|?z@@`H^O-CVXOWs$L3wn%k$4+m|UkS`C>UWonQW3$(;TrapJO; z^;}FdCpGK|q#XSu!Pm`?Jkje7gAJ#u`FmAbPdrUzN;L3vS2GfQ$zKQQ>d@GQjOPa& zrwLbX5b?yww03_1)KEn{ z`ZrQSK4!loCUHr5r+ZFaqnO4RA+sBoyDY%ieV8Bjg}M04m;GU?8lHu?7RyBiGb%Rz7f>up}R?UM} zt%FwWf>xb^R$Y^tqeitE12M7MG5_~E9jhG+3yFm#g^d-6jn$4#W{)JtIPUHm7x61# z`%wI$*stIc^>4u7ky$Jtpz_4Mz|rfmQFF+*`B^}vd-zVX7<}mcBn?SB%g3~q%B>-M z?BLt!KQ@&D72bYP1+yyZVjAb8#yL{u%C)(mSmQHvqVSDXA0ZpUsn}~iqFgzm(O%_5 zSyQt&z!jtX)dKy&`@Vvo(hktS13CZ!_e+7ohdz^-_~ zGa^m@+1$oZZf6d(=TZU`o$$g>XtaYC;}u_`Byjutih6omBt-pjliw!;|CalIkVNp6 zYyfk&x|pLgqgRQ5-!;IhuLT&Sn7$da*?EdQ-oK*FpWGoPxjFbZ?-`YyZNtKU0fC#a z>o(`kZ@pYWfcoIZZK1II$AH|sIH)+1*Qazvx)=Kv+HL2Pxdq#6i@>Hlp+kGZnP$t& z7@R|!a7%e2tZdC2>>%H5sOyiJW~#dTBI^YazYEbDeF=qYvPXqaunJXskNeorIvsSY zUTS3aP`KX(ojgNXvm3)NyUiw2MA9(}bB;a07?tmz1Sp~Si~y|<=5+s7WBY-S%w8^D zfP=#4WPSSNhm!}@DkY9U4&|ZT9tKQ=ui?cweXn{4sp6O3&*Kc^5oId?UDg>#j3omaR|bOa7yMhoi-y1DN#u+AO;g(3`mpus;`>g(dZ}huuPh z!Pg@mz9>LIIpF(L{+1)-OL1kpOMx`^Jx4Lj4h`m3H8-kF!_5fxIht#eiF_Bhz*jY+ zDaGC`0^y^xiqj?JjrYsNWI%1{!{tfR(kxQw{eE_ZqqjLMXY!S;Lhq8yQ@Q#XHmqec zpR;R|&`IwzI<%R0msVL@`#wu+1a*rYVKWkATo(=a8=dWbO(vnENW$5Z(5)qkLt~QJ z_Z~70%%c}}U8?h9=_4+^f*c3ElDu_1EB^8(+%E2)7nnJGniCg2dLGS}IpKA-)7!l% zY?EgQqEURqd#X=~+2l67={HXI1J6X=mu%2(i6>6C9qcHCf%G&1&6R(qADrca|wxG%`pvvB(%2H0a(M`m?3#~^Fy9%SM zWrNgmKx(%_MTL$Jp2Z;IGXP20|s2N0OlKDMU|A+c#7LJ zI@6t@JjG|g0nT}xq&mCo4AA=HQW>n3boN=s?$sw!hcA>pz*$9<>?|HO0rzABgsX39 zC6Ee5z~j!>0@X-=nE;E4-Jee+zHqw^Sh(?iSUibkho!f)tRQuN_XUsD9LoW;1p;c5 z_Z7fw3t*n3*Kucl*!R8zaLPeE^Zfc%yvOyh0$dtBgMIxMTaBzZ_8+?#@m-|30XB41 zOFzomhSM5zt=vt|SUc^-zJJZ%x)SK70>-BBp3;lvy~euLYd#%(Y2tfKsqONKwMHa= zlq=`=3)V-5foUZyc$=n=^yp~5pu}sF>qDotW{aCKNfBR|{7~1Xc0bU#-x2p~qJ(A3 zFg&|u3p(*YS!3aoLaJMC2EZeP_khzTrpIEwAIygkKWV_77>;$nfVw}(@W)~`VOMb1 z1@qKwBOTKN4(xZmr_lUsq$Ya~u#$ZWq?|*SHh`*iFqB$HpNIrrBr2*ggzLEZoS@|5 z@G|YmHY{%LQmu zEot}se{q3%b7lef;+-qDgox>6ZKScSGx=+F2c~%zXRljQ)tjc6nVG^|M#7KVh78x- z)pc{Pteje0n#-2Xs_L(vM$9qZ2E3(azO3|8`&8?-w=y;(c9&kJy6I~&i($TyD@$Sj zgWGJ{Tf#5=&}H#gN<}dio?FXoiDs*j`XEb>!#f&#g469B~sa`VDT`8_x+$aitsSPiRb^OE>Zdz4K!I!hw!ufK5qgLs^SP z?bJsL>dmH{b2Ij@3yGU}QN+TD)h$4i!D z{8&QqJ+yEj5gU_4jpKA$ZNc#`%&Ro^FAz&4m*rZYM{u*!hL}`|=J)EBx{q@NH!A9N z-r>V~0FyYN)${b=CuG@OGUWrgKabIWP=0?d(2EK>W)c&8l_s+eu2ReG&_*f4Md zm)#Zuzwu`7oR)pN)yC;7K&=i0bLEzrk;p7>IYB7JRquH)FlXC)x0}qdi9^!UobW+tY zyn-lKz77FPEB~%_3`H8Sy?K2O=yg6oVR!KR^$&y_RY)jZPA+)+Fj28+$T`aURnX=d zj02@)*>2hcHS8rwr-!cNvJtE)*yHBXCJfju+>~Zt2rwzodh+jw72iXuC`vRwBNY60 zHXk-;Ql_NVM@A>rdGNW?`xx;6JDUPSSWvLyTEmy z>IE|4q#SP{;c;htUKU% z6>!N(N$Ljv&TL`6&4y~s-5)ItTuI#4w*du+D^$PNwfLs3b5iz)4BqPX{Zcnoq<`q5 z1P)HWzQ`D3n$9AFexjzB7|fbt3sx7;?qr@l9AKu6v;3G%SjAQ$Y^jUiPwfC}iMyl; z4egW6%UWn_@7vfaJK7v2vlYm+t*IjxwS`khjQPx2hz6wMVoaO{%(@XQi6szj+?8{6$>5cB6_n-`mgW_e<`s|bIw^X=)T5L@27!># zLCEMKWDF28MhF=bgp3(N#sVQ@b!N263$iE*vM3F*s0^~G4YFtqvS=laLPZN9)Mbpo z!Ro=m!obB+#C<|jJpybw)4iHHEGWJ%VDJvx zj9HXuL(IkgB}_@DFu`=<_SDqfj49Q>kyhkEO_h6Vy8asnvT(FDum9y7nhi$g!nr<+ z*-ug;3fV}veyzs#${%Qlmc-g$x-MS&pYv(`b$@#B<|V7%!3Wyq4(w~U&GBC4lFu)d zmxx%eiGMn(AMr8ErPCyL-s7-`Yg4g@ztF?#BGzJSEt1JU2_Z@DtkMCKebg2an=Z6^ zu4=J0*83I@=IFe0K0o97sM1k4Jou4{{qd|Y5Aa{5QO|#r)!o4=mQO=fEYa!tc=Vo> zAHF`L`p40hC-#rSZ}(6-e|7$dDOoO`U~BOxpHzWYLGOtbUUFxAWEW?#ETkYxWli*> z%Gx)ys*G%upC|G^Fp~Xf}V+W z1+)wHb-o4JLI5Dh0ZLKYB=c{*BbTt~8VQlNbq$fx)64OvPzIX}#L+~-XA8nL34tdXU@HwMWwmsD6<-@3Kpl|^dn3HFY&wP3DB zV)CBkQGfaAat(Npg}d&aM&htxAd!RkH8>};T>zLW5AoNrfa4&72Iy5Rw?0az&N;tu zeH#=RbQ7%*G*2HHbZ?6Oc|p?lD1@Tu1F~XpaC++I$|vj zxJGRxF7@UuPzOH)Oin#D-~Ydb-A+Wr@wpN?-TZ&nb+J3Lm^YhK3HJTM^voe)%~79s zX#!ua>&WKr--L00kj;8}n?uJICTJwJ>yMXh>VuW-FZz`>us|UD%uz7=gFSCzeJ&w> zX0~9wdObPOB!+%)} zt%O~u4G~)-b>0eEm6M&jwPB=j^*j(`SW69B4~?$zZR5)@7R2(w1>*Uj>Ah*tG$fSw zkTT$Y(;Gjn5u7?Wk%gP!HTCxPjnjkqZW|K3nVkV{1dt-`QmnJ!&)~)lFbBSE6-b>o ziAoVQHbb&d2D;$S-fjB&b>+yIZZ7M2WH_S@InQ45H<&ChF9rtx;q1rf_epy{zR0%~ z+oTKM*d7@_&S;T8;`ec`?52ADP%OGP5$M)XbFADlo%}KhnbJd?U23>i#C%Cc5QPGTy~fs=4k`$A!t#lv$CI3L)WM1#gnxfL-3} zCLPU1pJP4`cp~updc{-bg~e#^dT>m9G-lZnGmYmdr;i8)r0sj0FvHdlM@9w9T&-)r zP_52r{03Iq$7U|rotaN4o3qT0QRLQF2mqMVlP)rybjHS5GzrAnl__9yJAAnP8`1IuqZ2H>|(H>b+Hn;53>@jgC!zEo6(c+$IHLUgm0fI$XW|ThmC+H zcy|ycs+>;|T*f;t7$*FBISFK`bOe)@Z&?O~n^+CQYG2mSNew7GX-cXoHof zr8o=^=rjzi(I9&^+_f3?h@6Ae_F~EAfwp3hgN=LOeA;JG4Cy!hpGf>i0!X;$-bc1; zaL(!vlzVYgg|RnmR>l_%p_^AI6R503~wE;*0Jo}Ptg`jQy_?Z190T) z0aAy_fSpJ_B#(&o11(R_jkyI_ta@%L6c|Z9otnG!+^qD*p2EV&Heh*8cjRsG(ucs| zC2)G^-PwylGysLBIa=?h0ivFN&%k^AdVs-{dr4&c?P2BS5sUn^O&WP=#owxC^#is? z+$QLR@3Lc1#7S4P_^~AtQ;~@hmnF3cddH>y2t_1Mi5bW?#1LiBG{q32QQqLPA zCr-J38%P*Oy-GwmAtVhvLRq=n)K}`t5tMR2Mpw;nW=pBxzcZQVim$3Q9~+_XceUE( zjjHlgyPcaTUJ*$d8P7IumOpCnbgrBvdhTyb<%fe6#;E1f z*u0&d{4Z(xipmuVAC!;Rv56dBMav8O)+@NYAIl}<9m%CCt}L3rsw>*!;@op`=5$MN zS6)no8Q+;YaPPLvy6pO#i6{f=vy5n0{&eJxH%|TE>--qg4}2IyY43;l8Qnq|r92v# zat5uGtsaaA$39aj>l8jt$vclX`m(Q4#Yj6+j|2TtmDig7Mp;tO=u3TW3lqbZ$LXiK zQLmDgo3ECh$G`HKMn}}u7t=HkK6tmGS$`{s%d~qUn9Af-jQB|Im9Zt3>6L#DR0G>t z_f;>%BaeDzjcK+}B1|e#lF&-DF$h$p#ZCx6y04wpGKn^#M_6aE4LCHlBrJqanSyc<~ z!9hlB%oKEUkr6w|K;QZ$ggp?4W z4S@al6qbX8SzH0;E}m{X5tCm?w8OVk)+n*}T?rbRAH-+_j|4pSb#buNm)<$GY#~YyNiS+u&K8U%)EwU)rnxW87ia&T7rU#iQ{Ciqn#DLa`VqqO@(1@^ z{?bTb*{+M}&)vWWGlzJD>&qWLZyPI?zX5JUSJ)BO6<>1M~R`IDL&OdD=sU06dd#1%!O*NnkKa8eU5#d;JO_X$PCJtm_^M{|-*EzFNPH7S#mQ^`}#%&KJ0gbk#dimasjH};7? zs5J|1O^?q^$@xyyKChaIO>$)t8W#|+=WDy;{0zwvBg}95&wYms?jV5ivhsn4k5os3 zBfD$H6M#C{#9orA=G5`cxOS0g8dF;%c3$+{jsAs)KDH>$w(l{_cIq~SE=N}dHa@ow zfO7QQWxa~MbS@*?r!?W4F>kOc^)cj|$*#95h1zx-0$I9BW5XLFI5Sn zm@G?Sa-jbY5OQSexnt6az4XW<+xN&P+wWE7o8hUpD$RBTJ6QsaDnWlIzL8z!+BBncXE$Hfzx_>H0z;t{sDG3c6h6$2VbT>PQstO z!g%w*a`<2n2Ur*sp$Xe-VU|vO$1^OIp$6ZHDGpHz6}PZq+Y9=dUxXH|s*M@TszA+K zKpz&}20{}$$I?|r!VPG+8dk4|;)N7KOr|0C> zMc2mUeta+?yx^{~P)=zu@8aw3aQ`?!mFadopPIRj?J#Y=s8$yaRh3XOXFVix;5nRM zwRg}v5mKMZKTmhDgH3pgl+SoWT_!L70CCJ8dcD;l+ejfk0roZPfeB-l-%+d!LY|T> z%c$EP2wVnt089*!>~UAq?2l7tyA33~>iHW#{@j@I2jCrhmp9FK2XpHA3!4cUhu4w) zfG>xP!@SuPfp0po$PE>~JC;%1rr$p;Ozbj%OU;&ZzKKzjDvQw zf@FwMC*C_Culp~YBEgVI5%2+_2B)x6_!n&;tI9p{b}yLEk#5^S02x0zGWpz&^5XHN z3XWJ{B;Xi6i-bM-&%@+5I1JwLQ!IGzy3PDR@qq$Ayqd zYTtQ%Ic|HsLb%|VqFM9LL+`663rmf`hk*~!d7Arp`yW0@j(#rwW|TFKZS}Lcg>`K) zqz?H;OvZC>V$@yITy=5YPZdy!%@sIi*ys*o&P~lm%&*h4IBc}<+3tJTe6K%Vu&fU( zWSmE4{HR4k8YxM=C1E*Pg`gP?GQFCj7dhCiTCB<2iaPg$A z$;7R}+kP+g`pw^(^AXu!EmeKJQn)V1be!j-wd-T2AnKn=!=mp+aG5=s^u9&7vB*_{ zkG%0p!`u42+}j3piNw`;_#k?V1MUjRgjTZLk;iX zAtA;fEn~&WWn{(KHq<8Ltj^(Ot;*p|bCI*;or&nQNW%@6p{0BhN+V2+kwDzxuSI5B zlfx_ODrb2jq`=EUjA2m6ic?<6iX#jmI+wM4Cm+-KT`vZVIOlp2dTC6Iq3HyYPUP~k zj_2}L{?Q^EYRKVzKLrw3gTyJg;T;fps}xooW(RFDPmq}GrFrPrT;9+2+GO(D=!yBA zL1`dS4M+slL4T%bc@jku-XQ{xr2=#~_{XIZ(`iuy@^FA4v%J+dE6zI@Ca(`MhAKEP z#qrte%H2>EE!BM4gBrk% zxFgM}_P^ev0ke8X$LoMNV*`p~Gn-qH91)#yV(c5VjwC;q69RtSTZv+k>POSK8tg0Q zKiJ{(iPxmP%Tfrh)4=cf@T1n_{SuB7gk%qZ%{1xxVI@Fm`W$LgYJ4;RO#El5;Sq3O ziNGY?+WuRx8;w?gn4UPhVqr;5RUP40wTGG40R(0;8m}#Bo=G3#-$LsFvH)+Hs@Dde zxoY9ZH*i*ie?d#oL(L0V=`1oETs~sax`P?7OaZ^|OM#sYmb!+6WD7D}^FcFcnd|uF z@0CO|Xq~THIL_+wI&4YJOxspD0d`|voTS=zYhDEW1y|4Z!CZx(&eL3QuP3?zfpS=$ z2;6eud{NO%H*oUT$EM+Q^Bb;1&&5~$*htz-TK9*C&6nHjU0#bk3QISt2oiUATwpj* z77(%FbLp^sz3Z*mUn`nFyjNd(Z@1)d=>{csm zC8f&Ty1(HzBWfz=BDyL1%*wsj5Ow9Ul2Nw=k9y`M&NIDn8M>X-;Bbaf70U8_E+UWO;1sRi{+X3V&t-~x)K8Lf=SV2=C4^MNx|6ytL6fr(;E1<;5~7bZpQgxZ)y9Hb_NpRd=M+( z`sYlz%;J2-#5tpB-NvsYLae?sWXt=g>a^Zy_p!H}E!0Gtr>srX>sx(Z)uL33o%!Lx z`S4*+gmZ1rsLjD5Eig6WcaG|flxgwq5P(@@K zGK}K&3A{N30MRq04qZhds($OATyCt!uQNoSLqtg+qNET}GKeVoQhK5&1w@n*B1#1j zrG|*oKtyRFq7aBE9YmBKBFdn!{%?O6ag_C=5RXS8p^rimABFsQq+=IODayGRIjKV~ zO+`xZE%i0Z`mdnXuAtSvpw*$;(PUA<=dZhp*@$`zi0IK9n$a;a(OEuH{(l?A7Jr=< zA2o-yq@bW`Eh)SV+T)f**Rvz!|1KUw@MzjdxlxFf0Bza`Q6+(n6|%=EO9U!&LFMPi z-gV%tCrtsi*T0*Ug}o>mCwPR4PK1h1kBZKZiY|+au9M>yM(`B!`6+||2l5#QLVyeT zj0+*agM7y8Hh-u1>@z-ufB^ED075_r`Ai5QAcFkQzOm<6Vc$`L^H73|P=ZTQf-8$W z8S@@vbt#-0JV-%!qrKzFe94o1u$4z6Y90li*Sc&9GA=dKN6qB-gfawR{wdR1;j z=WGiy{7D#;$u8tX$P7!L>BKj#o$#k*pXAut(x#VObWd(5K# zo91!&lvuoO+2>a~rgCOGdrhJJD-K-GM17=1VbV9aK_?RSeN6E12-7n^`$z_O)ahk> zt{Kyl-Ctz-=Haw|F?V2ppSpXR=Flxbk;h9wg^qF*FzE09Is}kdfaL|2b91Dgk^%-} zNdC)Dw{SKlFf*6s!))py@IB5Hjrse$x%Usm$G`5l;sL>9$aW_{&@5QWe5c{cptyAp z=zG(72ZQaw33h(x7q2$5{e}C?n60lkvu)wMe#ojq{CIk;9!R^L@ob-3T_xgdefOM- z@oCBaP`B-PU_$sdWbqF}($U&9R{U;frsa=Swoq~@_KgucJuuTqfXv87!4Z4K?JiGu z(D?#BKU(M4N1byz(@G;`1S1p#Bj4!7{N{t@15pp9>ER)GNo$Nv`?Cv^u=4E+x<~P? z^N(-*UKsWwtj>1Io21Z(s{ztl`)mI(Nn5aTBUq z&!DN%uz=*7_4Xumf?jngEf&tbLcY&hx-$1-zF$%0o2R8{?MA)}sYBerp;W+vSmNz& z-|x8d(bsRd4OdpFJX%hm(96t?^{~?+9tFb1{J_Cmb0v5mVy2u8Xt_(Xe1Jn=P<4u;%>IB3rAj|QPvw-I-OVW`z`ds>gyytGo43~W*oj3UiETk z8YaDLJbxsVnVlpwKUbTj$;_7nE6d5h6!g81VhGAF4Z)d<8s4Bp5<1o(SVy zE`6mvUqrRxzLp&I81WnN8#UQCX_~|&J* zH|=nPYgz{UhomH6#m!Vn;xEVKTe<9iG-Gu*&tlV2KRnJHrI)#0wGpeW(#9$wvB7GQ zGB~Q1fbtWHwBV`-L54O5v|T-;_WFfJlDuy?UOgC6&PzK zg-?ILu%lEI7Uz!N6QAPUD{P=v#$6%&qEYS<+tjK+c0c1X+4u|}d&8hYNd0|3quMv0 zY&PF%9iy}~7ygm{WZgSlVRZ`E&$=c)BKhG?oBh*2sCT)J6DE1Qyxy21j7DB)4f1Wt z%F#LfZiqW*gJO%tEPHL1-)r>^-uJvQzDeix`ootfcBxDU9dl%lOy?k;F>y&JIca=x zc>icJR=j45{5Xz(_`y5mh1175zI%I^@5q3B@8ZtFz`5;Ts&m>ymPVUHe^o!Ef?X7Q z`}B#?9nccP(p${)e30f<{szT`@yB-E?0{^W=b2x-JG4){#FTcq_@18QC{>y}21l7l=skkXBIU^;ybg?#KnIYAvX;+j(0=2Ms2))&ItH4L} zmCK$2R4GnZ2lKrKmT4gb{YGP%STsJeRZ0FxR}Aw;bBk&Wg?%RoPfHB5HfTWXJEq?Q4iCF4`niZ(B|MLkB# zejJ3M?ThIdIUw;}L2nsVGWQJ6o+#NzI@Qp32O3tS31Xkv>srx1C}5>4fF4}(Q{`5m z#ieeCsl zKr$7Sm!M=Z1ltJ%lHWh+9iax~xsS5HvX#jMB^4;!K=~e&OL^{^q8v5f19iR@GJ!IV zJ~u_D3G*UpK$7&IfAWB&-#}L}^DZc|+dt3Ai02LICgc0Zp4IAeHSD{&52^*k5q%0!FV*x4@_}H~2?0Y*8XP z1x3W5E8(R{a*=t|9l)mzWqnnw4K12AS3syFRmk7I{<00MkpcCmu+s6j-;xq`^KY_e z9aJ^}%SYe4fY-6WQi*dNn5u{mY0iHf@1;aD{#N|;n8ntd`dI<{@2rjA4Y;*eW|Q4S z&n(w4XQS(JkPn>D&5Sumyu7Y_iq#nVTRnfG>i4D*@qQnp#61^LjAFY!eIiTfBe$x` zBFQj3s_}}iI4PZr<{4&=PF+19JmvWX3xz$t)x1&dgLRtA1>zy;&pb{H2s_Ocks}PS zOsM~l+~cnvXK)BZZ(4@2q0tNsWw8;U)ffrgDJTVgUBbzIzq0il_*<8Y^q3FstCRv* z`WNttO&GIyd@*pI`z*D2s>Dmt@or0|YOZ&se>na^r0&n0hJ90hS@HDGpR)V;?FGU$ zJWuvYpBB__;5|9C!cUvOp5yfVi!3d+j>(rTG{#RCH&|lyX|En1-#W+KEI;OP^IXW_ z@>$8+`dj5vL{&+@JnliqgGqueelvyPJ{BgL$SkEbc6I3R1h0rEtXAi%XC2SeHn=Iz z5?_@>WTr5O$qtoFhg>ufMFgZ=d%oz~FOy)#J}n zv;!3blok?^Sj?dH^|b55L&Zy>yj2-2hJ>zYDes21Nm{@W}9+5InwhW=Bd%Ie8<50Ae@W`3)NRo)iR}qmZ zioPh7!UJeQ-P$xLrEzB{7QRWCeO4k~IgfQ{ASp|4B2r$07K)&SLiLMZp$E*|m1> z=5d1L+rXuiWrmnP5A&(Z3>o%;xUspnLY2|O6#;+GKbr}{F(SqvC=3-rM~dN!VCT1i zZ3)W^`+pujV7DnTbKbf)hWXj``~DJK&}o!I8v$V4=s#{KoRP$k>b~No%@x)!qfAHbOcbcrTo;y zrl?u=h)3yxmZH?F8+hm5)Q=YX*f+~4r@RaJ0|XfV#Ds2m-^0UF41uz72SJsGy z&-0jXE$59iF#uqwT`^;jvVPxo^~}U&m!r-&pTva6nW65hu7aN3{Y&TG0Pgx2NuIjn z8uNp(fb6NNZssc8vQIiYKkp%(^Cf#{w~CRa!bQ!8W{(J`t@Pu)j0lv~wJ%M{@Co(4 zMQh@Q^|8rKboEBhx69?N2BGlf>EkOK6lVRoA6%M|S}<=2tMD71_vvO=g@CFj>s@_Me{a>&8_Yyt{8!6pxMg0i;@Jsxz^Bv#rn|iOID$_{& zl#`8m3dooKb(R--!C1^$nj>wP^r_OzWPpG?=jE+3Y9U0YX{b=BonE8-^{Dvm4u01~ z4Af9S>JOdm5!5pCY~Y#IOhjtgT>J>32Z^NZa3&xsPqW~wd-gU`lxAxf!6Kc<7s_Kl zNlPDF<-c}WPhb>wJ_$VykIWT0^!-#{6nIqK`W2L46a5pM&^y3}^ESOV!`NL=gA{0!FG>wUQ)DuA7FQaGCDhdnT%w zBhnH>&rxIgT=nX|Qd!pH0pFq(u7g2bfRSXWH=47VmcHt*l=PeN0Q?W}-h;XKORq?l z;4YTq2q(^cEbpP*dzLcJ=4?918|HXiE-WLof~XG`C3BAI0p=*)y;h>cQ}PbMK_H6fU72oIfi@Yl@J> zy}2Li?KscE6)rDJIe!AuAPYXoa!g>P2kRWjy6p(c< zNSKohhyUsa$hweB*S@SzVMiTZ*uLCz_D03`8d>cc$dgY4em?1EMPO8N4&Oo=l|h;{ zyP!cU^1N}bR~@lZmkvc)1pB z0*yetMjBm+mz96-FsK5(Zt&**!2;D@^(5#4!*TsWi@_f8S<&T?Iux~y*9-6zF0 z_iWMRZa)GjZ!-*Dk+lc-{Crs27{*yKnSB~(`U&m6b+di3|CwvnY19u#`w-X6IDpQlNqk`W z(_lgib&11qJ0k7Z)${r%Z_U@X9xWKWFDZ>Ox1GI8-dqjx_j}1vQ(6M1-)>1^tqBPC z;$JXmEFQXgjz{>Xy;B{%zT(6cbh)Vo2?BL3?w?fr!(ID8s=_y*(*Nrr$J?ZjF8zWy zgP)RcRjqM8RJ6bKwO=5Wq>z8sv%82=PGkDYa*1Y|8pG%hy==e^AJ&%|YZgMwXafs*_q9L9z zVIrS^_Yv}!UkbmWp<-4OTbTG<$M&*snbvmHXi=+gX>2ho6`5$!e2jlTwRUOaF)!vP z1vBC%v#%ljsWCA5AxlWjqm_NMsAZ7%H?r}?c}pQ~O=->6TrUaP_KS9%v=*UZUe|3- zOQhU*gvtzUr85rGhokC*eK}LMPo_$3XIFTRn@@0Z6w7}RffcoX6%w!#`>#R)R)*-% zuf~i6EXK?$=75O6xKb;pt3vR1l+>gKo`saGj-BxMB`E)fQaw1xO)m*^CgB);oAA+n z-*Z$n?BMXoK6&{!4D|d&ght9nk`~PL)rr-UF=)GX#+%+8Fw3br{nHId*U zH54ALt2=-7)#f}!tie0wP0H|~kIUf@MNTd|K?pyD#Q9(S-*IM$)cKG~Hrt@KPQWS5 z_Fx#v1!>6xF806vf1eNR&OYxm&&=%J+}^%3`_8RT?3fU7MdhY^mn}Y*L%DK# zTNZ^@?v<7-5F{#a*&?4OrTY|{FR1-wSmM&&8KpDFy|NftwWjJ_!2gK*o-LDE9jNp! z#r0c1SpQ9(<`aJBi0EO}B^7Pxu!Nu;n1hIC<@5ObQ311VGlVv&KF%>#s5Cak^_F*` zJU;Dz^|87CMdYS{RcYdAY-mE4W~8__)`RtkMgf6;h{R?AfpLZEqAN z&ZpvAeMatoqX6ZTGtU`&X4y~po#5JuQjOnnRBhU}I7^dLWVOv6aPcDwT4@?wcTy9e zaw&43N-RU=L4|cbSv{3`{zO@M{bKru%AZb2L^TFWC>_?fztM?<5&y;24}0<+g)(H{ zWzVKBvi>>iaEEn}z6l#w@`nmaMtY`JVEFPB$|g`FyyBIkb9n(Zb$!cSatB;r#qcLw z`4?RD>`DFfwVmIYw-T=v%aBQ@6orrDjvxFx)e9@mwjmq0R*v|bPjrqSn})6C2g zISUsQlRRg&!0z>iaKlvi&kffFAR;og9%obr=+gtN=BogJ8Xa-2*=6w#o?LDz(;UM9 zw(2uN@J0#gHIp^W;5+A-EQ&*cue8f%RT(bs9q$@D#=bDNKWk6xIJ|PZ^zjflIP7&^ zk$#7?J6_zRVv??Xcj?Ue4mV`^=k(ejZ$Gw4aRD{K)OIM#SIaym7&_;WO>Z1$7b-8# ze0De8N$gSmoAN1#O>a&=^RTXfm=GO7Xa$E7k0F1^GbNZ zU5~*RTsCj?0&uXcMmA)$;&u(UZy4$;Wn)>ru*oJR_Zd~UZ)}X`AM_`;;@m1{?!@x7 z1~Zy%-_$In?*mG9)j8hF-F2LpkKW(9MYtBIwzNoXCKN*&6i*JYf_Z+II7e_39IDM9 zKy|4DZhUnArF3Nipvv6Y9SJni_QwI_eGMqye_T~?)9JaqFeZKWA5K+6K$zZN2T%gn z)yN~Goa4E%d~t4RW)2t)mWsq>KG-_eLL(b|%P&55G*}8K?eKwds-1!M$fFfkV#6GG zzqWo3klBPJmx)H`9KiZSh|QKO?G)5sdFHh~<0ZyE~9~QvIvpK8J^2`mY7i z+NE{YNMveYv)AK0K__8{Pa>x&nm_5E@}-$zFJ2u8VkgvJoTcKNuX6uGBe_w)djr7v zDboDJu8JKC=iWMw0Y>%+>6sUq1!630vRxN0I~E?uScl0L8kAdiPegmTCzBJD-T_bW>po*DQFp{a>?;+o>o2G*BkfXIlBE|xb({z z-b?8ZABeYU%`5!)eYR=sq>WBio$a+=hk4Px@13nZGxZZn^~T$VeDntwogUD&ErmfmGN%sevvEKu2}9kjT#-Tcbv zeb8|heDUUsTQ1ecq{as7g6}36Wif|>ZxVLM9ahR&cPBKP2@I76@2jA4Yen>PTF>a1 zl!0wSK2couzp4O(MNr}GA~bf*e;Z{v`vi$JoWS77WzW#4-Smx|V*6mI1qgVuoS~me zY@;Y#Mvti)M3B(oeoQd6QZ3UqDrzr%!`;m$IHv^Vpfq|cIr#*+5leme5hJ%qdxmEH zlfD5}H#Rxkw}VfdL%9b2v>w9USAeH58r^&xL;gAZq!`zJ49JeX*FXWC^_PSJ;%B2~ z8x3fWJnVLWkC_}?*P*GnxZ~o~Sur!fHhdZLaZ=*9t5Q3p{ljJ5&cS_MU-J9yD+gJM zXA$E~*>%2UpM-p`UBD}~<4(4A=cQf1J6VR_%Rd<>^|EhYmv?bhA%Ek{^BQn`5y2O+ z_b!+2wJvb-?6)KP>TO`L{EHQZy8dlA2v z#7z}TnnE%+KQ!gKKhm-?W(8yu9FARlias0*_@l}O`uck&?Kc?SN@hZYG~Wbx#w`ja zeA|KcaE}$7mO8&2dO>M7_`-f?#3uM2Uqa9fcY^c-!2~$3ZuVbm=l7ed@t&v2LAPEj zdM`<}e@YZB4HtGi@Mde$6KAVJmGrzp3Pqf62Sl7x9FwMZ<_xBzn@RP?k5e1wp&W0H zP55?X8coJ60&>2jwd+d=JWngRGxljLnRw({meNyU^-W%FwMzEG2?EVMBz>JoRhnYQUGGk zDX9(d+!D4BssG&OI}3|8eikHY8u8q@$N9j{v{3S=iO&;(>3fi}c*HwZ*zhCvLrBsM z;yE0af(J{%houm}QV3xww_qtmuoPlg3P}$Im@S2@C&=Xye+mUG4O)o0d(cPxCy0L&X@f~YM7mS0Gs1> zPoRyDsg^Ah@MBW@9?*SlXJgt7_W1_@e*bx_zD;@B6)SByrJx6pW7rBzWS&7$nbqFo zHp4*GcJFV_z{tFz;|mu}q~FZs!MnoF>m*>=?Av?ojniwwNW%*9U-RlSza+d=hV~8| zS`V*;TP@6uT7&$&T9pz^J?q2)=){jUbAO8i|2^h~!z`RNYTZ1Ud^=PzKZA27>3O?$(PR-c~P=oPA$MpM96B z=^XsvZa#8h@m}HJ>lI-gP-*u?&iM=G%By8pbeS=f^~Y5=94YrIc$2&cF%Vf zv;|K2RdJ(+=7KkEnAu`y%Dp)&VIjSD8uz7s@)58j5T&qjfryr5KGwaq(<8m~QDq_R zVk*@zQGa3P2x0b=rEs0s=^f8yM-H%Qitu}E63S1ltkOFO(rqsnQ}9(||F??=ciGKS zVPeXmI&acD%qkC}bc%S$WBDyu87;_bts)$w2?|Dwxns4YmO&1r1hhSakgH@Da|_>y zDdSV*@-071%al2Kx8p@ zj7geP<2?|#NYo7@s^b~%jc|AYiaF==sIf>8Dp%x}kr~tP+@Cp(awm4c9vcHRGoqsp zZZ{6o0FEa(pxZ7X3LXs%Tun&LIPi+Gbco@W|3d>rd{|3x^n4BV?(Ka&9O|OK-Do-a zm6!SZ>-5{J_@B1u0~KiAJLP(AsM)L2Wk@AFxIIm4(oD(W7Wlb+q?fA5_4sGFWBc`K zU<(-8J~yp?JdYMPz|s(VeVx68`N+3{dx=+#%_*kcF6Ls1Z7qA?s9p32DKeRuW|rtU zDsK1Q9;5W`@eT5iA}tp6@5ZobGV^{FTs*LQpDL8tu!6O+u28`$S( z;9vENgG7Xma^-bE=?47N$|*dSYn0>1z5TybT)}2!e+Mmfmr5-`hw--q{sPpMr6`VK z%JIDshQV<(Ti^IcU4z~x)W%BR&ZijkCO2a(y&uoRcT=!PUQHa*7+Cntka;AFY3UC= zHQDD|pLL$tx|gip0+T+;*;xEg9_eg-XQRwQOyp&(I=`$GuRCBQ^H|qMN+7L%YvS)Y z_X!%6l@G=I+W4eob-h7L9fHviIsKX^yFff)>Hrk73|5%|iVIHFJS9cRhll+i}c%Cd+BT5zn# zK<@?M2jnE3&!>#5@2ppb0+*F0g!@kxPKLYqDkf$NGi&!IHH|MU zYO0T~W___NKC#fppP1zLo0`VOSGo_JHdgabv{at&Fz0QelfE7G9^Li}qgJJaM?Q)& zp5@#w&8Aj?KveHSRB0fpv=CJ~h${V*&4rSYy>KmY+n^OBLh>0w%hBz(Ymh%}PrAHS ze~kGO$L1k>z3uT#@35OvrK94c36^Kj`O*aE=Roukse|fq%n@m++P`hf^M6~Z=f@Vl zw{qu_Hp70itTZ*~kP~-;9bB7d$$T2f#tcZTP1P=qFAbD>#5z zle6BYI_Ca$2xp(R;*8DaT?lqY=8d<#Qd}dEYg#Z{RMe)s)3A{9PSYXrAq#WAWb*M; zr>nzk@Yl(fpU;$NHkuTZM`CVkPHb?ANhiH_=;X4n>L3s{?0SXI$Y)i97fV-bgx62r zbjbfdXjZiLRo;f;m%njcH@8w=3rUXM{&ch)XkZ*er@LrUP`)tdsCz8=$RJ3I>%~u& z|B5=@&r@#%q7!eROS;o9-Gr3+4~m$D2VX{OxaLLg`MqFfe6J}!c$`SPhNCe!_hm2$ z5Av-bW|b~w_6)A#_1U*GSWU9f#VDy8D1DU?pd1-9U`iECj7$iYXHLrEd>mn?{?TJ< zdXQ78NJq9!tswYk(bVPjrRedVSF$?y#V*N%>!8!LB0(3<9PN_DQ@eAbFU}CMZ>fD^ ztAtcy5Veue^h6STy*vcbC=o=H7&1l-(IkP4kw7#_A!DQvO)|(B8ALOhApP=SolH!C z+^XvvY4>;TMEoZO^4qt}9)8&FCtxyiCNz4u<>dN<{Ec}PrFm#$qsi_3xZvVXcl67)kkg(PL4N=%rcfls=?3^j1_tzO2*boi>hAACDc z*xt!@6ONF$>c2r6+XSn$4a{@$ZY7`rTSl*$C$Z_D>@ZCSIA|}aGQekz8R+`E^V?7m zw}Ajuehj@oI!HN@B)%ieu2zmUh6hvhH<>*VK(%8LM~l^@1+RtEhBYtO39HOE-fkDW|;e&9#EeF@~@%U z=4yF8aH@E?ex%6ApVkTPmpBqMyx}B4R4e0Gcd}kTxJJpF_rh4;l^Y?qOABjkNj+{O z6SvW^@khQI{GwIOmubW8V>dL{E`wb+g@RovGX5s1*{oMj-xFPG49eLFQ{~@QG?g9Q z71NViYbrB0j#0K=T)I`gFn7BSmC0tFGo$qVFJ*5Z*jcF20@KmhoJzhuF3G!(=zVsG zZ)22WHL7}*y)n=4F<#3`4jxmAv|$6x&wWc;x;uqD!V@~iDW3wQ4DH@@48FgJL~wcU zS0@Dkx~SjeUtT-rNd=zY^MGACh_JoO|IE0ces2jT6u&p6_{}z9!pR`WdG;OlK;mWm z{tGrO$!u}7PxD#zp}%*~z!HA$BJv}jE#1qL;v6|STc%%|nzpPTZ&Zcu6WZkOlM^f7 zBPvjY^(w-)*l!Uk!5;OJFf+?dF8&QARwY%5q7su)6qXCj#b?ilY!XsJAaM`~l{k$=UK9 zV_Bhtf!m${H=8)%JYtqtq*^rl_%S9fQ^aCn03W2&c6rTDdHmhsZ32x=fv=Ruoo z+gs-$n~Czyude$IyxoS^j7%%7y850|LxLR0o7aEI1{gBc$ffRb~^zU zUMW&E8SX0Ko(b$IeZm2sBmv7lS->4QRFd8Pb)iA#I8lZZ;pbu%`QxSsq{n3Eu8sWF zYgvKELhljZLd~B>tEFTDtyVa25mhcGLfK(pGBL4!76^a$OzVFBUnKhgl&z-r;iIL* zw5;vW6CoQcmodxtvhc`wgMQ>@KQvf3Fz>1O#1o8=GjRMcOB6>ZgCn-_(aPYOUs`hm zN+$230Fs#-*2lPt&sQ(aPFKXgSYCbJl5vYY*_hzV*$M7!>A!LAeZEpdqV*SPkzNg8 zBgXx`Um^|@vdAz^0*}t&)#Tu;cFK7pOX=^X1xndPWXPH2#^qaPGbk04Op60|XPn~a zroQ&A3xPE8ug%NzFE^IYqngHgojUwGg*GH=qB|{WDgx1YIs4LL(j&gNw_NVSSNeQ! zPK?ZU60*13G4%`c7l)EnYjUer`?yhN=L92$6@nvdLeq|oo=YfCa^*Kaq?|*Se1*#3 ztTQ@zkYwNHf~_gv&m!yo&7>1o)oXLvpv9lefi*(yEB@}d=wq+acmYi4qreT#jg=eC zrD2pMCR;P-Z@f7B*5ehtLwe^oqmPOk_R#E3&d^DRH)STG@QUD)^i0wwYVzm6Kr3J!31H2T%U?7mu9tST~FRUI+{- z7oN@VcPb37&;F{l4k@3|maMg_bSa+r7TovD_QoJ+o{QDEAx_ifDT&Zo>&=GC`%JA< z&I`wt*}j@nyCY4r;Qp<^&TsvOj88I!WE%CLvVTPO&+U!&?dNkGvsz)Z6F0BFc>CBz z2ag8?Re6!9?(TF<_CG^9b9Jm}>KX)97y>bCdjPJ)S?g_Azd=VZws5Va|6+f~Hc-~l zH@M^uwh=#o5^q>JHe{$95djkvjem95!Zw7(nBLhpCoOl5ChYg4{0kZaLh2+U#NaL1 z8$xzb0*FcI^U*GnT0*#_XoMD?u_BX%6}jzVh(JUa<8ULsGxrSfXr2a3ZIq~M*FzA9 z0RfGC5cmiJ+#pc#kMR$P0RiFR#t(I|4@Bcc-MS<|;}&SFEYM&PPNHOXGoW+qLrZSYUi8w zm+av@TRwPnGeoIO1fR(Sh5kdBJ{wG*9j5;Xrq2P>e+<*-gz0m^^toaBJTQG; zMQuLMwp`URPAq(tRcL&x`aWYX_URb!tBu8|EfoQVpd{u&FqiK8siM8Ne=gH6Rj(>A zvSP!XVZo|jvHJCXNo&A=oL(qQYl!nb`d;yq`~BjsnYjwC+n(pM5fF z5!?vo^$CJMUO4>opbmX$VxF+G5QERx?Y_mP+i%S}d;b^lk^Lq4zJ!D=kl7hp9PVYNhi9O_LI_Carz#83-_pY;V<9nzvg z`}9^(j4Lz50y&#=R8uAr05)%c|(cBiQ#ctk8zFpb+ z2m@nq-0TfSMQwZuIOWXn_2B>vvZ&<2Dva2avx3B+dqC1h15>k^fH!m%jh^QZmXtwb zENEov3+fUBOK}BD*#b+M*I6YQEdZqlc&ql)zY@~dtL4~JQ ze_lT2RNYk+8;j8!K2T&4PHKxi3BXsV*5qBo0Elm4v0^V>2)x=cIh-6V-*4PU0Uq3j zGY%PjHiG@U2lp<&74oz-KR&y?|Gh5?w@ zt7cV0{52n}yY|PMgvbSM42m86P4yLzLdEor+b7NiKNB_g z$1ip*WjJlkpQxvk*sJM{bE=tS_PjB&>qx}MmaXsoi zjfvV;|5EevQ+rUGpr3lv%Q==NQa{_9l2D=S-&byHiFLDeo(a`&M@fM^O7tWrX}2IZ znzC78$-S0JO*=GByBz-@qJG1QF9k~+kar-tc-t1)?e>qNWa3`??|0v@fB1d2d;y(# zmIE?~Yc%yiG)OlC>F9ir&Ir=S|It-IMakjd@G+fBmV>c68r?Owqsa z-1CZXNRF^dLpWqcSml6rmLky314w7I{}3g%dXco zQ{V*1vQ=Qq0GH+ijz$1bJ?HOitY2+>cW8_D|6a-&{irEmK@a2h*KF~ik;NIlI332v z;P?iqPDJiE+=i6ElMNPdsrA7z@OLN4hX`Iw4aYT-`RE#{UU9xUF_lw_iw(Xr_@0Ut zy?A_+H{rG&rHr#yf{wNM*Q!Pj^%?q%O0n#1^54A6sSgaUV{Xs zM80HQdaqMziaCdSc|?Y5be{~f+U{<(OUr(Fed~hHROIZ~|Ms}%nAkS4|M`>QV4y~Y zg|>VZFbmycb^nrXq@B3kdIG3p7o5}&fD*kMs1ZHZ_x{YyF+(T%D&f!_`Wjlxe>3|v zG^aBGLPu&VZAvx7+|hU(R=?~>VH6b<#33dPV7PB9r()PldJnz zLBFDxj&Ds+kl;Tn%G2vRgZ6$&w@kD-SHl?<@YLZRaqG7N1$R%OeYY?#+gNZrlEIf= z7}sReHqqMe00#q*d>80womZ0uCbY)Bq;98E|G3$UMSBN?O8X90| zEi17{fq{H`#>YLl>4#x9h<}jSH{Dk7%48;G>=&x?*U>=%=BG9fW<3j#EDmrX(2mD$ zgoTIG;*9R%7*+t+p>LLkCkzv&AJda^wkAT84VI>FzuErr+IQs^Jv6NPPgW54xW$W# zTzd@I%#7}Sa9nb1UH<{+*++)I8@_ROKLt|vkx+@^mbJ2vU)~tZB#HtOzQe$UyMdvP zfjlNBX}fNsMHWz>1;WBou8_K?aF4yd4}(-LI7~{^`WJEIFf>>5>)>Jj=QZj!EN$q^DzK#8;a7`eq@7$MFChYli&^epaKf@VuuD4;v== zuHmi@LuhCdIlwo9L1vt_fc9}cOMvvw9Uc=vZx2{M2aZsdbW#aJ0Q7T%V3`iOrl_BC;<;vEf|{lv^0BxqQq#$& zhh2Q5LBpx;Ow(XQz|5e^uu7-t6>r({=yifxmaAdYedvohK|!54&@EA}M3q%;GcsPT z)s$KO46Uq|1t(8!GylWwExTmA*Id&!8-%XWP4Konstt35f+9I=t+__GXT-sUxqV1XH9@eg~d}5b_Cs3(=78SRyi&Tc_ z0$gTlH$#U-hz}k5Y5=c2z?;L{Nr(LwjQy&qEMaK$yVsw1D$xC09+My6ROzg35<4Sn z3VYONBc=}%GlYp5D+-%NvvN8|27cx}}nW}|MwLqr2AX9^o zsY%GxB4p|ZWaO6|VB@Ze(@;*A2V z=OMczX{eOgPvTQ*f>7!t7~(kxjDv>#E;;1I2gG+pm?s;1_h1>`5cQ`>8?|1M@9>Dl zt6pC}jMm5PH^BD$&A2O}yoXPTDY{B~^$#C@BmuERX@HnlJCnfgGe4l}g%msL)Lz5bIArm9yjg>soAyP2yS*rxb z&EJhhYCxTPQGd~lGc@KJFQ3nHYoOu*$!)uUA|OhoV(wEZzzt3p9)_dti;z4`*e?J8 zcYV*R?AJD~0p6XM&aSsBI2DE~Ld}6cROYC<2biV=++^$7ysuW%G;6SP&c$HG!iB0g z@H2A`(AzlpuH}5jdRcDfqV?OX^Q*$zUZ_IgzUEe?Yw@4+y}qC-*C?^MuOkjNt{WG= zK~apX&I1|t=UHKKjFJ1HMmjbyXExPdZm(q+Qnz1Fj#87_AUgvRq}`P0@1; zY+plonsd|lD>LNIE|x`ND!T<@ZxUZI%_mkQ-AvjkRpymQ@AU@nzdpQB@eVppXj?+_ z2dLhGOD|9Q?$NhyJh7m}4m`Mfj+z&Rod;_%T4Cm?moPv?DZ-ah2f0HQzT9cWcz*Jh ztsf|rwR;&ab5-Gb$>tn2RCmnZFd#tsUr@B>Gl!Av`U!Bh4Kmn4$cg>i`XbHWkhqGi z<|kJ7{j0iz)%f20aMpGq=;TPXcPOw>GSkREu0hg3qL?93ERd*skf{5RsBE{6sGnT7 zXyjo}a8JmPHoYCq$}fm! zRK0M_5@&*gV#r^6(s*D z{3FJLM1PQY_J|0NrjY;kXEu;V%R@o9{T!sd0BI(7!yMW3-`@@Us0;E_Kz=#M_bLLp z{2g3)D^#EZ@W-Ay=n*FzC8p*-DCaMz8QJ!{Gf zbw$m&0%G_Pqa2DtAp(EZkRvXpic#dZ77DV@#>sLX#_P+R^@Xn(HPv#+4$bS zO2Oi)VD%D;Vp@(1v~+|1b_|G0H2M{%v^sJfz8oq+&`NqgHV((6W=!DfE9Bx z<`X=L2-RVW3{A^PEFg?rDR?=WM6xioOo9~Cd2s6bDBz%cNpY2gSqbxntY^~bNaQxl zhNZB~X~SzjKeN}cpFB#6>XvLCzxE@)S1?5AuvOeH5WE_EQRTase3_j#fVqwcI%o?z zZSnFt*kW)?;rb>%B`4zj@yj<;cZN+#(tb1-qb>nGMe;P7MR#8VGM&G|Jlz>bGcAo~ zo=&)=6|Jxkej`_Nx7b|9YS6zi>I?SheZY8Qo99t;(B-$JJ4@$(RnW7ETlG)fKguGf zd|IuHQ#*c4pduV!$576UcW{h4JnIwQs#1+eNA@$>Rb>l~%FWFP+pjaeKDzVi_4;?Y z9?U?n*jrzu_je09Htt=EK;X?eDWv7_wZ8RJ{uNn_0!4`wS>-*5K$bbVu{4{p47;f; zyQv(zDU@9l4`LF6(2$2oJY;`FokWKiyv=KfFI{R(bUTlot~MfCzl)YUAw+=o0ltm# z=X3DQCDEu4FLwUOiXwVAtRNp=ba9X;hiKAr;ummh5E$xwzBQUJOtz@}`mS(n&m$`) zy0Q?*h%O0G!Xu*V8AyE(QqdrlSoiZS{z750&(B}qtxbx!%lHD^|3_ERnTHzGGOT>2 za-39DX>41B8Z-ot6my~~ZTrfrz~|hNXoI^Qs`L%kpVGniv`p%F!Pl@VOzNC?vB7U7 z`3xV|{v|cm>f^O~##WnUZD%FJR+|l0M2@XCr-wruk33nP2rT_^t~FgLK3#=81Z-)! zd>^kiF|T3hGwnKZ+OTMY+`DfEa%m%S>GcZ<`X1hz%VnqICi%I3cK(1a0<^O0R}wtV z0|m%H0iAyWil6{JDA4v#AQGgAf|R0v6iSfdL8{W-56`Oj;3&eJkFswS?5rhdfy96XpJSH zpqP&C{%Jm?m%MlGy_!&_pz(du`fYXj*Zw4bSSte<-z|)E3>2d?3zLGlvG)5B)ed3uYkJMkZC%YtYx}1+K9Mxl4eAUhy zVP?<+BDaPOagVc4B`rHt9X+mVr9YcZI1%&c8$MBm>W4@b93`hFYkXyQ~|Z94TBzx?j)hKgNUU13^U%`PNcUB^-O1--_$ zVmlVRNd9}pKoxji3MRKLfsQHs?mRrc0`fd0?% z=~eMI==kVkdY@nGYr21G6O4~)Yc@DLCF`G|)~Bc9w=xI@PSB|zx%$8SSZDo~CcU&z zgA1?r!S&j;U+alm{(6aRP6hsq&|%USrmq`>1ixkn*o9ss|0XiLUKiUh+18n#(9N&f zQH?NxvQEzVE6l9=*I5Yc7t!Zo7@FGtTAI7|!>^m5z}$e*by59p7M(iow)Wp78`k4ianN~7=8P)jjaEI*|LK(J&Jex% z8i}*l1$|_6d6=LzcGk{1{Clx(?_h=XuE%vI{bNC3r|-A1WxGw3!C`TYwEukNtKP$1 z%L;;=g&OI$-0%-Zp0&n%g!X1%uS8mwBF~p&f0cUwi;D8>r-EOaJxrgVV_N;&3|oTF zy2?>F)%bv`$&7$K)zPW%_+$7{L)6D07S6g4F%@JwcgYm!Av(+uod*ydc8CrqM28om zBM8wEh3H5?be=(UZDb0aj$?SwrG7H`$Z8LhDU3sOrpXlMAv()s3hNM^O)`aT zh|V6F!VyI0l%;WR2Bkze>>WPh4>9!=-NSt8hqux~5N`Mo3qtlblI8Kc=C|0} z$YAg25Pt+6kkW6N5Pu}7r@SBLqvc@?iYu=|SrlMKilj%kAQt58ZFI1A0*F7aO4Hd} z7-8?8ApST}Ppv)7A5nmP4M$XyK-Q?(+a4~vQLIw4x3RXcHWgrv}TOxzMYAi_GbfL4t30rGXJ~rp{BC|%O#2+{LYHmKxe@;R@^-TvdYwyG3 z7z3eXp26gZAq@U#l0n>dp+ZL;JK_x}L6iwy6U-^F>Jj&ia*xyK5XIXlFu|7St z>M=%ebQR=WNMj>s&;h+_*?Uvte!==qwgjkMYQNDFcfHX=cI1K`*#6x972j-5(3G__ zYE*QEYvj0T2(`T;Ctk8Fr_SUk4Yf`@Jj2gxrpN$a#y!kf5^Q2D1@cq1fWEo$8zH1y zIk5Lm6T5JXsRny+&BcbBTmf+OvEb&I)ldESbCV{}OB;h5coF5AM&q+w4U=<6zi3~+ z^g>6uBE@K3!7ki{^?%xDxHD8;-bN*1G-oEItxNZ(@TM%Qi1=d`KV^ZF8tT?Ut(X&# zS>(}M!><|m(k#PV&f3u*SKGAou_|>tVBXmDK&H~I)wZN%Dy`b^Yx-HOX_i3w zN_DR-K^yDPvez1Fbx9?~?=n+zc?)23EUWDwRM= zppG`g5Y(c03brAGjayb7UrZNlpVXxk`wrs6FRj;G%cOmks0@1AfdnW)s_o zK>0RIKzB%33Oc+MF|)|Kk7ox=BaC+^pq$lS??v4BOU-smVF_jA3ga-OtwNi}tH_m~gcfAco3d z6RCC=X!3oa2b37i2akGGWDd$Tgi#u7<2cdr>R*lMwugd)kab2g6rL9nPD+#)c&(gi zR;Zn4gSeb^_tM8k$UVta*@UE-rXan>9uw4 z?WcJjs5eTytu+KCX5lzI$3wpaxT_$>e^tY3q_yQ?kEeg?&>wkNNRT z$B$xx98$x;{^|+rn;XLKkXz-$zAEWLBD+OPJim@9Z$kYa0%gj86P|uro9eVvXtzS1Hw3RlfyhdV zju+FjtKH3@^^tGu>Gf872+%!WAc$k>{zmci_%As2lJgtRC;uhoidCqNZNfZ}L0HV) zG@GFx`3A#UNoEMT4)y>6G!iO{smE_uKu=p58~t9@VB0*pL4uCri>PqC$0qwPa8%%! zfM4;o*Y!CRR>C(8Pl!&=2Sx#V7QItmaE6}Jr<0nllLS7o?P9}xj63N%kQWQ@i-X%m zKXo;=^YJk)Vgx3wToK?z9w}U1dh0b)f1u6dpKu52npn6pI3ey88w~H$3m`n@pAM^) z#CV47c_JY<3E*uCy7}K>;#zk}@H?-mefllt`>nQ{YlBdh)e@V>?!uPdOdt%|EJ zbTLU4sdwL}T_cgWD%@M@To;NR6I==Hb|K^06VZ+?K=<5oM-DhEmEyUX*oN=+9}I5+ zN{UN2FTpY#FgCGv1y8F)7RrC{SSa1bI-vh3`y|Zz$3s!mkmk zpp++_=lutw^iQ@JrAB73WKT@o_OC|FuUnINHu0zdsF*QJWh|U19u5nRuhU%vUE4bS z|7J|lDf*lo4%0QG(FI)OEM^Qu~8M5du09Dqp#z5k%MU3goIXS7-dLuI=U z2R{V0Clexe2Ki{HQd2a}s1#R1NAFjOzv|i#{k-rm21z#>`RQUK8@_7Y=R>)}Ah#`@ zz!LMf{w7>;CZojb!&vY}ri7)Mj||+Qz-GVyGTt@yHRnDyIG!Q(&C^$6O;0+mc12H| zR2y)6D;t?kZv<$a4<23ZRk8OXL!z017nUubQE zlas39Y`3vhOu0eL=bejvBa4o_^6AbPadU9G_77rBfjPO0C66y&8$`)c+t zwF|F%?sNHWFV$--+SkP4q?eB8+th&?YIe3+%^{4 zm;xAV!fW~f@Ijpx9IyZ*DT#IqyTFh8PD2aldO~LofBDL(r%*w2cBMRd486~nIH#w+ zvUsT`7#j>^ll@$`cu-j{^i&9G?^gLtuCY#gr;97TO}+W^o~xF3DS1XF80g^8fpxua zDo^!^>Od+kUh8LoMi%y2CNb@-2P`sXwaVHpr`#a0qUDgyun)D(H858w{i5QQ$LvY+ z$H(NqDAOxL2^QwELV3-`@aFRMp3A27u;&Hg24f5q9n``bF*)xR}^9ro>w zOY#Uz@!PxE5n4gk3(*hi&q>pgs0ANiNPHN1(V_YCziH0YwuwGD}O=^Sih%LVcY5o8TS#pW(PLk1pp~LhnEeWux6!aW+bJh{*F6YIAwc$32dFBj^7?%tG%7N!H-p;r*hjb4Yb>GN_W{S8p8!Yc3%LAiqG!fRSK5k z`d}EKbvtN+^MeHjTDL$k@w@}I^D!t@Af@N4!0#KaEdGlCskD8lPpDds{SS8kk}~4k zGjEf|3Ji2YQEzpSz6O-#=qHkYwoJ?aSh65Q0-mXdL%*@#F)`DN4!R%|Ia;qNx*G67 zDCxzk^%g_fn2BAIBPnV;A1wB?)}Ge29B*zA{XhD3)kfcY==NF`%?hHw&yKLFC-1}i zHRbB`)PUDQX?16h47LMUT6ikfwQW0UKaXUerSRG>(F*!v0u9z@rN(L9^fm8ivSpa5 zl%EDrK`1c)74>Y(e(#}xOgC=qN6(;|((D3%QQKFelI=Qt%)fN}{ZDKs{kXNY)xhBF zC@3M56|VC;ob8=BbVBC!EBWh3&9Y(6T=Y2^@66MBj)`fb(NmFRiwV-*O$s9ps6(2g z#Z=BVm$Iw=J9SUon(7EO)w;&HgKjsg?Ap{ysv7EpA4((BT3Z&{!y;q11CUU#tL zsaUK9Wur}oOsD`&nC#3iGM5bMgAC4>Rx1dH4Y7{|EG3}MQ&49f)Vb6G%X|-(xu$o+ z=#m4L34lf2(Y|5yli#)ZK@dmtH*|WSi>#-{#KcNWC+kr#Xp&Og-P$oW!D|KgBqkyAg}Cf@qf|(Bm)oPb#DB)_$VGO za0h*h{zR1Ml_Hnj3+9zoV@-m8Jt z;w?&8XaEmWrw1sO3K?6~K}@J9A+#5)`vWh4gS{aVeHH%Tq{E;1wV@QF0=R6w*$!}` zqW4<9Q&07!0K+xdlg!we-JrLqm^ZJqDZB z8`Qpb5$*) zdW2d={nF1eI^llH=>Kd{CpAh`Cw-{4j8R{)j7dtcjQL5Y9{Xw)%h+F5smFD+S;n1g zsvd7?qaJ@O(lY*!4E2QVzpE#TKI(~2S}hX~om3~UIHMMl&#MJ>85gUd1rdT3QS=LzL3V=QQnU;fNeEU+2$qS6)=3B!N>Mae zDIr)YMbludgkZ6B@xW>c!Ey<~dMU~V3nm0Brf3^1nGmd*qHeHgLa=H=uxyIL!MZ65 z2MZ?zD<=d?r>GpPouYEEctWsxLa=;_(!u&EN(Tl|lnyMQ=p2}Uh-{$f92h~-c`J6^ zkQo%E13M^6Kgj-WWC=y-z!Zwofh`oJ17j#k2i8!O4$Pq_9oR!rIxvW$bYKxh>A)n4 z(t%AR#K0(u&Vf~gz$}W+fn9{aFpAQFWfY|Y(Ilo8%oX6zLiIy=g=-nVI}B$x*`WAmea7@!v{1lS~&qm##T;jp|!DSY_!q=3=R>Ejt~sbqibft zP?fMm2}`^hmQ-e;IZ- z&YPC$o&Ddmx}XyE{~#s)uPpgLU+1w3;~7hG_NuDJpTG0LIqgrMW6TDxRpE8H^PZ{N z=quvkD{e$07ad=wgp%u6SX-Y6xk()mBsm}$;SsWkep z(*`U3KPz(l2V~~w8w)w_jK5aHm)!qLx&Pan`tR%fazCz(`oH|*Q!Q(z8*x3u_3J;R z7UeW?s_n{V7O8Goe!aWq2nq=*!GDcbtMI>)@Sk@c z*FpZT<|l7o8$4TFd*Tnn`bOKh^)Cn7H~jpWxbe|RhE1m$bI(qH)V}%kC%NawwyTSDtys@apFy zxg85m+F#rMG57l9vGzAM)e(0_KV*1wNd)&+m)-VVQwl^|&7Tar<9-u&e>2Olr)L1S z=jikHef0!!-_}nJ`yDH|1JXVAgD0AChs=-L-`O4~9!@xFIP!?d9Sz~^T$xiM*N;%Z zX?3MUP<5hX63VD>H!h&N!r_HP#EVG?7ZedMDj{50M7+3&c!3e|A|v93rd|VZu@Uit zBjQD;-UD#qsrLX}eCj;_13Imb z9Bf1!aJsV&2A%G#gMp{Tq%iojnAB4S@Xz);H|l>7|9eUQOUwUgblCrMVERl`fujEv z;r~-IQ>PenE6ezY@;aT;|EnVB|1kPF9=X1EGXDz>DzW}Am{;PzN)rE7b&vm+Uldat zOrC0L*y)^rEm1hGE=|a+k^8zZD@)4pDrj^;lCI8@7@Bfo%6Uk@*f*pJQ+R1@#Kd+OsjS-Gd*RCajahQ zr)iC)o5LckcRVfDc0ALwfn)8gILEq>|2Wp|sAk%Ky_)Gj((B@Zk84_}C&&XF$9jTv zFufmJ-E`sD3GVWEtE0+{-45T#gN{1ye#q5*KFU#V##m03F-Fy3TBNDr}iMy0$6L)yM z+PkioyOd=e94xyx2El7c2-gu2uO%X0PnsUWHKo>aa9yeR0oN80uP-8AW12R?bwP#(%dc_= '3': - from inspect import getfullargspec - - def get_init(cls): - return cls.__init__ -else: - FullArgSpec = collections.namedtuple( - 'FullArgSpec', 'args varargs varkw defaults ' - 'kwonlyargs kwonlydefaults annotations') - - def getfullargspec(f): - "A quick and dirty replacement for getfullargspec for Python 2.X" - return FullArgSpec._make(inspect.getargspec(f) + ([], None, {})) - - def get_init(cls): - return cls.__init__.__func__ - -try: - iscoroutinefunction = inspect.iscoroutinefunction -except AttributeError: - # let's assume there are no coroutine functions in old Python - def iscoroutinefunction(f): - return False -try: - from inspect import isgeneratorfunction -except ImportError: - # assume no generator function in old Python versions - def isgeneratorfunction(caller): - return False +from contextlib import _GeneratorContextManager +from inspect import getfullargspec, iscoroutinefunction, isgeneratorfunction +__version__ = '5.1.1' DEF = re.compile(r'\s*def\s*([_\w][_\w\d]*)\s*\(') +POS = inspect.Parameter.POSITIONAL_OR_KEYWORD +EMPTY = inspect.Parameter.empty -# basic functionality +# this is not used anymore in the core, but kept for backward compatibility class FunctionMaker(object): """ An object with the ability to create functions with a given signature. @@ -100,7 +71,7 @@ class FunctionMaker(object): self.name = '_lambda_' self.doc = func.__doc__ self.module = func.__module__ - if inspect.isfunction(func): + if inspect.isroutine(func): argspec = getfullargspec(func) self.annotations = getattr(func, '__annotations__', {}) for a in ('args', 'varargs', 'varkw', 'defaults', 'kwonlyargs', @@ -143,7 +114,9 @@ class FunctionMaker(object): raise TypeError('You are decorating a non function: %s' % func) def update(self, func, **kw): - "Update the signature of func with the data in self" + """ + Update the signature of func with the data in self + """ func.__name__ = self.name func.__doc__ = getattr(self, 'doc', None) func.__dict__ = getattr(self, 'dict', {}) @@ -160,7 +133,9 @@ class FunctionMaker(object): func.__dict__.update(kw) def make(self, src_templ, evaldict=None, addsource=False, **attrs): - "Make a new function from a given template and update the signature" + """ + Make a new function from a given template and update the signature + """ src = src_templ % vars(self) # expand name and signature evaldict = evaldict or {} mo = DEF.search(src) @@ -179,8 +154,7 @@ class FunctionMaker(object): # Ensure each generated function has a unique filename for profilers # (such as cProfile) that depend on the tuple of (, # , ) being unique. - filename = '<%s:decorator-gen-%d>' % ( - __file__, next(self._compile_count)) + filename = '' % next(self._compile_count) try: code = compile(src, filename, 'single') exec(code, evaldict) @@ -222,106 +196,128 @@ class FunctionMaker(object): return self.make(body, evaldict, addsource, **attrs) -def decorate(func, caller, extras=()): +def fix(args, kwargs, sig): """ - decorate(func, caller) decorates a function using a caller. - If the caller is a generator function, the resulting function - will be a generator function. + Fix args and kwargs to be consistent with the signature """ - evaldict = dict(_call_=caller, _func_=func) - es = '' - for i, extra in enumerate(extras): - ex = '_e%d_' % i - evaldict[ex] = extra - es += ex + ', ' + ba = sig.bind(*args, **kwargs) + ba.apply_defaults() # needed for test_dan_schult + return ba.args, ba.kwargs - if '3.5' <= sys.version < '3.6': - # with Python 3.5 isgeneratorfunction returns True for all coroutines - # however we know that it is NOT possible to have a generator - # coroutine in python 3.5: PEP525 was not there yet - generatorcaller = isgeneratorfunction( - caller) and not iscoroutinefunction(caller) + +def decorate(func, caller, extras=(), kwsyntax=False): + """ + Decorates a function/generator/coroutine using a caller. + If kwsyntax is True calling the decorated functions with keyword + syntax will pass the named arguments inside the ``kw`` dictionary, + even if such argument are positional, similarly to what functools.wraps + does. By default kwsyntax is False and the the arguments are untouched. + """ + sig = inspect.signature(func) + if iscoroutinefunction(caller): + async def fun(*args, **kw): + if not kwsyntax: + args, kw = fix(args, kw, sig) + return await caller(func, *(extras + args), **kw) + elif isgeneratorfunction(caller): + def fun(*args, **kw): + if not kwsyntax: + args, kw = fix(args, kw, sig) + for res in caller(func, *(extras + args), **kw): + yield res else: - generatorcaller = isgeneratorfunction(caller) - if generatorcaller: - fun = FunctionMaker.create( - func, "for res in _call_(_func_, %s%%(shortsignature)s):\n" - " yield res" % es, evaldict, __wrapped__=func) - else: - fun = FunctionMaker.create( - func, "return _call_(_func_, %s%%(shortsignature)s)" % es, - evaldict, __wrapped__=func) - if hasattr(func, '__qualname__'): - fun.__qualname__ = func.__qualname__ + def fun(*args, **kw): + if not kwsyntax: + args, kw = fix(args, kw, sig) + return caller(func, *(extras + args), **kw) + fun.__name__ = func.__name__ + fun.__doc__ = func.__doc__ + fun.__wrapped__ = func + fun.__signature__ = sig + fun.__qualname__ = func.__qualname__ + # builtin functions like defaultdict.__setitem__ lack many attributes + try: + fun.__defaults__ = func.__defaults__ + except AttributeError: + pass + try: + fun.__kwdefaults__ = func.__kwdefaults__ + except AttributeError: + pass + try: + fun.__annotations__ = func.__annotations__ + except AttributeError: + pass + try: + fun.__module__ = func.__module__ + except AttributeError: + pass + try: + fun.__dict__.update(func.__dict__) + except AttributeError: + pass return fun -def decorator(caller, _func=None): - """decorator(caller) converts a caller function into a decorator""" +def decoratorx(caller): + """ + A version of "decorator" implemented via "exec" and not via the + Signature object. Use this if you are want to preserve the `.__code__` + object properties (https://github.com/micheles/decorator/issues/129). + """ + def dec(func): + return FunctionMaker.create( + func, + "return _call_(_func_, %(shortsignature)s)", + dict(_call_=caller, _func_=func), + __wrapped__=func, __qualname__=func.__qualname__) + return dec + + +def decorator(caller, _func=None, kwsyntax=False): + """ + decorator(caller) converts a caller function into a decorator + """ if _func is not None: # return a decorated function # this is obsolete behavior; you should use decorate instead - return decorate(_func, caller) + return decorate(_func, caller, (), kwsyntax) # else return a decorator function - defaultargs, defaults = '', () - if inspect.isclass(caller): - name = caller.__name__.lower() - doc = 'decorator(%s) converts functions/generators into ' \ - 'factories of %s objects' % (caller.__name__, caller.__name__) - elif inspect.isfunction(caller): - if caller.__name__ == '': - name = '_lambda_' + sig = inspect.signature(caller) + dec_params = [p for p in sig.parameters.values() if p.kind is POS] + + def dec(func=None, *args, **kw): + na = len(args) + 1 + extras = args + tuple(kw.get(p.name, p.default) + for p in dec_params[na:] + if p.default is not EMPTY) + if func is None: + return lambda func: decorate(func, caller, extras, kwsyntax) else: - name = caller.__name__ - doc = caller.__doc__ - nargs = caller.__code__.co_argcount - ndefs = len(caller.__defaults__ or ()) - defaultargs = ', '.join(caller.__code__.co_varnames[nargs-ndefs:nargs]) - if defaultargs: - defaultargs += ',' - defaults = caller.__defaults__ - else: # assume caller is an object with a __call__ method - name = caller.__class__.__name__.lower() - doc = caller.__call__.__doc__ - evaldict = dict(_call=caller, _decorate_=decorate) - dec = FunctionMaker.create( - '%s(func, %s)' % (name, defaultargs), - 'if func is None: return lambda func: _decorate_(func, _call, (%s))\n' - 'return _decorate_(func, _call, (%s))' % (defaultargs, defaultargs), - evaldict, doc=doc, module=caller.__module__, __wrapped__=caller) - if defaults: - dec.__defaults__ = (None,) + defaults + return decorate(func, caller, extras, kwsyntax) + dec.__signature__ = sig.replace(parameters=dec_params) + dec.__name__ = caller.__name__ + dec.__doc__ = caller.__doc__ + dec.__wrapped__ = caller + dec.__qualname__ = caller.__qualname__ + dec.__kwdefaults__ = getattr(caller, '__kwdefaults__', None) + dec.__dict__.update(caller.__dict__) return dec # ####################### contextmanager ####################### # -try: # Python >= 3.2 - from contextlib import _GeneratorContextManager -except ImportError: # Python >= 2.5 - from contextlib import GeneratorContextManager as _GeneratorContextManager - class ContextManager(_GeneratorContextManager): + def __init__(self, g, *a, **k): + _GeneratorContextManager.__init__(self, g, a, k) + def __call__(self, func): - """Context manager decorator""" - return FunctionMaker.create( - func, "with _self_: return _func_(%(shortsignature)s)", - dict(_self_=self, _func_=func), __wrapped__=func) + def caller(f, *a, **k): + with self.__class__(self.func, *self.args, **self.kwds): + return f(*a, **k) + return decorate(func, caller) -init = getfullargspec(_GeneratorContextManager.__init__) -n_args = len(init.args) -if n_args == 2 and not init.varargs: # (self, genobj) Python 2.7 - def __init__(self, g, *a, **k): - return _GeneratorContextManager.__init__(self, g(*a, **k)) - ContextManager.__init__ = __init__ -elif n_args == 2 and init.varargs: # (self, gen, *a, **k) Python 3.4 - pass -elif n_args == 4: # (self, gen, args, kwds) Python 3.5 - def __init__(self, g, *a, **k): - return _GeneratorContextManager.__init__(self, g, a, k) - ContextManager.__init__ = __init__ - _contextmanager = decorator(ContextManager) diff --git a/libs/deep_translator/__init__.py b/libs/deep_translator/__init__.py index 789ed970a..5b6100786 100644 --- a/libs/deep_translator/__init__.py +++ b/libs/deep_translator/__init__.py @@ -1,6 +1,5 @@ """Top-level package for Deep Translator""" -# TODO: Discussion: Do these need to be in __init__.py? Are they intended to be exportable? from .google_trans import GoogleTranslator from .pons import PonsTranslator from .linguee import LingueeTranslator @@ -11,11 +10,11 @@ from .deepl import DeepL from .detection import single_detection, batch_detection from .microsoft import MicrosoftTranslator from .papago import PapagoTranslator +from .libre import LibreTranslator -# TODO: Discussion: These should be declared in setup.cfg, setting them here is redundant __author__ = """Nidhal Baccouri""" __email__ = 'nidhalbacc@gmail.com' -__version__ = '1.5.0' +__version__ = '1.6.1' __all__ = [ "GoogleTranslator", @@ -26,6 +25,8 @@ __all__ = [ "MicrosoftTranslator", "QCRI", "DeepL", + "LibreTranslator", + "PapagoTranslator", "single_detection", "batch_detection" - ] + ] diff --git a/libs/deep_translator/constants.py b/libs/deep_translator/constants.py index 8fe51b1e4..dca6ed41a 100644 --- a/libs/deep_translator/constants.py +++ b/libs/deep_translator/constants.py @@ -11,7 +11,9 @@ BASE_URLS = { "DEEPL_FREE": "https://api-free.deepl.com/v2/", "MICROSOFT_TRANSLATE": "https://api.cognitive.microsofttranslator.com/translate?api-version=3.0", "PAPAGO": "https://papago.naver.com/", - "PAPAGO_API": "https://openapi.naver.com/v1/papago/n2mt" + "PAPAGO_API": "https://openapi.naver.com/v1/papago/n2mt", + "LIBRE": "https://libretranslate.com/", + "LIBRE_FREE": "https://libretranslate.de/", } GOOGLE_CODES_TO_LANGUAGES = { @@ -29,9 +31,8 @@ GOOGLE_CODES_TO_LANGUAGES = { 'ca': 'catalan', 'ceb': 'cebuano', 'ny': 'chichewa', - 'zh': 'chinese', - 'zh-cn': 'chinese (simplified)', - 'zh-tw': 'chinese (traditional)', + 'zh-CN': 'chinese (simplified)', + 'zh-TW': 'chinese (traditional)', 'co': 'corsican', 'hr': 'croatian', 'cs': 'czech', @@ -66,8 +67,9 @@ GOOGLE_CODES_TO_LANGUAGES = { 'kn': 'kannada', 'kk': 'kazakh', 'km': 'khmer', + 'rw': 'kinyarwanda', 'ko': 'korean', - 'ku': 'kurdish (kurmanji)', + 'ku': 'kurdish', 'ky': 'kyrgyz', 'lo': 'lao', 'la': 'latin', @@ -82,9 +84,10 @@ GOOGLE_CODES_TO_LANGUAGES = { 'mi': 'maori', 'mr': 'marathi', 'mn': 'mongolian', - 'my': 'myanmar (burmese)', + 'my': 'myanmar', 'ne': 'nepali', 'no': 'norwegian', + 'or': 'odia', 'ps': 'pashto', 'fa': 'persian', 'pl': 'polish', @@ -108,11 +111,14 @@ GOOGLE_CODES_TO_LANGUAGES = { 'sv': 'swedish', 'tg': 'tajik', 'ta': 'tamil', + 'tt': 'tatar', 'te': 'telugu', 'th': 'thai', 'tr': 'turkish', + 'tk': 'turkmen', 'uk': 'ukrainian', 'ur': 'urdu', + 'ug': 'uyghur', 'uz': 'uzbek', 'vi': 'vietnamese', 'cy': 'welsh', @@ -120,12 +126,18 @@ GOOGLE_CODES_TO_LANGUAGES = { 'yi': 'yiddish', 'yo': 'yoruba', 'zu': 'zulu', - 'fil': 'Filipino', - 'he': 'Hebrew' } GOOGLE_LANGUAGES_TO_CODES = {v: k for k, v in GOOGLE_CODES_TO_LANGUAGES.items()} +# This dictionary maps the primary name of language to its secondary names in list manner (if any) +GOOGLE_LANGUAGES_SECONDARY_NAMES = { + 'myanmar': ['burmese'], + 'odia': ['oriya'], + 'kurdish': ['kurmanji'] +} + + PONS_CODES_TO_LANGUAGES = { 'ar': 'arabic', 'bg': 'bulgarian', @@ -247,4 +259,28 @@ QCRI_CODE_TO_LANGUAGE = { QCRI_LANGUAGE_TO_CODE = { v: k for k, v in QCRI_CODE_TO_LANGUAGE.items() -} \ No newline at end of file +} + +LIBRE_CODES_TO_LANGUAGES = { + 'en': 'English', + 'ar': 'Arabic', + 'zh': 'Chinese', + 'fr': 'French', + 'de': 'German', + 'hi': 'Hindi', + 'id': 'Indonesian', + 'ga': 'Irish', + 'it': 'Italian', + 'ja': 'Japanese', + 'ko': 'Korean', + 'pl': 'Polish', + 'pt': 'Portuguese', + 'ru': 'Russian', + 'es': 'Spanish', + 'tr': 'Turkish', + 'vi': 'Vietnamese' +} + +LIBRE_LANGUAGES_TO_CODES = { + v: k for k, v in LIBRE_CODES_TO_LANGUAGES.items() +} diff --git a/libs/deep_translator/exceptions.py b/libs/deep_translator/exceptions.py index 67b7958e8..c2e174b07 100644 --- a/libs/deep_translator/exceptions.py +++ b/libs/deep_translator/exceptions.py @@ -123,12 +123,15 @@ class ServerException(Exception): Default YandexTranslate exception from the official website """ errors = { + 400: "ERR_BAD_REQUEST", 401: "ERR_KEY_INVALID", 402: "ERR_KEY_BLOCKED", 403: "ERR_DAILY_REQ_LIMIT_EXCEEDED", 404: "ERR_DAILY_CHAR_LIMIT_EXCEEDED", 413: "ERR_TEXT_TOO_LONG", + 429: "ERR_TOO_MANY_REQUESTS", 422: "ERR_UNPROCESSABLE_TEXT", + 500: "ERR_INTERNAL_SERVER_ERROR", 501: "ERR_LANG_NOT_SUPPORTED", 503: "ERR_SERVICE_NOT_AVAIBLE", } diff --git a/libs/deep_translator/google_trans.py b/libs/deep_translator/google_trans.py index e27926be2..3c8c42b14 100644 --- a/libs/deep_translator/google_trans.py +++ b/libs/deep_translator/google_trans.py @@ -2,7 +2,7 @@ google translator API """ -from .constants import BASE_URLS, GOOGLE_LANGUAGES_TO_CODES +from .constants import BASE_URLS, GOOGLE_LANGUAGES_TO_CODES, GOOGLE_LANGUAGES_SECONDARY_NAMES from .exceptions import TooManyRequests, LanguageNotSupportedException, TranslationNotFound, NotValidPayload, RequestError from .parent import BaseTranslator from bs4 import BeautifulSoup @@ -27,8 +27,19 @@ class GoogleTranslator(BaseTranslator): self.__base_url = BASE_URLS.get("GOOGLE_TRANSLATE") self.proxies = proxies - if self.is_language_supported(source, target): - self._source, self._target = self._map_language_to_code(source.lower(), target.lower()) + # code snipppet that converts the language into lower-case and skip lower-case conversion for abbreviations + # since abbreviations like zh-CN if converted to lower-case will result into error + ####################################### + source_lower = source + target_lower = target + if not source in self._languages.values(): + source_lower=source.lower() + if not target in self._languages.values(): + target_lower=target.lower() + ####################################### + + if self.is_language_supported(source_lower, target_lower): + self._source, self._target = self._map_language_to_code(source_lower, target_lower) super(GoogleTranslator, self).__init__(base_url=self.__base_url, source=self._source, @@ -51,6 +62,17 @@ class GoogleTranslator(BaseTranslator): """ return GoogleTranslator.supported_languages if not as_dict else GoogleTranslator._languages + def is_secondary(self, lang): + """ + Function to check if lang is a secondary name of any primary language + @param lang: language name + @return: primary name of a language if found otherwise False + """ + for primary_name, secondary_names in GOOGLE_LANGUAGES_SECONDARY_NAMES.items(): + if lang in secondary_names: + return primary_name + return False + def _map_language_to_code(self, *languages): """ map language to its corresponding code (abbreviation) if the language was passed by its full name by the user @@ -63,7 +85,7 @@ class GoogleTranslator(BaseTranslator): elif language in self._languages.keys(): yield self._languages[language] else: - raise LanguageNotSupportedException(language) + yield self._languages[self.is_secondary(language)] def is_language_supported(self, *languages): """ @@ -74,7 +96,8 @@ class GoogleTranslator(BaseTranslator): for lang in languages: if lang != 'auto' and lang not in self._languages.keys(): if lang != 'auto' and lang not in self._languages.values(): - raise LanguageNotSupportedException(lang) + if not self.is_secondary(lang): + raise LanguageNotSupportedException(lang) return True def translate(self, text, **kwargs): @@ -129,7 +152,7 @@ class GoogleTranslator(BaseTranslator): @return: str """ try: - with open(path) as f: + with open(path, 'r', encoding='utf-8') as f: text = f.read().strip() return self.translate(text) except Exception as e: @@ -168,22 +191,10 @@ class GoogleTranslator(BaseTranslator): """ if not batch: raise Exception("Enter your text list that you want to translate") - - print("Please wait.. This may take a couple of seconds because deep_translator sleeps " - "for two seconds after each request in order to not spam the google server.") arr = [] for i, text in enumerate(batch): translated = self.translate(text, **kwargs) arr.append(translated) - print("sentence number ", i+1, " has been translated successfully") - sleep(2) - return arr - - -if __name__ == '__main__': - translator = GoogleTranslator(source='ru', target='uk') - t = translator.translate("Я разработчик") # => "I am a developer" - print(t) diff --git a/libs/deep_translator/libre.py b/libs/deep_translator/libre.py new file mode 100644 index 000000000..b4c25330d --- /dev/null +++ b/libs/deep_translator/libre.py @@ -0,0 +1,137 @@ +""" +LibreTranslate API +""" + +import requests +from .parent import BaseTranslator +from .constants import BASE_URLS,LIBRE_LANGUAGES_TO_CODES, LIBRE_CODES_TO_LANGUAGES +from .exceptions import (ServerException, + TranslationNotFound, + LanguageNotSupportedException, + AuthorizationException, + NotValidPayload) + + +class LibreTranslator(BaseTranslator): + """ + class that wraps functions, which use libre translator under the hood to translate text(s) + """ + _languages = LIBRE_LANGUAGES_TO_CODES + supported_languages = list(_languages.keys()) + + def __init__(self,source="auto", target="en", base_url = BASE_URLS.get("LIBRE_FREE"), api_key=None, **kwargs): + """ + @param source: source language to translate from + List of LibreTranslate nedpoints can be found at : https://github.com/LibreTranslate/LibreTranslate#mirrors + Some require an API key + @param target: target language to translate to + """ + if base_url == BASE_URLS.get("LIBRE") and not api_key: + raise ServerException(401) + self.__base_url = base_url + self.api_key = api_key + if source == "auto": + self.source = "auto" + else: + self.source = self._map_language_to_code(source) + self.target = self._map_language_to_code(target) + + + @staticmethod + def get_supported_languages(as_dict=False, **kwargs): + """ + return the supported languages by the libre translator + @param as_dict: if True, the languages will be returned as a dictionary mapping languages to their abbreviations + @return: list or dict + """ + return [*LibreTranslator._languages.keys()] if not as_dict else LibreTranslator._languages + + def _map_language_to_code(self, language, **kwargs): + """ + map language to its corresponding code (abbreviation) if the language was passed by its full name by the user + @param language: a string for 1 language + @return: mapped value of the language or raise an exception if the language is not supported + """ + if language in self._languages.keys(): + return self._languages[language] + elif language in self._languages.values(): + return language + raise LanguageNotSupportedException(language) + + def _is_language_supported(self, language, **kwargs): + """ + check if the language is supported by the translator + @param language: a string for 1 language + @return: bool or raise an Exception + """ + if language == 'auto' or language in self._languages.keys() or language in self._languages.values(): + return True + else: + raise LanguageNotSupportedException(language) + + def translate(self, text, **kwargs): + """ + function that uses microsoft translate to translate a text + @param text: desired text to translate + @return: str: translated text + """ + # Create the request parameters. + if type(text) != str or text == "": + raise NotValidPayload(text) + + translate_endpoint = 'translate' + params = { + "q": text, + "source": self.source, + "target": self.target, + "format": 'text' + } + # Add API Key if required + if self.api_key: + params["api_key"] = self.api_key + # Do the request and check the connection. + try: + response = requests.post(self.__base_url + translate_endpoint, params=params) + except ConnectionError: + raise ServerException(503) + # If the answer is not success, raise server exception. + + if response.status_code == 403: + raise AuthorizationException(self.api_key) + elif response.status_code != 200: + raise ServerException(response.status_code) + # Get the response and check is not empty. + res = response.json() + if not res: + raise TranslationNotFound(text) + # Process and return the response. + return res['translatedText'] + + def translate_file(self, path, **kwargs): + """ + translate directly from file + @param path: path to the target file + @type path: str + @param kwargs: additional args + @return: str + """ + try: + with open(path, 'r', encoding='utf-8') as f: + text = f.read().strip() + return self.translate(text) + except Exception as e: + raise e + + def translate_batch(self, batch=None, **kwargs): + """ + translate a list of texts + @param batch: list of texts you want to translate + @return: list of translations + """ + if not batch: + raise Exception("Enter your text list that you want to translate") + arr = [] + for i, text in enumerate(batch): + translated = self.translate(text, **kwargs) + arr.append(translated) + return arr diff --git a/libs/deep_translator/main.py b/libs/deep_translator/main.py index 0b4af671a..17f2ab805 100644 --- a/libs/deep_translator/main.py +++ b/libs/deep_translator/main.py @@ -10,6 +10,7 @@ from .pons import PonsTranslator from .yandex import YandexTranslator from .microsoft import MicrosoftTranslator from .papago import PapagoTranslator +from .libre import LibreTranslator CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"]) @click.group() @@ -64,6 +65,11 @@ def translate(translator, source, target, text, api_key): source=source, target=target, api_key=api_key) + elif translator == "libre": + translator= LibreTranslator( + source=source, + target=target + ) else: raise AttributeError("The given translator is not supported.") @@ -105,6 +111,8 @@ def languages(translator, api_key): translator = MicrosoftTranslator(api_key=api_key) elif translator == "papago": translator = PapagoTranslator(api_key=api_key) + elif translator == "libre": + translator = LibreTranslator else: raise AttributeError("The given translator is not supported.") @@ -117,7 +125,7 @@ def languages(translator, api_key): @cli.command() def list(): """Lists available translators.""" - click.echo("Available translators include: Google, MyMemory, QCRI, Linguee, Pons, Yandex, Microsoft (Bing), and Papago.") + click.echo("Available translators include: Google, MyMemory, QCRI, Linguee, Pons, Yandex, Microsoft (Bing), Papago and LibreTranslate.") return 0 if __name__ == "__main__": diff --git a/libs/deep_translator/microsoft.py b/libs/deep_translator/microsoft.py index 415d8222b..5a0aca795 100644 --- a/libs/deep_translator/microsoft.py +++ b/libs/deep_translator/microsoft.py @@ -131,7 +131,7 @@ class MicrosoftTranslator: @return: translated text """ try: - with open(path) as f: + with open(path, 'r', encoding='utf-8') as f: text = f.read().strip() return self.translate(text) except Exception as e: diff --git a/libs/deep_translator/mymemory.py b/libs/deep_translator/mymemory.py index 0c3ab1ca7..8575c48ed 100644 --- a/libs/deep_translator/mymemory.py +++ b/libs/deep_translator/mymemory.py @@ -151,7 +151,7 @@ class MyMemoryTranslator(BaseTranslator): @return: str """ try: - with open(path) as f: + with open(path, 'r', encoding='utf-8') as f: text = f.read().strip() return self.translate(text=text) diff --git a/libs/deep_translator/papago.py b/libs/deep_translator/papago.py index 6bf900890..7cb5a8a29 100644 --- a/libs/deep_translator/papago.py +++ b/libs/deep_translator/papago.py @@ -105,7 +105,7 @@ class PapagoTranslator(object): @return: str """ try: - with open(path) as f: + with open(path, 'r', encoding='utf-8') as f: text = f.read().strip() return self.translate(text) except Exception as e: diff --git a/libs/deep_translator/parent.py b/libs/deep_translator/parent.py index 35cc94975..440492e8d 100644 --- a/libs/deep_translator/parent.py +++ b/libs/deep_translator/parent.py @@ -3,6 +3,8 @@ from .exceptions import NotValidPayload, NotValidLength, InvalidSourceOrTargetLanguage from abc import ABC, abstractmethod import string + + class BaseTranslator(ABC): """ Abstract class that serve as a parent translator for other different translators diff --git a/libs/deep_translator/qcri.py b/libs/deep_translator/qcri.py index 0435c8bc9..6f2bdf91b 100644 --- a/libs/deep_translator/qcri.py +++ b/libs/deep_translator/qcri.py @@ -3,6 +3,7 @@ import requests from .constants import BASE_URLS, QCRI_LANGUAGE_TO_CODE from .exceptions import (ServerException, TranslationNotFound) + class QCRI(object): """ class that wraps functions, which use the QRCI translator under the hood to translate word(s) diff --git a/libs/deep_translator/yandex.py b/libs/deep_translator/yandex.py index 35b525aad..c6bd6ad8c 100644 --- a/libs/deep_translator/yandex.py +++ b/libs/deep_translator/yandex.py @@ -119,7 +119,7 @@ class YandexTranslator(object): @return: translated text """ try: - with open(path) as f: + with open(path, 'r', encoding='utf-8') as f: text = f.read() return self.translate(text) diff --git a/libs/dns/__init__.py b/libs/dns/__init__.py index c848e4858..0473ca175 100644 --- a/libs/dns/__init__.py +++ b/libs/dns/__init__.py @@ -1,3 +1,5 @@ +# 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 @@ -16,13 +18,16 @@ """dnspython DNS toolkit""" __all__ = [ + 'asyncbackend', + 'asyncquery', + 'asyncresolver', 'dnssec', 'e164', 'edns', 'entropy', 'exception', 'flags', - 'hash', + 'immutable', 'inet', 'ipv4', 'ipv6', @@ -41,14 +46,21 @@ __all__ = [ 'resolver', 'reversename', 'rrset', + 'serial', 'set', 'tokenizer', + 'transaction', 'tsig', 'tsigkeyring', 'ttl', 'rdtypes', 'update', 'version', - 'wiredata', + 'versioned', + 'wire', + 'xfr', 'zone', + 'zonefile', ] + +from dns.version import version as __version__ # noqa diff --git a/libs/dns/_asyncbackend.py b/libs/dns/_asyncbackend.py new file mode 100644 index 000000000..1f3a82871 --- /dev/null +++ b/libs/dns/_asyncbackend.py @@ -0,0 +1,69 @@ +# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license + +# This is a nullcontext for both sync and async. 3.7 has a nullcontext, +# but it is only for sync use. + +class NullContext: + def __init__(self, enter_result=None): + self.enter_result = enter_result + + def __enter__(self): + return self.enter_result + + def __exit__(self, exc_type, exc_value, traceback): + pass + + async def __aenter__(self): + return self.enter_result + + async def __aexit__(self, exc_type, exc_value, traceback): + pass + + +# These are declared here so backends can import them without creating +# circular dependencies with dns.asyncbackend. + +class Socket: # pragma: no cover + async def close(self): + pass + + async def getpeername(self): + raise NotImplementedError + + async def getsockname(self): + raise NotImplementedError + + async def __aenter__(self): + return self + + async def __aexit__(self, exc_type, exc_value, traceback): + await self.close() + + +class DatagramSocket(Socket): # pragma: no cover + async def sendto(self, what, destination, timeout): + raise NotImplementedError + + async def recvfrom(self, size, timeout): + raise NotImplementedError + + +class StreamSocket(Socket): # pragma: no cover + async def sendall(self, what, timeout): + raise NotImplementedError + + async def recv(self, size, timeout): + raise NotImplementedError + + +class Backend: # pragma: no cover + def name(self): + return 'unknown' + + async def make_socket(self, af, socktype, proto=0, + source=None, destination=None, timeout=None, + ssl_context=None, server_hostname=None): + raise NotImplementedError + + def datagram_connection_required(self): + return False diff --git a/libs/dns/_asyncio_backend.py b/libs/dns/_asyncio_backend.py new file mode 100644 index 000000000..d737d13c7 --- /dev/null +++ b/libs/dns/_asyncio_backend.py @@ -0,0 +1,149 @@ +# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license + +"""asyncio library query support""" + +import socket +import asyncio +import sys + +import dns._asyncbackend +import dns.exception + + +_is_win32 = sys.platform == 'win32' + +def _get_running_loop(): + try: + return asyncio.get_running_loop() + except AttributeError: # pragma: no cover + return asyncio.get_event_loop() + + +class _DatagramProtocol: + def __init__(self): + self.transport = None + self.recvfrom = None + + def connection_made(self, transport): + self.transport = transport + + def datagram_received(self, data, addr): + if self.recvfrom and not self.recvfrom.done(): + self.recvfrom.set_result((data, addr)) + self.recvfrom = None + + def error_received(self, exc): # pragma: no cover + if self.recvfrom and not self.recvfrom.done(): + self.recvfrom.set_exception(exc) + + def connection_lost(self, exc): + if self.recvfrom and not self.recvfrom.done(): + self.recvfrom.set_exception(exc) + + def close(self): + self.transport.close() + + +async def _maybe_wait_for(awaitable, timeout): + if timeout: + try: + return await asyncio.wait_for(awaitable, timeout) + except asyncio.TimeoutError: + raise dns.exception.Timeout(timeout=timeout) + else: + return await awaitable + + +class DatagramSocket(dns._asyncbackend.DatagramSocket): + def __init__(self, family, transport, protocol): + self.family = family + self.transport = transport + self.protocol = protocol + + async def sendto(self, what, destination, timeout): # pragma: no cover + # no timeout for asyncio sendto + self.transport.sendto(what, destination) + + async def recvfrom(self, size, timeout): + # ignore size as there's no way I know to tell protocol about it + done = _get_running_loop().create_future() + assert self.protocol.recvfrom is None + self.protocol.recvfrom = done + await _maybe_wait_for(done, timeout) + return done.result() + + async def close(self): + self.protocol.close() + + async def getpeername(self): + return self.transport.get_extra_info('peername') + + async def getsockname(self): + return self.transport.get_extra_info('sockname') + + +class StreamSocket(dns._asyncbackend.StreamSocket): + def __init__(self, af, reader, writer): + self.family = af + self.reader = reader + self.writer = writer + + async def sendall(self, what, timeout): + self.writer.write(what) + return await _maybe_wait_for(self.writer.drain(), timeout) + + async def recv(self, size, timeout): + return await _maybe_wait_for(self.reader.read(size), + timeout) + + async def close(self): + self.writer.close() + try: + await self.writer.wait_closed() + except AttributeError: # pragma: no cover + pass + + async def getpeername(self): + return self.writer.get_extra_info('peername') + + async def getsockname(self): + return self.writer.get_extra_info('sockname') + + +class Backend(dns._asyncbackend.Backend): + def name(self): + return 'asyncio' + + async def make_socket(self, af, socktype, proto=0, + source=None, destination=None, timeout=None, + ssl_context=None, server_hostname=None): + if destination is None and socktype == socket.SOCK_DGRAM and \ + _is_win32: + raise NotImplementedError('destinationless datagram sockets ' + 'are not supported by asyncio ' + 'on Windows') + loop = _get_running_loop() + if socktype == socket.SOCK_DGRAM: + transport, protocol = await loop.create_datagram_endpoint( + _DatagramProtocol, source, family=af, + proto=proto, remote_addr=destination) + return DatagramSocket(af, transport, protocol) + elif socktype == socket.SOCK_STREAM: + (r, w) = await _maybe_wait_for( + asyncio.open_connection(destination[0], + destination[1], + ssl=ssl_context, + family=af, + proto=proto, + local_addr=source, + server_hostname=server_hostname), + timeout) + return StreamSocket(af, r, w) + raise NotImplementedError('unsupported socket ' + + f'type {socktype}') # pragma: no cover + + async def sleep(self, interval): + await asyncio.sleep(interval) + + def datagram_connection_required(self): + return _is_win32 diff --git a/libs/dns/_compat.py b/libs/dns/_compat.py deleted file mode 100644 index 956f9a133..000000000 --- a/libs/dns/_compat.py +++ /dev/null @@ -1,47 +0,0 @@ -import sys -import decimal -from decimal import Context - -if sys.version_info > (3,): - long = int - xrange = range -else: - long = long # pylint: disable=long-builtin - xrange = xrange # pylint: disable=xrange-builtin - -# unicode / binary types -if sys.version_info > (3,): - text_type = str - binary_type = bytes - string_types = (str,) - unichr = chr - def maybe_decode(x): - return x.decode() - def maybe_encode(x): - return x.encode() -else: - text_type = unicode # pylint: disable=unicode-builtin, undefined-variable - binary_type = str - string_types = ( - basestring, # pylint: disable=basestring-builtin, undefined-variable - ) - unichr = unichr # pylint: disable=unichr-builtin - def maybe_decode(x): - return x - def maybe_encode(x): - return x - - -def round_py2_compat(what): - """ - Python 2 and Python 3 use different rounding strategies in round(). This - function ensures that results are python2/3 compatible and backward - compatible with previous py2 releases - :param what: float - :return: rounded long - """ - d = Context( - prec=len(str(long(what))), # round to integer with max precision - rounding=decimal.ROUND_HALF_UP - ).create_decimal(str(what)) # str(): python 2.6 compat - return long(d) diff --git a/libs/dns/_curio_backend.py b/libs/dns/_curio_backend.py new file mode 100644 index 000000000..6fa7b3a17 --- /dev/null +++ b/libs/dns/_curio_backend.py @@ -0,0 +1,108 @@ +# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license + +"""curio async I/O library query support""" + +import socket +import curio +import curio.socket # type: ignore + +import dns._asyncbackend +import dns.exception +import dns.inet + + +def _maybe_timeout(timeout): + if timeout: + return curio.ignore_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): + self.socket = socket + self.family = socket.family + + async def sendto(self, what, destination, timeout): + async with _maybe_timeout(timeout): + return await self.socket.sendto(what, destination) + raise dns.exception.Timeout(timeout=timeout) # pragma: no cover + + async def recvfrom(self, size, timeout): + async with _maybe_timeout(timeout): + return await self.socket.recvfrom(size) + raise dns.exception.Timeout(timeout=timeout) + + async def close(self): + await self.socket.close() + + async def getpeername(self): + return self.socket.getpeername() + + async def getsockname(self): + return self.socket.getsockname() + + +class StreamSocket(dns._asyncbackend.StreamSocket): + def __init__(self, socket): + self.socket = socket + self.family = socket.family + + async def sendall(self, what, timeout): + async with _maybe_timeout(timeout): + return await self.socket.sendall(what) + raise dns.exception.Timeout(timeout=timeout) + + async def recv(self, size, timeout): + async with _maybe_timeout(timeout): + return await self.socket.recv(size) + raise dns.exception.Timeout(timeout=timeout) + + async def close(self): + await self.socket.close() + + async def getpeername(self): + return self.socket.getpeername() + + async def getsockname(self): + return self.socket.getsockname() + + +class Backend(dns._asyncbackend.Backend): + def name(self): + return 'curio' + + async def make_socket(self, af, socktype, proto=0, + source=None, destination=None, timeout=None, + ssl_context=None, server_hostname=None): + if socktype == socket.SOCK_DGRAM: + s = curio.socket.socket(af, socktype, proto) + try: + if source: + s.bind(_lltuple(source, af)) + except Exception: # pragma: no cover + await s.close() + raise + return DatagramSocket(s) + elif socktype == socket.SOCK_STREAM: + if source: + source_addr = _lltuple(source, af) + else: + source_addr = None + async with _maybe_timeout(timeout): + s = await curio.open_connection(destination[0], destination[1], + ssl=ssl_context, + source_addr=source_addr, + server_hostname=server_hostname) + return StreamSocket(s) + raise NotImplementedError('unsupported socket ' + + f'type {socktype}') # pragma: no cover + + async def sleep(self, interval): + await curio.sleep(interval) diff --git a/libs/dns/_immutable_attr.py b/libs/dns/_immutable_attr.py new file mode 100644 index 000000000..f7b9f8b0b --- /dev/null +++ b/libs/dns/_immutable_attr.py @@ -0,0 +1,84 @@ +# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license + +# This implementation of the immutable decorator is for python 3.6, +# which doesn't have Context Variables. This implementation is somewhat +# costly for classes with slots, as it adds a __dict__ to them. + + +import inspect + + +class _Immutable: + """Immutable mixin class""" + + # Note we MUST NOT have __slots__ as that causes + # + # TypeError: multiple bases have instance lay-out conflict + # + # when we get mixed in with another class with slots. When we + # get mixed into something with slots, it effectively adds __dict__ to + # the slots of the other class, which allows attribute setting to work, + # albeit at the cost of the dictionary. + + def __setattr__(self, name, value): + if not hasattr(self, '_immutable_init') or \ + self._immutable_init is not self: + raise TypeError("object doesn't support attribute assignment") + else: + super().__setattr__(name, value) + + def __delattr__(self, name): + if not hasattr(self, '_immutable_init') or \ + self._immutable_init is not self: + raise TypeError("object doesn't support attribute assignment") + else: + super().__delattr__(name) + + +def _immutable_init(f): + def nf(*args, **kwargs): + try: + # Are we already initializing an immutable class? + previous = args[0]._immutable_init + except AttributeError: + # We are the first! + previous = None + object.__setattr__(args[0], '_immutable_init', args[0]) + try: + # call the actual __init__ + f(*args, **kwargs) + finally: + if not previous: + # If we started the initialzation, establish immutability + # by removing the attribute that allows mutation + object.__delattr__(args[0], '_immutable_init') + 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): + + @_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/libs/dns/_immutable_ctx.py b/libs/dns/_immutable_ctx.py new file mode 100644 index 000000000..ececdbeb4 --- /dev/null +++ b/libs/dns/_immutable_ctx.py @@ -0,0 +1,75 @@ +# 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/libs/dns/_trio_backend.py b/libs/dns/_trio_backend.py new file mode 100644 index 000000000..a00d4a4e5 --- /dev/null +++ b/libs/dns/_trio_backend.py @@ -0,0 +1,121 @@ +# 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.exception +import dns.inet + + +def _maybe_timeout(timeout): + if timeout: + 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): + self.socket = socket + self.family = socket.family + + 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 + + async def recvfrom(self, size, timeout): + with _maybe_timeout(timeout): + return await self.socket.recvfrom(size) + raise dns.exception.Timeout(timeout=timeout) + + async def close(self): + self.socket.close() + + async def getpeername(self): + return self.socket.getpeername() + + async def getsockname(self): + return self.socket.getsockname() + + +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) + + async def recv(self, size, timeout): + with _maybe_timeout(timeout): + return await self.stream.receive_some(size) + raise dns.exception.Timeout(timeout=timeout) + + 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() + + +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: + with _maybe_timeout(timeout): + await s.connect(_lltuple(destination, af)) + 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) + s = None + 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) diff --git a/libs/dns/asyncbackend.py b/libs/dns/asyncbackend.py new file mode 100644 index 000000000..089d3d350 --- /dev/null +++ b/libs/dns/asyncbackend.py @@ -0,0 +1,101 @@ +# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license + +import dns.exception + +# pylint: disable=unused-import + +from dns._asyncbackend import Socket, DatagramSocket, \ + StreamSocket, Backend # noqa: + +# pylint: enable=unused-import + +_default_backend = None + +_backends = {} + +# Allow sniffio import to be disabled for testing purposes +_no_sniffio = False + +class AsyncLibraryNotFoundError(dns.exception.DNSException): + pass + + +def get_backend(name): + """Get the specified asynchronous backend. + + *name*, a ``str``, the name of the backend. Currently the "trio", + "curio", and "asyncio" backends are available. + + Raises NotImplementError 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 == 'curio': + import dns._curio_backend + backend = dns._curio_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(): + """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') + except AttributeError: # pragma: no cover + # we have to check current_task on 3.6 + if not asyncio.Task.current_task(): + raise AsyncLibraryNotFoundError('no async library detected') + return 'asyncio' + + +def get_default_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): + """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/libs/dns/asyncbackend.pyi b/libs/dns/asyncbackend.pyi new file mode 100644 index 000000000..1ec9d32b5 --- /dev/null +++ b/libs/dns/asyncbackend.pyi @@ -0,0 +1,13 @@ +# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license + +class Backend: + ... + +def get_backend(name: str) -> Backend: + ... +def sniff() -> str: + ... +def get_default_backend() -> Backend: + ... +def set_default_backend(name: str) -> Backend: + ... diff --git a/libs/dns/asyncquery.py b/libs/dns/asyncquery.py new file mode 100644 index 000000000..4ec97fb7d --- /dev/null +++ b/libs/dns/asyncquery.py @@ -0,0 +1,523 @@ +# 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 socket +import struct +import time + +import dns.asyncbackend +import dns.exception +import dns.inet +import dns.name +import dns.message +import dns.rcode +import dns.rdataclass +import dns.rdatatype + +from dns.query import _compute_times, _matches_destination, BadResponse, ssl, \ + UDPMode, _have_httpx, _have_http2, NoDOH + +if _have_httpx: + 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: + if not now: + now = time.time() + return max(expiration - now, 0) + else: + return None + + +async def send_udp(sock, what, destination, expiration=None): + """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. + + 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, destination=None, expiration=None, + ignore_unexpected=False, one_rr_per_rrset=False, + keyring=None, request_mac=b'', ignore_trailing=False, + raise_on_truncation=False): + """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, exceptions, and return type of this method. + """ + + wire = b'' + while 1: + (wire, from_address) = await sock.recvfrom(65535, _timeout(expiration)) + if _matches_destination(sock.family, from_address, destination, + ignore_unexpected): + break + 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, + raise_on_truncation=raise_on_truncation) + return (r, received_time, from_address) + +async def udp(q, where, timeout=None, port=53, source=None, source_port=0, + ignore_unexpected=False, one_rr_per_rrset=False, + ignore_trailing=False, raise_on_truncation=False, sock=None, + backend=None): + """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) + s = None + # After 3.6 is no longer supported, this can use an AsyncExitStack. + try: + af = dns.inet.af_for_address(where) + destination = _lltuple((where, port), af) + if sock: + s = 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 + s = await backend.make_socket(af, socket.SOCK_DGRAM, 0, stuple, + dtuple) + 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) + r.time = received_time - begin_time + if not q.is_response(r): + raise BadResponse + return r + finally: + if not sock and s: + await s.close() + +async def udp_with_fallback(q, where, timeout=None, port=53, source=None, + source_port=0, ignore_unexpected=False, + one_rr_per_rrset=False, ignore_trailing=False, + udp_sock=None, tcp_sock=None, backend=None): + """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) + 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, what, expiration=None): + """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): + what = what.to_wire() + l = len(what) + # copying the wire into tcpmsg is inefficient, but lets us + # avoid writev() or doing a short write that would get pushed + # onto the net + tcpmsg = struct.pack("!H", l) + 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, expiration=None, one_rr_per_rrset=False, + keyring=None, request_mac=b'', ignore_trailing=False): + """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, where, timeout=None, port=53, source=None, source_port=0, + one_rr_per_rrset=False, ignore_trailing=False, sock=None, + backend=None): + """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) + s = None + # After 3.6 is no longer supported, this can use an AsyncExitStack. + try: + 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() + s = sock + else: + # These are simple (address, port) pairs, not + # family-dependent tuples you pass to lowlevel 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() + s = await backend.make_socket(af, socket.SOCK_STREAM, 0, stuple, + dtuple, timeout) + 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 + finally: + if not sock and s: + await s.close() + +async def tls(q, where, timeout=None, port=853, source=None, source_port=0, + one_rr_per_rrset=False, ignore_trailing=False, sock=None, + backend=None, ssl_context=None, server_hostname=None): + """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. + """ + # After 3.6 is no longer supported, this can use an AsyncExitStack. + (begin_time, expiration) = _compute_times(timeout) + if not sock: + if ssl_context is None: + ssl_context = ssl.create_default_context() + if server_hostname is None: + ssl_context.check_hostname = False + else: + ssl_context = None + server_hostname = None + 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() + s = await backend.make_socket(af, socket.SOCK_STREAM, 0, stuple, + dtuple, timeout, ssl_context, + server_hostname) + else: + s = sock + try: + 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 + finally: + if not sock and s: + await s.close() + +async def https(q, where, timeout=None, port=443, source=None, source_port=0, + one_rr_per_rrset=False, ignore_trailing=False, client=None, + path='/dns-query', post=True, verify=True): + """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_httpx: + raise NoDOH('httpx is not available.') # pragma: no cover + + 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: + 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 + if source is not None: + transport = httpx.AsyncHTTPTransport(local_address=source[0]) + + # After 3.6 is no longer supported, this can use an AsyncExitStack + client_to_close = None + try: + if not client: + client = httpx.AsyncClient(http1=True, http2=_have_http2, + verify=verify, transport=transport) + client_to_close = 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 client.post(url, headers=headers, content=wire, + timeout=timeout) + else: + wire = base64.urlsafe_b64encode(wire).rstrip(b"=") + wire = wire.decode() # httpx does a repr() if we give it bytes + response = await client.get(url, headers=headers, timeout=timeout, + params={"dns": wire}) + finally: + if client_to_close: + await client.aclose() + + # 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 + if not q.is_response(r): + raise BadResponse + return r + +async def inbound_xfr(where, txn_manager, query=None, + port=53, timeout=None, lifetime=None, source=None, + source_port=0, udp_mode=UDPMode.NEVER, backend=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") diff --git a/libs/dns/asyncquery.pyi b/libs/dns/asyncquery.pyi new file mode 100644 index 000000000..21ef60dd4 --- /dev/null +++ b/libs/dns/asyncquery.pyi @@ -0,0 +1,43 @@ +from typing import Optional, Union, Dict, Generator, Any +from . import tsig, rdatatype, rdataclass, name, message, asyncbackend + +# If the ssl import works, then +# +# error: Name 'ssl' already defined (by an import) +# +# is expected and can be ignored. +try: + import ssl +except ImportError: + class ssl: # type: ignore + SSLContext : Dict = {} + +async def udp(q : message.Message, where : str, + timeout : Optional[float] = None, port=53, + source : Optional[str] = None, source_port : Optional[int] = 0, + ignore_unexpected : Optional[bool] = False, + one_rr_per_rrset : Optional[bool] = False, + ignore_trailing : Optional[bool] = False, + sock : Optional[asyncbackend.DatagramSocket] = None, + backend : Optional[asyncbackend.Backend]) -> message.Message: + pass + +async def tcp(q : message.Message, where : str, timeout : float = None, port=53, + af : Optional[int] = None, source : Optional[str] = None, + source_port : Optional[int] = 0, + one_rr_per_rrset : Optional[bool] = False, + ignore_trailing : Optional[bool] = False, + sock : Optional[asyncbackend.StreamSocket] = None, + backend : Optional[asyncbackend.Backend]) -> message.Message: + pass + +async def tls(q : message.Message, where : str, + timeout : Optional[float] = None, port=53, + source : Optional[str] = None, source_port : Optional[int] = 0, + one_rr_per_rrset : Optional[bool] = False, + ignore_trailing : Optional[bool] = False, + sock : Optional[asyncbackend.StreamSocket] = None, + backend : Optional[asyncbackend.Backend], + ssl_context: Optional[ssl.SSLContext] = None, + server_hostname: Optional[str] = None) -> message.Message: + pass diff --git a/libs/dns/asyncresolver.py b/libs/dns/asyncresolver.py new file mode 100644 index 000000000..ed29deed7 --- /dev/null +++ b/libs/dns/asyncresolver.py @@ -0,0 +1,232 @@ +# 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 time + +import dns.asyncbackend +import dns.asyncquery +import dns.exception +import dns.query +import dns.resolver + +# import some resolver symbols for brevity +from dns.resolver import NXDOMAIN, NoAnswer, NotAbsolute, NoRootSOA + + +# 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, rdtype=dns.rdatatype.A, + rdclass=dns.rdataclass.IN, + tcp=False, source=None, raise_on_no_answer=True, + source_port=0, lifetime=None, search=None, + backend=None): + """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 + done = False + while not done: + (nameserver, port, tcp, backoff) = resolution.next_nameserver() + if backoff: + await backend.sleep(backoff) + timeout = self._compute_timeout(start, lifetime, + resolution.errors) + try: + if dns.inet.is_address(nameserver): + if tcp: + response = await _tcp(request, nameserver, + timeout, port, + source, source_port, + backend=backend) + else: + response = await _udp(request, nameserver, + timeout, port, + source, source_port, + raise_on_truncation=True, + backend=backend) + else: + response = await dns.asyncquery.https(request, + nameserver, + timeout=timeout) + 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, *args, **kwargs): + """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. + + """ + + return await self.resolve(dns.reversename.from_address(ipaddr), + rdtype=dns.rdatatype.PTR, + rdclass=dns.rdataclass.IN, + *args, **kwargs) + + # pylint: disable=redefined-outer-name + + async def canonical_name(self, 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 + + +default_resolver = None + + +def get_default_resolver(): + """Get the default asynchronous resolver, initializing it if necessary.""" + if default_resolver is None: + reset_default_resolver() + return default_resolver + + +def reset_default_resolver(): + """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, rdtype=dns.rdatatype.A, rdclass=dns.rdataclass.IN, + tcp=False, source=None, raise_on_no_answer=True, + source_port=0, lifetime=None, search=None, backend=None): + """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, *args, **kwargs): + """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 canonical_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 zone_for_name(name, rdclass=dns.rdataclass.IN, tcp=False, + resolver=None, backend=None): + """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) + 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 diff --git a/libs/dns/asyncresolver.pyi b/libs/dns/asyncresolver.pyi new file mode 100644 index 000000000..92759d291 --- /dev/null +++ b/libs/dns/asyncresolver.pyi @@ -0,0 +1,26 @@ +from typing import Union, Optional, List, Any, Dict +from . import exception, rdataclass, name, rdatatype, asyncbackend + +async def resolve(qname : str, rdtype : Union[int,str] = 0, + rdclass : Union[int,str] = 0, + tcp=False, source=None, raise_on_no_answer=True, + source_port=0, lifetime : Optional[float]=None, + search : Optional[bool]=None, + backend : Optional[asyncbackend.Backend]=None): + ... +async def resolve_address(self, ipaddr: str, + *args: Any, **kwargs: Optional[Dict]): + ... + +class Resolver: + def __init__(self, filename : Optional[str] = '/etc/resolv.conf', + configure : Optional[bool] = True): + self.nameservers : List[str] + async def resolve(self, qname : str, rdtype : Union[int,str] = rdatatype.A, + rdclass : Union[int,str] = rdataclass.IN, + tcp : bool = False, source : Optional[str] = None, + raise_on_no_answer=True, source_port : int = 0, + lifetime : Optional[float]=None, + search : Optional[bool]=None, + backend : Optional[asyncbackend.Backend]=None): + ... diff --git a/libs/dns/dnssec.py b/libs/dns/dnssec.py index fec12082c..6e9946f4f 100644 --- a/libs/dns/dnssec.py +++ b/libs/dns/dnssec.py @@ -1,4 +1,6 @@ -# Copyright (C) 2003-2007, 2009, 2011 Nominum, Inc. +# 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, @@ -15,99 +17,85 @@ """Common DNSSEC-related functions and constants.""" -from io import BytesIO +import hashlib import struct import time +import base64 +import dns.enum import dns.exception -import dns.hash import dns.name import dns.node import dns.rdataset import dns.rdata import dns.rdatatype import dns.rdataclass -from ._compat import string_types class UnsupportedAlgorithm(dns.exception.DNSException): - """The DNSSEC algorithm is not supported.""" class ValidationFailure(dns.exception.DNSException): - """The DNSSEC signature is invalid.""" -RSAMD5 = 1 -DH = 2 -DSA = 3 -ECC = 4 -RSASHA1 = 5 -DSANSEC3SHA1 = 6 -RSASHA1NSEC3SHA1 = 7 -RSASHA256 = 8 -RSASHA512 = 10 -ECDSAP256SHA256 = 13 -ECDSAP384SHA384 = 14 -INDIRECT = 252 -PRIVATEDNS = 253 -PRIVATEOID = 254 -_algorithm_by_text = { - 'RSAMD5': RSAMD5, - 'DH': DH, - 'DSA': DSA, - 'ECC': ECC, - 'RSASHA1': RSASHA1, - 'DSANSEC3SHA1': DSANSEC3SHA1, - 'RSASHA1NSEC3SHA1': RSASHA1NSEC3SHA1, - 'RSASHA256': RSASHA256, - 'RSASHA512': RSASHA512, - 'INDIRECT': INDIRECT, - 'ECDSAP256SHA256': ECDSAP256SHA256, - 'ECDSAP384SHA384': ECDSAP384SHA384, - 'PRIVATEDNS': PRIVATEDNS, - 'PRIVATEOID': PRIVATEOID, -} +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 -# We construct the inverse mapping programmatically to ensure that we -# cannot make any mistakes (e.g. omissions, cut-and-paste errors) that -# would cause the mapping not to be true inverse. - -_algorithm_by_value = dict((y, x) for x, y in _algorithm_by_text.items()) + @classmethod + def _maximum(cls): + return 255 def algorithm_from_text(text): - """Convert text into a DNSSEC algorithm value - @rtype: int""" + """Convert text into a DNSSEC algorithm value. - value = _algorithm_by_text.get(text.upper()) - if value is None: - value = int(text) - return 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): """Convert a DNSSEC algorithm value to text - @rtype: string""" - text = _algorithm_by_value.get(value) - if text is None: - text = str(value) - return text + *value*, an ``int`` a DNSSEC algorithm. + + Returns a ``str``, the name of a DNSSEC algorithm. + """ + + return Algorithm.to_text(value) -def _to_rdata(record, origin): - s = BytesIO() - record.to_wire(s, origin=origin) - return s.getvalue() +def key_id(key): + """Return the key id (a 16-bit number) for the specified key. + *key*, a ``dns.rdtypes.ANY.DNSKEY.DNSKEY`` -def key_id(key, origin=None): - rdata = _to_rdata(key, origin) - rdata = bytearray(rdata) - if key.algorithm == RSAMD5: + 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 @@ -119,280 +107,354 @@ def key_id(key, origin=None): total += ((total >> 16) & 0xffff) return total & 0xffff +class DSDigest(dns.enum.IntEnum): + """DNSSEC Delgation Signer Digest Algorithm""" + + SHA1 = 1 + SHA256 = 2 + SHA384 = 4 + + @classmethod + def _maximum(cls): + return 255 + def make_ds(name, key, algorithm, origin=None): - if algorithm.upper() == 'SHA1': - dsalg = 1 - hash = dns.hash.hashes['SHA1']() - elif algorithm.upper() == 'SHA256': - dsalg = 2 - hash = dns.hash.hashes['SHA256']() + """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``, 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.DS`` + """ + + try: + if isinstance(algorithm, str): + algorithm = DSDigest[algorithm.upper()] + except Exception: + raise UnsupportedAlgorithm('unsupported algorithm "%s"' % algorithm) + + 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, string_types): + if isinstance(name, str): name = dns.name.from_text(name, origin) - hash.update(name.canonicalize().to_wire()) - hash.update(_to_rdata(key, origin)) - digest = hash.digest() + dshash.update(name.canonicalize().to_wire()) + dshash.update(key.to_wire(origin=origin)) + digest = dshash.digest() - dsrdata = struct.pack("!HBB", key_id(key), key.algorithm, dsalg) + digest + dsrdata = struct.pack("!HBB", key_id(key), key.algorithm, algorithm) + \ + digest return dns.rdata.from_wire(dns.rdataclass.IN, dns.rdatatype.DS, dsrdata, 0, len(dsrdata)) def _find_candidate_keys(keys, rrsig): - candidate_keys = [] value = keys.get(rrsig.signer) - if value is None: - return None if isinstance(value, dns.node.Node): - try: - rdataset = value.find_rdataset(dns.rdataclass.IN, - dns.rdatatype.DNSKEY) - except KeyError: - return None + rdataset = value.get_rdataset(dns.rdataclass.IN, dns.rdatatype.DNSKEY) else: rdataset = value - for rdata in rdataset: - if rdata.algorithm == rrsig.algorithm and \ - key_id(rdata) == rrsig.key_tag: - candidate_keys.append(rdata) - return candidate_keys + if rdataset is None: + return None + return [rd for rd in rdataset if + rd.algorithm == rrsig.algorithm and key_id(rd) == rrsig.key_tag] def _is_rsa(algorithm): - return algorithm in (RSAMD5, RSASHA1, - RSASHA1NSEC3SHA1, RSASHA256, - RSASHA512) + return algorithm in (Algorithm.RSAMD5, Algorithm.RSASHA1, + Algorithm.RSASHA1NSEC3SHA1, Algorithm.RSASHA256, + Algorithm.RSASHA512) def _is_dsa(algorithm): - return algorithm in (DSA, DSANSEC3SHA1) + return algorithm in (Algorithm.DSA, Algorithm.DSANSEC3SHA1) def _is_ecdsa(algorithm): - return _have_ecdsa and (algorithm in (ECDSAP256SHA256, ECDSAP384SHA384)) + return algorithm in (Algorithm.ECDSAP256SHA256, Algorithm.ECDSAP384SHA384) + + +def _is_eddsa(algorithm): + return algorithm in (Algorithm.ED25519, Algorithm.ED448) + + +def _is_gost(algorithm): + return algorithm == Algorithm.ECCGOST def _is_md5(algorithm): - return algorithm == RSAMD5 + return algorithm == Algorithm.RSAMD5 def _is_sha1(algorithm): - return algorithm in (DSA, RSASHA1, - DSANSEC3SHA1, RSASHA1NSEC3SHA1) + return algorithm in (Algorithm.DSA, Algorithm.RSASHA1, + Algorithm.DSANSEC3SHA1, Algorithm.RSASHA1NSEC3SHA1) def _is_sha256(algorithm): - return algorithm in (RSASHA256, ECDSAP256SHA256) + return algorithm in (Algorithm.RSASHA256, Algorithm.ECDSAP256SHA256) def _is_sha384(algorithm): - return algorithm == ECDSAP384SHA384 + return algorithm == Algorithm.ECDSAP384SHA384 def _is_sha512(algorithm): - return algorithm == RSASHA512 + return algorithm == Algorithm.RSASHA512 def _make_hash(algorithm): if _is_md5(algorithm): - return dns.hash.hashes['MD5']() + return hashes.MD5() if _is_sha1(algorithm): - return dns.hash.hashes['SHA1']() + return hashes.SHA1() if _is_sha256(algorithm): - return dns.hash.hashes['SHA256']() + return hashes.SHA256() if _is_sha384(algorithm): - return dns.hash.hashes['SHA384']() + return hashes.SHA384() if _is_sha512(algorithm): - return dns.hash.hashes['SHA512']() + return hashes.SHA512() + if algorithm == Algorithm.ED25519: + return hashes.SHA512() + if algorithm == Algorithm.ED448: + return hashes.SHAKE256(114) + raise ValidationFailure('unknown hash for algorithm %u' % algorithm) -def _make_algorithm_id(algorithm): - if _is_md5(algorithm): - oid = [0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x02, 0x05] - elif _is_sha1(algorithm): - oid = [0x2b, 0x0e, 0x03, 0x02, 0x1a] - elif _is_sha256(algorithm): - oid = [0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x01] - elif _is_sha512(algorithm): - oid = [0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x03] +def _bytes_to_long(b): + return int.from_bytes(b, 'big') + + +def _validate_signature(sig, data, key, chosen_hash): + if _is_rsa(key.algorithm): + 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_:] + try: + public_key = rsa.RSAPublicNumbers( + _bytes_to_long(rsa_e), + _bytes_to_long(rsa_n)).public_key(default_backend()) + except ValueError: + raise ValidationFailure('invalid public key') + public_key.verify(sig, data, padding.PKCS1v15(), chosen_hash) + elif _is_dsa(key.algorithm): + 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] + try: + public_key = dsa.DSAPublicNumbers( + _bytes_to_long(dsa_y), + dsa.DSAParameterNumbers( + _bytes_to_long(dsa_p), + _bytes_to_long(dsa_q), + _bytes_to_long(dsa_g))).public_key(default_backend()) + except ValueError: + raise ValidationFailure('invalid public key') + public_key.verify(sig, data, chosen_hash) + elif _is_ecdsa(key.algorithm): + keyptr = key.key + if key.algorithm == Algorithm.ECDSAP256SHA256: + curve = ec.SECP256R1() + octets = 32 + else: + curve = ec.SECP384R1() + octets = 48 + ecdsa_x = keyptr[0:octets] + ecdsa_y = keyptr[octets:octets * 2] + try: + public_key = ec.EllipticCurvePublicNumbers( + curve=curve, + x=_bytes_to_long(ecdsa_x), + y=_bytes_to_long(ecdsa_y)).public_key(default_backend()) + except ValueError: + raise ValidationFailure('invalid public key') + public_key.verify(sig, data, ec.ECDSA(chosen_hash)) + elif _is_eddsa(key.algorithm): + keyptr = key.key + if key.algorithm == Algorithm.ED25519: + loader = ed25519.Ed25519PublicKey + else: + loader = ed448.Ed448PublicKey + try: + public_key = loader.from_public_bytes(keyptr) + except ValueError: + raise ValidationFailure('invalid public key') + public_key.verify(sig, data) + elif _is_gost(key.algorithm): + raise UnsupportedAlgorithm( + 'algorithm "%s" not supported by dnspython' % + algorithm_to_text(key.algorithm)) else: - raise ValidationFailure('unknown algorithm %u' % algorithm) - olen = len(oid) - dlen = _make_hash(algorithm).digest_size - idbytes = [0x30] + [8 + olen + dlen] + \ - [0x30, olen + 4] + [0x06, olen] + oid + \ - [0x05, 0x00] + [0x04, dlen] - return struct.pack('!%dB' % len(idbytes), *idbytes) + raise ValidationFailure('unknown algorithm %u' % key.algorithm) def _validate_rrsig(rrset, rrsig, keys, origin=None, now=None): - """Validate an RRset against a single signature rdata + """Validate an RRset against a single signature rdata, throwing an + exception if validation is not successful. - The owner name of the rrsig is assumed to be the same as the owner name - of the rrset. + *rrset*, the RRset to validate. This can be a + ``dns.rrset.RRset`` or a (``dns.name.Name``, ``dns.rdataset.Rdataset``) + tuple. - @param rrset: The RRset to validate - @type rrset: dns.rrset.RRset or (dns.name.Name, dns.rdataset.Rdataset) - tuple - @param rrsig: The signature rdata - @type rrsig: dns.rrset.Rdata - @param keys: The key dictionary. - @type keys: a dictionary keyed by dns.name.Name with node or rdataset - values - @param origin: The origin to use for relative names - @type origin: dns.name.Name or None - @param now: The time to use when validating the signatures. The default - is the current time. - @type now: int + *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*, 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. + + 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 isinstance(origin, string_types): + if isinstance(origin, str): origin = dns.name.from_text(origin, dns.name.root) - for candidate_key in _find_candidate_keys(keys, rrsig): - if not candidate_key: - raise ValidationFailure('unknown key') + candidate_keys = _find_candidate_keys(keys, rrsig) + if candidate_keys is None: + raise ValidationFailure('unknown key') - # For convenience, allow the rrset to be specified as a (name, - # rdataset) tuple as well as a proper rrset - if isinstance(rrset, tuple): - rrname = rrset[0] - rdataset = rrset[1] + # For convenience, allow the rrset to be specified as a (name, + # rdataset) tuple as well as a proper rrset + if isinstance(rrset, tuple): + rrname = rrset[0] + rdataset = rrset[1] + else: + rrname = rrset.name + rdataset = rrset + + if now is None: + now = time.time() + if rrsig.expiration < now: + raise ValidationFailure('expired') + if rrsig.inception > now: + raise ValidationFailure('not yet valid') + + if _is_dsa(rrsig.algorithm): + sig_r = rrsig.signature[1:21] + sig_s = rrsig.signature[21:] + sig = utils.encode_dss_signature(_bytes_to_long(sig_r), + _bytes_to_long(sig_s)) + elif _is_ecdsa(rrsig.algorithm): + if rrsig.algorithm == Algorithm.ECDSAP256SHA256: + octets = 32 else: - rrname = rrset.name - rdataset = rrset + octets = 48 + sig_r = rrsig.signature[0:octets] + sig_s = rrsig.signature[octets:] + sig = utils.encode_dss_signature(_bytes_to_long(sig_r), + _bytes_to_long(sig_s)) + else: + sig = rrsig.signature - if now is None: - now = time.time() - if rrsig.expiration < now: - raise ValidationFailure('expired') - if rrsig.inception > now: - raise ValidationFailure('not yet valid') + data = b'' + data += rrsig.to_wire(origin=origin)[:18] + data += rrsig.signer.to_digestable(origin) - hash = _make_hash(rrsig.algorithm) + # Derelativize the name before considering labels. + rrname = rrname.derelativize(origin) - if _is_rsa(rrsig.algorithm): - keyptr = candidate_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_:] - keylen = len(rsa_n) * 8 - pubkey = Crypto.PublicKey.RSA.construct( - (Crypto.Util.number.bytes_to_long(rsa_n), - Crypto.Util.number.bytes_to_long(rsa_e))) - sig = (Crypto.Util.number.bytes_to_long(rrsig.signature),) - elif _is_dsa(rrsig.algorithm): - keyptr = candidate_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] - pubkey = Crypto.PublicKey.DSA.construct( - (Crypto.Util.number.bytes_to_long(dsa_y), - Crypto.Util.number.bytes_to_long(dsa_g), - Crypto.Util.number.bytes_to_long(dsa_p), - Crypto.Util.number.bytes_to_long(dsa_q))) - (dsa_r, dsa_s) = struct.unpack('!20s20s', rrsig.signature[1:]) - sig = (Crypto.Util.number.bytes_to_long(dsa_r), - Crypto.Util.number.bytes_to_long(dsa_s)) - elif _is_ecdsa(rrsig.algorithm): - if rrsig.algorithm == ECDSAP256SHA256: - curve = ecdsa.curves.NIST256p - key_len = 32 - elif rrsig.algorithm == ECDSAP384SHA384: - curve = ecdsa.curves.NIST384p - key_len = 48 - else: - # shouldn't happen - raise ValidationFailure('unknown ECDSA curve') - keyptr = candidate_key.key - x = Crypto.Util.number.bytes_to_long(keyptr[0:key_len]) - y = Crypto.Util.number.bytes_to_long(keyptr[key_len:key_len * 2]) - assert ecdsa.ecdsa.point_is_valid(curve.generator, x, y) - point = ecdsa.ellipticcurve.Point(curve.curve, x, y, curve.order) - verifying_key = ecdsa.keys.VerifyingKey.from_public_point(point, - curve) - pubkey = ECKeyWrapper(verifying_key, key_len) - r = rrsig.signature[:key_len] - s = rrsig.signature[key_len:] - sig = ecdsa.ecdsa.Signature(Crypto.Util.number.bytes_to_long(r), - Crypto.Util.number.bytes_to_long(s)) - else: - raise ValidationFailure('unknown algorithm %u' % rrsig.algorithm) + if len(rrname) - 1 < rrsig.labels: + raise ValidationFailure('owner name longer than RRSIG labels') + elif rrsig.labels < len(rrname) - 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 - hash.update(_to_rdata(rrsig, origin)[:18]) - hash.update(rrsig.signer.to_digestable(origin)) + chosen_hash = _make_hash(rrsig.algorithm) - if rrsig.labels < len(rrname) - 1: - suffix = rrname.split(rrsig.labels + 1)[1] - rrname = dns.name.from_text('*', suffix) - rrnamebuf = rrname.to_digestable(origin) - rrfixed = struct.pack('!HHI', rdataset.rdtype, rdataset.rdclass, - rrsig.original_ttl) - rrlist = sorted(rdataset) - for rr in rrlist: - hash.update(rrnamebuf) - hash.update(rrfixed) - rrdata = rr.to_digestable(origin) - rrlen = struct.pack('!H', len(rrdata)) - hash.update(rrlen) - hash.update(rrdata) - - digest = hash.digest() - - if _is_rsa(rrsig.algorithm): - # PKCS1 algorithm identifier goop - digest = _make_algorithm_id(rrsig.algorithm) + digest - padlen = keylen // 8 - len(digest) - 3 - digest = struct.pack('!%dB' % (2 + padlen + 1), - *([0, 1] + [0xFF] * padlen + [0])) + digest - elif _is_dsa(rrsig.algorithm) or _is_ecdsa(rrsig.algorithm): - pass - else: - # Raise here for code clarity; this won't actually ever happen - # since if the algorithm is really unknown we'd already have - # raised an exception above - raise ValidationFailure('unknown algorithm %u' % rrsig.algorithm) - - if pubkey.verify(digest, sig): + for candidate_key in candidate_keys: + try: + _validate_signature(sig, data, candidate_key, chosen_hash) return + except (InvalidSignature, ValidationFailure): + # this happens on an individual validation failure + continue + # nothing verified -- raise failure: raise ValidationFailure('verify failure') def _validate(rrset, rrsigset, keys, origin=None, now=None): - """Validate an RRset + """Validate an RRset against a signature RRset, throwing an exception + if none of the signatures validate. - @param rrset: The RRset to validate - @type rrset: dns.rrset.RRset or (dns.name.Name, dns.rdataset.Rdataset) - tuple - @param rrsigset: The signature RRset - @type rrsigset: dns.rrset.RRset or (dns.name.Name, dns.rdataset.Rdataset) - tuple - @param keys: The key dictionary. - @type keys: a dictionary keyed by dns.name.Name with node or rdataset - values - @param origin: The origin to use for relative names - @type origin: dns.name.Name or None - @param now: The time to use when validating the signatures. The default - is the current time. - @type now: int + *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. + + Raises ``ValidationFailure`` if the signature is expired, not yet valid, + the public key is invalid, the algorithm is unknown, the verification + fails, etc. """ - if isinstance(origin, string_types): + if isinstance(origin, str): origin = dns.name.from_text(origin, dns.name.root) if isinstance(rrset, tuple): @@ -408,7 +470,7 @@ def _validate(rrset, rrsigset, keys, origin=None, now=None): rrsigrdataset = rrsigset rrname = rrname.choose_relativity(origin) - rrsigname = rrname.choose_relativity(origin) + rrsigname = rrsigname.choose_relativity(origin) if rrname != rrsigname: raise ValidationFailure("owner names do not match") @@ -416,42 +478,117 @@ def _validate(rrset, rrsigset, keys, origin=None, now=None): try: _validate_rrsig(rrset, rrsig, keys, origin, now) return - except ValidationFailure: + except (ValidationFailure, UnsupportedAlgorithm): pass raise ValidationFailure("no RRSIGs validated") -def _need_pycrypto(*args, **kwargs): - raise NotImplementedError("DNSSEC validation requires pycrypto") +class NSEC3Hash(dns.enum.IntEnum): + """NSEC3 hash algorithm""" + + SHA1 = 1 + + @classmethod + def _maximum(cls): + return 255 + +def nsec3_hash(domain, salt, iterations, algorithm): + """ + 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)") + + salt_encoded = salt + 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") + + if not isinstance(domain, dns.name.Name): + domain = dns.name.from_text(domain) + domain_encoded = domain.canonicalize().to_wire() + + 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 _need_pyca(*args, **kwargs): + raise ImportError("DNSSEC validation requires " + + "python cryptography") # pragma: no cover + try: - import Crypto.PublicKey.RSA - import Crypto.PublicKey.DSA - import Crypto.Util.number - validate = _validate - validate_rrsig = _validate_rrsig - _have_pycrypto = True -except ImportError: - validate = _need_pycrypto - validate_rrsig = _need_pycrypto - _have_pycrypto = False + from cryptography.exceptions import InvalidSignature + from cryptography.hazmat.backends import default_backend + from cryptography.hazmat.primitives import hashes + from cryptography.hazmat.primitives.asymmetric import padding + from cryptography.hazmat.primitives.asymmetric import utils + from cryptography.hazmat.primitives.asymmetric import dsa + from cryptography.hazmat.primitives.asymmetric import ec + from cryptography.hazmat.primitives.asymmetric import ed25519 + from cryptography.hazmat.primitives.asymmetric import ed448 + from cryptography.hazmat.primitives.asymmetric import rsa +except ImportError: # pragma: no cover + validate = _need_pyca + validate_rrsig = _need_pyca + _have_pyca = False +else: + validate = _validate # type: ignore + validate_rrsig = _validate_rrsig # type: ignore + _have_pyca = True -try: - import ecdsa - import ecdsa.ecdsa - import ecdsa.ellipticcurve - import ecdsa.keys - _have_ecdsa = True +### BEGIN generated Algorithm constants - class ECKeyWrapper(object): +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 - def __init__(self, key, key_len): - self.key = key - self.key_len = key_len - - def verify(self, digest, sig): - diglong = Crypto.Util.number.bytes_to_long(digest) - return self.key.pubkey.verifies(diglong, sig) - -except ImportError: - _have_ecdsa = False +### END generated Algorithm constants diff --git a/libs/dns/dnssec.pyi b/libs/dns/dnssec.pyi new file mode 100644 index 000000000..e126f9b8e --- /dev/null +++ b/libs/dns/dnssec.pyi @@ -0,0 +1,21 @@ +from typing import Union, Dict, Tuple, Optional +from . import rdataset, rrset, exception, name, rdtypes, rdata, node +import dns.rdtypes.ANY.DS as DS +import dns.rdtypes.ANY.DNSKEY as DNSKEY + +_have_pyca : bool + +def validate_rrsig(rrset : Union[Tuple[name.Name, rdataset.Rdataset], rrset.RRset], rrsig : rdata.Rdata, keys : Dict[name.Name, Union[node.Node, rdataset.Rdataset]], origin : Optional[name.Name] = None, now : Optional[int] = None) -> None: + ... + +def validate(rrset: Union[Tuple[name.Name, rdataset.Rdataset], rrset.RRset], rrsigset : Union[Tuple[name.Name, rdataset.Rdataset], rrset.RRset], keys : Dict[name.Name, Union[node.Node, rdataset.Rdataset]], origin=None, now=None) -> None: + ... + +class ValidationFailure(exception.DNSException): + ... + +def make_ds(name : name.Name, key : DNSKEY.DNSKEY, algorithm : str, origin : Optional[name.Name] = None) -> DS.DS: + ... + +def nsec3_hash(domain: str, salt: Optional[Union[str, bytes]], iterations: int, algo: int) -> str: + ... diff --git a/libs/dns/e164.py b/libs/dns/e164.py index 99300730b..83731b2c5 100644 --- a/libs/dns/e164.py +++ b/libs/dns/e164.py @@ -1,4 +1,6 @@ -# Copyright (C) 2006, 2007, 2009, 2011 Nominum, Inc. +# 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, @@ -13,31 +15,31 @@ # 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 - -@var public_enum_domain: The DNS public ENUM domain, e164.arpa. -@type public_enum_domain: dns.name.Name object -""" - +"""DNS E.164 helpers.""" import dns.exception import dns.name import dns.resolver -from ._compat import string_types +#: The public E.164 domain. public_enum_domain = dns.name.from_text('e164.arpa.') def from_e164(text, origin=public_enum_domain): """Convert an E.164 number in textual form into a Name object whose value is the ENUM domain name for that number. - @param text: an E.164 number in textual form. - @type text: str - @param origin: The domain in which the number should be constructed. - The default is e164.arpa. - @type origin: dns.name.Name object or None - @rtype: dns.name.Name object + + 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) @@ -45,14 +47,23 @@ def from_e164(text, origin=public_enum_domain): def to_e164(name, origin=public_enum_domain, want_plus_prefix=True): """Convert an ENUM domain name into an E.164 number. - @param name: the ENUM domain name. - @type name: dns.name.Name object. - @param origin: A domain containing the ENUM domain name. The - name is relativized to this domain before being converted to text. - @type origin: dns.name.Name object or None - @param want_plus_prefix: if True, add a '+' to the beginning of the - returned number. - @rtype: str + + 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) @@ -63,23 +74,31 @@ def to_e164(name, origin=public_enum_domain, want_plus_prefix=True): text = b''.join(dlabels) if want_plus_prefix: text = b'+' + text - return text + return text.decode() def query(number, domains, resolver=None): """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, string_types): + if isinstance(domain, str): domain = dns.name.from_text(domain) qname = dns.e164.from_e164(number, domain) try: - return resolver.query(qname, 'NAPTR') + return resolver.resolve(qname, 'NAPTR') except dns.resolver.NXDOMAIN as e: e_nx += e raise e_nx diff --git a/libs/dns/e164.pyi b/libs/dns/e164.pyi new file mode 100644 index 000000000..37a99fed4 --- /dev/null +++ b/libs/dns/e164.pyi @@ -0,0 +1,10 @@ +from typing import Optional, Iterable +from . import name, resolver +def from_e164(text : str, origin=name.Name(".")) -> name.Name: + ... + +def to_e164(name : name.Name, origin : Optional[name.Name] = None, want_plus_prefix=True) -> str: + ... + +def query(number : str, domains : Iterable[str], resolver : Optional[resolver.Resolver] = None) -> resolver.Answer: + ... diff --git a/libs/dns/edns.py b/libs/dns/edns.py index 8ac676bc6..9d7e909db 100644 --- a/libs/dns/edns.py +++ b/libs/dns/edns.py @@ -1,4 +1,6 @@ -# Copyright (C) 2009, 2011 Nominum, Inc. +# 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, @@ -15,47 +17,88 @@ """EDNS Options""" -NSID = 3 +import math +import socket +import struct + +import dns.enum +import dns.inet +import dns.rdata -class Option(object): +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 - """Base class for all EDNS option types. - """ + @classmethod + def _maximum(cls): + return 65535 + + +class Option: + + """Base class for all EDNS option types.""" def __init__(self, otype): """Initialize an option. - @param otype: The rdata type - @type otype: int - """ - self.otype = otype - def to_wire(self, file): - """Convert an option to wire format. + *otype*, an ``int``, is the option type. """ - raise NotImplementedError + self.otype = OptionType.make(otype) + + def to_wire(self, file=None): + """Convert an option to wire format. + + Returns a ``bytes`` or ``None``. + + """ + raise NotImplementedError # pragma: no cover @classmethod - def from_wire(cls, otype, wire, current, olen): - """Build an EDNS option object from wire format + def from_wire_parser(cls, otype, parser): + """Build an EDNS option object from wire format. - @param otype: The option type - @type otype: int - @param wire: The wire-format message - @type wire: string - @param current: The offset in wire of the beginning of the rdata. - @type current: int - @param olen: The length of the wire-format option data - @type olen: int - @rtype: dns.edns.Option instance""" - raise NotImplementedError + *otype*, an ``int``, 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. - Return < 0 if self < other, 0 if self == other, - and > 0 if self > other. + + Returns < 0 if < *other*, 0 if == *other*, and > 0 if > *other*. """ - raise NotImplementedError + 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): @@ -66,9 +109,9 @@ class Option(object): def __ne__(self, other): if not isinstance(other, Option): - return False + return True if self.otype != other.otype: - return False + return True return self._cmp(other) != 0 def __lt__(self, other): @@ -95,56 +138,327 @@ class Option(object): return NotImplemented return self._cmp(other) > 0 + def __str__(self): + return self.to_text() + class GenericOption(Option): - """Generate Rdata Class + """Generic Option Class This class is used for EDNS option types for which we have no better implementation. """ def __init__(self, otype, data): - super(GenericOption, self).__init__(otype) - self.data = data + super().__init__(otype) + self.data = dns.rdata.Rdata._as_bytes(data, True) - def to_wire(self, file): - file.write(self.data) + def to_wire(self, file=None): + if file: + file.write(self.data) + else: + return self.data + + def to_text(self): + return "Generic %d" % self.otype @classmethod - def from_wire(cls, otype, wire, current, olen): - return cls(otype, wire[current: current + olen]) + def from_wire_parser(cls, otype, parser): + return cls(otype, parser.get_remaining()) + + +class ECSOption(Option): + """EDNS Client Subnet (ECS, RFC7871)""" + + def __init__(self, address, srclen=None, scopelen=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') + + 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): + return "ECS {}/{} scope/{}".format(self.address, self.srclen, + self.scopelen) + + @staticmethod + def from_text(text): + """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, srclen = ecs_text.split('/') + scope = 0 + elif n_slashes == 2: + address, srclen, scope = ecs_text.split('/') + else: + raise ValueError('could not parse ECS from "{}"'.format(text)) + try: + scope = int(scope) + except ValueError: + raise ValueError('invalid scope ' + + '"{}": scope must be an integer'.format(scope)) + try: + srclen = int(srclen) + except ValueError: + raise ValueError('invalid srclen ' + + '"{}": srclen must be an integer'.format(srclen)) + return ECSOption(address, srclen, scope) + + def to_wire(self, file=None): + value = (struct.pack('!HBB', self.family, self.srclen, self.scopelen) + + self.addrdata) + if file: + file.write(value) + else: + return value + + @classmethod + def from_wire_parser(cls, otype, parser): + 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): + """Extended DNS Error (EDE, RFC8914)""" + + def __init__(self, code, text=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.code = code + self.text = text + + def to_text(self): + output = f'EDE {self.code}' + if self.text is not None: + output += f': {self.text}' + return output + + def to_wire(self, file=None): + value = struct.pack('!H', self.code) + if self.text is not None: + value += self.text.encode('utf8') + + if file: + file.write(value) + else: + return value + + @classmethod + def from_wire_parser(cls, otype, parser): + code = parser.get_uint16() + text = parser.get_remaining() + + if text: + if text[-1] == 0: # text MAY be null-terminated + text = text[:-1] + text = text.decode('utf8') + else: + text = None + + return cls(code, text) - def _cmp(self, other): - if self.data == other.data: - return 0 - if self.data > other.data: - return 1 - return -1 _type_to_class = { + OptionType.ECS: ECSOption, + OptionType.EDE: EDEOption, } def get_option_class(otype): + """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(otype, wire, current, olen): - """Build an EDNS option object from wire format +def option_from_wire_parser(otype, parser): + """Build an EDNS option object from wire format. - @param otype: The option type - @type otype: int - @param wire: The wire-format message - @type wire: string - @param current: The offset in wire of the beginning of the rdata. - @type current: int - @param olen: The length of the wire-format option data - @type olen: int - @rtype: dns.edns.Option instance""" + *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``. + """ cls = get_option_class(otype) - return cls.from_wire(otype, wire, current, olen) + otype = OptionType.make(otype) + return cls.from_wire_parser(otype, parser) + + +def option_from_wire(otype, wire, current, olen): + """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, otype): + """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/libs/dns/entropy.py b/libs/dns/entropy.py index de7a70a51..086bba787 100644 --- a/libs/dns/entropy.py +++ b/libs/dns/entropy.py @@ -1,4 +1,6 @@ -# Copyright (C) 2009, 2011 Nominum, Inc. +# 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, @@ -14,90 +16,76 @@ # OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. import os +import hashlib import random import time -from ._compat import long, binary_type try: import threading as _threading -except ImportError: - import dummy_threading as _threading +except ImportError: # pragma: no cover + import dummy_threading as _threading # type: ignore -class EntropyPool(object): +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=None): self.pool_index = 0 self.digest = None self.next_byte = 0 self.lock = _threading.Lock() - try: - import hashlib - self.hash = hashlib.sha1() - self.hash_len = 20 - except ImportError: - try: - import sha - self.hash = sha.new() - self.hash_len = 20 - except ImportError: - import md5 # pylint: disable=import-error - self.hash = md5.new() - self.hash_len = 16 + self.hash = hashlib.sha1() + self.hash_len = 20 self.pool = bytearray(b'\0' * self.hash_len) if seed is not None: - self.stir(bytearray(seed)) + self._stir(bytearray(seed)) self.seeded = True self.seed_pid = os.getpid() else: self.seeded = False self.seed_pid = 0 - def stir(self, entropy, already_locked=False): - if not already_locked: - self.lock.acquire() - try: - 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 - finally: - if not already_locked: - self.lock.release() + def _stir(self, entropy): + 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): + with self.lock: + self._stir(entropy) def _maybe_seed(self): if not self.seeded or self.seed_pid != os.getpid(): try: seed = os.urandom(16) - except Exception: + except Exception: # pragma: no cover try: - r = open('/dev/urandom', 'rb', 0) - try: + with open('/dev/urandom', 'rb', 0) as r: seed = r.read(16) - finally: - r.close() except Exception: seed = str(time.time()) self.seeded = True self.seed_pid = os.getpid() self.digest = None seed = bytearray(seed) - self.stir(seed, True) + self._stir(seed) def random_8(self): - self.lock.acquire() - try: + with self.lock: self._maybe_seed() if self.digest is None or self.next_byte == self.hash_len: - self.hash.update(binary_type(self.pool)) + self.hash.update(bytes(self.pool)) self.digest = bytearray(self.hash.digest()) - self.stir(self.digest, True) + self._stir(self.digest) self.next_byte = 0 value = self.digest[self.next_byte] self.next_byte += 1 - finally: - self.lock.release() return value def random_16(self): @@ -108,11 +96,11 @@ class EntropyPool(object): def random_between(self, first, last): size = last - first + 1 - if size > long(4294967296): + if size > 4294967296: raise ValueError('too big') if size > 65536: rand = self.random_32 - max = long(4294967295) + max = 4294967295 elif size > 256: rand = self.random_16 max = 65535 @@ -125,7 +113,7 @@ pool = EntropyPool() try: system_random = random.SystemRandom() -except Exception: +except Exception: # pragma: no cover system_random = None def random_16(): diff --git a/libs/dns/entropy.pyi b/libs/dns/entropy.pyi new file mode 100644 index 000000000..818f805a4 --- /dev/null +++ b/libs/dns/entropy.pyi @@ -0,0 +1,10 @@ +from typing import Optional +from random import SystemRandom + +system_random : Optional[SystemRandom] + +def random_16() -> int: + pass + +def between(first: int, last: int) -> int: + pass diff --git a/libs/dns/enum.py b/libs/dns/enum.py new file mode 100644 index 000000000..b822dd51d --- /dev/null +++ b/libs/dns/enum.py @@ -0,0 +1,90 @@ +# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license + +# Copyright (C) 2003-2017 Nominum, Inc. +# +# Permission to use, copy, modify, and distribute this software and its +# documentation for any purpose with or without fee is hereby granted, +# provided that the above copyright notice and this permission notice +# appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT +# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +import enum + +class IntEnum(enum.IntEnum): + @classmethod + def _check_value(cls, value): + max = cls._maximum() + if value < 0 or value > max: + name = cls._short_name() + raise ValueError(f"{name} must be between >= 0 and <= {max}") + + @classmethod + def from_text(cls, text): + text = text.upper() + try: + return cls[text] + except KeyError: + pass + 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, value): + cls._check_value(value) + try: + return cls(value).name + except ValueError: + return f"{cls._prefix()}{value}" + + @classmethod + def make(cls, value): + """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) + try: + return cls(value) + except ValueError: + return 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 _unknown_exception_class(cls): + return ValueError diff --git a/libs/dns/exception.py b/libs/dns/exception.py index 6c0b1f4b2..939237349 100644 --- a/libs/dns/exception.py +++ b/libs/dns/exception.py @@ -1,4 +1,6 @@ -# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc. +# 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, @@ -13,32 +15,35 @@ # 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.""" +"""Common DNS Exceptions. +Dnspython modules may also define their own exceptions, which will +always be subclasses of ``DNSException``. +""" 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 standard Python Exception class - as before and all *args are printed by standard __str__ implementation. - Class variable msg (or doc string if msg is None) is returned from str() - if *args is empty. + 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 has to be empty and all kwargs has to exactly match - set in class variable self.supp_kwargs. All kwargs are stored inside - self.kwargs and used in new __str__ implementation to construct - formatted message based on self.fmt string. + 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 supp_kwargs and fmt - class variables to get nice parametrized messages. + In the simplest case it is enough to override the ``supp_kwargs`` + and ``fmt`` class variables to get nice parametrized messages. """ + msg = None # non-parametrized message supp_kwargs = set() # accepted parameters for _fmt_kwargs (sanity check) fmt = None # message parametrized with results from _fmt_kwargs @@ -54,9 +59,9 @@ class DNSException(Exception): # doc string is better implicit message than empty string self.msg = self.__doc__ if args: - super(DNSException, self).__init__(*args) + super().__init__(*args) else: - super(DNSException, self).__init__(self.msg) + super().__init__(self.msg) def _check_params(self, *args, **kwargs): """Old exceptions supported only args and not kwargs. @@ -98,31 +103,40 @@ class DNSException(Exception): return self.fmt.format(**fmtargs) else: # print *args directly in the same way as old DNSException - return super(DNSException, self).__str__() + 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 = set(['timeout']) + supp_kwargs = {'timeout'} fmt = "The DNS operation timed out after {timeout} seconds" + + +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/libs/dns/exception.pyi b/libs/dns/exception.pyi new file mode 100644 index 000000000..b29bfbea1 --- /dev/null +++ b/libs/dns/exception.pyi @@ -0,0 +1,10 @@ +from typing import Set, Optional, Dict + +class DNSException(Exception): + supp_kwargs : Set[str] + kwargs : Optional[Dict] + fmt : Optional[str] + +class SyntaxError(DNSException): ... +class FormError(DNSException): ... +class Timeout(DNSException): ... diff --git a/libs/dns/flags.py b/libs/dns/flags.py index 388d6aaa7..965228798 100644 --- a/libs/dns/flags.py +++ b/libs/dns/flags.py @@ -1,4 +1,6 @@ -# Copyright (C) 2001-2007, 2009-2011 Nominum, Inc. +# 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, @@ -15,98 +17,103 @@ """DNS Message Flags.""" +import enum + # Standard DNS flags -QR = 0x8000 -AA = 0x0400 -TC = 0x0200 -RD = 0x0100 -RA = 0x0080 -AD = 0x0020 -CD = 0x0010 +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 -DO = 0x8000 - -_by_text = { - 'QR': QR, - 'AA': AA, - 'TC': TC, - 'RD': RD, - 'RA': RA, - 'AD': AD, - 'CD': CD -} - -_edns_by_text = { - 'DO': DO -} +class EDNSFlag(enum.IntFlag): + #: DNSSEC answer OK + DO = 0x8000 -# We construct the inverse mappings programmatically to ensure that we -# cannot make any mistakes (e.g. omissions, cut-and-paste errors) that -# would cause the mappings not to be true inverses. - -_by_value = dict((y, x) for x, y in _by_text.items()) - -_edns_by_value = dict((y, x) for x, y in _edns_by_text.items()) - - -def _order_flags(table): - order = list(table.items()) - order.sort() - order.reverse() - return order - -_flags_order = _order_flags(_by_value) - -_edns_flags_order = _order_flags(_edns_by_value) - - -def _from_text(text, table): +def _from_text(text, enum_class): flags = 0 tokens = text.split() for t in tokens: - flags = flags | table[t.upper()] + flags |= enum_class[t.upper()] return flags -def _to_text(flags, table, order): +def _to_text(flags, enum_class): text_flags = [] - for k, v in order: - if flags & k != 0: - text_flags.append(v) + for k, v in enum_class.__members__.items(): + if flags & v != 0: + text_flags.append(k) return ' '.join(text_flags) def from_text(text): """Convert a space-separated list of flag text values into a flags value. - @rtype: int""" - return _from_text(text, _by_text) + Returns an ``int`` + """ + + return _from_text(text, Flag) def to_text(flags): """Convert a flags value into a space-separated list of flag text values. - @rtype: string""" - return _to_text(flags, _by_value, _flags_order) + Returns a ``str``. + """ + + return _to_text(flags, Flag) def edns_from_text(text): """Convert a space-separated list of EDNS flag text values into a EDNS flags value. - @rtype: int""" - return _from_text(text, _edns_by_text) + Returns an ``int`` + """ + + return _from_text(text, EDNSFlag) def edns_to_text(flags): """Convert an EDNS flags value into a space-separated list of EDNS flag text values. - @rtype: string""" - return _to_text(flags, _edns_by_value, _edns_flags_order) + 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/libs/dns/grange.py b/libs/dns/grange.py index 9ce9f67a0..112ede47c 100644 --- a/libs/dns/grange.py +++ b/libs/dns/grange.py @@ -1,4 +1,6 @@ -# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc. +# 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, @@ -17,22 +19,21 @@ import dns - def from_text(text): - """Convert the text form of a range in a GENERATE statement to an + """Convert the text form of a range in a ``$GENERATE`` statement to an integer. - @param text: the textual range - @type text: string - @return: The start, stop and step values. - @rtype: tuple - """ - # TODO, figure out the bounds on start, stop and step. + *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 3 4 + # state 0 1 2 # x - y / z if text and text[0] == '-': @@ -42,28 +43,27 @@ def from_text(text): if c == '-' and state == 0: start = int(cur) cur = '' - state = 2 + state = 1 elif c == '/': stop = int(cur) cur = '' - state = 4 + state = 2 elif c.isdigit(): cur += c else: raise dns.exception.SyntaxError("Could not parse %s" % (c)) - if state in (1, 3): - raise dns.exception.SyntaxError() - - if state == 2: + if state == 0: + raise dns.exception.SyntaxError("no stop value specified") + elif state == 1: stop = int(cur) - - if state == 4: + else: + assert state == 2 step = int(cur) assert step >= 1 assert start >= 0 - assert start <= stop - # TODO, can start == stop? + if start > stop: + raise dns.exception.SyntaxError('start must be <= stop') return (start, stop, step) diff --git a/libs/dns/immutable.py b/libs/dns/immutable.py new file mode 100644 index 000000000..db7abbcc1 --- /dev/null +++ b/libs/dns/immutable.py @@ -0,0 +1,70 @@ +# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license + +import collections.abc +import sys + +# pylint: disable=unused-import +if sys.version_info >= (3, 7): + odict = dict + from dns._immutable_ctx import immutable +else: + # pragma: no cover + from collections import OrderedDict as odict + from dns._immutable_attr import immutable # noqa +# pylint: enable=unused-import + + +@immutable +class Dict(collections.abc.Mapping): + def __init__(self, dictionary, no_copy=False): + """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, odict): + self._odict = dictionary + else: + self._odict = odict(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): + """ + 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 = odict() + for k, v in o.items(): + cdict[k] = constify(v) + return Dict(cdict, True) + return o diff --git a/libs/dns/inet.py b/libs/dns/inet.py index 73490a9d7..d3bdc64c8 100644 --- a/libs/dns/inet.py +++ b/libs/dns/inet.py @@ -1,4 +1,6 @@ -# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc. +# 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, @@ -21,36 +23,30 @@ import dns.ipv4 import dns.ipv6 -# We assume that AF_INET is always defined. - +# 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 might not be defined in the socket module, but we need it. -# We'll try to use the socket module's value, and if it doesn't work, -# we'll use our own value. - -try: - AF_INET6 = socket.AF_INET6 -except AttributeError: - AF_INET6 = 9999 +AF_INET6 = socket.AF_INET6 def inet_pton(family, text): """Convert the textual form of a network address into its binary form. - @param family: the address family - @type family: int - @param text: the textual address - @type text: string - @raises NotImplementedError: the address family specified is not + *family* is an ``int``, the address family. + + *text* is a ``str``, the textual address. + + Raises ``NotImplementedError`` if the address family specified is not implemented. - @rtype: string + + Returns a ``bytes``. """ if family == AF_INET: return dns.ipv4.inet_aton(text) elif family == AF_INET6: - return dns.ipv6.inet_aton(text) + return dns.ipv6.inet_aton(text, True) else: raise NotImplementedError @@ -58,14 +54,16 @@ def inet_pton(family, text): def inet_ntop(family, address): """Convert the binary form of a network address into its textual form. - @param family: the address family - @type family: int - @param address: the binary address - @type address: string - @raises NotImplementedError: the address family specified is not + *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. - @rtype: string + + Returns a ``str``. """ + if family == AF_INET: return dns.ipv4.inet_ntoa(address) elif family == AF_INET6: @@ -77,35 +75,96 @@ def inet_ntop(family, address): def af_for_address(text): """Determine the address family of a textual-form network address. - @param text: the textual address - @type text: string - @raises ValueError: the address family cannot be determined from the input. - @rtype: int + *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) + dns.ipv6.inet_aton(text, True) return AF_INET6 - except: + except Exception: raise ValueError def is_multicast(text): """Is the textual-form network address a multicast address? - @param text: the textual address - @raises ValueError: the address family cannot be determined from the input. - @rtype: bool + *text*, a ``str``, the textual address. + + Raises ``ValueError`` if the address family cannot be determined + from the input. + + Returns a ``bool``. """ + try: - first = ord(dns.ipv4.inet_aton(text)[0]) + first = dns.ipv4.inet_aton(text)[0] return first >= 224 and first <= 239 except Exception: try: - first = ord(dns.ipv6.inet_aton(text)[0]) + first = dns.ipv6.inet_aton(text, True)[0] return first == 255 except Exception: raise ValueError + + +def is_address(text): + """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, af=None): + """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}') diff --git a/libs/dns/inet.pyi b/libs/dns/inet.pyi new file mode 100644 index 000000000..6d9dcc70f --- /dev/null +++ b/libs/dns/inet.pyi @@ -0,0 +1,4 @@ +from typing import Union +from socket import AddressFamily + +AF_INET6 : Union[int, AddressFamily] diff --git a/libs/dns/ipv4.py b/libs/dns/ipv4.py index 3fef282b6..e1f38d3d4 100644 --- a/libs/dns/ipv4.py +++ b/libs/dns/ipv4.py @@ -1,4 +1,6 @@ -# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc. +# 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, @@ -18,30 +20,29 @@ import struct import dns.exception -from ._compat import binary_type def inet_ntoa(address): - """Convert an IPv4 address in network form to text form. + """Convert an IPv4 address in binary form to text form. - @param address: The IPv4 address - @type address: string - @returns: string + *address*, a ``bytes``, the IPv4 address in binary form. + + Returns a ``str``. """ + if len(address) != 4: raise dns.exception.SyntaxError - if not isinstance(address, bytearray): - address = bytearray(address) - return (u'%u.%u.%u.%u' % (address[0], address[1], - address[2], address[3])).encode() + return ('%u.%u.%u.%u' % (address[0], address[1], + address[2], address[3])) def inet_aton(text): - """Convert an IPv4 address in text form to network form. + """Convert an IPv4 address in text form to binary form. - @param text: The IPv4 address - @type text: string - @returns: string + *text*, a ``str``, the IPv4 address in textual form. + + Returns a ``bytes``. """ - if not isinstance(text, binary_type): + + if not isinstance(text, bytes): text = text.encode() parts = text.split(b'.') if len(parts) != 4: @@ -49,11 +50,11 @@ def inet_aton(text): for part in parts: if not part.isdigit(): raise dns.exception.SyntaxError - if len(part) > 1 and part[0] == '0': + if len(part) > 1 and part[0] == ord('0'): # No leading zeros raise dns.exception.SyntaxError try: - bytes = [int(part) for part in parts] - return struct.pack('BBBB', *bytes) - except: + b = [int(part) for part in parts] + return struct.pack('BBBB', *b) + except Exception: raise dns.exception.SyntaxError diff --git a/libs/dns/ipv6.py b/libs/dns/ipv6.py index cbaee8edc..0db6fcfaa 100644 --- a/libs/dns/ipv6.py +++ b/libs/dns/ipv6.py @@ -1,4 +1,6 @@ -# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc. +# 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, @@ -20,17 +22,16 @@ import binascii import dns.exception import dns.ipv4 -from ._compat import xrange, binary_type, maybe_decode -_leading_zero = re.compile(b'0+([0-9a-f]+)') +_leading_zero = re.compile(r'0+([0-9a-f]+)') def inet_ntoa(address): - """Convert a network format IPv6 address into text. + """Convert an IPv6 address in binary form to text form. - @param address: the binary address - @type address: string - @rtype: string - @raises ValueError: the address isn't 16 bytes long + *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: @@ -40,12 +41,12 @@ def inet_ntoa(address): i = 0 l = len(hex) while i < l: - chunk = hex[i : i + 4] + 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 not m is None: + if m is not None: chunk = m.group(1) chunks.append(chunk) i += 4 @@ -56,8 +57,8 @@ def inet_ntoa(address): best_len = 0 start = -1 last_was_zero = False - for i in xrange(8): - if chunks[i] != b'0': + for i in range(8): + if chunks[i] != '0': if last_was_zero: end = i current_len = end - start @@ -77,59 +78,76 @@ def inet_ntoa(address): if best_len > 1: if best_start == 0 and \ (best_len == 6 or - best_len == 5 and chunks[5] == b'ffff'): + best_len == 5 and chunks[5] == 'ffff'): # We have an embedded IPv4 address if best_len == 6: - prefix = b'::' + prefix = '::' else: - prefix = b'::ffff:' + prefix = '::ffff:' hex = prefix + dns.ipv4.inet_ntoa(address[12:]) else: - hex = b':'.join(chunks[:best_start]) + b'::' + \ - b':'.join(chunks[best_start + best_len:]) + hex = ':'.join(chunks[:best_start]) + '::' + \ + ':'.join(chunks[best_start + best_len:]) else: - hex = b':'.join(chunks) - return maybe_decode(hex) + hex = ':'.join(chunks) + return hex -_v4_ending = re.compile(b'(.*):(\d+\.\d+\.\d+\.\d+)$') -_colon_colon_start = re.compile(b'::.*') -_colon_colon_end = re.compile(b'.*::$') +_v4_ending = re.compile(br'(.*):(\d+\.\d+\.\d+\.\d+)$') +_colon_colon_start = re.compile(br'::.*') +_colon_colon_end = re.compile(br'.*::$') -def inet_aton(text): - """Convert a text format IPv6 address into network format. +def inet_aton(text, ignore_scope=False): + """Convert an IPv6 address in text form to binary form. - @param text: the textual address - @type text: string - @rtype: string - @raises dns.exception.SyntaxError: the text was not properly formatted + *text*, a ``str``, 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, binary_type): + if not isinstance(text, bytes): text = text.encode() - if text == b'::': + if ignore_scope: + parts = text.split(b'%') + l = len(parts) + if l == 2: + text = parts[0] + elif l > 2: + raise dns.exception.SyntaxError + + if text == b'': + raise dns.exception.SyntaxError + elif text.endswith(b':') and not text.endswith(b'::'): + raise dns.exception.SyntaxError + elif text.startswith(b':') and not text.startswith(b'::'): + raise dns.exception.SyntaxError + elif text == b'::': text = b'0::' # # Get rid of the icky dot-quad syntax if we have it. # m = _v4_ending.match(text) - if not m is None: - b = bytearray(dns.ipv4.inet_aton(m.group(2))) - text = (u"%s:%02x%02x:%02x%02x" % (m.group(1).decode(), b[0], b[1], - b[2], b[3])).encode() + if m is not None: + b = dns.ipv4.inet_aton(m.group(2)) + text = ("{}:{: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(text) - if not m is None: + if m is not None: text = text[1:] else: m = _colon_colon_end.match(text) - if not m is None: + if m is not None: text = text[:-1] # # Now canonicalize into 8 chunks of 4 hex digits each @@ -145,7 +163,7 @@ def inet_aton(text): if seen_empty: raise dns.exception.SyntaxError seen_empty = True - for i in xrange(0, 8 - l + 1): + for _ in range(0, 8 - l + 1): canonical.append(b'0000') else: lc = len(c) @@ -169,4 +187,11 @@ def inet_aton(text): _mapped_prefix = b'\x00' * 10 + b'\xff\xff' def is_mapped(address): + """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) diff --git a/libs/dns/message.py b/libs/dns/message.py index a0df18e67..1e67a17b8 100644 --- a/libs/dns/message.py +++ b/libs/dns/message.py @@ -1,4 +1,6 @@ -# Copyright (C) 2001-2007, 2009-2011 Nominum, Inc. +# 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, @@ -15,13 +17,13 @@ """DNS Messages""" -from __future__ import absolute_import - -from io import StringIO -import struct +import contextlib +import io import time +import dns.wire import dns.edns +import dns.enum import dns.exception import dns.flags import dns.name @@ -33,121 +35,92 @@ import dns.rdataclass import dns.rdatatype import dns.rrset import dns.renderer +import dns.ttl import dns.tsig -import dns.wiredata - -from ._compat import long, xrange, string_types +import dns.rdtypes.ANY.OPT +import dns.rdtypes.ANY.TSIG 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): - - """OPT record occurred somewhere other than the start of + """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 Message(object): +class Truncated(dns.exception.DNSException): + """The truncated flag is set.""" - """A DNS message. + supp_kwargs = {'message'} - @ivar id: The query id; the default is a randomly chosen id. - @type id: int - @ivar flags: The DNS flags of the message. @see: RFC 1035 for an - explanation of these flags. - @type flags: int - @ivar question: The question section. - @type question: list of dns.rrset.RRset objects - @ivar answer: The answer section. - @type answer: list of dns.rrset.RRset objects - @ivar authority: The authority section. - @type authority: list of dns.rrset.RRset objects - @ivar additional: The additional data section. - @type additional: list of dns.rrset.RRset objects - @ivar edns: The EDNS level to use. The default is -1, no Edns. - @type edns: int - @ivar ednsflags: The EDNS flags - @type ednsflags: long - @ivar payload: The EDNS payload size. The default is 0. - @type payload: int - @ivar options: The EDNS options - @type options: list of dns.edns.Option objects - @ivar request_payload: The associated request's EDNS payload size. - @type request_payload: int - @ivar keyring: The TSIG keyring to use. The default is None. - @type keyring: dict - @ivar keyname: The TSIG keyname to use. The default is None. - @type keyname: dns.name.Name object - @ivar keyalgorithm: The TSIG algorithm to use; defaults to - dns.tsig.default_algorithm. Constants for TSIG algorithms are defined - in dns.tsig, and the currently implemented algorithms are - HMAC_MD5, HMAC_SHA1, HMAC_SHA224, HMAC_SHA256, HMAC_SHA384, and - HMAC_SHA512. - @type keyalgorithm: string - @ivar request_mac: The TSIG MAC of the request message associated with - this message; used when validating TSIG signatures. @see: RFC 2845 for - more information on TSIG fields. - @type request_mac: string - @ivar fudge: TSIG time fudge; default is 300 seconds. - @type fudge: int - @ivar original_id: TSIG original id; defaults to the message's id - @type original_id: int - @ivar tsig_error: TSIG error code; default is 0. - @type tsig_error: int - @ivar other_data: TSIG other data. - @type other_data: string - @ivar mac: The TSIG MAC for this message. - @type mac: string - @ivar xfr: Is the message being used to contain the results of a DNS - zone transfer? The default is False. - @type xfr: bool - @ivar origin: The origin of the zone in messages which are used for - zone transfers or for DNS dynamic updates. The default is None. - @type origin: dns.name.Name object - @ivar tsig_ctx: The TSIG signature context associated with this - message. The default is None. - @type tsig_ctx: hmac.HMAC object - @ivar had_tsig: Did the message decoded from wire format have a TSIG - signature? - @type had_tsig: bool - @ivar multi: Is this message part of a multi-message sequence? The - default is false. This variable is used when validating TSIG signatures - on messages which are part of a zone transfer. - @type multi: bool - @ivar first: Is this message standalone, or the first of a multi - message sequence? This variable is used when validating TSIG signatures - on messages which are part of a zone transfer. - @type first: bool - @ivar index: An index of rrsets in the message. The index key is - (section, name, rdclass, rdtype, covers, deleting). Indexing can be - disabled by setting the index to None. - @type index: dict - """ + 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, offset): + self.exception = exception + self.offset = offset + + +DEFAULT_EDNS_PAYLOAD = 1232 +MAX_CHAIN = 16 + +class Message: + """A DNS message.""" + + _section_enum = MessageSection def __init__(self, id=None): if id is None: @@ -155,31 +128,53 @@ class Message(object): else: self.id = id self.flags = 0 - self.question = [] - self.answer = [] - self.authority = [] - self.additional = [] - self.edns = -1 - self.ednsflags = 0 - self.payload = 0 - self.options = [] + self.sections = [[], [], [], []] + self.opt = None self.request_payload = 0 self.keyring = None - self.keyname = None - self.keyalgorithm = dns.tsig.default_algorithm - self.request_mac = '' - self.other_data = '' - self.tsig_error = 0 - self.fudge = 300 - self.original_id = self.id - self.mac = '' + self.tsig = None + self.request_mac = b'' self.xfr = False self.origin = None self.tsig_ctx = None - self.had_tsig = False - self.multi = False - self.first = True self.index = {} + self.errors = [] + + @property + def question(self): + """ The question section.""" + return self.sections[0] + + @question.setter + def question(self, v): + self.sections[0] = v + + @property + def answer(self): + """ The answer section.""" + return self.sections[1] + + @answer.setter + def answer(self, v): + self.sections[1] = v + + @property + def authority(self): + """ The authority section.""" + return self.sections[2] + + @authority.setter + def authority(self, v): + self.sections[2] = v + + @property + def additional(self): + """ The additional data section.""" + return self.sections[3] + + @additional.setter + def additional(self, v): + self.sections[3] = v def __repr__(self): return '' @@ -190,51 +185,30 @@ class Message(object): def to_text(self, origin=None, relativize=True, **kw): """Convert the message to text. - The I{origin}, I{relativize}, and any other keyword - arguments are passed to the rrset to_wire() method. + The *origin*, *relativize*, and any other keyword + arguments are passed to the RRset ``to_wire()`` method. - @rtype: string + Returns a ``str``. """ - s = StringIO() - s.write(u'id %d\n' % self.id) - s.write(u'opcode %s\n' % - dns.opcode.to_text(dns.opcode.from_flags(self.flags))) - rc = dns.rcode.from_flags(self.flags, self.ednsflags) - s.write(u'rcode %s\n' % dns.rcode.to_text(rc)) - s.write(u'flags %s\n' % dns.flags.to_text(self.flags)) + 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(u'edns %s\n' % self.edns) + s.write('edns %s\n' % self.edns) if self.ednsflags != 0: - s.write(u'eflags %s\n' % + s.write('eflags %s\n' % dns.flags.edns_to_text(self.ednsflags)) - s.write(u'payload %d\n' % self.payload) - is_update = dns.opcode.is_update(self.flags) - if is_update: - s.write(u';ZONE\n') - else: - s.write(u';QUESTION\n') - for rrset in self.question: - s.write(rrset.to_text(origin, relativize, **kw)) - s.write(u'\n') - if is_update: - s.write(u';PREREQ\n') - else: - s.write(u';ANSWER\n') - for rrset in self.answer: - s.write(rrset.to_text(origin, relativize, **kw)) - s.write(u'\n') - if is_update: - s.write(u';UPDATE\n') - else: - s.write(u';AUTHORITY\n') - for rrset in self.authority: - s.write(rrset.to_text(origin, relativize, **kw)) - s.write(u'\n') - s.write(u';ADDITIONAL\n') - for rrset in self.additional: - s.write(rrset.to_text(origin, relativize, **kw)) - s.write(u'\n') + 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 @@ -245,50 +219,53 @@ class Message(object): def __eq__(self, other): """Two messages are equal if they have the same content in the header, question, answer, and authority sections. - @rtype: bool""" + + 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 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 - for n in self.answer: - if n not in other.answer: - return False - for n in other.answer: - if n not in self.answer: - return False - for n in self.authority: - if n not in other.authority: - return False - for n in other.authority: - if n not in self.authority: - 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): - """Are two messages not equal? - @rtype: bool""" return not self.__eq__(other) def is_response(self, other): - """Is other a response to self? - @rtype: 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 dns.rcode.from_flags(other.flags, other.ednsflags) != \ - dns.rcode.NOERROR: - return True + 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: @@ -299,46 +276,80 @@ class Message(object): return True def section_number(self, section): - if section is self.question: - return 0 - elif section is self.answer: - return 1 - elif section is self.authority: - return 2 - elif section is self.additional: - return 3 - else: - raise ValueError('unknown section') + """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): + """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, name, rdclass, rdtype, covers=dns.rdatatype.NONE, deleting=None, create=False, force_unique=False): """Find the RRset with the given attributes in the specified section. - @param section: the section of the message to look in, e.g. - self.answer. - @type section: list of dns.rrset.RRset objects - @param name: the name of the RRset - @type name: dns.name.Name object - @param rdclass: the class of the RRset - @type rdclass: int - @param rdtype: the type of the RRset - @type rdtype: int - @param covers: the covers value of the RRset - @type covers: int - @param deleting: the deleting value of the RRset - @type deleting: int - @param create: If True, create the RRset if it is not found. - The created RRset is appended to I{section}. - @type create: bool - @param force_unique: If True and create is also True, create a - new RRset regardless of whether a matching RRset exists already. - @type force_unique: bool - @raises KeyError: the RRset was not found and create was False - @rtype: dns.rrset.RRset object""" + *section*, an ``int`` section number, or one of the section + attributes of this message. This specifies the + the section of the message to search. For example:: - key = (self.section_number(section), - name, rdclass, rdtype, covers, deleting) + my_message.find_rrset(my_message.answer, name, rdclass, rdtype) + my_message.find_rrset(dns.message.ANSWER, name, rdclass, rdtype) + + *name*, a ``dns.name.Name``, the name of the RRset. + + *rdclass*, an ``int``, the class of the RRset. + + *rdtype*, an ``int``, the type of the RRset. + + *covers*, an ``int`` or ``None``, the covers value of the RRset. + The default is ``None``. + + *deleting*, an ``int`` 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. + + 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) + else: + section_number = self.section_number(section) + key = (section_number, name, rdclass, rdtype, covers, deleting) if not force_unique: if self.index is not None: rrset = self.index.get(key) @@ -346,7 +357,8 @@ class Message(object): return rrset else: for rrset in section: - if rrset.match(name, rdclass, rdtype, covers, deleting): + if rrset.full_match(name, rdclass, rdtype, covers, + deleting): return rrset if not create: raise KeyError @@ -363,26 +375,35 @@ class Message(object): If the RRset is not found, None is returned. - @param section: the section of the message to look in, e.g. - self.answer. - @type section: list of dns.rrset.RRset objects - @param name: the name of the RRset - @type name: dns.name.Name object - @param rdclass: the class of the RRset - @type rdclass: int - @param rdtype: the type of the RRset - @type rdtype: int - @param covers: the covers value of the RRset - @type covers: int - @param deleting: the deleting value of the RRset - @type deleting: int - @param create: If True, create the RRset if it is not found. - The created RRset is appended to I{section}. - @type create: bool - @param force_unique: If True and create is also True, create a - new RRset regardless of whether a matching RRset exists already. - @type force_unique: bool - @rtype: dns.rrset.RRset object or None""" + *section*, an ``int`` section number, 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) + + *name*, a ``dns.name.Name``, the name of the RRset. + + *rdclass*, an ``int``, the class of the RRset. + + *rdtype*, an ``int``, the type of the RRset. + + *covers*, an ``int`` or ``None``, the covers value of the RRset. + The default is ``None``. + + *deleting*, an ``int`` 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. + + Returns a ``dns.rrset.RRset object`` or ``None``. + """ try: rrset = self.find_rrset(section, name, rdclass, rdtype, covers, @@ -391,23 +412,35 @@ class Message(object): rrset = None return rrset - def to_wire(self, origin=None, max_size=0, **kw): + def to_wire(self, origin=None, max_size=0, multi=False, tsig_ctx=None, + **kw): """Return a string containing the message in DNS compressed wire format. - Additional keyword arguments are passed to the rrset to_wire() + Additional keyword arguments are passed to the RRset ``to_wire()`` method. - @param origin: The origin to be appended to any relative names. - @type origin: dns.name.Name object - @param max_size: The maximum size of the wire format output; default - is 0, which means 'the message's request payload, if nonzero, or - 65536'. - @type max_size: int - @raises dns.exception.TooBig: max_size was exceeded - @rtype: string + *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. + + 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 @@ -424,469 +457,762 @@ class Message(object): r.add_rrset(dns.renderer.ANSWER, rrset, **kw) for rrset in self.authority: r.add_rrset(dns.renderer.AUTHORITY, rrset, **kw) - if self.edns >= 0: - r.add_edns(self.edns, self.ednsflags, self.payload, self.options) + if self.opt is not None: + r.add_rrset(dns.renderer.ADDITIONAL, self.opt) for rrset in self.additional: r.add_rrset(dns.renderer.ADDITIONAL, rrset, **kw) r.write_header() - if self.keyname is not None: - r.add_tsig(self.keyname, self.keyring[self.keyname], - self.fudge, self.original_id, self.tsig_error, - self.other_data, self.request_mac, - self.keyalgorithm) - self.mac = r.mac + 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 return r.get_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, keyname=None, fudge=300, - original_id=None, tsig_error=0, other_data='', + original_id=None, tsig_error=0, other_data=b'', algorithm=dns.tsig.default_algorithm): - """When sending, a TSIG signature using the specified keyring - and keyname should be added. + """When sending, a TSIG signature using the specified key + should be added. - @param keyring: The TSIG keyring to use; defaults to None. - @type keyring: dict - @param keyname: The name of the TSIG key to use; defaults to None. - The key must be defined in the keyring. If a keyring is specified - but a keyname is not, then 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 keyring is used, unless - they know the keyring contains only one key. - @type keyname: dns.name.Name or string - @param fudge: TSIG time fudge; default is 300 seconds. - @type fudge: int - @param original_id: TSIG original id; defaults to the message's id - @type original_id: int - @param tsig_error: TSIG error code; default is 0. - @type tsig_error: int - @param other_data: TSIG other data. - @type other_data: string - @param algorithm: The TSIG algorithm to use; defaults to - dns.tsig.default_algorithm + *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 + thes 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``, the TSIG algorithm to use. This is + only used if *keyring* is a ``dict``, and the key entry is a ``bytes``. """ - self.keyring = keyring - if keyname is None: - self.keyname = list(self.keyring.keys())[0] + if isinstance(keyring, dns.tsig.Key): + key = keyring + keyname = key.name + elif callable(keyring): + key = keyring(self, keyname) else: - if isinstance(keyname, string_types): + if isinstance(keyname, str): keyname = dns.name.from_text(keyname) - self.keyname = keyname - self.keyalgorithm = algorithm - self.fudge = fudge + 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: - self.original_id = self.id - else: - self.original_id = original_id - self.tsig_error = tsig_error - self.other_data = other_data + original_id = self.id + self.tsig = self._make_tsig(keyname, self.keyring.algorithm, 0, fudge, + b'', original_id, tsig_error, other_data) - def use_edns(self, edns=0, ednsflags=0, payload=1280, request_payload=None, - options=None): + @property + def keyname(self): + if self.tsig: + return self.tsig.name + else: + return None + + @property + def keyalgorithm(self): + if self.tsig: + return self.tsig[0].algorithm + else: + return None + + @property + def mac(self): + if self.tsig: + return self.tsig[0].mac + else: + return None + + @property + def tsig_error(self): + if self.tsig: + return self.tsig[0].error + else: + return None + + @property + def had_tsig(self): + 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=0, ednsflags=0, payload=DEFAULT_EDNS_PAYLOAD, + request_payload=None, options=None): """Configure EDNS behavior. - @param edns: 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'. - @type edns: int or bool or None - @param ednsflags: EDNS flag values. - @type ednsflags: int - @param payload: The EDNS sender's payload field, which is the maximum - size of UDP datagram the sender can handle. - @type payload: int - @param request_payload: The EDNS payload size to use when sending - this message. If not specified, defaults to the value of payload. - @type request_payload: int or None - @param options: The EDNS options - @type options: None or list of dns.edns.Option objects - @see: RFC 2671 + + *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. """ + if edns is None or edns is False: edns = -1 - if edns is True: + elif edns is True: edns = 0 - if request_payload is None: - request_payload = payload if edns < 0: - ednsflags = 0 - payload = 0 - request_payload = 0 - options = [] + self.opt = None + self.request_payload = 0 else: # make sure the EDNS version in ednsflags agrees with edns - ednsflags &= long(0xFF00FFFF) + ednsflags &= 0xFF00FFFF ednsflags |= (edns << 16) if options is None: options = [] - self.edns = edns - self.ednsflags = ednsflags - self.payload = payload - self.options = options - self.request_payload = request_payload + self.opt = self._make_opt(ednsflags, payload, options) + if request_payload is None: + request_payload = payload + self.request_payload = request_payload + + @property + def edns(self): + if self.opt: + return (self.ednsflags & 0xff0000) >> 16 + else: + return -1 + + @property + def ednsflags(self): + 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): + if self.opt: + return self.opt[0].payload + else: + return 0 + + @property + def options(self): + if self.opt: + return self.opt[0].options + else: + return () def want_dnssec(self, wanted=True): """Enable or disable 'DNSSEC desired' flag in requests. - @param wanted: Is DNSSEC desired? If True, EDNS is enabled if - required, and then the DO bit is set. If False, the DO bit is - cleared if EDNS is enabled. - @type wanted: bool + + *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: - if self.edns < 0: - self.use_edns() self.ednsflags |= dns.flags.DO - elif self.edns >= 0: + elif self.opt: self.ednsflags &= ~dns.flags.DO def rcode(self): """Return the rcode. - @rtype: int + + Returns an ``int``. """ - return dns.rcode.from_flags(self.flags, self.ednsflags) + return dns.rcode.from_flags(int(self.flags), int(self.ednsflags)) def set_rcode(self, rcode): """Set the rcode. - @param rcode: the rcode - @type rcode: int + + *rcode*, an ``int``, is the rcode to set. """ (value, evalue) = dns.rcode.to_flags(rcode) self.flags &= 0xFFF0 self.flags |= value - self.ednsflags &= long(0x00FFFFFF) + self.ednsflags &= 0x00FFFFFF self.ednsflags |= evalue - if self.ednsflags != 0 and self.edns < 0: - self.edns = 0 def opcode(self): """Return the opcode. - @rtype: int + + Returns an ``int``. """ - return dns.opcode.from_flags(self.flags) + return dns.opcode.from_flags(int(self.flags)) def set_opcode(self, opcode): """Set the opcode. - @param opcode: the opcode - @type opcode: int + + *opcode*, an ``int``, 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 -class _WireReader(object): + # 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 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, answer, minimum_ttl, cnames): + self.canonical_name = canonical_name + self.answer = answer + self.minimum_ttl = minimum_ttl + self.cnames = cnames + + +class QueryMessage(Message): + def resolve_chaining(self): + """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): + """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. - @ivar wire: the wire-format message. - @type wire: string - @ivar message: The message object being built - @type message: dns.message.Message object - @ivar current: When building a message object from wire format, this - variable contains the offset from the beginning of wire of the next octet - to be read. - @type current: int - @ivar updating: Is the message a dynamic update? - @type updating: bool - @ivar one_rr_per_rrset: Put each RR into its own RRset? - @type one_rr_per_rrset: bool - @ivar ignore_trailing: Ignore trailing junk at end of request? - @type ignore_trailing: bool - @ivar zone_rdclass: The class of the zone in messages which are + 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. - @type zone_rdclass: int + 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, message, question_only=False, - one_rr_per_rrset=False, ignore_trailing=False): - self.wire = dns.wiredata.maybe_wrap(wire) - self.message = message - self.current = 0 - self.updating = False - self.zone_rdclass = dns.rdataclass.IN + 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, qcount): - """Read the next I{qcount} records from the wire data and add them to + def _get_question(self, section_number, qcount): + """Read the next *qcount* records from the wire data and add them to the question section. - @param qcount: the number of questions in the message - @type qcount: int""" + """ - if self.updating and qcount > 1: - raise dns.exception.FormError + 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) - for i in xrange(0, qcount): - (qname, used) = dns.name.from_wire(self.wire, self.current) - if self.message.origin is not None: - qname = qname.relativize(self.message.origin) - self.current = self.current + used - (rdtype, rdclass) = \ - struct.unpack('!HH', - self.wire[self.current:self.current + 4]) - self.current = self.current + 4 - self.message.find_rrset(self.message.question, qname, - rdclass, rdtype, create=True, - force_unique=True) - if self.updating: - self.zone_rdclass = rdclass + def _add_error(self, e): + self.errors.append(MessageError(e, self.parser.current)) - def _get_section(self, section, count): + def _get_section(self, section_number, count): """Read the next I{count} records from the wire data and add them to the specified section. - @param section: the section of the message to which to add records - @type section: list of dns.rrset.RRset objects - @param count: the number of records to read - @type count: int""" - if self.updating or self.one_rr_per_rrset: - force_unique = True - else: - force_unique = False - seen_opt = False - for i in xrange(0, count): - rr_start = self.current - (name, used) = dns.name.from_wire(self.wire, self.current) - absolute_name = name + section_number: the section of the message to which to add records + count: the number of records to read + """ + + 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 = name.relativize(self.message.origin) - self.current = self.current + used - (rdtype, rdclass, ttl, rdlen) = \ - struct.unpack('!HHIH', - self.wire[self.current:self.current + 10]) - self.current = self.current + 10 - if rdtype == dns.rdatatype.OPT: - if section is not self.message.additional or seen_opt: - raise BadEDNS - self.message.payload = rdclass - self.message.ednsflags = ttl - self.message.edns = (ttl & 0xff0000) >> 16 - self.message.options = [] - current = self.current - optslen = rdlen - while optslen > 0: - (otype, olen) = \ - struct.unpack('!HH', - self.wire[current:current + 4]) - current = current + 4 - opt = dns.edns.option_from_wire( - otype, self.wire, current, olen) - self.message.options.append(opt) - current = current + olen - optslen = optslen - 4 - olen - seen_opt = True - elif rdtype == dns.rdatatype.TSIG: - if not (section is self.message.additional and - i == (count - 1)): - raise BadTSIG - if self.message.keyring is None: - raise UnknownTSIGKey('got signed message without keyring') - secret = self.message.keyring.get(absolute_name) - if secret is None: - raise UnknownTSIGKey("key '%s' unknown" % name) - self.message.keyname = absolute_name - (self.message.keyalgorithm, self.message.mac) = \ - dns.tsig.get_algorithm_and_mac(self.wire, self.current, - rdlen) - self.message.tsig_ctx = \ - dns.tsig.validate(self.wire, - absolute_name, - secret, - int(time.time()), - self.message.request_mac, - rr_start, - self.current, - rdlen, - self.message.tsig_ctx, - self.message.multi, - self.message.first) - self.message.had_tsig = True + name = absolute_name.relativize(self.message.origin) else: - if ttl < 0: - ttl = 0 - if self.updating and \ - (rdclass == dns.rdataclass.ANY or - rdclass == dns.rdataclass.NONE): - deleting = rdclass - rdclass = self.zone_rdclass - else: - deleting = None - if deleting == dns.rdataclass.ANY or \ - (deleting == dns.rdataclass.NONE and - section is self.message.answer): - covers = dns.rdatatype.NONE + 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) + try: + rdata_start = self.parser.current + if empty: + if rdlen > 0: + raise dns.exception.FormError rd = None + covers = dns.rdatatype.NONE else: - rd = dns.rdata.from_wire(rdclass, rdtype, self.wire, - self.current, rdlen, - self.message.origin) + 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 - rrset = self.message.find_rrset(section, name, - rdclass, rdtype, covers, - deleting, True, force_unique) - if rd is not None: - rrset.add(rd, ttl) - self.current = self.current + rdlen + 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.""" - l = len(self.wire) - if l < 12: + if self.parser.remaining() < 12: raise ShortHeader - (self.message.id, self.message.flags, qcount, ancount, - aucount, adcount) = struct.unpack('!HHHHHH', self.wire[:12]) - self.current = 12 - if dns.opcode.is_update(self.message.flags): - self.updating = True - self._get_question(qcount) - if self.question_only: - return - self._get_section(self.message.answer, ancount) - self._get_section(self.message.authority, aucount) - self._get_section(self.message.additional, adcount) - if not self.ignore_trailing and self.current != l: - raise TrailingJunk - if self.message.multi and self.message.tsig_ctx and \ + (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.wire) + 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, keyring=None, request_mac='', xfr=False, origin=None, - tsig_ctx=None, multi=False, first=True, +def from_wire(wire, keyring=None, request_mac=b'', xfr=False, origin=None, + tsig_ctx=None, multi=False, question_only=False, one_rr_per_rrset=False, - ignore_trailing=False): - """Convert a DNS wire format message into a message - object. + ignore_trailing=False, raise_on_truncation=False, + continue_on_error=False): + """Convert a DNS wire format message into a message object. - @param keyring: The keyring to use if the message is signed. - @type keyring: dict - @param request_mac: If the message is a response to a TSIG-signed request, - I{request_mac} should be set to the MAC of that request. - @type request_mac: string - @param xfr: Is this message part of a zone transfer? - @type xfr: bool - @param origin: If the message is part of a zone transfer, I{origin} - should be the origin name of the zone. - @type origin: dns.name.Name object - @param tsig_ctx: The ongoing TSIG context, used when validating zone - transfers. - @type tsig_ctx: hmac.HMAC object - @param multi: Is this message part of a multiple message sequence? - @type multi: bool - @param first: Is this message standalone, or the first of a multi - message sequence? - @type first: bool - @param question_only: Read only up to the end of the question section? - @type question_only: bool - @param one_rr_per_rrset: Put each RR into its own RRset - @type one_rr_per_rrset: bool - @param ignore_trailing: Ignore trailing junk at end of request? - @type ignore_trailing: bool - @raises ShortHeader: The message is less than 12 octets long. - @raises TrailingJunk: There were octets in the message past the end - of the proper DNS message. - @raises BadEDNS: An OPT record was in the wrong section, or occurred more - than once. - @raises BadTSIG: A TSIG record was not the last record of the additional - data section. - @rtype: dns.message.Message object""" + *keyring*, a ``dns.tsig.Key`` or ``dict``, the key or keyring to use if the + message is signed. - m = Message(id=0) - m.keyring = keyring - m.request_mac = request_mac - m.xfr = xfr - m.origin = origin - m.tsig_ctx = tsig_ctx - m.multi = multi - m.first = first + *request_mac*, a ``bytes``. If the message is a response to a TSIG-signed + request, *request_mac* should be set to the MAC of that request. - reader = _WireReader(wire, m, question_only, one_rr_per_rrset, - ignore_trailing) - reader.read() + *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``. + """ + + 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(object): +class _TextReader: """Text format reader. - @ivar tok: the tokenizer - @type tok: dns.tokenizer.Tokenizer object - @ivar message: The message object being built - @type message: dns.message.Message object - @ivar updating: Is the message a dynamic update? - @type updating: bool - @ivar zone_rdclass: The class of the zone in messages which are + tok: the tokenizer. + message: The message object being built. DNS dynamic updates. - @type zone_rdclass: int - @ivar last_name: The most recently read name when building a message object - from text format. - @type last_name: dns.name.Name object + 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, message): - self.message = message - self.tok = dns.tokenizer.Tokenizer(text) + 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.zone_rdclass = dns.rdataclass.IN - self.updating = False + 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, section): + def _header_line(self, _): """Process one line from the text format header section.""" token = self.tok.get() what = token.value if what == 'id': - self.message.id = self.tok.get_int() + 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.message.flags = self.message.flags | \ - dns.flags.from_text(token.value) - if dns.opcode.is_update(self.message.flags): - self.updating = True + self.flags = self.flags | dns.flags.from_text(token.value) elif what == 'edns': - self.message.edns = self.tok.get_int() - self.message.ednsflags = self.message.ednsflags | \ - (self.message.edns << 16) + self.edns = self.tok.get_int() + self.ednsflags = self.ednsflags | (self.edns << 16) elif what == 'eflags': - if self.message.edns < 0: - self.message.edns = 0 + if self.edns < 0: + self.edns = 0 while True: token = self.tok.get() if not token.is_identifier(): self.tok.unget(token) break - self.message.ednsflags = self.message.ednsflags | \ + self.ednsflags = self.ednsflags | \ dns.flags.edns_from_text(token.value) elif what == 'payload': - self.message.payload = self.tok.get_int() - if self.message.edns < 0: - self.message.edns = 0 + self.payload = self.tok.get_int() + if self.edns < 0: + self.edns = 0 elif what == 'opcode': text = self.tok.get_string() - self.message.flags = self.message.flags | \ - dns.opcode.to_flags(dns.opcode.from_text(text)) + 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.message.set_rcode(dns.rcode.from_text(text)) + self.rcode = dns.rcode.from_text(text) else: raise UnknownHeaderField self.tok.get_eol() - def _question_line(self, section): + 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 = dns.name.from_text(token.value, None) + 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 @@ -902,24 +1228,27 @@ class _TextReader(object): rdclass = dns.rdataclass.IN # Type rdtype = dns.rdatatype.from_text(token.value) - self.message.find_rrset(self.message.question, name, - rdclass, rdtype, create=True, + (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) - if self.updating: - self.zone_rdclass = rdclass self.tok.get_eol() - def _rr_line(self, section): + def _rr_line(self, section_number): """Process one line from the text format answer, authority, or additional data sections. """ - deleting = None + section = self.message.sections[section_number] # Name token = self.tok.get(want_leading=True) if not token.is_whitespace(): - self.last_name = dns.name.from_text(token.value, None) + 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 @@ -939,35 +1268,52 @@ class _TextReader(object): token = self.tok.get() if not token.is_identifier(): raise dns.exception.SyntaxError - if rdclass == dns.rdataclass.ANY or rdclass == dns.rdataclass.NONE: - deleting = rdclass - rdclass = self.zone_rdclass 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, None) + 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.updating) + 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 = None + section_number = None while 1: token = self.tok.get(True, True) if token.is_eol_or_eof(): @@ -976,74 +1322,110 @@ class _TextReader(object): u = token.value.upper() if u == 'HEADER': line_method = self._header_line - elif u == 'QUESTION' or u == 'ZONE': - line_method = self._question_line - section = self.message.question - elif u == 'ANSWER' or u == 'PREREQ': - line_method = self._rr_line - section = self.message.answer - elif u == 'AUTHORITY' or u == 'UPDATE': - line_method = self._rr_line - section = self.message.authority - elif u == 'ADDITIONAL': - line_method = self._rr_line - section = self.message.additional + + 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) + line_method(section_number) + if not self.message: + self.message = self._make_message() + return self.message -def from_text(text): +def from_text(text, idna_codec=None, one_rr_per_rrset=False, + origin=None, relativize=True, relativize_to=None): """Convert the text format message into a message object. - @param text: The text format message. - @type text: string - @raises UnknownHeaderField: - @raises dns.exception.SyntaxError: - @rtype: dns.message.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(). - m = Message() - - reader = _TextReader(text, m) - reader.read() - - return m + reader = _TextReader(text, idna_codec, one_rr_per_rrset, origin, + relativize, relativize_to) + return reader.read() -def from_file(f): +def from_file(f, idna_codec=None, one_rr_per_rrset=False): """Read the next text format message from the specified file. - @param f: file or string. If I{f} is a string, it is treated - as the name of a file to open. - @raises UnknownHeaderField: - @raises dns.exception.SyntaxError: - @rtype: dns.message.Message object""" + Message blocks are separated by a single blank line. - str_type = string_types - opts = 'rU' + *f*, a ``file`` or ``str``. If *f* is text, it is treated as the + pathname of a file to open. - if isinstance(f, str_type): - f = open(f, opts) - want_close = True - else: - want_close = False + *idna_codec*, a ``dns.name.IDNACodec``, specifies the IDNA + encoder/decoder. If ``None``, the default IDNA 2003 encoder/decoder + is used. - try: - m = from_text(f) - finally: - if want_close: - f.close() - return m + *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`` + """ + + with contextlib.ExitStack() as stack: + if isinstance(f, str): + f = stack.enter_context(open(f)) + return from_text(f, idna_codec, one_rr_per_rrset) def make_query(qname, rdtype, rdclass=dns.rdataclass.IN, use_edns=None, want_dnssec=False, ednsflags=None, payload=None, - request_payload=None, options=None): + request_payload=None, options=None, idna_codec=None, + id=None, flags=dns.flags.RD): """Make a query message. The query name, type, and class may all be specified either @@ -1052,39 +1434,54 @@ def make_query(qname, rdtype, rdclass=dns.rdataclass.IN, use_edns=None, The query will have a randomly chosen query id, and its DNS flags will be set to dns.flags.RD. - @param qname: The query name. - @type qname: dns.name.Name object or string - @param rdtype: The desired rdata type. - @type rdtype: int - @param rdclass: The desired rdata class; the default is class IN. - @type rdclass: int - @param use_edns: The EDNS level to use; the default is None (no EDNS). + 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. - @type use_edns: int or bool or None - @param want_dnssec: Should the query indicate that DNSSEC is desired? - @type want_dnssec: bool - @param ednsflags: EDNS flag values. - @type ednsflags: int - @param payload: The EDNS sender's payload field, which is the maximum - size of UDP datagram the sender can handle. - @type payload: int - @param request_payload: The EDNS payload size to use when sending - this message. If not specified, defaults to the value of payload. - @type request_payload: int or None - @param options: The EDNS options - @type options: None or list of dns.edns.Option objects - @see: RFC 2671 - @rtype: dns.message.Message object""" - if isinstance(qname, string_types): - qname = dns.name.from_text(qname) - if isinstance(rdtype, string_types): - rdtype = dns.rdatatype.from_text(rdtype) - if isinstance(rdclass, string_types): - rdclass = dns.rdataclass.from_text(rdclass) - m = Message() - m.flags |= dns.flags.RD + *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``. + + 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 @@ -1093,20 +1490,14 @@ def make_query(qname, rdtype, rdclass=dns.rdataclass.IN, use_edns=None, kwargs = {} if ednsflags is not None: kwargs['ednsflags'] = ednsflags - if use_edns is None: - use_edns = 0 if payload is not None: kwargs['payload'] = payload - if use_edns is None: - use_edns = 0 if request_payload is not None: kwargs['request_payload'] = request_payload - if use_edns is None: - use_edns = 0 if options is not None: kwargs['options'] = options - if use_edns is None: - use_edns = 0 + if kwargs and use_edns is None: + use_edns = 0 kwargs['edns'] = use_edns m.use_edns(**kwargs) m.want_dnssec(want_dnssec) @@ -1114,7 +1505,7 @@ def make_query(qname, rdtype, rdclass=dns.rdataclass.IN, use_edns=None, def make_response(query, recursion_available=False, our_payload=8192, - fudge=300): + fudge=300, tsig_error=0): """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 @@ -1124,20 +1515,26 @@ def make_response(query, recursion_available=False, our_payload=8192, question section, so the query's question RRsets should not be changed. - @param query: the query to respond to - @type query: dns.message.Message object - @param recursion_available: should RA be set in the response? - @type recursion_available: bool - @param our_payload: payload size to advertise in EDNS responses; default - is 8192. - @type our_payload: int - @param fudge: TSIG time fudge; default is 300 seconds. - @type fudge: int - @rtype: dns.message.Message object""" + *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. + + 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') - response = dns.message.Message(query.id) + 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 @@ -1146,7 +1543,16 @@ def make_response(query, recursion_available=False, our_payload=8192, if query.edns >= 0: response.use_edns(0, 0, our_payload, query.payload) if query.had_tsig: - response.use_tsig(query.keyring, query.keyname, fudge, None, 0, '', - query.keyalgorithm) + 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/libs/dns/message.pyi b/libs/dns/message.pyi new file mode 100644 index 000000000..252a41185 --- /dev/null +++ b/libs/dns/message.pyi @@ -0,0 +1,47 @@ +from typing import Optional, Dict, List, Tuple, Union +from . import name, rrset, tsig, rdatatype, entropy, edns, rdataclass, rcode +import hmac + +class Message: + def to_wire(self, origin : Optional[name.Name]=None, max_size=0, **kw) -> bytes: + ... + def find_rrset(self, section : List[rrset.RRset], name : name.Name, rdclass : int, rdtype : int, + covers=rdatatype.NONE, deleting : Optional[int]=None, create=False, + force_unique=False) -> rrset.RRset: + ... + def __init__(self, id : Optional[int] =None) -> None: + self.id : int + self.flags = 0 + self.sections : List[List[rrset.RRset]] = [[], [], [], []] + self.opt : rrset.RRset = None + self.request_payload = 0 + self.keyring = None + self.tsig : rrset.RRset = None + self.request_mac = b'' + self.xfr = False + self.origin = None + self.tsig_ctx = None + self.index : Dict[Tuple[rrset.RRset, name.Name, int, int, Union[int,str], int], rrset.RRset] = {} + + def is_response(self, other : Message) -> bool: + ... + + def set_rcode(self, rcode : rcode.Rcode): + ... + +def from_text(a : str, idna_codec : Optional[name.IDNACodec] = None) -> Message: + ... + +def from_wire(wire, keyring : Optional[Dict[name.Name,bytes]] = None, request_mac = b'', xfr=False, origin=None, + tsig_ctx : Optional[Union[dns.tsig.HMACTSig, dns.tsig.GSSTSig]] = None, multi=False, + question_only=False, one_rr_per_rrset=False, + ignore_trailing=False) -> Message: + ... +def make_response(query : Message, recursion_available=False, our_payload=8192, + fudge=300) -> Message: + ... + +def make_query(qname : Union[name.Name,str], rdtype : Union[str,int], rdclass : Union[int,str] =rdataclass.IN, use_edns : Optional[bool] = None, + want_dnssec=False, ednsflags : Optional[int] = None, payload : Optional[int] = None, + request_payload : Optional[int] = None, options : Optional[List[edns.Option]] = None) -> Message: + ... diff --git a/libs/dns/name.py b/libs/dns/name.py index 97e216c8f..8905d70f7 100644 --- a/libs/dns/name.py +++ b/libs/dns/name.py @@ -1,4 +1,6 @@ -# Copyright (C) 2001-2007, 2009-2011 Nominum, Inc. +# 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, @@ -14,138 +16,125 @@ # OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. """DNS Names. - -@var root: The DNS root name. -@type root: dns.name.Name object -@var empty: The empty DNS name. -@type empty: dns.name.Name object """ -from io import BytesIO -import struct -import sys import copy -import encodings.idna +import struct + +import encodings.idna # type: ignore try: - import idna + import idna # type: ignore have_idna_2008 = True -except ImportError: +except ImportError: # pragma: no cover have_idna_2008 = False +import dns.wire import dns.exception -import dns.wiredata +import dns.immutable -from ._compat import long, binary_type, text_type, unichr, maybe_decode - -try: - maxint = sys.maxint -except AttributeError: - maxint = (1 << (8 * struct.calcsize("P"))) // 2 - 1 +# fullcompare() result values +#: The compared names have no relationship to each other. NAMERELN_NONE = 0 +#: the first name is a superdomain of the second. NAMERELN_SUPERDOMAIN = 1 +#: The first name is a subdomain of the second. NAMERELN_SUBDOMAIN = 2 +#: The compared names are equal. NAMERELN_EQUAL = 3 +#: The compared names have a common ancestor. NAMERELN_COMMONANCESTOR = 4 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 = set(['idna_exception']) + supp_kwargs = {'idna_exception'} fmt = "IDNA processing exception: {idna_exception}" -class IDNACodec(object): +class IDNACodec: """Abstract base class for IDNA encoder/decoders.""" def __init__(self): pass + def is_idna(self, label): + return label.lower().startswith(b'xn--') + def encode(self, label): - raise NotImplementedError + raise NotImplementedError # pragma: no cover def decode(self, label): - # We do not apply any IDNA policy on decode; we just - downcased = label.lower() - if downcased.startswith(b'xn--'): + # We do not apply any IDNA policy on decode. + if self.is_idna(label): try: - label = downcased[4:].decode('punycode') + label = label[4:].decode('punycode') except Exception as e: raise IDNAException(idna_exception=e) - else: - label = maybe_decode(label) - return _escapify(label, True) + return _escapify(label) + class IDNA2003Codec(IDNACodec): - """IDNA 2003 encoder/decoder.""" def __init__(self, strict_decode=False): """Initialize the IDNA 2003 encoder/decoder. - @param strict_decode: If True, then IDNA2003 checking is done when - decoding. This can cause failures if the name was encoded with - IDNA2008. The default is False. - @type strict_decode: bool + + *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(IDNA2003Codec, self).__init__() + + super().__init__() self.strict_decode = strict_decode def encode(self, label): + """Encode *label*.""" + if label == '': return b'' try: @@ -154,60 +143,59 @@ class IDNA2003Codec(IDNACodec): raise LabelTooLong def decode(self, label): + """Decode *label*.""" if not self.strict_decode: - return super(IDNA2003Codec, self).decode(label) + return super().decode(label) if label == b'': - return u'' + return '' try: - return _escapify(encodings.idna.ToUnicode(label), True) + return _escapify(encodings.idna.ToUnicode(label)) except Exception as e: raise IDNAException(idna_exception=e) -class IDNA2008Codec(IDNACodec): - """IDNA 2008 encoder/decoder.""" +class IDNA2008Codec(IDNACodec): + """IDNA 2008 encoder/decoder. + """ def __init__(self, uts_46=False, transitional=False, allow_pure_ascii=False, strict_decode=False): """Initialize the IDNA 2008 encoder/decoder. - @param uts_46: If True, apply Unicode IDNA compatibility processing - as described in Unicode Technical Standard #46 - (U{http://unicode.org/reports/tr46/}). This parameter is only - meaningful if IDNA 2008 is in use. If False, do not apply - the mapping. The default is False - @type uts_46: bool - @param transitional: If True, use the "transitional" mode described - in Unicode Technical Standard #46. This parameter is only - meaningful if IDNA 2008 is in use. The default is False. - @type transitional: bool - @param allow_pure_ascii: 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 - suffixm which would otherwise be disallowed. The default is False - @type allow_pure_ascii: bool - @param strict_decode: If True, then IDNA2008 checking is done when - decoding. This can cause failures if the name was encoded with - IDNA2003. The default is False. - @type strict_decode: bool + + *uts_46* is a ``bool``. If True, apply Unicode IDNA + compatibility processing as described in Unicode Technical + Standard #46 (http://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(IDNA2008Codec, self).__init__() + super().__init__() self.uts_46 = uts_46 self.transitional = transitional self.allow_pure_ascii = allow_pure_ascii self.strict_decode = strict_decode - def is_all_ascii(self, label): - for c in label: - if ord(c) > 0x7f: - return False - return True - def encode(self, label): if label == '': return b'' - if self.allow_pure_ascii and self.is_all_ascii(label): - return label.encode('ascii') + 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: @@ -215,23 +203,28 @@ class IDNA2008Codec(IDNACodec): label = idna.uts46_remap(label, False, self.transitional) return idna.alabel(label) except idna.IDNAError as e: - raise IDNAException(idna_exception=e) + if e.args[0] == 'Label too long': + raise LabelTooLong + else: + raise IDNAException(idna_exception=e) def decode(self, label): if not self.strict_decode: - return super(IDNA2008Codec, self).decode(label) + return super().decode(label) if label == b'': - return u'' + return '' if not have_idna_2008: raise NoIDNA2008 try: + ulabel = idna.ulabel(label) if self.uts_46: - label = idna.uts46_remap(label, False, False) - return _escapify(idna.ulabel(label), True) - except idna.IDNAError as e: + ulabel = idna.uts46_remap(ulabel, False, self.transitional) + return _escapify(ulabel) + except (idna.IDNAError, UnicodeError) as e: raise IDNAException(idna_exception=e) -_escaped = bytearray(b'"().;\\@$') +_escaped = b'"().;\\@$' +_escaped_text = '"().;\\@$' IDNA_2003_Practical = IDNA2003Codec(False) IDNA_2003_Strict = IDNA2003Codec(True) @@ -242,44 +235,45 @@ IDNA_2008_Strict = IDNA2008Codec(False, False, False, True) IDNA_2008_Transitional = IDNA2008Codec(True, True, False, False) IDNA_2008 = IDNA_2008_Practical -def _escapify(label, unicode_mode=False): +def _escapify(label): """Escape the characters in label which need it. - @param unicode_mode: escapify only special and whitespace (<= 0x20) - characters @returns: the escaped string @rtype: string""" - if not unicode_mode: + if isinstance(label, bytes): + # Ordinary DNS label mode. Escape special characters and values + # < 0x20 or > 0x7f. text = '' - if isinstance(label, text_type): - label = label.encode() - for c in bytearray(label): + 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.encode() + return text - text = u'' - if isinstance(label, binary_type): - label = label.decode() + # Unicode label mode. Escape only special characters and values < 0x20 + text = '' for c in label: - if c > u'\x20' and c < u'\x7f': - text += c + if c in _escaped_text: + text += '\\' + c + elif c <= '\x20': + text += '\\%03d' % ord(c) else: - if c >= u'\x7f': - text += c - else: - text += u'\\%03d' % ord(c) + text += c return text def _validate_labels(labels): """Check for empty labels in the middle of a label sequence, labels that are too long, and for too many labels. - @raises NameTooLong: the name as a whole is too long - @raises EmptyLabel: a label is empty (i.e. the root label) and appears - in a position other than the end of the label sequence""" + + 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 @@ -299,37 +293,38 @@ def _validate_labels(labels): raise EmptyLabel -def _ensure_bytes(label): - if isinstance(label, binary_type): +def _maybe_convert_to_binary(label): + """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, text_type): + if isinstance(label, str): return label.encode() - raise ValueError + raise ValueError # pragma: no cover -class Name(object): +@dns.immutable.immutable +class Name: """A DNS name. - The dns.name.Name class represents a DNS name as a tuple of labels. - Instances of the class are immutable. - - @ivar labels: The tuple of labels in the name. Each label is a string of - up to 63 octets.""" + 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): - """Initialize a domain name from a list of labels. - @param labels: the labels - @type labels: any iterable whose values are strings + """*labels* is any iterable whose values are ``str`` or ``bytes``. """ - labels = [_ensure_bytes(x) for x in labels] - super(Name, self).__setattr__('labels', tuple(labels)) - _validate_labels(self.labels) - def __setattr__(self, name, value): - raise TypeError("object doesn't support attribute assignment") + labels = [_maybe_convert_to_binary(x) for x in labels] + self.labels = tuple(labels) + _validate_labels(self.labels) def __copy__(self): return Name(self.labels) @@ -338,52 +333,71 @@ class Name(object): return Name(copy.deepcopy(self.labels, memo)) def __getstate__(self): + # Names can be pickled return {'labels': self.labels} def __setstate__(self, state): - super(Name, self).__setattr__('labels', state['labels']) + super().__setattr__('labels', state['labels']) _validate_labels(self.labels) def is_absolute(self): """Is the most significant label of this name the root label? - @rtype: bool + + Returns a ``bool``. """ return len(self.labels) > 0 and self.labels[-1] == b'' def is_wild(self): """Is this name wild? (I.e. Is the least significant label '*'?) - @rtype: bool + + Returns a ``bool``. """ return len(self.labels) > 0 and self.labels[0] == b'*' def __hash__(self): """Return a case-insensitive hash of the name. - @rtype: int + + Returns an ``int``. """ - h = long(0) + h = 0 for label in self.labels: - for c in bytearray(label.lower()): + for c in label.lower(): h += (h << 3) + c - return int(h % maxint) + return h def fullcompare(self, other): - """Compare two names, returning a 3-tuple (relation, order, nlabels). + """Compare two names, returning a 3-tuple + ``(relation, order, nlabels)``. - I{relation} describes the relation ship between the names, - and is one of: dns.name.NAMERELN_NONE, - dns.name.NAMERELN_SUPERDOMAIN, dns.name.NAMERELN_SUBDOMAIN, - dns.name.NAMERELN_EQUAL, or dns.name.NAMERELN_COMMONANCESTOR + *relation* describes the relation ship between the names, + and is one of: ``dns.name.NAMERELN_NONE``, + ``dns.name.NAMERELN_SUPERDOMAIN``, ``dns.name.NAMERELN_SUBDOMAIN``, + ``dns.name.NAMERELN_EQUAL``, or ``dns.name.NAMERELN_COMMONANCESTOR``. - I{order} is < 0 if self < other, > 0 if self > other, and == - 0 if self == other. A relative name is always less than an + *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. - I{nlabels} is the number of significant labels that the two names + *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() @@ -433,11 +447,13 @@ class Name(object): def is_subdomain(self, other): """Is self a subdomain of other? - The notion of subdomain includes equality. - @rtype: bool + Note that the notion of subdomain includes equality, e.g. + "dnpython.org" is a subdomain of itself. + + Returns a ``bool``. """ - (nr, o, nl) = self.fullcompare(other) + (nr, _, _) = self.fullcompare(other) if nr == NAMERELN_SUBDOMAIN or nr == NAMERELN_EQUAL: return True return False @@ -445,11 +461,13 @@ class Name(object): def is_superdomain(self, other): """Is self a superdomain of other? - The notion of subdomain includes equality. - @rtype: bool + Note that the notion of superdomain includes equality, e.g. + "dnpython.org" is a superdomain of itself. + + Returns a ``bool``. """ - (nr, o, nl) = self.fullcompare(other) + (nr, _, _) = self.fullcompare(other) if nr == NAMERELN_SUPERDOMAIN or nr == NAMERELN_EQUAL: return True return False @@ -457,7 +475,6 @@ class Name(object): def canonicalize(self): """Return a name which is equal to the current name, but is in DNSSEC canonical form. - @rtype: dns.name.Name object """ return Name([x.lower() for x in self.labels]) @@ -505,100 +522,121 @@ class Name(object): return self.to_text(False) def to_text(self, omit_final_dot=False): - """Convert name to text format. - @param omit_final_dot: If True, don't emit the final dot (denoting the - root label) for absolute names. The default is False. - @rtype: string + """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 maybe_decode(b'@') + return '@' if len(self.labels) == 1 and self.labels[0] == b'': - return maybe_decode(b'.') + return '.' if omit_final_dot and self.is_absolute(): l = self.labels[:-1] else: l = self.labels - s = b'.'.join(map(_escapify, l)) - return maybe_decode(s) + s = '.'.join(map(_escapify, l)) + return s def to_unicode(self, omit_final_dot=False, idna_codec=None): """Convert name to Unicode text format. IDN ACE labels are converted to Unicode. - @param omit_final_dot: If True, don't emit the final dot (denoting the - root label) for absolute names. The default is False. - @type omit_final_dot: bool - @param idna_codec: IDNA encoder/decoder. If None, the - 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. - @type idna_codec: dns.name.IDNA - @rtype: string + *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 u'@' + return '@' if len(self.labels) == 1 and self.labels[0] == b'': - return u'.' + 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 u'.'.join([idna_codec.decode(x) for x in l]) + return '.'.join([idna_codec.decode(x) for x in l]) def to_digestable(self, origin=None): """Convert name to a format suitable for digesting in hashes. - The name is canonicalized and converted to uncompressed wire format. + 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. - @param origin: If the name is relative and origin is not None, then - origin will be appended to it. - @type origin: dns.name.Name object - @raises NeedAbsoluteNameOrOrigin: All names in wire format are - absolute. If self is a relative name, then an origin must be supplied; - if it is missing, then this exception is raised - @rtype: string + *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``. """ - 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 - dlabels = [struct.pack('!B%ds' % len(x), len(x), x.lower()) - for x in labels] - return b''.join(dlabels) + return self.to_wire(origin=origin, canonicalize=True) - def to_wire(self, file=None, compress=None, origin=None): + def to_wire(self, file=None, compress=None, origin=None, + canonicalize=False): """Convert name to wire format, possibly compressing it. - @param file: the file where the name is emitted (typically - a BytesIO file). If None, a string containing the wire name - will be returned. - @type file: file or None - @param compress: The compression table. If None (the default) names - will not be compressed. - @type compress: dict - @param origin: If the name is relative and origin is not None, then - origin will be appended to it. - @type origin: dns.name.Name object - @raises NeedAbsoluteNameOrOrigin: All names in wire format are - absolute. If self is a relative name, then an origin must be supplied; - if it is missing, then this exception is raised + *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: - file = BytesIO() - want_return = True - else: - want_return = False + 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) if not self.is_absolute(): if origin is None or not origin.is_absolute(): @@ -628,13 +666,15 @@ class Name(object): l = len(label) file.write(struct.pack('!B', l)) if l > 0: - file.write(label) - if want_return: - return file.getvalue() + if canonicalize: + file.write(label.lower()) + else: + file.write(label) def __len__(self): """The length of the name (in labels). - @rtype: int + + Returns an ``int``. """ return len(self.labels) @@ -649,14 +689,14 @@ class Name(object): return self.relativize(other) def split(self, depth): - """Split a name into a prefix and suffix at depth. + """Split a name into a prefix and suffix names at the specified depth. - @param depth: the number of labels in the suffix - @type depth: int - @raises ValueError: the depth was not >= 0 and <= the length of the + *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) - @rtype: tuple + + Returns the tuple ``(prefix, suffix)``. """ l = len(self.labels) @@ -671,9 +711,11 @@ class Name(object): def concatenate(self, other): """Return a new name which is the concatenation of self and other. - @rtype: dns.name.Name object - @raises AbsoluteConcatenation: self is absolute and other is - not the empty name + + 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: @@ -683,9 +725,14 @@ class Name(object): return Name(labels) def relativize(self, origin): - """If self is a subdomain of origin, return a new name which is self - relative to origin. Otherwise return self. - @rtype: dns.name.Name object + """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): @@ -694,9 +741,14 @@ class Name(object): return self def derelativize(self, origin): - """If self is a relative name, return a new name which is the - concatenation of self and origin. Otherwise return self. - @rtype: dns.name.Name object + """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(): @@ -705,11 +757,14 @@ class Name(object): return self def choose_relativity(self, origin=None, relativize=True): - """Return a name with the relativity desired by the caller. If - origin is None, then self is returned. Otherwise, if - relativize is true the name is relativized, and if relativize is - false the name is derelativized. - @rtype: dns.name.Name object + """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: @@ -722,48 +777,58 @@ class Name(object): def parent(self): """Return the parent of the name. - @rtype: dns.name.Name object - @raises NoParent: the name is either the root name or the empty name, - and thus has no parent. + + 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:]) +#: The root name, '.' root = Name([b'']) -empty = Name([]) +#: The empty name. +empty = Name([]) def from_unicode(text, origin=root, idna_codec=None): """Convert unicode text into a Name object. - Labels are encoded in IDN ACE form. + Labels are encoded in IDN ACE form according to rules specified by + the IDNA codec. - @param text: The text to convert into a name. - @type text: Unicode string - @param origin: The origin to append to non-absolute names. - @type origin: dns.name.Name - @param idna_codec: IDNA encoder/decoder. If None, the default IDNA 2003 - encoder/decoder is used. - @type idna_codec: dns.name.IDNA - @rtype: dns.name.Name object + *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, text_type): + 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 = u'' + label = '' escaping = False edigits = 0 total = 0 if idna_codec is None: idna_codec = IDNA_2003 - if text == u'@': - text = u'' + if text == '@': + text = '' if text: - if text == u'.': + if text in ['.', '\u3002', '\uff0e', '\uff61']: return Name([b'']) # no Unicode "u" on this constant! for c in text: if escaping: @@ -782,13 +847,13 @@ def from_unicode(text, origin=root, idna_codec=None): edigits += 1 if edigits == 3: escaping = False - label += unichr(total) - elif c in [u'.', u'\u3002', u'\uff0e', u'\uff61']: + label += chr(total) + elif c in ['.', '\u3002', '\uff0e', '\uff61']: if len(label) == 0: raise EmptyLabel labels.append(idna_codec.encode(label)) - label = u'' - elif c == u'\\': + label = '' + elif c == '\\': escaping = True edigits = 0 total = 0 @@ -805,23 +870,41 @@ def from_unicode(text, origin=root, idna_codec=None): labels.extend(list(origin.labels)) return Name(labels) +def is_all_ascii(text): + for c in text: + if ord(c) > 0x7f: + return False + return True def from_text(text, origin=root, idna_codec=None): """Convert text into a Name object. - @param text: The text to convert into a name. - @type text: string - @param origin: The origin to append to non-absolute names. - @type origin: dns.name.Name - @param idna_codec: IDNA encoder/decoder. If None, the default IDNA 2003 - encoder/decoder is used. - @type idna_codec: dns.name.IDNA - @rtype: dns.name.Name object + *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 isinstance(text, text_type): - return from_unicode(text, origin, idna_codec) - if not isinstance(text, binary_type): + 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") @@ -835,7 +918,7 @@ def from_text(text, origin=root, idna_codec=None): if text: if text == b'.': return Name([b'']) - for c in bytearray(text): + for c in text: byte_ = struct.pack('!B', c) if escaping: if edigits == 0: @@ -876,49 +959,60 @@ def from_text(text, origin=root, idna_codec=None): return Name(labels) -def from_wire(message, current): +def from_wire_parser(parser): """Convert possibly compressed wire format into a Name. - @param message: the entire DNS message - @type message: string - @param current: the offset of the beginning of the name from the start - of the message - @type current: int - @raises dns.name.BadPointer: a compression pointer did not point backwards - in the message - @raises dns.name.BadLabelType: an invalid label type was encountered. - @returns: a tuple consisting of the name that was read and the number - of bytes of the wire format message which were consumed reading it - @rtype: (dns.name.Name object, int) tuple + + *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`` """ - if not isinstance(message, binary_type): - raise ValueError("input to from_wire() must be a byte string") - message = dns.wiredata.maybe_wrap(message) labels = [] - biggest_pointer = current - hops = 0 - count = message[current] - current += 1 - cused = 1 - while count != 0: - if count < 64: - labels.append(message[current: current + count].unwrap()) - current += count - if hops == 0: - cused += count - elif count >= 192: - current = (count & 0x3f) * 256 + message[current] - if hops == 0: - cused += 1 - if current >= biggest_pointer: - raise BadPointer - biggest_pointer = current - hops += 1 - else: - raise BadLabelType - count = message[current] - current += 1 - if hops == 0: - cused += 1 - labels.append('') - return (Name(labels), cused) + 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, current): + """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) diff --git a/libs/dns/name.pyi b/libs/dns/name.pyi new file mode 100644 index 000000000..c48d4bd1f --- /dev/null +++ b/libs/dns/name.pyi @@ -0,0 +1,40 @@ +from typing import Optional, Union, Tuple, Iterable, List + +have_idna_2008: bool + +class Name: + def is_subdomain(self, o : Name) -> bool: ... + def is_superdomain(self, o : Name) -> bool: ... + def __init__(self, labels : Iterable[Union[bytes,str]]) -> None: + self.labels : List[bytes] + def is_absolute(self) -> bool: ... + def is_wild(self) -> bool: ... + def fullcompare(self, other) -> Tuple[int,int,int]: ... + def canonicalize(self) -> Name: ... + def __eq__(self, other) -> bool: ... + def __ne__(self, other) -> bool: ... + def __lt__(self, other : Name) -> bool: ... + def __le__(self, other : Name) -> bool: ... + def __ge__(self, other : Name) -> bool: ... + def __gt__(self, other : Name) -> bool: ... + def to_text(self, omit_final_dot=False) -> str: ... + def to_unicode(self, omit_final_dot=False, idna_codec=None) -> str: ... + def to_digestable(self, origin=None) -> bytes: ... + def to_wire(self, file=None, compress=None, origin=None, + canonicalize=False) -> Optional[bytes]: ... + def __add__(self, other : Name) -> Name: ... + def __sub__(self, other : Name) -> Name: ... + def split(self, depth) -> List[Tuple[str,str]]: ... + def concatenate(self, other : Name) -> Name: ... + def relativize(self, origin) -> Name: ... + def derelativize(self, origin) -> Name: ... + def choose_relativity(self, origin : Optional[Name] = None, relativize=True) -> Name: ... + def parent(self) -> Name: ... + +class IDNACodec: + pass + +def from_text(text, origin : Optional[Name] = Name('.'), idna_codec : Optional[IDNACodec] = None) -> Name: + ... + +empty : Name diff --git a/libs/dns/namedict.py b/libs/dns/namedict.py index 58e403440..ec0750cec 100644 --- a/libs/dns/namedict.py +++ b/libs/dns/namedict.py @@ -1,4 +1,6 @@ -# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc. +# 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 @@ -25,26 +27,26 @@ """DNS name dictionary""" -import collections +from collections.abc import MutableMapping + import dns.name -from ._compat import xrange -class NameDict(collections.MutableMapping): - +class NameDict(MutableMapping): """A dictionary whose keys are dns.name.Name objects. - @ivar max_depth: the maximum depth of the keys that have ever been - added to the dictionary. - @type max_depth: int - @ivar max_depth_items: the number of items of maximum depth - @type max_depth_items: int + + 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)) @@ -65,8 +67,8 @@ class NameDict(collections.MutableMapping): self.__update_max_depth(key) def __delitem__(self, key): - value = self.__store.pop(key) - if len(value) == self.max_depth: + 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 @@ -83,20 +85,22 @@ class NameDict(collections.MutableMapping): return key in self.__store def get_deepest_match(self, name): - """Find the deepest match to I{name} in the dictionary. + """Find the deepest match to *name* in the dictionary. The deepest match is the longest name in the dictionary which is - a superdomain of I{name}. + a superdomain of *name*. Note that *superdomain* includes matching + *name* itself. - @param name: the name - @type name: dns.name.Name object - @rtype: (key, value) tuple + *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 xrange(-depth, 0): + for i in range(-depth, 0): n = dns.name.Name(name[i:]) if n in self: return (n, self[n]) diff --git a/libs/dns/node.py b/libs/dns/node.py index 7c25060e9..63ce008b9 100644 --- a/libs/dns/node.py +++ b/libs/dns/node.py @@ -1,4 +1,6 @@ -# Copyright (C) 2001-2007, 2009-2011 Nominum, Inc. +# 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, @@ -15,28 +17,74 @@ """DNS nodes. A node is a set of rdatasets.""" -from io import StringIO +import enum +import io +import dns.immutable import dns.rdataset import dns.rdatatype import dns.renderer -class Node(object): +_cname_types = { + dns.rdatatype.CNAME, +} - """A DNS node. +# "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 +} - A node is a set of rdatasets +def _matches_type_or_its_signature(rdtypes, rdtype, covers): + return rdtype in rdtypes or \ + (rdtype == dns.rdatatype.RRSIG and covers in rdtypes) - @ivar rdatasets: the node's rdatasets - @type rdatasets: list of dns.rdataset.Rdataset objects""" + +@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, covers): + 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): + 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): - """Initialize a DNS node. - """ - + # the set of rdatasets, represented as a list. self.rdatasets = [] def to_text(self, name, **kw): @@ -44,26 +92,25 @@ class Node(object): Each rdataset at the node is printed. Any keyword arguments to this method are passed on to the rdataset's to_text() method. - @param name: the owner name of the rdatasets - @type name: dns.name.Name object - @rtype: string + + *name*, a ``dns.name.Name`` or ``str``, the owner name of the + rdatasets. + + Returns a ``str``. + """ - s = StringIO() + s = io.StringIO() for rds in self.rdatasets: if len(rds) > 0: s.write(rds.to_text(name, **kw)) - s.write(u'\n') + s.write('\n') return s.getvalue()[:-1] def __repr__(self): return '' def __eq__(self, other): - """Two nodes are equal if they have the same rdatasets. - - @rtype: bool - """ # # This is inefficient. Good thing we don't need to do it much. # @@ -84,29 +131,55 @@ class Node(object): 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, rdtype, covers=dns.rdatatype.NONE, create=False): """Find an rdataset matching the specified properties in the current node. - @param rdclass: The class of the rdataset - @type rdclass: int - @param rdtype: The type of the rdataset - @type rdtype: int - @param covers: 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. - @type covers: int - @param create: If True, create the rdataset if it is not found. - @type create: bool - @raises KeyError: An rdataset of the desired type and class does - not exist and I{create} is not True. - @rtype: dns.rdataset.Rdataset object + *rdclass*, an ``int``, the class of the rdataset. + + *rdtype*, an ``int``, the type of the rdataset. + + *covers*, an ``int`` 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. + + *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: @@ -114,8 +187,8 @@ class Node(object): return rds if not create: raise KeyError - rds = dns.rdataset.Rdataset(rdclass, rdtype) - self.rdatasets.append(rds) + rds = dns.rdataset.Rdataset(rdclass, rdtype, covers) + self._append_rdataset(rds) return rds def get_rdataset(self, rdclass, rdtype, covers=dns.rdatatype.NONE, @@ -124,17 +197,24 @@ class Node(object): current node. None is returned if an rdataset of the specified type and - class does not exist and I{create} is not True. + class does not exist and *create* is not ``True``. - @param rdclass: The class of the rdataset - @type rdclass: int - @param rdtype: The type of the rdataset - @type rdtype: int - @param covers: The covered type. - @type covers: int - @param create: If True, create the rdataset if it is not found. - @type create: bool - @rtype: dns.rdataset.Rdataset object or None + *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: @@ -149,12 +229,11 @@ class Node(object): If a matching rdataset does not exist, it is not an error. - @param rdclass: The class of the rdataset - @type rdclass: int - @param rdtype: The type of the rdataset - @type rdtype: int - @param covers: The covered type. - @type covers: int + *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) @@ -164,15 +243,78 @@ class Node(object): def replace_rdataset(self, replacement): """Replace an rdataset. - It is not an error if there is no rdataset matching I{replacement}. + It is not an error if there is no rdataset matching *replacement*. - Ownership of the I{replacement} object is transferred to the node; - in other words, this method does not store a copy of I{replacement} - at the node, it stores I{replacement} itself. + 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.rdatasets.append(replacement) + self._append_rdataset(replacement) + + def classify(self): + """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): + 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, rdtype, covers=dns.rdatatype.NONE, + create=False): + if create: + raise TypeError("immutable") + return super().find_rdataset(rdclass, rdtype, covers, False) + + def get_rdataset(self, rdclass, rdtype, covers=dns.rdatatype.NONE, + create=False): + if create: + raise TypeError("immutable") + return super().get_rdataset(rdclass, rdtype, covers, False) + + def delete_rdataset(self, rdclass, rdtype, covers=dns.rdatatype.NONE): + raise TypeError("immutable") + + def replace_rdataset(self, replacement): + raise TypeError("immutable") + + def is_immutable(self): + return True diff --git a/libs/dns/node.pyi b/libs/dns/node.pyi new file mode 100644 index 000000000..0997edf9f --- /dev/null +++ b/libs/dns/node.pyi @@ -0,0 +1,17 @@ +from typing import List, Optional, Union +from . import rdataset, rdatatype, name +class Node: + def __init__(self): + self.rdatasets : List[rdataset.Rdataset] + def to_text(self, name : Union[str,name.Name], **kw) -> str: + ... + def find_rdataset(self, rdclass : int, rdtype : int, covers=rdatatype.NONE, + create=False) -> rdataset.Rdataset: + ... + def get_rdataset(self, rdclass : int, rdtype : int, covers=rdatatype.NONE, + create=False) -> Optional[rdataset.Rdataset]: + ... + def delete_rdataset(self, rdclass : int, rdtype : int, covers=rdatatype.NONE): + ... + def replace_rdataset(self, replacement : rdataset.Rdataset) -> None: + ... diff --git a/libs/dns/opcode.py b/libs/dns/opcode.py index 70d704fb3..5cf6143c7 100644 --- a/libs/dns/opcode.py +++ b/libs/dns/opcode.py @@ -1,4 +1,6 @@ -# Copyright (C) 2001-2007, 2009-2011 Nominum, Inc. +# 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, @@ -15,58 +17,53 @@ """DNS Opcodes.""" +import dns.enum import dns.exception -QUERY = 0 -IQUERY = 1 -STATUS = 2 -NOTIFY = 4 -UPDATE = 5 +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 -_by_text = { - 'QUERY': QUERY, - 'IQUERY': IQUERY, - 'STATUS': STATUS, - 'NOTIFY': NOTIFY, - 'UPDATE': UPDATE -} + @classmethod + def _maximum(cls): + return 15 -# We construct the inverse mapping programmatically to ensure that we -# cannot make any mistakes (e.g. omissions, cut-and-paste errors) that -# would cause the mapping not to be true inverse. - -_by_value = dict((y, x) for x, y in _by_text.items()) + @classmethod + def _unknown_exception_class(cls): + return UnknownOpcode class UnknownOpcode(dns.exception.DNSException): - """An DNS opcode is unknown.""" def from_text(text): """Convert text into an opcode. - @param text: the textual opcode - @type text: string - @raises UnknownOpcode: the opcode is unknown - @rtype: int + *text*, a ``str``, the textual opcode + + Raises ``dns.opcode.UnknownOpcode`` if the opcode is unknown. + + Returns an ``int``. """ - if text.isdigit(): - value = int(text) - if value >= 0 and value <= 15: - return value - value = _by_text.get(text.upper()) - if value is None: - raise UnknownOpcode - return value + return Opcode.from_text(text) def from_flags(flags): """Extract an opcode from DNS message flags. - @param flags: int - @rtype: int + *flags*, an ``int``, the DNS flags. + + Returns an ``int``. """ return (flags & 0x7800) >> 11 @@ -75,7 +72,10 @@ def from_flags(flags): def to_flags(value): """Convert an opcode to a value suitable for ORing into DNS message flags. - @rtype: int + + *value*, an ``int``, the DNS opcode value. + + Returns an ``int``. """ return (value << 11) & 0x7800 @@ -84,24 +84,32 @@ def to_flags(value): def to_text(value): """Convert an opcode to text. - @param value: the opcdoe - @type value: int - @raises UnknownOpcode: the opcode is unknown - @rtype: string + *value*, an ``int`` the opcode value, + + Raises ``dns.opcode.UnknownOpcode`` if the opcode is unknown. + + Returns a ``str``. """ - text = _by_value.get(value) - if text is None: - text = str(value) - return text + return Opcode.to_text(value) def is_update(flags): - """True if the opcode in flags is UPDATE. + """Is the opcode in flags UPDATE? - @param flags: DNS flags - @type flags: int - @rtype: bool + *flags*, an ``int``, the DNS message flags. + + Returns a ``bool``. """ - return from_flags(flags) == UPDATE + 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/libs/git/test/performance/__init__.py b/libs/dns/py.typed similarity index 100% rename from libs/git/test/performance/__init__.py rename to libs/dns/py.typed diff --git a/libs/dns/query.py b/libs/dns/query.py index bfecd43e5..fbf76d8bc 100644 --- a/libs/dns/query.py +++ b/libs/dns/query.py @@ -1,4 +1,6 @@ -# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc. +# 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, @@ -15,142 +17,139 @@ """Talk to a DNS server.""" -from __future__ import generators - +import contextlib +import enum import errno -import select +import os +import selectors import socket import struct -import sys import time +import base64 +import urllib.parse import dns.exception import dns.inet import dns.name import dns.message +import dns.rcode import dns.rdataclass import dns.rdatatype -from ._compat import long, string_types +import dns.serial +import dns.xfr -if sys.version_info > (3,): - select_error = OSError -else: - select_error = select.error +try: + import requests + from requests_toolbelt.adapters.source import SourceAddressAdapter + from requests_toolbelt.adapters.host_header_ssl import HostHeaderSSLAdapter + _have_requests = True +except ImportError: # pragma: no cover + _have_requests = False + +_have_httpx = False +_have_http2 = False +try: + import httpx + _have_httpx = True + try: + # See if http2 support is available. + with httpx.Client(http2=True): + _have_http2 = True + except Exception: + pass +except ImportError: # pragma: no cover + pass + +have_doh = _have_requests or _have_httpx + +try: + import ssl +except ImportError: # pragma: no cover + class ssl: # type: ignore + + class WantReadException(Exception): + pass + + class WantWriteException(Exception): + pass + + class SSLSocket: + pass + + def create_default_context(self, *args, **kwargs): + raise Exception('no ssl support') # 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.""" -def _compute_expiration(timeout): +class NoDOH(dns.exception.DNSException): + """DNS over HTTPS (DOH) was requested but the requests module is not + available.""" + + +# for backwards compatibility +TransferError = dns.xfr.TransferError + + +def _compute_times(timeout): + now = time.time() if timeout is None: - return None + return (now, None) else: - return time.time() + timeout + return (now, now + timeout) -def _poll_for(fd, readable, writable, error, timeout): - """Poll polling backend. - @param fd: File descriptor - @type fd: int - @param readable: Whether to wait for readability - @type readable: bool - @param writable: Whether to wait for writability - @type writable: bool - @param timeout: Deadline timeout (expiration time, in seconds) - @type timeout: float - @return True on success, False on timeout - """ - event_mask = 0 +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: - event_mask |= select.POLLIN + events |= selectors.EVENT_READ if writable: - event_mask |= select.POLLOUT - if error: - event_mask |= select.POLLERR - - pollable = select.poll() - pollable.register(fd, event_mask) - - if timeout: - event_list = pollable.poll(long(timeout * 1000)) + events |= selectors.EVENT_WRITE + if events: + sel.register(fd, events) + if expiration is None: + timeout = None else: - event_list = pollable.poll() - - return bool(event_list) + timeout = expiration - time.time() + if timeout <= 0.0: + raise dns.exception.Timeout + if not sel.select(timeout): + raise dns.exception.Timeout -def _select_for(fd, readable, writable, error, timeout): - """Select polling backend. - @param fd: File descriptor - @type fd: int - @param readable: Whether to wait for readability - @type readable: bool - @param writable: Whether to wait for writability - @type writable: bool - @param timeout: Deadline timeout (expiration time, in seconds) - @type timeout: float - @return True on success, False on timeout - """ - rset, wset, xset = [], [], [] +def _set_selector_class(selector_class): + # Internal API. Do not use. - if readable: - rset = [fd] - if writable: - wset = [fd] - if error: - xset = [fd] + global _selector_class - if timeout is None: - (rcount, wcount, xcount) = select.select(rset, wset, xset) - else: - (rcount, wcount, xcount) = select.select(rset, wset, xset, timeout) + _selector_class = selector_class - return bool((rcount or wcount or xcount)) - - -def _wait_for(fd, readable, writable, error, expiration): - done = False - while not done: - if expiration is None: - timeout = None - else: - timeout = expiration - time.time() - if timeout <= 0.0: - raise dns.exception.Timeout - try: - if not _polling_backend(fd, readable, writable, error, timeout): - raise dns.exception.Timeout - except select_error as e: - if e.args[0] != errno.EINTR: - raise e - done = True - - -def _set_polling_backend(fn): - """ - Internal API. Do not use. - """ - global _polling_backend - - _polling_backend = fn - -if hasattr(select, 'poll'): +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). - _polling_backend = _poll_for + _selector_class = selectors.PollSelector else: - _polling_backend = _select_for + _selector_class = selectors.SelectSelector # pragma: no cover def _wait_for_readable(s, expiration): @@ -165,101 +164,466 @@ 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 - n1 = dns.inet.inet_pton(af, a1[0]) - n2 = dns.inet.inet_pton(af, a2[0]) + 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 _destination_and_source(af, where, port, source, source_port): +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(). - if af is None: - try: - af = dns.inet.af_for_address(where) - except Exception: - af = dns.inet.AF_INET - if af == dns.inet.AF_INET: - destination = (where, port) - if source is not None or source_port != 0: - if source is None: - source = '0.0.0.0' - source = (source, source_port) - elif af == dns.inet.AF_INET6: - destination = (where, port, 0, 0) - if source is not None or source_port != 0: - if source is None: - source = '::' - source = (source, source_port, 0, 0) + 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. + if af == socket.AF_INET: + source = '0.0.0.0' + elif af == socket.AF_INET6: + source = '::' + else: + 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 udp(q, where, timeout=None, port=53, af=None, source=None, source_port=0, - ignore_unexpected=False, one_rr_per_rrset=False): - """Return the response obtained after sending a query via UDP. - - @param q: the query - @type q: dns.message.Message - @param where: where to send the message - @type where: string containing an IPv4 or IPv6 address - @param timeout: The number of seconds to wait before the query times out. - If None, the default, wait forever. - @type timeout: float - @param port: The port to which to send the message. The default is 53. - @type port: int - @param af: the address family to use. The default is None, which - causes the address family to use to be inferred from the form of where. - If the inference attempt fails, AF_INET is used. - @type af: int - @rtype: dns.message.Message object - @param source: source address. The default is the wildcard address. - @type source: string - @param source_port: The port from which to send the message. - The default is 0. - @type source_port: int - @param ignore_unexpected: If True, ignore responses from unexpected - sources. The default is False. - @type ignore_unexpected: bool - @param one_rr_per_rrset: Put each RR into its own RRset - @type one_rr_per_rrset: bool - """ - - wire = q.to_wire() - (af, destination, source) = _destination_and_source(af, where, port, - source, source_port) - s = socket_factory(af, socket.SOCK_DGRAM, 0) - begin_time = None +def _make_socket(af, type, source, ssl_context=None, server_hostname=None): + s = socket_factory(af, type) try: - expiration = _compute_expiration(timeout) - s.setblocking(0) + s.setblocking(False) if source is not None: s.bind(source) - _wait_for_writable(s, expiration) - begin_time = time.time() - s.sendto(wire, destination) - while 1: - _wait_for_readable(s, expiration) - (wire, from_address) = s.recvfrom(65535) - if _addresses_equal(af, from_address, destination) or \ - (dns.inet.is_multicast(where) and - from_address[1:] == destination[1:]): - break - if not ignore_unexpected: - raise UnexpectedSource('got a response from ' - '%s instead of %s' % (from_address, - destination)) - finally: - if begin_time is None: - response_time = 0 + if ssl_context: + return ssl_context.wrap_socket(s, do_handshake_on_connect=False, + server_hostname=server_hostname) else: - response_time = time.time() - begin_time + return s + except Exception: s.close() - r = dns.message.from_wire(wire, keyring=q.keyring, request_mac=q.mac, - one_rr_per_rrset=one_rr_per_rrset) - r.time = response_time + raise + +def https(q, where, timeout=None, port=443, source=None, source_port=0, + one_rr_per_rrset=False, ignore_trailing=False, + session=None, path='/dns-query', post=True, + bootstrap_address=None, verify=True): + """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`` or ``requests.session.Session``. 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 the + system's DNS resolver. + + *verify*, a ``str``, containing a path to a certificate file or directory. + + Returns a ``dns.message.Message``. + """ + + if not have_doh: + raise NoDOH('Neither httpx nor requests is available.') # pragma: no cover + + _httpx_ok = _have_httpx + + wire = q.to_wire() + (af, _, source) = _destination_and_source(where, port, source, source_port, + False) + transport_adapter = None + transport = None + headers = { + "accept": "application/dns-message" + } + if af is not None: + if af == socket.AF_INET: + url = 'https://{}:{}{}'.format(where, port, path) + elif af == socket.AF_INET6: + url = 'https://[{}]:{}{}'.format(where, port, path) + elif bootstrap_address is not None: + _httpx_ok = False + split_url = urllib.parse.urlsplit(where) + headers['Host'] = split_url.hostname + url = where.replace(split_url.hostname, bootstrap_address) + if _have_requests: + transport_adapter = HostHeaderSSLAdapter() + else: + url = where + if source is not None: + # set source port and source address + if _have_httpx: + if source_port == 0: + transport = httpx.HTTPTransport(local_address=source[0]) + else: + _httpx_ok = False + if _have_requests: + transport_adapter = SourceAddressAdapter(source) + + if session: + if _have_httpx: + _is_httpx = isinstance(session, httpx.Client) + else: + _is_httpx = False + if _is_httpx and not _httpx_ok: + raise NoDOH('Session is httpx, but httpx cannot be used for ' + 'the requested operation.') + else: + _is_httpx = _httpx_ok + + if not _httpx_ok and not _have_requests: + raise NoDOH('Cannot use httpx for this operation, and ' + 'requests is not available.') + + with contextlib.ExitStack() as stack: + if not session: + if _is_httpx: + session = stack.enter_context(httpx.Client(http1=True, + http2=_have_http2, + verify=verify, + transport=transport)) + else: + session = stack.enter_context(requests.sessions.Session()) + + if transport_adapter: + session.mount(url, transport_adapter) + + # 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)) + }) + if _is_httpx: + response = session.post(url, headers=headers, content=wire, + timeout=timeout) + else: + response = session.post(url, headers=headers, data=wire, + timeout=timeout, verify=verify) + else: + wire = base64.urlsafe_b64encode(wire).rstrip(b"=") + if _is_httpx: + wire = wire.decode() # httpx does a repr() if we give it bytes + response = session.get(url, headers=headers, + timeout=timeout, + params={"dns": wire}) + else: + response = session.get(url, headers=headers, + timeout=timeout, verify=verify, + params={"dns": wire}) + + # 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 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, what, destination, expiration=None): + """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, destination=None, expiration=None, + ignore_unexpected=False, one_rr_per_rrset=False, + keyring=None, request_mac=b'', ignore_trailing=False, + raise_on_truncation=False): + """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``, 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. + """ + + wire = b'' + while True: + (wire, from_address) = _udp_recv(sock, 65535, expiration) + if _matches_destination(sock.family, from_address, destination, + ignore_unexpected): + break + 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, + raise_on_truncation=raise_on_truncation) + if destination: + return (r, received_time) + else: + return (r, received_time, from_address) + +def udp(q, where, timeout=None, port=53, source=None, source_port=0, + ignore_unexpected=False, one_rr_per_rrset=False, ignore_trailing=False, + raise_on_truncation=False, sock=None): + """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. + + 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) + with contextlib.ExitStack() as stack: + if sock: + s = sock + else: + s = stack.enter_context(_make_socket(af, socket.SOCK_DGRAM, source)) + 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) + r.time = received_time - begin_time + if not q.is_response(r): + raise BadResponse + return r + +def udp_with_fallback(q, where, timeout=None, port=53, source=None, + source_port=0, ignore_unexpected=False, + one_rr_per_rrset=False, ignore_trailing=False, + udp_sock=None, tcp_sock=None): + """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. + + 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) + 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 @@ -269,12 +633,16 @@ def _net_read(sock, count, expiration): """ s = b'' while count > 0: - _wait_for_readable(sock, expiration) - n = sock.recv(count) - if n == b'': - raise EOFError - count = count - len(n) - s = s + n + 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 @@ -286,145 +654,285 @@ def _net_write(sock, data, expiration): current = 0 l = len(data) while current < l: - _wait_for_writable(sock, expiration) - current += sock.send(data[current:]) + 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 _connect(s, address): - try: - s.connect(address) - except socket.error: - (ty, v) = sys.exc_info()[:2] +def send_tcp(sock, what, expiration=None): + """Send a DNS message to the specified TCP socket. - if hasattr(v, 'errno'): - v_err = v.errno - else: - v_err = v[0] - if v_err not in [errno.EINPROGRESS, errno.EWOULDBLOCK, errno.EALREADY]: - raise v + *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): + what = what.to_wire() + l = len(what) + # copying the wire into tcpmsg is inefficient, but lets us + # avoid writev() or doing a short write that would get pushed + # onto the net + tcpmsg = struct.pack("!H", l) + what + sent_time = time.time() + _net_write(sock, tcpmsg, expiration) + return (len(tcpmsg), sent_time) + +def receive_tcp(sock, expiration=None, one_rr_per_rrset=False, + keyring=None, request_mac=b'', ignore_trailing=False): + """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``, 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, where, timeout=None, port=53, af=None, source=None, source_port=0, - one_rr_per_rrset=False): +def tcp(q, where, timeout=None, port=53, source=None, source_port=0, + one_rr_per_rrset=False, ignore_trailing=False, sock=None): """Return the response obtained after sending a query via TCP. - @param q: the query - @type q: dns.message.Message object - @param where: where to send the message - @type where: string containing an IPv4 or IPv6 address - @param timeout: The number of seconds to wait before the query times out. - If None, the default, wait forever. - @type timeout: float - @param port: The port to which to send the message. The default is 53. - @type port: int - @param af: the address family to use. The default is None, which - causes the address family to use to be inferred from the form of where. - If the inference attempt fails, AF_INET is used. - @type af: int - @rtype: dns.message.Message object - @param source: source address. The default is the wildcard address. - @type source: string - @param source_port: The port from which to send the message. + *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. - @type source_port: int - @param one_rr_per_rrset: Put each RR into its own RRset - @type one_rr_per_rrset: bool + + *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() - (af, destination, source) = _destination_and_source(af, where, port, - source, source_port) - s = socket_factory(af, socket.SOCK_STREAM, 0) - begin_time = None - try: - expiration = _compute_expiration(timeout) - s.setblocking(0) - begin_time = time.time() - if source is not None: - s.bind(source) - _connect(s, destination) - - l = len(wire) - - # copying the wire into tcpmsg is inefficient, but lets us - # avoid writev() or doing a short write that would get pushed - # onto the net - tcpmsg = struct.pack("!H", l) + wire - _net_write(s, tcpmsg, expiration) - ldata = _net_read(s, 2, expiration) - (l,) = struct.unpack("!H", ldata) - wire = _net_read(s, l, expiration) - finally: - if begin_time is None: - response_time = 0 + (begin_time, expiration) = _compute_times(timeout) + with contextlib.ExitStack() as stack: + if sock: + s = sock else: - response_time = time.time() - begin_time - s.close() - r = dns.message.from_wire(wire, keyring=q.keyring, request_mac=q.mac, - one_rr_per_rrset=one_rr_per_rrset) - r.time = response_time - if not q.is_response(r): - raise BadResponse - return r + (af, destination, source) = _destination_and_source(where, port, + source, + source_port) + s = stack.enter_context(_make_socket(af, socket.SOCK_STREAM, + source)) + _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 + + +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 tls(q, where, timeout=None, port=853, source=None, source_port=0, + one_rr_per_rrset=False, ignore_trailing=False, sock=None, + ssl_context=None, server_hostname=None): + """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. + + 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 = ssl.create_default_context() + if server_hostname is None: + ssl_context.check_hostname = False + + 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 def xfr(where, zone, rdtype=dns.rdatatype.AXFR, rdclass=dns.rdataclass.IN, timeout=None, port=53, keyring=None, keyname=None, relativize=True, - af=None, lifetime=None, source=None, source_port=0, serial=0, + lifetime=None, source=None, source_port=0, serial=0, use_udp=False, keyalgorithm=dns.tsig.default_algorithm): """Return a generator for the responses to a zone transfer. - @param where: where to send the message - @type where: string containing an IPv4 or IPv6 address - @param zone: The name of the zone to transfer - @type zone: dns.name.Name object or string - @param rdtype: The type of zone transfer. The default is - dns.rdatatype.AXFR. - @type rdtype: int or string - @param rdclass: The class of the zone transfer. The default is - dns.rdataclass.IN. - @type rdclass: int or string - @param timeout: The number of seconds to wait for each response message. - If None, the default, wait forever. - @type timeout: float - @param port: The port to which to send the message. The default is 53. - @type port: int - @param keyring: The TSIG keyring to use - @type keyring: dict - @param keyname: The name of the TSIG key to use - @type keyname: dns.name.Name object or string - @param relativize: 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(). - @type relativize: bool - @param af: the address family to use. The default is None, which - causes the address family to use to be inferred from the form of where. - If the inference attempt fails, AF_INET is used. - @type af: int - @param lifetime: 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. - @type lifetime: float - @rtype: generator of dns.message.Message objects. - @param source: source address. The default is the wildcard address. - @type source: string - @param source_port: The port from which to send the message. + *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. - @type source_port: int - @param serial: The SOA serial number to use as the base for an IXFR diff - sequence (only meaningful if rdtype == dns.rdatatype.IXFR). - @type serial: int - @param use_udp: Use UDP (only meaningful for IXFR) - @type use_udp: bool - @param keyalgorithm: The TSIG algorithm to use; defaults to - dns.tsig.default_algorithm - @type keyalgorithm: string + + *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, string_types): + if isinstance(zone, str): zone = dns.name.from_text(zone) - if isinstance(rdtype, string_types): - rdtype = dns.rdatatype.from_text(rdtype) + 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', @@ -433,107 +941,214 @@ def xfr(where, zone, rdtype=dns.rdatatype.AXFR, rdclass=dns.rdataclass.IN, if keyring is not None: q.use_tsig(keyring, keyname, algorithm=keyalgorithm) wire = q.to_wire() - (af, destination, source) = _destination_and_source(af, where, port, + (af, destination, source) = _destination_and_source(where, port, source, source_port) - if use_udp: - if rdtype != dns.rdatatype.IXFR: - raise ValueError('cannot do a UDP AXFR') - s = socket_factory(af, socket.SOCK_DGRAM, 0) - else: - s = socket_factory(af, socket.SOCK_STREAM, 0) - s.setblocking(0) - if source is not None: - s.bind(source) - expiration = _compute_expiration(lifetime) - _connect(s, destination) - l = len(wire) - if use_udp: - _wait_for_writable(s, expiration) - s.send(wire) - 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 - first = True - while not done: - mexpiration = _compute_expiration(timeout) - if mexpiration is None or mexpiration > expiration: - mexpiration = expiration + 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: - _wait_for_readable(s, expiration) - (wire, from_address) = s.recvfrom(65535) + _udp_send(s, wire, None, expiration) 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, first=first, - one_rr_per_rrset=is_ixfr) - tsig_ctx = r.tsig_ctx - first = False - 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 soa_rrset[0].serial <= serial: + 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 # - # We're already up-to-date. + # 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. # - 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") + 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 - 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 - s.close() + 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, txn_manager, query=None, + port=53, timeout=None, lifetime=None, source=None, + source_port=0, udp_mode=UDPMode.NEVER): + """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 possibilites 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 succeeed. + + 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/libs/dns/query.pyi b/libs/dns/query.pyi new file mode 100644 index 000000000..a22e229fb --- /dev/null +++ b/libs/dns/query.pyi @@ -0,0 +1,64 @@ +from typing import Optional, Union, Dict, Generator, Any +from . import tsig, rdatatype, rdataclass, name, message +from requests.sessions import Session + +import socket + +# If the ssl import works, then +# +# error: Name 'ssl' already defined (by an import) +# +# is expected and can be ignored. +try: + import ssl +except ImportError: + class ssl: # type: ignore + SSLContext : Dict = {} + +have_doh: bool + +def https(q : message.Message, where: str, timeout : Optional[float] = None, + port : Optional[int] = 443, source : Optional[str] = None, + source_port : Optional[int] = 0, + session: Optional[Session] = None, + path : Optional[str] = '/dns-query', post : Optional[bool] = True, + bootstrap_address : Optional[str] = None, + verify : Optional[bool] = True) -> message.Message: + pass + +def tcp(q : message.Message, where : str, timeout : float = None, port=53, + af : Optional[int] = None, source : Optional[str] = None, + source_port : Optional[int] = 0, + one_rr_per_rrset : Optional[bool] = False, + ignore_trailing : Optional[bool] = False, + sock : Optional[socket.socket] = None) -> message.Message: + pass + +def xfr(where : None, zone : Union[name.Name,str], rdtype=rdatatype.AXFR, + rdclass=rdataclass.IN, + timeout : Optional[float] = None, port=53, + keyring : Optional[Dict[name.Name, bytes]] = None, + keyname : Union[str,name.Name]= None, relativize=True, + lifetime : Optional[float] = None, + source : Optional[str] = None, source_port=0, serial=0, + use_udp : Optional[bool] = False, + keyalgorithm=tsig.default_algorithm) \ + -> Generator[Any,Any,message.Message]: + pass + +def udp(q : message.Message, where : str, timeout : Optional[float] = None, + port=53, source : Optional[str] = None, source_port : Optional[int] = 0, + ignore_unexpected : Optional[bool] = False, + one_rr_per_rrset : Optional[bool] = False, + ignore_trailing : Optional[bool] = False, + sock : Optional[socket.socket] = None) -> message.Message: + pass + +def tls(q : message.Message, where : str, timeout : Optional[float] = None, + port=53, source : Optional[str] = None, source_port : Optional[int] = 0, + one_rr_per_rrset : Optional[bool] = False, + ignore_trailing : Optional[bool] = False, + sock : Optional[socket.socket] = None, + ssl_context: Optional[ssl.SSLContext] = None, + server_hostname: Optional[str] = None) -> message.Message: + pass diff --git a/libs/dns/rcode.py b/libs/dns/rcode.py index 314815f7c..49fee6950 100644 --- a/libs/dns/rcode.py +++ b/libs/dns/rcode.py @@ -1,4 +1,6 @@ -# Copyright (C) 2001-2007, 2009-2011 Nominum, Inc. +# 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, @@ -15,111 +17,148 @@ """DNS Result Codes.""" +import dns.enum import dns.exception -from ._compat import long +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 -NOERROR = 0 -FORMERR = 1 -SERVFAIL = 2 -NXDOMAIN = 3 -NOTIMP = 4 -REFUSED = 5 -YXDOMAIN = 6 -YXRRSET = 7 -NXRRSET = 8 -NOTAUTH = 9 -NOTZONE = 10 -BADVERS = 16 + @classmethod + def _maximum(cls): + return 4095 -_by_text = { - 'NOERROR': NOERROR, - 'FORMERR': FORMERR, - 'SERVFAIL': SERVFAIL, - 'NXDOMAIN': NXDOMAIN, - 'NOTIMP': NOTIMP, - 'REFUSED': REFUSED, - 'YXDOMAIN': YXDOMAIN, - 'YXRRSET': YXRRSET, - 'NXRRSET': NXRRSET, - 'NOTAUTH': NOTAUTH, - 'NOTZONE': NOTZONE, - 'BADVERS': BADVERS -} - -# We construct the inverse mapping programmatically to ensure that we -# cannot make any mistakes (e.g. omissions, cut-and-paste errors) that -# would cause the mapping not to be a true inverse. - -_by_value = dict((y, x) for x, y in _by_text.items()) + @classmethod + def _unknown_exception_class(cls): + return UnknownRcode class UnknownRcode(dns.exception.DNSException): - """A DNS rcode is unknown.""" def from_text(text): """Convert text into an rcode. - @param text: the textual rcode - @type text: string - @raises UnknownRcode: the rcode is unknown - @rtype: int + *text*, a ``str``, the textual rcode or an integer in textual form. + + Raises ``dns.rcode.UnknownRcode`` if the rcode mnemonic is unknown. + + Returns an ``int``. """ - if text.isdigit(): - v = int(text) - if v >= 0 and v <= 4095: - return v - v = _by_text.get(text.upper()) - if v is None: - raise UnknownRcode - return v + return Rcode.from_text(text) def from_flags(flags, ednsflags): """Return the rcode value encoded by flags and ednsflags. - @param flags: the DNS flags - @type flags: int - @param ednsflags: the EDNS flags - @type ednsflags: int - @raises ValueError: rcode is < 0 or > 4095 - @rtype: int + *flags*, an ``int``, the DNS flags field. + + *ednsflags*, an ``int``, the EDNS flags field. + + Raises ``ValueError`` if rcode is < 0 or > 4095 + + Returns an ``int``. """ value = (flags & 0x000f) | ((ednsflags >> 20) & 0xff0) - if value < 0 or value > 4095: - raise ValueError('rcode must be >= 0 and <= 4095') return value def to_flags(value): """Return a (flags, ednsflags) tuple which encodes the rcode. - @param value: the rcode - @type value: int - @raises ValueError: rcode is < 0 or > 4095 - @rtype: (int, int) tuple + *value*, an ``int``, 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 = long(value & 0xff0) << 20 + ev = (value & 0xff0) << 20 return (v, ev) -def to_text(value): +def to_text(value, tsig=False): """Convert rcode into text. - @param value: the rcode - @type value: int - @rtype: string + *value*, an ``int``, the rcode. + + Raises ``ValueError`` if rcode is < 0 or > 4095. + + Returns a ``str``. """ - text = _by_value.get(value) - if text is None: - text = str(value) - return text + 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/libs/dns/rdata.py b/libs/dns/rdata.py index 9e9344d5c..624063e00 100644 --- a/libs/dns/rdata.py +++ b/libs/dns/rdata.py @@ -1,4 +1,6 @@ -# Copyright (C) 2001-2007, 2009-2011 Nominum, Inc. +# 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, @@ -13,79 +15,83 @@ # ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT # OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -"""DNS rdata. +"""DNS rdata.""" -@var _rdata_modules: A dictionary mapping a (rdclass, rdtype) tuple to -the module which implements that type. -@type _rdata_modules: dict -@var _module_prefix: The prefix to use when forming modules names. The -default is 'dns.rdtypes'. Changing this value will break the library. -@type _module_prefix: string -@var _hex_chunk: At most this many octets that will be represented in each -chunk of hexstring that _hexify() produces before whitespace occurs. -@type _hex_chunk: int""" - -from io import BytesIO +from importlib import import_module import base64 import binascii +import io +import inspect +import itertools +import random +import dns.wire 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.wiredata -from ._compat import xrange, string_types, text_type +import dns.ttl -_hex_chunksize = 32 +_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 -def _hexify(data, chunksize=_hex_chunksize): +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 I{chunksize} characters separated by a space. - - @param data: the binary string - @type data: string - @param chunksize: the chunk size. Default is L{dns.rdata._hex_chunksize} - @rtype: string + of chunksize characters separated by a separator. """ - line = binascii.hexlify(data) - return b' '.join([line[i:i + chunksize] - for i - in range(0, len(line), chunksize)]).decode() - -_base64_chunksize = 32 + return _wordbreak(binascii.hexlify(data), chunksize, separator) -def _base64ify(data, chunksize=_base64_chunksize): +def _base64ify(data, chunksize=_chunksize, separator=b' ', **kw): """Convert a binary string into its base64 encoding, broken up into chunks - of I{chunksize} characters separated by a space. - - @param data: the binary string - @type data: string - @param chunksize: the chunk size. Default is - L{dns.rdata._base64_chunksize} - @rtype: string + of chunksize characters separated by a separator. """ - line = base64.b64encode(data) - return b' '.join([line[i:i + chunksize] - for i - in range(0, len(line), chunksize)]).decode() + return _wordbreak(base64.b64encode(data), chunksize, separator) -__escaped = bytearray(b'"\\') +# pylint: enable=unused-argument + +__escaped = b'"\\' def _escapify(qstring): - """Escape the characters in a quoted string which need it. + """Escape the characters in a quoted string which need it.""" - @param qstring: the string - @type qstring: string - @returns: the escaped string - @rtype: string - """ - - if isinstance(qstring, text_type): + if isinstance(qstring, str): qstring = qstring.encode() if not isinstance(qstring, bytearray): qstring = bytearray(qstring) @@ -104,43 +110,71 @@ def _escapify(qstring): 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. - - @param what: a string of octets representing a bitmap. - @type what: string - @rtype: string """ - for i in xrange(len(what) - 1, -1, -1): + 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 -class Rdata(object): - """Base class for all DNS rdata types. - """ +@dns.immutable.immutable +class Rdata: + """Base class for all DNS rdata types.""" - __slots__ = ['rdclass', 'rdtype'] + __slots__ = ['rdclass', 'rdtype', 'rdcomment'] def __init__(self, rdclass, rdtype): """Initialize an rdata. - @param rdclass: The rdata class - @type rdclass: int - @param rdtype: The rdata type - @type rdtype: int + + *rdclass*, an ``int`` is the rdataclass of the Rdata. + + *rdtype*, an ``int`` is the rdatatype of the Rdata. """ - self.rdclass = rdclass - self.rdtype = rdtype + 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 SIG/RRSIG rdatas apply to a specific type; this type is + """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). - @rtype: int + + Returns an ``int``. """ return dns.rdatatype.NONE @@ -149,38 +183,53 @@ class Rdata(object): """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. - @rtype: int + + Returns an ``int``. """ return self.covers() << 16 | self.rdtype def to_text(self, origin=None, relativize=True, **kw): """Convert an rdata to text format. - @rtype: string - """ - raise NotImplementedError - def to_wire(self, file, compress=None, origin=None): + Returns a ``str``. + """ + + raise NotImplementedError # pragma: no cover + + def _to_wire(self, file, compress=None, origin=None, canonicalize=False): + raise NotImplementedError # pragma: no cover + + def to_wire(self, file=None, compress=None, origin=None, + canonicalize=False): """Convert an rdata to wire format. - @rtype: string + + Returns a ``bytes`` or ``None``. """ - raise NotImplementedError + 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=None): + """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=None): """Convert rdata to a format suitable for digesting in hashes. This - is also the DNSSEC canonical form.""" - f = BytesIO() - self.to_wire(f, None, origin) - return f.getvalue() + is also the DNSSEC canonical form. - def validate(self): - """Check that the current contents of the rdata's fields are - valid. If you change an rdata by assigning to its fields, - it is a good idea to call validate() when you are done making - changes. + Returns a ``bytes``. """ - dns.rdata.from_text(self.rdclass, self.rdtype, self.to_text()) + + return self.to_wire(origin=origin, canonicalize=True) def __repr__(self): covers = self.covers() @@ -197,31 +246,78 @@ class Rdata(object): def _cmp(self, other): """Compare an rdata with another rdata of the same rdtype and - rdclass. Return < 0 if self < other in the DNSSEC ordering, - 0 if self == other, and > 0 if self > other. + 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. """ - our = self.to_digestable(dns.name.root) - their = other.to_digestable(dns.name.root) + 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 - if our > their: + elif our > their: return 1 - - 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 - return self._cmp(other) == 0 + 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 self._cmp(other) != 0 + return not self.__eq__(other) def __lt__(self, other): if not isinstance(other, Rdata) or \ @@ -252,56 +348,193 @@ class Rdata(object): return hash(self.to_digestable(dns.name.root)) @classmethod - def from_text(cls, rdclass, rdtype, tok, origin=None, relativize=True): - """Build an rdata object from text format. - - @param rdclass: The rdata class - @type rdclass: int - @param rdtype: The rdata type - @type rdtype: int - @param tok: The tokenizer - @type tok: dns.tokenizer.Tokenizer - @param origin: The origin to use for relative names - @type origin: dns.name.Name - @param relativize: should names be relativized? - @type relativize: bool - @rtype: dns.rdata.Rdata instance - """ - - raise NotImplementedError + def from_text(cls, rdclass, rdtype, tok, origin=None, relativize=True, + relativize_to=None): + raise NotImplementedError # pragma: no cover @classmethod - def from_wire(cls, rdclass, rdtype, wire, current, rdlen, origin=None): - """Build an rdata object from wire format + def from_wire_parser(cls, rdclass, rdtype, parser, origin=None): + raise NotImplementedError # pragma: no cover - @param rdclass: The rdata class - @type rdclass: int - @param rdtype: The rdata type - @type rdtype: int - @param wire: The wire-format message - @type wire: string - @param current: The offset in wire of the beginning of the rdata. - @type current: int - @param rdlen: The length of the wire-format rdata - @type rdlen: int - @param origin: The origin to use for relative names - @type origin: dns.name.Name - @rtype: dns.rdata.Rdata instance + def replace(self, **kwargs): + """ + 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*. """ - raise NotImplementedError + # Get the constructor parameters. + parameters = inspect.signature(self.__init__).parameters - def choose_relativity(self, origin=None, relativize=True): - """Convert any domain names in the rdata to the specified - relativization. - """ + # 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)) - pass + # 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, encode=False, max_length=None, empty_ok=True): + if encode and isinstance(value, str): + value = value.encode() + elif isinstance(value, bytearray): + value = bytes(value) + elif not isinstance(value, bytes): + raise ValueError('not bytes') + if max_length is not None and len(value) > max_length: + raise ValueError('too long') + if not empty_ok and len(value) == 0: + raise ValueError('empty bytes not allowed') + return value + + @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): + # call to check validity + dns.ipv4.inet_aton(value) + return 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): + # call to check validity + dns.ipv6.inet_aton(value) + return 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 class GenericRdata(Rdata): - """Generate Rdata Class + """Generic Rdata Class This class is used for rdata types for which we have no better implementation. It implements the DNS "unknown RRs" scheme. @@ -310,78 +543,67 @@ class GenericRdata(Rdata): __slots__ = ['data'] def __init__(self, rdclass, rdtype, data): - super(GenericRdata, self).__init__(rdclass, rdtype) - self.data = data + super().__init__(rdclass, rdtype) + object.__setattr__(self, 'data', data) def to_text(self, origin=None, relativize=True, **kw): - return r'\# %d ' % len(self.data) + _hexify(self.data) + return r'\# %d ' % len(self.data) + _hexify(self.data, **kw) @classmethod - def from_text(cls, rdclass, rdtype, tok, origin=None, relativize=True): + 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 != '\#': + if not token.is_identifier() or token.value != r'\#': raise dns.exception.SyntaxError( r'generic rdata does not start with \#') length = tok.get_int() - chunks = [] - while 1: - token = tok.get() - if token.is_eol_or_eof(): - break - chunks.append(token.value.encode()) - hex = b''.join(chunks) + hex = tok.concatenate_remaining_identifiers().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): + def _to_wire(self, file, compress=None, origin=None, canonicalize=False): file.write(self.data) @classmethod - def from_wire(cls, rdclass, rdtype, wire, current, rdlen, origin=None): - return cls(rdclass, rdtype, wire[current: current + rdlen]) + def from_wire_parser(cls, rdclass, rdtype, parser, origin=None): + return cls(rdclass, rdtype, parser.get_remaining()) -_rdata_modules = {} +_rdata_classes = {} _module_prefix = 'dns.rdtypes' - def get_rdata_class(rdclass, rdtype): - - def import_module(name): - mod = __import__(name) - components = name.split('.') - for comp in components[1:]: - mod = getattr(mod, comp) - return mod - - mod = _rdata_modules.get((rdclass, rdtype)) - rdclass_text = dns.rdataclass.to_text(rdclass) - rdtype_text = dns.rdatatype.to_text(rdtype) - rdtype_text = rdtype_text.replace('-', '_') - if not mod: - mod = _rdata_modules.get((dns.rdatatype.ANY, rdtype)) - if not mod: + 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])) - _rdata_modules[(rdclass, rdtype)] = mod + cls = getattr(mod, rdtype_text) + _rdata_classes[(rdclass, rdtype)] = cls except ImportError: try: mod = import_module('.'.join([_module_prefix, 'ANY', rdtype_text])) - _rdata_modules[(dns.rdataclass.ANY, rdtype)] = mod + cls = getattr(mod, rdtype_text) + _rdata_classes[(dns.rdataclass.ANY, rdtype)] = cls + _rdata_classes[(rdclass, rdtype)] = cls except ImportError: - mod = None - if mod: - cls = getattr(mod, rdtype_text) - else: + pass + if not cls: cls = GenericRdata + _rdata_classes[(rdclass, rdtype)] = cls return cls -def from_text(rdclass, rdtype, tok, origin=None, relativize=True): +def from_text(rdclass, rdtype, tok, origin=None, relativize=True, + relativize_to=None, idna_codec=None): """Build an rdata object from text format. This function attempts to dynamically load a class which @@ -392,40 +614,102 @@ def from_text(rdclass, rdtype, tok, origin=None, relativize=True): Once a class is chosen, its from_text() class method is called with the parameters to this function. - If I{tok} is a string, then a tokenizer is created and the string + If *tok* is a ``str``, then a tokenizer is created and the string is used as its input. - @param rdclass: The rdata class - @type rdclass: int - @param rdtype: The rdata type - @type rdtype: int - @param tok: The tokenizer or input text - @type tok: dns.tokenizer.Tokenizer or string - @param origin: The origin to use for relative names - @type origin: dns.name.Name - @param relativize: Should names be relativized? - @type relativize: bool - @rtype: dns.rdata.Rdata instance""" + *rdclass*, an ``int``, the rdataclass. - if isinstance(tok, string_types): - tok = dns.tokenizer.Tokenizer(tok) + *rdtype*, an ``int``, 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) - 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. - # - rdata = GenericRdata.from_text(rdclass, rdtype, tok, origin, - relativize) - return from_wire(rdclass, rdtype, rdata.data, 0, len(rdata.data), - origin) - return cls.from_text(rdclass, rdtype, tok, origin, relativize) + 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, rdtype, parser, origin=None): + """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. + + *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, rdtype, wire, current, rdlen, origin=None): @@ -439,20 +723,60 @@ def from_wire(rdclass, rdtype, wire, current, rdlen, origin=None): Once a class is chosen, its from_wire() class method is called with the parameters to this function. - @param rdclass: The rdata class - @type rdclass: int - @param rdtype: The rdata type - @type rdtype: int - @param wire: The wire-format message - @type wire: string - @param current: The offset in wire of the beginning of the rdata. - @type current: int - @param rdlen: The length of the wire-format rdata - @type rdlen: int - @param origin: The origin to use for relative names - @type origin: dns.name.Name - @rtype: dns.rdata.Rdata instance""" + *rdclass*, an ``int``, the rdataclass. - wire = dns.wiredata.maybe_wrap(wire) - cls = get_rdata_class(rdclass, rdtype) - return cls.from_wire(rdclass, rdtype, wire, current, rdlen, origin) + *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, rdtype, rdtype_text, is_singleton=False, + rdclass=dns.rdataclass.IN): + """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. + """ + + existing_cls = get_rdata_class(rdclass, rdtype) + if existing_cls != GenericRdata or dns.rdatatype.is_metatype(rdtype): + raise RdatatypeExists(rdclass=rdclass, rdtype=rdtype) + try: + if dns.rdatatype.RdataType(rdtype).name != rdtype_text: + raise RdatatypeExists(rdclass=rdclass, rdtype=rdtype) + except ValueError: + pass + _rdata_classes[(rdclass, rdtype)] = getattr(implementation, + rdtype_text.replace('-', '_')) + dns.rdatatype.register_type(rdtype, rdtype_text, is_singleton) diff --git a/libs/dns/rdata.pyi b/libs/dns/rdata.pyi new file mode 100644 index 000000000..f394791fb --- /dev/null +++ b/libs/dns/rdata.pyi @@ -0,0 +1,19 @@ +from typing import Dict, Tuple, Any, Optional, BinaryIO +from .name import Name, IDNACodec +class Rdata: + def __init__(self): + self.address : str + def to_wire(self, file : Optional[BinaryIO], compress : Optional[Dict[Name,int]], origin : Optional[Name], canonicalize : Optional[bool]) -> Optional[bytes]: + ... + @classmethod + def from_text(cls, rdclass : int, rdtype : int, tok, origin=None, relativize=True): + ... +_rdata_modules : Dict[Tuple[Any,Rdata],Any] + +def from_text(rdclass : int, rdtype : int, tok : Optional[str], origin : Optional[Name] = None, + relativize : bool = True, relativize_to : Optional[Name] = None, + idna_codec : Optional[IDNACodec] = None): + ... + +def from_wire(rdclass : int, rdtype : int, wire : bytes, current : int, rdlen : int, origin : Optional[Name] = None): + ... diff --git a/libs/dns/rdataclass.py b/libs/dns/rdataclass.py index 17a4810d5..41bba693b 100644 --- a/libs/dns/rdataclass.py +++ b/libs/dns/rdataclass.py @@ -1,4 +1,6 @@ -# Copyright (C) 2001-2007, 2009-2011 Nominum, Inc. +# 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, @@ -13,106 +15,101 @@ # 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. - -@var _by_text: The rdata class textual name to value mapping -@type _by_text: dict -@var _by_value: The rdata class value to textual name mapping -@type _by_value: dict -@var _metaclasses: If an rdataclass is a metaclass, there will be a mapping -whose key is the rdatatype value and whose value is True in this dictionary. -@type _metaclasses: dict""" - -import re +"""DNS Rdata Classes.""" +import dns.enum import dns.exception -RESERVED0 = 0 -IN = 1 -CH = 3 -HS = 4 -NONE = 254 -ANY = 255 +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 -_by_text = { - 'RESERVED0': RESERVED0, - 'IN': IN, - 'CH': CH, - 'HS': HS, - 'NONE': NONE, - 'ANY': ANY -} + @classmethod + def _maximum(cls): + return 65535 -# We construct the inverse mapping programmatically to ensure that we -# cannot make any mistakes (e.g. omissions, cut-and-paste errors) that -# would cause the mapping not to be true inverse. + @classmethod + def _short_name(cls): + return "class" -_by_value = dict((y, x) for x, y in _by_text.items()) + @classmethod + def _prefix(cls): + return "CLASS" -# Now that we've built the inverse map, we can add class aliases to -# the _by_text mapping. + @classmethod + def _unknown_exception_class(cls): + return UnknownRdataclass -_by_text.update({ - 'INTERNET': IN, - 'CHAOS': CH, - 'HESIOD': HS -}) -_metaclasses = { - NONE: True, - ANY: True -} - -_unknown_class_pattern = re.compile('CLASS([0-9]+)$', re.I) +_metaclasses = {RdataClass.NONE, RdataClass.ANY} class UnknownRdataclass(dns.exception.DNSException): - """A DNS class is unknown.""" def from_text(text): """Convert text into a DNS rdata class value. - @param text: the text - @type text: string - @rtype: int - @raises dns.rdataclass.UnknownRdataclass: the class is unknown - @raises ValueError: the rdata class value is not >= 0 and <= 65535 + + 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 an ``int``. """ - value = _by_text.get(text.upper()) - if value is None: - match = _unknown_class_pattern.match(text) - if match is None: - raise UnknownRdataclass - value = int(match.group(1)) - if value < 0 or value > 65535: - raise ValueError("class must be between >= 0 and <= 65535") - return value + return RdataClass.from_text(text) def to_text(value): - """Convert a DNS rdata class to text. - @param value: the rdata class value - @type value: int - @rtype: string - @raises ValueError: the rdata class value is not >= 0 and <= 65535 + """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``. """ - if value < 0 or value > 65535: - raise ValueError("class must be between >= 0 and <= 65535") - text = _by_value.get(value) - if text is None: - text = 'CLASS' + repr(value) - return text + return RdataClass.to_text(value) def is_metaclass(rdclass): - """True if the class is a metaclass. - @param rdclass: the rdata class - @type rdclass: int - @rtype: bool""" + """True if the specified class is a metaclass. + + The currently defined metaclasses are ANY and NONE. + + *rdclass* is an ``int``. + """ 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/libs/dns/rdataset.py b/libs/dns/rdataset.py index db266f2f0..e69ee2325 100644 --- a/libs/dns/rdataset.py +++ b/libs/dns/rdataset.py @@ -1,4 +1,6 @@ -# Copyright (C) 2001-2007, 2009-2011 Nominum, Inc. +# 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, @@ -15,69 +17,56 @@ """DNS rdatasets (an rdataset is a set of rdatas of a given type and class)""" +import io import random -from io import StringIO import struct import dns.exception +import dns.immutable import dns.rdatatype import dns.rdataclass import dns.rdata import dns.set -from ._compat import string_types # 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. - - @ivar rdclass: The class of the rdataset - @type rdclass: int - @ivar rdtype: The type of the rdataset - @type rdtype: int - @ivar covers: 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. - @type covers: int - @ivar ttl: The DNS TTL (Time To Live) value - @type ttl: int - """ + """A DNS rdataset.""" __slots__ = ['rdclass', 'rdtype', 'covers', 'ttl'] - def __init__(self, rdclass, rdtype, covers=dns.rdatatype.NONE): + def __init__(self, rdclass, rdtype, covers=dns.rdatatype.NONE, ttl=0): """Create a new rdataset of the specified class and type. - @see: the description of the class instance variables for the - meaning of I{rdclass} and I{rdtype}""" + *rdclass*, an ``int``, the rdataclass. - super(Rdataset, self).__init__() + *rdtype*, an ``int``, the rdatatype. + + *covers*, an ``int``, the covered rdatatype. + + *ttl*, an ``int``, the TTL. + """ + + super().__init__() self.rdclass = rdclass self.rdtype = rdtype self.covers = covers - self.ttl = 0 + self.ttl = ttl def _clone(self): - obj = super(Rdataset, self)._clone() + obj = super()._clone() obj.rdclass = self.rdclass obj.rdtype = self.rdtype obj.covers = self.covers @@ -85,27 +74,36 @@ class Rdataset(dns.set.Set): return obj def update_ttl(self, ttl): - """Set the TTL of the rdataset to be the lesser of the set's current + """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. - @param ttl: The TTL - @type ttl: int""" + *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(self, rd, ttl=None): + def add(self, rd, ttl=None): # pylint: disable=arguments-differ """Add the specified rdata to the rdataset. - If the optional I{ttl} parameter is supplied, then - self.update_ttl(ttl) will be called prior to adding the rdata. + If the optional *ttl* parameter is supplied, then + ``self.update_ttl(ttl)`` will be called prior to adding the rdata. - @param rd: The rdata - @type rd: dns.rdata.Rdata object - @param ttl: The TTL - @type ttl: int""" + *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 @@ -126,24 +124,33 @@ class Rdataset(dns.set.Set): raise DifferingCovers if dns.rdatatype.is_singleton(rd.rdtype) and len(self) > 0: self.clear() - super(Rdataset, self).add(rd) + super().add(rd) def union_update(self, other): self.update_ttl(other.ttl) - super(Rdataset, self).union_update(other) + super().union_update(other) def intersection_update(self, other): self.update_ttl(other.ttl) - super(Rdataset, self).intersection_update(other) + super().intersection_update(other) def update(self, other): """Add all rdatas in other to self. - @param other: The rdataset from which to update - @type other: dns.rdataset.Rdataset object""" + *other*, a ``dns.rdataset.Rdataset``, the rdataset from which + to update. + """ self.update_ttl(other.ttl) - super(Rdataset, self).update(other) + 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: @@ -151,45 +158,51 @@ class Rdataset(dns.set.Set): else: ctext = '(' + dns.rdatatype.to_text(self.covers) + ')' return '' + dns.rdatatype.to_text(self.rdtype) + ctext + \ + ' rdataset: ' + self._rdata_repr() + '>' def __str__(self): return self.to_text() def __eq__(self, other): - """Two rdatasets are equal if they have the same class, type, and - covers, and contain the same rdata. - @rtype: bool""" - 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(Rdataset, self).__eq__(other) + return super().__eq__(other) def __ne__(self, other): return not self.__eq__(other) def to_text(self, name=None, origin=None, relativize=True, - override_rdclass=None, **kw): - """Convert the rdataset into DNS master file format. + override_rdclass=None, want_comments=False, **kw): + """Convert the rdataset into DNS zone file format. - @see: L{dns.name.Name.choose_relativity} for more information - on how I{origin} and I{relativize} determine the way names + 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. + ``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``. + """ - @param name: If name is not None, emit a RRs with I{name} as - the owner name. - @type name: dns.name.Name object - @param origin: The origin for relative names, or None. - @type origin: dns.name.Name object - @param relativize: True if names should names be relativized - @type relativize: bool""" if name is not None: name = name.choose_relativity(origin, relativize) ntext = str(name) @@ -197,7 +210,7 @@ class Rdataset(dns.set.Set): else: ntext = '' pad = '' - s = StringIO() + s = io.StringIO() if override_rdclass is not None: rdclass = override_rdclass else: @@ -208,16 +221,21 @@ class Rdataset(dns.set.Set): # some dynamic updates, so we don't need to print out the TTL # (which is meaningless anyway). # - s.write(u'%s%s%s %s\n' % (ntext, pad, - dns.rdataclass.to_text(rdclass), - dns.rdatatype.to_text(self.rdtype))) + s.write('{}{}{} {}\n'.format(ntext, pad, + dns.rdataclass.to_text(rdclass), + dns.rdatatype.to_text(self.rdtype))) else: for rd in self: - s.write(u'%s%s%d %s %s %s\n' % + 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))) + **kw), + extra)) # # We strip off the final \n for the caller's convenience in printing # @@ -227,16 +245,26 @@ class Rdataset(dns.set.Set): override_rdclass=None, want_shuffle=True): """Convert the rdataset to wire format. - @param name: The owner name of the RRset that will be emitted - @type name: dns.name.Name object - @param file: The file to which the wire format data will be appended - @type file: file - @param compress: The compression table to use; the default is None. - @type compress: dict - @param origin: The origin to be appended to any relative names when - they are emitted. The default is None. - @returns: the number of records emitted - @rtype: int + *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: @@ -244,7 +272,7 @@ class Rdataset(dns.set.Set): want_shuffle = False else: rdclass = self.rdclass - file.seek(0, 2) + file.seek(0, io.SEEK_END) if len(self) == 0: name.to_wire(file, compress, origin) stuff = struct.pack("!HHIH", self.rdtype, rdclass, 0, 0) @@ -268,34 +296,124 @@ class Rdataset(dns.set.Set): file.seek(start - 2) stuff = struct.pack("!H", end - start) file.write(stuff) - file.seek(0, 2) + file.seek(0, io.SEEK_END) return len(self) def match(self, rdclass, rdtype, covers): - """Returns True if this rdataset matches the specified class, type, - and covers""" + """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): + """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 perference + shuffled. -def from_text_list(rdclass, rdtype, ttl, text_rdatas): + 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): + + """An immutable DNS rdataset.""" + + _clone_class = Rdataset + + def __init__(self, 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') + + def __ior__(self, other): + raise TypeError('immutable') + + def __iand__(self, other): + raise TypeError('immutable') + + def __iadd__(self, other): + raise TypeError('immutable') + + def __isub__(self, other): + 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 from_text_list(rdclass, rdtype, ttl, text_rdatas, idna_codec=None, + origin=None, relativize=True, relativize_to=None): """Create an rdataset with the specified class, type, and TTL, and with the specified list of rdatas in text format. - @rtype: dns.rdataset.Rdataset object + *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. """ - if isinstance(rdclass, string_types): - rdclass = dns.rdataclass.from_text(rdclass) - if isinstance(rdtype, string_types): - rdtype = dns.rdatatype.from_text(rdtype) + 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) + rd = dns.rdata.from_text(r.rdclass, r.rdtype, t, origin, relativize, + relativize_to, idna_codec) r.add(rd) return r @@ -304,7 +422,7 @@ def from_text(rdclass, rdtype, ttl, *text_rdatas): """Create an rdataset with the specified class, type, and TTL, and with the specified rdatas in text format. - @rtype: dns.rdataset.Rdataset object + Returns a ``dns.rdataset.Rdataset`` object. """ return from_text_list(rdclass, rdtype, ttl, text_rdatas) @@ -314,7 +432,7 @@ def from_rdata_list(ttl, rdatas): """Create an rdataset with the specified TTL, and with the specified list of rdata objects. - @rtype: dns.rdataset.Rdataset object + Returns a ``dns.rdataset.Rdataset`` object. """ if len(rdatas) == 0: @@ -332,7 +450,7 @@ def from_rdata(ttl, *rdatas): """Create an rdataset with the specified TTL, and with the specified rdata objects. - @rtype: dns.rdataset.Rdataset object + Returns a ``dns.rdataset.Rdataset`` object. """ return from_rdata_list(ttl, rdatas) diff --git a/libs/dns/rdataset.pyi b/libs/dns/rdataset.pyi new file mode 100644 index 000000000..a7bbf2d4c --- /dev/null +++ b/libs/dns/rdataset.pyi @@ -0,0 +1,58 @@ +from typing import Optional, Dict, List, Union +from io import BytesIO +from . import exception, name, set, rdatatype, rdata, rdataset + +class DifferingCovers(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(exception.DNSException): + """An attempt was made to add DNS RR data of an incompatible type.""" + + +class Rdataset(set.Set): + def __init__(self, rdclass, rdtype, covers=rdatatype.NONE, ttl=0): + self.rdclass : int = rdclass + self.rdtype : int = rdtype + self.covers : int = covers + self.ttl : int = ttl + + def update_ttl(self, ttl : int) -> None: + ... + + def add(self, rd : rdata.Rdata, ttl : Optional[int] =None): + ... + + def union_update(self, other : Rdataset): + ... + + def intersection_update(self, other : Rdataset): + ... + + def update(self, other : Rdataset): + ... + + def to_text(self, name : Optional[name.Name] =None, origin : Optional[name.Name] =None, relativize=True, + override_rdclass : Optional[int] =None, **kw) -> bytes: + ... + + def to_wire(self, name : Optional[name.Name], file : BytesIO, compress : Optional[Dict[name.Name, int]] = None, origin : Optional[name.Name] = None, + override_rdclass : Optional[int] = None, want_shuffle=True) -> int: + ... + + def match(self, rdclass : int, rdtype : int, covers : int) -> bool: + ... + + +def from_text_list(rdclass : Union[int,str], rdtype : Union[int,str], ttl : int, text_rdatas : str, idna_codec : Optional[name.IDNACodec] = None) -> rdataset.Rdataset: + ... + +def from_text(rdclass : Union[int,str], rdtype : Union[int,str], ttl : int, *text_rdatas : str) -> rdataset.Rdataset: + ... + +def from_rdata_list(ttl : int, rdatas : List[rdata.Rdata]) -> rdataset.Rdataset: + ... + +def from_rdata(ttl : int, *rdatas : List[rdata.Rdata]) -> rdataset.Rdataset: + ... diff --git a/libs/dns/rdatatype.py b/libs/dns/rdatatype.py index 15284f64d..9499c7b9b 100644 --- a/libs/dns/rdatatype.py +++ b/libs/dns/rdatatype.py @@ -1,4 +1,6 @@ -# Copyright (C) 2001-2007, 2009-2011 Nominum, Inc. +# 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, @@ -13,243 +15,299 @@ # 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. - -@var _by_text: The rdata type textual name to value mapping -@type _by_text: dict -@var _by_value: The rdata type value to textual name mapping -@type _by_value: dict -@var _metatypes: If an rdatatype is a metatype, there will be a mapping -whose key is the rdatatype value and whose value is True in this dictionary. -@type _metatypes: dict -@var _singletons: If an rdatatype is a singleton, there will be a mapping -whose key is the rdatatype value and whose value is True in this dictionary. -@type _singletons: dict""" - -import re +"""DNS Rdata Types.""" +import dns.enum import dns.exception -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 -HIP = 55 -CDS = 59 -CDNSKEY = 60 -CSYNC = 62 -SPF = 99 -UNSPEC = 103 -EUI48 = 108 -EUI64 = 109 -TKEY = 249 -TSIG = 250 -IXFR = 251 -AXFR = 252 -MAILB = 253 -MAILA = 254 -ANY = 255 -URI = 256 -CAA = 257 -AVC = 258 -TA = 32768 -DLV = 32769 +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 -_by_text = { - 'NONE': NONE, - 'A': A, - 'NS': NS, - 'MD': MD, - 'MF': MF, - 'CNAME': CNAME, - 'SOA': SOA, - 'MB': MB, - 'MG': MG, - 'MR': MR, - 'NULL': NULL, - 'WKS': WKS, - 'PTR': PTR, - 'HINFO': HINFO, - 'MINFO': MINFO, - 'MX': MX, - 'TXT': TXT, - 'RP': RP, - 'AFSDB': AFSDB, - 'X25': X25, - 'ISDN': ISDN, - 'RT': RT, - 'NSAP': NSAP, - 'NSAP-PTR': NSAP_PTR, - 'SIG': SIG, - 'KEY': KEY, - 'PX': PX, - 'GPOS': GPOS, - 'AAAA': AAAA, - 'LOC': LOC, - 'NXT': NXT, - 'SRV': SRV, - 'NAPTR': NAPTR, - 'KX': KX, - 'CERT': CERT, - 'A6': A6, - 'DNAME': DNAME, - 'OPT': OPT, - 'APL': APL, - 'DS': DS, - 'SSHFP': SSHFP, - 'IPSECKEY': IPSECKEY, - 'RRSIG': RRSIG, - 'NSEC': NSEC, - 'DNSKEY': DNSKEY, - 'DHCID': DHCID, - 'NSEC3': NSEC3, - 'NSEC3PARAM': NSEC3PARAM, - 'TLSA': TLSA, - 'HIP': HIP, - 'CDS': CDS, - 'CDNSKEY': CDNSKEY, - 'CSYNC': CSYNC, - 'SPF': SPF, - 'UNSPEC': UNSPEC, - 'EUI48': EUI48, - 'EUI64': EUI64, - 'TKEY': TKEY, - 'TSIG': TSIG, - 'IXFR': IXFR, - 'AXFR': AXFR, - 'MAILB': MAILB, - 'MAILA': MAILA, - 'ANY': ANY, - 'URI': URI, - 'CAA': CAA, - 'AVC': AVC, - 'TA': TA, - 'DLV': DLV, -} + @classmethod + def _maximum(cls): + return 65535 -# We construct the inverse mapping programmatically to ensure that we -# cannot make any mistakes (e.g. omissions, cut-and-paste errors) that -# would cause the mapping not to be true inverse. + @classmethod + def _short_name(cls): + return "type" -_by_value = dict((y, x) for x, y in _by_text.items()) + @classmethod + def _prefix(cls): + return "TYPE" + @classmethod + def _unknown_exception_class(cls): + return UnknownRdatatype -_metatypes = { - OPT: True -} +_registered_by_text = {} +_registered_by_value = {} -_singletons = { - SOA: True, - NXT: True, - DNAME: True, - NSEC: True, - # CNAME is technically a singleton, but we allow multiple CNAMEs. -} +_metatypes = {RdataType.OPT} -_unknown_type_pattern = re.compile('TYPE([0-9]+)$', re.I) +_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): """Convert text into a DNS rdata type value. - @param text: the text - @type text: string - @raises dns.rdatatype.UnknownRdatatype: the type is unknown - @raises ValueError: the rdata type value is not >= 0 and <= 65535 - @rtype: int""" - value = _by_text.get(text.upper()) - if value is None: - match = _unknown_type_pattern.match(text) - if match is None: - raise UnknownRdatatype - value = int(match.group(1)) - if value < 0 or value > 65535: - raise ValueError("type must be between >= 0 and <= 65535") - return 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 an ``int``. + """ + + text = text.upper().replace('-', '_') + try: + return RdataType.from_text(text) + except UnknownRdatatype: + registered_type = _registered_by_text.get(text) + if registered_type: + return registered_type + raise def to_text(value): - """Convert a DNS rdata type to text. - @param value: the rdata type value - @type value: int - @raises ValueError: the rdata type value is not >= 0 and <= 65535 - @rtype: string""" + """Convert a DNS rdata type value to text. - if value < 0 or value > 65535: - raise ValueError("type must be between >= 0 and <= 65535") - text = _by_value.get(value) - if text is None: - text = 'TYPE' + repr(value) - return 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``. + """ + + text = RdataType.to_text(value) + if text.startswith("TYPE"): + registered_text = _registered_by_value.get(value) + if registered_text: + text = registered_text + return text.replace('_', '-') def is_metatype(rdtype): - """True if the type is a metatype. - @param rdtype: the type - @type rdtype: int - @rtype: bool""" + """True if the specified type is a metatype. - if rdtype >= TKEY and rdtype <= ANY or rdtype in _metatypes: - return True - return False + *rdtype* is an ``int``. + + 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): - """True if the type is a singleton. - @param rdtype: the type - @type rdtype: int - @rtype: 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, rdtype_text, is_singleton=False): + """Dynamically register an rdatatype. + + *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.) + """ + + _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/libs/dns/rdtypes/ANY/AFSDB.py b/libs/dns/rdtypes/ANY/AFSDB.py index f3d515403..d7838e7e7 100644 --- a/libs/dns/rdtypes/ANY/AFSDB.py +++ b/libs/dns/rdtypes/ANY/AFSDB.py @@ -1,3 +1,5 @@ +# 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 @@ -14,16 +16,13 @@ # OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. import dns.rdtypes.mxbase +import dns.immutable +@dns.immutable.immutable class AFSDB(dns.rdtypes.mxbase.UncompressedDowncasingMX): - """AFSDB record - - @ivar subtype: the subtype value - @type subtype: int - @ivar hostname: the hostname name - @type hostname: dns.name.Name object""" + """AFSDB record""" # Use the property mechanism to make "subtype" an alias for the # "preference" attribute, and "hostname" an alias for the "exchange" @@ -36,18 +35,12 @@ class AFSDB(dns.rdtypes.mxbase.UncompressedDowncasingMX): # implementation, but this way we don't copy code, and that's # good. - def get_subtype(self): + @property + def subtype(self): + "the AFSDB subtype" return self.preference - def set_subtype(self, subtype): - self.preference = subtype - - subtype = property(get_subtype, set_subtype) - - def get_hostname(self): + @property + def hostname(self): + "the AFSDB hostname" return self.exchange - - def set_hostname(self, hostname): - self.exchange = hostname - - hostname = property(get_hostname, set_hostname) diff --git a/libs/dns/rdtypes/ANY/AMTRELAY.py b/libs/dns/rdtypes/ANY/AMTRELAY.py new file mode 100644 index 000000000..9f093deed --- /dev/null +++ b/libs/dns/rdtypes/ANY/AMTRELAY.py @@ -0,0 +1,86 @@ +# 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/libs/dns/rdtypes/ANY/AVC.py b/libs/dns/rdtypes/ANY/AVC.py index 137c9de92..11e026d08 100644 --- a/libs/dns/rdtypes/ANY/AVC.py +++ b/libs/dns/rdtypes/ANY/AVC.py @@ -1,3 +1,5 @@ +# 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 @@ -14,10 +16,12 @@ # OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. import dns.rdtypes.txtbase +import dns.immutable +@dns.immutable.immutable class AVC(dns.rdtypes.txtbase.TXTBase): - """AVC record + """AVC record""" - @see: U{http://www.iana.org/assignments/dns-parameters/AVC/avc-completed-template}""" + # See: IANA dns parameters for AVC diff --git a/libs/dns/rdtypes/ANY/CAA.py b/libs/dns/rdtypes/ANY/CAA.py index f2e41ad0b..c86b45ea2 100644 --- a/libs/dns/rdtypes/ANY/CAA.py +++ b/libs/dns/rdtypes/ANY/CAA.py @@ -1,3 +1,5 @@ +# 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 @@ -16,29 +18,27 @@ 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 + """CAA (Certification Authority Authorization) record""" - @ivar flags: the flags - @type flags: int - @ivar tag: the tag - @type tag: string - @ivar value: the value - @type value: string - @see: RFC 6844""" + # see: RFC 6844 __slots__ = ['flags', 'tag', 'value'] def __init__(self, rdclass, rdtype, flags, tag, value): - super(CAA, self).__init__(rdclass, rdtype) - self.flags = flags - self.tag = tag - self.value = 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, @@ -46,17 +46,14 @@ class CAA(dns.rdata.Rdata): dns.rdata._escapify(self.value)) @classmethod - def from_text(cls, rdclass, rdtype, tok, origin=None, relativize=True): + def from_text(cls, rdclass, rdtype, tok, origin=None, relativize=True, + relativize_to=None): flags = tok.get_uint8() tag = tok.get_string().encode() - if len(tag) > 255: - raise dns.exception.SyntaxError("tag too long") - if not tag.isalnum(): - raise dns.exception.SyntaxError("tag is not alphanumeric") value = tok.get_string().encode() return cls(rdclass, rdtype, flags, tag, value) - def to_wire(self, file, compress=None, origin=None): + 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 @@ -65,9 +62,8 @@ class CAA(dns.rdata.Rdata): file.write(self.value) @classmethod - def from_wire(cls, rdclass, rdtype, wire, current, rdlen, origin=None): - (flags, l) = struct.unpack('!BB', wire[current: current + 2]) - current += 2 - tag = wire[current: current + l] - value = wire[current + l:current + rdlen - 2] + 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/libs/dns/rdtypes/ANY/CDNSKEY.py b/libs/dns/rdtypes/ANY/CDNSKEY.py index 83f3d51fc..14b19417d 100644 --- a/libs/dns/rdtypes/ANY/CDNSKEY.py +++ b/libs/dns/rdtypes/ANY/CDNSKEY.py @@ -1,3 +1,5 @@ +# 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 @@ -14,12 +16,13 @@ # OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. import dns.rdtypes.dnskeybase -from dns.rdtypes.dnskeybase import flags_to_text_set, flags_from_text_set - - -__all__ = ['flags_to_text_set', 'flags_from_text_set'] +import dns.immutable +# pylint: disable=unused-import +from dns.rdtypes.dnskeybase import SEP, REVOKE, ZONE # noqa: F401 +# pylint: enable=unused-import +@dns.immutable.immutable class CDNSKEY(dns.rdtypes.dnskeybase.DNSKEYBase): """CDNSKEY record""" diff --git a/libs/dns/rdtypes/ANY/CDS.py b/libs/dns/rdtypes/ANY/CDS.py index e1abfc368..094de12bf 100644 --- a/libs/dns/rdtypes/ANY/CDS.py +++ b/libs/dns/rdtypes/ANY/CDS.py @@ -1,3 +1,5 @@ +# 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 @@ -14,8 +16,15 @@ # OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. import dns.rdtypes.dsbase +import dns.immutable +@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/libs/dns/rdtypes/ANY/CERT.py b/libs/dns/rdtypes/ANY/CERT.py index 1c35c23d2..f35ce3adf 100644 --- a/libs/dns/rdtypes/ANY/CERT.py +++ b/libs/dns/rdtypes/ANY/CERT.py @@ -1,3 +1,5 @@ +# 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 @@ -17,6 +19,7 @@ import struct import base64 import dns.exception +import dns.immutable import dns.dnssec import dns.rdata import dns.tokenizer @@ -25,6 +28,11 @@ _ctype_by_value = { 1: 'PKIX', 2: 'SPKI', 3: 'PGP', + 4: 'IPKIX', + 5: 'ISPKI', + 6: 'IPGP', + 7: 'ACPKIX', + 8: 'IACPKIX', 253: 'URI', 254: 'OID', } @@ -33,6 +41,11 @@ _ctype_by_name = { 'PKIX': 1, 'SPKI': 2, 'PGP': 3, + 'IPKIX': 4, + 'ISPKI': 5, + 'IPGP': 6, + 'ACPKIX': 7, + 'IACPKIX': 8, 'URI': 253, 'OID': 254, } @@ -52,70 +65,49 @@ def _ctype_to_text(what): return str(what) +@dns.immutable.immutable class CERT(dns.rdata.Rdata): - """CERT record + """CERT record""" - @ivar certificate_type: certificate type - @type certificate_type: int - @ivar key_tag: key tag - @type key_tag: int - @ivar algorithm: algorithm - @type algorithm: int - @ivar certificate: the certificate or CRL - @type certificate: string - @see: RFC 2538""" + # see RFC 4398 __slots__ = ['certificate_type', 'key_tag', 'algorithm', 'certificate'] def __init__(self, rdclass, rdtype, certificate_type, key_tag, algorithm, certificate): - super(CERT, self).__init__(rdclass, rdtype) - self.certificate_type = certificate_type - self.key_tag = key_tag - self.algorithm = algorithm - self.certificate = 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.dnssec.algorithm_to_text(self.algorithm), - dns.rdata._base64ify(self.certificate)) + dns.rdata._base64ify(self.certificate, **kw)) @classmethod - def from_text(cls, rdclass, rdtype, tok, origin=None, relativize=True): + 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.dnssec.algorithm_from_text(tok.get_string()) - if algorithm < 0 or algorithm > 255: - raise dns.exception.SyntaxError("bad algorithm type") - chunks = [] - while 1: - t = tok.get().unescape() - if t.is_eol_or_eof(): - break - if not t.is_identifier(): - raise dns.exception.SyntaxError - chunks.append(t.value.encode()) - b64 = b''.join(chunks) + 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): + 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(cls, rdclass, rdtype, wire, current, rdlen, origin=None): - prefix = wire[current: current + 5].unwrap() - current += 5 - rdlen -= 5 - if rdlen < 0: - raise dns.exception.FormError - (certificate_type, key_tag, algorithm) = struct.unpack("!HHB", prefix) - certificate = wire[current: current + rdlen].unwrap() + 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/libs/dns/rdtypes/ANY/CNAME.py b/libs/dns/rdtypes/ANY/CNAME.py index 65cf570c7..a4fcfa888 100644 --- a/libs/dns/rdtypes/ANY/CNAME.py +++ b/libs/dns/rdtypes/ANY/CNAME.py @@ -1,3 +1,5 @@ +# 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 @@ -14,8 +16,10 @@ # OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. import dns.rdtypes.nsbase +import dns.immutable +@dns.immutable.immutable class CNAME(dns.rdtypes.nsbase.NSBase): """CNAME record diff --git a/libs/dns/rdtypes/ANY/CSYNC.py b/libs/dns/rdtypes/ANY/CSYNC.py index bf95cb273..979028aeb 100644 --- a/libs/dns/rdtypes/ANY/CSYNC.py +++ b/libs/dns/rdtypes/ANY/CSYNC.py @@ -1,3 +1,5 @@ +# 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 @@ -16,109 +18,51 @@ import struct import dns.exception +import dns.immutable import dns.rdata import dns.rdatatype import dns.name -from dns._compat import xrange +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 - - @ivar serial: the SOA serial number - @type serial: int - @ivar flags: the CSYNC flags - @type flags: int - @ivar windows: the windowed bitmap list - @type windows: list of (window number, string) tuples""" + """CSYNC record""" __slots__ = ['serial', 'flags', 'windows'] def __init__(self, rdclass, rdtype, serial, flags, windows): - super(CSYNC, self).__init__(rdclass, rdtype) - self.serial = serial - self.flags = flags - self.windows = 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 = '' - for (window, bitmap) in self.windows: - bits = [] - for i in xrange(0, len(bitmap)): - byte = bitmap[i] - for j in xrange(0, 8): - if byte & (0x80 >> j): - bits.append(dns.rdatatype.to_text(window * 256 + - i * 8 + j)) - text += (' ' + ' '.join(bits)) + 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): + def from_text(cls, rdclass, rdtype, tok, origin=None, relativize=True, + relativize_to=None): serial = tok.get_uint32() flags = tok.get_uint16() - rdtypes = [] - while 1: - token = tok.get().unescape() - if token.is_eol_or_eof(): - break - nrdtype = dns.rdatatype.from_text(token.value) - if nrdtype == 0: - raise dns.exception.SyntaxError("CSYNC with bit 0") - if nrdtype > 65535: - raise dns.exception.SyntaxError("CSYNC with bit > 65535") - rdtypes.append(nrdtype) - rdtypes.sort() - window = 0 - octets = 0 - prior_rdtype = 0 - bitmap = bytearray(b'\0' * 32) - windows = [] - for nrdtype in rdtypes: - if nrdtype == prior_rdtype: - continue - prior_rdtype = nrdtype - new_window = nrdtype // 256 - if new_window != window: - windows.append((window, bitmap[0:octets])) - bitmap = bytearray(b'\0' * 32) - window = new_window - offset = nrdtype % 256 - byte = offset // 8 - bit = offset % 8 - octets = byte + 1 - bitmap[byte] = bitmap[byte] | (0x80 >> bit) + bitmap = Bitmap.from_text(tok) + return cls(rdclass, rdtype, serial, flags, bitmap) - windows.append((window, bitmap[0:octets])) - return cls(rdclass, rdtype, serial, flags, windows) - - def to_wire(self, file, compress=None, origin=None): + def _to_wire(self, file, compress=None, origin=None, canonicalize=False): file.write(struct.pack('!IH', self.serial, self.flags)) - for (window, bitmap) in self.windows: - file.write(struct.pack('!BB', window, len(bitmap))) - file.write(bitmap) + Bitmap(self.windows).to_wire(file) @classmethod - def from_wire(cls, rdclass, rdtype, wire, current, rdlen, origin=None): - if rdlen < 6: - raise dns.exception.FormError("CSYNC too short") - (serial, flags) = struct.unpack("!IH", wire[current: current + 6]) - current += 6 - rdlen -= 6 - windows = [] - while rdlen > 0: - if rdlen < 3: - raise dns.exception.FormError("CSYNC too short") - window = wire[current] - octets = wire[current + 1] - if octets == 0 or octets > 32: - raise dns.exception.FormError("bad CSYNC octets") - current += 2 - rdlen -= 2 - if rdlen < octets: - raise dns.exception.FormError("bad CSYNC bitmap length") - bitmap = bytearray(wire[current: current + octets].unwrap()) - current += octets - rdlen -= octets - windows.append((window, bitmap)) - return cls(rdclass, rdtype, serial, flags, windows) + 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/libs/dns/rdtypes/ANY/DLV.py b/libs/dns/rdtypes/ANY/DLV.py index cd1244c19..947dc42e1 100644 --- a/libs/dns/rdtypes/ANY/DLV.py +++ b/libs/dns/rdtypes/ANY/DLV.py @@ -1,3 +1,5 @@ +# 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 @@ -14,8 +16,10 @@ # OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. import dns.rdtypes.dsbase +import dns.immutable +@dns.immutable.immutable class DLV(dns.rdtypes.dsbase.DSBase): """DLV record""" diff --git a/libs/dns/rdtypes/ANY/DNAME.py b/libs/dns/rdtypes/ANY/DNAME.py index dac97214a..f4984b555 100644 --- a/libs/dns/rdtypes/ANY/DNAME.py +++ b/libs/dns/rdtypes/ANY/DNAME.py @@ -1,3 +1,5 @@ +# 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 @@ -14,11 +16,13 @@ # OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. import dns.rdtypes.nsbase +import dns.immutable +@dns.immutable.immutable class DNAME(dns.rdtypes.nsbase.UncompressedNS): """DNAME record""" - def to_digestable(self, origin=None): - return self.target.to_digestable(origin) + def _to_wire(self, file, compress=None, origin=None, canonicalize=False): + self.target.to_wire(file, None, origin, canonicalize) diff --git a/libs/dns/rdtypes/ANY/DNSKEY.py b/libs/dns/rdtypes/ANY/DNSKEY.py index e915e98bb..e69a7c197 100644 --- a/libs/dns/rdtypes/ANY/DNSKEY.py +++ b/libs/dns/rdtypes/ANY/DNSKEY.py @@ -1,3 +1,5 @@ +# 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 @@ -14,12 +16,13 @@ # OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. import dns.rdtypes.dnskeybase -from dns.rdtypes.dnskeybase import flags_to_text_set, flags_from_text_set - - -__all__ = ['flags_to_text_set', 'flags_from_text_set'] +import dns.immutable +# pylint: disable=unused-import +from dns.rdtypes.dnskeybase import SEP, REVOKE, ZONE # noqa: F401 +# pylint: enable=unused-import +@dns.immutable.immutable class DNSKEY(dns.rdtypes.dnskeybase.DNSKEYBase): """DNSKEY record""" diff --git a/libs/dns/rdtypes/ANY/DS.py b/libs/dns/rdtypes/ANY/DS.py index 577c8d841..3f6c3ee8b 100644 --- a/libs/dns/rdtypes/ANY/DS.py +++ b/libs/dns/rdtypes/ANY/DS.py @@ -1,3 +1,5 @@ +# 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 @@ -14,8 +16,10 @@ # OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. import dns.rdtypes.dsbase +import dns.immutable +@dns.immutable.immutable class DS(dns.rdtypes.dsbase.DSBase): """DS record""" diff --git a/libs/dns/rdtypes/ANY/EUI48.py b/libs/dns/rdtypes/ANY/EUI48.py index aa260e205..0ab88ad0f 100644 --- a/libs/dns/rdtypes/ANY/EUI48.py +++ b/libs/dns/rdtypes/ANY/EUI48.py @@ -1,3 +1,5 @@ +# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license + # Copyright (C) 2015 Red Hat, Inc. # Author: Petr Spacek # @@ -15,15 +17,15 @@ # OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. import dns.rdtypes.euibase +import dns.immutable +@dns.immutable.immutable class EUI48(dns.rdtypes.euibase.EUIBase): - """EUI48 record + """EUI48 record""" - @ivar fingerprint: 48-bit Extended Unique Identifier (EUI-48) - @type fingerprint: string - @see: rfc7043.txt""" + # see: rfc7043.txt byte_len = 6 # 0123456789ab (in hex) text_len = byte_len * 3 - 1 # 01-23-45-67-89-ab diff --git a/libs/dns/rdtypes/ANY/EUI64.py b/libs/dns/rdtypes/ANY/EUI64.py index 5eba350d8..c42957efe 100644 --- a/libs/dns/rdtypes/ANY/EUI64.py +++ b/libs/dns/rdtypes/ANY/EUI64.py @@ -1,3 +1,5 @@ +# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license + # Copyright (C) 2015 Red Hat, Inc. # Author: Petr Spacek # @@ -15,15 +17,15 @@ # OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. import dns.rdtypes.euibase +import dns.immutable +@dns.immutable.immutable class EUI64(dns.rdtypes.euibase.EUIBase): - """EUI64 record + """EUI64 record""" - @ivar fingerprint: 64-bit Extended Unique Identifier (EUI-64) - @type fingerprint: string - @see: rfc7043.txt""" + # 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/libs/dns/rdtypes/ANY/GPOS.py b/libs/dns/rdtypes/ANY/GPOS.py index a359a7712..29fa8f8b0 100644 --- a/libs/dns/rdtypes/ANY/GPOS.py +++ b/libs/dns/rdtypes/ANY/GPOS.py @@ -1,3 +1,5 @@ +# 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 @@ -16,17 +18,22 @@ import struct import dns.exception +import dns.immutable import dns.rdata import dns.tokenizer -from dns._compat import long, text_type 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 - (left, right) = what.split(b'.') + 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(): @@ -35,64 +42,56 @@ def _validate_float_string(what): raise dns.exception.FormError -def _sanitize(value): - if isinstance(value, text_type): - return value.encode() - return value - - +@dns.immutable.immutable class GPOS(dns.rdata.Rdata): - """GPOS record + """GPOS record""" - @ivar latitude: latitude - @type latitude: string - @ivar longitude: longitude - @type longitude: string - @ivar altitude: altitude - @type altitude: string - @see: RFC 1712""" + # see: RFC 1712 __slots__ = ['latitude', 'longitude', 'altitude'] def __init__(self, rdclass, rdtype, latitude, longitude, altitude): - super(GPOS, self).__init__(rdclass, rdtype) + super().__init__(rdclass, rdtype) if isinstance(latitude, float) or \ - isinstance(latitude, int) or \ - isinstance(latitude, long): + isinstance(latitude, int): latitude = str(latitude) if isinstance(longitude, float) or \ - isinstance(longitude, int) or \ - isinstance(longitude, long): + isinstance(longitude, int): longitude = str(longitude) if isinstance(altitude, float) or \ - isinstance(altitude, int) or \ - isinstance(altitude, long): + isinstance(altitude, int): altitude = str(altitude) - latitude = _sanitize(latitude) - longitude = _sanitize(longitude) - altitude = _sanitize(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 '%s %s %s' % (self.latitude.decode(), - self.longitude.decode(), - self.altitude.decode()) + return '{} {} {}'.format(self.latitude.decode(), + self.longitude.decode(), + self.altitude.decode()) @classmethod - def from_text(cls, rdclass, rdtype, tok, origin=None, relativize=True): + 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() - tok.get_eol() return cls(rdclass, rdtype, latitude, longitude, altitude) - def to_wire(self, file, compress=None, origin=None): + def _to_wire(self, file, compress=None, origin=None, canonicalize=False): l = len(self.latitude) assert l < 256 file.write(struct.pack('!B', l)) @@ -107,54 +106,23 @@ class GPOS(dns.rdata.Rdata): file.write(self.altitude) @classmethod - def from_wire(cls, rdclass, rdtype, wire, current, rdlen, origin=None): - l = wire[current] - current += 1 - rdlen -= 1 - if l > rdlen: - raise dns.exception.FormError - latitude = wire[current: current + l].unwrap() - current += l - rdlen -= l - l = wire[current] - current += 1 - rdlen -= 1 - if l > rdlen: - raise dns.exception.FormError - longitude = wire[current: current + l].unwrap() - current += l - rdlen -= l - l = wire[current] - current += 1 - rdlen -= 1 - if l != rdlen: - raise dns.exception.FormError - altitude = wire[current: current + l].unwrap() + 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) - def _get_float_latitude(self): + @property + def float_latitude(self): + "latitude as a floating point value" return float(self.latitude) - def _set_float_latitude(self, value): - self.latitude = str(value) - - float_latitude = property(_get_float_latitude, _set_float_latitude, - doc="latitude as a floating point value") - - def _get_float_longitude(self): + @property + def float_longitude(self): + "longitude as a floating point value" return float(self.longitude) - def _set_float_longitude(self, value): - self.longitude = str(value) - - float_longitude = property(_get_float_longitude, _set_float_longitude, - doc="longitude as a floating point value") - - def _get_float_altitude(self): + @property + def float_altitude(self): + "altitude as a floating point value" return float(self.altitude) - - def _set_float_altitude(self, value): - self.altitude = str(value) - - float_altitude = property(_get_float_altitude, _set_float_altitude, - doc="altitude as a floating point value") diff --git a/libs/dns/rdtypes/ANY/HINFO.py b/libs/dns/rdtypes/ANY/HINFO.py index e5a1bea3d..cd049693b 100644 --- a/libs/dns/rdtypes/ANY/HINFO.py +++ b/libs/dns/rdtypes/ANY/HINFO.py @@ -1,3 +1,5 @@ +# 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 @@ -16,46 +18,37 @@ import struct import dns.exception +import dns.immutable import dns.rdata import dns.tokenizer -from dns._compat import text_type +@dns.immutable.immutable class HINFO(dns.rdata.Rdata): - """HINFO record + """HINFO record""" - @ivar cpu: the CPU type - @type cpu: string - @ivar os: the OS type - @type os: string - @see: RFC 1035""" + # see: RFC 1035 __slots__ = ['cpu', 'os'] def __init__(self, rdclass, rdtype, cpu, os): - super(HINFO, self).__init__(rdclass, rdtype) - if isinstance(cpu, text_type): - self.cpu = cpu.encode() - else: - self.cpu = cpu - if isinstance(os, text_type): - self.os = os.encode() - else: - self.os = 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 '"%s" "%s"' % (dns.rdata._escapify(self.cpu), - dns.rdata._escapify(self.os)) + return '"{}" "{}"'.format(dns.rdata._escapify(self.cpu), + dns.rdata._escapify(self.os)) @classmethod - def from_text(cls, rdclass, rdtype, tok, origin=None, relativize=True): - cpu = tok.get_string() - os = tok.get_string() - tok.get_eol() + 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): + def _to_wire(self, file, compress=None, origin=None, canonicalize=False): l = len(self.cpu) assert l < 256 file.write(struct.pack('!B', l)) @@ -66,19 +59,7 @@ class HINFO(dns.rdata.Rdata): file.write(self.os) @classmethod - def from_wire(cls, rdclass, rdtype, wire, current, rdlen, origin=None): - l = wire[current] - current += 1 - rdlen -= 1 - if l > rdlen: - raise dns.exception.FormError - cpu = wire[current:current + l].unwrap() - current += l - rdlen -= l - l = wire[current] - current += 1 - rdlen -= 1 - if l != rdlen: - raise dns.exception.FormError - os = wire[current: current + l].unwrap() + 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/libs/dns/rdtypes/ANY/HIP.py b/libs/dns/rdtypes/ANY/HIP.py index fbe955c35..e887359b7 100644 --- a/libs/dns/rdtypes/ANY/HIP.py +++ b/libs/dns/rdtypes/ANY/HIP.py @@ -1,3 +1,5 @@ +# 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 @@ -18,96 +20,66 @@ import base64 import binascii import dns.exception +import dns.immutable import dns.rdata import dns.rdatatype +@dns.immutable.immutable class HIP(dns.rdata.Rdata): - """HIP record + """HIP record""" - @ivar hit: the host identity tag - @type hit: string - @ivar algorithm: the public key cryptographic algorithm - @type algorithm: int - @ivar key: the public key - @type key: string - @ivar servers: the rendezvous servers - @type servers: list of dns.name.Name objects - @see: RFC 5205""" + # see: RFC 5205 __slots__ = ['hit', 'algorithm', 'key', 'servers'] def __init__(self, rdclass, rdtype, hit, algorithm, key, servers): - super(HIP, self).__init__(rdclass, rdtype) - self.hit = hit - self.algorithm = algorithm - self.key = key - self.servers = 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 = u'' + text = '' servers = [] for server in self.servers: servers.append(server.choose_relativity(origin, relativize)) if len(servers) > 0: - text += (u' ' + u' '.join((x.to_unicode() for x in servers))) - return u'%u %s %s%s' % (self.algorithm, hit, key, text) + 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): + 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()) - if len(hit) > 255: - raise dns.exception.SyntaxError("HIT too long") key = base64.b64decode(tok.get_string().encode()) servers = [] - while 1: - token = tok.get() - if token.is_eol_or_eof(): - break - server = dns.name.from_text(token.value, origin) - server.choose_relativity(origin, relativize) + 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): + 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) + server.to_wire(file, None, origin, False) @classmethod - def from_wire(cls, rdclass, rdtype, wire, current, rdlen, origin=None): - (lh, algorithm, lk) = struct.unpack('!BBH', - wire[current: current + 4]) - current += 4 - rdlen -= 4 - hit = wire[current: current + lh].unwrap() - current += lh - rdlen -= lh - key = wire[current: current + lk].unwrap() - current += lk - rdlen -= lk + 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 rdlen > 0: - (server, cused) = dns.name.from_wire(wire[: current + rdlen], - current) - current += cused - rdlen -= cused - if origin is not None: - server = server.relativize(origin) + while parser.remaining() > 0: + server = parser.get_name(origin) servers.append(server) return cls(rdclass, rdtype, hit, algorithm, key, servers) - - def choose_relativity(self, origin=None, relativize=True): - servers = [] - for server in self.servers: - server = server.choose_relativity(origin, relativize) - servers.append(server) - self.servers = servers diff --git a/libs/dns/rdtypes/ANY/ISDN.py b/libs/dns/rdtypes/ANY/ISDN.py index da2ae3af3..b9a49adbd 100644 --- a/libs/dns/rdtypes/ANY/ISDN.py +++ b/libs/dns/rdtypes/ANY/ISDN.py @@ -1,3 +1,5 @@ +# 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 @@ -16,55 +18,44 @@ import struct import dns.exception +import dns.immutable import dns.rdata import dns.tokenizer -from dns._compat import text_type +@dns.immutable.immutable class ISDN(dns.rdata.Rdata): - """ISDN record + """ISDN record""" - @ivar address: the ISDN address - @type address: string - @ivar subaddress: the ISDN subaddress (or '' if not present) - @type subaddress: string - @see: RFC 1183""" + # see: RFC 1183 __slots__ = ['address', 'subaddress'] def __init__(self, rdclass, rdtype, address, subaddress): - super(ISDN, self).__init__(rdclass, rdtype) - if isinstance(address, text_type): - self.address = address.encode() - else: - self.address = address - if isinstance(address, text_type): - self.subaddress = subaddress.encode() - else: - self.subaddress = 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 '"%s" "%s"' % (dns.rdata._escapify(self.address), - dns.rdata._escapify(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): + def from_text(cls, rdclass, rdtype, tok, origin=None, relativize=True, + relativize_to=None): address = tok.get_string() - t = tok.get() - if not t.is_eol_or_eof(): - tok.unget(t) - subaddress = tok.get_string() + tokens = tok.get_remaining(max_tokens=1) + if len(tokens) >= 1: + subaddress = tokens[0].unescape().value else: - tok.unget(t) subaddress = '' - tok.get_eol() return cls(rdclass, rdtype, address, subaddress) - def to_wire(self, file, compress=None, origin=None): + def _to_wire(self, file, compress=None, origin=None, canonicalize=False): l = len(self.address) assert l < 256 file.write(struct.pack('!B', l)) @@ -76,22 +67,10 @@ class ISDN(dns.rdata.Rdata): file.write(self.subaddress) @classmethod - def from_wire(cls, rdclass, rdtype, wire, current, rdlen, origin=None): - l = wire[current] - current += 1 - rdlen -= 1 - if l > rdlen: - raise dns.exception.FormError - address = wire[current: current + l].unwrap() - current += l - rdlen -= l - if rdlen > 0: - l = wire[current] - current += 1 - rdlen -= 1 - if l != rdlen: - raise dns.exception.FormError - subaddress = wire[current: current + l].unwrap() + 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 = '' + subaddress = b'' return cls(rdclass, rdtype, address, subaddress) diff --git a/libs/dns/rdtypes/ANY/L32.py b/libs/dns/rdtypes/ANY/L32.py new file mode 100644 index 000000000..47eff9584 --- /dev/null +++ b/libs/dns/rdtypes/ANY/L32.py @@ -0,0 +1,40 @@ +# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license + +import struct + +import dns.immutable + + +@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/libs/dns/rdtypes/ANY/L64.py b/libs/dns/rdtypes/ANY/L64.py new file mode 100644 index 000000000..aab36a828 --- /dev/null +++ b/libs/dns/rdtypes/ANY/L64.py @@ -0,0 +1,48 @@ +# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license + +import struct + +import dns.immutable +import dns.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/libs/dns/rdtypes/ANY/LOC.py b/libs/dns/rdtypes/ANY/LOC.py index b433da948..c93989947 100644 --- a/libs/dns/rdtypes/ANY/LOC.py +++ b/libs/dns/rdtypes/ANY/LOC.py @@ -1,3 +1,5 @@ +# 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 @@ -13,29 +15,33 @@ # ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT # OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -from __future__ import division - import struct import dns.exception +import dns.immutable import dns.rdata -from dns._compat import long, xrange, round_py2_compat -_pows = tuple(long(10**i) for i in range(0, 11)) +_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 in xrange(len(_pows)): - if what // _pows[i] == long(0): + for (i, pow) in enumerate(_pows): + if what < pow: exp = i - 1 break if exp is None or exp < 0: @@ -49,7 +55,7 @@ def _float_to_tuple(what): what *= -1 else: sign = 1 - what = round_py2_compat(what * 3600000) + what = round(what * 3600000) degrees = int(what // 3600000) what -= degrees * 3600000 minutes = int(what // 60000) @@ -69,7 +75,7 @@ def _tuple_to_float(what): def _encode_size(what, desc): - what = long(what) + what = int(what) exponent = _exponent_of(what, desc) & 0xF base = what // pow(10, exponent) & 0xF return base * 16 + exponent @@ -78,32 +84,32 @@ def _encode_size(what, desc): def _decode_size(what, desc): exponent = what & 0x0F if exponent > 9: - raise dns.exception.SyntaxError("bad %s exponent" % desc) + raise dns.exception.FormError("bad %s exponent" % desc) base = (what & 0xF0) >> 4 if base > 9: - raise dns.exception.SyntaxError("bad %s base" % desc) - return long(base) * pow(10, exponent) + 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 + """LOC record""" - @ivar latitude: latitude - @type latitude: (int, int, int, int, sign) tuple specifying the degrees, minutes, - seconds, milliseconds, and sign of the coordinate. - @ivar longitude: longitude - @type longitude: (int, int, int, int, sign) tuple specifying the degrees, - minutes, seconds, milliseconds, and sign of the coordinate. - @ivar altitude: altitude - @type altitude: float - @ivar size: size of the sphere - @type size: float - @ivar horizontal_precision: horizontal precision - @type horizontal_precision: float - @ivar vertical_precision: vertical precision - @type vertical_precision: float - @see: RFC 1876""" + # see: RFC 1876 __slots__ = ['latitude', 'longitude', 'altitude', 'size', 'horizontal_precision', 'vertical_precision'] @@ -119,17 +125,19 @@ class LOC(dns.rdata.Rdata): degrees. The other parameters are floats. Size, horizontal precision, and vertical precision are specified in centimeters.""" - super(LOC, self).__init__(rdclass, rdtype) - if isinstance(latitude, int) or isinstance(latitude, long): + super().__init__(rdclass, rdtype) + if isinstance(latitude, int): latitude = float(latitude) if isinstance(latitude, float): latitude = _float_to_tuple(latitude) - self.latitude = latitude - if isinstance(longitude, int) or isinstance(longitude, long): + _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) - self.longitude = longitude + _check_coordinate_list(longitude, -180, 180) + self.longitude = tuple(longitude) self.altitude = float(altitude) self.size = float(size) self.horizontal_precision = float(hprec) @@ -156,14 +164,15 @@ class LOC(dns.rdata.Rdata): if self.size != _default_size or \ self.horizontal_precision != _default_hprec or \ self.vertical_precision != _default_vprec: - text += " %0.2fm %0.2fm %0.2fm" % ( + 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): + 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 @@ -181,8 +190,6 @@ class LOC(dns.rdata.Rdata): raise dns.exception.SyntaxError( 'bad latitude seconds value') latitude[2] = int(seconds) - if latitude[2] >= 60: - raise dns.exception.SyntaxError('latitude seconds >= 60') l = len(milliseconds) if l == 0 or l > 3 or not milliseconds.isdigit(): raise dns.exception.SyntaxError( @@ -214,8 +221,6 @@ class LOC(dns.rdata.Rdata): raise dns.exception.SyntaxError( 'bad longitude seconds value') longitude[2] = int(seconds) - if longitude[2] >= 60: - raise dns.exception.SyntaxError('longitude seconds >= 60') l = len(milliseconds) if l == 0 or l > 3 or not milliseconds.isdigit(): raise dns.exception.SyntaxError( @@ -241,41 +246,43 @@ class LOC(dns.rdata.Rdata): t = t[0: -1] altitude = float(t) * 100.0 # m -> cm - token = tok.get().unescape() - if not token.is_eol_or_eof(): - value = token.value + 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 - token = tok.get().unescape() - if not token.is_eol_or_eof(): - value = token.value + if len(tokens) >= 2: + value = tokens[1].unescape().value if value[-1] == 'm': value = value[0: -1] hprec = float(value) * 100.0 # m -> cm - token = tok.get().unescape() - if not token.is_eol_or_eof(): - value = token.value + if len(tokens) >= 3: + value = tokens[2].unescape().value if value[-1] == 'm': value = value[0: -1] vprec = float(value) * 100.0 # m -> cm - tok.get_eol() + + # 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): + 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 = long(0x80000000) + milliseconds + latitude = 0x80000000 + milliseconds milliseconds = (self.longitude[0] * 3600000 + self.longitude[1] * 60000 + self.longitude[2] * 1000 + self.longitude[3]) * self.longitude[4] - longitude = long(0x80000000) + milliseconds - altitude = long(self.altitude) + long(10000000) + 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") @@ -284,21 +291,23 @@ class LOC(dns.rdata.Rdata): file.write(wire) @classmethod - def from_wire(cls, rdclass, rdtype, wire, current, rdlen, origin=None): + def from_wire_parser(cls, rdclass, rdtype, parser, origin=None): (version, size, hprec, vprec, latitude, longitude, altitude) = \ - struct.unpack("!BBBBIII", wire[current: current + rdlen]) - if latitude > long(0x80000000): - latitude = float(latitude - long(0x80000000)) / 3600000 - else: - latitude = -1 * float(long(0x80000000) - latitude) / 3600000 - if latitude < -90.0 or latitude > 90.0: + 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 longitude > long(0x80000000): - longitude = float(longitude - long(0x80000000)) / 3600000 + if latitude > 0x80000000: + latitude = (latitude - 0x80000000) / 3600000 else: - longitude = -1 * float(long(0x80000000) - longitude) / 3600000 - if longitude < -180.0 or longitude > 180.0: + 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") @@ -306,20 +315,12 @@ class LOC(dns.rdata.Rdata): return cls(rdclass, rdtype, latitude, longitude, altitude, size, hprec, vprec) - def _get_float_latitude(self): + @property + def float_latitude(self): + "latitude as a floating point value" return _tuple_to_float(self.latitude) - def _set_float_latitude(self, value): - self.latitude = _float_to_tuple(value) - - float_latitude = property(_get_float_latitude, _set_float_latitude, - doc="latitude as a floating point value") - - def _get_float_longitude(self): + @property + def float_longitude(self): + "longitude as a floating point value" return _tuple_to_float(self.longitude) - - def _set_float_longitude(self, value): - self.longitude = _float_to_tuple(value) - - float_longitude = property(_get_float_longitude, _set_float_longitude, - doc="longitude as a floating point value") diff --git a/libs/dns/rdtypes/ANY/LP.py b/libs/dns/rdtypes/ANY/LP.py new file mode 100644 index 000000000..b6a2e36ca --- /dev/null +++ b/libs/dns/rdtypes/ANY/LP.py @@ -0,0 +1,41 @@ +# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license + +import struct + +import dns.immutable + + +@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/libs/dns/rdtypes/ANY/MX.py b/libs/dns/rdtypes/ANY/MX.py index 3a6735dc5..a697ea455 100644 --- a/libs/dns/rdtypes/ANY/MX.py +++ b/libs/dns/rdtypes/ANY/MX.py @@ -1,3 +1,5 @@ +# 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 @@ -14,8 +16,10 @@ # OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. import dns.rdtypes.mxbase +import dns.immutable +@dns.immutable.immutable class MX(dns.rdtypes.mxbase.MXBase): """MX record""" diff --git a/libs/dns/rdtypes/ANY/NID.py b/libs/dns/rdtypes/ANY/NID.py new file mode 100644 index 000000000..74951bbf5 --- /dev/null +++ b/libs/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/libs/dns/hash.py b/libs/dns/rdtypes/ANY/NINFO.py similarity index 67% rename from libs/dns/hash.py rename to libs/dns/rdtypes/ANY/NINFO.py index 966838a17..d53e96765 100644 --- a/libs/dns/hash.py +++ b/libs/dns/rdtypes/ANY/NINFO.py @@ -1,4 +1,6 @@ -# Copyright (C) 2011 Nominum, Inc. +# 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, @@ -13,19 +15,13 @@ # ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT # OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -"""Hashing backwards compatibility wrapper""" - -import hashlib +import dns.rdtypes.txtbase +import dns.immutable -hashes = {} -hashes['MD5'] = hashlib.md5 -hashes['SHA1'] = hashlib.sha1 -hashes['SHA224'] = hashlib.sha224 -hashes['SHA256'] = hashlib.sha256 -hashes['SHA384'] = hashlib.sha384 -hashes['SHA512'] = hashlib.sha512 +@dns.immutable.immutable +class NINFO(dns.rdtypes.txtbase.TXTBase): + """NINFO record""" -def get(algorithm): - return hashes[algorithm.upper()] + # see: draft-reid-dnsext-zs-01 diff --git a/libs/dns/rdtypes/ANY/NS.py b/libs/dns/rdtypes/ANY/NS.py index ae56d819e..a0cc232a1 100644 --- a/libs/dns/rdtypes/ANY/NS.py +++ b/libs/dns/rdtypes/ANY/NS.py @@ -1,3 +1,5 @@ +# 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 @@ -14,8 +16,10 @@ # OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. import dns.rdtypes.nsbase +import dns.immutable +@dns.immutable.immutable class NS(dns.rdtypes.nsbase.NSBase): """NS record""" diff --git a/libs/dns/rdtypes/ANY/NSEC.py b/libs/dns/rdtypes/ANY/NSEC.py index dfe96859f..dc31f4c41 100644 --- a/libs/dns/rdtypes/ANY/NSEC.py +++ b/libs/dns/rdtypes/ANY/NSEC.py @@ -1,3 +1,5 @@ +# 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 @@ -13,114 +15,53 @@ # 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.rdatatype import dns.name -from dns._compat import xrange +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 - - @ivar next: the next name - @type next: dns.name.Name object - @ivar windows: the windowed bitmap list - @type windows: list of (window number, string) tuples""" + """NSEC record""" __slots__ = ['next', 'windows'] def __init__(self, rdclass, rdtype, next, windows): - super(NSEC, self).__init__(rdclass, rdtype) - self.next = next - self.windows = 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 = '' - for (window, bitmap) in self.windows: - bits = [] - for i in xrange(0, len(bitmap)): - byte = bitmap[i] - for j in xrange(0, 8): - if byte & (0x80 >> j): - bits.append(dns.rdatatype.to_text(window * 256 + - i * 8 + j)) - text += (' ' + ' '.join(bits)) - return '%s%s' % (next, text) + text = Bitmap(self.windows).to_text() + return '{}{}'.format(next, text) @classmethod - def from_text(cls, rdclass, rdtype, tok, origin=None, relativize=True): - next = tok.get_name() - next = next.choose_relativity(origin, relativize) - rdtypes = [] - while 1: - token = tok.get().unescape() - if token.is_eol_or_eof(): - break - nrdtype = dns.rdatatype.from_text(token.value) - if nrdtype == 0: - raise dns.exception.SyntaxError("NSEC with bit 0") - if nrdtype > 65535: - raise dns.exception.SyntaxError("NSEC with bit > 65535") - rdtypes.append(nrdtype) - rdtypes.sort() - window = 0 - octets = 0 - prior_rdtype = 0 - bitmap = bytearray(b'\0' * 32) - windows = [] - for nrdtype in rdtypes: - if nrdtype == prior_rdtype: - continue - prior_rdtype = nrdtype - new_window = nrdtype // 256 - if new_window != window: - windows.append((window, bitmap[0:octets])) - bitmap = bytearray(b'\0' * 32) - window = new_window - offset = nrdtype % 256 - byte = offset // 8 - bit = offset % 8 - octets = byte + 1 - bitmap[byte] = bitmap[byte] | (0x80 >> bit) - - windows.append((window, bitmap[0:octets])) + 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): - self.next.to_wire(file, None, origin) - for (window, bitmap) in self.windows: - file.write(struct.pack('!BB', window, len(bitmap))) - file.write(bitmap) + 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(cls, rdclass, rdtype, wire, current, rdlen, origin=None): - (next, cused) = dns.name.from_wire(wire[: current + rdlen], current) - current += cused - rdlen -= cused - windows = [] - while rdlen > 0: - if rdlen < 3: - raise dns.exception.FormError("NSEC too short") - window = wire[current] - octets = wire[current + 1] - if octets == 0 or octets > 32: - raise dns.exception.FormError("bad NSEC octets") - current += 2 - rdlen -= 2 - if rdlen < octets: - raise dns.exception.FormError("bad NSEC bitmap length") - bitmap = bytearray(wire[current: current + octets].unwrap()) - current += octets - rdlen -= octets - windows.append((window, bitmap)) - if origin is not None: - next = next.relativize(origin) - return cls(rdclass, rdtype, next, windows) - - def choose_relativity(self, origin=None, relativize=True): - self.next = self.next.choose_relativity(origin, relativize) + 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/libs/dns/rdtypes/ANY/NSEC3.py b/libs/dns/rdtypes/ANY/NSEC3.py index 9a15687ba..14242bda7 100644 --- a/libs/dns/rdtypes/ANY/NSEC3.py +++ b/libs/dns/rdtypes/ANY/NSEC3.py @@ -1,4 +1,6 @@ -# Copyright (C) 2004-2007, 2009-2011 Nominum, Inc. +# 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, @@ -15,24 +17,19 @@ import base64 import binascii -import string import struct import dns.exception +import dns.immutable import dns.rdata import dns.rdatatype -from dns._compat import xrange, text_type +import dns.rdtypes.util -try: - b32_hex_to_normal = string.maketrans('0123456789ABCDEFGHIJKLMNOPQRSTUV', - 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567') - b32_normal_to_hex = string.maketrans('ABCDEFGHIJKLMNOPQRSTUVWXYZ234567', - '0123456789ABCDEFGHIJKLMNOPQRSTUV') -except AttributeError: - b32_hex_to_normal = bytes.maketrans(b'0123456789ABCDEFGHIJKLMNOPQRSTUV', - b'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567') - b32_normal_to_hex = bytes.maketrans(b'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567', - b'0123456789ABCDEFGHIJKLMNOPQRSTUV') + +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 @@ -41,37 +38,29 @@ SHA1 = 1 OPTOUT = 1 +@dns.immutable.immutable +class Bitmap(dns.rdtypes.util.Bitmap): + type_name = 'NSEC3' + + +@dns.immutable.immutable class NSEC3(dns.rdata.Rdata): - """NSEC3 record - - @ivar algorithm: the hash algorithm number - @type algorithm: int - @ivar flags: the flags - @type flags: int - @ivar iterations: the number of iterations - @type iterations: int - @ivar salt: the salt - @type salt: string - @ivar next: the next name hash - @type next: string - @ivar windows: the windowed bitmap list - @type windows: list of (window number, string) tuples""" + """NSEC3 record""" __slots__ = ['algorithm', 'flags', 'iterations', 'salt', 'next', 'windows'] def __init__(self, rdclass, rdtype, algorithm, flags, iterations, salt, next, windows): - super(NSEC3, self).__init__(rdclass, rdtype) - self.algorithm = algorithm - self.flags = flags - self.iterations = iterations - if isinstance(salt, text_type): - self.salt = salt.encode() - else: - self.salt = salt - self.next = next - self.windows = 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 to_text(self, origin=None, relativize=True, **kw): next = base64.b32encode(self.next).translate( @@ -80,70 +69,29 @@ class NSEC3(dns.rdata.Rdata): salt = '-' else: salt = binascii.hexlify(self.salt).decode() - text = u'' - for (window, bitmap) in self.windows: - bits = [] - for i in xrange(0, len(bitmap)): - byte = bitmap[i] - for j in xrange(0, 8): - if byte & (0x80 >> j): - bits.append(dns.rdatatype.to_text(window * 256 + - i * 8 + j)) - text += (u' ' + u' '.join(bits)) - return u'%u %u %u %s %s%s' % (self.algorithm, self.flags, - self.iterations, salt, next, text) + 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): + 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 == u'-': + if salt == '-': salt = b'' else: salt = binascii.unhexlify(salt.encode('ascii')) next = tok.get_string().encode( 'ascii').upper().translate(b32_hex_to_normal) next = base64.b32decode(next) - rdtypes = [] - while 1: - token = tok.get().unescape() - if token.is_eol_or_eof(): - break - nrdtype = dns.rdatatype.from_text(token.value) - if nrdtype == 0: - raise dns.exception.SyntaxError("NSEC3 with bit 0") - if nrdtype > 65535: - raise dns.exception.SyntaxError("NSEC3 with bit > 65535") - rdtypes.append(nrdtype) - rdtypes.sort() - window = 0 - octets = 0 - prior_rdtype = 0 - bitmap = bytearray(b'\0' * 32) - windows = [] - for nrdtype in rdtypes: - if nrdtype == prior_rdtype: - continue - prior_rdtype = nrdtype - new_window = nrdtype // 256 - if new_window != window: - if octets != 0: - windows.append((window, ''.join(bitmap[0:octets]))) - bitmap = bytearray(b'\0' * 32) - window = new_window - offset = nrdtype % 256 - byte = offset // 8 - bit = offset % 8 - octets = byte + 1 - bitmap[byte] = bitmap[byte] | (0x80 >> bit) - if octets != 0: - windows.append((window, bitmap[0:octets])) + bitmap = Bitmap.from_text(tok) return cls(rdclass, rdtype, algorithm, flags, iterations, salt, next, - windows) + bitmap) - def to_wire(self, file, compress=None, origin=None): + 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)) @@ -151,41 +99,13 @@ class NSEC3(dns.rdata.Rdata): l = len(self.next) file.write(struct.pack("!B", l)) file.write(self.next) - for (window, bitmap) in self.windows: - file.write(struct.pack("!BB", window, len(bitmap))) - file.write(bitmap) + Bitmap(self.windows).to_wire(file) @classmethod - def from_wire(cls, rdclass, rdtype, wire, current, rdlen, origin=None): - (algorithm, flags, iterations, slen) = \ - struct.unpack('!BBHB', wire[current: current + 5]) - - current += 5 - rdlen -= 5 - salt = wire[current: current + slen].unwrap() - current += slen - rdlen -= slen - nlen = wire[current] - current += 1 - rdlen -= 1 - next = wire[current: current + nlen].unwrap() - current += nlen - rdlen -= nlen - windows = [] - while rdlen > 0: - if rdlen < 3: - raise dns.exception.FormError("NSEC3 too short") - window = wire[current] - octets = wire[current + 1] - if octets == 0 or octets > 32: - raise dns.exception.FormError("bad NSEC3 octets") - current += 2 - rdlen -= 2 - if rdlen < octets: - raise dns.exception.FormError("bad NSEC3 bitmap length") - bitmap = bytearray(wire[current: current + octets].unwrap()) - current += octets - rdlen -= octets - windows.append((window, bitmap)) + 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, - windows) + bitmap) diff --git a/libs/dns/rdtypes/ANY/NSEC3PARAM.py b/libs/dns/rdtypes/ANY/NSEC3PARAM.py index 36bf74094..299bf6ed1 100644 --- a/libs/dns/rdtypes/ANY/NSEC3PARAM.py +++ b/libs/dns/rdtypes/ANY/NSEC3PARAM.py @@ -1,3 +1,5 @@ +# 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 @@ -17,34 +19,23 @@ import struct import binascii import dns.exception +import dns.immutable import dns.rdata -from dns._compat import text_type +@dns.immutable.immutable class NSEC3PARAM(dns.rdata.Rdata): - """NSEC3PARAM record - - @ivar algorithm: the hash algorithm number - @type algorithm: int - @ivar flags: the flags - @type flags: int - @ivar iterations: the number of iterations - @type iterations: int - @ivar salt: the salt - @type salt: string""" + """NSEC3PARAM record""" __slots__ = ['algorithm', 'flags', 'iterations', 'salt'] def __init__(self, rdclass, rdtype, algorithm, flags, iterations, salt): - super(NSEC3PARAM, self).__init__(rdclass, rdtype) - self.algorithm = algorithm - self.flags = flags - self.iterations = iterations - if isinstance(salt, text_type): - self.salt = salt.encode() - else: - self.salt = 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'': @@ -55,7 +46,8 @@ class NSEC3PARAM(dns.rdata.Rdata): salt) @classmethod - def from_text(cls, rdclass, rdtype, tok, origin=None, relativize=True): + 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() @@ -64,25 +56,16 @@ class NSEC3PARAM(dns.rdata.Rdata): salt = '' else: salt = binascii.unhexlify(salt.encode()) - tok.get_eol() return cls(rdclass, rdtype, algorithm, flags, iterations, salt) - def to_wire(self, file, compress=None, origin=None): + 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(cls, rdclass, rdtype, wire, current, rdlen, origin=None): - (algorithm, flags, iterations, slen) = \ - struct.unpack('!BBHB', - wire[current: current + 5]) - current += 5 - rdlen -= 5 - salt = wire[current: current + slen].unwrap() - current += slen - rdlen -= slen - if rdlen != 0: - raise dns.exception.FormError + 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/libs/dns/rdtypes/ANY/OPENPGPKEY.py b/libs/dns/rdtypes/ANY/OPENPGPKEY.py new file mode 100644 index 000000000..dcfa028d0 --- /dev/null +++ b/libs/dns/rdtypes/ANY/OPENPGPKEY.py @@ -0,0 +1,52 @@ +# 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/libs/dns/rdtypes/ANY/OPT.py b/libs/dns/rdtypes/ANY/OPT.py new file mode 100644 index 000000000..69b8fe75d --- /dev/null +++ b/libs/dns/rdtypes/ANY/OPT.py @@ -0,0 +1,76 @@ +# 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.immutable +import dns.exception +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/libs/dns/rdtypes/ANY/PTR.py b/libs/dns/rdtypes/ANY/PTR.py index 250187a61..265bed035 100644 --- a/libs/dns/rdtypes/ANY/PTR.py +++ b/libs/dns/rdtypes/ANY/PTR.py @@ -1,3 +1,5 @@ +# 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 @@ -14,8 +16,10 @@ # OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. import dns.rdtypes.nsbase +import dns.immutable +@dns.immutable.immutable class PTR(dns.rdtypes.nsbase.NSBase): """PTR record""" diff --git a/libs/dns/rdtypes/ANY/RP.py b/libs/dns/rdtypes/ANY/RP.py index e9071c763..a4e2297d4 100644 --- a/libs/dns/rdtypes/ANY/RP.py +++ b/libs/dns/rdtypes/ANY/RP.py @@ -1,3 +1,5 @@ +# 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 @@ -14,67 +16,43 @@ # OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. import dns.exception +import dns.immutable import dns.rdata import dns.name +@dns.immutable.immutable class RP(dns.rdata.Rdata): - """RP record + """RP record""" - @ivar mbox: The responsible person's mailbox - @type mbox: dns.name.Name object - @ivar txt: The owner name of a node with TXT records, or the root name - if no TXT records are associated with this RP. - @type txt: dns.name.Name object - @see: RFC 1183""" + # see: RFC 1183 __slots__ = ['mbox', 'txt'] def __init__(self, rdclass, rdtype, mbox, txt): - super(RP, self).__init__(rdclass, rdtype) - self.mbox = mbox - self.txt = 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 "%s %s" % (str(mbox), str(txt)) + return "{} {}".format(str(mbox), str(txt)) @classmethod - def from_text(cls, rdclass, rdtype, tok, origin=None, relativize=True): - mbox = tok.get_name() - txt = tok.get_name() - mbox = mbox.choose_relativity(origin, relativize) - txt = txt.choose_relativity(origin, relativize) - tok.get_eol() + 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): - self.mbox.to_wire(file, None, origin) - self.txt.to_wire(file, None, origin) - - def to_digestable(self, origin=None): - return self.mbox.to_digestable(origin) + \ - self.txt.to_digestable(origin) + 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(cls, rdclass, rdtype, wire, current, rdlen, origin=None): - (mbox, cused) = dns.name.from_wire(wire[: current + rdlen], - current) - current += cused - rdlen -= cused - if rdlen <= 0: - raise dns.exception.FormError - (txt, cused) = dns.name.from_wire(wire[: current + rdlen], - current) - if cused != rdlen: - raise dns.exception.FormError - if origin is not None: - mbox = mbox.relativize(origin) - txt = txt.relativize(origin) + 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) - - def choose_relativity(self, origin=None, relativize=True): - self.mbox = self.mbox.choose_relativity(origin, relativize) - self.txt = self.txt.choose_relativity(origin, relativize) diff --git a/libs/dns/rdtypes/ANY/RRSIG.py b/libs/dns/rdtypes/ANY/RRSIG.py index 953dfb9a5..d050ccc6f 100644 --- a/libs/dns/rdtypes/ANY/RRSIG.py +++ b/libs/dns/rdtypes/ANY/RRSIG.py @@ -1,3 +1,5 @@ +# 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 @@ -19,6 +21,7 @@ import struct import time import dns.dnssec +import dns.immutable import dns.exception import dns.rdata import dns.rdatatype @@ -30,6 +33,8 @@ class BadSigTime(dns.exception.DNSException): 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]) @@ -46,28 +51,10 @@ 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 - - @ivar type_covered: the rdata type this signature covers - @type type_covered: int - @ivar algorithm: the algorithm used for the sig - @type algorithm: int - @ivar labels: number of labels - @type labels: int - @ivar original_ttl: the original TTL - @type original_ttl: long - @ivar expiration: signature expiration time - @type expiration: long - @ivar inception: signature inception time - @type inception: long - @ivar key_tag: the key tag - @type key_tag: int - @ivar signer: the signer - @type signer: dns.name.Name object - @ivar signature: the signature - @type signature: string""" + """RRSIG record""" __slots__ = ['type_covered', 'algorithm', 'labels', 'original_ttl', 'expiration', 'inception', 'key_tag', 'signer', @@ -76,16 +63,16 @@ class RRSIG(dns.rdata.Rdata): def __init__(self, rdclass, rdtype, type_covered, algorithm, labels, original_ttl, expiration, inception, key_tag, signer, signature): - super(RRSIG, self).__init__(rdclass, rdtype) - self.type_covered = type_covered - self.algorithm = algorithm - self.labels = labels - self.original_ttl = original_ttl - self.expiration = expiration - self.inception = inception - self.key_tag = key_tag - self.signer = signer - self.signature = signature + super().__init__(rdclass, rdtype) + self.type_covered = self._as_rdatatype(type_covered) + self.algorithm = dns.dnssec.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 @@ -100,11 +87,12 @@ class RRSIG(dns.rdata.Rdata): posixtime_to_sigtime(self.inception), self.key_tag, self.signer.choose_relativity(origin, relativize), - dns.rdata._base64ify(self.signature) + dns.rdata._base64ify(self.signature, **kw) ) @classmethod - def from_text(cls, rdclass, rdtype, tok, origin=None, relativize=True): + 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.dnssec.algorithm_from_text(tok.get_string()) labels = tok.get_int() @@ -112,45 +100,25 @@ class RRSIG(dns.rdata.Rdata): expiration = sigtime_to_posixtime(tok.get_string()) inception = sigtime_to_posixtime(tok.get_string()) key_tag = tok.get_int() - signer = tok.get_name() - signer = signer.choose_relativity(origin, relativize) - chunks = [] - while 1: - t = tok.get().unescape() - if t.is_eol_or_eof(): - break - if not t.is_identifier(): - raise dns.exception.SyntaxError - chunks.append(t.value.encode()) - b64 = b''.join(chunks) + 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): + 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) + self.signer.to_wire(file, None, origin, canonicalize) file.write(self.signature) @classmethod - def from_wire(cls, rdclass, rdtype, wire, current, rdlen, origin=None): - header = struct.unpack('!HBBIIIH', wire[current: current + 18]) - current += 18 - rdlen -= 18 - (signer, cused) = dns.name.from_wire(wire[: current + rdlen], current) - current += cused - rdlen -= cused - if origin is not None: - signer = signer.relativize(origin) - signature = wire[current: current + rdlen].unwrap() - return cls(rdclass, rdtype, header[0], header[1], header[2], - header[3], header[4], header[5], header[6], signer, - signature) - - def choose_relativity(self, origin=None, relativize=True): - self.signer = self.signer.choose_relativity(origin, relativize) + 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/libs/dns/rdtypes/ANY/RT.py b/libs/dns/rdtypes/ANY/RT.py index 88b754868..8d9c6bd05 100644 --- a/libs/dns/rdtypes/ANY/RT.py +++ b/libs/dns/rdtypes/ANY/RT.py @@ -1,3 +1,5 @@ +# 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 @@ -14,8 +16,10 @@ # OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. import dns.rdtypes.mxbase +import dns.immutable +@dns.immutable.immutable class RT(dns.rdtypes.mxbase.UncompressedDowncasingMX): """RT record""" diff --git a/libs/dns/rdtypes/ANY/SMIMEA.py b/libs/dns/rdtypes/ANY/SMIMEA.py new file mode 100644 index 000000000..55d87bf85 --- /dev/null +++ b/libs/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/libs/dns/rdtypes/ANY/SOA.py b/libs/dns/rdtypes/ANY/SOA.py index cc0098e8b..7ce886525 100644 --- a/libs/dns/rdtypes/ANY/SOA.py +++ b/libs/dns/rdtypes/ANY/SOA.py @@ -1,3 +1,5 @@ +# 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 @@ -16,44 +18,31 @@ import struct import dns.exception +import dns.immutable import dns.rdata import dns.name +@dns.immutable.immutable class SOA(dns.rdata.Rdata): - """SOA record + """SOA record""" - @ivar mname: the SOA MNAME (master name) field - @type mname: dns.name.Name object - @ivar rname: the SOA RNAME (responsible name) field - @type rname: dns.name.Name object - @ivar serial: The zone's serial number - @type serial: int - @ivar refresh: The zone's refresh value (in seconds) - @type refresh: int - @ivar retry: The zone's retry value (in seconds) - @type retry: int - @ivar expire: The zone's expiration value (in seconds) - @type expire: int - @ivar minimum: The zone's negative caching time (in seconds, called - "minimum" for historical reasons) - @type minimum: int - @see: RFC 1035""" + # see: RFC 1035 __slots__ = ['mname', 'rname', 'serial', 'refresh', 'retry', 'expire', 'minimum'] def __init__(self, rdclass, rdtype, mname, rname, serial, refresh, retry, expire, minimum): - super(SOA, self).__init__(rdclass, rdtype) - self.mname = mname - self.rname = rname - self.serial = serial - self.refresh = refresh - self.retry = retry - self.expire = expire - self.minimum = 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) @@ -63,52 +52,27 @@ class SOA(dns.rdata.Rdata): self.expire, self.minimum) @classmethod - def from_text(cls, rdclass, rdtype, tok, origin=None, relativize=True): - mname = tok.get_name() - rname = tok.get_name() - mname = mname.choose_relativity(origin, relativize) - rname = rname.choose_relativity(origin, relativize) + 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() - tok.get_eol() return cls(rdclass, rdtype, mname, rname, serial, refresh, retry, expire, minimum) - def to_wire(self, file, compress=None, origin=None): - self.mname.to_wire(file, compress, origin) - self.rname.to_wire(file, compress, origin) + 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) - def to_digestable(self, origin=None): - return self.mname.to_digestable(origin) + \ - self.rname.to_digestable(origin) + \ - struct.pack('!IIIII', self.serial, self.refresh, - self.retry, self.expire, self.minimum) - @classmethod - def from_wire(cls, rdclass, rdtype, wire, current, rdlen, origin=None): - (mname, cused) = dns.name.from_wire(wire[: current + rdlen], current) - current += cused - rdlen -= cused - (rname, cused) = dns.name.from_wire(wire[: current + rdlen], current) - current += cused - rdlen -= cused - if rdlen != 20: - raise dns.exception.FormError - five_ints = struct.unpack('!IIIII', - wire[current: current + rdlen]) - if origin is not None: - mname = mname.relativize(origin) - rname = rname.relativize(origin) - return cls(rdclass, rdtype, mname, rname, - five_ints[0], five_ints[1], five_ints[2], five_ints[3], - five_ints[4]) - - def choose_relativity(self, origin=None, relativize=True): - self.mname = self.mname.choose_relativity(origin, relativize) - self.rname = self.rname.choose_relativity(origin, relativize) + 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/libs/dns/rdtypes/ANY/SPF.py b/libs/dns/rdtypes/ANY/SPF.py index f3e0904e6..1190e0ded 100644 --- a/libs/dns/rdtypes/ANY/SPF.py +++ b/libs/dns/rdtypes/ANY/SPF.py @@ -1,3 +1,5 @@ +# 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 @@ -14,10 +16,12 @@ # OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. import dns.rdtypes.txtbase +import dns.immutable +@dns.immutable.immutable class SPF(dns.rdtypes.txtbase.TXTBase): - """SPF record + """SPF record""" - @see: RFC 4408""" + # see: RFC 4408 diff --git a/libs/dns/rdtypes/ANY/SSHFP.py b/libs/dns/rdtypes/ANY/SSHFP.py index 7e846b342..cc035195d 100644 --- a/libs/dns/rdtypes/ANY/SSHFP.py +++ b/libs/dns/rdtypes/ANY/SSHFP.py @@ -1,3 +1,5 @@ +# 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 @@ -17,61 +19,51 @@ import struct import binascii import dns.rdata +import dns.immutable import dns.rdatatype +@dns.immutable.immutable class SSHFP(dns.rdata.Rdata): - """SSHFP record + """SSHFP record""" - @ivar algorithm: the algorithm - @type algorithm: int - @ivar fp_type: the digest type - @type fp_type: int - @ivar fingerprint: the fingerprint - @type fingerprint: string - @see: draft-ietf-secsh-dns-05.txt""" + # See RFC 4255 __slots__ = ['algorithm', 'fp_type', 'fingerprint'] def __init__(self, rdclass, rdtype, algorithm, fp_type, fingerprint): - super(SSHFP, self).__init__(rdclass, rdtype) - self.algorithm = algorithm - self.fp_type = fp_type - self.fingerprint = 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=128)) + chunksize=chunksize, + **kw)) @classmethod - def from_text(cls, rdclass, rdtype, tok, origin=None, relativize=True): + def from_text(cls, rdclass, rdtype, tok, origin=None, relativize=True, + relativize_to=None): algorithm = tok.get_uint8() fp_type = tok.get_uint8() - chunks = [] - while 1: - t = tok.get().unescape() - if t.is_eol_or_eof(): - break - if not t.is_identifier(): - raise dns.exception.SyntaxError - chunks.append(t.value.encode()) - fingerprint = b''.join(chunks) + 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): + 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(cls, rdclass, rdtype, wire, current, rdlen, origin=None): - header = struct.unpack("!BB", wire[current: current + 2]) - current += 2 - rdlen -= 2 - fingerprint = wire[current: current + rdlen].unwrap() + 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/libs/dns/rdtypes/ANY/TKEY.py b/libs/dns/rdtypes/ANY/TKEY.py new file mode 100644 index 000000000..f8c473723 --- /dev/null +++ b/libs/dns/rdtypes/ANY/TKEY.py @@ -0,0 +1,118 @@ +# 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.dnssec +import dns.immutable +import dns.exception +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().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/libs/dns/rdtypes/ANY/TLSA.py b/libs/dns/rdtypes/ANY/TLSA.py index 790a93b9d..c9ba19911 100644 --- a/libs/dns/rdtypes/ANY/TLSA.py +++ b/libs/dns/rdtypes/ANY/TLSA.py @@ -1,82 +1,10 @@ -# 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. +# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license -import struct -import binascii - -import dns.rdata -import dns.rdatatype +import dns.immutable +import dns.rdtypes.tlsabase -class TLSA(dns.rdata.Rdata): +@dns.immutable.immutable +class TLSA(dns.rdtypes.tlsabase.TLSABase): - """TLSA record - - @ivar usage: The certificate usage - @type usage: int - @ivar selector: The selector field - @type selector: int - @ivar mtype: The 'matching type' field - @type mtype: int - @ivar cert: The 'Certificate Association Data' field - @type cert: string - @see: RFC 6698""" - - __slots__ = ['usage', 'selector', 'mtype', 'cert'] - - def __init__(self, rdclass, rdtype, usage, selector, - mtype, cert): - super(TLSA, self).__init__(rdclass, rdtype) - self.usage = usage - self.selector = selector - self.mtype = mtype - self.cert = cert - - def to_text(self, origin=None, relativize=True, **kw): - return '%d %d %d %s' % (self.usage, - self.selector, - self.mtype, - dns.rdata._hexify(self.cert, - chunksize=128)) - - @classmethod - def from_text(cls, rdclass, rdtype, tok, origin=None, relativize=True): - usage = tok.get_uint8() - selector = tok.get_uint8() - mtype = tok.get_uint8() - cert_chunks = [] - while 1: - t = tok.get().unescape() - if t.is_eol_or_eof(): - break - if not t.is_identifier(): - raise dns.exception.SyntaxError - cert_chunks.append(t.value.encode()) - cert = b''.join(cert_chunks) - cert = binascii.unhexlify(cert) - return cls(rdclass, rdtype, usage, selector, mtype, cert) - - def to_wire(self, file, compress=None, origin=None): - header = struct.pack("!BBB", self.usage, self.selector, self.mtype) - file.write(header) - file.write(self.cert) - - @classmethod - def from_wire(cls, rdclass, rdtype, wire, current, rdlen, origin=None): - header = struct.unpack("!BBB", wire[current: current + 3]) - current += 3 - rdlen -= 3 - cert = wire[current: current + rdlen].unwrap() - return cls(rdclass, rdtype, header[0], header[1], header[2], cert) + """TLSA record""" diff --git a/libs/dns/rdtypes/ANY/TSIG.py b/libs/dns/rdtypes/ANY/TSIG.py new file mode 100644 index 000000000..b43a78f1f --- /dev/null +++ b/libs/dns/rdtypes/ANY/TSIG.py @@ -0,0 +1,120 @@ +# 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/libs/dns/rdtypes/ANY/TXT.py b/libs/dns/rdtypes/ANY/TXT.py index 6c7fa4502..cc4b66110 100644 --- a/libs/dns/rdtypes/ANY/TXT.py +++ b/libs/dns/rdtypes/ANY/TXT.py @@ -1,3 +1,5 @@ +# 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 @@ -14,8 +16,10 @@ # OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. import dns.rdtypes.txtbase +import dns.immutable +@dns.immutable.immutable class TXT(dns.rdtypes.txtbase.TXTBase): """TXT record""" diff --git a/libs/dns/rdtypes/ANY/URI.py b/libs/dns/rdtypes/ANY/URI.py index b5595b510..524fa1ba6 100644 --- a/libs/dns/rdtypes/ANY/URI.py +++ b/libs/dns/rdtypes/ANY/URI.py @@ -1,3 +1,5 @@ +# 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. # @@ -17,64 +19,62 @@ import struct import dns.exception +import dns.immutable import dns.rdata +import dns.rdtypes.util import dns.name -from dns._compat import text_type +@dns.immutable.immutable class URI(dns.rdata.Rdata): - """URI record + """URI record""" - @ivar priority: the priority - @type priority: int - @ivar weight: the weight - @type weight: int - @ivar target: the target host - @type target: dns.name.Name object - @see: draft-faltstrom-uri-13""" + # see RFC 7553 __slots__ = ['priority', 'weight', 'target'] def __init__(self, rdclass, rdtype, priority, weight, target): - super(URI, self).__init__(rdclass, rdtype) - self.priority = priority - self.weight = weight - if len(target) < 1: + 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") - if isinstance(target, text_type): - self.target = target.encode() - else: - self.target = target 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): + 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") - tok.get_eol() return cls(rdclass, rdtype, priority, weight, target.value) - def to_wire(self, file, compress=None, origin=None): + 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(cls, rdclass, rdtype, wire, current, rdlen, origin=None): - if rdlen < 5: - raise dns.exception.FormError('URI RR is shorter than 5 octets') - - (priority, weight) = struct.unpack('!HH', wire[current: current + 4]) - current += 4 - rdlen -= 4 - target = wire[current: current + rdlen] - current += rdlen - + 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/libs/dns/rdtypes/ANY/X25.py b/libs/dns/rdtypes/ANY/X25.py index 8732ccf0d..4f7230c04 100644 --- a/libs/dns/rdtypes/ANY/X25.py +++ b/libs/dns/rdtypes/ANY/X25.py @@ -1,3 +1,5 @@ +# 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 @@ -16,49 +18,40 @@ import struct import dns.exception +import dns.immutable import dns.rdata import dns.tokenizer -from dns._compat import text_type +@dns.immutable.immutable class X25(dns.rdata.Rdata): - """X25 record + """X25 record""" - @ivar address: the PSDN address - @type address: string - @see: RFC 1183""" + # see RFC 1183 __slots__ = ['address'] def __init__(self, rdclass, rdtype, address): - super(X25, self).__init__(rdclass, rdtype) - if isinstance(address, text_type): - self.address = address.encode() - else: - self.address = 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): + def from_text(cls, rdclass, rdtype, tok, origin=None, relativize=True, + relativize_to=None): address = tok.get_string() - tok.get_eol() return cls(rdclass, rdtype, address) - def to_wire(self, file, compress=None, origin=None): + 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(cls, rdclass, rdtype, wire, current, rdlen, origin=None): - l = wire[current] - current += 1 - rdlen -= 1 - if l != rdlen: - raise dns.exception.FormError - address = wire[current: current + l].unwrap() + def from_wire_parser(cls, rdclass, rdtype, parser, origin=None): + address = parser.get_counted_bytes() return cls(rdclass, rdtype, address) diff --git a/libs/dns/rdtypes/ANY/ZONEMD.py b/libs/dns/rdtypes/ANY/ZONEMD.py new file mode 100644 index 000000000..035f7b327 --- /dev/null +++ b/libs/dns/rdtypes/ANY/ZONEMD.py @@ -0,0 +1,65 @@ +# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license + +import struct +import binascii + +import dns.immutable +import dns.rdata +import dns.rdatatype +import dns.zone + + +@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.zone.DigestScheme.make(scheme) + self.hash_algorithm = dns.zone.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.zone._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/libs/dns/rdtypes/ANY/__init__.py b/libs/dns/rdtypes/ANY/__init__.py index ea9c3e2e0..6c56baffd 100644 --- a/libs/dns/rdtypes/ANY/__init__.py +++ b/libs/dns/rdtypes/ANY/__init__.py @@ -1,3 +1,5 @@ +# 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 @@ -17,10 +19,14 @@ __all__ = [ 'AFSDB', + 'AMTRELAY', + 'AVC', + 'CAA', 'CDNSKEY', 'CDS', 'CERT', 'CNAME', + 'CSYNC', 'DLV', 'DNAME', 'DNSKEY', @@ -33,18 +39,26 @@ __all__ = [ 'ISDN', 'LOC', 'MX', + 'NINFO', 'NS', 'NSEC', 'NSEC3', 'NSEC3PARAM', - 'TLSA', + 'OPENPGPKEY', + 'OPT', 'PTR', 'RP', 'RRSIG', 'RT', + 'SMIMEA', 'SOA', 'SPF', 'SSHFP', + 'TKEY', + 'TLSA', + 'TSIG', 'TXT', + 'URI', 'X25', + 'ZONEMD', ] diff --git a/libs/dns/rdtypes/CH/A.py b/libs/dns/rdtypes/CH/A.py new file mode 100644 index 000000000..828701b41 --- /dev/null +++ b/libs/dns/rdtypes/CH/A.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 struct + +import dns.rdtypes.mxbase +import dns.immutable + +@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/libs/dns/rdtypes/CH/__init__.py b/libs/dns/rdtypes/CH/__init__.py new file mode 100644 index 000000000..7184a7332 --- /dev/null +++ b/libs/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/libs/dns/rdtypes/IN/A.py b/libs/dns/rdtypes/IN/A.py index 3775548f2..74b591efc 100644 --- a/libs/dns/rdtypes/IN/A.py +++ b/libs/dns/rdtypes/IN/A.py @@ -1,3 +1,5 @@ +# 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 @@ -14,39 +16,36 @@ # OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. import dns.exception +import dns.immutable import dns.ipv4 import dns.rdata import dns.tokenizer +@dns.immutable.immutable class A(dns.rdata.Rdata): - """A record. - - @ivar address: an IPv4 address - @type address: string (in the standard "dotted quad" format)""" + """A record.""" __slots__ = ['address'] def __init__(self, rdclass, rdtype, address): - super(A, self).__init__(rdclass, rdtype) - # check that it's OK - dns.ipv4.inet_aton(address) - self.address = address + super().__init__(rdclass, rdtype) + self.address = self._as_ipv4_address(address) def to_text(self, origin=None, relativize=True, **kw): return self.address @classmethod - def from_text(cls, rdclass, rdtype, tok, origin=None, relativize=True): + def from_text(cls, rdclass, rdtype, tok, origin=None, relativize=True, + relativize_to=None): address = tok.get_identifier() - tok.get_eol() return cls(rdclass, rdtype, address) - def to_wire(self, file, compress=None, origin=None): + def _to_wire(self, file, compress=None, origin=None, canonicalize=False): file.write(dns.ipv4.inet_aton(self.address)) @classmethod - def from_wire(cls, rdclass, rdtype, wire, current, rdlen, origin=None): - address = dns.ipv4.inet_ntoa(wire[current: current + rdlen]).decode() + def from_wire_parser(cls, rdclass, rdtype, parser, origin=None): + address = parser.get_remaining() return cls(rdclass, rdtype, address) diff --git a/libs/dns/rdtypes/IN/AAAA.py b/libs/dns/rdtypes/IN/AAAA.py index 4352404d7..2d3ec902b 100644 --- a/libs/dns/rdtypes/IN/AAAA.py +++ b/libs/dns/rdtypes/IN/AAAA.py @@ -1,3 +1,5 @@ +# 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 @@ -14,40 +16,36 @@ # OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. import dns.exception -import dns.inet +import dns.immutable +import dns.ipv6 import dns.rdata import dns.tokenizer +@dns.immutable.immutable class AAAA(dns.rdata.Rdata): - """AAAA record. - - @ivar address: an IPv6 address - @type address: string (in the standard IPv6 format)""" + """AAAA record.""" __slots__ = ['address'] def __init__(self, rdclass, rdtype, address): - super(AAAA, self).__init__(rdclass, rdtype) - # check that it's OK - dns.inet.inet_pton(dns.inet.AF_INET6, address) - self.address = address + super().__init__(rdclass, rdtype) + self.address = self._as_ipv6_address(address) def to_text(self, origin=None, relativize=True, **kw): return self.address @classmethod - def from_text(cls, rdclass, rdtype, tok, origin=None, relativize=True): + def from_text(cls, rdclass, rdtype, tok, origin=None, relativize=True, + relativize_to=None): address = tok.get_identifier() - tok.get_eol() return cls(rdclass, rdtype, address) - def to_wire(self, file, compress=None, origin=None): - file.write(dns.inet.inet_pton(dns.inet.AF_INET6, self.address)) + def _to_wire(self, file, compress=None, origin=None, canonicalize=False): + file.write(dns.ipv6.inet_aton(self.address)) @classmethod - def from_wire(cls, rdclass, rdtype, wire, current, rdlen, origin=None): - address = dns.inet.inet_ntop(dns.inet.AF_INET6, - wire[current: current + rdlen]) + def from_wire_parser(cls, rdclass, rdtype, parser, origin=None): + address = parser.get_remaining() return cls(rdclass, rdtype, address) diff --git a/libs/dns/rdtypes/IN/APL.py b/libs/dns/rdtypes/IN/APL.py index 57ef6c0a9..ae94fb24c 100644 --- a/libs/dns/rdtypes/IN/APL.py +++ b/libs/dns/rdtypes/IN/APL.py @@ -1,4 +1,6 @@ -# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc. +# 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, @@ -13,37 +15,36 @@ # 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 binascii +import codecs +import struct import dns.exception -import dns.inet +import dns.immutable +import dns.ipv4 +import dns.ipv6 import dns.rdata import dns.tokenizer -from dns._compat import xrange +@dns.immutable.immutable +class APLItem: -class APLItem(object): - - """An APL list item. - - @ivar family: the address family (IANA address family registry) - @type family: int - @ivar negation: is this item negated? - @type negation: bool - @ivar address: the address - @type address: string - @ivar prefix: the prefix length - @type prefix: int - """ + """An APL list item.""" __slots__ = ['family', 'negation', 'address', 'prefix'] def __init__(self, family, negation, address, prefix): - self.family = family - self.negation = negation - self.address = address - self.prefix = prefix + self.family = dns.rdata.Rdata._as_uint16(family) + self.negation = dns.rdata.Rdata._as_bool(negation) + if self.family == 1: + self.address = dns.rdata.Rdata._as_ipv4_address(address) + self.prefix = dns.rdata.Rdata._as_int(prefix, 0, 32) + elif self.family == 2: + self.address = dns.rdata.Rdata._as_ipv6_address(address) + self.prefix = dns.rdata.Rdata._as_int(prefix, 0, 128) + else: + self.address = dns.rdata.Rdata._as_bytes(address, max_length=127) + self.prefix = dns.rdata.Rdata._as_uint8(prefix) def __str__(self): if self.negation: @@ -53,17 +54,17 @@ class APLItem(object): def to_wire(self, file): if self.family == 1: - address = dns.inet.inet_pton(dns.inet.AF_INET, self.address) + address = dns.ipv4.inet_aton(self.address) elif self.family == 2: - address = dns.inet.inet_pton(dns.inet.AF_INET6, self.address) + address = dns.ipv6.inet_aton(self.address) else: address = binascii.unhexlify(self.address) # # Truncate least significant zero bytes. # last = 0 - for i in xrange(len(address) - 1, -1, -1): - if address[i] != chr(0): + for i in range(len(address) - 1, -1, -1): + if address[i] != 0: last = i + 1 break address = address[0: last] @@ -76,31 +77,31 @@ class APLItem(object): file.write(address) +@dns.immutable.immutable class APL(dns.rdata.Rdata): - """APL record. + """APL record.""" - @ivar items: a list of APL items - @type items: list of APL_Item - @see: RFC 3123""" + # see: RFC 3123 __slots__ = ['items'] def __init__(self, rdclass, rdtype, items): - super(APL, self).__init__(rdclass, rdtype) - self.items = items + super().__init__(rdclass, rdtype) + for item in items: + if not isinstance(item, APLItem): + raise ValueError('item not an APLItem') + self.items = tuple(items) def to_text(self, origin=None, relativize=True, **kw): return ' '.join(map(str, self.items)) @classmethod - def from_text(cls, rdclass, rdtype, tok, origin=None, relativize=True): + def from_text(cls, rdclass, rdtype, tok, origin=None, relativize=True, + relativize_to=None): items = [] - while 1: - token = tok.get().unescape() - if token.is_eol_or_eof(): - break - item = token.value + for token in tok.get_remaining(): + item = token.unescape().value if item[0] == '!': negation = True item = item[1:] @@ -115,47 +116,36 @@ class APL(dns.rdata.Rdata): return cls(rdclass, rdtype, items) - def to_wire(self, file, compress=None, origin=None): + def _to_wire(self, file, compress=None, origin=None, canonicalize=False): for item in self.items: item.to_wire(file) @classmethod - def from_wire(cls, rdclass, rdtype, wire, current, rdlen, origin=None): + def from_wire_parser(cls, rdclass, rdtype, parser, origin=None): + items = [] - while 1: - if rdlen == 0: - break - if rdlen < 4: - raise dns.exception.FormError - header = struct.unpack('!HBB', wire[current: current + 4]) + while parser.remaining() > 0: + header = parser.get_struct('!HBB') afdlen = header[2] if afdlen > 127: negation = True afdlen -= 128 else: negation = False - current += 4 - rdlen -= 4 - if rdlen < afdlen: - raise dns.exception.FormError - address = wire[current: current + afdlen].unwrap() + address = parser.get_bytes(afdlen) l = len(address) if header[0] == 1: if l < 4: - address += '\x00' * (4 - l) - address = dns.inet.inet_ntop(dns.inet.AF_INET, address) + address += b'\x00' * (4 - l) elif header[0] == 2: if l < 16: - address += '\x00' * (16 - l) - address = dns.inet.inet_ntop(dns.inet.AF_INET6, address) + address += b'\x00' * (16 - l) else: # # This isn't really right according to the RFC, but it # seems better than throwing an exception # - address = address.encode('hex_codec') - current += afdlen - rdlen -= afdlen + 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/libs/dns/rdtypes/IN/DHCID.py b/libs/dns/rdtypes/IN/DHCID.py index 5b8626a5a..a9185989c 100644 --- a/libs/dns/rdtypes/IN/DHCID.py +++ b/libs/dns/rdtypes/IN/DHCID.py @@ -1,3 +1,5 @@ +# 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 @@ -16,44 +18,36 @@ import base64 import dns.exception +import dns.immutable +@dns.immutable.immutable class DHCID(dns.rdata.Rdata): - """DHCID record + """DHCID record""" - @ivar data: the data (the content of the RR is opaque as far as the - DNS is concerned) - @type data: string - @see: RFC 4701""" + # see: RFC 4701 __slots__ = ['data'] def __init__(self, rdclass, rdtype, data): - super(DHCID, self).__init__(rdclass, rdtype) - self.data = 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) + return dns.rdata._base64ify(self.data, **kw) @classmethod - def from_text(cls, rdclass, rdtype, tok, origin=None, relativize=True): - chunks = [] - while 1: - t = tok.get().unescape() - if t.is_eol_or_eof(): - break - if not t.is_identifier(): - raise dns.exception.SyntaxError - chunks.append(t.value.encode()) - b64 = b''.join(chunks) + 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): + def _to_wire(self, file, compress=None, origin=None, canonicalize=False): file.write(self.data) @classmethod - def from_wire(cls, rdclass, rdtype, wire, current, rdlen, origin=None): - data = wire[current: current + rdlen].unwrap() + def from_wire_parser(cls, rdclass, rdtype, parser, origin=None): + data = parser.get_remaining() return cls(rdclass, rdtype, data) diff --git a/libs/dns/rdtypes/IN/HTTPS.py b/libs/dns/rdtypes/IN/HTTPS.py new file mode 100644 index 000000000..6a67e8ed2 --- /dev/null +++ b/libs/dns/rdtypes/IN/HTTPS.py @@ -0,0 +1,8 @@ +# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license + +import dns.rdtypes.svcbbase +import dns.immutable + +@dns.immutable.immutable +class HTTPS(dns.rdtypes.svcbbase.SVCBBase): + """HTTPS record""" diff --git a/libs/dns/rdtypes/IN/IPSECKEY.py b/libs/dns/rdtypes/IN/IPSECKEY.py index c673e839d..d1d394383 100644 --- a/libs/dns/rdtypes/IN/IPSECKEY.py +++ b/libs/dns/rdtypes/IN/IPSECKEY.py @@ -1,3 +1,5 @@ +# 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 @@ -17,132 +19,65 @@ import struct import base64 import dns.exception -import dns.inet -import dns.name +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 + """IPSECKEY record""" - @ivar precedence: the precedence for this key data - @type precedence: int - @ivar gateway_type: the gateway type - @type gateway_type: int - @ivar algorithm: the algorithm to use - @type algorithm: int - @ivar gateway: the public key - @type gateway: None, IPv4 address, IPV6 address, or domain name - @ivar key: the public key - @type key: string - @see: RFC 4025""" + # see: RFC 4025 __slots__ = ['precedence', 'gateway_type', 'algorithm', 'gateway', 'key'] def __init__(self, rdclass, rdtype, precedence, gateway_type, algorithm, gateway, key): - super(IPSECKEY, self).__init__(rdclass, rdtype) - if gateway_type == 0: - if gateway != '.' and gateway is not None: - raise SyntaxError('invalid gateway for gateway type 0') - gateway = None - elif gateway_type == 1: - # check that it's OK - dns.inet.inet_pton(dns.inet.AF_INET, gateway) - elif gateway_type == 2: - # check that it's OK - dns.inet.inet_pton(dns.inet.AF_INET6, gateway) - elif gateway_type == 3: - pass - else: - raise SyntaxError( - 'invalid IPSECKEY gateway type: %d' % gateway_type) - self.precedence = precedence - self.gateway_type = gateway_type - self.algorithm = algorithm - self.gateway = gateway - self.key = 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): - if self.gateway_type == 0: - gateway = '.' - elif self.gateway_type == 1: - gateway = self.gateway - elif self.gateway_type == 2: - gateway = self.gateway - elif self.gateway_type == 3: - gateway = str(self.gateway.choose_relativity(origin, relativize)) - else: - raise ValueError('invalid gateway type') + 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)) + dns.rdata._base64ify(self.key, **kw)) @classmethod - def from_text(cls, rdclass, rdtype, tok, origin=None, relativize=True): + 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() - if gateway_type == 3: - gateway = tok.get_name().choose_relativity(origin, relativize) - else: - gateway = tok.get_string() - chunks = [] - while 1: - t = tok.get().unescape() - if t.is_eol_or_eof(): - break - if not t.is_identifier(): - raise dns.exception.SyntaxError - chunks.append(t.value.encode()) - b64 = b''.join(chunks) + 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, key) + gateway.gateway, key) - def to_wire(self, file, compress=None, origin=None): + 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) - if self.gateway_type == 0: - pass - elif self.gateway_type == 1: - file.write(dns.inet.inet_pton(dns.inet.AF_INET, self.gateway)) - elif self.gateway_type == 2: - file.write(dns.inet.inet_pton(dns.inet.AF_INET6, self.gateway)) - elif self.gateway_type == 3: - self.gateway.to_wire(file, None, origin) - else: - raise ValueError('invalid gateway type') + Gateway(self.gateway_type, self.gateway).to_wire(file, compress, + origin, canonicalize) file.write(self.key) @classmethod - def from_wire(cls, rdclass, rdtype, wire, current, rdlen, origin=None): - if rdlen < 3: - raise dns.exception.FormError - header = struct.unpack('!BBB', wire[current: current + 3]) + def from_wire_parser(cls, rdclass, rdtype, parser, origin=None): + header = parser.get_struct('!BBB') gateway_type = header[1] - current += 3 - rdlen -= 3 - if gateway_type == 0: - gateway = None - elif gateway_type == 1: - gateway = dns.inet.inet_ntop(dns.inet.AF_INET, - wire[current: current + 4]) - current += 4 - rdlen -= 4 - elif gateway_type == 2: - gateway = dns.inet.inet_ntop(dns.inet.AF_INET6, - wire[current: current + 16]) - current += 16 - rdlen -= 16 - elif gateway_type == 3: - (gateway, cused) = dns.name.from_wire(wire[: current + rdlen], - current) - current += cused - rdlen -= cused - else: - raise dns.exception.FormError('invalid IPSECKEY gateway type') - key = wire[current: current + rdlen].unwrap() + gateway = Gateway.from_wire_parser(gateway_type, parser, origin) + key = parser.get_remaining() return cls(rdclass, rdtype, header[0], gateway_type, header[2], - gateway, key) + gateway.gateway, key) diff --git a/libs/dns/rdtypes/IN/KX.py b/libs/dns/rdtypes/IN/KX.py index adbfe34b1..c27e9215a 100644 --- a/libs/dns/rdtypes/IN/KX.py +++ b/libs/dns/rdtypes/IN/KX.py @@ -1,3 +1,5 @@ +# 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 @@ -14,8 +16,10 @@ # OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. import dns.rdtypes.mxbase +import dns.immutable -class KX(dns.rdtypes.mxbase.UncompressedMX): +@dns.immutable.immutable +class KX(dns.rdtypes.mxbase.UncompressedDowncasingMX): """KX record""" diff --git a/libs/dns/rdtypes/IN/NAPTR.py b/libs/dns/rdtypes/IN/NAPTR.py index 5ae2feb15..b107974d2 100644 --- a/libs/dns/rdtypes/IN/NAPTR.py +++ b/libs/dns/rdtypes/IN/NAPTR.py @@ -1,3 +1,5 @@ +# 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 @@ -16,9 +18,10 @@ import struct import dns.exception +import dns.immutable import dns.name import dns.rdata -from dns._compat import xrange, text_type +import dns.rdtypes.util def _write_string(file, s): @@ -28,42 +31,25 @@ def _write_string(file, s): file.write(s) -def _sanitize(value): - if isinstance(value, text_type): - return value.encode() - return value - - +@dns.immutable.immutable class NAPTR(dns.rdata.Rdata): - """NAPTR record + """NAPTR record""" - @ivar order: order - @type order: int - @ivar preference: preference - @type preference: int - @ivar flags: flags - @type flags: string - @ivar service: service - @type service: string - @ivar regexp: regular expression - @type regexp: string - @ivar replacement: replacement name - @type replacement: dns.name.Name object - @see: RFC 3403""" + # see: RFC 3403 __slots__ = ['order', 'preference', 'flags', 'service', 'regexp', 'replacement'] def __init__(self, rdclass, rdtype, order, preference, flags, service, regexp, replacement): - super(NAPTR, self).__init__(rdclass, rdtype) - self.flags = _sanitize(flags) - self.service = _sanitize(service) - self.regexp = _sanitize(regexp) - self.order = order - self.preference = preference - self.replacement = 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) @@ -75,51 +61,39 @@ class NAPTR(dns.rdata.Rdata): replacement) @classmethod - def from_text(cls, rdclass, rdtype, tok, origin=None, relativize=True): + 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() - replacement = replacement.choose_relativity(origin, relativize) - tok.get_eol() + 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): + 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) + self.replacement.to_wire(file, compress, origin, canonicalize) @classmethod - def from_wire(cls, rdclass, rdtype, wire, current, rdlen, origin=None): - (order, preference) = struct.unpack('!HH', wire[current: current + 4]) - current += 4 - rdlen -= 4 + def from_wire_parser(cls, rdclass, rdtype, parser, origin=None): + (order, preference) = parser.get_struct('!HH') strings = [] - for i in xrange(3): - l = wire[current] - current += 1 - rdlen -= 1 - if l > rdlen or rdlen < 0: - raise dns.exception.FormError - s = wire[current: current + l].unwrap() - current += l - rdlen -= l + for _ in range(3): + s = parser.get_counted_bytes() strings.append(s) - (replacement, cused) = dns.name.from_wire(wire[: current + rdlen], - current) - if cused != rdlen: - raise dns.exception.FormError - if origin is not None: - replacement = replacement.relativize(origin) + replacement = parser.get_name(origin) return cls(rdclass, rdtype, order, preference, strings[0], strings[1], strings[2], replacement) - def choose_relativity(self, origin=None, relativize=True): - self.replacement = self.replacement.choose_relativity(origin, - relativize) + 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/libs/dns/rdtypes/IN/NSAP.py b/libs/dns/rdtypes/IN/NSAP.py index 05d0745ef..23ae9b1a8 100644 --- a/libs/dns/rdtypes/IN/NSAP.py +++ b/libs/dns/rdtypes/IN/NSAP.py @@ -1,3 +1,5 @@ +# 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 @@ -16,31 +18,31 @@ import binascii import dns.exception +import dns.immutable import dns.rdata import dns.tokenizer +@dns.immutable.immutable class NSAP(dns.rdata.Rdata): - """NSAP record. + """NSAP record.""" - @ivar address: a NASP - @type address: string - @see: RFC 1706""" + # see: RFC 1706 __slots__ = ['address'] def __init__(self, rdclass, rdtype, address): - super(NSAP, self).__init__(rdclass, rdtype) - self.address = 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): + def from_text(cls, rdclass, rdtype, tok, origin=None, relativize=True, + relativize_to=None): address = tok.get_string() - tok.get_eol() if address[0:2] != '0x': raise dns.exception.SyntaxError('string does not start with 0x') address = address[2:].replace('.', '') @@ -49,10 +51,10 @@ class NSAP(dns.rdata.Rdata): address = binascii.unhexlify(address.encode()) return cls(rdclass, rdtype, address) - def to_wire(self, file, compress=None, origin=None): + def _to_wire(self, file, compress=None, origin=None, canonicalize=False): file.write(self.address) @classmethod - def from_wire(cls, rdclass, rdtype, wire, current, rdlen, origin=None): - address = wire[current: current + rdlen].unwrap() + def from_wire_parser(cls, rdclass, rdtype, parser, origin=None): + address = parser.get_remaining() return cls(rdclass, rdtype, address) diff --git a/libs/dns/rdtypes/IN/NSAP_PTR.py b/libs/dns/rdtypes/IN/NSAP_PTR.py index 56967df02..57dadd474 100644 --- a/libs/dns/rdtypes/IN/NSAP_PTR.py +++ b/libs/dns/rdtypes/IN/NSAP_PTR.py @@ -1,3 +1,5 @@ +# 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 @@ -14,8 +16,10 @@ # OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. import dns.rdtypes.nsbase +import dns.immutable +@dns.immutable.immutable class NSAP_PTR(dns.rdtypes.nsbase.UncompressedNS): """NSAP-PTR record""" diff --git a/libs/dns/rdtypes/IN/PX.py b/libs/dns/rdtypes/IN/PX.py index e1ef102b1..113d409cd 100644 --- a/libs/dns/rdtypes/IN/PX.py +++ b/libs/dns/rdtypes/IN/PX.py @@ -1,3 +1,5 @@ +# 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 @@ -16,29 +18,26 @@ import struct import dns.exception +import dns.immutable import dns.rdata +import dns.rdtypes.util import dns.name +@dns.immutable.immutable class PX(dns.rdata.Rdata): - """PX record. + """PX record.""" - @ivar preference: the preference value - @type preference: int - @ivar map822: the map822 name - @type map822: dns.name.Name object - @ivar mapx400: the mapx400 name - @type mapx400: dns.name.Name object - @see: RFC 2163""" + # see: RFC 2163 __slots__ = ['preference', 'map822', 'mapx400'] def __init__(self, rdclass, rdtype, preference, map822, mapx400): - super(PX, self).__init__(rdclass, rdtype) - self.preference = preference - self.map822 = map822 - self.mapx400 = 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) @@ -46,42 +45,29 @@ class PX(dns.rdata.Rdata): return '%d %s %s' % (self.preference, map822, mapx400) @classmethod - def from_text(cls, rdclass, rdtype, tok, origin=None, relativize=True): + def from_text(cls, rdclass, rdtype, tok, origin=None, relativize=True, + relativize_to=None): preference = tok.get_uint16() - map822 = tok.get_name() - map822 = map822.choose_relativity(origin, relativize) - mapx400 = tok.get_name(None) - mapx400 = mapx400.choose_relativity(origin, relativize) - tok.get_eol() + 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): + 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) - self.mapx400.to_wire(file, None, origin) + self.map822.to_wire(file, None, origin, canonicalize) + self.mapx400.to_wire(file, None, origin, canonicalize) @classmethod - def from_wire(cls, rdclass, rdtype, wire, current, rdlen, origin=None): - (preference, ) = struct.unpack('!H', wire[current: current + 2]) - current += 2 - rdlen -= 2 - (map822, cused) = dns.name.from_wire(wire[: current + rdlen], - current) - if cused > rdlen: - raise dns.exception.FormError - current += cused - rdlen -= cused - if origin is not None: - map822 = map822.relativize(origin) - (mapx400, cused) = dns.name.from_wire(wire[: current + rdlen], - current) - if cused != rdlen: - raise dns.exception.FormError - if origin is not None: - mapx400 = mapx400.relativize(origin) + 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 choose_relativity(self, origin=None, relativize=True): - self.map822 = self.map822.choose_relativity(origin, relativize) - self.mapx400 = self.mapx400.choose_relativity(origin, relativize) + def _processing_priority(self): + return self.preference + + @classmethod + def _processing_order(cls, iterable): + return dns.rdtypes.util.priority_processing_order(iterable) diff --git a/libs/dns/rdtypes/IN/SRV.py b/libs/dns/rdtypes/IN/SRV.py index f4396d614..5b5ff4229 100644 --- a/libs/dns/rdtypes/IN/SRV.py +++ b/libs/dns/rdtypes/IN/SRV.py @@ -1,3 +1,5 @@ +# 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 @@ -16,32 +18,27 @@ import struct import dns.exception +import dns.immutable import dns.rdata +import dns.rdtypes.util import dns.name +@dns.immutable.immutable class SRV(dns.rdata.Rdata): - """SRV record + """SRV record""" - @ivar priority: the priority - @type priority: int - @ivar weight: the weight - @type weight: int - @ivar port: the port of the service - @type port: int - @ivar target: the target host - @type target: dns.name.Name object - @see: RFC 2782""" + # see: RFC 2782 __slots__ = ['priority', 'weight', 'port', 'target'] def __init__(self, rdclass, rdtype, priority, weight, port, target): - super(SRV, self).__init__(rdclass, rdtype) - self.priority = priority - self.weight = weight - self.port = port - self.target = 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) @@ -49,33 +46,31 @@ class SRV(dns.rdata.Rdata): target) @classmethod - def from_text(cls, rdclass, rdtype, tok, origin=None, relativize=True): + 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(None) - target = target.choose_relativity(origin, relativize) - tok.get_eol() + 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): + 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) + self.target.to_wire(file, compress, origin, canonicalize) @classmethod - def from_wire(cls, rdclass, rdtype, wire, current, rdlen, origin=None): - (priority, weight, port) = struct.unpack('!HHH', - wire[current: current + 6]) - current += 6 - rdlen -= 6 - (target, cused) = dns.name.from_wire(wire[: current + rdlen], - current) - if cused != rdlen: - raise dns.exception.FormError - if origin is not None: - target = target.relativize(origin) + 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 choose_relativity(self, origin=None, relativize=True): - self.target = self.target.choose_relativity(origin, relativize) + 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/libs/dns/rdtypes/IN/SVCB.py b/libs/dns/rdtypes/IN/SVCB.py new file mode 100644 index 000000000..14838e162 --- /dev/null +++ b/libs/dns/rdtypes/IN/SVCB.py @@ -0,0 +1,8 @@ +# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license + +import dns.rdtypes.svcbbase +import dns.immutable + +@dns.immutable.immutable +class SVCB(dns.rdtypes.svcbbase.SVCBBase): + """SVCB record""" diff --git a/libs/dns/rdtypes/IN/WKS.py b/libs/dns/rdtypes/IN/WKS.py index 1d4012c30..264e45d36 100644 --- a/libs/dns/rdtypes/IN/WKS.py +++ b/libs/dns/rdtypes/IN/WKS.py @@ -1,3 +1,5 @@ +# 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 @@ -17,48 +19,44 @@ import socket import struct import dns.ipv4 +import dns.immutable import dns.rdata -from dns._compat import xrange - -_proto_tcp = socket.getprotobyname('tcp') -_proto_udp = socket.getprotobyname('udp') +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 + """WKS record""" - @ivar address: the address - @type address: string - @ivar protocol: the protocol - @type protocol: int - @ivar bitmap: the bitmap - @type bitmap: string - @see: RFC 1035""" + # see: RFC 1035 __slots__ = ['address', 'protocol', 'bitmap'] def __init__(self, rdclass, rdtype, address, protocol, bitmap): - super(WKS, self).__init__(rdclass, rdtype) - self.address = address - self.protocol = protocol - if not isinstance(bitmap, bytearray): - self.bitmap = bytearray(bitmap) - else: - self.bitmap = 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 in xrange(0, len(self.bitmap)): - byte = self.bitmap[i] - for j in xrange(0, 8): + 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): + 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(): @@ -66,12 +64,10 @@ class WKS(dns.rdata.Rdata): else: protocol = socket.getprotobyname(protocol) bitmap = bytearray() - while 1: - token = tok.get().unescape() - if token.is_eol_or_eof(): - break - if token.value.isdigit(): - serv = int(token.value) + 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") @@ -79,27 +75,25 @@ class WKS(dns.rdata.Rdata): protocol_text = "udp" else: protocol_text = "tcp" - serv = socket.getservbyname(token.value, protocol_text) + serv = socket.getservbyname(value, protocol_text) i = serv // 8 l = len(bitmap) if l < i + 1: - for j in xrange(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): + 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(cls, rdclass, rdtype, wire, current, rdlen, origin=None): - address = dns.ipv4.inet_ntoa(wire[current: current + 4]) - protocol, = struct.unpack('!B', wire[current + 4: current + 5]) - current += 5 - rdlen -= 5 - bitmap = wire[current: current + rdlen].unwrap() + 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/libs/dns/rdtypes/IN/__init__.py b/libs/dns/rdtypes/IN/__init__.py index 24cf1ece4..d51b99e72 100644 --- a/libs/dns/rdtypes/IN/__init__.py +++ b/libs/dns/rdtypes/IN/__init__.py @@ -1,3 +1,5 @@ +# 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 @@ -20,11 +22,14 @@ __all__ = [ 'AAAA', 'APL', 'DHCID', + 'HTTPS', + 'IPSECKEY', 'KX', 'NAPTR', 'NSAP', 'NSAP_PTR', 'PX', 'SRV', + 'SVCB', 'WKS', ] diff --git a/libs/dns/rdtypes/__init__.py b/libs/dns/rdtypes/__init__.py index 826efbb6c..c3af264e4 100644 --- a/libs/dns/rdtypes/__init__.py +++ b/libs/dns/rdtypes/__init__.py @@ -1,3 +1,5 @@ +# 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 @@ -18,7 +20,14 @@ __all__ = [ 'ANY', 'IN', + 'CH', + 'dnskeybase', + 'dsbase', 'euibase', 'mxbase', 'nsbase', + 'svcbbase', + 'tlsabase', + 'txtbase', + 'util' ] diff --git a/libs/dns/rdtypes/dnskeybase.py b/libs/dns/rdtypes/dnskeybase.py index 85c4b23f0..788bb2bf9 100644 --- a/libs/dns/rdtypes/dnskeybase.py +++ b/libs/dns/rdtypes/dnskeybase.py @@ -1,3 +1,5 @@ +# 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 @@ -14,123 +16,67 @@ # OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. import base64 +import enum import struct import dns.exception +import dns.immutable import dns.dnssec import dns.rdata # wildcard import -__all__ = ["SEP", "REVOKE", "ZONE", - "flags_to_text_set", "flags_from_text_set"] +__all__ = ["SEP", "REVOKE", "ZONE"] # noqa: F822 -# flag constants -SEP = 0x0001 -REVOKE = 0x0080 -ZONE = 0x0100 - -_flag_by_text = { - 'SEP': SEP, - 'REVOKE': REVOKE, - 'ZONE': ZONE -} - -# We construct the inverse mapping programmatically to ensure that we -# cannot make any mistakes (e.g. omissions, cut-and-paste errors) that -# would cause the mapping not to be true inverse. -_flag_by_value = dict((y, x) for x, y in _flag_by_text.items()) - - -def flags_to_text_set(flags): - """Convert a DNSKEY flags value to set texts - @rtype: set([string])""" - - flags_set = set() - mask = 0x1 - while mask <= 0x8000: - if flags & mask: - text = _flag_by_value.get(mask) - if not text: - text = hex(mask) - flags_set.add(text) - mask <<= 1 - return flags_set - - -def flags_from_text_set(texts_set): - """Convert set of DNSKEY flag mnemonic texts to DNSKEY flag value - @rtype: int""" - - flags = 0 - for text in texts_set: - try: - flags += _flag_by_text[text] - except KeyError: - raise NotImplementedError( - "DNSKEY flag '%s' is not supported" % text) - return flags +class Flag(enum.IntFlag): + SEP = 0x0001 + REVOKE = 0x0080 + ZONE = 0x0100 +@dns.immutable.immutable class DNSKEYBase(dns.rdata.Rdata): - """Base class for rdata that is like a DNSKEY record - - @ivar flags: the key flags - @type flags: int - @ivar protocol: the protocol for which this key may be used - @type protocol: int - @ivar algorithm: the algorithm used for the key - @type algorithm: int - @ivar key: the public key - @type key: string""" + """Base class for rdata that is like a DNSKEY record""" __slots__ = ['flags', 'protocol', 'algorithm', 'key'] def __init__(self, rdclass, rdtype, flags, protocol, algorithm, key): - super(DNSKEYBase, self).__init__(rdclass, rdtype) - self.flags = flags - self.protocol = protocol - self.algorithm = algorithm - self.key = key + super().__init__(rdclass, rdtype) + self.flags = self._as_uint16(flags) + self.protocol = self._as_uint8(protocol) + self.algorithm = dns.dnssec.Algorithm.make(algorithm) + self.key = self._as_bytes(key) def to_text(self, origin=None, relativize=True, **kw): return '%d %d %d %s' % (self.flags, self.protocol, self.algorithm, - dns.rdata._base64ify(self.key)) + dns.rdata._base64ify(self.key, **kw)) @classmethod - def from_text(cls, rdclass, rdtype, tok, origin=None, relativize=True): + def from_text(cls, rdclass, rdtype, tok, origin=None, relativize=True, + relativize_to=None): flags = tok.get_uint16() protocol = tok.get_uint8() - algorithm = dns.dnssec.algorithm_from_text(tok.get_string()) - chunks = [] - while 1: - t = tok.get().unescape() - if t.is_eol_or_eof(): - break - if not t.is_identifier(): - raise dns.exception.SyntaxError - chunks.append(t.value.encode()) - b64 = b''.join(chunks) + algorithm = tok.get_string() + b64 = tok.concatenate_remaining_identifiers().encode() key = base64.b64decode(b64) return cls(rdclass, rdtype, flags, protocol, algorithm, key) - def to_wire(self, file, compress=None, origin=None): + def _to_wire(self, file, compress=None, origin=None, canonicalize=False): header = struct.pack("!HBB", self.flags, self.protocol, self.algorithm) file.write(header) file.write(self.key) @classmethod - def from_wire(cls, rdclass, rdtype, wire, current, rdlen, origin=None): - if rdlen < 4: - raise dns.exception.FormError - header = struct.unpack('!HBB', wire[current: current + 4]) - current += 4 - rdlen -= 4 - key = wire[current: current + rdlen].unwrap() + def from_wire_parser(cls, rdclass, rdtype, parser, origin=None): + header = parser.get_struct('!HBB') + key = parser.get_remaining() return cls(rdclass, rdtype, header[0], header[1], header[2], key) - def flags_to_text_set(self): - """Convert a DNSKEY flags value to set texts - @rtype: set([string])""" - return flags_to_text_set(self.flags) +### BEGIN generated Flag constants + +SEP = Flag.SEP +REVOKE = Flag.REVOKE +ZONE = Flag.ZONE + +### END generated Flag constants diff --git a/libs/dns/rdtypes/dnskeybase.pyi b/libs/dns/rdtypes/dnskeybase.pyi new file mode 100644 index 000000000..1b999cfde --- /dev/null +++ b/libs/dns/rdtypes/dnskeybase.pyi @@ -0,0 +1,38 @@ +from typing import Set, Any + +SEP : int +REVOKE : int +ZONE : int + +def flags_to_text_set(flags : int) -> Set[str]: + ... + +def flags_from_text_set(texts_set) -> int: + ... + +from .. import rdata + +class DNSKEYBase(rdata.Rdata): + def __init__(self, rdclass, rdtype, flags, protocol, algorithm, key): + self.flags : int + self.protocol : int + self.key : str + self.algorithm : int + + def to_text(self, origin : Any = None, relativize=True, **kw : Any): + ... + + @classmethod + def from_text(cls, rdclass, rdtype, tok, origin=None, relativize=True, + relativize_to=None): + ... + + def _to_wire(self, file, compress=None, origin=None, canonicalize=False): + ... + + @classmethod + def from_parser(cls, rdclass, rdtype, parser, origin=None): + ... + + def flags_to_text_set(self) -> Set[str]: + ... diff --git a/libs/dns/rdtypes/dsbase.py b/libs/dns/rdtypes/dsbase.py index 1ee28e4a3..0c2e7471b 100644 --- a/libs/dns/rdtypes/dsbase.py +++ b/libs/dns/rdtypes/dsbase.py @@ -1,3 +1,5 @@ +# 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 @@ -16,68 +18,69 @@ import struct import binascii +import dns.dnssec +import dns.immutable import dns.rdata import dns.rdatatype +@dns.immutable.immutable class DSBase(dns.rdata.Rdata): - """Base class for rdata that is like a DS record - - @ivar key_tag: the key tag - @type key_tag: int - @ivar algorithm: the algorithm - @type algorithm: int - @ivar digest_type: the digest type - @type digest_type: int - @ivar digest: the digest - @type digest: int - @see: draft-ietf-dnsext-delegation-signer-14.txt""" + """Base class for rdata that is like a DS record""" __slots__ = ['key_tag', 'algorithm', 'digest_type', 'digest'] + # Digest types registry: https://www.iana.org/assignments/ds-rr-types/ds-rr-types.xhtml + _digest_length_by_type = { + 1: 20, # SHA-1, RFC 3658 Sec. 2.4 + 2: 32, # SHA-256, RFC 4509 Sec. 2.2 + 3: 32, # GOST R 34.11-94, RFC 5933 Sec. 4 in conjunction with RFC 4490 Sec. 2.1 + 4: 48, # SHA-384, RFC 6605 Sec. 2 + } + def __init__(self, rdclass, rdtype, key_tag, algorithm, digest_type, digest): - super(DSBase, self).__init__(rdclass, rdtype) - self.key_tag = key_tag - self.algorithm = algorithm - self.digest_type = digest_type - self.digest = digest + super().__init__(rdclass, rdtype) + self.key_tag = self._as_uint16(key_tag) + self.algorithm = dns.dnssec.Algorithm.make(algorithm) + self.digest_type = self._as_uint8(digest_type) + self.digest = self._as_bytes(digest) + try: + if len(self.digest) != self._digest_length_by_type[self.digest_type]: + raise ValueError('digest length inconsistent with digest type') + except KeyError: + if self.digest_type == 0: # reserved, RFC 3658 Sec. 2.4 + raise ValueError('digest type 0 is reserved') def to_text(self, origin=None, relativize=True, **kw): + kw = kw.copy() + chunksize = kw.pop('chunksize', 128) return '%d %d %d %s' % (self.key_tag, self.algorithm, self.digest_type, dns.rdata._hexify(self.digest, - chunksize=128)) + chunksize=chunksize, + **kw)) @classmethod - def from_text(cls, rdclass, rdtype, tok, origin=None, relativize=True): + def from_text(cls, rdclass, rdtype, tok, origin=None, relativize=True, + relativize_to=None): key_tag = tok.get_uint16() - algorithm = tok.get_uint8() + algorithm = tok.get_string() digest_type = tok.get_uint8() - chunks = [] - while 1: - t = tok.get().unescape() - if t.is_eol_or_eof(): - break - if not t.is_identifier(): - raise dns.exception.SyntaxError - chunks.append(t.value.encode()) - digest = b''.join(chunks) + digest = tok.concatenate_remaining_identifiers().encode() digest = binascii.unhexlify(digest) return cls(rdclass, rdtype, key_tag, algorithm, digest_type, digest) - def to_wire(self, file, compress=None, origin=None): + def _to_wire(self, file, compress=None, origin=None, canonicalize=False): header = struct.pack("!HBB", self.key_tag, self.algorithm, self.digest_type) file.write(header) file.write(self.digest) @classmethod - def from_wire(cls, rdclass, rdtype, wire, current, rdlen, origin=None): - header = struct.unpack("!HBB", wire[current: current + 4]) - current += 4 - rdlen -= 4 - digest = wire[current: current + rdlen].unwrap() + def from_wire_parser(cls, rdclass, rdtype, parser, origin=None): + header = parser.get_struct("!HBB") + digest = parser.get_remaining() return cls(rdclass, rdtype, header[0], header[1], header[2], digest) diff --git a/libs/dns/rdtypes/euibase.py b/libs/dns/rdtypes/euibase.py index cc5fdaa63..48b69bd3b 100644 --- a/libs/dns/rdtypes/euibase.py +++ b/libs/dns/rdtypes/euibase.py @@ -17,16 +17,15 @@ import binascii import dns.rdata -from dns._compat import xrange +import dns.immutable +@dns.immutable.immutable class EUIBase(dns.rdata.Rdata): - """EUIxx record + """EUIxx record""" - @ivar fingerprint: xx-bit Extended Unique Identifier (EUI-xx) - @type fingerprint: string - @see: rfc7043.txt""" + # see: rfc7043.txt __slots__ = ['eui'] # define these in subclasses @@ -34,24 +33,23 @@ class EUIBase(dns.rdata.Rdata): # text_len = byte_len * 3 - 1 # 01-23-45-67-89-ab def __init__(self, rdclass, rdtype, eui): - super(EUIBase, self).__init__(rdclass, rdtype) - if len(eui) != self.byte_len: + 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)) - self.eui = eui def to_text(self, origin=None, relativize=True, **kw): - return dns.rdata._hexify(self.eui, chunksize=2).replace(' ', '-') + return dns.rdata._hexify(self.eui, chunksize=2, separator=b'-', **kw) @classmethod - def from_text(cls, rdclass, rdtype, tok, origin=None, relativize=True): + def from_text(cls, rdclass, rdtype, tok, origin=None, relativize=True, + relativize_to=None): text = tok.get_string() - tok.get_eol() if len(text) != cls.text_len: raise dns.exception.SyntaxError( 'Input text must have %s characters' % cls.text_len) - expected_dash_idxs = xrange(2, cls.byte_len * 3 - 1, 3) - for i in expected_dash_idxs: + for i in range(2, cls.byte_len * 3 - 1, 3): if text[i] != '-': raise dns.exception.SyntaxError('Dash expected at position %s' % i) @@ -62,10 +60,10 @@ class EUIBase(dns.rdata.Rdata): raise dns.exception.SyntaxError('Hex decoding error: %s' % str(ex)) return cls(rdclass, rdtype, data) - def to_wire(self, file, compress=None, origin=None): + def _to_wire(self, file, compress=None, origin=None, canonicalize=False): file.write(self.eui) @classmethod - def from_wire(cls, rdclass, rdtype, wire, current, rdlen, origin=None): - eui = wire[current:current + rdlen].unwrap() + 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/libs/dns/rdtypes/mxbase.py b/libs/dns/rdtypes/mxbase.py index 5ac8cef9e..564182347 100644 --- a/libs/dns/rdtypes/mxbase.py +++ b/libs/dns/rdtypes/mxbase.py @@ -1,3 +1,5 @@ +# 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 @@ -15,87 +17,73 @@ """MX-like base classes.""" -from io import BytesIO import struct import dns.exception +import dns.immutable import dns.rdata import dns.name +import dns.rdtypes.util +@dns.immutable.immutable class MXBase(dns.rdata.Rdata): - """Base class for rdata that is like an MX record. - - @ivar preference: the preference value - @type preference: int - @ivar exchange: the exchange name - @type exchange: dns.name.Name object""" + """Base class for rdata that is like an MX record.""" __slots__ = ['preference', 'exchange'] def __init__(self, rdclass, rdtype, preference, exchange): - super(MXBase, self).__init__(rdclass, rdtype) - self.preference = preference - self.exchange = 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): + def from_text(cls, rdclass, rdtype, tok, origin=None, relativize=True, + relativize_to=None): preference = tok.get_uint16() - exchange = tok.get_name() - exchange = exchange.choose_relativity(origin, relativize) - tok.get_eol() + exchange = tok.get_name(origin, relativize, relativize_to) return cls(rdclass, rdtype, preference, exchange) - def to_wire(self, file, compress=None, origin=None): + 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) - - def to_digestable(self, origin=None): - return struct.pack("!H", self.preference) + \ - self.exchange.to_digestable(origin) + self.exchange.to_wire(file, compress, origin, canonicalize) @classmethod - def from_wire(cls, rdclass, rdtype, wire, current, rdlen, origin=None): - (preference, ) = struct.unpack('!H', wire[current: current + 2]) - current += 2 - rdlen -= 2 - (exchange, cused) = dns.name.from_wire(wire[: current + rdlen], - current) - if cused != rdlen: - raise dns.exception.FormError - if origin is not None: - exchange = exchange.relativize(origin) + 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 choose_relativity(self, origin=None, relativize=True): - self.exchange = self.exchange.choose_relativity(origin, relativize) + 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): - super(UncompressedMX, self).to_wire(file, None, origin) - - def to_digestable(self, origin=None): - f = BytesIO() - self.to_wire(f, None, origin) - return f.getvalue() + 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): - super(UncompressedDowncasingMX, self).to_wire(file, None, origin) + def _to_wire(self, file, compress=None, origin=None, canonicalize=False): + super()._to_wire(file, None, origin, canonicalize) diff --git a/libs/dns/rdtypes/nsbase.py b/libs/dns/rdtypes/nsbase.py index 79333a140..b3e25506d 100644 --- a/libs/dns/rdtypes/nsbase.py +++ b/libs/dns/rdtypes/nsbase.py @@ -1,3 +1,5 @@ +# 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 @@ -15,67 +17,48 @@ """NS-like base classes.""" -from io import BytesIO - import dns.exception +import dns.immutable import dns.rdata import dns.name +@dns.immutable.immutable class NSBase(dns.rdata.Rdata): - """Base class for rdata that is like an NS record. - - @ivar target: the target name of the rdata - @type target: dns.name.Name object""" + """Base class for rdata that is like an NS record.""" __slots__ = ['target'] def __init__(self, rdclass, rdtype, target): - super(NSBase, self).__init__(rdclass, rdtype) - self.target = 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): - target = tok.get_name() - target = target.choose_relativity(origin, relativize) - tok.get_eol() + 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): - self.target.to_wire(file, compress, origin) - - def to_digestable(self, origin=None): - return self.target.to_digestable(origin) + def _to_wire(self, file, compress=None, origin=None, canonicalize=False): + self.target.to_wire(file, compress, origin, canonicalize) @classmethod - def from_wire(cls, rdclass, rdtype, wire, current, rdlen, origin=None): - (target, cused) = dns.name.from_wire(wire[: current + rdlen], - current) - if cused != rdlen: - raise dns.exception.FormError - if origin is not None: - target = target.relativize(origin) + def from_wire_parser(cls, rdclass, rdtype, parser, origin=None): + target = parser.get_name(origin) return cls(rdclass, rdtype, target) - def choose_relativity(self, origin=None, relativize=True): - self.target = self.target.choose_relativity(origin, relativize) - +@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): - super(UncompressedNS, self).to_wire(file, None, origin) - - def to_digestable(self, origin=None): - f = BytesIO() - self.to_wire(f, None, origin) - return f.getvalue() + def _to_wire(self, file, compress=None, origin=None, canonicalize=False): + self.target.to_wire(file, None, origin, False) diff --git a/libs/dns/rdtypes/svcbbase.py b/libs/dns/rdtypes/svcbbase.py new file mode 100644 index 000000000..09d7a52ba --- /dev/null +++ b/libs/dns/rdtypes/svcbbase.py @@ -0,0 +1,555 @@ +# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license + +import base64 +import enum +import io +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.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 + + @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-01 + + __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("not a Param") + self.params = dns.immutable.Dict(params) + # Make sure any paramater 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] + # placeholder for length (or actual length of empty values) + file.write(struct.pack("!H", 0)) + if value is None: + continue + else: + start = file.tell() + value.to_wire(file, origin) + end = file.tell() + assert end - start < 65536 + file.seek(start - 2) + stuff = struct.pack("!H", end - start) + file.write(stuff) + file.seek(0, io.SEEK_END) + + @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/libs/dns/rdtypes/tlsabase.py b/libs/dns/rdtypes/tlsabase.py new file mode 100644 index 000000000..786fca554 --- /dev/null +++ b/libs/dns/rdtypes/tlsabase.py @@ -0,0 +1,72 @@ +# 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 struct +import binascii + +import dns.rdata +import dns.immutable +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/libs/dns/rdtypes/txtbase.py b/libs/dns/rdtypes/txtbase.py index 352b027bf..68071ee0a 100644 --- a/libs/dns/rdtypes/txtbase.py +++ b/libs/dns/rdtypes/txtbase.py @@ -1,4 +1,6 @@ -# Copyright (C) 2006, 2007, 2009-2011 Nominum, Inc. +# 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, @@ -18,56 +20,58 @@ import struct import dns.exception +import dns.immutable import dns.rdata import dns.tokenizer -from dns._compat import binary_type +@dns.immutable.immutable class TXTBase(dns.rdata.Rdata): - """Base class for rdata that is like a TXT record - - @ivar strings: the text strings - @type strings: list of string - @see: RFC 1035""" + """Base class for rdata that is like a TXT record (see RFC 1035).""" __slots__ = ['strings'] def __init__(self, rdclass, rdtype, strings): - super(TXTBase, self).__init__(rdclass, rdtype) - if isinstance(strings, str): - strings = [strings] - self.strings = strings[:] + """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 = self._as_tuple(strings, + lambda x: self._as_bytes(x, True, 255)) def to_text(self, origin=None, relativize=True, **kw): txt = '' prefix = '' for s in self.strings: - txt += '%s"%s"' % (prefix, dns.rdata._escapify(s)) + txt += '{}"{}"'.format(prefix, dns.rdata._escapify(s)) prefix = ' ' return txt @classmethod - def from_text(cls, rdclass, rdtype, tok, origin=None, relativize=True): + def from_text(cls, rdclass, rdtype, tok, origin=None, relativize=True, + relativize_to=None): strings = [] - while 1: - token = tok.get().unescape() - if token.is_eol_or_eof(): - break - if not (token.is_quoted_string() or token.is_identifier()): + 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") - value = token.value - if isinstance(value, binary_type): - strings.append(value) - else: - strings.append(value.encode()) + 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): + def _to_wire(self, file, compress=None, origin=None, canonicalize=False): for s in self.strings: l = len(s) assert l < 256 @@ -75,16 +79,9 @@ class TXTBase(dns.rdata.Rdata): file.write(s) @classmethod - def from_wire(cls, rdclass, rdtype, wire, current, rdlen, origin=None): + def from_wire_parser(cls, rdclass, rdtype, parser, origin=None): strings = [] - while rdlen > 0: - l = wire[current] - current += 1 - rdlen -= 1 - if l > rdlen: - raise dns.exception.FormError - s = wire[current: current + l].unwrap() - current += l - rdlen -= l + while parser.remaining() > 0: + s = parser.get_counted_bytes() strings.append(s) return cls(rdclass, rdtype, strings) diff --git a/libs/dns/rdtypes/txtbase.pyi b/libs/dns/rdtypes/txtbase.pyi new file mode 100644 index 000000000..af447d500 --- /dev/null +++ b/libs/dns/rdtypes/txtbase.pyi @@ -0,0 +1,6 @@ +from .. import rdata + +class TXTBase(rdata.Rdata): + ... +class TXT(TXTBase): + ... diff --git a/libs/dns/rdtypes/util.py b/libs/dns/rdtypes/util.py new file mode 100644 index 000000000..9bf8f7e95 --- /dev/null +++ b/libs/dns/rdtypes/util.py @@ -0,0 +1,244 @@ +# 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 + +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): + 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): + 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) + rdtypes.sort() + 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): + for (window, bitmap) in self.windows: + file.write(struct.pack('!BB', window, len(bitmap))) + file.write(bitmap) + + @classmethod + def from_wire_parser(cls, parser): + 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/libs/dns/renderer.py b/libs/dns/renderer.py index 670fb28fa..72f0f7a8a 100644 --- a/libs/dns/renderer.py +++ b/libs/dns/renderer.py @@ -1,4 +1,6 @@ -# Copyright (C) 2001-2007, 2009-2011 Nominum, Inc. +# 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, @@ -15,14 +17,14 @@ """Help for building DNS wire format messages""" -from io import BytesIO +import contextlib +import io import struct import random import time import dns.exception import dns.tsig -from ._compat import long QUESTION = 0 @@ -31,8 +33,7 @@ AUTHORITY = 2 ADDITIONAL = 3 -class Renderer(object): - +class Renderer: """Helper class for building DNS wire-format messages. Most applications can use the higher-level L{dns.message.Message} @@ -54,43 +55,29 @@ class Renderer(object): r.add_tsig(keyname, secret, 300, 1, 0, '', request_mac) wire = r.get_wire() - @ivar output: where rendering is written - @type output: BytesIO object - @ivar id: the message id - @type id: int - @ivar flags: the message flags - @type flags: int - @ivar max_size: the maximum size of the message - @type max_size: int - @ivar origin: the origin to use when rendering relative names - @type origin: dns.name.Name object - @ivar compress: the compression table - @type compress: dict - @ivar section: the section currently being rendered - @type section: int (dns.renderer.QUESTION, dns.renderer.ANSWER, - dns.renderer.AUTHORITY, or dns.renderer.ADDITIONAL) - @ivar counts: list of the number of RRs in each section - @type counts: int list of length 4 - @ivar mac: the MAC of the rendered message (if TSIG was used) - @type mac: string + 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. + """Initialize a new renderer.""" - @param id: the message id - @type id: int - @param flags: the DNS message flags - @type flags: int - @param max_size: the maximum message size; the default is 65535. - If rendering results in a message greater than I{max_size}, - then L{dns.exception.TooBig} will be raised. - @type max_size: int - @param origin: the origin to use when rendering relative names - @type origin: dns.name.Name or None. - """ - - self.output = BytesIO() + self.output = io.BytesIO() if id is None: self.id = random.randint(0, 65535) else: @@ -105,12 +92,9 @@ class Renderer(object): self.mac = '' def _rollback(self, where): - """Truncate the output buffer at offset I{where}, and remove any + """Truncate the output buffer at offset *where*, and remove any compression table entries that pointed beyond the truncation point. - - @param where: the offset - @type where: int """ self.output.seek(where) @@ -128,9 +112,7 @@ class Renderer(object): Sections must be rendered order: QUESTION, ANSWER, AUTHORITY, ADDITIONAL. Sections may be empty. - @param section: the section - @type section: int - @raises dns.exception.FormError: an attempt was made to set + Raises dns.exception.FormError if an attempt was made to set a section value less than the current section. """ @@ -139,25 +121,21 @@ class Renderer(object): raise dns.exception.FormError self.section = section - def add_question(self, qname, rdtype, rdclass=dns.rdataclass.IN): - """Add a question to the message. + @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 - @param qname: the question name - @type qname: dns.name.Name - @param rdtype: the question rdata type - @type rdtype: int - @param rdclass: the question rdata class - @type rdclass: int - """ + def add_question(self, qname, rdtype, rdclass=dns.rdataclass.IN): + """Add a question to the message.""" self._set_section(QUESTION) - before = self.output.tell() - qname.to_wire(self.output, self.compress, self.origin) - self.output.write(struct.pack("!HH", rdtype, rdclass)) - after = self.output.tell() - if after >= self.max_size: - self._rollback(before) - raise dns.exception.TooBig + 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): @@ -165,20 +143,11 @@ class Renderer(object): Any keyword arguments are passed on to the rdataset's to_wire() routine. - - @param section: the section - @type section: int - @param rrset: the rrset - @type rrset: dns.rrset.RRset object """ self._set_section(section) - before = self.output.tell() - n = rrset.to_wire(self.output, self.compress, self.origin, **kw) - after = self.output.tell() - if after >= self.max_size: - self._rollback(before) - raise dns.exception.TooBig + 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): @@ -187,124 +156,79 @@ class Renderer(object): Any keyword arguments are passed on to the rdataset's to_wire() routine. - - @param section: the section - @type section: int - @param name: the owner name - @type name: dns.name.Name object - @param rdataset: the rdataset - @type rdataset: dns.rdataset.Rdataset object """ self._set_section(section) - before = self.output.tell() - n = rdataset.to_wire(name, self.output, self.compress, self.origin, - **kw) - after = self.output.tell() - if after >= self.max_size: - self._rollback(before) - raise dns.exception.TooBig + with self._track_size(): + n = rdataset.to_wire(name, self.output, self.compress, self.origin, + **kw) self.counts[section] += n def add_edns(self, edns, ednsflags, payload, options=None): - """Add an EDNS OPT record to the message. - - @param edns: The EDNS level to use. - @type edns: int - @param ednsflags: EDNS flag values. - @type ednsflags: int - @param payload: The EDNS sender's payload field, which is the maximum - size of UDP datagram the sender can handle. - @type payload: int - @param options: The EDNS options list - @type options: list of dns.edns.Option instances - @see: RFC 2671 - """ + """Add an EDNS OPT record to the message.""" # make sure the EDNS version in ednsflags agrees with edns - ednsflags &= long(0xFF00FFFF) + ednsflags &= 0xFF00FFFF ednsflags |= (edns << 16) - self._set_section(ADDITIONAL) - before = self.output.tell() - self.output.write(struct.pack('!BHHIH', 0, dns.rdatatype.OPT, payload, - ednsflags, 0)) - if options is not None: - lstart = self.output.tell() - for opt in options: - stuff = struct.pack("!HH", opt.otype, 0) - self.output.write(stuff) - start = self.output.tell() - opt.to_wire(self.output) - end = self.output.tell() - assert end - start < 65536 - self.output.seek(start - 2) - stuff = struct.pack("!H", end - start) - self.output.write(stuff) - self.output.seek(0, 2) - lend = self.output.tell() - assert lend - lstart < 65536 - self.output.seek(lstart - 2) - stuff = struct.pack("!H", lend - lstart) - self.output.write(stuff) - self.output.seek(0, 2) - after = self.output.tell() - if after >= self.max_size: - self._rollback(before) - raise dns.exception.TooBig - self.counts[ADDITIONAL] += 1 + opt = dns.message.Message._make_opt(ednsflags, payload, options) + self.add_rrset(ADDITIONAL, 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. + """Add a TSIG signature to the message.""" - @param keyname: the TSIG key name - @type keyname: dns.name.Name object - @param secret: the secret to use - @type secret: string - @param fudge: TSIG time fudge - @type fudge: int - @param id: the message id to encode in the tsig signature - @type id: int - @param tsig_error: TSIG error code; default is 0. - @type tsig_error: int - @param other_data: TSIG other data. - @type other_data: string - @param request_mac: This message is a response to the request which - had the specified MAC. - @type request_mac: string - @param algorithm: the TSIG algorithm to use - @type algorithm: dns.name.Name object - """ - - self._set_section(ADDITIONAL) - before = self.output.tell() s = self.output.getvalue() - (tsig_rdata, self.mac, ctx) = dns.tsig.sign(s, - keyname, - secret, - int(time.time()), - fudge, - id, - tsig_error, - other_data, - request_mac, - algorithm=algorithm) - keyname.to_wire(self.output, self.compress, self.origin) - self.output.write(struct.pack('!HHIH', dns.rdatatype.TSIG, - dns.rdataclass.ANY, 0, 0)) - rdata_start = self.output.tell() - self.output.write(tsig_rdata) + + 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): + self._set_section(ADDITIONAL) + with self._track_size(): + keyname.to_wire(self.output, self.compress, self.origin) + self.output.write(struct.pack('!HHIH', dns.rdatatype.TSIG, + dns.rdataclass.ANY, 0, 0)) + rdata_start = self.output.tell() + tsig.to_wire(self.output) + after = self.output.tell() - assert after - rdata_start < 65536 - if after >= self.max_size: - self._rollback(before) - raise dns.exception.TooBig self.output.seek(rdata_start - 2) self.output.write(struct.pack('!H', after - rdata_start)) self.counts[ADDITIONAL] += 1 self.output.seek(10) self.output.write(struct.pack('!H', self.counts[ADDITIONAL])) - self.output.seek(0, 2) + self.output.seek(0, io.SEEK_END) def write_header(self): """Write the DNS message header. @@ -318,12 +242,9 @@ class Renderer(object): self.output.write(struct.pack('!HHHHHH', self.id, self.flags, self.counts[0], self.counts[1], self.counts[2], self.counts[3])) - self.output.seek(0, 2) + self.output.seek(0, io.SEEK_END) def get_wire(self): - """Return the wire format message. - - @rtype: string - """ + """Return the wire format message.""" return self.output.getvalue() diff --git a/libs/dns/resolver.py b/libs/dns/resolver.py index abc431d7f..166f84921 100644 --- a/libs/dns/resolver.py +++ b/libs/dns/resolver.py @@ -1,4 +1,6 @@ -# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc. +# 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, @@ -13,23 +15,22 @@ # 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. - -@var default_resolver: The default resolver object -@type default_resolver: dns.resolver.Resolver object""" - +"""DNS stub resolver.""" +from urllib.parse import urlparse +import contextlib import socket import sys import time import random - +import warnings try: import threading as _threading -except ImportError: - import dummy_threading as _threading +except ImportError: # pragma: no cover + import dummy_threading as _threading # type: ignore import dns.exception import dns.flags +import dns.inet import dns.ipv4 import dns.ipv6 import dns.message @@ -40,21 +41,19 @@ import dns.rdataclass import dns.rdatatype import dns.reversename import dns.tsig -from ._compat import xrange, string_types if sys.platform == 'win32': - try: - import winreg as _winreg - except ImportError: - import _winreg # pylint: disable=import-error + import dns.win32util class NXDOMAIN(dns.exception.DNSException): - """The DNS query name does not exist.""" - supp_kwargs = set(['qnames', 'responses']) + supp_kwargs = {'qnames', 'responses'} fmt = None # we have our own __str__ implementation - def _check_kwargs(self, qnames, responses=None): + # pylint: disable=arguments-differ + + 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: @@ -68,32 +67,31 @@ class NXDOMAIN(dns.exception.DNSException): def __str__(self): if 'qnames' not in self.kwargs: - return super(NXDOMAIN, self).__str__() + return super().__str__() qnames = self.kwargs['qnames'] if len(qnames) > 1: msg = 'None of DNS query names exist' else: - msg = self.__doc__[:-1] + msg = 'The DNS query name does not exist' qnames = ', '.join(map(str, qnames)) - return "%s: %s" % (msg, qnames) + return "{}: {}".format(msg, qnames) + @property def canonical_name(self): - if not 'qnames' in self.kwargs: + """Return the unresolved canonical name.""" + if 'qnames' not in self.kwargs: raise TypeError("parametrized exception required") - IN = dns.rdataclass.IN - CNAME = dns.rdatatype.CNAME - cname = None for qname in self.kwargs['qnames']: response = self.kwargs['responses'][qname] - for answer in response.answer: - if answer.rdtype != CNAME or answer.rdclass != IN: - continue - cname = answer.items[0].target.to_text() - if cname is not None: - return dns.name.from_text(cname) + 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] - canonical_name = property(canonical_name, doc=( - "Return the unresolved canonical name.")) def __add__(self, e_nx): """Augment by results from another NXDOMAIN exception.""" @@ -107,157 +105,139 @@ class NXDOMAIN(dns.exception.DNSException): 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.""" -# The definition of the Timeout exception has moved from here to the -# dns.exception module. We keep dns.resolver.Timeout defined for -# backwards compatibility. -Timeout = dns.exception.Timeout +def _errors_to_text(errors): + """Turn a resolution errors trace into a list of text.""" + texts = [] + for err in errors: + texts.append('Server {} {} port {} answered {}'.format(err[0], + 'TCP' if err[1] else 'UDP', err[2], err[3])) + return texts + + +class LifetimeTimeout(dns.exception.Timeout): + """The resolution lifetime expired.""" + + msg = "The resolution lifetime expired." + fmt = "%s after {timeout} seconds: {errors}" % msg[:-1] + supp_kwargs = {'timeout', 'errors'} + + 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 = set(['response']) + supp_kwargs = {'response'} def _fmt_kwargs(self, **kwargs): - return super(NoAnswer, self)._fmt_kwargs( - query=kwargs['response'].question) + 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)]. + [(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 = set(['request', 'errors']) + supp_kwargs = {'request', 'errors'} def _fmt_kwargs(self, **kwargs): - srv_msgs = [] - for err in kwargs['errors']: - srv_msgs.append('Server %s %s port %s answered %s' % (err[0], - 'TCP' if err[1] else 'UDP', err[2], err[3])) - return super(NoNameservers, self)._fmt_kwargs( - query=kwargs['request'].question, errors='; '.join(srv_msgs)) + 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(object): - - """DNS stub resolver answer +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. 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]". + 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 - node's name might not be the query name. - - @ivar qname: The query name - @type qname: dns.name.Name object - @ivar rdtype: The query type - @type rdtype: int - @ivar rdclass: The query class - @type rdclass: int - @ivar response: The response message - @type response: dns.message.Message object - @ivar rrset: The answer - @type rrset: dns.rrset.RRset object - @ivar expiration: The time when the answer expires - @type expiration: float (seconds since the epoch) - @ivar canonical_name: The canonical name of the query name - @type canonical_name: dns.name.Name object + RRset's name might not be the query name. """ - def __init__(self, qname, rdtype, rdclass, response, - raise_on_no_answer=True): + def __init__(self, qname, rdtype, rdclass, response, nameserver=None, + port=None): self.qname = qname self.rdtype = rdtype self.rdclass = rdclass self.response = response - min_ttl = -1 - rrset = None - for count in xrange(0, 15): - try: - rrset = response.find_rrset(response.answer, qname, - rdclass, rdtype) - if min_ttl == -1 or rrset.ttl < min_ttl: - min_ttl = rrset.ttl - break - except KeyError: - if rdtype != dns.rdatatype.CNAME: - try: - crrset = response.find_rrset(response.answer, - qname, - rdclass, - dns.rdatatype.CNAME) - if min_ttl == -1 or crrset.ttl < min_ttl: - min_ttl = crrset.ttl - for rd in crrset: - qname = rd.target - break - continue - except KeyError: - if raise_on_no_answer: - raise NoAnswer(response=response) - if raise_on_no_answer: - raise NoAnswer(response=response) - if rrset is None and raise_on_no_answer: - raise NoAnswer(response=response) - self.canonical_name = qname - self.rrset = rrset - if rrset is None: - while 1: - # Look for a SOA RR whose owner name is a superdomain - # of qname. - try: - srrset = response.find_rrset(response.authority, qname, - rdclass, dns.rdatatype.SOA) - if min_ttl == -1 or srrset.ttl < min_ttl: - min_ttl = srrset.ttl - if srrset[0].minimum < min_ttl: - min_ttl = srrset[0].minimum - break - except KeyError: - try: - qname = qname.parent() - except dns.name.NoParent: - break - self.expiration = time.time() + min_ttl + 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): + def __getattr__(self, attr): # pragma: no cover if attr == 'name': return self.rrset.name elif attr == 'ttl': @@ -278,38 +258,75 @@ class Answer(object): 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 Cache(object): - - """Simple DNS answer cache. - - @ivar data: A dictionary of cached data - @type data: dict - @ivar cleaning_interval: The number of seconds between cleanings. The - default is 300 (5 minutes). - @type cleaning_interval: float - @ivar next_cleaning: The time the cache should next be cleaned (in seconds - since the epoch.) - @type next_cleaning: float +class CacheStatistics: + """Cache Statistics """ - def __init__(self, cleaning_interval=300.0): - """Initialize a DNS cache. + def __init__(self, hits=0, misses=0): + self.hits = hits + self.misses = misses - @param cleaning_interval: the number of seconds between periodic - cleanings. The default is 300.0 - @type cleaning_interval: float. + def reset(self): + self.hits = 0 + self.misses = 0 + + def clone(self): + return CacheStatistics(self.hits, self.misses) + + +class CacheBase: + def __init__(self): + self.lock = _threading.Lock() + self.statistics = CacheStatistics() + + def reset_statistics(self): + """Reset all statistics to zero.""" + with self.lock: + self.statistics.reset() + + def hits(self): + """How many hits has the cache had?""" + with self.lock: + return self.statistics.hits + + def misses(self): + """How many misses has the cache had?""" + with self.lock: + return self.statistics.misses + + def get_statistics_snapshot(self): + """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() + + +class Cache(CacheBase): + """Simple thread-safe DNS answer cache.""" + + def __init__(self, cleaning_interval=300.0): + """*cleaning_interval*, a ``float`` is the number of seconds between + periodic cleanings. """ + super().__init__() self.data = {} self.cleaning_interval = cleaning_interval self.next_cleaning = time.time() + self.cleaning_interval - self.lock = _threading.Lock() def _maybe_clean(self): """Clean the cache if it's time to do so.""" @@ -326,79 +343,67 @@ class Cache(object): self.next_cleaning = now + self.cleaning_interval def get(self, key): - """Get the answer associated with I{key}. Returns None if - no answer is cached for the key. - @param key: the key - @type key: (dns.name.Name, int, int) tuple whose values are the - query name, rdtype, and rdclass. - @rtype: dns.resolver.Answer object or None + """Get the answer associated with *key*. + + Returns None if no answer is cached for the key. + + *key*, a ``(dns.name.Name, int, int)`` tuple whose values are the + query name, rdtype, and rdclass respectively. + + Returns a ``dns.resolver.Answer`` or ``None``. """ - try: - self.lock.acquire() + 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 - finally: - self.lock.release() def put(self, key, value): """Associate key and value in the cache. - @param key: the key - @type key: (dns.name.Name, int, int) tuple whose values are the - query name, rdtype, and rdclass. - @param value: The answer being cached - @type value: dns.resolver.Answer object + + *key*, a ``(dns.name.Name, int, int)`` tuple whose values are the + query name, rdtype, and rdclass respectively. + + *value*, a ``dns.resolver.Answer``, the answer. """ - try: - self.lock.acquire() + with self.lock: self._maybe_clean() self.data[key] = value - finally: - self.lock.release() def flush(self, key=None): """Flush the cache. - If I{key} is specified, only that item is flushed. Otherwise + If *key* is not ``None``, only that item is flushed. Otherwise the entire cache is flushed. - @param key: the key to flush - @type key: (dns.name.Name, int, int) tuple or None + *key*, a ``(dns.name.Name, int, int)`` tuple whose values are the + query name, rdtype, and rdclass respectively. """ - try: - self.lock.acquire() + 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 - finally: - self.lock.release() -class LRUCacheNode(object): - - """LRUCache node. - """ +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_before(self, node): - self.prev = node.prev - self.next = node - node.prev.next = self - node.prev = self - def link_after(self, node): self.prev = node self.next = node.next @@ -410,35 +415,27 @@ class LRUCacheNode(object): self.prev.next = self.next -class LRUCache(object): - - """Bounded least-recently-used DNS answer cache. +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. - - @ivar data: A dictionary of cached data - @type data: dict - @ivar sentinel: sentinel node for circular doubly linked list of nodes - @type sentinel: LRUCacheNode object - @ivar max_size: The maximum number of nodes - @type max_size: int """ def __init__(self, max_size=100000): - """Initialize a DNS cache. - - @param max_size: The maximum number of nodes to cache; the default is - 100,000. Must be greater than 1. - @type max_size: int + """*max_size*, an ``int``, is the maximum number of nodes to cache; + it must be greater than 0. """ + + super().__init__() self.data = {} self.set_max_size(max_size) self.sentinel = LRUCacheNode(None, None) - self.lock = _threading.Lock() + self.sentinel.prev = self.sentinel + self.sentinel.next = self.sentinel def set_max_size(self, max_size): if max_size < 1: @@ -446,39 +443,52 @@ class LRUCache(object): self.max_size = max_size def get(self, key): - """Get the answer associated with I{key}. Returns None if - no answer is cached for the key. - @param key: the key - @type key: (dns.name.Name, int, int) tuple whose values are the - query name, rdtype, and rdclass. - @rtype: dns.resolver.Answer object or None + """Get the answer associated with *key*. + + Returns None if no answer is cached for the key. + + *key*, a ``(dns.name.Name, int, int)`` tuple whose values are the + query name, rdtype, and rdclass respectively. + + Returns a ``dns.resolver.Answer`` or ``None``. """ - try: - self.lock.acquire() + + 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 - finally: - self.lock.release() + + def get_hits_for_key(self, key): + """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, value): """Associate key and value in the cache. - @param key: the key - @type key: (dns.name.Name, int, int) tuple whose values are the - query name, rdtype, and rdclass. - @param value: The answer being cached - @type value: dns.resolver.Answer object + + *key*, a ``(dns.name.Name, int, int)`` tuple whose values are the + query name, rdtype, and rdclass respectively. + + *value*, a ``dns.resolver.Answer``, the answer. """ - try: - self.lock.acquire() + + with self.lock: node = self.data.get(key) if node is not None: node.unlink() @@ -490,20 +500,18 @@ class LRUCache(object): node = LRUCacheNode(key, value) node.link_after(self.sentinel) self.data[key] = node - finally: - self.lock.release() def flush(self, key=None): """Flush the cache. - If I{key} is specified, only that item is flushed. Otherwise + If *key* is not ``None``, only that item is flushed. Otherwise the entire cache is flushed. - @param key: the key to flush - @type key: (dns.name.Name, int, int) tuple or None + *key*, a ``(dns.name.Name, int, int)`` tuple whose values are the + query name, rdtype, and rdclass respectively. """ - try: - self.lock.acquire() + + with self.lock: if key is not None: node = self.data.get(key) if node is not None: @@ -513,89 +521,234 @@ class LRUCache(object): node = self.sentinel.next while node != self.sentinel: next = node.next - node.prev = None - node.next = None + node.unlink() node = next self.data = {} - finally: - self.lock.release() +class _Resolution: + """Helper class for dns.resolver.Resolver.resolve(). -class Resolver(object): + 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. - """DNS stub resolver - - @ivar domain: The domain of this host - @type domain: dns.name.Name object - @ivar nameservers: A list of nameservers to query. Each nameserver is - a string which contains the IP address of a nameserver. - @type nameservers: list of strings - @ivar search: The search list. If the query name is a relative name, - the resolver will construct an absolute query name by appending the search - names one by one to the query name. - @type search: list of dns.name.Name objects - @ivar port: The port to which to send queries. The default is 53. - @type port: int - @ivar timeout: The number of seconds to wait for a response from a - server, before timing out. - @type timeout: float - @ivar lifetime: The total number of seconds to spend trying to get an - answer to the question. If the lifetime expires, a Timeout exception - will occur. - @type lifetime: float - @ivar keyring: The TSIG keyring to use. The default is None. - @type keyring: dict - @ivar keyname: The TSIG keyname to use. The default is None. - @type keyname: dns.name.Name object - @ivar keyalgorithm: The TSIG key algorithm to use. The default is - dns.tsig.default_algorithm. - @type keyalgorithm: string - @ivar edns: The EDNS level to use. The default is -1, no Edns. - @type edns: int - @ivar ednsflags: The EDNS flags - @type ednsflags: int - @ivar payload: The EDNS payload size. The default is 0. - @type payload: int - @ivar flags: The message flags to use. The default is None (i.e. not - overwritten) - @type flags: int - @ivar cache: The cache to use. The default is None. - @type cache: dns.resolver.Cache object - @ivar retry_servfail: should we retry a nameserver if it says SERVFAIL? - The default is 'false'. - @type retry_servfail: bool + This class is a "friend" to dns.resolver.Resolver and manipulates + resolver data structures directly. """ + def __init__(self, resolver, qname, rdtype, rdclass, tcp, + raise_on_no_answer, search): + 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 = {} + # + # Initialize other things to help analysis tools + self.qname = dns.name.empty + self.nameservers = [] + self.current_nameservers = [] + self.errors = [] + self.nameserver = None + self.port = 0 + self.tcp_attempt = False + self.retry_with_tcp = False + self.request = None + self.backoff = 0 + + def next_request(self): + """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) + if self.resolver.flags is not None: + request.flags = self.resolver.flags + + self.nameservers = self.resolver.nameservers[:] + 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): + if self.retry_with_tcp: + assert self.nameserver is not None + self.tcp_attempt = True + self.retry_with_tcp = False + return (self.nameserver, self.port, True, 0) + + backoff = 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.port = self.resolver.nameserver_ports.get(self.nameserver, + self.resolver.port) + self.tcp_attempt = self.tcp + return (self.nameserver, self.port, self.tcp_attempt, backoff) + + def query_result(self, response, ex): + # + # returns an (answer: Answer, end_loop: bool) tuple. + # + if ex: + # Exception during I/O or from_wire() + assert response is None + self.errors.append((self.nameserver, self.tcp_attempt, self.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 + rcode = response.rcode() + if rcode == dns.rcode.NOERROR: + try: + answer = Answer(self.qname, self.rdtype, self.rdclass, response, + self.nameserver, self.port) + except Exception as e: + self.errors.append((self.nameserver, self.tcp_attempt, + self.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((self.nameserver, self.tcp_attempt, + self.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((self.nameserver, self.tcp_attempt, + self.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((self.nameserver, self.tcp_attempt, self.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 + def __init__(self, filename='/etc/resolv.conf', configure=True): - """Initialize a resolver instance. + """*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. - @param filename: The filename of a configuration file in - standard /etc/resolv.conf format. This parameter is meaningful - only when I{configure} is true and the platform is POSIX. - @type filename: string or file object - @param configure: If True (the default), the resolver instance - is configured in the normal fashion for the operating system - the resolver is running on. (I.e. a /etc/resolv.conf file on - POSIX systems and from the registry on Windows systems.) - @type configure: bool""" - - self.domain = None - self.nameservers = None - self.nameserver_ports = None - self.port = None - self.search = None - self.timeout = None - self.lifetime = None - self.keyring = None - self.keyname = None - self.keyalgorithm = None - self.edns = None - self.ednsflags = None - self.payload = None - self.cache = None - self.flags = None - self.retry_servfail = False - self.rotate = False + *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: @@ -606,6 +759,7 @@ class Resolver(object): def reset(self): """Reset all resolver configuration to the defaults.""" + self.domain = \ dns.name.Name(dns.name.from_text(socket.gethostname())[1:]) if len(self.domain) == 0: @@ -614,8 +768,9 @@ class Resolver(object): self.nameserver_ports = {} self.port = 53 self.search = [] + self.use_search_by_default = False self.timeout = 2.0 - self.lifetime = 30.0 + self.lifetime = 5.0 self.keyring = None self.keyname = None self.keyalgorithm = dns.tsig.default_algorithm @@ -626,23 +781,33 @@ class Resolver(object): self.flags = None self.retry_servfail = False self.rotate = False + self.ndots = None def read_resolv_conf(self, f): - """Process f as a file in the /etc/resolv.conf format. If f is - a string, it is used as the name of the file to open; otherwise it - is treated as the file itself.""" - if isinstance(f, string_types): - try: - f = open(f, 'r') - except IOError: - # /etc/resolv.conf doesn't exist, can't be read, etc. - # We'll just use the default resolver configuration. - self.nameservers = ['127.0.0.1'] - return - want_close = True - else: - want_close = False - try: + """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 + + """ + + with contextlib.ExitStack() as stack: + if isinstance(f, str): + try: + f = stack.enter_context(open(f)) + except OSError: + # /etc/resolv.conf doesn't exist, can't be read, etc. + raise NoResolverConfiguration(f'cannot open {f}') + for l in f: if len(l) == 0 or l[0] == '#' or l[0] == ';': continue @@ -656,450 +821,342 @@ class Resolver(object): self.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': - if 'rotate' in tokens[1:]: - self.rotate = True - finally: - if want_close: - f.close() + 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(self.nameservers) == 0: - self.nameservers.append('127.0.0.1') - - 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_win32_nameservers(self, nameservers): - """Configure a NameServer registry entry.""" - # we call str() on nameservers to convert it from unicode to ascii - nameservers = str(nameservers) - split_char = self._determine_split_char(nameservers) - ns_list = nameservers.split(split_char) - for ns in ns_list: - if ns not in self.nameservers: - self.nameservers.append(ns) - - def _config_win32_domain(self, domain): - """Configure a Domain registry entry.""" - # we call str() on domain to convert it from unicode to ascii - self.domain = dns.name.from_text(str(domain)) - - def _config_win32_search(self, search): - """Configure a Search registry entry.""" - # we call str() on search to convert it from unicode to ascii - search = str(search) - split_char = self._determine_split_char(search) - search_list = search.split(split_char) - for s in search_list: - if s not in self.search: - self.search.append(dns.name.from_text(s)) - - def _config_win32_fromkey(self, key): - """Extract DNS info from a registry key.""" - try: - servers, rtype = _winreg.QueryValueEx(key, 'NameServer') - except WindowsError: # pylint: disable=undefined-variable - servers = None - if servers: - self._config_win32_nameservers(servers) - try: - dom, rtype = _winreg.QueryValueEx(key, 'Domain') - if dom: - self._config_win32_domain(dom) - except WindowsError: # pylint: disable=undefined-variable - pass - else: - try: - servers, rtype = _winreg.QueryValueEx(key, 'DhcpNameServer') - except WindowsError: # pylint: disable=undefined-variable - servers = None - if servers: - self._config_win32_nameservers(servers) - try: - dom, rtype = _winreg.QueryValueEx(key, 'DhcpDomain') - if dom: - self._config_win32_domain(dom) - except WindowsError: # pylint: disable=undefined-variable - pass - try: - search, rtype = _winreg.QueryValueEx(key, 'SearchList') - except WindowsError: # pylint: disable=undefined-variable - search = None - if search: - self._config_win32_search(search) + raise NoResolverConfiguration('no nameservers') def read_registry(self): """Extract resolver configuration from the Windows registry.""" - lm = _winreg.ConnectRegistry(None, _winreg.HKEY_LOCAL_MACHINE) - want_scan = False try: - try: - # XP, 2000 - tcp_params = _winreg.OpenKey(lm, - r'SYSTEM\CurrentControlSet' - r'\Services\Tcpip\Parameters') - want_scan = True - except EnvironmentError: - # ME - tcp_params = _winreg.OpenKey(lm, - r'SYSTEM\CurrentControlSet' - r'\Services\VxD\MSTCP') - try: - self._config_win32_fromkey(tcp_params) - finally: - tcp_params.Close() - if want_scan: - interfaces = _winreg.OpenKey(lm, - r'SYSTEM\CurrentControlSet' - r'\Services\Tcpip\Parameters' - r'\Interfaces') - try: - i = 0 - while True: - try: - guid = _winreg.EnumKey(interfaces, i) - i += 1 - key = _winreg.OpenKey(interfaces, guid) - if not self._win32_is_nic_enabled(lm, guid, key): - continue - try: - self._config_win32_fromkey(key) - finally: - key.Close() - except EnvironmentError: - break - finally: - interfaces.Close() - finally: - lm.Close() + info = dns.win32util.get_dns_info() + if info.domain is not None: + self.domain = info.domain + self.nameservers = info.nameservers + self.search = info.search + except AttributeError: + raise NotImplementedError - def _win32_is_nic_enabled(self, lm, guid, interface_key): - # 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 - - 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 - - # Based on experimentation, bit 0x1 indicates that the - # device is disabled. - return not flags & 0x1 - - finally: - device_key.Close() - finally: - connection_key.Close() - except (EnvironmentError, ValueError): - # Pre-vista, enabled interfaces seem to have a non-empty - # NTEContextList; this was how dnspython detected enabled - # nics before the code above was contributed. We've retained - # the old method since we don't know if the code above works - # on Windows 95/98/ME. - try: - (nte, ttype) = _winreg.QueryValueEx(interface_key, - 'NTEContextList') - return nte is not None - except WindowsError: # pylint: disable=undefined-variable - return False - - def _compute_timeout(self, start): + def _compute_timeout(self, start, lifetime=None, errors=None): + 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 Timeout(timeout=duration) + 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. now = start - if duration >= self.lifetime: - raise Timeout(timeout=duration) - return min(self.lifetime - duration, self.timeout) + if duration >= lifetime: + raise LifetimeTimeout(timeout=duration, errors=errors) + return min(lifetime - duration, self.timeout) - def query(self, qname, rdtype=dns.rdatatype.A, rdclass=dns.rdataclass.IN, - tcp=False, source=None, raise_on_no_answer=True, source_port=0): - """Query nameservers to find the answer to the question. - - The I{qname}, I{rdtype}, and I{rdclass} parameters may be objects - of the appropriate type, or strings that can be converted into objects - of the appropriate type. E.g. For I{rdtype} the integer 2 and the - the string 'NS' both mean to query for records with DNS rdata type NS. - - @param qname: the query name - @type qname: dns.name.Name object or string - @param rdtype: the query type - @type rdtype: int or string - @param rdclass: the query class - @type rdclass: int or string - @param tcp: use TCP to make the query (default is False). - @type tcp: bool - @param source: bind to this IP address (defaults to machine default - IP). - @type source: IP address in dotted quad notation - @param raise_on_no_answer: raise NoAnswer if there's no answer - (defaults is True). - @type raise_on_no_answer: bool - @param source_port: The port from which to send the message. - The default is 0. - @type source_port: int - @rtype: dns.resolver.Answer instance - @raises Timeout: no answers could be found in the specified lifetime - @raises NXDOMAIN: the query name does not exist - @raises YXDOMAIN: the query name is too long after DNAME substitution - @raises NoAnswer: the response did not contain an answer and - raise_on_no_answer is True. - @raises NoNameservers: no non-broken nameservers are available to - answer the question.""" - - if isinstance(qname, string_types): - qname = dns.name.from_text(qname, None) - if isinstance(rdtype, string_types): - rdtype = dns.rdatatype.from_text(rdtype) - if dns.rdatatype.is_metatype(rdtype): - raise NoMetaqueries - if isinstance(rdclass, string_types): - rdclass = dns.rdataclass.from_text(rdclass) - if dns.rdataclass.is_metaclass(rdclass): - raise NoMetaqueries + def _get_qnames_to_try(self, qname, search): + # 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: - if len(qname) > 1: - qnames_to_try.append(qname.concatenate(dns.name.root)) - if self.search: - for suffix in self.search: - qnames_to_try.append(qname.concatenate(suffix)) + 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(qname.concatenate(self.domain)) - all_nxdomain = True - nxdomain_responses = {} - start = time.time() - _qname = None # make pylint happy - for _qname in qnames_to_try: - if self.cache: - answer = self.cache.get((_qname, rdtype, rdclass)) - if answer is not None: - if answer.rrset is None and raise_on_no_answer: - raise NoAnswer(response=answer.response) - else: - return answer - request = dns.message.make_query(_qname, rdtype, rdclass) - if self.keyname is not None: - request.use_tsig(self.keyring, self.keyname, - algorithm=self.keyalgorithm) - request.use_edns(self.edns, self.ednsflags, self.payload) - if self.flags is not None: - request.flags = self.flags - response = None - # - # make a copy of the servers list so we can alter it later. - # - nameservers = self.nameservers[:] - errors = [] - if self.rotate: - random.shuffle(nameservers) - backoff = 0.10 - while response is None: - if len(nameservers) == 0: - raise NoNameservers(request=request, errors=errors) - for nameserver in nameservers[:]: - timeout = self._compute_timeout(start) - port = self.nameserver_ports.get(nameserver, self.port) - try: - tcp_attempt = tcp - if tcp: - response = dns.query.tcp(request, nameserver, - timeout, port, - source=source, - source_port=source_port) - else: - response = dns.query.udp(request, nameserver, - timeout, port, - source=source, - source_port=source_port) - if response.flags & dns.flags.TC: - # Response truncated; retry with TCP. - tcp_attempt = True - timeout = self._compute_timeout(start) - response = \ - dns.query.tcp(request, nameserver, - timeout, port, - source=source, - source_port=source_port) - except (socket.error, dns.exception.Timeout) as ex: - # - # Communication failure or timeout. Go to the - # next server - # - errors.append((nameserver, tcp_attempt, port, ex, - response)) - response = None - continue - except dns.query.UnexpectedSource as ex: - # - # Who knows? Keep going. - # - errors.append((nameserver, tcp_attempt, port, ex, - response)) - response = None - continue - except dns.exception.FormError as ex: - # - # We don't understand what this server is - # saying. Take it out of the mix and - # continue. - # - nameservers.remove(nameserver) - errors.append((nameserver, tcp_attempt, port, ex, - response)) - response = None - continue - except EOFError as ex: - # - # We're using TCP and they hung up on us. - # Probably they don't support TCP (though - # they're supposed to!). Take it out of the - # mix and continue. - # - nameservers.remove(nameserver) - errors.append((nameserver, tcp_attempt, port, ex, - response)) - response = None - continue - rcode = response.rcode() - if rcode == dns.rcode.YXDOMAIN: - ex = YXDOMAIN() - errors.append((nameserver, tcp_attempt, port, ex, - response)) - raise ex - if rcode == dns.rcode.NOERROR or \ - rcode == dns.rcode.NXDOMAIN: - break - # - # We got a response, but we're not happy with the - # rcode in it. Remove the server from the mix if - # the rcode isn't SERVFAIL. - # - if rcode != dns.rcode.SERVFAIL or not self.retry_servfail: - nameservers.remove(nameserver) - errors.append((nameserver, tcp_attempt, port, - dns.rcode.to_text(rcode), response)) - response = None - if response is not None: - break - # - # All nameservers failed! - # - if len(nameservers) > 0: - # - # But we still have servers to try. Sleep a bit - # so we don't pound them! - # - timeout = self._compute_timeout(start) - sleep_time = min(timeout, backoff) - backoff *= 2 - time.sleep(sleep_time) - if response.rcode() == dns.rcode.NXDOMAIN: - nxdomain_responses[_qname] = response - continue - all_nxdomain = False - break - if all_nxdomain: - raise NXDOMAIN(qnames=qnames_to_try, responses=nxdomain_responses) - answer = Answer(_qname, rdtype, rdclass, response, - raise_on_no_answer) - if self.cache: - self.cache.put((_qname, rdtype, rdclass), answer) - return answer + qnames_to_try.append(abs_qname) + return qnames_to_try def use_tsig(self, keyring, keyname=None, algorithm=dns.tsig.default_algorithm): - """Add a TSIG signature to the query. + """Add a TSIG signature to each query. + + The parameters are passed to ``dns.message.Message.use_tsig()``; + see its documentation for details. + """ - @param keyring: The TSIG keyring to use; defaults to None. - @type keyring: dict - @param keyname: The name of the TSIG key to use; defaults to None. - The key must be defined in the keyring. If a keyring is specified - but a keyname is not, then 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 keyring is used, unless - they know the keyring contains only one key. - @param algorithm: The TSIG key algorithm to use. The default - is dns.tsig.default_algorithm. - @type algorithm: string""" self.keyring = keyring - if keyname is None: - self.keyname = list(self.keyring.keys())[0] - else: - self.keyname = keyname + self.keyname = keyname self.keyalgorithm = algorithm - def use_edns(self, edns, ednsflags, payload): - """Configure Edns. + def use_edns(self, edns=0, ednsflags=0, + payload=dns.message.DEFAULT_EDNS_PAYLOAD): + """Configure EDNS behavior. - @param edns: The EDNS level to use. The default is -1, no Edns. - @type edns: int - @param ednsflags: The EDNS flags - @type ednsflags: int - @param payload: The EDNS payload size. The default is 0. - @type payload: int""" + *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". - if edns is None: + *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. + """ + + 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 def set_flags(self, flags): - """Overrides the default flags with your own + """Overrides the default flags with your own. + + *flags*, an ``int``, the message flags to use. + """ - @param flags: The flags to overwrite the default with - @type flags: int""" self.flags = flags + @property + def nameservers(self): + return self._nameservers + + @nameservers.setter + def nameservers(self, nameservers): + """ + *nameservers*, a ``list`` of nameservers. + + Raises ``ValueError`` if *nameservers* is anything other than a + ``list``. + """ + if isinstance(nameservers, list): + for nameserver in nameservers: + if not dns.inet.is_address(nameserver): + try: + if urlparse(nameserver).scheme != 'https': + raise NotImplementedError + except Exception: + raise ValueError(f'nameserver {nameserver} is not an ' + 'IP address or valid https URL') + self._nameservers = nameservers + else: + raise ValueError('nameservers must be a list' + ' (not a {})'.format(type(nameservers))) + + +class Resolver(BaseResolver): + """DNS stub resolver.""" + + def resolve(self, qname, rdtype=dns.rdatatype.A, rdclass=dns.rdataclass.IN, + tcp=False, source=None, raise_on_no_answer=True, source_port=0, + lifetime=None, search=None): # 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 + done = False + while not done: + (nameserver, port, tcp, backoff) = resolution.next_nameserver() + if backoff: + time.sleep(backoff) + timeout = self._compute_timeout(start, lifetime, + resolution.errors) + try: + if dns.inet.is_address(nameserver): + if tcp: + response = dns.query.tcp(request, nameserver, + timeout=timeout, + port=port, + source=source, + source_port=source_port) + else: + response = dns.query.udp(request, + nameserver, + timeout=timeout, + port=port, + source=source, + source_port=source_port, + raise_on_truncation=True) + else: + response = dns.query.https(request, nameserver, + timeout=timeout) + 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, rdtype=dns.rdatatype.A, rdclass=dns.rdataclass.IN, + tcp=False, source=None, raise_on_no_answer=True, source_port=0, + lifetime=None): # pragma: no cover + """Query nameservers to find the answer to the question. + + This method calls resolve() with ``search=True``, and is + provided for backwards compatbility 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, *args, **kwargs): + """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. + """ + + return self.resolve(dns.reversename.from_address(ipaddr), + rdtype=dns.rdatatype.PTR, + rdclass=dns.rdataclass.IN, + *args, **kwargs) + + # pylint: disable=redefined-outer-name + + def canonical_name(self, 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 + + +#: The default resolver. default_resolver = None @@ -1113,52 +1170,138 @@ def get_default_resolver(): def reset_default_resolver(): """Re-initialize default resolver. - resolv.conf will be re-read immediatelly. + 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 query(qname, rdtype=dns.rdatatype.A, rdclass=dns.rdataclass.IN, - tcp=False, source=None, raise_on_no_answer=True, - source_port=0): +def resolve(qname, rdtype=dns.rdatatype.A, rdclass=dns.rdataclass.IN, + tcp=False, source=None, raise_on_no_answer=True, + source_port=0, lifetime=None, search=None): """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: L{dns.resolver.Resolver.query} for more information on the - parameters.""" - return get_default_resolver().query(qname, rdtype, rdclass, tcp, source, - raise_on_no_answer, source_port) + + 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, rdtype=dns.rdatatype.A, rdclass=dns.rdataclass.IN, + tcp=False, source=None, raise_on_no_answer=True, + source_port=0, lifetime=None): # pragma: no cover + """Query nameservers to find the answer to the question. + + This method calls resolve() with ``search=True``, and is + provided for backwards compatbility 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 zone_for_name(name, rdclass=dns.rdataclass.IN, tcp=False, resolver=None): +def resolve_address(ipaddr, *args, **kwargs): + """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 canonical_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 zone_for_name(name, rdclass=dns.rdataclass.IN, tcp=False, resolver=None, + lifetime=None): """Find the name of the zone which contains the specified name. - @param name: the query name - @type name: absolute dns.name.Name object or string - @param rdclass: The query class - @type rdclass: int - @param tcp: use TCP to make the query (default is False). - @type tcp: bool - @param resolver: the resolver to use - @type resolver: dns.resolver.Resolver object or None - @rtype: dns.name.Name""" + *name*, an absolute ``dns.name.Name`` or ``str``, the query name. - if isinstance(name, string_types): + *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 alotted 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() + if lifetime is not None: + expiration = start + lifetime + else: + expiration = None while 1: try: - answer = resolver.query(name, dns.rdatatype.SOA, rdclass, tcp) + if expiration: + rlifetime = expiration - time.time() + if rlifetime <= 0: + rlifetime = 0 + else: + rlifetime = None + answer = resolver.resolve(name, dns.rdatatype.SOA, rdclass, tcp, + lifetime=rlifetime) 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): - pass + 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: @@ -1185,63 +1328,72 @@ _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: - raise NotImplementedError + # 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) + raise socket.gaierror(socket.EAI_NONAME, 'Name or service not known') v6addrs = [] v4addrs = [] - canonical_name = None + 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: - # Is host None or a V6 address literal? - if host is None: - canonical_name = 'localhost' - if flags & socket.AI_PASSIVE != 0: - v6addrs.append('::') - v4addrs.append('0.0.0.0') - else: - v6addrs.append('::1') - v4addrs.append('127.0.0.1') - else: - parts = host.split('%') - if len(parts) == 2: - ahost = parts[0] - else: - ahost = host - addr = dns.ipv6.inet_aton(ahost) - v6addrs.append(host) - canonical_name = host + # 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: - try: - # Is it a V4 address literal? - addr = dns.ipv4.inet_aton(host) - v4addrs.append(host) - canonical_name = host - except Exception: - if flags & socket.AI_NUMERICHOST == 0: - try: - if family == socket.AF_INET6 or family == socket.AF_UNSPEC: - v6 = _resolver.query(host, dns.rdatatype.AAAA, - raise_on_no_answer=False) - # Note that setting host ensures we query the same name - # for A as we did for AAAA. - host = v6.qname - canonical_name = v6.canonical_name.to_text(True) - if v6.rrset is not None: - for rdata in v6.rrset: - v6addrs.append(rdata.address) - if family == socket.AF_INET or family == socket.AF_UNSPEC: - v4 = _resolver.query(host, dns.rdatatype.A, - raise_on_no_answer=False) - host = v4.qname - canonical_name = v4.canonical_name.to_text(True) - if v4.rrset is not None: - for rdata in v4.rrset: - v4addrs.append(rdata.address) - except dns.resolver.NXDOMAIN: - raise socket.gaierror(socket.EAI_NONAME) - except: - raise socket.gaierror(socket.EAI_SYSTEM) + pass + # Something needs resolution! + try: + if family == socket.AF_INET6 or family == socket.AF_UNSPEC: + v6 = _resolver.resolve(host, dns.rdatatype.AAAA, + raise_on_no_answer=False) + # Note that setting host ensures we query the same name + # for A as we did for AAAA. + host = v6.qname + canonical_name = v6.canonical_name.to_text(True) + if v6.rrset is not None: + for rdata in v6.rrset: + v6addrs.append(rdata.address) + if family == socket.AF_INET or family == socket.AF_UNSPEC: + v4 = _resolver.resolve(host, dns.rdatatype.A, + raise_on_no_answer=False) + host = v4.qname + canonical_name = v4.canonical_name.to_text(True) + if v4.rrset is not None: + for rdata in v4.rrset: + v4addrs.append(rdata.address) + 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? @@ -1256,7 +1408,7 @@ def _getaddrinfo(host=None, service=None, family=socket.AF_UNSPEC, socktype=0, except Exception: pass if port is None: - raise socket.gaierror(socket.EAI_NONAME) + raise socket.gaierror(socket.EAI_NONAME, 'Name or service not known') tuples = [] if socktype == 0: socktypes = [socket.SOCK_DGRAM, socket.SOCK_STREAM] @@ -1279,7 +1431,7 @@ def _getaddrinfo(host=None, service=None, family=socket.AF_UNSPEC, socktype=0, tuples.append((socket.AF_INET, socktype, proto, cname, (addr, port))) if len(tuples) == 0: - raise socket.gaierror(socket.EAI_NONAME) + raise socket.gaierror(socket.EAI_NONAME, 'Name or service not known') return tuples @@ -1304,11 +1456,12 @@ def _getnameinfo(sockaddr, flags=0): qname = dns.reversename.from_address(addr) if flags & socket.NI_NUMERICHOST == 0: try: - answer = _resolver.query(qname, 'PTR') + 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) + raise socket.gaierror(socket.EAI_NONAME, + 'Name or service not known') hostname = addr if scope is not None: hostname += '%' + str(scope) @@ -1327,9 +1480,12 @@ def _getfqdn(name=None): if name is None: name = socket.gethostname() try: - return _getnameinfo(_getaddrinfo(name, 80)[0][4])[0] + (name, _, _) = _gethostbyaddr(name) + # Python's version checks aliases too, but our gethostbyname + # ignores them, so we do so here as well. except Exception: - return name + pass + return name def _gethostbyname(name): @@ -1354,16 +1510,28 @@ def _gethostbyaddr(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, port) = _getnameinfo(sockaddr, socket.NI_NAMEREQD) + (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: - addresses.append(item[4][0]) + 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) @@ -1379,9 +1547,9 @@ def override_system_resolver(resolver=None): The resolver to use may be specified; if it's not, the default resolver will be used. - @param resolver: the resolver to use - @type resolver: dns.resolver.Resolver object or None + resolver, a ``dns.resolver.Resolver`` or ``None``, the resolver to use. """ + if resolver is None: resolver = get_default_resolver() global _resolver @@ -1395,8 +1563,8 @@ def override_system_resolver(resolver=None): def restore_system_resolver(): - """Undo the effects of override_system_resolver(). - """ + """Undo the effects of prior override_system_resolver().""" + global _resolver _resolver = None socket.getaddrinfo = _original_getaddrinfo diff --git a/libs/dns/resolver.pyi b/libs/dns/resolver.pyi new file mode 100644 index 000000000..6da21f125 --- /dev/null +++ b/libs/dns/resolver.pyi @@ -0,0 +1,61 @@ +from typing import Union, Optional, List, Any, Dict +from . import exception, rdataclass, name, rdatatype + +import socket +_gethostbyname = socket.gethostbyname + +class NXDOMAIN(exception.DNSException): ... +class YXDOMAIN(exception.DNSException): ... +class NoAnswer(exception.DNSException): ... +class NoNameservers(exception.DNSException): ... +class NotAbsolute(exception.DNSException): ... +class NoRootSOA(exception.DNSException): ... +class NoMetaqueries(exception.DNSException): ... +class NoResolverConfiguration(exception.DNSException): ... +Timeout = exception.Timeout + +def resolve(qname : str, rdtype : Union[int,str] = 0, + rdclass : Union[int,str] = 0, + tcp=False, source=None, raise_on_no_answer=True, + source_port=0, lifetime : Optional[float]=None, + search : Optional[bool]=None): + ... +def query(qname : str, rdtype : Union[int,str] = 0, + rdclass : Union[int,str] = 0, + tcp=False, source=None, raise_on_no_answer=True, + source_port=0, lifetime : Optional[float]=None): + ... +def resolve_address(self, ipaddr: str, *args: Any, **kwargs: Optional[Dict]): + ... +class LRUCache: + def __init__(self, max_size=1000): + ... + def get(self, key): + ... + def put(self, key, val): + ... +class Answer: + def __init__(self, qname, rdtype, rdclass, response, + raise_on_no_answer=True): + ... +def zone_for_name(name, rdclass : int = rdataclass.IN, tcp=False, + resolver : Optional[Resolver] = None): + ... + +class Resolver: + def __init__(self, filename : Optional[str] = '/etc/resolv.conf', + configure : Optional[bool] = True): + self.nameservers : List[str] + def resolve(self, qname : str, rdtype : Union[int,str] = rdatatype.A, + rdclass : Union[int,str] = rdataclass.IN, + tcp : bool = False, source : Optional[str] = None, + raise_on_no_answer=True, source_port : int = 0, + lifetime : Optional[float]=None, + search : Optional[bool]=None): + ... + def query(self, qname : str, rdtype : Union[int,str] = rdatatype.A, + rdclass : Union[int,str] = rdataclass.IN, + tcp : bool = False, source : Optional[str] = None, + raise_on_no_answer=True, source_port : int = 0, + lifetime : Optional[float]=None): + ... diff --git a/libs/dns/reversename.py b/libs/dns/reversename.py index 9ea9395a8..e0beb03df 100644 --- a/libs/dns/reversename.py +++ b/libs/dns/reversename.py @@ -1,4 +1,6 @@ -# Copyright (C) 2006, 2007, 2009-2011 Nominum, Inc. +# 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, @@ -13,16 +15,9 @@ # 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. - -@var ipv4_reverse_domain: The DNS IPv4 reverse-map domain, in-addr.arpa. -@type ipv4_reverse_domain: dns.name.Name object -@var ipv6_reverse_domain: The DNS IPv6 reverse-map domain, ip6.arpa. -@type ipv6_reverse_domain: dns.name.Name object -""" +"""DNS Reverse Map Names.""" import binascii -import sys import dns.name import dns.ipv6 @@ -32,58 +27,74 @@ ipv4_reverse_domain = dns.name.from_text('in-addr.arpa.') ipv6_reverse_domain = dns.name.from_text('ip6.arpa.') -def from_address(text): +def from_address(text, v4_origin=ipv4_reverse_domain, + v6_origin=ipv6_reverse_domain): """Convert an IPv4 or IPv6 address in textual form into a Name object whose value is the reverse-map domain name of the address. - @param text: an IPv4 or IPv6 address in textual form (e.g. '127.0.0.1', - '::1') - @type text: str - @rtype: dns.name.Name object + + *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): - if sys.version_info >= (3,): - parts = ['%d' % byte for byte in v6[12:]] - else: - parts = ['%d' % ord(byte) for byte in v6[12:]] - origin = ipv4_reverse_domain + parts = ['%d' % byte for byte in v6[12:]] + origin = v4_origin else: parts = [x for x in str(binascii.hexlify(v6).decode())] - origin = ipv6_reverse_domain + origin = v6_origin except Exception: parts = ['%d' % - byte for byte in bytearray(dns.ipv4.inet_aton(text))] - origin = ipv4_reverse_domain - parts.reverse() - return dns.name.from_text('.'.join(parts), origin=origin) + 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): +def to_address(name, v4_origin=ipv4_reverse_domain, + v6_origin=ipv6_reverse_domain): """Convert a reverse map domain name into textual address form. - @param name: an IPv4 or IPv6 address in reverse-map form. - @type name: dns.name.Name object - @rtype: str + + *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(ipv4_reverse_domain): - name = name.relativize(ipv4_reverse_domain) - labels = list(name.labels) - labels.reverse() - text = b'.'.join(labels) - # run through inet_aton() to check syntax and make pretty. + + 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(ipv6_reverse_domain): - name = name.relativize(ipv6_reverse_domain) - labels = list(name.labels) - labels.reverse() + elif name.is_subdomain(v6_origin): + name = name.relativize(v6_origin) + labels = list(reversed(name.labels)) parts = [] - i = 0 - l = len(labels) - while i < l: + for i in range(0, len(labels), 4): parts.append(b''.join(labels[i:i + 4])) - i += 4 text = b':'.join(parts) - # run through inet_aton() to check syntax and make pretty. + # 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/libs/dns/reversename.pyi b/libs/dns/reversename.pyi new file mode 100644 index 000000000..97f072ea8 --- /dev/null +++ b/libs/dns/reversename.pyi @@ -0,0 +1,6 @@ +from . import name +def from_address(text : str) -> name.Name: + ... + +def to_address(name : name.Name) -> str: + ... diff --git a/libs/dns/rrset.py b/libs/dns/rrset.py index d0f8f9377..a71d45737 100644 --- a/libs/dns/rrset.py +++ b/libs/dns/rrset.py @@ -1,4 +1,6 @@ -# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc. +# 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, @@ -20,7 +22,6 @@ import dns.name import dns.rdataset import dns.rdataclass import dns.renderer -from ._compat import string_types class RRset(dns.rdataset.Rdataset): @@ -40,12 +41,12 @@ class RRset(dns.rdataset.Rdataset): deleting=None): """Create a new RRset.""" - super(RRset, self).__init__(rdclass, rdtype, covers) + super().__init__(rdclass, rdtype, covers) self.name = name self.deleting = deleting def _clone(self): - obj = super(RRset, self)._clone() + obj = super()._clone() obj.name = self.name obj.deleting = self.deleting return obj @@ -61,82 +62,123 @@ class RRset(dns.rdataset.Rdataset): dtext = '' return '' + dns.rdatatype.to_text(self.rdtype) + ctext + dtext + \ + ' RRset: ' + self._rdata_repr() + '>' def __str__(self): return self.to_text() def __eq__(self, other): - """Two RRsets are equal if they have the same name and the same - rdataset - - @rtype: bool""" - if not isinstance(other, RRset): + if isinstance(other, RRset): + if self.name != other.name: + return False + elif not isinstance(other, dns.rdataset.Rdataset): return False - if self.name != other.name: - return False - return super(RRset, self).__eq__(other) + return super().__eq__(other) - def match(self, name, rdclass, rdtype, covers, deleting=None): - """Returns True if this rrset matches the specified class, type, - covers, and deletion state.""" + def match(self, *args, **kwargs): + """Does this rrset match the specified attributes? - if not super(RRset, self).match(rdclass, rdtype, covers): + 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) + else: + return super().match(*args, **kwargs) + + def full_match(self, name, rdclass, rdtype, covers, + deleting=None): + """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 - def to_text(self, origin=None, relativize=True, **kw): - """Convert the RRset into DNS master file format. + # pylint: disable=arguments-differ - @see: L{dns.name.Name.choose_relativity} for more information - on how I{origin} and I{relativize} determine the way names + def to_text(self, origin=None, relativize=True, **kw): + """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. + ``to_text()`` method. - @param origin: The origin for relative names, or None. - @type origin: dns.name.Name object - @param relativize: True if names should names be relativized - @type relativize: bool""" + *origin*, a ``dns.name.Name`` or ``None``, the origin for relative + names. - return super(RRset, self).to_text(self.name, origin, relativize, - self.deleting, **kw) + *relativize*, a ``bool``. If ``True``, names will be relativized + to *origin*. + """ - def to_wire(self, file, compress=None, origin=None, **kw): - """Convert the RRset to wire format.""" + return super().to_text(self.name, origin, relativize, + self.deleting, **kw) - return super(RRset, self).to_wire(self.name, file, compress, origin, - self.deleting, **kw) + def to_wire(self, file, compress=None, origin=None, + **kw): + """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) + + # pylint: enable=arguments-differ def to_rdataset(self): """Convert an RRset into an Rdataset. - @rtype: dns.rdataset.Rdataset object + Returns a ``dns.rdataset.Rdataset``. """ return dns.rdataset.from_rdata_list(self.ttl, list(self)) def from_text_list(name, ttl, rdclass, rdtype, text_rdatas, - idna_codec=None): + idna_codec=None, origin=None, relativize=True, + relativize_to=None): """Create an RRset with the specified name, TTL, class, and type, and with the specified list of rdatas in text format. - @rtype: dns.rrset.RRset object + *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, string_types): + if isinstance(name, str): name = dns.name.from_text(name, None, idna_codec=idna_codec) - if isinstance(rdclass, string_types): - rdclass = dns.rdataclass.from_text(rdclass) - if isinstance(rdtype, string_types): - rdtype = dns.rdatatype.from_text(rdtype) + 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) + rd = dns.rdata.from_text(r.rdclass, r.rdtype, t, origin, relativize, + relativize_to, idna_codec) r.add(rd) return r @@ -145,7 +187,7 @@ def from_text(name, ttl, rdclass, rdtype, *text_rdatas): """Create an RRset with the specified name, TTL, class, and type and with the specified rdatas in text format. - @rtype: dns.rrset.RRset object + Returns a ``dns.rrset.RRset`` object. """ return from_text_list(name, ttl, rdclass, rdtype, text_rdatas) @@ -155,10 +197,15 @@ def from_rdata_list(name, ttl, rdatas, idna_codec=None): """Create an RRset with the specified name and TTL, and with the specified list of rdata objects. - @rtype: dns.rrset.RRset object + *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, string_types): + if isinstance(name, str): name = dns.name.from_text(name, None, idna_codec=idna_codec) if len(rdatas) == 0: @@ -176,7 +223,7 @@ def from_rdata(name, ttl, *rdatas): """Create an RRset with the specified name and TTL, and with the specified rdata objects. - @rtype: dns.rrset.RRset object + Returns a ``dns.rrset.RRset`` object. """ return from_rdata_list(name, ttl, rdatas) diff --git a/libs/dns/rrset.pyi b/libs/dns/rrset.pyi new file mode 100644 index 000000000..0a81a2a06 --- /dev/null +++ b/libs/dns/rrset.pyi @@ -0,0 +1,10 @@ +from typing import List, Optional +from . import rdataset, rdatatype + +class RRset(rdataset.Rdataset): + def __init__(self, name, rdclass : int , rdtype : int, covers=rdatatype.NONE, + deleting : Optional[int] =None) -> None: + self.name = name + self.deleting = deleting +def from_text(name : str, ttl : int, rdclass : str, rdtype : str, *text_rdatas : str): + ... diff --git a/libs/dns/serial.py b/libs/dns/serial.py new file mode 100644 index 000000000..b0474151c --- /dev/null +++ b/libs/dns/serial.py @@ -0,0 +1,117 @@ +# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license + +"""Serial Number Arthimetic from RFC 1982""" + +class Serial: + def __init__(self, value, bits=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/libs/dns/set.py b/libs/dns/set.py index ef7fd2955..1fd4d0ae7 100644 --- a/libs/dns/set.py +++ b/libs/dns/set.py @@ -1,4 +1,6 @@ -# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc. +# 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, @@ -13,52 +15,61 @@ # ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT # OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -"""A simple Set class.""" +import itertools +import sys +if sys.version_info >= (3, 7): + odict = dict +else: + from collections import OrderedDict as odict # pragma: no cover -class Set(object): +class Set: """A simple set class. - Sets are not in Python until 2.3, and rdata are not immutable so - we cannot use sets.Set anyway. This class implements subset of - the 2.3 Set interface using a list as the container. - - @ivar items: A list of the items which are in the set - @type items: list""" + 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. - @param items: the initial set of items - @type items: any iterable or None + *items*, an iterable or ``None``, the initial set of items. """ - self.items = [] + self.items = odict() if items is not None: for item in items: self.add(item) def __repr__(self): - return "dns.simpleset.Set(%s)" % repr(self.items) + return "dns.set.Set(%s)" % repr(list(self.items.keys())) def add(self, item): - """Add an item to the set.""" + """Add an item to the set. + """ + if item not in self.items: - self.items.append(item) + self.items[item] = None def remove(self, item): - """Remove an item from the set.""" - self.items.remove(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.""" - try: - self.items.remove(item) - except ValueError: - pass + """Remove an item from the set if present. + """ + + self.items.pop(item, None) def _clone(self): """Make a (shallow) copy of the set. @@ -73,25 +84,32 @@ class Set(object): subclasses. """ - cls = self.__class__ + if hasattr(self, '_clone_class'): + cls = self._clone_class + else: + cls = self.__class__ obj = cls.__new__(cls) - obj.items = list(self.items) + obj.items = odict() + obj.items.update(self.items) return obj def __copy__(self): - """Make a (shallow) copy of the set.""" + """Make a (shallow) copy of the set. + """ + return self._clone() def copy(self): - """Make a (shallow) copy of the set.""" + """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. - @param other: the collection of items with which to update the set - @type other: Set object """ + if not isinstance(other, Set): raise ValueError('other must be a Set instance') if self is other: @@ -102,9 +120,8 @@ class Set(object): def intersection_update(self, other): """Update the set, removing any elements from other which are not in both sets. - @param other: the collection of items with which to update the set - @type other: Set object """ + if not isinstance(other, Set): raise ValueError('other must be a Set instance') if self is other: @@ -113,28 +130,25 @@ class Set(object): # the list without breaking the iterator. for item in list(self.items): if item not in other.items: - self.items.remove(item) + del self.items[item] def difference_update(self, other): """Update the set, removing any elements from other which are in the set. - @param other: the collection of items with which to update the set - @type other: Set object """ + if not isinstance(other, Set): raise ValueError('other must be a Set instance') if self is other: - self.items = [] + self.items.clear() else: for item in other.items: self.discard(item) def union(self, other): - """Return a new set which is the union of I{self} and I{other}. + """Return a new set which is the union of ``self`` and ``other``. - @param other: the other set - @type other: Set object - @rtype: the same type as I{self} + Returns the same Set type as this set. """ obj = self._clone() @@ -142,11 +156,10 @@ class Set(object): return obj def intersection(self, other): - """Return a new set which is the intersection of I{self} and I{other}. + """Return a new set which is the intersection of ``self`` and + ``other``. - @param other: the other set - @type other: Set object - @rtype: the same type as I{self} + Returns the same Set type as this set. """ obj = self._clone() @@ -154,12 +167,10 @@ class Set(object): return obj def difference(self, other): - """Return a new set which I{self} - I{other}, i.e. the items - in I{self} which are not also in I{other}. + """Return a new set which ``self`` - ``other``, i.e. the items + in ``self`` which are not also in ``other``. - @param other: the other set - @type other: Set object - @rtype: the same type as I{self} + Returns the same Set type as this set. """ obj = self._clone() @@ -197,25 +208,26 @@ class Set(object): def update(self, other): """Update the set, adding any elements from other which are not already in the set. - @param other: the collection of items with which to update the set - @type other: any iterable type""" + + *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 = [] + self.items.clear() def __eq__(self, other): - # Yes, this is inefficient but the sets we're dealing with are - # usually quite small, so it shouldn't hurt too much. - for item in self.items: - if item not in other.items: + if odict == dict: + return self.items == other.items + else: + # We don't want an ordered comparison. + if len(self.items) != len(other.items): return False - for item in other.items: - if item not in self.items: - return False - return True + return all(elt in other.items for elt in self.items) def __ne__(self, other): return not self.__eq__(other) @@ -227,15 +239,22 @@ class Set(object): return iter(self.items) def __getitem__(self, i): - return self.items[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): - del self.items[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 I{self} a subset of I{other}? + """Is this set a subset of *other*? - @rtype: bool + Returns a ``bool``. """ if not isinstance(other, Set): @@ -246,9 +265,9 @@ class Set(object): return True def issuperset(self, other): - """Is I{self} a superset of I{other}? + """Is this set a superset of *other*? - @rtype: bool + Returns a ``bool``. """ if not isinstance(other, Set): diff --git a/libs/dns/tokenizer.py b/libs/dns/tokenizer.py index 04b982549..7ddc7a968 100644 --- a/libs/dns/tokenizer.py +++ b/libs/dns/tokenizer.py @@ -1,4 +1,6 @@ -# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc. +# 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, @@ -13,26 +15,17 @@ # ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT # OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -"""Tokenize DNS master file format""" +"""Tokenize DNS zone file format""" -from io import StringIO +import io import sys import dns.exception import dns.name import dns.ttl -from ._compat import long, text_type, binary_type -_DELIMITERS = { - ' ': True, - '\t': True, - '\n': True, - ';': True, - '(': True, - ')': True, - '"': True} - -_QUOTING_DELIMITERS = {'"': True} +_DELIMITERS = {' ', '\t', '\n', ';', '(', ')', '"'} +_QUOTING_DELIMITERS = {'"'} EOF = 0 EOL = 1 @@ -44,35 +37,24 @@ DELIMITER = 6 class UngetBufferFull(dns.exception.DNSException): - """An attempt was made to unget a token when the unget buffer was full.""" -class Token(object): +class Token: + """A DNS zone file format token. - """A DNS master file format token. - - @ivar ttype: The token type - @type ttype: int - @ivar value: The token value - @type value: string - @ivar has_escape: Does the token value contain escapes? - @type has_escape: bool + ttype: The token type + value: The token value + has_escape: Does the token value contain escapes? """ - def __init__(self, ttype, value='', has_escape=False): - """Initialize a token instance. + def __init__(self, ttype, value='', has_escape=False, comment=None): + """Initialize a token instance.""" - @param ttype: The token type - @type ttype: int - @param value: The token value - @type value: string - @param has_escape: Does the token value contain escapes? - @type has_escape: bool - """ self.ttype = ttype self.value = value self.has_escape = has_escape + self.comment = comment def is_eof(self): return self.ttype == EOF @@ -92,7 +74,7 @@ class Token(object): def is_comment(self): return self.ttype == COMMENT - def is_delimiter(self): + def is_delimiter(self): # pragma: no cover (we don't return delimiters yet) return self.ttype == DELIMITER def is_eol_or_eof(self): @@ -123,7 +105,7 @@ class Token(object): c = self.value[i] i += 1 if c == '\\': - if i >= l: + if i >= l: # pragma: no cover (can't happen via get()) raise dns.exception.UnexpectedEnd c = self.value[i] i += 1 @@ -138,76 +120,130 @@ class Token(object): i += 1 if not (c2.isdigit() and c3.isdigit()): raise dns.exception.SyntaxError - c = chr(int(c) * 100 + int(c2) * 10 + int(c3)) + 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) - # compatibility for old-style tuple tokens - - def __len__(self): - return 2 - - def __iter__(self): - return iter((self.ttype, self.value)) - - def __getitem__(self, i): - if i == 0: - return self.ttype - elif i == 1: - return self.value - else: - raise IndexError + def unescape_to_bytes(self): + # 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(object): +class Tokenizer: + """A DNS zone file format tokenizer. - """A DNS master 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. - A token is a (type, value) tuple, where I{type} is an int, and - I{value} is a string. The valid types are EOF, EOL, WHITESPACE, - IDENTIFIER, QUOTED_STRING, COMMENT, and DELIMITER. + file: The file to tokenize - @ivar file: The file to tokenize - @type file: file - @ivar ungotten_char: The most recently ungotten character, or None. - @type ungotten_char: string - @ivar ungotten_token: The most recently ungotten token, or None. - @type ungotten_token: (int, string) token tuple - @ivar multiline: The current multiline level. This value is increased + 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. - @type multiline: int - @ivar quoting: This variable is true if the tokenizer is currently + + quoting: This variable is true if the tokenizer is currently reading a quoted string. - @type quoting: bool - @ivar eof: This variable is true if the tokenizer has encountered EOF. - @type eof: bool - @ivar delimiters: The current delimiter dictionary. - @type delimiters: dict - @ivar line_number: The current line number - @type line_number: int - @ivar filename: A filename that will be returned by the L{where} method. - @type filename: 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=sys.stdin, filename=None): + def __init__(self, f=sys.stdin, filename=None, idna_codec=None): """Initialize a tokenizer instance. - @param f: The file to tokenize. The default is sys.stdin. + 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. - @type f: file or string - @param filename: the name of the filename that the L{where} method + + filename: the name of the filename that the where() method will return. - @type filename: string + + idna_codec: A dns.name.IDNACodec, specifies the IDNA + encoder/decoder. If None, the default IDNA 2003 + encoder/decoder is used. """ - if isinstance(f, text_type): - f = StringIO(f) + if isinstance(f, str): + f = io.StringIO(f) if filename is None: filename = '' - elif isinstance(f, binary_type): - f = StringIO(f.decode()) + elif isinstance(f, bytes): + f = io.StringIO(f.decode()) if filename is None: filename = '' else: @@ -225,10 +261,12 @@ class Tokenizer(object): self.delimiters = _DELIMITERS self.line_number = 1 self.filename = filename + if idna_codec is None: + idna_codec = dns.name.IDNA_2003 + self.idna_codec = idna_codec def _get_char(self): """Read a character from input. - @rtype: string """ if self.ungotten_char is None: @@ -248,7 +286,7 @@ class Tokenizer(object): def where(self): """Return the current location in the input. - @rtype: (string, int) tuple. The first item is the filename of + Returns a (string, int) tuple. The first item is the filename of the input, the second is the current line number. """ @@ -261,13 +299,13 @@ class Tokenizer(object): an error to try to unget a character when the unget buffer is not empty. - @param c: the character to unget - @type c: string - @raises UngetBufferFull: there is already an ungotten char + c: the character to unget + raises UngetBufferFull: there is already an ungotten char """ if self.ungotten_char is not None: - raise UngetBufferFull + # this should never happen! + raise UngetBufferFull # pragma: no cover self.ungotten_char = c def skip_whitespace(self): @@ -278,7 +316,7 @@ class Tokenizer(object): If the tokenizer is in multiline mode, then newlines are whitespace. - @rtype: int + Returns the number of characters skipped. """ skipped = 0 @@ -293,15 +331,17 @@ class Tokenizer(object): def get(self, want_leading=False, want_comment=False): """Get the next token. - @param want_leading: If True, return a WHITESPACE token if the + want_leading: If True, return a WHITESPACE token if the first character read is whitespace. The default is False. - @type want_leading: bool - @param want_comment: If True, return a COMMENT token if the + + want_comment: If True, return a COMMENT token if the first token read is a comment. The default is False. - @type want_comment: bool - @rtype: Token object - @raises dns.exception.UnexpectedEnd: input ended prematurely - @raises dns.exception.SyntaxError: input was badly formed + + 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: @@ -363,13 +403,13 @@ class Tokenizer(object): if self.multiline: raise dns.exception.SyntaxError( 'unbalanced parentheses') - return Token(EOF) + return Token(EOF, comment=token) elif self.multiline: self.skip_whitespace() token = '' continue else: - return Token(EOL, '\n') + return Token(EOL, '\n', comment=token) else: # This code exists in case we ever want a # delimiter to be returned. It never produces @@ -379,23 +419,8 @@ class Tokenizer(object): else: self._unget_char(c) break - elif self.quoting: - if c == '\\': - c = self._get_char() - if c == '': - raise dns.exception.UnexpectedEnd - if c.isdigit(): - c2 = self._get_char() - if c2 == '': - raise dns.exception.UnexpectedEnd - c3 = self._get_char() - if c == '': - raise dns.exception.UnexpectedEnd - if not (c2.isdigit() and c3.isdigit()): - raise dns.exception.SyntaxError - c = chr(int(c) * 100 + int(c2) * 10 + int(c3)) - elif c == '\n': - raise dns.exception.SyntaxError('newline in quoted string') + 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 @@ -404,7 +429,7 @@ class Tokenizer(object): token += c has_escape = True c = self._get_char() - if c == '' or c == '\n': + if c == '' or (c == '\n' and not self.quoting): raise dns.exception.UnexpectedEnd token += c if token == '' and ttype != QUOTED_STRING: @@ -420,9 +445,9 @@ class Tokenizer(object): an error to try to unget a token when the unget buffer is not empty. - @param token: the token to unget - @type token: Token object - @raises UngetBufferFull: there is already an ungotten token + token: the token to unget + + Raises UngetBufferFull: there is already an ungotten token """ if self.ungotten_token is not None: @@ -431,7 +456,8 @@ class Tokenizer(object): def next(self): """Return the next item in an iteration. - @rtype: (int, string) + + Returns a Token. """ token = self.get() @@ -446,11 +472,12 @@ class Tokenizer(object): # Helpers - def get_int(self): - """Read the next token and interpret it as an integer. + def get_int(self, base=10): + """Read the next token and interpret it as an unsigned integer. - @raises dns.exception.SyntaxError: - @rtype: int + Raises dns.exception.SyntaxError if not an unsigned integer. + + Returns an int. """ token = self.get().unescape() @@ -458,14 +485,15 @@ class Tokenizer(object): raise dns.exception.SyntaxError('expecting an identifier') if not token.value.isdigit(): raise dns.exception.SyntaxError('expecting an integer') - return int(token.value) + return int(token.value, base) def get_uint8(self): """Read the next token and interpret it as an 8-bit unsigned integer. - @raises dns.exception.SyntaxError: - @rtype: int + Raises dns.exception.SyntaxError if not an 8-bit unsigned integer. + + Returns an int. """ value = self.get_int() @@ -474,56 +502,78 @@ class Tokenizer(object): '%d is not an unsigned 8-bit integer' % value) return value - def get_uint16(self): + def get_uint16(self, base=10): """Read the next token and interpret it as a 16-bit unsigned integer. - @raises dns.exception.SyntaxError: - @rtype: int + Raises dns.exception.SyntaxError if not a 16-bit unsigned integer. + + Returns an int. """ - value = self.get_int() + value = self.get_int(base=base) if value < 0 or value > 65535: - raise dns.exception.SyntaxError( - '%d is not an unsigned 16-bit integer' % value) + 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): + def get_uint32(self, base=10): """Read the next token and interpret it as a 32-bit unsigned integer. - @raises dns.exception.SyntaxError: - @rtype: int + Raises dns.exception.SyntaxError if not a 32-bit 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') - value = long(token.value) - if value < 0 or value > long(4294967296): + 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_string(self, origin=None): + def get_uint48(self, base=10): + """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=None): """Read the next token and interpret it as a string. - @raises dns.exception.SyntaxError: - @rtype: 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, origin=None): - """Read the next token and raise an exception if it is not an identifier. + def get_identifier(self): + """Read the next token, which should be an identifier. - @raises dns.exception.SyntaxError: - @rtype: string + Raises dns.exception.SyntaxError if not an identifier. + + Returns a string. """ token = self.get().unescape() @@ -531,23 +581,73 @@ class Tokenizer(object): raise dns.exception.SyntaxError('expecting an identifier') return token.value - def get_name(self, origin=None): - """Read the next token and interpret it as a DNS name. + def get_remaining(self, max_tokens=None): + """Return the remaining tokens on the line, until an EOL or EOF is seen. - @raises dns.exception.SyntaxError: - @rtype: dns.name.Name object""" + max_tokens: If not None, stop after this number of tokens. - token = self.get() + 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): + """Read the remaining tokens on the line, which should be identifiers. + + 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 + return s + + def as_name(self, token, origin=None, relativize=False, relativize_to=None): + """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') - return dns.name.from_text(token.value, origin) + name = dns.name.from_text(token.value, origin, self.idna_codec) + return name.choose_relativity(relativize_to or origin, relativize) - def get_eol(self): + def get_name(self, origin=None, relativize=False, relativize_to=None): + """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): """Read the next token and raise an exception if it isn't EOL or EOF. - @raises dns.exception.SyntaxError: - @rtype: string + Returns a string. """ token = self.get() @@ -555,9 +655,20 @@ class Tokenizer(object): raise dns.exception.SyntaxError( 'expected EOL or EOF, got %d "%s"' % (token.ttype, token.value)) - return token.value + return token + + def get_eol(self): + return self.get_eol_as_token().value def get_ttl(self): + """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') diff --git a/libs/dns/transaction.py b/libs/dns/transaction.py new file mode 100644 index 000000000..ae7417edb --- /dev/null +++ b/libs/dns/transaction.py @@ -0,0 +1,587 @@ +# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license + +import collections + +import dns.exception +import dns.name +import dns.rdataclass +import dns.rdataset +import dns.rdatatype +import dns.rrset +import dns.serial +import dns.ttl + + +class TransactionManager: + def reader(self): + """Begin a read-only transaction.""" + raise NotImplementedError # pragma: no cover + + def writer(self, replacement=False): + """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): + """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): + """The class of the transaction manager. + """ + raise NotImplementedError # pragma: no cover + + def from_wire_origin(self): + """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) + + +class Transaction: + + def __init__(self, manager, replacement=False, read_only=False): + self.manager = manager + self.replacement = replacement + self.read_only = read_only + self._ended = False + self._check_put_rdataset = [] + self._check_delete_rdataset = [] + self._check_delete_name = [] + + # + # This is the high level API + # + + def get(self, name, rdtype, covers=dns.rdatatype.NONE): + """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) + rdataset = self._get_rdataset(name, rdtype, covers) + return _ensure_immutable_rdataset(rdataset) + + def get_node(self, name): + """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): + if self.read_only: + raise ReadOnly + + def add(self, *args): + """Add records. + + The arguments may be: + + - rrset + + - name, rdataset... + + - name, ttl, rdata... + """ + self._check_ended() + self._check_read_only() + return self._add(False, args) + + def replace(self, *args): + """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() + return self._add(True, args) + + def delete(self, *args): + """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, rdataclass, rdatatype, [covers] + + - name, rdataset... + + - name, rdata... + """ + self._check_ended() + self._check_read_only() + return self._delete(False, args) + + def delete_exact(self, *args): + """Delete records. + + The arguments may be: + + - rrset + + - name + + - name, rdataclass, 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() + return self._delete(True, args) + + def name_exists(self, name): + """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=1, relative=True, name=dns.name.empty): + """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): + """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): + """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): + """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): + """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): + """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): + """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) + + # + # 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.manager.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 adn 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 _get_node(self, name): + """Return the node at *name*, if any. + + Returns a node or ``None``. + """ + raise NotImplementedError # pragma: no cover diff --git a/libs/dns/tsig.py b/libs/dns/tsig.py index c57d879fa..50b2d47ea 100644 --- a/libs/dns/tsig.py +++ b/libs/dns/tsig.py @@ -1,3 +1,5 @@ +# 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 @@ -15,14 +17,15 @@ """DNS TSIG support.""" +import base64 +import hashlib import hmac import struct import dns.exception -import dns.hash import dns.rdataclass import dns.name -from ._compat import long, string_types, text_type +import dns.rcode class BadTime(dns.exception.DNSException): @@ -34,6 +37,16 @@ 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""" @@ -58,177 +71,276 @@ 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") -_hashes = { - HMAC_SHA224: 'SHA224', - HMAC_SHA256: 'SHA256', - HMAC_SHA384: 'SHA384', - HMAC_SHA512: 'SHA512', - HMAC_SHA1: 'SHA1', - HMAC_MD5: 'MD5', -} - -default_algorithm = HMAC_MD5 - -BADSIG = 16 -BADKEY = 17 -BADTIME = 18 -BADTRUNC = 22 +default_algorithm = HMAC_SHA256 -def sign(wire, keyname, secret, time, fudge, original_id, error, - other_data, request_mac, ctx=None, multi=False, first=True, - algorithm=default_algorithm): - """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, string, hmac.HMAC object) +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 """ - if isinstance(other_data, text_type): - other_data = other_data.encode() - (algorithm_name, digestmod) = get_algorithm(algorithm) + first = not (ctx and multi) if first: - ctx = hmac.new(secret, digestmod=digestmod) - ml = len(request_mac) - if ml > 0: - ctx.update(struct.pack('!H', ml)) + ctx = get_context(key) + if request_mac: + ctx.update(struct.pack('!H', len(request_mac))) ctx.update(request_mac) - id = struct.pack('!H', original_id) - ctx.update(id) + ctx.update(struct.pack('!H', rdata.original_id)) ctx.update(wire[2:]) if first: - ctx.update(keyname.to_digestable()) + ctx.update(key.name.to_digestable()) ctx.update(struct.pack('!H', dns.rdataclass.ANY)) ctx.update(struct.pack('!I', 0)) - long_time = time + long(0) - upper_time = (long_time >> 32) & long(0xffff) - lower_time = long_time & long(0xffffffff) - time_mac = struct.pack('!HIH', upper_time, lower_time, fudge) - pre_mac = algorithm_name + time_mac - ol = len(other_data) - if ol > 65535: + 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') - post_mac = struct.pack('!HH', error, ol) + other_data if first: - ctx.update(pre_mac) - ctx.update(post_mac) + ctx.update(key.algorithm.to_digestable() + time_encoded) + ctx.update(struct.pack('!HH', rdata.error, other_len) + rdata.other) else: - ctx.update(time_mac) - mac = ctx.digest() - mpack = struct.pack('!H', len(mac)) - tsig_rdata = pre_mac + mpack + mac + id + post_mac + 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 = hmac.new(secret, digestmod=digestmod) - ml = len(mac) - ctx.update(struct.pack('!H', ml)) + ctx = get_context(key) + ctx.update(struct.pack('!H', len(mac))) ctx.update(mac) + return ctx else: - ctx = None - return (tsig_rdata, mac, ctx) + return None -def hmac_md5(wire, keyname, secret, time, fudge, original_id, error, - other_data, request_mac, ctx=None, multi=False, first=True, - algorithm=default_algorithm): - return sign(wire, keyname, secret, time, fudge, original_id, error, - other_data, request_mac, ctx, multi, first, algorithm) +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, keyname, secret, now, request_mac, tsig_start, tsig_rdata, - tsig_rdlen, ctx=None, multi=False, first=True): +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: hmac.HMAC object""" + @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] - current = tsig_rdata - (aname, used) = dns.name.from_wire(wire, current) - current = current + used - (upper_time, lower_time, fudge, mac_size) = \ - struct.unpack("!HIHH", wire[current:current + 10]) - time = ((upper_time + long(0)) << 32) + (lower_time + long(0)) - current += 10 - mac = wire[current:current + mac_size] - current += mac_size - (original_id, error, other_size) = \ - struct.unpack("!HHH", wire[current:current + 6]) - current += 6 - other_data = wire[current:current + other_size] - current += other_size - if current != tsig_rdata + tsig_rdlen: - raise dns.exception.FormError - if error != 0: - if error == BADSIG: + if rdata.error != 0: + if rdata.error == dns.rcode.BADSIG: raise PeerBadSignature - elif error == BADKEY: + elif rdata.error == dns.rcode.BADKEY: raise PeerBadKey - elif error == BADTIME: + elif rdata.error == dns.rcode.BADTIME: raise PeerBadTime - elif error == BADTRUNC: + elif rdata.error == dns.rcode.BADTRUNC: raise PeerBadTruncation else: - raise PeerError('unknown TSIG error code %d' % error) - time_low = time - fudge - time_high = time + fudge - if now < time_low or now > time_high: + raise PeerError('unknown TSIG error code %d' % rdata.error) + if abs(rdata.time_signed - now) > rdata.fudge: raise BadTime - (junk, our_mac, ctx) = sign(new_wire, keyname, secret, time, fudge, - original_id, error, other_data, - request_mac, ctx, multi, first, aname) - if our_mac != mac: - raise BadSignature - return ctx + 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_algorithm(algorithm): - """Returns the wire format string and the hash module to use for the - specified TSIG algorithm +def get_context(key): + """Returns an HMAC context for the specified key. - @rtype: (string, hash constructor) + @rtype: HMAC context @raises NotImplementedError: I{algorithm} is not supported """ - if isinstance(algorithm, string_types): - algorithm = dns.name.from_text(algorithm) - - try: - return (algorithm.to_digestable(), dns.hash.hashes[_hashes[algorithm]]) - except KeyError: - raise NotImplementedError("TSIG algorithm " + str(algorithm) + - " is not supported") + if key.algorithm == GSS_TSIG: + return GSSTSig(key.secret) + else: + return HMACTSig(key.secret, key.algorithm) -def get_algorithm_and_mac(wire, tsig_rdata, tsig_rdlen): - """Return the tsig algorithm for the specified tsig_rdata - @raises FormError: The TSIG is badly formed. - """ - current = tsig_rdata - (aname, used) = dns.name.from_wire(wire, current) - current = current + used - (upper_time, lower_time, fudge, mac_size) = \ - struct.unpack("!HIHH", wire[current:current + 10]) - current += 10 - mac = wire[current:current + mac_size] - current += mac_size - if current > tsig_rdata + tsig_rdlen: - raise dns.exception.FormError - return (aname, mac) +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[name.Name,bytes]: + ... +def to_text(keyring : Dict[name.Name,bytes]) -> Dict[str, str]: + ... diff --git a/libs/dns/ttl.py b/libs/dns/ttl.py index a27d82518..df92b2b60 100644 --- a/libs/dns/ttl.py +++ b/libs/dns/ttl.py @@ -1,4 +1,6 @@ -# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc. +# 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, @@ -16,11 +18,16 @@ """DNS TTL conversion.""" import dns.exception -from ._compat import long + +# 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.""" @@ -29,40 +36,55 @@ def from_text(text): The BIND 8 units syntax for TTLs (e.g. '1w6d4h3m10s') is supported. - @param text: the textual TTL - @type text: string - @raises dns.ttl.BadTTL: the TTL is not well-formed - @rtype: int + *text*, a ``str``, the textual TTL. + + Raises ``dns.ttl.BadTTL`` if the TTL is not well-formed. + + Returns an ``int``. """ if text.isdigit(): - total = long(text) + total = int(text) + elif len(text) == 0: + raise BadTTL else: - if not text[0].isdigit(): - raise BadTTL - total = long(0) - current = long(0) + total = 0 + current = 0 + need_digit = True for c in text: if c.isdigit(): current *= 10 - current += long(c) + current += int(c) + need_digit = False else: + if need_digit: + raise BadTTL c = c.lower() if c == 'w': - total += current * long(604800) + total += current * 604800 elif c == 'd': - total += current * long(86400) + total += current * 86400 elif c == 'h': - total += current * long(3600) + total += current * 3600 elif c == 'm': - total += current * long(60) + 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 < long(0) or total > long(2147483647): - raise BadTTL("TTL should be between 0 and 2^31 - 1 (inclusive)") + if total < 0 or total > MAX_TTL: + raise BadTTL("TTL should be between 0 and 2**32 - 1 (inclusive)") return total + + +def make(value): + 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/libs/dns/update.py b/libs/dns/update.py index 59728d982..a541af22d 100644 --- a/libs/dns/update.py +++ b/libs/dns/update.py @@ -1,3 +1,5 @@ +# 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 @@ -23,62 +25,97 @@ import dns.rdata import dns.rdataclass import dns.rdataset import dns.tsig -from ._compat import string_types -class Update(dns.message.Message): +class UpdateSection(dns.enum.IntEnum): + """Update sections""" + ZONE = 0 + PREREQ = 1 + UPDATE = 2 + ADDITIONAL = 3 - def __init__(self, zone, rdclass=dns.rdataclass.IN, keyring=None, - keyname=None, keyalgorithm=dns.tsig.default_algorithm): + @classmethod + def _maximum(cls): + return 3 + + +class UpdateMessage(dns.message.Message): + + _section_enum = UpdateSection + + def __init__(self, zone=None, rdclass=dns.rdataclass.IN, keyring=None, + keyname=None, keyalgorithm=dns.tsig.default_algorithm, + id=None): """Initialize a new DNS Update object. - @param zone: The zone which is being updated. - @type zone: A dns.name.Name or string - @param rdclass: The class of the zone; defaults to dns.rdataclass.IN. - @type rdclass: An int designating the class, or a string whose value - is the name of a class. - @param keyring: The TSIG keyring to use; defaults to None. - @type keyring: dict - @param keyname: The name of the TSIG key to use; defaults to None. - The key must be defined in the keyring. If a keyring is specified - but a keyname is not, then 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 keyring is used, unless - they know the keyring contains only one key. - @type keyname: dns.name.Name or string - @param keyalgorithm: The TSIG algorithm to use; defaults to - dns.tsig.default_algorithm. Constants for TSIG algorithms are defined - in dns.tsig, and the currently implemented algorithms are - HMAC_MD5, HMAC_SHA1, HMAC_SHA224, HMAC_SHA256, HMAC_SHA384, and - HMAC_SHA512. - @type keyalgorithm: string + 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(Update, self).__init__() + super().__init__(id=id) self.flags |= dns.opcode.to_flags(dns.opcode.UPDATE) - if isinstance(zone, string_types): + if isinstance(zone, str): zone = dns.name.from_text(zone) self.origin = zone - if isinstance(rdclass, string_types): - rdclass = dns.rdataclass.from_text(rdclass) + rdclass = dns.rdataclass.RdataClass.make(rdclass) self.zone_rdclass = rdclass - self.find_rrset(self.question, self.origin, rdclass, dns.rdatatype.SOA, - create=True, force_unique=True) + 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): + """The zone section.""" + return self.sections[0] + + @zone.setter + def zone(self, v): + self.sections[0] = v + + @property + def prerequisite(self): + """The prerequisite section.""" + return self.sections[1] + + @prerequisite.setter + def prerequisite(self, v): + self.sections[1] = v + + @property + def update(self): + """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.authority + 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. The first argument is the replace mode. If - false, RRs are added to an existing RRset; if true, the RRset + """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: @@ -87,9 +124,10 @@ class Update(dns.message.Message): - ttl, rdata... - - ttl, rdtype, string...""" + - ttl, rdtype, string... + """ - if isinstance(name, string_types): + if isinstance(name, str): name = dns.name.from_text(name, None) if isinstance(args[0], dns.rdataset.Rdataset): for rds in args: @@ -106,9 +144,7 @@ class Update(dns.message.Message): for rd in args: self._add_rr(name, ttl, rd, section=section) else: - rdtype = args.pop(0) - if isinstance(rdtype, string_types): - rdtype = dns.rdatatype.from_text(rdtype) + rdtype = dns.rdatatype.RdataType.make(args.pop(0)) if replace: self.delete(name, rdtype) for s in args: @@ -117,32 +153,39 @@ class Update(dns.message.Message): self._add_rr(name, ttl, rd, section=section) def add(self, name, *args): - """Add records. The first argument is always a name. The other + """Add records. + + The first argument is always a name. The other arguments can be: - rdataset... - ttl, rdata... - - ttl, rdtype, string...""" - self._add(False, self.authority, name, *args) + - ttl, rdtype, string... + """ + + self._add(False, self.update, name, *args) def delete(self, name, *args): - """Delete records. The first argument is always a name. The other + """Delete records. + + The first argument is always a name. The other arguments can be: - - I{nothing} + - *empty* - rdataset... - rdata... - - rdtype, [string...]""" + - rdtype, [string...] + """ - if isinstance(name, string_types): + if isinstance(name, str): name = dns.name.from_text(name, None) if len(args) == 0: - self.find_rrset(self.authority, name, dns.rdataclass.ANY, + self.find_rrset(self.update, name, dns.rdataclass.ANY, dns.rdatatype.ANY, dns.rdatatype.NONE, dns.rdatatype.ANY, True, True) elif isinstance(args[0], dns.rdataset.Rdataset): @@ -155,11 +198,9 @@ class Update(dns.message.Message): for rd in args: self._add_rr(name, 0, rd, dns.rdataclass.NONE) else: - rdtype = args.pop(0) - if isinstance(rdtype, string_types): - rdtype = dns.rdatatype.from_text(rdtype) + rdtype = dns.rdatatype.RdataType.make(args.pop(0)) if len(args) == 0: - self.find_rrset(self.authority, name, + self.find_rrset(self.update, name, self.zone_rdclass, rdtype, dns.rdatatype.NONE, dns.rdataclass.ANY, @@ -171,7 +212,9 @@ class Update(dns.message.Message): self._add_rr(name, 0, rd, dns.rdataclass.NONE) def replace(self, name, *args): - """Replace records. The first argument is always a name. The other + """Replace records. + + The first argument is always a name. The other arguments can be: - rdataset... @@ -181,26 +224,30 @@ class Update(dns.message.Message): - 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.""" + a delete of the name followed by one or more calls to add. + """ - self._add(True, self.authority, name, *args) + self._add(True, self.update, name, *args) def present(self, name, *args): """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. + execution of the update. + + The first argument is always a name. The other arguments can be: - rdataset... - rdata... - - rdtype, string...""" + - rdtype, string... + """ - if isinstance(name, string_types): + if isinstance(name, str): name = dns.name.from_text(name, None) if len(args) == 0: - self.find_rrset(self.answer, name, + self.find_rrset(self.prerequisite, name, dns.rdataclass.ANY, dns.rdatatype.ANY, dns.rdatatype.NONE, None, True, True) @@ -211,12 +258,10 @@ class Update(dns.message.Message): # Add a 0 TTL args = list(args) args.insert(0, 0) - self._add(False, self.answer, name, *args) + self._add(False, self.prerequisite, name, *args) else: - rdtype = args[0] - if isinstance(rdtype, string_types): - rdtype = dns.rdatatype.from_text(rdtype) - self.find_rrset(self.answer, name, + rdtype = dns.rdatatype.RdataType.make(args[0]) + self.find_rrset(self.prerequisite, name, dns.rdataclass.ANY, rdtype, dns.rdatatype.NONE, None, True, True) @@ -225,25 +270,50 @@ class Update(dns.message.Message): """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, string_types): + if isinstance(name, str): name = dns.name.from_text(name, None) if rdtype is None: - self.find_rrset(self.answer, name, + self.find_rrset(self.prerequisite, name, dns.rdataclass.NONE, dns.rdatatype.ANY, dns.rdatatype.NONE, None, True, True) else: - if isinstance(rdtype, string_types): - rdtype = dns.rdatatype.from_text(rdtype) - self.find_rrset(self.answer, name, + rdtype = dns.rdatatype.RdataType.make(rdtype) + self.find_rrset(self.prerequisite, name, dns.rdataclass.NONE, rdtype, dns.rdatatype.NONE, None, True, True) - def to_wire(self, origin=None, max_size=65535): - """Return a string containing the update in DNS compressed wire - format. - @rtype: string""" - if origin is None: - origin = self.origin - return super(Update, self).to_wire(origin, max_size) + 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/libs/dns/update.pyi b/libs/dns/update.pyi new file mode 100644 index 000000000..eeac0591d --- /dev/null +++ b/libs/dns/update.pyi @@ -0,0 +1,21 @@ +from typing import Optional,Dict,Union,Any + +from . import message, tsig, rdataclass, name + +class Update(message.Message): + def __init__(self, zone : Union[name.Name, str], rdclass : Union[int,str] = rdataclass.IN, keyring : Optional[Dict[name.Name,bytes]] = None, + keyname : Optional[name.Name] = None, keyalgorithm : Optional[name.Name] = tsig.default_algorithm) -> None: + self.id : int + def add(self, name : Union[str,name.Name], *args : Any): + ... + def delete(self, name, *args : Any): + ... + def replace(self, name : Union[str,name.Name], *args : Any): + ... + def present(self, name : Union[str,name.Name], *args : Any): + ... + def absent(self, name : Union[str,name.Name], rdtype=None): + """Require that an owner name (and optionally an rdata type) does + not exist as a prerequisite to the execution of the update.""" + def to_wire(self, origin : Optional[name.Name] = None, max_size=65535, **kw) -> bytes: + ... diff --git a/libs/dns/version.py b/libs/dns/version.py index 9e8dbb1b0..745a5c7fc 100644 --- a/libs/dns/version.py +++ b/libs/dns/version.py @@ -1,4 +1,6 @@ -# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc. +# 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, @@ -15,20 +17,30 @@ """dnspython release version information.""" -MAJOR = 1 -MINOR = 15 +#: MAJOR +MAJOR = 2 +#: MINOR +MINOR = 2 +#: MICRO MICRO = 0 +#: RELEASELEVEL RELEASELEVEL = 0x0f +#: SERIAL SERIAL = 0 -if RELEASELEVEL == 0x0f: +if RELEASELEVEL == 0x0f: # pragma: no cover + #: version version = '%d.%d.%d' % (MAJOR, MINOR, MICRO) -elif RELEASELEVEL == 0x00: - version = '%d.%d.%dx%d' % \ +elif RELEASELEVEL == 0x00: # pragma: no cover + version = '%d.%d.%ddev%d' % \ (MAJOR, MINOR, MICRO, SERIAL) -else: +elif RELEASELEVEL == 0x0c: # pragma: no cover + version = '%d.%d.%drc%d' % \ + (MAJOR, MINOR, MICRO, SERIAL) +else: # pragma: no cover version = '%d.%d.%d%x%d' % \ (MAJOR, MINOR, MICRO, RELEASELEVEL, SERIAL) +#: hexversion hexversion = MAJOR << 24 | MINOR << 16 | MICRO << 8 | RELEASELEVEL << 4 | \ SERIAL diff --git a/libs/dns/versioned.py b/libs/dns/versioned.py new file mode 100644 index 000000000..42f2c8140 --- /dev/null +++ b/libs/dns/versioned.py @@ -0,0 +1,274 @@ +# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license + +"""DNS Versioned Zones.""" + +import collections +try: + import threading as _threading +except ImportError: # pragma: no cover + import dummy_threading as _threading # type: ignore + +import dns.exception +import dns.immutable +import dns.name +import dns.rdataclass +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): + + __slots__ = ['_versions', '_versions_lock', '_write_txn', + '_write_waiters', '_write_event', '_pruning_policy', + '_readers'] + + node_factory = Node + + def __init__(self, origin, rdclass=dns.rdataclass.IN, relativize=True, + pruning_policy=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 `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 = 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 = None + self._write_event = None + self._write_waiters = collections.deque() + self._readers = set() + self._commit_version_unlocked(None, + WritableVersion(self, replacement=True), + origin) + + def reader(self, id=None, serial=None): # 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: + 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=False): + 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 + # cancelation) 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 + # or curio 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): + """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(*_): + return False + else: + def policy(zone, _): + return len(zone._versions) > max_versions + self.set_pruning_policy(policy) + + def set_pruning_policy(self, policy): + """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, create=False): + if create: + raise UseTransaction + return super().find_node(name) + + def delete_node(self, name): + raise UseTransaction + + def find_rdataset(self, name, rdtype, covers=dns.rdatatype.NONE, + create=False): + if create: + raise UseTransaction + rdataset = super().find_rdataset(name, rdtype, covers) + return dns.rdataset.ImmutableRdataset(rdataset) + + def get_rdataset(self, name, rdtype, covers=dns.rdatatype.NONE, + create=False): + if create: + raise UseTransaction + rdataset = super().get_rdataset(name, rdtype, covers) + return dns.rdataset.ImmutableRdataset(rdataset) + + def delete_rdataset(self, name, rdtype, covers=dns.rdatatype.NONE): + raise UseTransaction + + def replace_rdataset(self, name, replacement): + raise UseTransaction diff --git a/libs/dns/win32util.py b/libs/dns/win32util.py new file mode 100755 index 000000000..745317a39 --- /dev/null +++ b/libs/dns/win32util.py @@ -0,0 +1,235 @@ +import sys + +if sys.platform == 'win32': + + import dns.name + + _prefer_wmi = True + + import winreg + + try: + try: + import threading as _threading + except ImportError: # pragma: no cover + import dummy_threading as _threading # type: ignore + import pythoncom + import wmi + _have_wmi = True + except Exception: + _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: + self.info.domain = _config_domain(interface.DNSDomain) + self.info.nameservers = list(interface.DNSServerSearchOrder) + self.info.search = [dns.name.from_text(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: + 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 = dns.name.from_text(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' + r'\Services\Tcpip\Parameters') + try: + self._config_fromkey(tcp_params, True) + finally: + tcp_params.Close() + interfaces = winreg.OpenKey(lm, + r'SYSTEM\CurrentControlSet' + r'\Services\Tcpip\Parameters' + r'\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 + + 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/libs/dns/wire.py b/libs/dns/wire.py new file mode 100644 index 000000000..572e27e70 --- /dev/null +++ b/libs/dns/wire.py @@ -0,0 +1,85 @@ +# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license + +import contextlib +import struct + +import dns.exception +import dns.name + +class Parser: + def __init__(self, wire, current=0): + self.wire = wire + self.current = 0 + self.end = len(self.wire) + if current: + self.seek(current) + self.furthest = current + + def remaining(self): + return self.end - self.current + + def get_bytes(self, size): + 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=1): + length = int.from_bytes(self.get_bytes(length_size), 'big') + return self.get_bytes(length) + + def get_remaining(self): + return self.get_bytes(self.remaining()) + + def get_uint8(self): + return struct.unpack('!B', self.get_bytes(1))[0] + + def get_uint16(self): + return struct.unpack('!H', self.get_bytes(2))[0] + + def get_uint32(self): + return struct.unpack('!I', self.get_bytes(4))[0] + + def get_uint48(self): + return int.from_bytes(self.get_bytes(6), 'big') + + def get_struct(self, format): + return struct.unpack(format, self.get_bytes(struct.calcsize(format))) + + def get_name(self, origin=None): + name = dns.name.from_wire_parser(self) + if origin: + name = name.relativize(origin) + return name + + def seek(self, where): + # 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): + 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): + try: + yield None + finally: + self.current = self.furthest diff --git a/libs/dns/wiredata.py b/libs/dns/wiredata.py deleted file mode 100644 index ccef59545..000000000 --- a/libs/dns/wiredata.py +++ /dev/null @@ -1,103 +0,0 @@ -# Copyright (C) 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 Wire Data Helper""" - -import sys - -import dns.exception -from ._compat import binary_type, string_types - -# Figure out what constant python passes for an unspecified slice bound. -# It's supposed to be sys.maxint, yet on 64-bit windows sys.maxint is 2^31 - 1 -# but Python uses 2^63 - 1 as the constant. Rather than making pointless -# extra comparisons, duplicating code, or weakening WireData, we just figure -# out what constant Python will use. - - -class _SliceUnspecifiedBound(binary_type): - - def __getitem__(self, key): - return key.stop - - if sys.version_info < (3,): - def __getslice__(self, i, j): # pylint: disable=getslice-method - return self.__getitem__(slice(i, j)) - -_unspecified_bound = _SliceUnspecifiedBound()[1:] - - -class WireData(binary_type): - # WireData is a string with stricter slicing - - def __getitem__(self, key): - try: - if isinstance(key, slice): - # make sure we are not going outside of valid ranges, - # do stricter control of boundaries than python does - # by default - start = key.start - stop = key.stop - - if sys.version_info < (3,): - if stop == _unspecified_bound: - # handle the case where the right bound is unspecified - stop = len(self) - - if start < 0 or stop < 0: - raise dns.exception.FormError - # If it's not an empty slice, access left and right bounds - # to make sure they're valid - if start != stop: - super(WireData, self).__getitem__(start) - super(WireData, self).__getitem__(stop - 1) - else: - for index in (start, stop): - if index is None: - continue - elif abs(index) > len(self): - raise dns.exception.FormError - - return WireData(super(WireData, self).__getitem__( - slice(start, stop))) - return bytearray(self.unwrap())[key] - except IndexError: - raise dns.exception.FormError - - if sys.version_info < (3,): - def __getslice__(self, i, j): # pylint: disable=getslice-method - return self.__getitem__(slice(i, j)) - - def __iter__(self): - i = 0 - while 1: - try: - yield self[i] - i += 1 - except dns.exception.FormError: - raise StopIteration - - def unwrap(self): - return binary_type(self) - - -def maybe_wrap(wire): - if isinstance(wire, WireData): - return wire - elif isinstance(wire, binary_type): - return WireData(wire) - elif isinstance(wire, string_types): - return WireData(wire.encode()) - raise ValueError("unhandled type %s" % type(wire)) diff --git a/libs/dns/xfr.py b/libs/dns/xfr.py new file mode 100644 index 000000000..cf9a163ea --- /dev/null +++ b/libs/dns/xfr.py @@ -0,0 +1,313 @@ +# 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 dns.exception +import dns.message +import dns.name +import dns.rcode +import dns.serial +import dns.rdatatype +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, rdtype=dns.rdatatype.AXFR, + serial=None, is_udp=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 = 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 = None + self.done = False + self.expecting_SOA = False + self.delete_mode = False + + def process_message(self, message): + """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] + name = rrset.name + 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") + 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, serial=0, + use_edns=None, ednsflags=None, payload=None, + request_payload=None, options=None, + keyring=None, keyname=None, + keyalgorithm=dns.tsig.default_algorithm): + """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 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): + """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.""" + + 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/libs/dns/zone.py b/libs/dns/zone.py index 468618f67..2e7314461 100644 --- a/libs/dns/zone.py +++ b/libs/dns/zone.py @@ -1,3 +1,5 @@ +# 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 @@ -15,27 +17,27 @@ """DNS Zones.""" -from __future__ import generators - -import sys -import re +import contextlib +import hashlib +import io import os -from io import BytesIO +import struct import dns.exception +import dns.immutable import dns.name import dns.node import dns.rdataclass import dns.rdatatype import dns.rdata +import dns.rdtypes.ANY.SOA +import dns.rdtypes.ANY.ZONEMD import dns.rrset import dns.tokenizer +import dns.transaction import dns.ttl import dns.grange -from ._compat import string_types, text_type - - -_py3 = sys.version_info > (3,) +import dns.zonefile class BadZone(dns.exception.DNSException): @@ -58,28 +60,63 @@ class UnknownOrigin(BadZone): """The DNS zone's origin is unknown.""" -class Zone(object): +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.""" + + +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, +} + + +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 I{name} may be a - dns.name.Name object, or it may be a string. In the either case, + 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. - - @ivar rdclass: The zone's rdata class; the default is class IN. - @type rdclass: int - @ivar origin: The origin of the zone. - @type origin: dns.name.Name object - @ivar nodes: A dictionary mapping the names of nodes in the zone to the - nodes themselves. - @type nodes: dict - @ivar relativize: should names in the zone be relativized? - @type relativize: bool - @cvar node_factory: the factory used to create a new node - @type node_factory: class or callable """ node_factory = dns.node.Node @@ -89,13 +126,18 @@ class Zone(object): def __init__(self, origin, rdclass=dns.rdataclass.IN, relativize=True): """Initialize a zone object. - @param origin: The origin of the zone. - @type origin: dns.name.Name object - @param rdclass: The zone's rdata class; the default is class IN. - @type rdclass: int""" + *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, string_types): + 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 " @@ -110,7 +152,8 @@ class Zone(object): def __eq__(self, other): """Two zones are equal if they have the same origin, class, and nodes. - @rtype: bool + + Returns a ``bool``. """ if not isinstance(other, Zone): @@ -123,13 +166,14 @@ class Zone(object): def __ne__(self, other): """Are two zones not equal? - @rtype: bool + + Returns a ``bool``. """ return not self.__eq__(other) def _validate_name(self, name): - if isinstance(name, string_types): + 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") @@ -156,45 +200,38 @@ class Zone(object): def __iter__(self): return self.nodes.__iter__() - def iterkeys(self): - if _py3: - return self.nodes.keys() - else: - return self.nodes.iterkeys() # pylint: disable=dict-iter-method - def keys(self): return self.nodes.keys() - def itervalues(self): - if _py3: - return self.nodes.values() - else: - return self.nodes.itervalues() # pylint: disable=dict-iter-method - def values(self): return self.nodes.values() def items(self): return self.nodes.items() - iteritems = items - def get(self, key): key = self._validate_name(key) return self.nodes.get(key) - def __contains__(self, other): - return other in self.nodes + def __contains__(self, key): + key = self._validate_name(key) + return key in self.nodes def find_node(self, name, create=False): """Find a node in the zone, possibly creating it. - @param name: the name of the node to find - @type name: dns.name.Name object or string - @param create: should the node be created if it doesn't exist? - @type create: bool - @raises KeyError: the name is not known and create was not specified. - @rtype: dns.node.Node object + *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) @@ -209,15 +246,22 @@ class Zone(object): def get_node(self, name, create=False): """Get a node in the zone, possibly creating it. - This method is like L{find_node}, except it returns None instead + 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. - @param name: the name of the node to find - @type name: dns.name.Name object or string - @param create: should the node be created if it doesn't exist? - @type create: bool - @rtype: dns.node.Node object or None + *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`` or ``None``. """ try: @@ -229,6 +273,11 @@ class Zone(object): def delete_node(self, name): """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. """ @@ -238,65 +287,82 @@ class Zone(object): def find_rdataset(self, name, rdtype, covers=dns.rdatatype.NONE, create=False): - """Look for rdata with the specified name and type in the zone, + """Look for an rdataset with the specified name and type in the zone, and return an rdataset encapsulating it. - The I{name}, I{rdtype}, and I{covers} parameters may be - strings, in which case they will be converted to their proper - type. - 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. - Use L{get_rdataset} if you want to have None returned instead. - @param name: the owner name to look for - @type name: DNS.name.Name object or string - @param rdtype: the rdata type desired - @type rdtype: int or string - @param covers: the covered type (defaults to None) - @type covers: int or string - @param create: should the node and rdataset be created if they do not - exist? - @type create: bool - @raises KeyError: the node or rdata could not be found - @rtype: dns.rrset.RRset object + *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*, an ``int`` or ``str``, the rdata type desired. + + *covers*, an ``int`` 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. + + *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) - if isinstance(rdtype, string_types): - rdtype = dns.rdatatype.from_text(rdtype) - if isinstance(covers, string_types): - covers = dns.rdatatype.from_text(covers) + rdtype = dns.rdatatype.RdataType.make(rdtype) + if covers is not None: + 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, rdtype, covers=dns.rdatatype.NONE, create=False): - """Look for rdata with the specified name and type in the zone, - and return an rdataset encapsulating it. + """Look for an rdataset with the specified name and type in the zone. - The I{name}, I{rdtype}, and I{covers} parameters may be - strings, in which case they will be converted to their proper - type. + 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. - None is returned if the name or type are not found. - Use L{find_rdataset} if you want to have KeyError raised 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. - @param name: the owner name to look for - @type name: DNS.name.Name object or string - @param rdtype: the rdata type desired - @type rdtype: int or string - @param covers: the covered type (defaults to None) - @type covers: int or string - @param create: should the node and rdataset be created if they do not - exist? - @type create: bool - @rtype: dns.rrset.RRset object + *rdtype*, an ``int`` or ``str``, the rdata type desired. + + *covers*, an ``int`` 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. + + *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: @@ -306,12 +372,8 @@ class Zone(object): return rdataset def delete_rdataset(self, name, rdtype, covers=dns.rdatatype.NONE): - """Delete the rdataset matching I{rdtype} and I{covers}, if it - exists at the node specified by I{name}. - - The I{name}, I{rdtype}, and I{covers} parameters may be - strings, in which case they will be converted to their proper - type. + """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. @@ -319,19 +381,28 @@ class Zone(object): If the node has no rdatasets after the deletion, it will itself be deleted. - @param name: the owner name to look for - @type name: DNS.name.Name object or string - @param rdtype: the rdata type desired - @type rdtype: int or string - @param covers: the covered type (defaults to None) - @type covers: int or string + *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*, an ``int`` or ``str``, the rdata type desired. + + *covers*, an ``int`` 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) - if isinstance(rdtype, string_types): - rdtype = dns.rdatatype.from_text(rdtype) - if isinstance(covers, string_types): - covers = dns.rdatatype.from_text(covers) + rdtype = dns.rdatatype.RdataType.make(rdtype) + if covers is not None: + covers = dns.rdatatype.RdataType.make(covers) node = self.get_node(name) if node is not None: node.delete_rdataset(self.rdclass, rdtype, covers) @@ -343,16 +414,18 @@ class Zone(object): It is not an error if there is no rdataset matching I{replacement}. - Ownership of the I{replacement} object is transferred to the zone; - in other words, this method does not store a copy of I{replacement} - at the node, it stores I{replacement} itself. + 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 I{name} node does not exist, it is created. + If the node does not exist, it is created. - @param name: the owner name - @type name: DNS.name.Name object or string - @param replacement: the replacement rdataset - @type replacement: dns.rdataset.Rdataset + *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: @@ -361,71 +434,89 @@ class Zone(object): node.replace_rdataset(replacement) def find_rrset(self, name, rdtype, covers=dns.rdatatype.NONE): - """Look for rdata with the specified name and type in the zone, + """Look for an rdataset with the specified name and type in the zone, and return an RRset encapsulating it. - The I{name}, I{rdtype}, and I{covers} parameters may be - strings, in which case they will be converted to their proper - type. - This method is less efficient than the similar - L{find_rdataset} because it creates an RRset instead of + ``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 rdata. + name to the rdataset. This method may not be used to create new nodes or rdatasets; - use L{find_rdataset} instead. + use ``find_rdataset`` instead. - KeyError is raised if the name or type are not found. - Use L{get_rrset} if you want to have None returned 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. - @param name: the owner name to look for - @type name: DNS.name.Name object or string - @param rdtype: the rdata type desired - @type rdtype: int or string - @param covers: the covered type (defaults to None) - @type covers: int or string - @raises KeyError: the node or rdata could not be found - @rtype: dns.rrset.RRset object + *rdtype*, an ``int`` or ``str``, the rdata type desired. + + *covers*, an ``int`` 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. + + *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``. """ name = self._validate_name(name) - if isinstance(rdtype, string_types): - rdtype = dns.rdatatype.from_text(rdtype) - if isinstance(covers, string_types): - covers = dns.rdatatype.from_text(covers) + rdtype = dns.rdatatype.RdataType.make(rdtype) + if covers is not None: + covers = dns.rdatatype.RdataType.make(covers) rdataset = self.nodes[name].find_rdataset(self.rdclass, rdtype, covers) rrset = dns.rrset.RRset(name, self.rdclass, rdtype, covers) rrset.update(rdataset) return rrset def get_rrset(self, name, rdtype, covers=dns.rdatatype.NONE): - """Look for rdata with the specified name and type in the zone, + """Look for an rdataset with the specified name and type in the zone, and return an RRset encapsulating it. - The I{name}, I{rdtype}, and I{covers} parameters may be - strings, in which case they will be converted to their proper - type. - - This method is less efficient than the similar L{get_rdataset} + 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 rdata. + returns an object which binds the owner name to the rdataset. This method may not be used to create new nodes or rdatasets; - use L{find_rdataset} instead. + use ``get_rdataset()`` instead. - None is returned if the name or type are not found. - Use L{find_rrset} if you want to have KeyError raised 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. - @param name: the owner name to look for - @type name: DNS.name.Name object or string - @param rdtype: the rdata type desired - @type rdtype: int or string - @param covers: the covered type (defaults to None) - @type covers: int or string - @rtype: dns.rrset.RRset object + *rdtype*, an ``int`` or ``str``, the rdata type desired. + + *covers*, an ``int`` 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. + + *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``. """ try: @@ -437,21 +528,27 @@ class Zone(object): def iterate_rdatasets(self, rdtype=dns.rdatatype.ANY, covers=dns.rdatatype.NONE): """Return a generator which yields (name, rdataset) tuples for - all rdatasets in the zone which have the specified I{rdtype} - and I{covers}. If I{rdtype} is dns.rdatatype.ANY, the default, + 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. - @param rdtype: int or string - @type rdtype: int or string - @param covers: the covered type (defaults to None) - @type covers: int or string + *rdtype*, an ``int`` or ``str``, the rdata type desired. + + *covers*, an ``int`` 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. """ - if isinstance(rdtype, string_types): - rdtype = dns.rdatatype.from_text(rdtype) - if isinstance(covers, string_types): - covers = dns.rdatatype.from_text(covers) - for (name, node) in self.iteritems(): + rdtype = dns.rdatatype.RdataType.make(rdtype) + if covers is not None: + 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): @@ -460,80 +557,102 @@ class Zone(object): def iterate_rdatas(self, rdtype=dns.rdatatype.ANY, covers=dns.rdatatype.NONE): """Return a generator which yields (name, ttl, rdata) tuples for - all rdatas in the zone which have the specified I{rdtype} - and I{covers}. If I{rdtype} is dns.rdatatype.ANY, the default, + 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. - @param rdtype: int or string - @type rdtype: int or string - @param covers: the covered type (defaults to None) - @type covers: int or string + *rdtype*, an ``int`` or ``str``, the rdata type desired. + + *covers*, an ``int`` 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. """ - if isinstance(rdtype, string_types): - rdtype = dns.rdatatype.from_text(rdtype) - if isinstance(covers, string_types): - covers = dns.rdatatype.from_text(covers) - for (name, node) in self.iteritems(): + rdtype = dns.rdatatype.RdataType.make(rdtype) + if covers is not None: + 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, sorted=True, relativize=True, nl=None): + def to_file(self, f, sorted=True, relativize=True, nl=None, + want_comments=False, want_origin=False): """Write a zone to a file. - @param f: file or string. If I{f} is a string, it is treated + *f*, a file or `str`. If *f* is a string, it is treated as the name of a file to open. - @param sorted: if True, 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. - @param relativize: if True, domain names in the output will be - relativized to the zone's origin (if possible). - @type relativize: bool - @param nl: The end of line string. If not specified, the - output will use the platform's native end-of-line marker (i.e. - LF on POSIX, CRLF on Windows, CR on Macintosh). - @type nl: string or None + + *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, string_types): - f = open(f, 'wb') - want_close = True - else: - want_close = False + with contextlib.ExitStack() as stack: + if isinstance(f, str): + f = stack.enter_context(open(f, 'wb')) - # 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' + # 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: - nl_b = os.linesep.encode(file_enc) # binary mode, '\n' is not enough - nl = u'\n' - elif isinstance(nl, string_types): - nl_b = nl.encode(file_enc) - else: - nl_b = nl - nl = nl.decode() + 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: + 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) - try: if sorted: names = list(self.keys()) names.sort() else: - names = self.iterkeys() + names = self.keys() for n in names: l = self[n].to_text(n, origin=self.origin, - relativize=relativize) - if isinstance(l, text_type): - l_b = l.encode(file_enc) - else: - l_b = l - l = l.decode() + relativize=relativize, + want_comments=want_comments) + l_b = l.encode(file_enc) try: f.write(l_b) @@ -541,27 +660,37 @@ class Zone(object): except TypeError: # textual mode f.write(l) f.write(nl) - finally: - if want_close: - f.close() - def to_text(self, sorted=True, relativize=True, nl=None): + def to_text(self, sorted=True, relativize=True, nl=None, + want_comments=False, want_origin=False): """Return a zone's text as though it were written to a file. - @param sorted: if True, 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. - @param relativize: if True, domain names in the output will be - relativized to the zone's origin (if possible). - @type relativize: bool - @param nl: The end of line string. If not specified, the - output will use the platform's native end-of-line marker (i.e. - LF on POSIX, CRLF on Windows, CR on Macintosh). - @type nl: string or None + *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 = BytesIO() - self.to_file(temp_buffer, sorted, relativize, nl) + 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 @@ -569,9 +698,11 @@ class Zone(object): def check_origin(self): """Do some simple checking of the zone's origin. - @raises dns.zone.NoSOA: there is no SOA RR - @raises dns.zone.NoNS: there is no NS RRset - @raises KeyError: there is no origin node + 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 @@ -582,400 +713,366 @@ class Zone(object): if self.get_rdataset(name, dns.rdatatype.NS) is None: raise NoNS + def _compute_digest(self, hash_algorithm, scheme=DigestScheme.SIMPLE): + hashinfo = _digest_hashers.get(hash_algorithm) + if not hashinfo: + raise UnsupportedDigestHashAlgorithm + if scheme != DigestScheme.SIMPLE: + raise UnsupportedDigestScheme -class _MasterReader(object): - - """Read a DNS master file - - @ivar tok: The tokenizer - @type tok: dns.tokenizer.Tokenizer object - @ivar ttl: The default TTL - @type ttl: int - @ivar last_name: The last name read - @type last_name: dns.name.Name object - @ivar current_origin: The current origin - @type current_origin: dns.name.Name object - @ivar relativize: should names in the zone be relativized? - @type relativize: bool - @ivar zone: the zone - @type zone: dns.zone.Zone object - @ivar saved_state: saved reader state (used when processing $INCLUDE) - @type saved_state: list of (tokenizer, current_origin, last_name, file) - tuples. - @ivar current_file: the file object of the $INCLUDed file being parsed - (None if no $INCLUDE is active). - @ivar allow_include: is $INCLUDE allowed? - @type allow_include: bool - @ivar check_origin: should sanity checks of the origin node be done? - The default is True. - @type check_origin: bool - """ - - def __init__(self, tok, origin, rdclass, relativize, zone_factory=Zone, - allow_include=False, check_origin=True): - if isinstance(origin, string_types): - origin = dns.name.from_text(origin) - self.tok = tok - self.current_origin = origin - self.relativize = relativize - self.ttl = 0 - self.last_name = self.current_origin - self.zone = zone_factory(origin, rdclass, relativize=relativize) - self.saved_state = [] - self.current_file = None - self.allow_include = allow_include - self.check_origin = check_origin - - def _eat_line(self): - while 1: - token = self.tok.get() - if token.is_eol_or_eof(): - break - - def _rr_line(self): - """Process one line from a DNS master file.""" - # Name - if self.current_origin is None: - raise UnknownOrigin - token = self.tok.get(want_leading=True) - if not token.is_whitespace(): - self.last_name = dns.name.from_text( - token.value, 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) - token = self.tok.get() - if not token.is_identifier(): - raise dns.exception.SyntaxError - # TTL - try: - ttl = dns.ttl.from_text(token.value) - token = self.tok.get() - if not token.is_identifier(): - raise dns.exception.SyntaxError - except dns.ttl.BadTTL: - ttl = self.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) - except: - raise dns.exception.SyntaxError( - "unknown rdatatype '%s'" % token.value) - n = self.zone.nodes.get(name) - if n is None: - n = self.zone.node_factory() - self.zone.nodes[name] = n - try: - rd = dns.rdata.from_text(rdclass, rdtype, self.tok, - self.current_origin, False) - except dns.exception.SyntaxError: - # Catch and reraise. - (ty, va) = sys.exc_info()[:2] - raise va - except: - # 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))) + origin_name = dns.name.empty + else: + 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() - rd.choose_relativity(self.zone.origin, self.relativize) - covers = rd.covers() - rds = n.find_rdataset(rdclass, rdtype, covers, True) - rds.add(rd, ttl) + def compute_digest(self, hash_algorithm, scheme=DigestScheme.SIMPLE): + if self.relativize: + origin_name = dns.name.empty + else: + origin_name = self.origin + serial = self.get_rdataset(origin_name, dns.rdatatype.SOA)[0].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 _parse_modify(self, side): - # Here we catch everything in '{' '}' in a group so we can replace it - # with ''. - is_generate1 = re.compile("^.*\$({(\+|-?)(\d+),(\d+),(.)}).*$") - is_generate2 = re.compile("^.*\$({(\+|-?)(\d+)}).*$") - is_generate3 = re.compile("^.*\$({(\+|-?)(\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 = g1.groups() - if sign == '': - sign = '+' - width = g1.groups()[2] - base = 'd' - - if not (g1 or g2 or g3): - mod = '' - sign = '+' - offset = 0 - width = 0 - base = 'd' - - if base != 'd': - raise NotImplementedError() - - 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 - master 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: - raise dns.exception.SyntaxError - - # lhs (required) - try: - lhs = token.value - token = self.tok.get() - if not token.is_identifier(): - raise dns.exception.SyntaxError - except: - raise dns.exception.SyntaxError - - # TTL - try: - ttl = dns.ttl.from_text(token.value) - token = self.tok.get() - if not token.is_identifier(): - raise dns.exception.SyntaxError - except dns.ttl.BadTTL: - ttl = self.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) - - # lhs (required) - try: - rhs = token.value - except: - raise dns.exception.SyntaxError - - 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 - - if lsign == u'+': - lindex = i + int(loffset) - elif lsign == u'-': - lindex = i - int(loffset) - - if rsign == u'-': - rindex = i - int(roffset) - elif rsign == u'+': - rindex = i + int(roffset) - - lzfindex = str(lindex).zfill(int(lwidth)) - rzfindex = str(rindex).zfill(int(rwidth)) - - name = lhs.replace(u'$%s' % (lmod), lzfindex) - rdata = rhs.replace(u'$%s' % (rmod), rzfindex) - - self.last_name = dns.name.from_text(name, self.current_origin) - 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) - - n = self.zone.nodes.get(name) - if n is None: - n = self.zone.node_factory() - self.zone.nodes[name] = n + def verify_digest(self, zonemd=None): + if zonemd: + digests = [zonemd] + else: + digests = self.get_rdataset(self.origin, dns.rdatatype.ZONEMD) + if digests is None: + raise NoDigest + for digest in digests: try: - rd = dns.rdata.from_text(rdclass, rdtype, rdata, - self.current_origin, False) - except dns.exception.SyntaxError: - # Catch and reraise. - (ty, va) = sys.exc_info()[:2] - raise va - except: - # 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))) + computed = self._compute_digest(digest.hash_algorithm, + digest.scheme) + if computed == digest.digest: + return + except Exception: + pass + raise DigestVerificationFailure - rd.choose_relativity(self.zone.origin, self.relativize) - covers = rd.covers() - rds = n.find_rdataset(rdclass, rdtype, covers, True) - rds.add(rd, ttl) + # TransactionManager methods - def read(self): - """Read a DNS master file and build a zone object. + def reader(self): + return Transaction(self, False, + Version(self, 1, self.nodes, self.origin)) - @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 - """ + def writer(self, replacement=False): + txn = Transaction(self, replacement) + txn._setup_version() + return txn - 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.ttl) = 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] == u'$': - c = token.value.upper() - if c == u'$TTL': - token = self.tok.get() - if not token.is_identifier(): - raise dns.exception.SyntaxError("bad $TTL") - self.ttl = dns.ttl.from_text(token.value) - self.tok.get_eol() - elif c == u'$ORIGIN': - self.current_origin = self.tok.get_name() - self.tok.get_eol() - if self.zone.origin is None: - self.zone.origin = self.current_origin - elif c == u'$INCLUDE' and self.allow_include: - token = self.tok.get() - filename = token.value - token = self.tok.get() - if token.is_identifier(): - new_origin =\ - dns.name.from_text(token.value, - self.current_origin) - 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.ttl)) - self.current_file = open(filename, 'r') - self.tok = dns.tokenizer.Tokenizer(self.current_file, - filename) - self.current_origin = new_origin - elif c == u'$GENERATE': - self._generate_line() - else: - raise dns.exception.SyntaxError( - "Unknown master 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" - raise dns.exception.SyntaxError( - "%s:%d: %s" % (filename, line_number, detail)) + def origin_information(self): + if self.relativize: + effective = dns.name.empty + else: + effective = self.origin + return (self.origin, self.relativize, effective) - # Now that we're done reading, do some basic checking of the zone. - if self.check_origin: - self.zone.check_origin() + 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): + __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): + __slots__ = ['id'] + + 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, rdtype, covers=dns.rdatatype.NONE, + create=False): + if create: + raise TypeError("immutable") + return super().find_rdataset(rdclass, rdtype, covers, False) + + def get_rdataset(self, rdclass, rdtype, covers=dns.rdatatype.NONE, + create=False): + if create: + raise TypeError("immutable") + return super().get_rdataset(rdclass, rdtype, covers, False) + + def delete_rdataset(self, rdclass, rdtype, covers=dns.rdatatype.NONE): + raise TypeError("immutable") + + def replace_rdataset(self, replacement): + raise TypeError("immutable") + + def is_immutable(self): + return True + + +class Version: + def __init__(self, zone, id, nodes=None, origin=None): + self.zone = zone + self.id = id + if nodes is not None: + self.nodes = nodes + else: + self.nodes = {} + self.origin = origin + + def _validate_name(self, name): + if name.is_absolute(): + if not name.is_subdomain(self.zone.origin): + raise KeyError("name is not a subdomain of the zone origin") + if self.zone.relativize: + # XXXRTH should it be an error if self.origin is still None? + name = name.relativize(self.origin) + return name + + def get_node(self, name): + name = self._validate_name(name) + return self.nodes.get(name) + + def get_rdataset(self, name, rdtype, covers): + node = self.get_node(name) + if node is None: + return None + return node.get_rdataset(self.zone.rdclass, rdtype, covers) + + def items(self): + return self.nodes.items() + + +class WritableVersion(Version): + def __init__(self, zone, replacement=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() + + def _maybe_cow(self, name): + name = self._validate_name(name) + node = self.nodes.get(name) + if node is None or name not in self.changed: + new_node = self.zone.node_factory() + if hasattr(new_node, 'id'): + # We keep doing this for backwards compatibility, as earlier + # code used new_node.id != self.id for the "do we need to CoW?" + # test. Now we use the changed set as this works with both + # regular zones and versioned zones. + new_node.id = self.id + 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): + name = self._validate_name(name) + if name in self.nodes: + del self.nodes[name] + self.changed.add(name) + + def put_rdataset(self, name, rdataset): + node = self._maybe_cow(name) + node.replace_rdataset(rdataset) + + def delete_rdataset(self, name, rdtype, covers): + 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): + # 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) + self.nodes = dns.immutable.Dict(version.nodes, True) + + +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 + self.version = WritableVersion(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: + version = ImmutableVersion(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 _get_node(self, name): + return self.version.get_node(name) def from_text(text, origin=None, rdclass=dns.rdataclass.IN, relativize=True, zone_factory=Zone, filename=None, - allow_include=False, check_origin=True): - """Build a zone object from a master file format string. + allow_include=False, check_origin=True, idna_codec=None): + """Build a zone object from a zone file format string. - @param text: the master file format input - @type text: string. - @param origin: The origin of the zone; if not specified, the first - $ORIGIN statement in the master file will determine the origin of the - zone. - @type origin: dns.name.Name object or string - @param rdclass: The zone's rdata class; the default is class IN. - @type rdclass: int - @param relativize: should names be relativized? The default is True - @type relativize: bool - @param zone_factory: The zone factory to use - @type zone_factory: function returning a Zone - @param filename: The filename to emit when describing where an error - occurred; the default is ''. - @type filename: string - @param allow_include: is $INCLUDE allowed? - @type allow_include: bool - @param check_origin: should sanity checks of the origin node be done? - The default is True. - @type check_origin: bool - @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 - @rtype: dns.zone.Zone object + *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*, 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. + + 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``. """ # 'text' can also be a file, but we don't publish that fact @@ -984,82 +1081,98 @@ def from_text(text, origin=None, rdclass=dns.rdataclass.IN, if filename is None: filename = '' - tok = dns.tokenizer.Tokenizer(text, filename) - reader = _MasterReader(tok, origin, rdclass, relativize, zone_factory, - allow_include=allow_include, - check_origin=check_origin) - reader.read() - return reader.zone + 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) + 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_file(f, origin=None, rdclass=dns.rdataclass.IN, relativize=True, zone_factory=Zone, filename=None, allow_include=True, check_origin=True): - """Read a master file and build a zone object. + """Read a zone file and build a zone object. - @param f: file or string. If I{f} is a string, it is treated + *f*, a file or ``str``. If *f* is a string, it is treated as the name of a file to open. - @param origin: The origin of the zone; if not specified, the first - $ORIGIN statement in the master file will determine the origin of the - zone. - @type origin: dns.name.Name object or string - @param rdclass: The zone's rdata class; the default is class IN. - @type rdclass: int - @param relativize: should names be relativized? The default is True - @type relativize: bool - @param zone_factory: The zone factory to use - @type zone_factory: function returning a Zone - @param filename: The filename to emit when describing where an error - occurred; the default is '', or the value of I{f} if I{f} is a - string. - @type filename: string - @param allow_include: is $INCLUDE allowed? - @type allow_include: bool - @param check_origin: should sanity checks of the origin node be done? - The default is True. - @type check_origin: bool - @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 - @rtype: dns.zone.Zone object + + *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. + + 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``. """ - str_type = string_types - opts = 'rU' - - if isinstance(f, str_type): - if filename is None: - filename = f - f = open(f, opts) - want_close = True - else: - if filename is None: - filename = '' - want_close = False - - try: - z = from_text(f, origin, rdclass, relativize, zone_factory, - filename, allow_include, check_origin) - finally: - if want_close: - f.close() - return z + with contextlib.ExitStack() as stack: + if isinstance(f, str): + if filename is None: + filename = f + f = stack.enter_context(open(f)) + return from_text(f, origin, rdclass, relativize, zone_factory, + filename, allow_include, check_origin) def from_xfr(xfr, zone_factory=Zone, relativize=True, check_origin=True): """Convert the output of a zone transfer generator into a zone object. - @param xfr: The xfr generator - @type xfr: generator of dns.message.Message objects - @param relativize: should names be relativized? The default is True. + *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 dns.query.xfr(). - @type relativize: bool - @param check_origin: should sanity checks of the origin node be done? - The default is True. - @type check_origin: bool - @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 - @rtype: dns.zone.Zone object + 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. + + Returns a subclass of ``dns.zone.Zone``. """ z = None @@ -1080,7 +1193,6 @@ def from_xfr(xfr, zone_factory=Zone, relativize=True, check_origin=True): rrset.covers, True) zrds.update_ttl(rrset.ttl) for rd in rrset: - rd.choose_relativity(z.origin, relativize) zrds.add(rd) if check_origin: z.check_origin() diff --git a/libs/dns/zone.pyi b/libs/dns/zone.pyi new file mode 100644 index 000000000..272814fed --- /dev/null +++ b/libs/dns/zone.pyi @@ -0,0 +1,55 @@ +from typing import Generator, Optional, Union, Tuple, Iterable, Callable, Any, Iterator, TextIO, BinaryIO, Dict +from . import rdata, zone, rdataclass, name, rdataclass, message, rdatatype, exception, node, rdataset, rrset, rdatatype + +class BadZone(exception.DNSException): ... +class NoSOA(BadZone): ... +class NoNS(BadZone): ... +class UnknownOrigin(BadZone): ... + +class Zone: + def __getitem__(self, key : str) -> node.Node: + ... + def __init__(self, origin : Union[str,name.Name], rdclass : int = rdataclass.IN, relativize : bool = True) -> None: + self.nodes : Dict[str,node.Node] + self.origin = origin + def values(self): + return self.nodes.values() + def iterate_rdatas(self, rdtype : Union[int,str] = rdatatype.ANY, covers : Union[int,str] = None) -> Iterable[Tuple[name.Name, int, rdata.Rdata]]: + ... + def __iter__(self) -> Iterator[str]: + ... + def get_node(self, name : Union[name.Name,str], create=False) -> Optional[node.Node]: + ... + def find_rrset(self, name : Union[str,name.Name], rdtype : Union[int,str], covers=rdatatype.NONE) -> rrset.RRset: + ... + def find_rdataset(self, name : Union[str,name.Name], rdtype : Union[str,int], covers=rdatatype.NONE, + create=False) -> rdataset.Rdataset: + ... + def get_rdataset(self, name : Union[str,name.Name], rdtype : Union[str,int], covers=rdatatype.NONE, create=False) -> Optional[rdataset.Rdataset]: + ... + def get_rrset(self, name : Union[str,name.Name], rdtype : Union[str,int], covers=rdatatype.NONE) -> Optional[rrset.RRset]: + ... + def replace_rdataset(self, name : Union[str,name.Name], replacement : rdataset.Rdataset) -> None: + ... + def delete_rdataset(self, name : Union[str,name.Name], rdtype : Union[str,int], covers=rdatatype.NONE) -> None: + ... + def iterate_rdatasets(self, rdtype : Union[str,int] =rdatatype.ANY, + covers : Union[str,int] =rdatatype.NONE): + ... + def to_file(self, f : Union[TextIO, BinaryIO, str], sorted=True, relativize=True, nl : Optional[bytes] = None): + ... + def to_text(self, sorted=True, relativize=True, nl : Optional[str] = None) -> str: + ... + +def from_xfr(xfr : Generator[Any,Any,message.Message], zone_factory : Callable[..., zone.Zone] = zone.Zone, relativize=True, check_origin=True): + ... + +def from_text(text : str, origin : Optional[Union[str,name.Name]] = None, rdclass : int = rdataclass.IN, + relativize=True, zone_factory : Callable[...,zone.Zone] = zone.Zone, filename : Optional[str] = None, + allow_include=False, check_origin=True) -> zone.Zone: + ... + +def from_file(f, origin : Optional[Union[str,name.Name]] = None, rdclass=rdataclass.IN, + relativize=True, zone_factory : Callable[..., zone.Zone] = Zone, filename : Optional[str] = None, + allow_include=True, check_origin=True) -> zone.Zone: + ... diff --git a/libs/dns/zonefile.py b/libs/dns/zonefile.py new file mode 100644 index 000000000..53b40880b --- /dev/null +++ b/libs/dns/zonefile.py @@ -0,0 +1,624 @@ +# 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 + +import dns.exception +import dns.name +import dns.node +import dns.rdataclass +import dns.rdatatype +import dns.rdata +import dns.rdtypes.ANY.SOA +import dns.rrset +import dns.tokenizer +import dns.transaction +import dns.ttl +import dns.grange + + +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 + + +class Reader: + + """Read a DNS zone file into a transaction.""" + + def __init__(self, tok, rdclass, txn, allow_include=False, + allow_directives=True, force_name=None, + force_ttl=None, force_rdclass=None, force_rdtype=None, + default_ttl=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 = [] + self.current_file = None + self.allow_include = allow_include + self.allow_directives = 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: + if self.default_ttl_known: + ttl = self.default_ttl + elif self.last_ttl_known: + ttl = self.last_ttl + 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") + + # 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): + # 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' + + if base != 'd': + raise NotImplementedError() + + 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 + + # The code currently only supports base 'd', so the last value + # in the tuple _parse_modify returns is ignored + lmod, lsign, loffset, lwidth, _ = self._parse_modify(lhs) + rmod, rsign, roffset, rwidth, _ = self._parse_modify(rhs) + for i in range(start, stop + 1, step): + # +1 because bind is inclusive and python is exclusive + + if lsign == '+': + lindex = i + int(loffset) + elif lsign == '-': + lindex = i - int(loffset) + + if rsign == '-': + rindex = i - int(roffset) + elif rsign == '+': + rindex = i + int(roffset) + + lzfindex = str(lindex).zfill(int(lwidth)) + rzfindex = str(rindex).zfill(int(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): + """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 self.allow_directives: + c = token.value.upper() + 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' and self.allow_include: + token = self.tok.get() + filename = token.value + token = self.tok.get() + 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( + "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 + + +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 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, name=None, ttl=None, rdclass=dns.rdataclass.IN, + default_rdclass=dns.rdataclass.IN, + rdtype=None, default_ttl=None, idna_codec=None, + origin=dns.name.root, relativize=False): + """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) + default_rdclass = dns.rdataclass.RdataClass.make(default_rdclass) + if rdtype is not None: + rdtype = dns.rdatatype.RdataType.make(rdtype) + 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/libs/dogpile/__init__.py b/libs/dogpile/__init__.py index fc8fd4524..650a6ce95 100644 --- a/libs/dogpile/__init__.py +++ b/libs/dogpile/__init__.py @@ -1,4 +1,4 @@ -__version__ = '0.7.1' +__version__ = "1.1.5" from .lock import Lock # noqa from .lock import NeedRegenerationException # noqa diff --git a/libs/dogpile/cache/__init__.py b/libs/dogpile/cache/__init__.py index fb57cbcc2..91c3a82c9 100644 --- a/libs/dogpile/cache/__init__.py +++ b/libs/dogpile/cache/__init__.py @@ -1,4 +1,6 @@ -from .region import CacheRegion, register_backend, make_region # noqa +from .region import CacheRegion # noqa +from .region import make_region # noqa +from .region import register_backend # noqa +from .. import __version__ # noqa # backwards compat -from .. import __version__ # noqa diff --git a/libs/dogpile/cache/api.py b/libs/dogpile/cache/api.py index d66e5a707..0717d4394 100644 --- a/libs/dogpile/cache/api.py +++ b/libs/dogpile/cache/api.py @@ -1,14 +1,22 @@ -import operator -from ..util.compat import py3k +import abc +import pickle +from typing import Any +from typing import Callable +from typing import cast +from typing import Mapping +from typing import NamedTuple +from typing import Optional +from typing import Sequence +from typing import Union -class NoValue(object): +class NoValue: """Describe a missing cache value. - The :attr:`.NO_VALUE` module global - should be used. + The :data:`.NO_VALUE` constant should be used. """ + @property def payload(self): return self @@ -18,49 +26,125 @@ class NoValue(object): fill another cache key. """ - return '' + return "" - if py3k: - def __bool__(self): # pragma NO COVERAGE - return False - else: - def __nonzero__(self): # pragma NO COVERAGE - return False + def __bool__(self): # pragma NO COVERAGE + return False NO_VALUE = NoValue() """Value returned from ``get()`` that describes a key not present.""" +MetaDataType = Mapping[str, Any] -class CachedValue(tuple): + +KeyType = str +"""A cache key.""" + +ValuePayload = Any +"""An object to be placed in the cache against a key.""" + + +KeyManglerType = Callable[[KeyType], KeyType] +Serializer = Callable[[ValuePayload], bytes] +Deserializer = Callable[[bytes], ValuePayload] + + +class CacheMutex(abc.ABC): + """Describes a mutexing object with acquire and release methods. + + This is an abstract base class; any object that has acquire/release + methods may be used. + + .. versionadded:: 1.1 + + + .. seealso:: + + :meth:`.CacheBackend.get_mutex` - the backend method that optionally + returns this locking object. + + """ + + @abc.abstractmethod + def acquire(self, wait: bool = True) -> bool: + """Acquire the mutex. + + :param wait: if True, block until available, else return True/False + immediately. + + :return: True if the lock succeeded. + + """ + raise NotImplementedError() + + @abc.abstractmethod + def release(self) -> None: + """Release the mutex.""" + + raise NotImplementedError() + + @abc.abstractmethod + def locked(self) -> bool: + """Check if the mutex was acquired. + + :return: true if the lock is acquired. + + .. versionadded:: 1.1.2 + + """ + raise NotImplementedError() + + @classmethod + def __subclasshook__(cls, C): + return hasattr(C, "acquire") and hasattr(C, "release") + + +class CachedValue(NamedTuple): """Represent a value stored in the cache. :class:`.CachedValue` is a two-tuple of ``(payload, metadata)``, where ``metadata`` is dogpile.cache's tracking information ( - currently the creation time). The metadata - and tuple structure is pickleable, if - the backend requires serialization. + currently the creation time). """ - payload = property(operator.itemgetter(0)) - """Named accessor for the payload.""" - metadata = property(operator.itemgetter(1)) - """Named accessor for the dogpile.cache metadata dictionary.""" + payload: ValuePayload - def __new__(cls, payload, metadata): - return tuple.__new__(cls, (payload, metadata)) - - def __reduce__(self): - return CachedValue, (self.payload, self.metadata) + metadata: MetaDataType -class CacheBackend(object): - """Base class for backend implementations.""" +CacheReturnType = Union[CachedValue, NoValue] +"""The non-serialized form of what may be returned from a backend +get method. - key_mangler = None +""" + +SerializedReturnType = Union[bytes, NoValue] +"""the serialized form of what may be returned from a backend get method.""" + +BackendFormatted = Union[CacheReturnType, SerializedReturnType] +"""Describes the type returned from the :meth:`.CacheBackend.get` method.""" + +BackendSetType = Union[CachedValue, bytes] +"""Describes the value argument passed to the :meth:`.CacheBackend.set` +method.""" + +BackendArguments = Mapping[str, Any] + + +class CacheBackend: + """Base class for backend implementations. + + Backends which set and get Python object values should subclass this + backend. For backends in which the value that's stored is ultimately + a stream of bytes, the :class:`.BytesBackend` should be used. + + """ + + key_mangler: Optional[Callable[[KeyType], KeyType]] = None """Key mangling function. May be None, or otherwise declared @@ -68,7 +152,23 @@ class CacheBackend(object): """ - def __init__(self, arguments): # pragma NO COVERAGE + serializer: Union[None, Serializer] = None + """Serializer function that will be used by default if not overridden + by the region. + + .. versionadded:: 1.1 + + """ + + deserializer: Union[None, Deserializer] = None + """deserializer function that will be used by default if not overridden + by the region. + + .. versionadded:: 1.1 + + """ + + def __init__(self, arguments: BackendArguments): # pragma NO COVERAGE """Construct a new :class:`.CacheBackend`. Subclasses should override this to @@ -91,10 +191,10 @@ class CacheBackend(object): ) ) - def has_lock_timeout(self): + def has_lock_timeout(self) -> bool: return False - def get_mutex(self, key): + def get_mutex(self, key: KeyType) -> Optional[CacheMutex]: """Return an optional mutexing object for the given key. This object need only provide an ``acquire()`` @@ -127,48 +227,141 @@ class CacheBackend(object): """ return None - def get(self, key): # pragma NO COVERAGE - """Retrieve a value from the cache. + def get(self, key: KeyType) -> BackendFormatted: # pragma NO COVERAGE + """Retrieve an optionally serialized value from the cache. - The returned value should be an instance of - :class:`.CachedValue`, or ``NO_VALUE`` if - not present. + :param key: String key that was passed to the :meth:`.CacheRegion.get` + method, which will also be processed by the "key mangling" function + if one was present. + + :return: the Python object that corresponds to + what was established via the :meth:`.CacheBackend.set` method, + or the :data:`.NO_VALUE` constant if not present. + + If a serializer is in use, this method will only be called if the + :meth:`.CacheBackend.get_serialized` method is not overridden. """ raise NotImplementedError() - def get_multi(self, keys): # pragma NO COVERAGE - """Retrieve multiple values from the cache. + def get_multi( + self, keys: Sequence[KeyType] + ) -> Sequence[BackendFormatted]: # pragma NO COVERAGE + """Retrieve multiple optionally serialized values from the cache. - The returned value should be a list, corresponding - to the list of keys given. + :param keys: sequence of string keys that was passed to the + :meth:`.CacheRegion.get_multi` method, which will also be processed + by the "key mangling" function if one was present. + + :return a list of values as would be returned + individually via the :meth:`.CacheBackend.get` method, corresponding + to the list of keys given. + + If a serializer is in use, this method will only be called if the + :meth:`.CacheBackend.get_serialized_multi` method is not overridden. .. versionadded:: 0.5.0 """ raise NotImplementedError() - def set(self, key, value): # pragma NO COVERAGE - """Set a value in the cache. + def get_serialized(self, key: KeyType) -> SerializedReturnType: + """Retrieve a serialized value from the cache. - The key will be whatever was passed - to the registry, processed by the - "key mangling" function, if any. - The value will always be an instance - of :class:`.CachedValue`. + :param key: String key that was passed to the :meth:`.CacheRegion.get` + method, which will also be processed by the "key mangling" function + if one was present. + + :return: a bytes object, or :data:`.NO_VALUE` + constant if not present. + + The default implementation of this method for :class:`.CacheBackend` + returns the value of the :meth:`.CacheBackend.get` method. + + .. versionadded:: 1.1 + + .. seealso:: + + :class:`.BytesBackend` + + """ + return cast(SerializedReturnType, self.get(key)) + + def get_serialized_multi( + self, keys: Sequence[KeyType] + ) -> Sequence[SerializedReturnType]: # pragma NO COVERAGE + """Retrieve multiple serialized values from the cache. + + :param keys: sequence of string keys that was passed to the + :meth:`.CacheRegion.get_multi` method, which will also be processed + by the "key mangling" function if one was present. + + :return: list of bytes objects + + The default implementation of this method for :class:`.CacheBackend` + returns the value of the :meth:`.CacheBackend.get_multi` method. + + .. versionadded:: 1.1 + + .. seealso:: + + :class:`.BytesBackend` + + """ + return cast(Sequence[SerializedReturnType], self.get_multi(keys)) + + def set( + self, key: KeyType, value: BackendSetType + ) -> None: # pragma NO COVERAGE + """Set an optionally serialized value in the cache. + + :param key: String key that was passed to the :meth:`.CacheRegion.set` + method, which will also be processed by the "key mangling" function + if one was present. + + :param value: The optionally serialized :class:`.CachedValue` object. + May be an instance of :class:`.CachedValue` or a bytes object + depending on if a serializer is in use with the region and if the + :meth:`.CacheBackend.set_serialized` method is not overridden. + + .. seealso:: + + :meth:`.CacheBackend.set_serialized` """ raise NotImplementedError() - def set_multi(self, mapping): # pragma NO COVERAGE + def set_serialized( + self, key: KeyType, value: bytes + ) -> None: # pragma NO COVERAGE + """Set a serialized value in the cache. + + :param key: String key that was passed to the :meth:`.CacheRegion.set` + method, which will also be processed by the "key mangling" function + if one was present. + + :param value: a bytes object to be stored. + + The default implementation of this method for :class:`.CacheBackend` + calls upon the :meth:`.CacheBackend.set` method. + + .. versionadded:: 1.1 + + .. seealso:: + + :class:`.BytesBackend` + + """ + self.set(key, value) + + def set_multi( + self, mapping: Mapping[KeyType, BackendSetType] + ) -> None: # pragma NO COVERAGE """Set multiple values in the cache. - ``mapping`` is a dict in which - the key will be whatever was passed - to the registry, processed by the - "key mangling" function, if any. - The value will always be an instance - of :class:`.CachedValue`. + :param mapping: a dict in which the key will be whatever was passed to + the :meth:`.CacheRegion.set_multi` method, processed by the "key + mangling" function, if any. When implementing a new :class:`.CacheBackend` or cutomizing via :class:`.ProxyBackend`, be aware that when this method is invoked by @@ -178,17 +371,52 @@ class CacheBackend(object): -- that will have the undesirable effect of modifying the returned values as well. + If a serializer is in use, this method will only be called if the + :meth:`.CacheBackend.set_serialized_multi` method is not overridden. + + .. versionadded:: 0.5.0 """ raise NotImplementedError() - def delete(self, key): # pragma NO COVERAGE + def set_serialized_multi( + self, mapping: Mapping[KeyType, bytes] + ) -> None: # pragma NO COVERAGE + """Set multiple serialized values in the cache. + + :param mapping: a dict in which the key will be whatever was passed to + the :meth:`.CacheRegion.set_multi` method, processed by the "key + mangling" function, if any. + + When implementing a new :class:`.CacheBackend` or cutomizing via + :class:`.ProxyBackend`, be aware that when this method is invoked by + :meth:`.Region.get_or_create_multi`, the ``mapping`` values are the + same ones returned to the upstream caller. If the subclass alters the + values in any way, it must not do so 'in-place' on the ``mapping`` dict + -- that will have the undesirable effect of modifying the returned + values as well. + + .. versionadded:: 1.1 + + The default implementation of this method for :class:`.CacheBackend` + calls upon the :meth:`.CacheBackend.set_multi` method. + + .. seealso:: + + :class:`.BytesBackend` + + + """ + self.set_multi(mapping) + + def delete(self, key: KeyType) -> None: # pragma NO COVERAGE """Delete a value from the cache. - The key will be whatever was passed - to the registry, processed by the - "key mangling" function, if any. + :param key: String key that was passed to the + :meth:`.CacheRegion.delete` + method, which will also be processed by the "key mangling" function + if one was present. The behavior here should be idempotent, that is, can be called any number of times @@ -197,12 +425,14 @@ class CacheBackend(object): """ raise NotImplementedError() - def delete_multi(self, keys): # pragma NO COVERAGE + def delete_multi( + self, keys: Sequence[KeyType] + ) -> None: # pragma NO COVERAGE """Delete multiple values from the cache. - The key will be whatever was passed - to the registry, processed by the - "key mangling" function, if any. + :param keys: sequence of string keys that was passed to the + :meth:`.CacheRegion.delete_multi` method, which will also be processed + by the "key mangling" function if one was present. The behavior here should be idempotent, that is, can be called any number of times @@ -213,3 +443,95 @@ class CacheBackend(object): """ raise NotImplementedError() + + +class DefaultSerialization: + serializer: Union[None, Serializer] = staticmethod( # type: ignore + pickle.dumps + ) + deserializer: Union[None, Deserializer] = staticmethod( # type: ignore + pickle.loads + ) + + +class BytesBackend(DefaultSerialization, CacheBackend): + """A cache backend that receives and returns series of bytes. + + This backend only supports the "serialized" form of values; subclasses + should implement :meth:`.BytesBackend.get_serialized`, + :meth:`.BytesBackend.get_serialized_multi`, + :meth:`.BytesBackend.set_serialized`, + :meth:`.BytesBackend.set_serialized_multi`. + + .. versionadded:: 1.1 + + """ + + def get_serialized(self, key: KeyType) -> SerializedReturnType: + """Retrieve a serialized value from the cache. + + :param key: String key that was passed to the :meth:`.CacheRegion.get` + method, which will also be processed by the "key mangling" function + if one was present. + + :return: a bytes object, or :data:`.NO_VALUE` + constant if not present. + + .. versionadded:: 1.1 + + """ + raise NotImplementedError() + + def get_serialized_multi( + self, keys: Sequence[KeyType] + ) -> Sequence[SerializedReturnType]: # pragma NO COVERAGE + """Retrieve multiple serialized values from the cache. + + :param keys: sequence of string keys that was passed to the + :meth:`.CacheRegion.get_multi` method, which will also be processed + by the "key mangling" function if one was present. + + :return: list of bytes objects + + .. versionadded:: 1.1 + + """ + raise NotImplementedError() + + def set_serialized( + self, key: KeyType, value: bytes + ) -> None: # pragma NO COVERAGE + """Set a serialized value in the cache. + + :param key: String key that was passed to the :meth:`.CacheRegion.set` + method, which will also be processed by the "key mangling" function + if one was present. + + :param value: a bytes object to be stored. + + .. versionadded:: 1.1 + + """ + raise NotImplementedError() + + def set_serialized_multi( + self, mapping: Mapping[KeyType, bytes] + ) -> None: # pragma NO COVERAGE + """Set multiple serialized values in the cache. + + :param mapping: a dict in which the key will be whatever was passed to + the :meth:`.CacheRegion.set_multi` method, processed by the "key + mangling" function, if any. + + When implementing a new :class:`.CacheBackend` or cutomizing via + :class:`.ProxyBackend`, be aware that when this method is invoked by + :meth:`.Region.get_or_create_multi`, the ``mapping`` values are the + same ones returned to the upstream caller. If the subclass alters the + values in any way, it must not do so 'in-place' on the ``mapping`` dict + -- that will have the undesirable effect of modifying the returned + values as well. + + .. versionadded:: 1.1 + + """ + raise NotImplementedError() diff --git a/libs/dogpile/cache/backends/__init__.py b/libs/dogpile/cache/backends/__init__.py index 041f05a3e..e3d90400b 100644 --- a/libs/dogpile/cache/backends/__init__.py +++ b/libs/dogpile/cache/backends/__init__.py @@ -1,22 +1,47 @@ -from dogpile.cache.region import register_backend +from ...util import PluginLoader + +_backend_loader = PluginLoader("dogpile.cache") +register_backend = _backend_loader.register register_backend( - "dogpile.cache.null", "dogpile.cache.backends.null", "NullBackend") + "dogpile.cache.null", "dogpile.cache.backends.null", "NullBackend" +) register_backend( - "dogpile.cache.dbm", "dogpile.cache.backends.file", "DBMBackend") + "dogpile.cache.dbm", "dogpile.cache.backends.file", "DBMBackend" +) register_backend( - "dogpile.cache.pylibmc", "dogpile.cache.backends.memcached", - "PylibmcBackend") + "dogpile.cache.pylibmc", + "dogpile.cache.backends.memcached", + "PylibmcBackend", +) register_backend( - "dogpile.cache.bmemcached", "dogpile.cache.backends.memcached", - "BMemcachedBackend") + "dogpile.cache.bmemcached", + "dogpile.cache.backends.memcached", + "BMemcachedBackend", +) register_backend( - "dogpile.cache.memcached", "dogpile.cache.backends.memcached", - "MemcachedBackend") + "dogpile.cache.memcached", + "dogpile.cache.backends.memcached", + "MemcachedBackend", +) register_backend( - "dogpile.cache.memory", "dogpile.cache.backends.memory", "MemoryBackend") + "dogpile.cache.pymemcache", + "dogpile.cache.backends.memcached", + "PyMemcacheBackend", +) register_backend( - "dogpile.cache.memory_pickle", "dogpile.cache.backends.memory", - "MemoryPickleBackend") + "dogpile.cache.memory", "dogpile.cache.backends.memory", "MemoryBackend" +) register_backend( - "dogpile.cache.redis", "dogpile.cache.backends.redis", "RedisBackend") + "dogpile.cache.memory_pickle", + "dogpile.cache.backends.memory", + "MemoryPickleBackend", +) +register_backend( + "dogpile.cache.redis", "dogpile.cache.backends.redis", "RedisBackend" +) +register_backend( + "dogpile.cache.redis_sentinel", + "dogpile.cache.backends.redis", + "RedisSentinelBackend", +) diff --git a/libs/dogpile/cache/backends/file.py b/libs/dogpile/cache/backends/file.py index 309c055a2..bc52d8bc6 100644 --- a/libs/dogpile/cache/backends/file.py +++ b/libs/dogpile/cache/backends/file.py @@ -7,16 +7,20 @@ Provides backends that deal with local filesystem access. """ from __future__ import with_statement -from ..api import CacheBackend, NO_VALUE + from contextlib import contextmanager -from ...util import compat -from ... import util +import dbm import os +import threading -__all__ = 'DBMBackend', 'FileLock', 'AbstractFileLock' +from ..api import BytesBackend +from ..api import NO_VALUE +from ... import util + +__all__ = ["DBMBackend", "FileLock", "AbstractFileLock"] -class DBMBackend(CacheBackend): +class DBMBackend(BytesBackend): """A file-backend using a dbm file to store keys. Basic usage:: @@ -134,28 +138,25 @@ class DBMBackend(CacheBackend): """ + def __init__(self, arguments): self.filename = os.path.abspath( - os.path.normpath(arguments['filename']) + os.path.normpath(arguments["filename"]) ) dir_, filename = os.path.split(self.filename) self.lock_factory = arguments.get("lock_factory", FileLock) self._rw_lock = self._init_lock( - arguments.get('rw_lockfile'), - ".rw.lock", dir_, filename) + arguments.get("rw_lockfile"), ".rw.lock", dir_, filename + ) self._dogpile_lock = self._init_lock( - arguments.get('dogpile_lockfile'), + arguments.get("dogpile_lockfile"), ".dogpile.lock", - dir_, filename, - util.KeyReentrantMutex.factory) + dir_, + filename, + util.KeyReentrantMutex.factory, + ) - # TODO: make this configurable - if compat.py3k: - import dbm - else: - import anydbm as dbm - self.dbmmodule = dbm self._init_dbm_file() def _init_lock(self, argument, suffix, basedir, basefile, wrapper=None): @@ -163,9 +164,8 @@ class DBMBackend(CacheBackend): lock = self.lock_factory(os.path.join(basedir, basefile + suffix)) elif argument is not False: lock = self.lock_factory( - os.path.abspath( - os.path.normpath(argument) - )) + os.path.abspath(os.path.normpath(argument)) + ) else: return None if wrapper: @@ -175,12 +175,12 @@ class DBMBackend(CacheBackend): def _init_dbm_file(self): exists = os.access(self.filename, os.F_OK) if not exists: - for ext in ('db', 'dat', 'pag', 'dir'): + for ext in ("db", "dat", "pag", "dir"): if os.access(self.filename + os.extsep + ext, os.F_OK): exists = True break if not exists: - fh = self.dbmmodule.open(self.filename, 'c') + fh = dbm.open(self.filename, "c") fh.close() def get_mutex(self, key): @@ -210,57 +210,50 @@ class DBMBackend(CacheBackend): @contextmanager def _dbm_file(self, write): with self._use_rw_lock(write): - dbm = self.dbmmodule.open( - self.filename, - "w" if write else "r") - yield dbm - dbm.close() + with dbm.open(self.filename, "w" if write else "r") as dbm_obj: + yield dbm_obj - def get(self, key): - with self._dbm_file(False) as dbm: - if hasattr(dbm, 'get'): - value = dbm.get(key, NO_VALUE) + def get_serialized(self, key): + with self._dbm_file(False) as dbm_obj: + if hasattr(dbm_obj, "get"): + value = dbm_obj.get(key, NO_VALUE) else: # gdbm objects lack a .get method try: - value = dbm[key] + value = dbm_obj[key] except KeyError: value = NO_VALUE - if value is not NO_VALUE: - value = compat.pickle.loads(value) return value - def get_multi(self, keys): - return [self.get(key) for key in keys] + def get_serialized_multi(self, keys): + return [self.get_serialized(key) for key in keys] - def set(self, key, value): - with self._dbm_file(True) as dbm: - dbm[key] = compat.pickle.dumps(value, - compat.pickle.HIGHEST_PROTOCOL) + def set_serialized(self, key, value): + with self._dbm_file(True) as dbm_obj: + dbm_obj[key] = value - def set_multi(self, mapping): - with self._dbm_file(True) as dbm: + def set_serialized_multi(self, mapping): + with self._dbm_file(True) as dbm_obj: for key, value in mapping.items(): - dbm[key] = compat.pickle.dumps(value, - compat.pickle.HIGHEST_PROTOCOL) + dbm_obj[key] = value def delete(self, key): - with self._dbm_file(True) as dbm: + with self._dbm_file(True) as dbm_obj: try: - del dbm[key] + del dbm_obj[key] except KeyError: pass def delete_multi(self, keys): - with self._dbm_file(True) as dbm: + with self._dbm_file(True) as dbm_obj: for key in keys: try: - del dbm[key] + del dbm_obj[key] except KeyError: pass -class AbstractFileLock(object): +class AbstractFileLock: """Coordinate read/write access to a file. typically is a file-based lock but doesn't necessarily have to be. @@ -392,17 +385,18 @@ class FileLock(AbstractFileLock): """ def __init__(self, filename): - self._filedescriptor = compat.threading.local() + self._filedescriptor = threading.local() self.filename = filename @util.memoized_property def _module(self): import fcntl + return fcntl @property def is_open(self): - return hasattr(self._filedescriptor, 'fileno') + return hasattr(self._filedescriptor, "fileno") def acquire_read_lock(self, wait): return self._acquire(wait, os.O_RDONLY, self._module.LOCK_SH) diff --git a/libs/dogpile/cache/backends/memcached.py b/libs/dogpile/cache/backends/memcached.py index 6758a9980..8163d705b 100644 --- a/libs/dogpile/cache/backends/memcached.py +++ b/libs/dogpile/cache/backends/memcached.py @@ -6,23 +6,43 @@ Provides backends for talking to `memcached `_. """ -from ..api import CacheBackend, NO_VALUE -from ...util import compat -from ... import util import random +import threading import time +import typing +from typing import Any +from typing import Mapping +import warnings -__all__ = 'GenericMemcachedBackend', 'MemcachedBackend',\ - 'PylibmcBackend', 'BMemcachedBackend', 'MemcachedLock' +from ..api import CacheBackend +from ..api import NO_VALUE +from ... import util + + +if typing.TYPE_CHECKING: + import bmemcached + import memcache + import pylibmc + import pymemcache +else: + # delayed import + bmemcached = None # noqa F811 + memcache = None # noqa F811 + pylibmc = None # noqa F811 + pymemcache = None # noqa F811 + +__all__ = ( + "GenericMemcachedBackend", + "MemcachedBackend", + "PylibmcBackend", + "PyMemcacheBackend", + "BMemcachedBackend", + "MemcachedLock", +) class MemcachedLock(object): - """Simple distributed lock using memcached. - - This is an adaptation of the lock featured at - http://amix.dk/blog/post/19386 - - """ + """Simple distributed lock using memcached.""" def __init__(self, client_fn, key, timeout=0): self.client_fn = client_fn @@ -43,6 +63,10 @@ class MemcachedLock(object): if i < 15: i += 1 + def locked(self): + client = self.client_fn() + return client.get(self.key) is not None + def release(self): client = self.client_fn() client.delete(self.key) @@ -100,10 +124,17 @@ class GenericMemcachedBackend(CacheBackend): """ - set_arguments = {} + set_arguments: Mapping[str, Any] = {} """Additional arguments which will be passed to the :meth:`set` method.""" + # No need to override serializer, as all the memcached libraries + # handles that themselves. Still, we support customizing the + # serializer/deserializer to use better default pickle protocol + # or completely different serialization mechanism + serializer = None + deserializer = None + def __init__(self, arguments): self._imports() # using a plain threading.local here. threading.local @@ -111,11 +142,10 @@ class GenericMemcachedBackend(CacheBackend): # so the idea is that this is superior to pylibmc's # own ThreadMappedPool which doesn't handle this # automatically. - self.url = util.to_list(arguments['url']) - self.distributed_lock = arguments.get('distributed_lock', False) - self.lock_timeout = arguments.get('lock_timeout', 0) - self.memcached_expire_time = arguments.get( - 'memcached_expire_time', 0) + self.url = util.to_list(arguments["url"]) + self.distributed_lock = arguments.get("distributed_lock", False) + self.lock_timeout = arguments.get("lock_timeout", 0) + self.memcached_expire_time = arguments.get("memcached_expire_time", 0) def has_lock_timeout(self): return self.lock_timeout != 0 @@ -132,7 +162,7 @@ class GenericMemcachedBackend(CacheBackend): def _clients(self): backend = self - class ClientPool(compat.threading.local): + class ClientPool(threading.local): def __init__(self): self.memcached = backend._create_client() @@ -152,8 +182,9 @@ class GenericMemcachedBackend(CacheBackend): def get_mutex(self, key): if self.distributed_lock: - return MemcachedLock(lambda: self.client, key, - timeout=self.lock_timeout) + return MemcachedLock( + lambda: self.client, key, timeout=self.lock_timeout + ) else: return None @@ -166,23 +197,18 @@ class GenericMemcachedBackend(CacheBackend): def get_multi(self, keys): values = self.client.get_multi(keys) + return [ - NO_VALUE if key not in values - else values[key] for key in keys + NO_VALUE if val is None else val + for val in [values.get(key, NO_VALUE) for key in keys] ] def set(self, key, value): - self.client.set( - key, - value, - **self.set_arguments - ) + self.client.set(key, value, **self.set_arguments) def set_multi(self, mapping): - self.client.set_multi( - mapping, - **self.set_arguments - ) + mapping = {key: value for key, value in mapping.items()} + self.client.set_multi(mapping, **self.set_arguments) def delete(self, key): self.client.delete(key) @@ -191,24 +217,24 @@ class GenericMemcachedBackend(CacheBackend): self.client.delete_multi(keys) -class MemcacheArgs(object): +class MemcacheArgs(GenericMemcachedBackend): """Mixin which provides support for the 'time' argument to set(), 'min_compress_len' to other methods. """ + def __init__(self, arguments): - self.min_compress_len = arguments.get('min_compress_len', 0) + self.min_compress_len = arguments.get("min_compress_len", 0) self.set_arguments = {} if "memcached_expire_time" in arguments: self.set_arguments["time"] = arguments["memcached_expire_time"] if "min_compress_len" in arguments: - self.set_arguments["min_compress_len"] = \ - arguments["min_compress_len"] + self.set_arguments["min_compress_len"] = arguments[ + "min_compress_len" + ] super(MemcacheArgs, self).__init__(arguments) -pylibmc = None - class PylibmcBackend(MemcacheArgs, GenericMemcachedBackend): """A backend for the @@ -245,8 +271,8 @@ class PylibmcBackend(MemcacheArgs, GenericMemcachedBackend): """ def __init__(self, arguments): - self.binary = arguments.get('binary', False) - self.behaviors = arguments.get('behaviors', {}) + self.binary = arguments.get("binary", False) + self.behaviors = arguments.get("behaviors", {}) super(PylibmcBackend, self).__init__(arguments) def _imports(self): @@ -255,13 +281,9 @@ class PylibmcBackend(MemcacheArgs, GenericMemcachedBackend): def _create_client(self): return pylibmc.Client( - self.url, - binary=self.binary, - behaviors=self.behaviors + self.url, binary=self.binary, behaviors=self.behaviors ) -memcache = None - class MemcachedBackend(MemcacheArgs, GenericMemcachedBackend): """A backend using the standard @@ -282,6 +304,7 @@ class MemcachedBackend(MemcacheArgs, GenericMemcachedBackend): ) """ + def _imports(self): global memcache import memcache # noqa @@ -290,18 +313,17 @@ class MemcachedBackend(MemcacheArgs, GenericMemcachedBackend): return memcache.Client(self.url) -bmemcached = None - - class BMemcachedBackend(GenericMemcachedBackend): """A backend for the `python-binary-memcached `_ memcached client. - This is a pure Python memcached client which - includes the ability to authenticate with a memcached - server using SASL. + This is a pure Python memcached client which includes + security features like SASL and SSL/TLS. + + SASL is a standard for adding authentication mechanisms + to protocols in a way that is protocol independent. A typical configuration using username/password:: @@ -317,6 +339,25 @@ class BMemcachedBackend(GenericMemcachedBackend): } ) + A typical configuration using tls_context:: + + import ssl + from dogpile.cache import make_region + + ctx = ssl.create_default_context(cafile="/path/to/my-ca.pem") + + region = make_region().configure( + 'dogpile.cache.bmemcached', + expiration_time = 3600, + arguments = { + 'url':["127.0.0.1"], + 'tls_context':ctx, + } + ) + + For advanced ways to configure TLS creating a more complex + tls_context visit https://docs.python.org/3/library/ssl.html + Arguments which can be passed to the ``arguments`` dictionary include: @@ -324,11 +365,17 @@ class BMemcachedBackend(GenericMemcachedBackend): SASL authentication. :param password: optional password, will be used for SASL authentication. + :param tls_context: optional TLS context, will be used for + TLS connections. + + .. versionadded:: 1.0.2 """ + def __init__(self, arguments): - self.username = arguments.get('username', None) - self.password = arguments.get('password', None) + self.username = arguments.get("username", None) + self.password = arguments.get("password", None) + self.tls_context = arguments.get("tls_context", None) super(BMemcachedBackend, self).__init__(arguments) def _imports(self): @@ -345,7 +392,8 @@ class BMemcachedBackend(GenericMemcachedBackend): def add(self, key, value, timeout=0): try: return super(RepairBMemcachedAPI, self).add( - key, value, timeout) + key, value, timeout + ) except ValueError: return False @@ -355,10 +403,213 @@ class BMemcachedBackend(GenericMemcachedBackend): return self.Client( self.url, username=self.username, - password=self.password + password=self.password, + tls_context=self.tls_context, ) def delete_multi(self, keys): """python-binary-memcached api does not implements delete_multi""" for key in keys: self.delete(key) + + +class PyMemcacheBackend(GenericMemcachedBackend): + """A backend for the + `pymemcache `_ + memcached client. + + A comprehensive, fast, pure Python memcached client + + .. versionadded:: 1.1.2 + + pymemcache supports the following features: + + * Complete implementation of the memcached text protocol. + * Configurable timeouts for socket connect and send/recv calls. + * Access to the "noreply" flag, which can significantly increase + the speed of writes. + * Flexible, simple approach to serialization and deserialization. + * The (optional) ability to treat network and memcached errors as + cache misses. + + dogpile.cache uses the ``HashClient`` from pymemcache in order to reduce + API differences when compared to other memcached client drivers. + This allows the user to provide a single server or a list of memcached + servers. + + Arguments which can be passed to the ``arguments`` + dictionary include: + + :param tls_context: optional TLS context, will be used for + TLS connections. + + A typical configuration using tls_context:: + + import ssl + from dogpile.cache import make_region + + ctx = ssl.create_default_context(cafile="/path/to/my-ca.pem") + + region = make_region().configure( + 'dogpile.cache.pymemcache', + expiration_time = 3600, + arguments = { + 'url':["127.0.0.1"], + 'tls_context':ctx, + } + ) + + .. seealso:: + + ``_ - additional TLS + documentation. + + :param serde: optional "serde". Defaults to + ``pymemcache.serde.pickle_serde``. + + :param default_noreply: defaults to False. When set to True this flag + enables the pymemcache "noreply" feature. See the pymemcache + documentation for further details. + + :param socket_keepalive: optional socket keepalive, will be used for + TCP keepalive configuration. Use of this parameter requires pymemcache + 3.5.0 or greater. This parameter + accepts a + `pymemcache.client.base.KeepAliveOpts + `_ + object. + + A typical configuration using ``socket_keepalive``:: + + from pymemcache import KeepaliveOpts + from dogpile.cache import make_region + + # Using the default keepalive configuration + socket_keepalive = KeepaliveOpts() + + region = make_region().configure( + 'dogpile.cache.pymemcache', + expiration_time = 3600, + arguments = { + 'url':["127.0.0.1"], + 'socket_keepalive': socket_keepalive + } + ) + + .. versionadded:: 1.1.4 - added support for ``socket_keepalive``. + + :param enable_retry_client: optional flag to enable retry client + mechanisms to handle failure. Defaults to False. When set to ``True``, + the :paramref:`.PyMemcacheBackend.retry_attempts` parameter must also + be set, along with optional parameters + :paramref:`.PyMemcacheBackend.retry_delay`. + :paramref:`.PyMemcacheBackend.retry_for`, + :paramref:`.PyMemcacheBackend.do_not_retry_for`. + + .. seealso:: + + ``_ - + in the pymemcache documentation + + .. versionadded:: 1.1.4 + + :param retry_attempts: how many times to attempt an action with + pymemcache's retrying wrapper before failing. Must be 1 or above. + Defaults to None. + + .. versionadded:: 1.1.4 + + :param retry_delay: optional int|float, how many seconds to sleep between + each attempt. Used by the retry wrapper. Defaults to None. + + .. versionadded:: 1.1.4 + + :param retry_for: optional None|tuple|set|list, what exceptions to + allow retries for. Will allow retries for all exceptions if None. + Example: ``(MemcacheClientError, MemcacheUnexpectedCloseError)`` + Accepts any class that is a subclass of Exception. Defaults to None. + + .. versionadded:: 1.1.4 + + :param do_not_retry_for: optional None|tuple|set|list, what + exceptions should be retried. Will not block retries for any Exception if + None. Example: ``(IOError, MemcacheIllegalInputError)`` + Accepts any class that is a subclass of Exception. Defaults to None. + + .. versionadded:: 1.1.4 + + :param hashclient_retry_attempts: Amount of times a client should be tried + before it is marked dead and removed from the pool in the HashClient's + internal mechanisms. + + .. versionadded:: 1.1.5 + + :param hashclient_retry_timeout: Time in seconds that should pass between + retry attempts in the HashClient's internal mechanisms. + + .. versionadded:: 1.1.5 + + :param dead_timeout: Time in seconds before attempting to add a node + back in the pool in the HashClient's internal mechanisms. + + .. versionadded:: 1.1.5 + + """ # noqa E501 + + def __init__(self, arguments): + super().__init__(arguments) + + self.serde = arguments.get("serde", pymemcache.serde.pickle_serde) + self.default_noreply = arguments.get("default_noreply", False) + self.tls_context = arguments.get("tls_context", None) + self.socket_keepalive = arguments.get("socket_keepalive", None) + self.enable_retry_client = arguments.get("enable_retry_client", False) + self.retry_attempts = arguments.get("retry_attempts", None) + self.retry_delay = arguments.get("retry_delay", None) + self.retry_for = arguments.get("retry_for", None) + self.do_not_retry_for = arguments.get("do_not_retry_for", None) + self.hashclient_retry_attempts = arguments.get( + "hashclient_retry_attempts", 2 + ) + self.hashclient_retry_timeout = arguments.get( + "hashclient_retry_timeout", 1 + ) + self.dead_timeout = arguments.get("hashclient_dead_timeout", 60) + if ( + self.retry_delay is not None + or self.retry_attempts is not None + or self.retry_for is not None + or self.do_not_retry_for is not None + ) and not self.enable_retry_client: + warnings.warn( + "enable_retry_client is not set; retry options " + "will be ignored" + ) + + def _imports(self): + global pymemcache + import pymemcache + + def _create_client(self): + _kwargs = { + "serde": self.serde, + "default_noreply": self.default_noreply, + "tls_context": self.tls_context, + "retry_attempts": self.hashclient_retry_attempts, + "retry_timeout": self.hashclient_retry_timeout, + "dead_timeout": self.dead_timeout, + } + if self.socket_keepalive is not None: + _kwargs.update({"socket_keepalive": self.socket_keepalive}) + + client = pymemcache.client.hash.HashClient(self.url, **_kwargs) + if self.enable_retry_client: + return pymemcache.client.retrying.RetryingClient( + client, + attempts=self.retry_attempts, + retry_delay=self.retry_delay, + retry_for=self.retry_for, + do_not_retry_for=self.do_not_retry_for, + ) + + return client diff --git a/libs/dogpile/cache/backends/memory.py b/libs/dogpile/cache/backends/memory.py index e2083f7f0..f09b30290 100644 --- a/libs/dogpile/cache/backends/memory.py +++ b/libs/dogpile/cache/backends/memory.py @@ -10,8 +10,10 @@ places the value as given into the dictionary. """ -from ..api import CacheBackend, NO_VALUE -from ...util.compat import pickle + +from ..api import CacheBackend +from ..api import DefaultSerialization +from ..api import NO_VALUE class MemoryBackend(CacheBackend): @@ -47,39 +49,21 @@ class MemoryBackend(CacheBackend): """ - pickle_values = False def __init__(self, arguments): self._cache = arguments.pop("cache_dict", {}) def get(self, key): - value = self._cache.get(key, NO_VALUE) - if value is not NO_VALUE and self.pickle_values: - value = pickle.loads(value) - return value + return self._cache.get(key, NO_VALUE) def get_multi(self, keys): - ret = [ - self._cache.get(key, NO_VALUE) - for key in keys] - if self.pickle_values: - ret = [ - pickle.loads(value) - if value is not NO_VALUE else value - for value in ret - ] - return ret + return [self._cache.get(key, NO_VALUE) for key in keys] def set(self, key, value): - if self.pickle_values: - value = pickle.dumps(value, pickle.HIGHEST_PROTOCOL) self._cache[key] = value def set_multi(self, mapping): - pickle_values = self.pickle_values for key, value in mapping.items(): - if pickle_values: - value = pickle.dumps(value, pickle.HIGHEST_PROTOCOL) self._cache[key] = value def delete(self, key): @@ -90,7 +74,7 @@ class MemoryBackend(CacheBackend): self._cache.pop(key, None) -class MemoryPickleBackend(MemoryBackend): +class MemoryPickleBackend(DefaultSerialization, MemoryBackend): """A backend that uses a plain dictionary, but serializes objects on :meth:`.MemoryBackend.set` and deserializes :meth:`.MemoryBackend.get`. @@ -121,4 +105,3 @@ class MemoryPickleBackend(MemoryBackend): .. versionadded:: 0.5.3 """ - pickle_values = True diff --git a/libs/dogpile/cache/backends/null.py b/libs/dogpile/cache/backends/null.py index 603cca3f6..b4ad0fb35 100644 --- a/libs/dogpile/cache/backends/null.py +++ b/libs/dogpile/cache/backends/null.py @@ -10,10 +10,11 @@ caching for a region that is otherwise used normally. """ -from ..api import CacheBackend, NO_VALUE +from ..api import CacheBackend +from ..api import NO_VALUE -__all__ = ['NullBackend'] +__all__ = ["NullBackend"] class NullLock(object): @@ -23,6 +24,9 @@ class NullLock(object): def release(self): pass + def locked(self): + return False + class NullBackend(CacheBackend): """A "null" backend that effectively disables all cache operations. diff --git a/libs/dogpile/cache/backends/redis.py b/libs/dogpile/cache/backends/redis.py index d665320a7..5bff3ea3f 100644 --- a/libs/dogpile/cache/backends/redis.py +++ b/libs/dogpile/cache/backends/redis.py @@ -7,15 +7,23 @@ Provides backends for talking to `Redis `_. """ from __future__ import absolute_import -from ..api import CacheBackend, NO_VALUE -from ...util.compat import pickle, u -redis = None +import typing +import warnings -__all__ = 'RedisBackend', +from ..api import BytesBackend +from ..api import NO_VALUE + +if typing.TYPE_CHECKING: + import redis +else: + # delayed import + redis = None # noqa F811 + +__all__ = ("RedisBackend", "RedisSentinelBackend") -class RedisBackend(CacheBackend): +class RedisBackend(BytesBackend): """A `Redis `_ backend, using the `redis-py `_ backend. @@ -30,23 +38,21 @@ class RedisBackend(CacheBackend): 'port': 6379, 'db': 0, 'redis_expiration_time': 60*60*2, # 2 hours - 'distributed_lock': True + 'distributed_lock': True, + 'thread_local_lock': False } ) + Arguments accepted in the arguments dictionary: :param url: string. If provided, will override separate host/port/db params. The format is that accepted by ``StrictRedis.from_url()``. - .. versionadded:: 0.4.1 - :param host: string, default is ``localhost``. :param password: string, default is no password. - .. versionadded:: 0.4.1 - :param port: integer, default is ``6379``. :param db: integer, default is ``0``. @@ -56,57 +62,58 @@ class RedisBackend(CacheBackend): cache expiration. By default no expiration is set. :param distributed_lock: boolean, when True, will use a - redis-lock as the dogpile lock. - Use this when multiple - processes will be talking to the same redis instance. - When left at False, dogpile will coordinate on a regular - threading mutex. + redis-lock as the dogpile lock. Use this when multiple processes will be + talking to the same redis instance. When left at False, dogpile will + coordinate on a regular threading mutex. :param lock_timeout: integer, number of seconds after acquiring a lock that Redis should expire it. This argument is only valid when ``distributed_lock`` is ``True``. - .. versionadded:: 0.5.0 - :param socket_timeout: float, seconds for socket timeout. Default is None (no timeout). - .. versionadded:: 0.5.4 - :param lock_sleep: integer, number of seconds to sleep when failed to acquire a lock. This argument is only valid when ``distributed_lock`` is ``True``. - .. versionadded:: 0.5.0 - :param connection_pool: ``redis.ConnectionPool`` object. If provided, this object supersedes other connection arguments passed to the ``redis.StrictRedis`` instance, including url and/or host as well as socket_timeout, and will be passed to ``redis.StrictRedis`` as the source of connectivity. - .. versionadded:: 0.5.4 - + :param thread_local_lock: bool, whether a thread-local Redis lock object + should be used. This is the default, but is not compatible with + asynchronous runners, as they run in a different thread than the one + used to create the lock. """ def __init__(self, arguments): arguments = arguments.copy() self._imports() - self.url = arguments.pop('url', None) - self.host = arguments.pop('host', 'localhost') - self.password = arguments.pop('password', None) - self.port = arguments.pop('port', 6379) - self.db = arguments.pop('db', 0) - self.distributed_lock = arguments.get('distributed_lock', False) - self.socket_timeout = arguments.pop('socket_timeout', None) + self.url = arguments.pop("url", None) + self.host = arguments.pop("host", "localhost") + self.password = arguments.pop("password", None) + self.port = arguments.pop("port", 6379) + self.db = arguments.pop("db", 0) + self.distributed_lock = arguments.get("distributed_lock", False) + self.socket_timeout = arguments.pop("socket_timeout", None) - self.lock_timeout = arguments.get('lock_timeout', None) - self.lock_sleep = arguments.get('lock_sleep', 0.1) + self.lock_timeout = arguments.get("lock_timeout", None) + self.lock_sleep = arguments.get("lock_sleep", 0.1) + self.thread_local_lock = arguments.get("thread_local_lock", True) - self.redis_expiration_time = arguments.pop('redis_expiration_time', 0) - self.connection_pool = arguments.get('connection_pool', None) - self.client = self._create_client() + if self.distributed_lock and self.thread_local_lock: + warnings.warn( + "The Redis backend thread_local_lock parameter should be " + "set to False when distributed_lock is True" + ) + + self.redis_expiration_time = arguments.pop("redis_expiration_time", 0) + self.connection_pool = arguments.get("connection_pool", None) + self._create_client() def _imports(self): # defer imports until backend is used @@ -118,66 +125,187 @@ class RedisBackend(CacheBackend): # the connection pool already has all other connection # options present within, so here we disregard socket_timeout # and others. - return redis.StrictRedis(connection_pool=self.connection_pool) - - args = {} - if self.socket_timeout: - args['socket_timeout'] = self.socket_timeout - - if self.url is not None: - args.update(url=self.url) - return redis.StrictRedis.from_url(**args) - else: - args.update( - host=self.host, password=self.password, - port=self.port, db=self.db + self.writer_client = redis.StrictRedis( + connection_pool=self.connection_pool ) - return redis.StrictRedis(**args) + self.reader_client = self.writer_client + else: + args = {} + if self.socket_timeout: + args["socket_timeout"] = self.socket_timeout + + if self.url is not None: + args.update(url=self.url) + self.writer_client = redis.StrictRedis.from_url(**args) + self.reader_client = self.writer_client + else: + args.update( + host=self.host, + password=self.password, + port=self.port, + db=self.db, + ) + self.writer_client = redis.StrictRedis(**args) + self.reader_client = self.writer_client def get_mutex(self, key): if self.distributed_lock: - return self.client.lock(u('_lock{0}').format(key), - self.lock_timeout, self.lock_sleep) + return self.writer_client.lock( + "_lock{0}".format(key), + timeout=self.lock_timeout, + sleep=self.lock_sleep, + thread_local=self.thread_local_lock, + ) else: return None - def get(self, key): - value = self.client.get(key) + def get_serialized(self, key): + value = self.reader_client.get(key) if value is None: return NO_VALUE - return pickle.loads(value) + return value - def get_multi(self, keys): + def get_serialized_multi(self, keys): if not keys: return [] - values = self.client.mget(keys) - return [ - pickle.loads(v) if v is not None else NO_VALUE - for v in values] + values = self.reader_client.mget(keys) + return [v if v is not None else NO_VALUE for v in values] - def set(self, key, value): + def set_serialized(self, key, value): if self.redis_expiration_time: - self.client.setex(key, self.redis_expiration_time, - pickle.dumps(value, pickle.HIGHEST_PROTOCOL)) + self.writer_client.setex(key, self.redis_expiration_time, value) else: - self.client.set(key, pickle.dumps(value, pickle.HIGHEST_PROTOCOL)) - - def set_multi(self, mapping): - mapping = dict( - (k, pickle.dumps(v, pickle.HIGHEST_PROTOCOL)) - for k, v in mapping.items() - ) + self.writer_client.set(key, value) + def set_serialized_multi(self, mapping): if not self.redis_expiration_time: - self.client.mset(mapping) + self.writer_client.mset(mapping) else: - pipe = self.client.pipeline() + pipe = self.writer_client.pipeline() for key, value in mapping.items(): pipe.setex(key, self.redis_expiration_time, value) pipe.execute() def delete(self, key): - self.client.delete(key) + self.writer_client.delete(key) def delete_multi(self, keys): - self.client.delete(*keys) + self.writer_client.delete(*keys) + + +class RedisSentinelBackend(RedisBackend): + """A `Redis `_ backend, using the + `redis-py `_ backend. + It will use the Sentinel of a Redis cluster. + + .. versionadded:: 1.0.0 + + Example configuration:: + + from dogpile.cache import make_region + + region = make_region().configure( + 'dogpile.cache.redis_sentinel', + arguments = { + 'sentinels': [ + ['redis_sentinel_1', 26379], + ['redis_sentinel_2', 26379] + ], + 'db': 0, + 'redis_expiration_time': 60*60*2, # 2 hours + 'distributed_lock': True, + 'thread_local_lock': False + } + ) + + + Arguments accepted in the arguments dictionary: + + :param db: integer, default is ``0``. + + :param redis_expiration_time: integer, number of seconds after setting + a value that Redis should expire it. This should be larger than dogpile's + cache expiration. By default no expiration is set. + + :param distributed_lock: boolean, when True, will use a + redis-lock as the dogpile lock. Use this when multiple processes will be + talking to the same redis instance. When False, dogpile will + coordinate on a regular threading mutex, Default is True. + + :param lock_timeout: integer, number of seconds after acquiring a lock that + Redis should expire it. This argument is only valid when + ``distributed_lock`` is ``True``. + + :param socket_timeout: float, seconds for socket timeout. + Default is None (no timeout). + + :param sentinels: is a list of sentinel nodes. Each node is represented by + a pair (hostname, port). + Default is None (not in sentinel mode). + + :param service_name: str, the service name. + Default is 'mymaster'. + + :param sentinel_kwargs: is a dictionary of connection arguments used when + connecting to sentinel instances. Any argument that can be passed to + a normal Redis connection can be specified here. + Default is {}. + + :param connection_kwargs: dict, are keyword arguments that will be used + when establishing a connection to a Redis server. + Default is {}. + + :param lock_sleep: integer, number of seconds to sleep when failed to + acquire a lock. This argument is only valid when + ``distributed_lock`` is ``True``. + + :param thread_local_lock: bool, whether a thread-local Redis lock object + should be used. This is the default, but is not compatible with + asynchronous runners, as they run in a different thread than the one + used to create the lock. + + """ + + def __init__(self, arguments): + arguments = arguments.copy() + + self.sentinels = arguments.pop("sentinels", None) + self.service_name = arguments.pop("service_name", "mymaster") + self.sentinel_kwargs = arguments.pop("sentinel_kwargs", {}) + self.connection_kwargs = arguments.pop("connection_kwargs", {}) + + super().__init__( + arguments={ + "distributed_lock": True, + "thread_local_lock": False, + **arguments, + } + ) + + def _imports(self): + # defer imports until backend is used + global redis + import redis.sentinel # noqa + + def _create_client(self): + sentinel_kwargs = {} + sentinel_kwargs.update(self.sentinel_kwargs) + sentinel_kwargs.setdefault("password", self.password) + + connection_kwargs = {} + connection_kwargs.update(self.connection_kwargs) + connection_kwargs.setdefault("password", self.password) + + if self.db is not None: + connection_kwargs.setdefault("db", self.db) + sentinel_kwargs.setdefault("db", self.db) + if self.socket_timeout is not None: + connection_kwargs.setdefault("socket_timeout", self.socket_timeout) + + sentinel = redis.sentinel.Sentinel( + self.sentinels, + sentinel_kwargs=sentinel_kwargs, + **connection_kwargs, + ) + self.writer_client = sentinel.master_for(self.service_name) + self.reader_client = sentinel.slave_for(self.service_name) diff --git a/libs/dogpile/cache/plugins/mako_cache.py b/libs/dogpile/cache/plugins/mako_cache.py index 61f4ffaf3..b1bf6201c 100644 --- a/libs/dogpile/cache/plugins/mako_cache.py +++ b/libs/dogpile/cache/plugins/mako_cache.py @@ -51,20 +51,22 @@ class MakoPlugin(CacheImpl): def __init__(self, cache): super(MakoPlugin, self).__init__(cache) try: - self.regions = self.cache.template.cache_args['regions'] + self.regions = self.cache.template.cache_args["regions"] except KeyError: raise KeyError( "'cache_regions' argument is required on the " "Mako Lookup or Template object for usage " - "with the dogpile.cache plugin.") + "with the dogpile.cache plugin." + ) def _get_region(self, **kw): try: - region = kw['region'] + region = kw["region"] except KeyError: raise KeyError( "'cache_region' argument must be specified with 'cache=True'" - "within templates for usage with the dogpile.cache plugin.") + "within templates for usage with the dogpile.cache plugin." + ) try: return self.regions[region] except KeyError: @@ -73,8 +75,8 @@ class MakoPlugin(CacheImpl): def get_and_replace(self, key, creation_function, **kw): expiration_time = kw.pop("timeout", None) return self._get_region(**kw).get_or_create( - key, creation_function, - expiration_time=expiration_time) + key, creation_function, expiration_time=expiration_time + ) def get_or_create(self, key, creation_function, **kw): return self.get_and_replace(key, creation_function, **kw) diff --git a/libs/dogpile/cache/proxy.py b/libs/dogpile/cache/proxy.py index 15c6b5746..bf6e296b4 100644 --- a/libs/dogpile/cache/proxy.py +++ b/libs/dogpile/cache/proxy.py @@ -10,7 +10,16 @@ base backend. """ +from typing import Mapping +from typing import Optional +from typing import Sequence + +from .api import BackendFormatted +from .api import BackendSetType from .api import CacheBackend +from .api import CacheMutex +from .api import KeyType +from .api import SerializedReturnType class ProxyBackend(CacheBackend): @@ -55,17 +64,17 @@ class ProxyBackend(CacheBackend): """ - def __init__(self, *args, **kwargs): - self.proxied = None + def __init__(self, *arg, **kw): + pass - def wrap(self, backend): - ''' Take a backend as an argument and setup the self.proxied property. + def wrap(self, backend: CacheBackend) -> "ProxyBackend": + """Take a backend as an argument and setup the self.proxied property. Return an object that be used as a backend by a :class:`.CacheRegion` object. - ''' - assert( - isinstance(backend, CacheBackend) or - isinstance(backend, ProxyBackend)) + """ + assert isinstance(backend, CacheBackend) or isinstance( + backend, ProxyBackend + ) self.proxied = backend return self @@ -73,23 +82,37 @@ class ProxyBackend(CacheBackend): # Delegate any functions that are not already overridden to # the proxies backend # - def get(self, key): + def get(self, key: KeyType) -> BackendFormatted: return self.proxied.get(key) - def set(self, key, value): + def set(self, key: KeyType, value: BackendSetType) -> None: self.proxied.set(key, value) - def delete(self, key): + def delete(self, key: KeyType) -> None: self.proxied.delete(key) - def get_multi(self, keys): + def get_multi(self, keys: Sequence[KeyType]) -> Sequence[BackendFormatted]: return self.proxied.get_multi(keys) - def set_multi(self, mapping): + def set_multi(self, mapping: Mapping[KeyType, BackendSetType]) -> None: self.proxied.set_multi(mapping) - def delete_multi(self, keys): + def delete_multi(self, keys: Sequence[KeyType]) -> None: self.proxied.delete_multi(keys) - def get_mutex(self, key): + def get_mutex(self, key: KeyType) -> Optional[CacheMutex]: return self.proxied.get_mutex(key) + + def get_serialized(self, key: KeyType) -> SerializedReturnType: + return self.proxied.get_serialized(key) + + def get_serialized_multi( + self, keys: Sequence[KeyType] + ) -> Sequence[SerializedReturnType]: + return self.proxied.get_serialized_multi(keys) + + def set_serialized(self, key: KeyType, value: bytes) -> None: + self.proxied.set_serialized(key, value) + + def set_serialized_multi(self, mapping: Mapping[KeyType, bytes]) -> None: + self.proxied.set_serialized_multi(mapping) diff --git a/libs/dogpile/cache/region.py b/libs/dogpile/cache/region.py index 261a8db48..ef0dbc49a 100644 --- a/libs/dogpile/cache/region.py +++ b/libs/dogpile/cache/region.py @@ -1,32 +1,75 @@ from __future__ import with_statement -from .. import Lock, NeedRegenerationException -from ..util import NameRegistry -from . import exception -from ..util import PluginLoader, memoized_property, coerce_string_conf -from .util import function_key_generator, function_multi_key_generator -from .api import NO_VALUE, CachedValue -from .proxy import ProxyBackend -from ..util import compat -import time + +import contextlib import datetime +from functools import partial +from functools import wraps +import json +import logging from numbers import Number -from functools import wraps, partial import threading +import time +from typing import Any +from typing import Callable +from typing import cast +from typing import Mapping +from typing import Optional +from typing import Sequence +from typing import Tuple +from typing import Type +from typing import Union + from decorator import decorate -_backend_loader = PluginLoader("dogpile.cache") -register_backend = _backend_loader.register -from . import backends # noqa +from . import exception +from .api import BackendArguments +from .api import BackendFormatted +from .api import CachedValue +from .api import CacheMutex +from .api import CacheReturnType +from .api import KeyType +from .api import MetaDataType +from .api import NO_VALUE +from .api import SerializedReturnType +from .api import Serializer +from .api import ValuePayload +from .backends import _backend_loader +from .backends import register_backend # noqa +from .proxy import ProxyBackend +from .util import function_key_generator +from .util import function_multi_key_generator +from .util import repr_obj +from .. import Lock +from .. import NeedRegenerationException +from ..util import coerce_string_conf +from ..util import memoized_property +from ..util import NameRegistry +from ..util import PluginLoader -value_version = 1 +value_version = 2 """An integer placed in the :class:`.CachedValue` so that new versions of dogpile.cache can detect cached values from a previous, backwards-incompatible version. """ +log = logging.getLogger(__name__) -class RegionInvalidationStrategy(object): + +AsyncCreator = Callable[ + ["CacheRegion", KeyType, Callable[[], ValuePayload], CacheMutex], None +] + +ExpirationTimeCallable = Callable[[], float] + +ToStr = Callable[[Any], str] + +FunctionKeyGenerator = Callable[..., Callable[..., KeyType]] + +FunctionMultiKeyGenerator = Callable[..., Callable[..., Sequence[KeyType]]] + + +class RegionInvalidationStrategy: """Region invalidation strategy interface Implement this interface and pass implementation instance @@ -74,7 +117,7 @@ class RegionInvalidationStrategy(object): region = CacheRegion() - region = region.configure(region_invalidator=CustomInvalidationStrategy()) + region = region.configure(region_invalidator=CustomInvalidationStrategy()) # noqa Invalidation strategies that wish to have access to the :class:`.CacheRegion` itself should construct the invalidator given the @@ -98,7 +141,7 @@ class RegionInvalidationStrategy(object): """ - def invalidate(self, hard=True): + def invalidate(self, hard: bool = True) -> None: """Region invalidation. :class:`.CacheRegion` propagated call. @@ -110,7 +153,7 @@ class RegionInvalidationStrategy(object): raise NotImplementedError() - def is_hard_invalidated(self, timestamp): + def is_hard_invalidated(self, timestamp: float) -> bool: """Check timestamp to determine if it was hard invalidated. :return: Boolean. True if ``timestamp`` is older than @@ -121,7 +164,7 @@ class RegionInvalidationStrategy(object): raise NotImplementedError() - def is_soft_invalidated(self, timestamp): + def is_soft_invalidated(self, timestamp: float) -> bool: """Check timestamp to determine if it was soft invalidated. :return: Boolean. True if ``timestamp`` is older than @@ -132,7 +175,7 @@ class RegionInvalidationStrategy(object): raise NotImplementedError() - def is_invalidated(self, timestamp): + def is_invalidated(self, timestamp: float) -> bool: """Check timestamp to determine if it was invalidated. :return: Boolean. True if ``timestamp`` is older than @@ -142,7 +185,7 @@ class RegionInvalidationStrategy(object): raise NotImplementedError() - def was_soft_invalidated(self): + def was_soft_invalidated(self) -> bool: """Indicate the region was invalidated in soft mode. :return: Boolean. True if region was invalidated in soft mode. @@ -151,7 +194,7 @@ class RegionInvalidationStrategy(object): raise NotImplementedError() - def was_hard_invalidated(self): + def was_hard_invalidated(self) -> bool: """Indicate the region was invalidated in hard mode. :return: Boolean. True if region was invalidated in hard mode. @@ -162,33 +205,31 @@ class RegionInvalidationStrategy(object): class DefaultInvalidationStrategy(RegionInvalidationStrategy): - def __init__(self): self._is_hard_invalidated = None self._invalidated = None - def invalidate(self, hard=True): + def invalidate(self, hard: bool = True) -> None: self._is_hard_invalidated = bool(hard) self._invalidated = time.time() - def is_invalidated(self, timestamp): - return (self._invalidated is not None and - timestamp < self._invalidated) + def is_invalidated(self, timestamp: float) -> bool: + return self._invalidated is not None and timestamp < self._invalidated - def was_hard_invalidated(self): + def was_hard_invalidated(self) -> bool: return self._is_hard_invalidated is True - def is_hard_invalidated(self, timestamp): + def is_hard_invalidated(self, timestamp: float) -> bool: return self.was_hard_invalidated() and self.is_invalidated(timestamp) - def was_soft_invalidated(self): + def was_soft_invalidated(self) -> bool: return self._is_hard_invalidated is False - def is_soft_invalidated(self, timestamp): + def is_soft_invalidated(self, timestamp: float) -> bool: return self.was_soft_invalidated() and self.is_invalidated(timestamp) -class CacheRegion(object): +class CacheRegion: r"""A front end to a particular cache backend. :param name: Optional, a string name for the region. @@ -274,6 +315,21 @@ class CacheRegion(object): to convert non-string or Unicode keys to bytestrings, which is needed when using a backend such as bsddb or dbm under Python 2.x in conjunction with Unicode keys. + + :param serializer: function which will be applied to all values before + passing to the backend. Defaults to ``None``, in which case the + serializer recommended by the backend will be used. Typical + serializers include ``pickle.dumps`` and ``json.dumps``. + + .. versionadded:: 1.1.0 + + :param deserializer: function which will be applied to all values returned + by the backend. Defaults to ``None``, in which case the + deserializer recommended by the backend will be used. Typical + deserializers include ``pickle.dumps`` and ``json.dumps``. + + .. versionadded:: 1.1.0 + :param async_creation_runner: A callable that, when specified, will be passed to and called by dogpile.lock when there is a stale value present in the cache. It will be passed the @@ -328,31 +384,38 @@ class CacheRegion(object): """ def __init__( - self, - name=None, - function_key_generator=function_key_generator, - function_multi_key_generator=function_multi_key_generator, - key_mangler=None, - async_creation_runner=None, + self, + name: Optional[str] = None, + function_key_generator: FunctionKeyGenerator = function_key_generator, + function_multi_key_generator: FunctionMultiKeyGenerator = function_multi_key_generator, # noqa E501 + key_mangler: Optional[Callable[[KeyType], KeyType]] = None, + serializer: Optional[Callable[[ValuePayload], bytes]] = None, + deserializer: Optional[Callable[[bytes], ValuePayload]] = None, + async_creation_runner: Optional[AsyncCreator] = None, ): """Construct a new :class:`.CacheRegion`.""" self.name = name self.function_key_generator = function_key_generator self.function_multi_key_generator = function_multi_key_generator self.key_mangler = self._user_defined_key_mangler = key_mangler + self.serializer = self._user_defined_serializer = serializer + self.deserializer = self._user_defined_deserializer = deserializer self.async_creation_runner = async_creation_runner - self.region_invalidator = DefaultInvalidationStrategy() + self.region_invalidator: RegionInvalidationStrategy = ( + DefaultInvalidationStrategy() + ) def configure( - self, backend, - expiration_time=None, - arguments=None, - _config_argument_dict=None, - _config_prefix=None, - wrap=None, - replace_existing_backend=False, - region_invalidator=None - ): + self, + backend: str, + expiration_time: Optional[Union[float, datetime.timedelta]] = None, + arguments: Optional[BackendArguments] = None, + _config_argument_dict: Optional[Mapping[str, Any]] = None, + _config_prefix: Optional[str] = None, + wrap: Sequence[Union[ProxyBackend, Type[ProxyBackend]]] = (), + replace_existing_backend: bool = False, + region_invalidator: Optional[RegionInvalidationStrategy] = None, + ) -> "CacheRegion": """Configure a :class:`.CacheRegion`. The :class:`.CacheRegion` itself @@ -403,44 +466,53 @@ class CacheRegion(object): .. versionadded:: 0.6.2 - """ + """ if "backend" in self.__dict__ and not replace_existing_backend: raise exception.RegionAlreadyConfigured( "This region is already " "configured with backend: %s. " "Specify replace_existing_backend=True to replace." - % self.backend) + % self.backend + ) try: backend_cls = _backend_loader.load(backend) except PluginLoader.NotFound: raise exception.PluginNotFound( - "Couldn't find cache plugin to load: %s" % backend) + "Couldn't find cache plugin to load: %s" % backend + ) if _config_argument_dict: self.backend = backend_cls.from_config_dict( - _config_argument_dict, - _config_prefix + _config_argument_dict, _config_prefix ) else: self.backend = backend_cls(arguments or {}) + self.expiration_time: Union[float, None] + if not expiration_time or isinstance(expiration_time, Number): - self.expiration_time = expiration_time + self.expiration_time = cast(Union[None, float], expiration_time) elif isinstance(expiration_time, datetime.timedelta): - self.expiration_time = int( - compat.timedelta_total_seconds(expiration_time)) + self.expiration_time = int(expiration_time.total_seconds()) else: raise exception.ValidationError( - 'expiration_time is not a number or timedelta.') + "expiration_time is not a number or timedelta." + ) if not self._user_defined_key_mangler: self.key_mangler = self.backend.key_mangler + if not self._user_defined_serializer: + self.serializer = self.backend.serializer + + if not self._user_defined_deserializer: + self.deserializer = self.backend.deserializer + self._lock_registry = NameRegistry(self._create_mutex) - if getattr(wrap, '__iter__', False): + if getattr(wrap, "__iter__", False): for wrapper in reversed(wrap): self.wrap(wrapper) @@ -449,26 +521,30 @@ class CacheRegion(object): return self - def wrap(self, proxy): - ''' Takes a ProxyBackend instance or class and wraps the - attached backend. ''' + def wrap(self, proxy: Union[ProxyBackend, Type[ProxyBackend]]) -> None: + """Takes a ProxyBackend instance or class and wraps the + attached backend.""" # if we were passed a type rather than an instance then # initialize it. - if type(proxy) == type: - proxy = proxy() + if isinstance(proxy, type): + proxy_instance = proxy() + else: + proxy_instance = proxy - if not issubclass(type(proxy), ProxyBackend): - raise TypeError("Type %s is not a valid ProxyBackend" - % type(proxy)) + if not isinstance(proxy_instance, ProxyBackend): + raise TypeError( + "%r is not a valid ProxyBackend" % (proxy_instance,) + ) - self.backend = proxy.wrap(self.backend) + self.backend = proxy_instance.wrap(self.backend) def _mutex(self, key): return self._lock_registry.get(key) - class _LockWrapper(object): + class _LockWrapper(CacheMutex): """weakref-capable wrapper for threading.Lock""" + def __init__(self): self.lock = threading.Lock() @@ -478,6 +554,9 @@ class CacheRegion(object): def release(self): self.lock.release() + def locked(self): + return self.lock.locked() + def _create_mutex(self, key): mutex = self.backend.get_mutex(key) if mutex is not None: @@ -500,7 +579,7 @@ class CacheRegion(object): """ if self._actual_backend is None: _backend = self.backend - while hasattr(_backend, 'proxied'): + while hasattr(_backend, "proxied"): _backend = _backend.proxied self._actual_backend = _backend return self._actual_backend @@ -583,19 +662,21 @@ class CacheRegion(object): return self.configure( config_dict["%sbackend" % prefix], expiration_time=config_dict.get( - "%sexpiration_time" % prefix, None), + "%sexpiration_time" % prefix, None + ), _config_argument_dict=config_dict, _config_prefix="%sarguments." % prefix, - wrap=config_dict.get( - "%swrap" % prefix, None), + wrap=config_dict.get("%swrap" % prefix, None), replace_existing_backend=config_dict.get( - "%sreplace_existing_backend" % prefix, False), + "%sreplace_existing_backend" % prefix, False + ), ) @memoized_property def backend(self): raise exception.RegionNotConfigured( - "No backend is configured on this region.") + "No backend is configured on this region." + ) @property def is_configured(self): @@ -605,7 +686,7 @@ class CacheRegion(object): .. versionadded:: 0.5.1 """ - return 'backend' in self.__dict__ + return "backend" in self.__dict__ def get(self, key, expiration_time=None, ignore_expiration=False): """Return a value from the cache, based on the given key. @@ -650,6 +731,13 @@ class CacheRegion(object): which will supersede that configured on the :class:`.CacheRegion` itself. + .. note:: The :paramref:`.CacheRegion.get.expiration_time` + argument is **not persisted in the cache** and is relevant + only to **this specific cache retrieval operation**, relative to + the creation time stored with the existing cached value. + Subsequent calls to :meth:`.CacheRegion.get` are **not** affected + by this value. + .. versionadded:: 0.3.0 :param ignore_expiration: if ``True``, the value is returned @@ -659,13 +747,25 @@ class CacheRegion(object): .. versionadded:: 0.3.0 + .. seealso:: + + :meth:`.CacheRegion.get_multi` + + :meth:`.CacheRegion.get_or_create` + + :meth:`.CacheRegion.set` + + :meth:`.CacheRegion.delete` + + """ if self.key_mangler: key = self.key_mangler(key) - value = self.backend.get(key) - value = self._unexpired_value_fn( - expiration_time, ignore_expiration)(value) + value = self._get_from_backend(key) + value = self._unexpired_value_fn(expiration_time, ignore_expiration)( + value + ) return value.payload @@ -681,11 +781,14 @@ class CacheRegion(object): def value_fn(value): if value is NO_VALUE: return value - elif expiration_time is not None and \ - current_time - value.metadata["ct"] > expiration_time: + elif ( + expiration_time is not None + and current_time - value.metadata["ct"] > expiration_time + ): return NO_VALUE elif self.region_invalidator.is_invalidated( - value.metadata["ct"]): + value.metadata["ct"] + ): return NO_VALUE else: return value @@ -727,25 +830,64 @@ class CacheRegion(object): if not keys: return [] - if self.key_mangler: - keys = list(map(lambda key: self.key_mangler(key), keys)) + if self.key_mangler is not None: + keys = [self.key_mangler(key) for key in keys] - backend_values = self.backend.get_multi(keys) + backend_values = self._get_multi_from_backend(keys) _unexpired_value_fn = self._unexpired_value_fn( - expiration_time, ignore_expiration) + expiration_time, ignore_expiration + ) return [ value.payload if value is not NO_VALUE else value - for value in - ( - _unexpired_value_fn(value) for value in - backend_values + for value in ( + _unexpired_value_fn(value) for value in backend_values ) ] + @contextlib.contextmanager + def _log_time(self, keys): + start_time = time.time() + yield + seconds = time.time() - start_time + log.debug( + "Cache value generated in %(seconds).3f seconds for key(s): " + "%(keys)r", + {"seconds": seconds, "keys": repr_obj(keys)}, + ) + + def _is_cache_miss(self, value, orig_key): + if value is NO_VALUE: + log.debug("No value present for key: %r", orig_key) + elif value.metadata["v"] != value_version: + log.debug("Dogpile version update for key: %r", orig_key) + elif self.region_invalidator.is_hard_invalidated(value.metadata["ct"]): + log.debug("Hard invalidation detected for key: %r", orig_key) + else: + return False + + return True + + def key_is_locked(self, key: KeyType) -> bool: + """Return True if a particular cache key is currently being generated + within the dogpile lock. + + .. versionadded:: 1.1.2 + + """ + mutex = self._mutex(key) + locked: bool = mutex.locked() + return locked + def get_or_create( - self, key, creator, expiration_time=None, should_cache_fn=None, - creator_args=None): + self, + key: KeyType, + creator: Callable[..., ValuePayload], + expiration_time: Optional[float] = None, + should_cache_fn: Optional[Callable[[ValuePayload], bool]] = None, + creator_args: Optional[Tuple[Any, Mapping[str, Any]]] = None, + ) -> ValuePayload: + """Return a cached value based on the given key. If the value does not exist or is considered to be expired @@ -786,10 +928,17 @@ class CacheRegion(object): .. versionadded:: 0.7.0 - :param expiration_time: optional expiration time which will overide + :param expiration_time: optional expiration time which will override the expiration time already configured on this :class:`.CacheRegion` if not None. To set no expiration, use the value -1. + .. note:: The :paramref:`.CacheRegion.get_or_create.expiration_time` + argument is **not persisted in the cache** and is relevant + only to **this specific cache retrieval operation**, relative to + the creation time stored with the existing cached value. + Subsequent calls to :meth:`.CacheRegion.get_or_create` are **not** + affected by this value. + :param should_cache_fn: optional callable function which will receive the value returned by the "creator", and will then return True or False, indicating if the value should actually be cached or not. If @@ -811,11 +960,13 @@ class CacheRegion(object): .. seealso:: + :meth:`.CacheRegion.get` + :meth:`.CacheRegion.cache_on_arguments` - applies :meth:`.get_or_create` to any function using a decorator. :meth:`.CacheRegion.get_or_create_multi` - multiple key/value - version + version """ orig_key = key @@ -823,64 +974,87 @@ class CacheRegion(object): key = self.key_mangler(key) def get_value(): - value = self.backend.get(key) - if (value is NO_VALUE or value.metadata['v'] != value_version or - self.region_invalidator.is_hard_invalidated( - value.metadata["ct"])): + value = self._get_from_backend(key) + if self._is_cache_miss(value, orig_key): raise NeedRegenerationException() - ct = value.metadata["ct"] + + ct = cast(CachedValue, value).metadata["ct"] if self.region_invalidator.is_soft_invalidated(ct): - ct = time.time() - expiration_time - .0001 + if expiration_time is None: + raise exception.DogpileCacheException( + "Non-None expiration time required " + "for soft invalidation" + ) + ct = time.time() - expiration_time - 0.0001 return value.payload, ct def gen_value(): - if creator_args: - created_value = creator(*creator_args[0], **creator_args[1]) - else: - created_value = creator() + with self._log_time(orig_key): + if creator_args: + created_value = creator( + *creator_args[0], **creator_args[1] + ) + else: + created_value = creator() value = self._value(created_value) - if not should_cache_fn or \ - should_cache_fn(created_value): - self.backend.set(key, value) + if ( + expiration_time is None + and self.region_invalidator.was_soft_invalidated() + ): + raise exception.DogpileCacheException( + "Non-None expiration time required " + "for soft invalidation" + ) + + if not should_cache_fn or should_cache_fn(created_value): + self._set_cached_value_to_backend(key, value) return value.payload, value.metadata["ct"] if expiration_time is None: expiration_time = self.expiration_time - if (expiration_time is None and - self.region_invalidator.was_soft_invalidated()): - raise exception.DogpileCacheException( - "Non-None expiration time required " - "for soft invalidation") - if expiration_time == -1: expiration_time = None + async_creator: Optional[Callable[[CacheMutex], AsyncCreator]] if self.async_creation_runner: + acr = self.async_creation_runner + def async_creator(mutex): if creator_args: + + ca = creator_args + @wraps(creator) def go(): - return creator(*creator_args[0], **creator_args[1]) + return creator(*ca[0], **ca[1]) + else: go = creator - return self.async_creation_runner(self, orig_key, go, mutex) + return acr(self, orig_key, go, mutex) + else: async_creator = None with Lock( - self._mutex(key), - gen_value, - get_value, - expiration_time, - async_creator) as value: + self._mutex(key), + gen_value, + get_value, + expiration_time, + async_creator, + ) as value: return value def get_or_create_multi( - self, keys, creator, expiration_time=None, should_cache_fn=None): + self, + keys: Sequence[KeyType], + creator: Callable[[], ValuePayload], + expiration_time: Optional[float] = None, + should_cache_fn: Optional[Callable[[ValuePayload], bool]] = None, + ) -> Sequence[ValuePayload]: """Return a sequence of cached values based on a sequence of keys. The behavior for generation of values based on keys corresponds @@ -906,7 +1080,7 @@ class CacheRegion(object): :param creator: function which accepts a sequence of keys and returns a sequence of new values. - :param expiration_time: optional expiration time which will overide + :param expiration_time: optional expiration time which will override the expiration time already configured on this :class:`.CacheRegion` if not None. To set no expiration, use the value -1. @@ -929,40 +1103,35 @@ class CacheRegion(object): def get_value(key): value = values.get(key, NO_VALUE) - if (value is NO_VALUE or value.metadata['v'] != value_version or - self.region_invalidator.is_hard_invalidated( - value.metadata['ct'])): + if self._is_cache_miss(value, orig_key): # dogpile.core understands a 0 here as # "the value is not available", e.g. # _has_value() will return False. return value.payload, 0 else: - ct = value.metadata["ct"] + ct = cast(CachedValue, value).metadata["ct"] if self.region_invalidator.is_soft_invalidated(ct): - ct = time.time() - expiration_time - .0001 + if expiration_time is None: + raise exception.DogpileCacheException( + "Non-None expiration time required " + "for soft invalidation" + ) + ct = time.time() - expiration_time - 0.0001 return value.payload, ct - def gen_value(): + def gen_value() -> ValuePayload: raise NotImplementedError() - def async_creator(key, mutex): + def async_creator(mutexes, key, mutex): mutexes[key] = mutex if expiration_time is None: expiration_time = self.expiration_time - if (expiration_time is None and - self.region_invalidator.was_soft_invalidated()): - raise exception.DogpileCacheException( - "Non-None expiration time required " - "for soft invalidation") - if expiration_time == -1: expiration_time = None - mutexes = {} - sorted_unique_keys = sorted(set(keys)) if self.key_mangler: @@ -972,15 +1141,21 @@ class CacheRegion(object): orig_to_mangled = dict(zip(sorted_unique_keys, mangled_keys)) - values = dict(zip(mangled_keys, self.backend.get_multi(mangled_keys))) + values = dict( + zip(mangled_keys, self._get_multi_from_backend(mangled_keys)) + ) + + mutexes: Mapping[KeyType, Any] = {} for orig_key, mangled_key in orig_to_mangled.items(): with Lock( - self._mutex(mangled_key), - gen_value, - lambda: get_value(mangled_key), - expiration_time, - async_creator=lambda mutex: async_creator(orig_key, mutex) + self._mutex(mangled_key), + gen_value, + lambda: get_value(mangled_key), + expiration_time, + async_creator=lambda mutex: async_creator( + mutexes, orig_key, mutex + ), ): pass try: @@ -988,24 +1163,35 @@ class CacheRegion(object): # sort the keys, the idea is to prevent deadlocks. # though haven't been able to simulate one anyway. keys_to_get = sorted(mutexes) - new_values = creator(*keys_to_get) - values_w_created = dict( - (orig_to_mangled[k], self._value(v)) + with self._log_time(keys_to_get): + new_values = creator(*keys_to_get) + + values_w_created = { + orig_to_mangled[k]: self._value(v) for k, v in zip(keys_to_get, new_values) - ) + } - if not should_cache_fn: - self.backend.set_multi(values_w_created) - else: - values_to_cache = dict( - (k, v) - for k, v in values_w_created.items() - if should_cache_fn(v[0]) + if ( + expiration_time is None + and self.region_invalidator.was_soft_invalidated() + ): + raise exception.DogpileCacheException( + "Non-None expiration time required " + "for soft invalidation" ) - if values_to_cache: - self.backend.set_multi(values_to_cache) + if not should_cache_fn: + self._set_multi_cached_value_to_backend(values_w_created) + + else: + self._set_multi_cached_value_to_backend( + { + k: v + for k, v in values_w_created.items() + if should_cache_fn(v.payload) + } + ) values.update(values_w_created) return [values[orig_to_mangled[k]].payload for k in keys] @@ -1013,40 +1199,162 @@ class CacheRegion(object): for mutex in mutexes.values(): mutex.release() - def _value(self, value): + def _value( + self, value: Any, metadata: Optional[MetaDataType] = None + ) -> CachedValue: """Return a :class:`.CachedValue` given a value.""" - return CachedValue( - value, - { - "ct": time.time(), - "v": value_version - }) - def set(self, key, value): + if metadata is None: + metadata = self._gen_metadata() + return CachedValue(value, metadata) + + def _parse_serialized_from_backend( + self, value: SerializedReturnType + ) -> CacheReturnType: + if value in (None, NO_VALUE): + return NO_VALUE + + assert self.deserializer + byte_value = cast(bytes, value) + + bytes_metadata, _, bytes_payload = byte_value.partition(b"|") + metadata = json.loads(bytes_metadata) + payload = self.deserializer(bytes_payload) + return CachedValue(payload, metadata) + + def _serialize_cached_value_elements( + self, payload: ValuePayload, metadata: MetaDataType + ) -> bytes: + serializer = cast(Serializer, self.serializer) + + return b"%b|%b" % ( + json.dumps(metadata).encode("ascii"), + serializer(payload), + ) + + def _serialized_payload( + self, payload: ValuePayload, metadata: Optional[MetaDataType] = None + ) -> BackendFormatted: + """Return a backend formatted representation of a value. + + If a serializer is in use then this will return a string representation + with the value formatted by the serializer. + + """ + if metadata is None: + metadata = self._gen_metadata() + + return self._serialize_cached_value_elements(payload, metadata) + + def _serialized_cached_value(self, value: CachedValue) -> BackendFormatted: + """Return a backend formatted representation of a :class:`.CachedValue`. + + If a serializer is in use then this will return a string representation + with the value formatted by the serializer. + + """ + + assert self.serializer + return self._serialize_cached_value_elements( + value.payload, value.metadata + ) + + def _get_from_backend(self, key: KeyType) -> CacheReturnType: + if self.deserializer: + return self._parse_serialized_from_backend( + self.backend.get_serialized(key) + ) + else: + return cast(CacheReturnType, self.backend.get(key)) + + def _get_multi_from_backend( + self, keys: Sequence[KeyType] + ) -> Sequence[CacheReturnType]: + if self.deserializer: + return [ + self._parse_serialized_from_backend(v) + for v in self.backend.get_serialized_multi(keys) + ] + else: + return cast( + Sequence[CacheReturnType], self.backend.get_multi(keys) + ) + + def _set_cached_value_to_backend( + self, key: KeyType, value: CachedValue + ) -> None: + if self.serializer: + self.backend.set_serialized( + key, self._serialized_cached_value(value) + ) + else: + self.backend.set(key, value) + + def _set_multi_cached_value_to_backend( + self, mapping: Mapping[KeyType, CachedValue] + ) -> None: + if not mapping: + return + + if self.serializer: + self.backend.set_serialized_multi( + { + k: self._serialized_cached_value(v) + for k, v in mapping.items() + } + ) + else: + self.backend.set_multi(mapping) + + def _gen_metadata(self) -> MetaDataType: + return {"ct": time.time(), "v": value_version} + + def set(self, key: KeyType, value: ValuePayload) -> None: """Place a new value in the cache under the given key.""" if self.key_mangler: key = self.key_mangler(key) - self.backend.set(key, self._value(value)) - def set_multi(self, mapping): - """Place new values in the cache under the given keys. + if self.serializer: + self.backend.set_serialized(key, self._serialized_payload(value)) + else: + self.backend.set(key, self._value(value)) - .. versionadded:: 0.5.0 - - """ + def set_multi(self, mapping: Mapping[KeyType, ValuePayload]) -> None: + """Place new values in the cache under the given keys.""" if not mapping: return - if self.key_mangler: - mapping = dict(( - self.key_mangler(k), self._value(v)) - for k, v in mapping.items()) - else: - mapping = dict((k, self._value(v)) for k, v in mapping.items()) - self.backend.set_multi(mapping) + metadata = self._gen_metadata() - def delete(self, key): + if self.serializer: + if self.key_mangler: + mapping = { + self.key_mangler(k): self._serialized_payload( + v, metadata=metadata + ) + for k, v in mapping.items() + } + else: + mapping = { + k: self._serialized_payload(v, metadata=metadata) + for k, v in mapping.items() + } + self.backend.set_serialized_multi(mapping) + else: + if self.key_mangler: + mapping = { + self.key_mangler(k): self._value(v, metadata=metadata) + for k, v in mapping.items() + } + else: + mapping = { + k: self._value(v, metadata=metadata) + for k, v in mapping.items() + } + self.backend.set_multi(mapping) + + def delete(self, key: KeyType) -> None: """Remove a value from the cache. This operation is idempotent (can be called multiple times, or on a @@ -1058,7 +1366,7 @@ class CacheRegion(object): self.backend.delete(key) - def delete_multi(self, keys): + def delete_multi(self, keys: Sequence[KeyType]) -> None: """Remove multiple values from the cache. This operation is idempotent (can be called multiple times, or on a @@ -1069,16 +1377,19 @@ class CacheRegion(object): """ if self.key_mangler: - keys = list(map(lambda key: self.key_mangler(key), keys)) + km = self.key_mangler + keys = [km(key) for key in keys] self.backend.delete_multi(keys) def cache_on_arguments( - self, namespace=None, - expiration_time=None, - should_cache_fn=None, - to_str=compat.string_type, - function_key_generator=None): + self, + namespace: Optional[str] = None, + expiration_time: Union[float, ExpirationTimeCallable, None] = None, + should_cache_fn: Optional[Callable[[ValuePayload], bool]] = None, + to_str: Callable[[Any], str] = str, + function_key_generator: Optional[FunctionKeyGenerator] = None, + ) -> Callable[[Callable[..., ValuePayload]], Callable[..., ValuePayload]]: """A function decorator that will cache the return value of the function using a key derived from the function itself and its arguments. @@ -1171,7 +1482,7 @@ class CacheRegion(object): (with caveats) for use with instance or class methods. Given the example:: - class MyClass(object): + class MyClass: @region.cache_on_arguments(namespace="foo") def one(self, a, b): return a + b @@ -1185,12 +1496,12 @@ class CacheRegion(object): name within the same module, as can occur when decorating instance or class methods as below:: - class MyClass(object): + class MyClass: @region.cache_on_arguments(namespace='MC') def somemethod(self, x, y): "" - class MyOtherClass(object): + class MyOtherClass: @region.cache_on_arguments(namespace='MOC') def somemethod(self, x, y): "" @@ -1228,14 +1539,8 @@ class CacheRegion(object): end of the day, week or time period" and "cache until a certain date or time passes". - .. versionchanged:: 0.5.0 - ``expiration_time`` may be passed as a callable to - :meth:`.CacheRegion.cache_on_arguments`. - :param should_cache_fn: passed to :meth:`.CacheRegion.get_or_create`. - .. versionadded:: 0.4.3 - :param to_str: callable, will be called on each function argument in order to convert to a string. Defaults to ``str()``. If the function accepts non-ascii unicode arguments on Python 2.x, the @@ -1243,14 +1548,10 @@ class CacheRegion(object): produce unicode cache keys which may require key mangling before reaching the cache. - .. versionadded:: 0.5.0 - :param function_key_generator: a function that will produce a "cache key". This function will supersede the one configured on the :class:`.CacheRegion` itself. - .. versionadded:: 0.5.5 - .. seealso:: :meth:`.CacheRegion.cache_multi_on_arguments` @@ -1258,27 +1559,35 @@ class CacheRegion(object): :meth:`.CacheRegion.get_or_create` """ - expiration_time_is_callable = compat.callable(expiration_time) + expiration_time_is_callable = callable(expiration_time) if function_key_generator is None: - function_key_generator = self.function_key_generator + _function_key_generator = self.function_key_generator + else: + _function_key_generator = function_key_generator def get_or_create_for_user_func(key_generator, user_func, *arg, **kw): key = key_generator(*arg, **kw) - timeout = expiration_time() if expiration_time_is_callable \ - else expiration_time - return self.get_or_create(key, user_func, timeout, - should_cache_fn, (arg, kw)) + timeout: Optional[float] = ( + cast(ExpirationTimeCallable, expiration_time)() + if expiration_time_is_callable + else cast(Optional[float], expiration_time) + ) + return self.get_or_create( + key, user_func, timeout, should_cache_fn, (arg, kw) + ) def cache_decorator(user_func): - if to_str is compat.string_type: + if to_str is cast(Callable[[Any], str], str): # backwards compatible - key_generator = function_key_generator(namespace, user_func) + key_generator = _function_key_generator( + namespace, user_func + ) # type: ignore else: - key_generator = function_key_generator( - namespace, user_func, - to_str=to_str) + key_generator = _function_key_generator( + namespace, user_func, to_str + ) def refresh(*arg, **kw): """ @@ -1309,16 +1618,28 @@ class CacheRegion(object): # Use `decorate` to preserve the signature of :param:`user_func`. - return decorate(user_func, partial( - get_or_create_for_user_func, key_generator)) + return decorate( + user_func, partial(get_or_create_for_user_func, key_generator) + ) return cache_decorator def cache_multi_on_arguments( - self, namespace=None, expiration_time=None, - should_cache_fn=None, - asdict=False, to_str=compat.string_type, - function_multi_key_generator=None): + self, + namespace: Optional[str] = None, + expiration_time: Union[float, ExpirationTimeCallable, None] = None, + should_cache_fn: Optional[Callable[[ValuePayload], bool]] = None, + asdict: bool = False, + to_str: ToStr = str, + function_multi_key_generator: Optional[ + FunctionMultiKeyGenerator + ] = None, + ) -> Callable[ + [Callable[..., Sequence[ValuePayload]]], + Callable[ + ..., Union[Sequence[ValuePayload], Mapping[KeyType, ValuePayload]] + ], + ]: """A function decorator that will cache multiple return values from the function using a sequence of keys derived from the function itself and the arguments passed to it. @@ -1435,12 +1756,19 @@ class CacheRegion(object): :meth:`.CacheRegion.get_or_create_multi` """ - expiration_time_is_callable = compat.callable(expiration_time) + expiration_time_is_callable = callable(expiration_time) if function_multi_key_generator is None: - function_multi_key_generator = self.function_multi_key_generator + _function_multi_key_generator = self.function_multi_key_generator + else: + _function_multi_key_generator = function_multi_key_generator - def get_or_create_for_user_func(key_generator, user_func, *arg, **kw): + def get_or_create_for_user_func( + key_generator: Callable[..., Sequence[KeyType]], + user_func: Callable[..., Sequence[ValuePayload]], + *arg: Any, + **kw: Any, + ) -> Union[Sequence[ValuePayload], Mapping[KeyType, ValuePayload]]: cache_keys = arg keys = key_generator(*arg, **kw) key_lookup = dict(zip(keys, cache_keys)) @@ -1449,15 +1777,23 @@ class CacheRegion(object): def creator(*keys_to_create): return user_func(*[key_lookup[k] for k in keys_to_create]) - timeout = expiration_time() if expiration_time_is_callable \ - else expiration_time + timeout: Optional[float] = ( + cast(ExpirationTimeCallable, expiration_time)() + if expiration_time_is_callable + else cast(Optional[float], expiration_time) + ) + + result: Union[ + Sequence[ValuePayload], Mapping[KeyType, ValuePayload] + ] if asdict: + def dict_create(*keys): d_values = creator(*keys) return [ - d_values.get(key_lookup[k], NO_VALUE) - for k in keys] + d_values.get(key_lookup[k], NO_VALUE) for k in keys + ] def wrap_cache_fn(value): if value is NO_VALUE: @@ -1468,21 +1804,24 @@ class CacheRegion(object): return should_cache_fn(value) result = self.get_or_create_multi( - keys, dict_create, timeout, wrap_cache_fn) + keys, dict_create, timeout, wrap_cache_fn + ) result = dict( - (k, v) for k, v in zip(cache_keys, result) - if v is not NO_VALUE) + (k, v) + for k, v in zip(cache_keys, result) + if v is not NO_VALUE + ) else: result = self.get_or_create_multi( - keys, creator, timeout, - should_cache_fn) + keys, creator, timeout, should_cache_fn + ) return result def cache_decorator(user_func): - key_generator = function_multi_key_generator( - namespace, user_func, - to_str=to_str) + key_generator = _function_multi_key_generator( + namespace, user_func, to_str=to_str + ) def invalidate(*arg): keys = key_generator(*arg) @@ -1491,10 +1830,11 @@ class CacheRegion(object): def set_(mapping): keys = list(mapping) gen_keys = key_generator(*keys) - self.set_multi(dict( - (gen_key, mapping[key]) - for gen_key, key - in zip(gen_keys, keys)) + self.set_multi( + dict( + (gen_key, mapping[key]) + for gen_key, key in zip(gen_keys, keys) + ) ) def get(*arg): @@ -1505,14 +1845,10 @@ class CacheRegion(object): keys = key_generator(*arg) values = user_func(*arg) if asdict: - self.set_multi( - dict(zip(keys, [values[a] for a in arg])) - ) + self.set_multi(dict(zip(keys, [values[a] for a in arg]))) return values else: - self.set_multi( - dict(zip(keys, values)) - ) + self.set_multi(dict(zip(keys, values))) return values user_func.set = set_ @@ -1522,14 +1858,14 @@ class CacheRegion(object): # Use `decorate` to preserve the signature of :param:`user_func`. - return decorate(user_func, partial(get_or_create_for_user_func, key_generator)) + return decorate( + user_func, partial(get_or_create_for_user_func, key_generator) + ) return cache_decorator - - -def make_region(*arg, **kw): +def make_region(*arg: Any, **kw: Any) -> CacheRegion: """Instantiate a new :class:`.CacheRegion`. Currently, :func:`.make_region` is a passthrough diff --git a/libs/dogpile/cache/util.py b/libs/dogpile/cache/util.py index 16bcd1c97..6bf6cefeb 100644 --- a/libs/dogpile/cache/util.py +++ b/libs/dogpile/cache/util.py @@ -1,9 +1,10 @@ from hashlib import sha1 + from ..util import compat from ..util import langhelpers -def function_key_generator(namespace, fn, to_str=compat.string_type): +def function_key_generator(namespace, fn, to_str=str): """Return a function that generates a string key, based on a given function as well as arguments to the returned function itself. @@ -23,47 +24,51 @@ def function_key_generator(namespace, fn, to_str=compat.string_type): """ if namespace is None: - namespace = '%s:%s' % (fn.__module__, fn.__name__) + namespace = "%s:%s" % (fn.__module__, fn.__name__) else: - namespace = '%s:%s|%s' % (fn.__module__, fn.__name__, namespace) + namespace = "%s:%s|%s" % (fn.__module__, fn.__name__, namespace) args = compat.inspect_getargspec(fn) - has_self = args[0] and args[0][0] in ('self', 'cls') + has_self = args[0] and args[0][0] in ("self", "cls") def generate_key(*args, **kw): if kw: raise ValueError( "dogpile.cache's default key creation " - "function does not accept keyword arguments.") + "function does not accept keyword arguments." + ) if has_self: args = args[1:] return namespace + "|" + " ".join(map(to_str, args)) + return generate_key -def function_multi_key_generator(namespace, fn, to_str=compat.string_type): +def function_multi_key_generator(namespace, fn, to_str=str): if namespace is None: - namespace = '%s:%s' % (fn.__module__, fn.__name__) + namespace = "%s:%s" % (fn.__module__, fn.__name__) else: - namespace = '%s:%s|%s' % (fn.__module__, fn.__name__, namespace) + namespace = "%s:%s|%s" % (fn.__module__, fn.__name__, namespace) args = compat.inspect_getargspec(fn) - has_self = args[0] and args[0][0] in ('self', 'cls') + has_self = args[0] and args[0][0] in ("self", "cls") def generate_keys(*args, **kw): if kw: raise ValueError( "dogpile.cache's default key creation " - "function does not accept keyword arguments.") + "function does not accept keyword arguments." + ) if has_self: args = args[1:] return [namespace + "|" + key for key in map(to_str, args)] + return generate_keys -def kwarg_function_key_generator(namespace, fn, to_str=compat.string_type): +def kwarg_function_key_generator(namespace, fn, to_str=str): """Return a function that generates a string key, based on a given function as well as arguments to the returned function itself. @@ -83,9 +88,9 @@ def kwarg_function_key_generator(namespace, fn, to_str=compat.string_type): """ if namespace is None: - namespace = '%s:%s' % (fn.__module__, fn.__name__) + namespace = "%s:%s" % (fn.__module__, fn.__name__) else: - namespace = '%s:%s|%s' % (fn.__module__, fn.__name__, namespace) + namespace = "%s:%s|%s" % (fn.__module__, fn.__name__, namespace) argspec = compat.inspect_getargspec(fn) default_list = list(argspec.defaults or []) @@ -94,32 +99,41 @@ def kwarg_function_key_generator(namespace, fn, to_str=compat.string_type): # enumerate() default_list.reverse() # use idx*-1 to create the correct right-lookup index. - args_with_defaults = dict((argspec.args[(idx*-1)], default) - for idx, default in enumerate(default_list, 1)) - if argspec.args and argspec.args[0] in ('self', 'cls'): + args_with_defaults = dict( + (argspec.args[(idx * -1)], default) + for idx, default in enumerate(default_list, 1) + ) + if argspec.args and argspec.args[0] in ("self", "cls"): arg_index_start = 1 else: arg_index_start = 0 def generate_key(*args, **kwargs): as_kwargs = dict( - [(argspec.args[idx], arg) - for idx, arg in enumerate(args[arg_index_start:], - arg_index_start)]) + [ + (argspec.args[idx], arg) + for idx, arg in enumerate( + args[arg_index_start:], arg_index_start + ) + ] + ) as_kwargs.update(kwargs) for arg, val in args_with_defaults.items(): if arg not in as_kwargs: as_kwargs[arg] = val - argument_values = [as_kwargs[key] - for key in sorted(as_kwargs.keys())] - return namespace + '|' + " ".join(map(to_str, argument_values)) + argument_values = [as_kwargs[key] for key in sorted(as_kwargs.keys())] + return namespace + "|" + " ".join(map(to_str, argument_values)) + return generate_key def sha1_mangle_key(key): """a SHA1 key mangler.""" + if isinstance(key, str): + key = key.encode("utf-8") + return sha1(key).hexdigest() @@ -128,13 +142,16 @@ def length_conditional_mangler(length, mangler): past a certain threshold. """ + def mangle(key): if len(key) >= length: return mangler(key) else: return key + return mangle + # in the 0.6 release these functions were moved to the dogpile.util namespace. # They are linked here to maintain compatibility with older versions. @@ -143,3 +160,30 @@ KeyReentrantMutex = langhelpers.KeyReentrantMutex memoized_property = langhelpers.memoized_property PluginLoader = langhelpers.PluginLoader to_list = langhelpers.to_list + + +class repr_obj: + + __slots__ = ("value", "max_chars") + + def __init__(self, value, max_chars=300): + self.value = value + self.max_chars = max_chars + + def __eq__(self, other): + return other.value == self.value + + def __repr__(self): + rep = repr(self.value) + lenrep = len(rep) + if lenrep > self.max_chars: + segment_length = self.max_chars // 2 + rep = ( + rep[0:segment_length] + + ( + " ... (%d characters truncated) ... " + % (lenrep - self.max_chars) + ) + + rep[-segment_length:] + ) + return rep diff --git a/libs/dogpile/core.py b/libs/dogpile/core.py index 2bcfaf813..ce1476126 100644 --- a/libs/dogpile/core.py +++ b/libs/dogpile/core.py @@ -8,10 +8,10 @@ dogpile.core installation is present. """ -from .util import nameregistry # noqa -from .util import readwrite_lock # noqa -from .util.readwrite_lock import ReadWriteMutex # noqa -from .util.nameregistry import NameRegistry # noqa +from . import __version__ # noqa from .lock import Lock # noqa from .lock import NeedRegenerationException # noqa -from . import __version__ # noqa +from .util import nameregistry # noqa +from .util import readwrite_lock # noqa +from .util.nameregistry import NameRegistry # noqa +from .util.readwrite_lock import ReadWriteMutex # noqa diff --git a/libs/dogpile/lock.py b/libs/dogpile/lock.py index 2ac22dcfe..465cd90db 100644 --- a/libs/dogpile/lock.py +++ b/libs/dogpile/lock.py @@ -1,5 +1,5 @@ -import time import logging +import time log = logging.getLogger(__name__) @@ -11,10 +11,11 @@ class NeedRegenerationException(Exception): """ + NOT_REGENERATED = object() -class Lock(object): +class Lock: """Dogpile lock class. Provides an interface around an arbitrary mutex @@ -70,8 +71,8 @@ class Lock(object): value is available.""" return not self._has_value(createdtime) or ( - self.expiretime is not None and - time.time() - createdtime > self.expiretime + self.expiretime is not None + and time.time() - createdtime > self.expiretime ) def _has_value(self, createdtime): @@ -109,7 +110,8 @@ class Lock(object): raise Exception( "Generation function should " "have just been called by a concurrent " - "thread.") + "thread." + ) else: return value @@ -122,9 +124,7 @@ class Lock(object): if self._has_value(createdtime): has_value = True if not self.mutex.acquire(False): - log.debug( - "creation function in progress " - "elsewhere, returning") + log.debug("creation function in progress elsewhere, returning") return NOT_REGENERATED else: has_value = False @@ -173,8 +173,7 @@ class Lock(object): # there's no value at all, and we have to create it synchronously log.debug( "Calling creation function for %s value", - "not-yet-present" if not has_value else - "previously expired" + "not-yet-present" if not has_value else "previously expired", ) return self.creator() finally: @@ -185,5 +184,5 @@ class Lock(object): def __enter__(self): return self._enter() - def __exit__(self, type, value, traceback): + def __exit__(self, type_, value, traceback): pass diff --git a/libs/dogpile/util/__init__.py b/libs/dogpile/util/__init__.py index 91b075207..77d65ff40 100644 --- a/libs/dogpile/util/__init__.py +++ b/libs/dogpile/util/__init__.py @@ -1,4 +1,7 @@ +from .langhelpers import coerce_string_conf # noqa +from .langhelpers import KeyReentrantMutex # noqa +from .langhelpers import memoized_property # noqa +from .langhelpers import PluginLoader # noqa +from .langhelpers import to_list # noqa from .nameregistry import NameRegistry # noqa from .readwrite_lock import ReadWriteMutex # noqa -from .langhelpers import PluginLoader, memoized_property, \ - coerce_string_conf, to_list, KeyReentrantMutex # noqa diff --git a/libs/dogpile/util/compat.py b/libs/dogpile/util/compat.py index 198c76276..85e4e85f4 100644 --- a/libs/dogpile/util/compat.py +++ b/libs/dogpile/util/compat.py @@ -1,87 +1,72 @@ -import sys - -py2k = sys.version_info < (3, 0) -py3k = sys.version_info >= (3, 0) -py32 = sys.version_info >= (3, 2) -py27 = sys.version_info >= (2, 7) -jython = sys.platform.startswith('java') -win32 = sys.platform.startswith('win') - -try: - import threading -except ImportError: - import dummy_threading as threading # noqa +import collections +import inspect -if py3k: # pragma: no cover - string_types = str, - text_type = str - string_type = str +FullArgSpec = collections.namedtuple( + "FullArgSpec", + [ + "args", + "varargs", + "varkw", + "defaults", + "kwonlyargs", + "kwonlydefaults", + "annotations", + ], +) - if py32: - callable = callable - else: - def callable(fn): - return hasattr(fn, '__call__') - - def u(s): - return s - - def ue(s): - return s - - import configparser - import io - import _thread as thread -else: - string_types = basestring, - text_type = unicode - string_type = str - - def u(s): - return unicode(s, "utf-8") - - def ue(s): - return unicode(s, "unicode_escape") - - import ConfigParser as configparser # noqa - import StringIO as io # noqa - - callable = callable # noqa - import thread # noqa +ArgSpec = collections.namedtuple( + "ArgSpec", ["args", "varargs", "keywords", "defaults"] +) -if py3k: - import collections - ArgSpec = collections.namedtuple( - "ArgSpec", - ["args", "varargs", "keywords", "defaults"]) +def inspect_getfullargspec(func): + """Fully vendored version of getfullargspec from Python 3.3. - from inspect import getfullargspec as inspect_getfullargspec + This version is more performant than the one which appeared in + later Python 3 versions. - def inspect_getargspec(func): - return ArgSpec( - *inspect_getfullargspec(func)[0:4] - ) -else: - from inspect import getargspec as inspect_getargspec # noqa + """ -if py3k or jython: - import pickle -else: - import cPickle as pickle # noqa + # if a Signature is already present, as is the case with newer + # "decorator" package, defer back to built in + if hasattr(func, "__signature__"): + return inspect.getfullargspec(func) -if py3k: - def read_config_file(config, fileobj): - return config.read_file(fileobj) -else: - def read_config_file(config, fileobj): - return config.readfp(fileobj) + if inspect.ismethod(func): + func = func.__func__ + if not inspect.isfunction(func): + raise TypeError("{!r} is not a Python function".format(func)) + + co = func.__code__ + if not inspect.iscode(co): + raise TypeError("{!r} is not a code object".format(co)) + + nargs = co.co_argcount + names = co.co_varnames + nkwargs = co.co_kwonlyargcount + args = list(names[:nargs]) + kwonlyargs = list(names[nargs : nargs + nkwargs]) + + nargs += nkwargs + varargs = None + if co.co_flags & inspect.CO_VARARGS: + varargs = co.co_varnames[nargs] + nargs = nargs + 1 + varkw = None + if co.co_flags & inspect.CO_VARKEYWORDS: + varkw = co.co_varnames[nargs] + + return FullArgSpec( + args, + varargs, + varkw, + func.__defaults__, + kwonlyargs, + func.__kwdefaults__, + func.__annotations__, + ) -def timedelta_total_seconds(td): - if py27: - return td.total_seconds() - else: - return (td.microseconds + ( - td.seconds + td.days * 24 * 3600) * 1e6) / 1e6 +def inspect_getargspec(func): + return ArgSpec(*inspect_getfullargspec(func)[0:4]) diff --git a/libs/dogpile/util/langhelpers.py b/libs/dogpile/util/langhelpers.py index 4ff8e3e3e..a59b24ef4 100644 --- a/libs/dogpile/util/langhelpers.py +++ b/libs/dogpile/util/langhelpers.py @@ -1,44 +1,54 @@ -import re +import abc import collections -from . import compat +import re +import threading +from typing import MutableMapping +from typing import MutableSet + +import stevedore def coerce_string_conf(d): result = {} for k, v in d.items(): - if not isinstance(v, compat.string_types): + if not isinstance(v, str): result[k] = v continue v = v.strip() - if re.match(r'^[-+]?\d+$', v): + if re.match(r"^[-+]?\d+$", v): result[k] = int(v) - elif re.match(r'^[-+]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][-+]?\d+)?$', v): + elif re.match(r"^[-+]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][-+]?\d+)?$", v): result[k] = float(v) - elif v.lower() in ('false', 'true'): - result[k] = v.lower() == 'true' - elif v == 'None': + elif v.lower() in ("false", "true"): + result[k] = v.lower() == "true" + elif v == "None": result[k] = None else: result[k] = v return result -class PluginLoader(object): +class PluginLoader: def __init__(self, group): self.group = group - self.impls = {} + self.impls = {} # loaded plugins + self._mgr = None # lazily defined stevedore manager + self._unloaded = {} # plugins registered but not loaded def load(self, name): + if name in self._unloaded: + self.impls[name] = self._unloaded[name]() + return self.impls[name] if name in self.impls: - return self.impls[name]() + return self.impls[name] else: # pragma NO COVERAGE - import pkg_resources - for impl in pkg_resources.iter_entry_points( - self.group, name): - self.impls[name] = impl.load - return impl.load() - else: + if self._mgr is None: + self._mgr = stevedore.ExtensionManager(self.group) + try: + self.impls[name] = self._mgr[name].plugin + return self.impls[name] + except KeyError: raise self.NotFound( "Can't load plugin %s %s" % (self.group, name) ) @@ -47,14 +57,16 @@ class PluginLoader(object): def load(): mod = __import__(modulepath, fromlist=[objname]) return getattr(mod, objname) - self.impls[name] = load + + self._unloaded[name] = load class NotFound(Exception): """The specified plugin could not be found.""" -class memoized_property(object): +class memoized_property: """A read-only @property that is only evaluated once.""" + def __init__(self, fget, doc=None): self.fget = fget self.__doc__ = doc or fget.__doc__ @@ -77,9 +89,23 @@ def to_list(x, default=None): return x -class KeyReentrantMutex(object): +class Mutex(abc.ABC): + @abc.abstractmethod + def acquire(self, wait: bool = True) -> bool: + raise NotImplementedError() - def __init__(self, key, mutex, keys): + @abc.abstractmethod + def release(self) -> None: + raise NotImplementedError() + + +class KeyReentrantMutex: + def __init__( + self, + key: str, + mutex: Mutex, + keys: MutableMapping[int, MutableSet[str]], + ): self.key = key self.mutex = mutex self.keys = keys @@ -89,17 +115,19 @@ class KeyReentrantMutex(object): # this collection holds zero or one # thread idents as the key; a set of # keynames held as the value. - keystore = collections.defaultdict(set) + keystore: MutableMapping[ + int, MutableSet[str] + ] = collections.defaultdict(set) def fac(key): return KeyReentrantMutex(key, mutex, keystore) + return fac def acquire(self, wait=True): - current_thread = compat.threading.current_thread().ident + current_thread = threading.get_ident() keys = self.keys.get(current_thread) - if keys is not None and \ - self.key not in keys: + if keys is not None and self.key not in keys: # current lockholder, new key. add it in keys.add(self.key) return True @@ -111,7 +139,7 @@ class KeyReentrantMutex(object): return False def release(self): - current_thread = compat.threading.current_thread().ident + current_thread = threading.get_ident() keys = self.keys.get(current_thread) assert keys is not None, "this thread didn't do the acquire" assert self.key in keys, "No acquire held for key '%s'" % self.key @@ -121,3 +149,10 @@ class KeyReentrantMutex(object): # the thread ident and unlock. del self.keys[current_thread] self.mutex.release() + + def locked(self): + current_thread = threading.get_ident() + keys = self.keys.get(current_thread) + if keys is None: + return False + return self.key in keys diff --git a/libs/dogpile/util/nameregistry.py b/libs/dogpile/util/nameregistry.py index 7087f7cd6..3c4cc0229 100644 --- a/libs/dogpile/util/nameregistry.py +++ b/libs/dogpile/util/nameregistry.py @@ -1,4 +1,7 @@ -from .compat import threading +import threading +from typing import Any +from typing import Callable +from typing import MutableMapping import weakref @@ -37,19 +40,16 @@ class NameRegistry(object): method. """ - _locks = weakref.WeakValueDictionary() + _mutex = threading.RLock() - def __init__(self, creator): - """Create a new :class:`.NameRegistry`. - - - """ - self._values = weakref.WeakValueDictionary() + def __init__(self, creator: Callable[..., Any]): + """Create a new :class:`.NameRegistry`.""" + self._values: MutableMapping[str, Any] = weakref.WeakValueDictionary() self._mutex = threading.RLock() self.creator = creator - def get(self, identifier, *args, **kw): + def get(self, identifier: str, *args: Any, **kw: Any) -> Any: r"""Get and possibly create the value. :param identifier: Hash key for the value. @@ -68,7 +68,7 @@ class NameRegistry(object): except KeyError: return self._sync_get(identifier, *args, **kw) - def _sync_get(self, identifier, *args, **kw): + def _sync_get(self, identifier: str, *args: Any, **kw: Any) -> Any: self._mutex.acquire() try: try: @@ -76,11 +76,13 @@ class NameRegistry(object): return self._values[identifier] else: self._values[identifier] = value = self.creator( - identifier, *args, **kw) + identifier, *args, **kw + ) return value except KeyError: self._values[identifier] = value = self.creator( - identifier, *args, **kw) + identifier, *args, **kw + ) return value finally: self._mutex.release() diff --git a/libs/dogpile/util/readwrite_lock.py b/libs/dogpile/util/readwrite_lock.py index 9b953edb8..3f3751034 100644 --- a/libs/dogpile/util/readwrite_lock.py +++ b/libs/dogpile/util/readwrite_lock.py @@ -1,6 +1,6 @@ -from .compat import threading - import logging +import threading + log = logging.getLogger(__name__) @@ -62,13 +62,15 @@ class ReadWriteMutex(object): # check if we are the last asynchronous reader thread # out the door. if self.async_ == 0: - # yes. so if a sync operation is waiting, notifyAll to wake + # yes. so if a sync operation is waiting, notify_all to wake # it up if self.current_sync_operation is not None: - self.condition.notifyAll() + self.condition.notify_all() elif self.async_ < 0: - raise LockError("Synchronizer error - too many " - "release_read_locks called") + raise LockError( + "Synchronizer error - too many " + "release_read_locks called" + ) log.debug("%s released read lock", self) finally: self.condition.release() @@ -93,7 +95,7 @@ class ReadWriteMutex(object): # establish ourselves as the current sync # this indicates to other read/write operations # that they should wait until this is None again - self.current_sync_operation = threading.currentThread() + self.current_sync_operation = threading.current_thread() # now wait again for asyncs to finish if self.async_ > 0: @@ -115,16 +117,18 @@ class ReadWriteMutex(object): """Release the 'write' lock.""" self.condition.acquire() try: - if self.current_sync_operation is not threading.currentThread(): - raise LockError("Synchronizer error - current thread doesn't " - "have the write lock") + if self.current_sync_operation is not threading.current_thread(): + raise LockError( + "Synchronizer error - current thread doesn't " + "have the write lock" + ) # reset the current sync operation so # another can get it self.current_sync_operation = None # tell everyone to get ready - self.condition.notifyAll() + self.condition.notify_all() log.debug("%s released write lock", self) finally: diff --git a/libs/dumprar.py b/libs/dumprar.py deleted file mode 100644 index 4e17b1d60..000000000 --- a/libs/dumprar.py +++ /dev/null @@ -1,556 +0,0 @@ -#! /usr/bin/env python - -"""Dump archive contents, test extraction.""" - -from __future__ import division, absolute_import, print_function - -import io -import sys -import getopt - -from datetime import datetime - -import rarfile as rf - - -usage = """ -dumprar [switches] [ARC1 ARC2 ...] [@ARCLIST] -switches: - @file read archive names from file - -pPSW set password - -Ccharset set fallback charset - -v increase verbosity - -t attempt to read all files - -x write read files out - -c show archive comment - -h show usage - -- stop switch parsing -""".strip() - -os_list = ['DOS', 'OS2', 'WIN', 'UNIX', 'MACOS', 'BEOS'] - -block_strs = ['MARK', 'MAIN', 'FILE', 'OLD_COMMENT', 'OLD_EXTRA', - 'OLD_SUB', 'OLD_RECOVERY', 'OLD_AUTH', 'SUB', 'ENDARC'] - -r5_block_types = { - rf.RAR5_BLOCK_MAIN: 'R5_MAIN', - rf.RAR5_BLOCK_FILE: 'R5_FILE', - rf.RAR5_BLOCK_SERVICE: 'R5_SVC', - rf.RAR5_BLOCK_ENCRYPTION: 'R5_ENC', - rf.RAR5_BLOCK_ENDARC: 'R5_ENDARC', -} - - -def rar3_type(btype): - """RAR3 type code as string.""" - if btype < rf.RAR_BLOCK_MARK or btype > rf.RAR_BLOCK_ENDARC: - return "*UNKNOWN*" - return block_strs[btype - rf.RAR_BLOCK_MARK] - - -def rar5_type(btype): - """RAR5 type code as string.""" - return r5_block_types.get(btype, '*UNKNOWN*') - - -main_bits = ( - (rf.RAR_MAIN_VOLUME, "VOL"), - (rf.RAR_MAIN_COMMENT, "COMMENT"), - (rf.RAR_MAIN_LOCK, "LOCK"), - (rf.RAR_MAIN_SOLID, "SOLID"), - (rf.RAR_MAIN_NEWNUMBERING, "NEWNR"), - (rf.RAR_MAIN_AUTH, "AUTH"), - (rf.RAR_MAIN_RECOVERY, "RECOVERY"), - (rf.RAR_MAIN_PASSWORD, "PASSWORD"), - (rf.RAR_MAIN_FIRSTVOLUME, "FIRSTVOL"), - (rf.RAR_SKIP_IF_UNKNOWN, "SKIP"), - (rf.RAR_LONG_BLOCK, "LONG"), -) - -endarc_bits = ( - (rf.RAR_ENDARC_NEXT_VOLUME, "NEXTVOL"), - (rf.RAR_ENDARC_DATACRC, "DATACRC"), - (rf.RAR_ENDARC_REVSPACE, "REVSPACE"), - (rf.RAR_ENDARC_VOLNR, "VOLNR"), - (rf.RAR_SKIP_IF_UNKNOWN, "SKIP"), - (rf.RAR_LONG_BLOCK, "LONG"), -) - -file_bits = ( - (rf.RAR_FILE_SPLIT_BEFORE, "SPLIT_BEFORE"), - (rf.RAR_FILE_SPLIT_AFTER, "SPLIT_AFTER"), - (rf.RAR_FILE_PASSWORD, "PASSWORD"), - (rf.RAR_FILE_COMMENT, "COMMENT"), - (rf.RAR_FILE_SOLID, "SOLID"), - (rf.RAR_FILE_LARGE, "LARGE"), - (rf.RAR_FILE_UNICODE, "UNICODE"), - (rf.RAR_FILE_SALT, "SALT"), - (rf.RAR_FILE_VERSION, "VERSION"), - (rf.RAR_FILE_EXTTIME, "EXTTIME"), - (rf.RAR_FILE_EXTFLAGS, "EXTFLAGS"), - (rf.RAR_SKIP_IF_UNKNOWN, "SKIP"), - (rf.RAR_LONG_BLOCK, "LONG"), -) - -generic_bits = ( - (rf.RAR_SKIP_IF_UNKNOWN, "SKIP"), - (rf.RAR_LONG_BLOCK, "LONG"), -) - -file_parms = ("D64", "D128", "D256", "D512", - "D1024", "D2048", "D4096", "DIR") - -r5_block_flags = ( - (rf.RAR5_BLOCK_FLAG_EXTRA_DATA, 'EXTRA'), - (rf.RAR5_BLOCK_FLAG_DATA_AREA, 'DATA'), - (rf.RAR5_BLOCK_FLAG_SKIP_IF_UNKNOWN, 'SKIP'), - (rf.RAR5_BLOCK_FLAG_SPLIT_BEFORE, 'SPLIT_BEFORE'), - (rf.RAR5_BLOCK_FLAG_SPLIT_AFTER, 'SPLIT_AFTER'), - (rf.RAR5_BLOCK_FLAG_DEPENDS_PREV, 'DEPENDS'), - (rf.RAR5_BLOCK_FLAG_KEEP_WITH_PARENT, 'KEEP'), -) - -r5_main_flags = ( - (rf.RAR5_MAIN_FLAG_ISVOL, 'ISVOL'), - (rf.RAR5_MAIN_FLAG_HAS_VOLNR, 'VOLNR'), - (rf.RAR5_MAIN_FLAG_SOLID, 'SOLID'), - (rf.RAR5_MAIN_FLAG_RECOVERY, 'RECOVERY'), - (rf.RAR5_MAIN_FLAG_LOCKED, 'LOCKED'), -) - -r5_file_flags = ( - (rf.RAR5_FILE_FLAG_ISDIR, 'DIR'), - (rf.RAR5_FILE_FLAG_HAS_MTIME, 'MTIME'), - (rf.RAR5_FILE_FLAG_HAS_CRC32, 'CRC32'), - (rf.RAR5_FILE_FLAG_UNKNOWN_SIZE, 'NOSIZE'), -) - -r5_enc_flags = ( - (rf.RAR5_ENC_FLAG_HAS_CHECKVAL, 'CHECKVAL'), -) - -r5_endarc_flags = ( - (rf.RAR5_ENDARC_FLAG_NEXT_VOL, 'NEXTVOL'), -) - -r5_file_enc_flags = ( - (rf.RAR5_XENC_CHECKVAL, 'CHECKVAL'), - (rf.RAR5_XENC_TWEAKED, 'TWEAKED'), -) - -r5_file_redir_types = { - rf.RAR5_XREDIR_UNIX_SYMLINK: 'UNIX_SYMLINK', - rf.RAR5_XREDIR_WINDOWS_SYMLINK: 'WINDOWS_SYMLINK', - rf.RAR5_XREDIR_WINDOWS_JUNCTION: 'WINDOWS_JUNCTION', - rf.RAR5_XREDIR_HARD_LINK: 'HARD_LINK', - rf.RAR5_XREDIR_FILE_COPY: 'FILE_COPY', -} - -r5_file_redir_flags = ( - (rf.RAR5_XREDIR_ISDIR, 'DIR'), -) - - -def xprint(m, *args): - """Print string to stdout. - - Format unicode safely. - """ - if sys.hexversion < 0x3000000: - m = m.decode('utf8') - if args: - m = m % args - if sys.hexversion < 0x3000000: - m = m.encode('utf8') - sys.stdout.write(m) - sys.stdout.write('\n') - - -def render_flags(flags, bit_list): - """Show bit names. - """ - res = [] - known = 0 - for bit in bit_list: - known = known | bit[0] - if flags & bit[0]: - res.append(bit[1]) - unknown = flags & ~known - n = 0 - while unknown: - if unknown & 1: - res.append("UNK_%04x" % (1 << n)) - unknown = unknown >> 1 - n += 1 - - if not res: - return '-' - - return ",".join(res) - - -def get_file_flags(flags): - """Show flag names and handle dict size. - """ - res = render_flags(flags & ~rf.RAR_FILE_DICTMASK, file_bits) - - xf = (flags & rf.RAR_FILE_DICTMASK) >> 5 - res += "," + file_parms[xf] - return res - - -def fmt_time(t): - """Format time. - """ - if t is None: - return '(-)' - if isinstance(t, datetime): - return t.isoformat('T') - return "%04d-%02d-%02d %02d:%02d:%02d" % t - - -def show_item(h): - """Show any RAR3/5 record. - """ - if isinstance(h, rf.Rar3Info): - show_item_v3(h) - elif isinstance(h, rf.Rar5Info): - show_item_v5(h) - else: - xprint('Unknown info record') - - -def show_item_v3(h): - """Show any RAR3 record. - """ - st = rar3_type(h.type) - xprint("%s: hdrlen=%d datlen=%d", st, h.header_size, h.add_size) - if h.type in (rf.RAR_BLOCK_FILE, rf.RAR_BLOCK_SUB): - if h.host_os == rf.RAR_OS_UNIX: - s_mode = "0%o" % h.mode - else: - s_mode = "0x%x" % h.mode - xprint(" flags=0x%04x:%s", h.flags, get_file_flags(h.flags)) - if h.host_os >= 0 and h.host_os < len(os_list): - s_os = os_list[h.host_os] - else: - s_os = "?" - xprint(" os=%d:%s ver=%d mode=%s meth=%c cmp=%d dec=%d vol=%d", - h.host_os, s_os, - h.extract_version, s_mode, h.compress_type, - h.compress_size, h.file_size, h.volume) - ucrc = (h.CRC + (1 << 32)) & ((1 << 32) - 1) - xprint(" crc=0x%08x (%d) date_time=%s", ucrc, h.CRC, fmt_time(h.date_time)) - xprint(" name=%s", h.filename) - if h.mtime: - xprint(" mtime=%s", fmt_time(h.mtime)) - if h.ctime: - xprint(" ctime=%s", fmt_time(h.ctime)) - if h.atime: - xprint(" atime=%s", fmt_time(h.atime)) - if h.arctime: - xprint(" arctime=%s", fmt_time(h.arctime)) - elif h.type == rf.RAR_BLOCK_MAIN: - xprint(" flags=0x%04x:%s", h.flags, render_flags(h.flags, main_bits)) - elif h.type == rf.RAR_BLOCK_ENDARC: - xprint(" flags=0x%04x:%s", h.flags, render_flags(h.flags, endarc_bits)) - elif h.type == rf.RAR_BLOCK_MARK: - xprint(" flags=0x%04x:", h.flags) - else: - xprint(" flags=0x%04x:%s", h.flags, render_flags(h.flags, generic_bits)) - - if h.comment is not None: - cm = repr(h.comment) - if cm[0] == 'u': - cm = cm[1:] - xprint(" comment=%s", cm) - - -def show_item_v5(h): - """Show any RAR5 record. - """ - st = rar5_type(h.block_type) - xprint("%s: hdrlen=%d datlen=%d hdr_extra=%d", st, h.header_size, - h.compress_size, h.block_extra_size) - xprint(" block_flags=0x%04x:%s", h.block_flags, render_flags(h.block_flags, r5_block_flags)) - if h.block_type in (rf.RAR5_BLOCK_FILE, rf.RAR5_BLOCK_SERVICE): - xprint(" name=%s", h.filename) - if h.file_host_os == rf.RAR5_OS_UNIX: - s_os = 'UNIX' - s_mode = "0%o" % h.mode - else: - s_os = 'WINDOWS' - s_mode = "0x%x" % h.mode - xprint(" file_flags=0x%04x:%s", h.file_flags, render_flags(h.file_flags, r5_file_flags)) - - cmp_flags = h.file_compress_flags - xprint(" cmp_algo=%d cmp_meth=%d dict=%d solid=%r", - cmp_flags & 0x3f, - (cmp_flags >> 7) & 0x07, - cmp_flags >> 10, - cmp_flags & rf.RAR5_COMPR_SOLID > 0) - xprint(" os=%d:%s mode=%s cmp=%r dec=%r vol=%r", - h.file_host_os, s_os, s_mode, - h.compress_size, h.file_size, h.volume) - if h.CRC is not None: - xprint(" crc=0x%08x (%d)", h.CRC, h.CRC) - if h.blake2sp_hash is not None: - xprint(" blake2sp=%s", rf.tohex(h.blake2sp_hash)) - if h.date_time is not None: - xprint(" date_time=%s", fmt_time(h.date_time)) - if h.mtime: - xprint(" mtime=%s", fmt_time(h.mtime)) - if h.ctime: - xprint(" ctime=%s", fmt_time(h.ctime)) - if h.atime: - xprint(" atime=%s", fmt_time(h.atime)) - if h.arctime: - xprint(" arctime=%s", fmt_time(h.arctime)) - if h.flags & rf.RAR_FILE_PASSWORD: - enc_algo, enc_flags, kdf_count, salt, iv, checkval = h.file_encryption - algo_name = enc_algo == rf.RAR5_XENC_CIPHER_AES256 and 'AES256' or 'UnknownAlgo' - xprint(' algo=%d:%s enc_flags=%04x:%s kdf_lg=%d kdf_count=%d salt=%s iv=%s checkval=%s', - enc_algo, algo_name, enc_flags, render_flags(enc_flags, r5_file_enc_flags), - kdf_count, 1 << kdf_count, rf.tohex(salt), rf.tohex(iv), - checkval and rf.tohex(checkval) or '-') - if h.file_redir: - redir_type, redir_flags, redir_name = h.file_redir - xprint(' redir: type=%s flags=%d:%s destination=%s', - r5_file_redir_types.get(redir_type, 'Unknown'), - redir_flags, render_flags(redir_flags, r5_file_redir_flags), - redir_name) - if h.file_owner: - uname, gname, uid, gid = h.file_owner - xprint(' owner: name=%r group=%r uid=%r gid=%r', - uname, gname, uid, gid) - if h.file_version: - flags, version = h.file_version - xprint(' version: flags=%r version=%r', flags, version) - elif h.block_type == rf.RAR5_BLOCK_MAIN: - xprint(" flags=0x%04x:%s", h.flags, render_flags(h.main_flags, r5_main_flags)) - elif h.block_type == rf.RAR5_BLOCK_ENDARC: - xprint(" flags=0x%04x:%s", h.flags, render_flags(h.endarc_flags, r5_endarc_flags)) - elif h.block_type == rf.RAR5_BLOCK_ENCRYPTION: - algo_name = h.encryption_algo == rf.RAR5_XENC_CIPHER_AES256 and 'AES256' or 'UnknownAlgo' - xprint(" algo=%d:%s flags=0x%04x:%s", h.encryption_algo, algo_name, h.flags, - render_flags(h.encryption_flags, r5_enc_flags)) - xprint(" kdf_lg=%d kdf_count=%d", h.encryption_kdf_count, 1 << h.encryption_kdf_count) - xprint(" salt=%s", rf.tohex(h.encryption_salt)) - else: - xprint(" - missing info -") - - if h.comment is not None: - cm = repr(h.comment) - if cm[0] == 'u': - cm = cm[1:] - xprint(" comment=%s", cm) - - -cf_show_comment = 0 -cf_verbose = 0 -cf_charset = None -cf_extract = 0 -cf_test_read = 0 -cf_test_unrar = 0 -cf_test_memory = 0 - - -def check_crc(f, inf, desc): - """Compare result crc to expected value. - """ - exp = inf._md_expect - if exp is None: - return - ucrc = f._md_context.digest() - if ucrc != exp: - print('crc error - %s - exp=%r got=%r' % (desc, exp, ucrc)) - - -def test_read_long(r, inf): - """Test read and readinto. - """ - md_class = inf._md_class or rf.NoHashContext - bctx = md_class() - f = r.open(inf.filename) - total = 0 - while 1: - data = f.read(8192) - if not data: - break - bctx.update(data) - total += len(data) - if total != inf.file_size: - xprint("\n *** %s has corrupt file: %s ***", r.rarfile, inf.filename) - xprint(" *** short read: got=%d, need=%d ***\n", total, inf.file_size) - check_crc(f, inf, 'read') - bhash = bctx.hexdigest() - if cf_verbose > 1: - if f._md_context.digest() == inf._md_expect: - #xprint(" checkhash: %r", bhash) - pass - else: - xprint(" checkhash: %r got=%r exp=%r cls=%r\n", - bhash, f._md_context.digest(), inf._md_expect, inf._md_class) - - # test .seek() & .readinto() - if cf_test_read > 1: - f.seek(0, 0) - - total = 0 - buf = bytearray(rf.ZERO * 1024) - while 1: - res = f.readinto(buf) - if not res: - break - total += res - if inf.file_size != total: - xprint(" *** readinto failed: got=%d, need=%d ***\n", total, inf.file_size) - #check_crc(f, inf, 'readinto') - f.close() - - -def test_read(r, inf): - """Test file read.""" - test_read_long(r, inf) - - -def test_real(fn, psw): - """Actual archive processing. - """ - xprint("Archive: %s", fn) - - cb = None - if cf_verbose > 1: - cb = show_item - - rfarg = fn - if cf_test_memory: - rfarg = io.BytesIO(open(fn, 'rb').read()) - - # check if rar - if not rf.is_rarfile(rfarg): - xprint(" --- %s is not a RAR file ---", fn) - return - - # open - r = rf.RarFile(rfarg, charset=cf_charset, info_callback=cb) - # set password - if r.needs_password(): - if psw: - r.setpassword(psw) - else: - xprint(" --- %s requires password ---", fn) - return - - # show comment - if cf_show_comment and r.comment: - for ln in r.comment.split('\n'): - xprint(" %s", ln) - elif cf_verbose > 0 and r.comment: - cm = repr(r.comment) - if cm[0] == 'u': - cm = cm[1:] - xprint(" comment=%s", cm) - - # process - for n in r.namelist(): - inf = r.getinfo(n) - if inf.isdir(): - continue - if cf_verbose == 1: - show_item(inf) - if cf_test_read: - test_read(r, inf) - - if cf_extract: - r.extractall() - for inf in r.infolist(): - r.extract(inf) - - if cf_test_unrar: - r.testrar() - - -def test(fn, psw): - """Process one archive with error handling. - """ - try: - test_real(fn, psw) - except rf.NeedFirstVolume: - xprint(" --- %s is middle part of multi-vol archive ---", fn) - except rf.Error: - exc, msg, tb = sys.exc_info() - xprint("\n *** %s: %s ***\n", exc.__name__, msg) - del tb - except IOError: - exc, msg, tb = sys.exc_info() - xprint("\n *** %s: %s ***\n", exc.__name__, msg) - del tb - - -def main(): - """Program entry point. - """ - global cf_verbose, cf_show_comment, cf_charset - global cf_extract, cf_test_read, cf_test_unrar - global cf_test_memory - - psw = None - - # parse args - try: - opts, args = getopt.getopt(sys.argv[1:], 'p:C:hvcxtRM') - except getopt.error as ex: - print(str(ex), file=sys.stderr) - sys.exit(1) - - for o, v in opts: - if o == '-p': - psw = v - elif o == '-h': - xprint(usage) - return - elif o == '-v': - cf_verbose += 1 - elif o == '-c': - cf_show_comment = 1 - elif o == '-x': - cf_extract = 1 - elif o == '-t': - cf_test_read += 1 - elif o == '-T': - cf_test_unrar = 1 - elif o == '-M': - cf_test_memory = 1 - elif o == '-C': - cf_charset = v - else: - raise Exception("unhandled switch: " + o) - - args2 = [] - for a in args: - if a[0] == "@": - for ln in open(a[1:], 'r'): - fn = ln[:-1] - args2.append(fn) - else: - args2.append(a) - args = args2 - - if not args: - xprint(usage) - - # pypy .readinto()+memoryview() is buggy - #if cf_test_read > 1 and hasattr(sys, 'pypy_version_info'): - # cf_test_read = 1 - - for fn in args: - test(fn, psw) - - -if __name__ == '__main__': - try: - main() - except KeyboardInterrupt: - pass - diff --git a/libs/engineio/__init__.py b/libs/engineio/__init__.py index b87baf0c0..a4c21bb66 100644 --- a/libs/engineio/__init__.py +++ b/libs/engineio/__init__.py @@ -17,9 +17,7 @@ else: # pragma: no cover get_tornado_handler = None ASGIApp = None -__version__ = '4.2.1dev' - -__all__ = ['__version__', 'Server', 'WSGIApp', 'Middleware', 'Client'] +__all__ = ['Server', 'WSGIApp', 'Middleware', 'Client'] if AsyncServer is not None: # pragma: no cover __all__ += ['AsyncServer', 'ASGIApp', 'get_tornado_handler', 'AsyncClient'], diff --git a/libs/engineio/async_drivers/sanic.py b/libs/engineio/async_drivers/sanic.py index e9555f310..88b3e5ffb 100644 --- a/libs/engineio/async_drivers/sanic.py +++ b/libs/engineio/async_drivers/sanic.py @@ -3,7 +3,11 @@ from urllib.parse import urlsplit try: # pragma: no cover from sanic.response import HTTPResponse - from sanic.websocket import WebSocketProtocol + try: + from sanic.server.protocols.websocket_protocol import WebSocketProtocol + except ImportError: + print('yay') + from sanic.websocket import WebSocketProtocol except ImportError: HTTPResponse = None WebSocketProtocol = None diff --git a/libs/ffsubsync/__init__.py b/libs/ffsubsync/__init__.py index 3c40aeeba..0ad6c1236 100644 --- a/libs/ffsubsync/__init__.py +++ b/libs/ffsubsync/__init__.py @@ -1,4 +1,4 @@ -# -*- coding: utf-8 -*- +# -*- coding: utf-8 -*- import logging import sys @@ -12,7 +12,7 @@ try: level=logging.INFO, format="%(message)s", datefmt="[%X]", - handlers=[RichHandler(console=Console(file=sys.stderr))] + handlers=[RichHandler(console=Console(file=sys.stderr))], ) except ImportError: logging.basicConfig(stream=sys.stderr, level=logging.INFO) diff --git a/libs/ffsubsync/_version.py b/libs/ffsubsync/_version.py index 910ca384f..7215e42bb 100644 --- a/libs/ffsubsync/_version.py +++ b/libs/ffsubsync/_version.py @@ -1,520 +1,21 @@ -# This file helps to compute a version number in source trees obtained from -# git-archive tarball (such as those provided by githubs download-from-tag -# feature). Distribution tarballs (built by setup.py sdist) and build -# directories (produced by setup.py build) will contain a much shorter file -# that just contains the computed version number. - -# This file is released into the public domain. Generated by -# versioneer-0.18 (https://github.com/warner/python-versioneer) - -"""Git implementation of _version.py.""" - -import errno -import os -import re -import subprocess -import sys - - -def get_keywords(): - """Get the keywords needed to look up the version information.""" - # these strings will be replaced by git during git-archive. - # setup.py/versioneer.py will grep for the variable names, so they must - # each be defined on a line of their own. _version.py will just call - # get_keywords(). - git_refnames = " (tag: 0.4.11)" - git_full = "fe416b437c28cd6cf383248b90005a2d516549f2" - git_date = "2021-01-29 22:33:25 -0800" - keywords = {"refnames": git_refnames, "full": git_full, "date": git_date} - return keywords - - -class VersioneerConfig: - """Container for Versioneer configuration parameters.""" - - -def get_config(): - """Create, populate and return the VersioneerConfig() object.""" - # these strings are filled in when 'setup.py versioneer' creates - # _version.py - cfg = VersioneerConfig() - cfg.VCS = "git" - cfg.style = "pep440-pre" - cfg.tag_prefix = "" - cfg.parentdir_prefix = "ffsubsync-" - cfg.versionfile_source = "ffsubsync/_version.py" - cfg.verbose = False - return cfg - - -class NotThisMethod(Exception): - """Exception raised if a method is not valid for the current scenario.""" - - -LONG_VERSION_PY = {} -HANDLERS = {} - - -def register_vcs_handler(vcs, method): # decorator - """Decorator to mark a method as the handler for a particular VCS.""" - def decorate(f): - """Store f in HANDLERS[vcs][method].""" - if vcs not in HANDLERS: - HANDLERS[vcs] = {} - HANDLERS[vcs][method] = f - return f - return decorate - - -def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False, - env=None): - """Call the given command(s).""" - assert isinstance(commands, list) - p = None - for c in commands: - try: - dispcmd = str([c] + args) - # remember shell=False, so use git.cmd on windows, not just git - p = subprocess.Popen([c] + args, cwd=cwd, env=env, - stdout=subprocess.PIPE, - stderr=(subprocess.PIPE if hide_stderr - else None)) - break - except EnvironmentError: - e = sys.exc_info()[1] - if e.errno == errno.ENOENT: - continue - if verbose: - print("unable to run %s" % dispcmd) - print(e) - return None, None - else: - if verbose: - print("unable to find command, tried %s" % (commands,)) - return None, None - stdout = p.communicate()[0].strip() - if sys.version_info[0] >= 3: - stdout = stdout.decode() - if p.returncode != 0: - if verbose: - print("unable to run %s (error)" % dispcmd) - print("stdout was %s" % stdout) - return None, p.returncode - return stdout, p.returncode - - -def versions_from_parentdir(parentdir_prefix, root, verbose): - """Try to determine the version from the parent directory name. - - Source tarballs conventionally unpack into a directory that includes both - the project name and a version string. We will also support searching up - two directory levels for an appropriately named parent directory - """ - rootdirs = [] - - for i in range(3): - dirname = os.path.basename(root) - if dirname.startswith(parentdir_prefix): - return {"version": dirname[len(parentdir_prefix):], - "full-revisionid": None, - "dirty": False, "error": None, "date": None} - else: - rootdirs.append(root) - root = os.path.dirname(root) # up a level - - if verbose: - print("Tried directories %s but none started with prefix %s" % - (str(rootdirs), parentdir_prefix)) - raise NotThisMethod("rootdir doesn't start with parentdir_prefix") - - -@register_vcs_handler("git", "get_keywords") -def git_get_keywords(versionfile_abs): - """Extract version information from the given file.""" - # the code embedded in _version.py can just fetch the value of these - # keywords. When used from setup.py, we don't want to import _version.py, - # so we do it with a regexp instead. This function is not used from - # _version.py. - keywords = {} - try: - f = open(versionfile_abs, "r") - for line in f.readlines(): - if line.strip().startswith("git_refnames ="): - mo = re.search(r'=\s*"(.*)"', line) - if mo: - keywords["refnames"] = mo.group(1) - if line.strip().startswith("git_full ="): - mo = re.search(r'=\s*"(.*)"', line) - if mo: - keywords["full"] = mo.group(1) - if line.strip().startswith("git_date ="): - mo = re.search(r'=\s*"(.*)"', line) - if mo: - keywords["date"] = mo.group(1) - f.close() - except EnvironmentError: - pass - return keywords - - -@register_vcs_handler("git", "keywords") -def git_versions_from_keywords(keywords, tag_prefix, verbose): - """Get version information from git keywords.""" - if not keywords: - raise NotThisMethod("no keywords at all, weird") - date = keywords.get("date") - if date is not None: - # git-2.2.0 added "%cI", which expands to an ISO-8601 -compliant - # datestamp. However we prefer "%ci" (which expands to an "ISO-8601 - # -like" string, which we must then edit to make compliant), because - # it's been around since git-1.5.3, and it's too difficult to - # discover which version we're using, or to work around using an - # older one. - date = date.strip().replace(" ", "T", 1).replace(" ", "", 1) - refnames = keywords["refnames"].strip() - if refnames.startswith("$Format"): - if verbose: - print("keywords are unexpanded, not using") - raise NotThisMethod("unexpanded keywords, not a git-archive tarball") - refs = set([r.strip() for r in refnames.strip("()").split(",")]) - # starting in git-1.8.3, tags are listed as "tag: foo-1.0" instead of - # just "foo-1.0". If we see a "tag: " prefix, prefer those. - TAG = "tag: " - tags = set([r[len(TAG):] for r in refs if r.startswith(TAG)]) - if not tags: - # Either we're using git < 1.8.3, or there really are no tags. We use - # a heuristic: assume all version tags have a digit. The old git %d - # expansion behaves like git log --decorate=short and strips out the - # refs/heads/ and refs/tags/ prefixes that would let us distinguish - # between branches and tags. By ignoring refnames without digits, we - # filter out many common branch names like "release" and - # "stabilization", as well as "HEAD" and "master". - tags = set([r for r in refs if re.search(r'\d', r)]) - if verbose: - print("discarding '%s', no digits" % ",".join(refs - tags)) - if verbose: - print("likely tags: %s" % ",".join(sorted(tags))) - for ref in sorted(tags): - # sorting will prefer e.g. "2.0" over "2.0rc1" - if ref.startswith(tag_prefix): - r = ref[len(tag_prefix):] - if verbose: - print("picking %s" % r) - return {"version": r, - "full-revisionid": keywords["full"].strip(), - "dirty": False, "error": None, - "date": date} - # no suitable tags, so version is "0+unknown", but full hex is still there - if verbose: - print("no suitable tags, using unknown + full revision id") - return {"version": "0+unknown", - "full-revisionid": keywords["full"].strip(), - "dirty": False, "error": "no suitable tags", "date": None} - - -@register_vcs_handler("git", "pieces_from_vcs") -def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): - """Get version from 'git describe' in the root of the source tree. - - This only gets called if the git-archive 'subst' keywords were *not* - expanded, and _version.py hasn't already been rewritten with a short - version string, meaning we're inside a checked out source tree. - """ - GITS = ["git"] - if sys.platform == "win32": - GITS = ["git.cmd", "git.exe"] - - out, rc = run_command(GITS, ["rev-parse", "--git-dir"], cwd=root, - hide_stderr=True) - if rc != 0: - if verbose: - print("Directory %s not under git control" % root) - raise NotThisMethod("'git rev-parse --git-dir' returned error") - - # if there is a tag matching tag_prefix, this yields TAG-NUM-gHEX[-dirty] - # if there isn't one, this yields HEX[-dirty] (no NUM) - describe_out, rc = run_command(GITS, ["describe", "--tags", "--dirty", - "--always", "--long", - "--match", "%s*" % tag_prefix], - cwd=root) - # --long was added in git-1.5.5 - if describe_out is None: - raise NotThisMethod("'git describe' failed") - describe_out = describe_out.strip() - full_out, rc = run_command(GITS, ["rev-parse", "HEAD"], cwd=root) - if full_out is None: - raise NotThisMethod("'git rev-parse' failed") - full_out = full_out.strip() - - pieces = {} - pieces["long"] = full_out - pieces["short"] = full_out[:7] # maybe improved later - pieces["error"] = None - - # parse describe_out. It will be like TAG-NUM-gHEX[-dirty] or HEX[-dirty] - # TAG might have hyphens. - git_describe = describe_out - - # look for -dirty suffix - dirty = git_describe.endswith("-dirty") - pieces["dirty"] = dirty - if dirty: - git_describe = git_describe[:git_describe.rindex("-dirty")] - - # now we have TAG-NUM-gHEX or HEX - - if "-" in git_describe: - # TAG-NUM-gHEX - mo = re.search(r'^(.+)-(\d+)-g([0-9a-f]+)$', git_describe) - if not mo: - # unparseable. Maybe git-describe is misbehaving? - pieces["error"] = ("unable to parse git-describe output: '%s'" - % describe_out) - return pieces - - # tag - full_tag = mo.group(1) - if not full_tag.startswith(tag_prefix): - if verbose: - fmt = "tag '%s' doesn't start with prefix '%s'" - print(fmt % (full_tag, tag_prefix)) - pieces["error"] = ("tag '%s' doesn't start with prefix '%s'" - % (full_tag, tag_prefix)) - return pieces - pieces["closest-tag"] = full_tag[len(tag_prefix):] - - # distance: number of commits since tag - pieces["distance"] = int(mo.group(2)) - - # commit: short hex revision ID - pieces["short"] = mo.group(3) - - else: - # HEX: no tags - pieces["closest-tag"] = None - count_out, rc = run_command(GITS, ["rev-list", "HEAD", "--count"], - cwd=root) - pieces["distance"] = int(count_out) # total number of commits - - # commit date: see ISO-8601 comment in git_versions_from_keywords() - date = run_command(GITS, ["show", "-s", "--format=%ci", "HEAD"], - cwd=root)[0].strip() - pieces["date"] = date.strip().replace(" ", "T", 1).replace(" ", "", 1) - - return pieces - - -def plus_or_dot(pieces): - """Return a + if we don't already have one, else return a .""" - if "+" in pieces.get("closest-tag", ""): - return "." - return "+" - - -def render_pep440(pieces): - """Build up version string, with post-release "local version identifier". - - Our goal: TAG[+DISTANCE.gHEX[.dirty]] . Note that if you - get a tagged build and then dirty it, you'll get TAG+0.gHEX.dirty - - Exceptions: - 1: no tags. git_describe was just HEX. 0+untagged.DISTANCE.gHEX[.dirty] - """ - if pieces["closest-tag"]: - rendered = pieces["closest-tag"] - if pieces["distance"] or pieces["dirty"]: - rendered += plus_or_dot(pieces) - rendered += "%d.g%s" % (pieces["distance"], pieces["short"]) - if pieces["dirty"]: - rendered += ".dirty" - else: - # exception #1 - rendered = "0+untagged.%d.g%s" % (pieces["distance"], - pieces["short"]) - if pieces["dirty"]: - rendered += ".dirty" - return rendered - - -def render_pep440_pre(pieces): - """TAG[.post.devDISTANCE] -- No -dirty. - - Exceptions: - 1: no tags. 0.post.devDISTANCE - """ - if pieces["closest-tag"]: - rendered = pieces["closest-tag"] - if pieces["distance"]: - rendered += ".post.dev%d" % pieces["distance"] - else: - # exception #1 - rendered = "0.post.dev%d" % pieces["distance"] - return rendered - - -def render_pep440_post(pieces): - """TAG[.postDISTANCE[.dev0]+gHEX] . - - The ".dev0" means dirty. Note that .dev0 sorts backwards - (a dirty tree will appear "older" than the corresponding clean one), - but you shouldn't be releasing software with -dirty anyways. - - Exceptions: - 1: no tags. 0.postDISTANCE[.dev0] - """ - if pieces["closest-tag"]: - rendered = pieces["closest-tag"] - if pieces["distance"] or pieces["dirty"]: - rendered += ".post%d" % pieces["distance"] - if pieces["dirty"]: - rendered += ".dev0" - rendered += plus_or_dot(pieces) - rendered += "g%s" % pieces["short"] - else: - # exception #1 - rendered = "0.post%d" % pieces["distance"] - if pieces["dirty"]: - rendered += ".dev0" - rendered += "+g%s" % pieces["short"] - return rendered - - -def render_pep440_old(pieces): - """TAG[.postDISTANCE[.dev0]] . - - The ".dev0" means dirty. - - Eexceptions: - 1: no tags. 0.postDISTANCE[.dev0] - """ - if pieces["closest-tag"]: - rendered = pieces["closest-tag"] - if pieces["distance"] or pieces["dirty"]: - rendered += ".post%d" % pieces["distance"] - if pieces["dirty"]: - rendered += ".dev0" - else: - # exception #1 - rendered = "0.post%d" % pieces["distance"] - if pieces["dirty"]: - rendered += ".dev0" - return rendered - - -def render_git_describe(pieces): - """TAG[-DISTANCE-gHEX][-dirty]. - - Like 'git describe --tags --dirty --always'. - - Exceptions: - 1: no tags. HEX[-dirty] (note: no 'g' prefix) - """ - if pieces["closest-tag"]: - rendered = pieces["closest-tag"] - if pieces["distance"]: - rendered += "-%d-g%s" % (pieces["distance"], pieces["short"]) - else: - # exception #1 - rendered = pieces["short"] - if pieces["dirty"]: - rendered += "-dirty" - return rendered - - -def render_git_describe_long(pieces): - """TAG-DISTANCE-gHEX[-dirty]. - - Like 'git describe --tags --dirty --always -long'. - The distance/hash is unconditional. - - Exceptions: - 1: no tags. HEX[-dirty] (note: no 'g' prefix) - """ - if pieces["closest-tag"]: - rendered = pieces["closest-tag"] - rendered += "-%d-g%s" % (pieces["distance"], pieces["short"]) - else: - # exception #1 - rendered = pieces["short"] - if pieces["dirty"]: - rendered += "-dirty" - return rendered - - -def render(pieces, style): - """Render the given version pieces into the requested style.""" - if pieces["error"]: - return {"version": "unknown", - "full-revisionid": pieces.get("long"), - "dirty": None, - "error": pieces["error"], - "date": None} - - if not style or style == "default": - style = "pep440" # the default - - if style == "pep440": - rendered = render_pep440(pieces) - elif style == "pep440-pre": - rendered = render_pep440_pre(pieces) - elif style == "pep440-post": - rendered = render_pep440_post(pieces) - elif style == "pep440-old": - rendered = render_pep440_old(pieces) - elif style == "git-describe": - rendered = render_git_describe(pieces) - elif style == "git-describe-long": - rendered = render_git_describe_long(pieces) - else: - raise ValueError("unknown style '%s'" % style) - - return {"version": rendered, "full-revisionid": pieces["long"], - "dirty": pieces["dirty"], "error": None, - "date": pieces.get("date")} +# This file was generated by 'versioneer.py' (0.18) from +# revision-control system data, or from the parent directory name of an +# unpacked source archive. Distribution tarballs contain a pre-generated copy +# of this file. + +import json + +version_json = ''' +{ + "date": "2022-01-07T20:35:34-0800", + "dirty": false, + "error": null, + "full-revisionid": "9ae15d825b24b3445112683bbb7b2e4a9d3ecb8f", + "version": "0.4.20" +} +''' # END VERSION_JSON def get_versions(): - """Get version information or return default if unable to do so.""" - # I am in _version.py, which lives at ROOT/VERSIONFILE_SOURCE. If we have - # __file__, we can work backwards from there to the root. Some - # py2exe/bbfreeze/non-CPython implementations don't do __file__, in which - # case we can only use expanded keywords. - - cfg = get_config() - verbose = cfg.verbose - - try: - return git_versions_from_keywords(get_keywords(), cfg.tag_prefix, - verbose) - except NotThisMethod: - pass - - try: - root = os.path.realpath(__file__) - # versionfile_source is the relative path from the top of the source - # tree (where the .git directory might live) to this file. Invert - # this to find the root from __file__. - for i in cfg.versionfile_source.split('/'): - root = os.path.dirname(root) - except NameError: - return {"version": "0+unknown", "full-revisionid": None, - "dirty": None, - "error": "unable to find root of source tree", - "date": None} - - try: - pieces = git_pieces_from_vcs(cfg.tag_prefix, root, verbose) - return render(pieces, cfg.style) - except NotThisMethod: - pass - - try: - if cfg.parentdir_prefix: - return versions_from_parentdir(cfg.parentdir_prefix, root, verbose) - except NotThisMethod: - pass - - return {"version": "0+unknown", "full-revisionid": None, - "dirty": None, - "error": "unable to compute version", "date": None} + return json.loads(version_json) diff --git a/libs/ffsubsync/aligners.py b/libs/ffsubsync/aligners.py index b74cf23c2..f02243dd2 100644 --- a/libs/ffsubsync/aligners.py +++ b/libs/ffsubsync/aligners.py @@ -1,15 +1,20 @@ -# -*- coding: utf-8 -*- +# -*- coding: utf-8 -*- import logging import math +from typing import List, Optional, Tuple, Type, Union import numpy as np -from .constants import FRAMERATE_RATIOS -from .golden_section_search import gss -from .sklearn_shim import TransformerMixin +from ffsubsync.golden_section_search import gss +from ffsubsync.sklearn_shim import Pipeline, TransformerMixin + logging.basicConfig(level=logging.INFO) -logger = logging.getLogger(__name__) +logger: logging.Logger = logging.getLogger(__name__) + + +MIN_FRAMERATE_RATIO = 0.9 +MAX_FRAMERATE_RATIO = 1.1 class FailedToFindAlignmentException(Exception): @@ -17,46 +22,52 @@ class FailedToFindAlignmentException(Exception): class FFTAligner(TransformerMixin): - def __init__(self, max_offset_samples=None): - self.max_offset_samples = max_offset_samples - self.best_offset_ = None - self.best_score_ = None - self.get_score_ = False + def __init__(self, max_offset_samples: Optional[int] = None) -> None: + self.max_offset_samples: Optional[int] = max_offset_samples + self.best_offset_: Optional[int] = None + self.best_score_: Optional[float] = None + self.get_score_: bool = False - def _zero_out_extreme_offsets(self, convolve, substring): + def _eliminate_extreme_offsets_from_solutions( + self, convolve: np.ndarray, substring: np.ndarray + ) -> np.ndarray: convolve = np.copy(convolve) if self.max_offset_samples is None: return convolve offset_to_index = lambda offset: len(convolve) - 1 + offset - len(substring) - convolve[:offset_to_index(-self.max_offset_samples)] = convolve[offset_to_index(self.max_offset_samples):] = 0 + convolve[: offset_to_index(-self.max_offset_samples)] = float("-inf") + convolve[offset_to_index(self.max_offset_samples) :] = float("-inf") return convolve - def _compute_argmax(self, convolve, substring): + def _compute_argmax(self, convolve: np.ndarray, substring: np.ndarray) -> None: best_idx = np.argmax(convolve) self.best_offset_ = len(convolve) - 1 - best_idx - len(substring) self.best_score_ = convolve[best_idx] - def fit(self, refstring, substring, get_score=False): + def fit(self, refstring, substring, get_score: bool = False) -> "FFTAligner": refstring, substring = [ - list(map(int, s)) - if isinstance(s, str) else s + list(map(int, s)) if isinstance(s, str) else s for s in [refstring, substring] ] refstring, substring = map( - lambda s: 2 * np.array(s).astype(float) - 1, [refstring, substring]) + lambda s: 2 * np.array(s).astype(float) - 1, [refstring, substring] + ) total_bits = math.log(len(substring) + len(refstring), 2) total_length = int(2 ** math.ceil(total_bits)) extra_zeros = total_length - len(substring) - len(refstring) subft = np.fft.fft(np.append(np.zeros(extra_zeros + len(refstring)), substring)) - refft = np.fft.fft(np.flip(np.append(refstring, np.zeros(len(substring) + extra_zeros)), 0)) + refft = np.fft.fft( + np.flip(np.append(refstring, np.zeros(len(substring) + extra_zeros)), 0) + ) convolve = np.real(np.fft.ifft(subft * refft)) - self._compute_argmax(self._zero_out_extreme_offsets(convolve, substring), substring) - if self.best_score_ == 0.: - self._compute_argmax(convolve, substring) + self._compute_argmax( + self._eliminate_extreme_offsets_from_solutions(convolve, substring), + substring, + ) self.get_score_ = get_score return self - def transform(self, *_): + def transform(self, *_) -> Union[int, Tuple[float, int]]: if self.get_score_: return self.best_score_, self.best_offset_ else: @@ -64,57 +75,81 @@ class FFTAligner(TransformerMixin): class MaxScoreAligner(TransformerMixin): - def __init__(self, base_aligner, srtin=None, sample_rate=None, max_offset_seconds=None): - self.srtin = srtin + def __init__( + self, + base_aligner: Union[FFTAligner, Type[FFTAligner]], + srtin: Optional[str] = None, + sample_rate=None, + max_offset_seconds=None, + ) -> None: + self.srtin: Optional[str] = srtin if sample_rate is None or max_offset_seconds is None: - self.max_offset_samples = None + self.max_offset_samples: Optional[int] = None else: self.max_offset_samples = abs(int(max_offset_seconds * sample_rate)) if isinstance(base_aligner, type): - self.base_aligner = base_aligner(max_offset_samples=self.max_offset_samples) + self.base_aligner: FFTAligner = base_aligner( + max_offset_samples=self.max_offset_samples + ) else: self.base_aligner = base_aligner - self.max_offset_seconds = max_offset_seconds - self._scores = [] + self.max_offset_seconds: Optional[int] = max_offset_seconds + self._scores: List[Tuple[Tuple[float, int], Pipeline]] = [] def fit_gss(self, refstring, subpipe_maker): def opt_func(framerate_ratio, is_last_iter): subpipe = subpipe_maker(framerate_ratio) substring = subpipe.fit_transform(self.srtin) - score = self.base_aligner.fit_transform(refstring, substring, get_score=True) - logger.info('got score %.0f (offset %d) for ratio %.3f', score[0], score[1], framerate_ratio) + score = self.base_aligner.fit_transform( + refstring, substring, get_score=True + ) + logger.info( + "got score %.0f (offset %d) for ratio %.3f", + score[0], + score[1], + framerate_ratio, + ) if is_last_iter: self._scores.append((score, subpipe)) return -score[0] - gss(opt_func, 0.9, 1.1) + + gss(opt_func, MIN_FRAMERATE_RATIO, MAX_FRAMERATE_RATIO) return self - def fit(self, refstring, subpipes): + def fit( + self, refstring, subpipes: Union[Pipeline, List[Pipeline]] + ) -> "MaxScoreAligner": if not isinstance(subpipes, list): subpipes = [subpipes] for subpipe in subpipes: if callable(subpipe): self.fit_gss(refstring, subpipe) continue - elif hasattr(subpipe, 'transform'): + elif hasattr(subpipe, "transform"): substring = subpipe.transform(self.srtin) else: substring = subpipe - self._scores.append(( - self.base_aligner.fit_transform( - refstring, substring, get_score=True - ), - subpipe - )) + self._scores.append( + ( + self.base_aligner.fit_transform( + refstring, substring, get_score=True + ), + subpipe, + ) + ) return self - def transform(self, *_): + def transform(self, *_) -> Tuple[Tuple[float, float], Pipeline]: scores = self._scores if self.max_offset_samples is not None: - scores = list(filter(lambda s: abs(s[0][1]) <= self.max_offset_samples, scores)) + scores = list( + filter(lambda s: abs(s[0][1]) <= self.max_offset_samples, scores) + ) if len(scores) == 0: - raise FailedToFindAlignmentException('Synchronization failed; consider passing ' - '--max-offset-seconds with a number larger than ' - '{}'.format(self.max_offset_seconds)) + raise FailedToFindAlignmentException( + "Synchronization failed; consider passing " + "--max-offset-seconds with a number larger than " + "{}".format(self.max_offset_seconds) + ) (score, offset), subpipe = max(scores, key=lambda x: x[0][0]) return (score, offset), subpipe diff --git a/libs/ffsubsync/constants.py b/libs/ffsubsync/constants.py index ef4a0267f..b8df034fe 100644 --- a/libs/ffsubsync/constants.py +++ b/libs/ffsubsync/constants.py @@ -1,32 +1,41 @@ # -*- coding: utf-8 -*- -SUBSYNC_RESOURCES_ENV_MAGIC = "ffsubsync_resources_xj48gjdkl340" +from typing import List, Tuple -SAMPLE_RATE = 100 -FRAMERATE_RATIOS = [24./23.976, 25./23.976, 25./24.] +SUBSYNC_RESOURCES_ENV_MAGIC: str = "ffsubsync_resources_xj48gjdkl340" -DEFAULT_FRAME_RATE = 48000 -DEFAULT_NON_SPEECH_LABEL = 0. -DEFAULT_ENCODING = 'infer' -DEFAULT_MAX_SUBTITLE_SECONDS = 10 -DEFAULT_START_SECONDS = 0 -DEFAULT_SCALE_FACTOR = 1 -DEFAULT_VAD = 'subs_then_webrtc' -DEFAULT_MAX_OFFSET_SECONDS = 60 -DEFAULT_APPLY_OFFSET_SECONDS = 0 +SAMPLE_RATE: int = 100 -SUBTITLE_EXTENSIONS = ('srt', 'ass', 'ssa', 'sub') +FRAMERATE_RATIOS: List[float] = [24.0 / 23.976, 25.0 / 23.976, 25.0 / 24.0] -GITHUB_DEV_USER = 'smacke' -PROJECT_NAME = 'FFsubsync' -PROJECT_LICENSE = 'MIT' -COPYRIGHT_YEAR = '2019' -GITHUB_REPO = 'ffsubsync' -DESCRIPTION = 'Synchronize subtitles with video.' -LONG_DESCRIPTION = 'Automatic and language-agnostic synchronization of subtitles with video.' -WEBSITE = 'https://github.com/{}/{}/'.format(GITHUB_DEV_USER, GITHUB_REPO) -DEV_WEBSITE = 'https://smacke.net/' +DEFAULT_FRAME_RATE: int = 48000 +DEFAULT_NON_SPEECH_LABEL: float = 0.0 +DEFAULT_ENCODING: str = "infer" +DEFAULT_MAX_SUBTITLE_SECONDS: int = 10 +DEFAULT_START_SECONDS: int = 0 +DEFAULT_SCALE_FACTOR: float = 1 +DEFAULT_VAD: str = "subs_then_webrtc" +DEFAULT_MAX_OFFSET_SECONDS: int = 60 +DEFAULT_APPLY_OFFSET_SECONDS: int = 0 + +SUBTITLE_EXTENSIONS: Tuple[str, ...] = ("srt", "ass", "ssa", "sub") + +GITHUB_DEV_USER: str = "smacke" +PROJECT_NAME: str = "FFsubsync" +PROJECT_LICENSE: str = "MIT" +COPYRIGHT_YEAR: str = "2019" +GITHUB_REPO: str = "ffsubsync" +DESCRIPTION: str = "Synchronize subtitles with video." +LONG_DESCRIPTION: str = ( + "Automatic and language-agnostic synchronization of subtitles with video." +) +WEBSITE: str = "https://github.com/{}/{}/".format(GITHUB_DEV_USER, GITHUB_REPO) +DEV_WEBSITE: str = "https://smacke.net/" # No trailing slash important for this one... -API_RELEASE_URL = 'https://api.github.com/repos/{}/{}/releases/latest'.format(GITHUB_DEV_USER, GITHUB_REPO) -RELEASE_URL = 'https://github.com/{}/{}/releases/latest/'.format(GITHUB_DEV_USER, GITHUB_REPO) +API_RELEASE_URL: str = "https://api.github.com/repos/{}/{}/releases/latest".format( + GITHUB_DEV_USER, GITHUB_REPO +) +RELEASE_URL: str = "https://github.com/{}/{}/releases/latest/".format( + GITHUB_DEV_USER, GITHUB_REPO +) diff --git a/libs/ffsubsync/ffmpeg_utils.py b/libs/ffsubsync/ffmpeg_utils.py index 2bb0876db..7f5733404 100644 --- a/libs/ffsubsync/ffmpeg_utils.py +++ b/libs/ffsubsync/ffmpeg_utils.py @@ -4,10 +4,10 @@ import os import platform import subprocess -from .constants import SUBSYNC_RESOURCES_ENV_MAGIC +from ffsubsync.constants import SUBSYNC_RESOURCES_ENV_MAGIC logging.basicConfig(level=logging.INFO) -logger = logging.getLogger(__name__) +logger: logging.Logger = logging.getLogger(__name__) # ref: https://github.com/pyinstaller/pyinstaller/wiki/Recipe-subprocess @@ -23,7 +23,7 @@ logger = logging.getLogger(__name__) # **subprocess_args(False)) def subprocess_args(include_stdout=True): # The following is true only on Windows. - if hasattr(subprocess, 'STARTUPINFO'): + if hasattr(subprocess, "STARTUPINFO"): # On Windows, subprocess calls will pop up a command window by default # when run from Pyinstaller with the ``--noconsole`` option. Avoid this # distraction. @@ -47,7 +47,7 @@ def subprocess_args(include_stdout=True): # # So, add it only if it's needed. if include_stdout: - ret = {'stdout': subprocess.PIPE} + ret = {"stdout": subprocess.PIPE} else: ret = {} @@ -55,23 +55,29 @@ def subprocess_args(include_stdout=True): # with the ``--noconsole`` option requires redirecting everything # (stdin, stdout, stderr) to avoid an OSError exception # "[Error 6] the handle is invalid." - ret.update({'stdin': subprocess.PIPE, - 'stderr': subprocess.PIPE, - 'startupinfo': si, - 'env': env}) + ret.update( + { + "stdin": subprocess.PIPE, + "stderr": subprocess.PIPE, + "startupinfo": si, + "env": env, + } + ) return ret def ffmpeg_bin_path(bin_name, gui_mode, ffmpeg_resources_path=None): - if platform.system() == 'Windows': - bin_name = '{}.exe'.format(bin_name) + if platform.system() == "Windows": + bin_name = "{}.exe".format(bin_name) if ffmpeg_resources_path is not None: return os.path.join(ffmpeg_resources_path, bin_name) try: resource_path = os.environ[SUBSYNC_RESOURCES_ENV_MAGIC] if len(resource_path) > 0: - return os.path.join(resource_path, 'ffmpeg-bin', bin_name) + return os.path.join(resource_path, "ffmpeg-bin", bin_name) except KeyError: if gui_mode: - logger.info("Couldn't find resource path; falling back to searching system path") + logger.info( + "Couldn't find resource path; falling back to searching system path" + ) return bin_name diff --git a/libs/ffsubsync/ffsubsync.py b/libs/ffsubsync/ffsubsync.py index 9a79cd9a9..6fc8f2a20 100755 --- a/libs/ffsubsync/ffsubsync.py +++ b/libs/ffsubsync/ffsubsync.py @@ -7,47 +7,68 @@ import os import shutil import subprocess import sys +from typing import cast, Any, Callable, Dict, List, Optional, Tuple, Union import numpy as np -from .aligners import FFTAligner, MaxScoreAligner, FailedToFindAlignmentException -from .constants import * -from .ffmpeg_utils import ffmpeg_bin_path -from .sklearn_shim import Pipeline -from .speech_transformers import ( +from ffsubsync.aligners import ( + FFTAligner, + MaxScoreAligner, + FailedToFindAlignmentException, +) +from ffsubsync.constants import ( + DEFAULT_APPLY_OFFSET_SECONDS, + DEFAULT_FRAME_RATE, + DEFAULT_MAX_OFFSET_SECONDS, + DEFAULT_MAX_SUBTITLE_SECONDS, + DEFAULT_NON_SPEECH_LABEL, + DEFAULT_START_SECONDS, + DEFAULT_VAD, + DEFAULT_ENCODING, + FRAMERATE_RATIOS, + SAMPLE_RATE, + SUBTITLE_EXTENSIONS, +) +from ffsubsync.ffmpeg_utils import ffmpeg_bin_path +from ffsubsync.sklearn_shim import Pipeline, TransformerMixin +from ffsubsync.speech_transformers import ( VideoSpeechTransformer, DeserializeSpeechTransformer, - make_subtitle_speech_pipeline + make_subtitle_speech_pipeline, ) -from .subtitle_parser import make_subtitle_parser -from .subtitle_transformers import SubtitleMerger, SubtitleShifter -from .version import get_version - -logger = logging.getLogger(__name__) +from ffsubsync.subtitle_parser import make_subtitle_parser +from ffsubsync.subtitle_transformers import SubtitleMerger, SubtitleShifter +from ffsubsync.version import get_version -def override(args, **kwargs): +logger: logging.Logger = logging.getLogger(__name__) + + +def override(args: argparse.Namespace, **kwargs: Any) -> Dict[str, Any]: args_dict = dict(args.__dict__) args_dict.update(kwargs) return args_dict -def _ref_format(ref_fname): +def _ref_format(ref_fname: Optional[str]) -> Optional[str]: + if ref_fname is None: + return None return ref_fname[-3:] -def make_test_case(args, npy_savename, sync_was_successful): +def make_test_case( + args: argparse.Namespace, npy_savename: Optional[str], sync_was_successful: bool +) -> int: if npy_savename is None: - raise ValueError('need non-null npy_savename') - tar_dir = '{}.{}'.format( - args.reference, - datetime.now().strftime('%Y-%m-%d-%H-%M-%S') + raise ValueError("need non-null npy_savename") + tar_dir = "{}.{}".format( + args.reference, datetime.now().strftime("%Y-%m-%d-%H-%M-%S") ) - logger.info('creating test archive {}.tar.gz...'.format(tar_dir)) + logger.info("creating test archive {}.tar.gz...".format(tar_dir)) os.mkdir(tar_dir) try: - log_path = 'ffsubsync.log' - if args.log_dir_path and os.path.isdir(args.log_dir_path): + log_path = "ffsubsync.log" + if args.log_dir_path is not None and os.path.isdir(args.log_dir_path): log_path = os.path.join(args.log_dir_path, log_path) shutil.copy(log_path, tar_dir) shutil.copy(args.srtin[0], tar_dir) @@ -60,24 +81,28 @@ def make_test_case(args, npy_savename, sync_was_successful): else: shutil.move(npy_savename, tar_dir) supported_formats = set(list(zip(*shutil.get_archive_formats()))[0]) - preferred_formats = ['gztar', 'bztar', 'xztar', 'zip', 'tar'] + preferred_formats = ["gztar", "bztar", "xztar", "zip", "tar"] for archive_format in preferred_formats: if archive_format in supported_formats: shutil.make_archive(tar_dir, archive_format, os.curdir, tar_dir) break else: - logger.error('failed to create test archive; no formats supported ' - '(this should not happen)') + logger.error( + "failed to create test archive; no formats supported " + "(this should not happen)" + ) return 1 - logger.info('...done') + logger.info("...done") finally: shutil.rmtree(tar_dir) return 0 -def get_srt_pipe_maker(args, srtin): +def get_srt_pipe_maker( + args: argparse.Namespace, srtin: Optional[str] +) -> Callable[[Optional[float]], Union[Pipeline, Callable[[float], Pipeline]]]: if srtin is None: - srtin_format = 'srt' + srtin_format = "srt" else: srtin_format = os.path.splitext(srtin)[-1][1:] parser = make_subtitle_parser(fmt=srtin_format, caching=True, **args.__dict__) @@ -86,47 +111,69 @@ def get_srt_pipe_maker(args, srtin): ) -def get_framerate_ratios_to_try(args): +def get_framerate_ratios_to_try(args: argparse.Namespace) -> List[Optional[float]]: if args.no_fix_framerate: return [] else: - framerate_ratios = list(np.concatenate([ - np.array(FRAMERATE_RATIOS), 1./np.array(FRAMERATE_RATIOS) - ])) + framerate_ratios = list( + np.concatenate( + [np.array(FRAMERATE_RATIOS), 1.0 / np.array(FRAMERATE_RATIOS)] + ) + ) if args.gss: framerate_ratios.append(None) return framerate_ratios -def try_sync(args, reference_pipe, result): +def try_sync( + args: argparse.Namespace, reference_pipe: Optional[Pipeline], result: Dict[str, Any] +) -> bool: sync_was_successful = True exc = None try: - logger.info('extracting speech segments from %s...', - 'stdin' if not args.srtin else 'subtitles file(s) {}'.format(args.srtin)) + logger.info( + "extracting speech segments from %s...", + "stdin" if not args.srtin else "subtitles file(s) {}".format(args.srtin), + ) if not args.srtin: args.srtin = [None] for srtin in args.srtin: + skip_sync = args.skip_sync or reference_pipe is None + skip_infer_framerate_ratio = ( + args.skip_infer_framerate_ratio or reference_pipe is None + ) srtout = srtin if args.overwrite_input else args.srtout srt_pipe_maker = get_srt_pipe_maker(args, srtin) framerate_ratios = get_framerate_ratios_to_try(args) - srt_pipes = [srt_pipe_maker(1.)] + [srt_pipe_maker(rat) for rat in framerate_ratios] + srt_pipes = [srt_pipe_maker(1.0)] + [ + srt_pipe_maker(rat) for rat in framerate_ratios + ] for srt_pipe in srt_pipes: if callable(srt_pipe): continue else: srt_pipe.fit(srtin) - if not args.skip_infer_framerate_ratio and hasattr(reference_pipe[-1], 'num_frames'): - inferred_framerate_ratio_from_length = float(reference_pipe[-1].num_frames) / srt_pipes[0][-1].num_frames - logger.info('inferred frameratio ratio: %.3f' % inferred_framerate_ratio_from_length) - srt_pipes.append(srt_pipe_maker(inferred_framerate_ratio_from_length).fit(srtin)) - logger.info('...done') - logger.info('computing alignments...') - if args.skip_sync: - best_score = 0. - best_srt_pipe = srt_pipes[0] - if callable(best_srt_pipe): - best_srt_pipe = best_srt_pipe(1.0).fit(srtin) + if not skip_infer_framerate_ratio and hasattr( + reference_pipe[-1], "num_frames" + ): + inferred_framerate_ratio_from_length = ( + float(reference_pipe[-1].num_frames) + / cast(Pipeline, srt_pipes[0])[-1].num_frames + ) + logger.info( + "inferred frameratio ratio: %.3f" + % inferred_framerate_ratio_from_length + ) + srt_pipes.append( + cast( + Pipeline, srt_pipe_maker(inferred_framerate_ratio_from_length) + ).fit(srtin) + ) + logger.info("...done") + logger.info("computing alignments...") + if skip_sync: + best_score = 0.0 + best_srt_pipe = cast(Pipeline, srt_pipes[0]) offset_samples = 0 else: (best_score, offset_samples), best_srt_pipe = MaxScoreAligner( @@ -135,23 +182,38 @@ def try_sync(args, reference_pipe, result): reference_pipe.transform(args.reference), srt_pipes, ) - logger.info('...done') - offset_seconds = offset_samples / float(SAMPLE_RATE) + args.apply_offset_seconds - scale_step = best_srt_pipe.named_steps['scale'] - logger.info('score: %.3f', best_score) - logger.info('offset seconds: %.3f', offset_seconds) - logger.info('framerate scale factor: %.3f', scale_step.scale_factor) - output_steps = [('shift', SubtitleShifter(offset_seconds))] + logger.info("...done") + offset_seconds = ( + offset_samples / float(SAMPLE_RATE) + args.apply_offset_seconds + ) + scale_step = best_srt_pipe.named_steps["scale"] + logger.info("score: %.3f", best_score) + logger.info("offset seconds: %.3f", offset_seconds) + logger.info("framerate scale factor: %.3f", scale_step.scale_factor) + output_steps: List[Tuple[str, TransformerMixin]] = [ + ("shift", SubtitleShifter(offset_seconds)) + ] if args.merge_with_reference: output_steps.append( - ('merge', SubtitleMerger(reference_pipe.named_steps['parse'].subs_)) + ("merge", SubtitleMerger(reference_pipe.named_steps["parse"].subs_)) ) output_pipe = Pipeline(output_steps) out_subs = output_pipe.fit_transform(scale_step.subs_) - if args.output_encoding != 'same': + if args.output_encoding != "same": out_subs = out_subs.set_encoding(args.output_encoding) - logger.info('writing output to {}'.format(srtout or 'stdout')) - out_subs.write_file(srtout) + suppress_output_thresh = args.suppress_output_if_offset_less_than + if suppress_output_thresh is None or ( + scale_step.scale_factor == 1.0 + and offset_seconds >= suppress_output_thresh + ): + logger.info("writing output to {}".format(srtout or "stdout")) + out_subs.write_file(srtout) + else: + logger.warning( + "suppressing output because offset %s was less than suppression threshold %s", + offset_seconds, + args.suppress_output_if_offset_less_than, + ) except FailedToFindAlignmentException as e: sync_was_successful = False logger.error(e) @@ -160,285 +222,487 @@ def try_sync(args, reference_pipe, result): sync_was_successful = False logger.error(e) else: - result['offset_seconds'] = offset_seconds - result['framerate_scale_factor'] = scale_step.scale_factor + result["offset_seconds"] = offset_seconds + result["framerate_scale_factor"] = scale_step.scale_factor finally: if exc is not None: raise exc - result['sync_was_successful'] = sync_was_successful + result["sync_was_successful"] = sync_was_successful return sync_was_successful -def make_reference_pipe(args): +def make_reference_pipe(args: argparse.Namespace) -> Pipeline: ref_format = _ref_format(args.reference) if ref_format in SUBTITLE_EXTENSIONS: if args.vad is not None: - logger.warning('Vad specified, but reference was not a movie') - return make_subtitle_speech_pipeline( - fmt=ref_format, - **override( - args, - encoding=args.reference_encoding or DEFAULT_ENCODING - ) + logger.warning("Vad specified, but reference was not a movie") + return cast( + Pipeline, + make_subtitle_speech_pipeline( + fmt=ref_format, + **override(args, encoding=args.reference_encoding or DEFAULT_ENCODING), + ), ) - elif ref_format in ('npy', 'npz'): + elif ref_format in ("npy", "npz"): if args.vad is not None: - logger.warning('Vad specified, but reference was not a movie') - return Pipeline([ - ('deserialize', DeserializeSpeechTransformer(args.non_speech_label)) - ]) + logger.warning("Vad specified, but reference was not a movie") + return Pipeline( + [("deserialize", DeserializeSpeechTransformer(args.non_speech_label))] + ) else: vad = args.vad or DEFAULT_VAD if args.reference_encoding is not None: - logger.warning('Reference srt encoding specified, but reference was a video file') + logger.warning( + "Reference srt encoding specified, but reference was a video file" + ) ref_stream = args.reference_stream - if ref_stream is not None and not ref_stream.startswith('0:'): - ref_stream = '0:' + ref_stream - return Pipeline([ - ('speech_extract', VideoSpeechTransformer( - vad=vad, - sample_rate=SAMPLE_RATE, - frame_rate=args.frame_rate, - non_speech_label=args.non_speech_label, - start_seconds=args.start_seconds, - ffmpeg_path=args.ffmpeg_path, - ref_stream=ref_stream, - vlc_mode=args.vlc_mode, - gui_mode=args.gui_mode - )), - ]) + if ref_stream is not None and not ref_stream.startswith("0:"): + ref_stream = "0:" + ref_stream + return Pipeline( + [ + ( + "speech_extract", + VideoSpeechTransformer( + vad=vad, + sample_rate=SAMPLE_RATE, + frame_rate=args.frame_rate, + non_speech_label=args.non_speech_label, + start_seconds=args.start_seconds, + ffmpeg_path=args.ffmpeg_path, + ref_stream=ref_stream, + vlc_mode=args.vlc_mode, + gui_mode=args.gui_mode, + ), + ), + ] + ) -def extract_subtitles_from_reference(args): +def extract_subtitles_from_reference(args: argparse.Namespace) -> int: stream = args.extract_subs_from_stream - if not stream.startswith('0:s:'): - stream = '0:s:{}'.format(stream) - elif not stream.startswith('0:') and stream.startswith('s:'): - stream = '0:{}'.format(stream) - if not stream.startswith('0:s:'): - logger.error('invalid stream for subtitle extraction: %s', args.extract_subs_from_stream) - ffmpeg_args = [ffmpeg_bin_path('ffmpeg', args.gui_mode, ffmpeg_resources_path=args.ffmpeg_path)] - ffmpeg_args.extend([ - '-y', - '-nostdin', - '-loglevel', 'fatal', - '-i', args.reference, - '-map', '{}'.format(stream), - '-f', 'srt', - ]) + if not stream.startswith("0:s:"): + stream = "0:s:{}".format(stream) + elif not stream.startswith("0:") and stream.startswith("s:"): + stream = "0:{}".format(stream) + if not stream.startswith("0:s:"): + logger.error( + "invalid stream for subtitle extraction: %s", args.extract_subs_from_stream + ) + ffmpeg_args = [ + ffmpeg_bin_path("ffmpeg", args.gui_mode, ffmpeg_resources_path=args.ffmpeg_path) + ] + ffmpeg_args.extend( + [ + "-y", + "-nostdin", + "-loglevel", + "fatal", + "-i", + args.reference, + "-map", + "{}".format(stream), + "-f", + "srt", + ] + ) if args.srtout is None: - ffmpeg_args.append('-') + ffmpeg_args.append("-") else: ffmpeg_args.append(args.srtout) - logger.info('attempting to extract subtitles to {} ...'.format('stdout' if args.srtout is None else args.srtout)) + logger.info( + "attempting to extract subtitles to {} ...".format( + "stdout" if args.srtout is None else args.srtout + ) + ) retcode = subprocess.call(ffmpeg_args) if retcode == 0: - logger.info('...done') + logger.info("...done") else: - logger.error('ffmpeg unable to extract subtitles from reference; return code %d', retcode) + logger.error( + "ffmpeg unable to extract subtitles from reference; return code %d", retcode + ) return retcode -def validate_args(args): +def validate_args(args: argparse.Namespace) -> None: if args.vlc_mode: logger.setLevel(logging.CRITICAL) - if len(args.srtin) > 1 and not args.overwrite_input: - raise ValueError('cannot specify multiple input srt files without overwriting') - if len(args.srtin) > 1 and args.make_test_case: - raise ValueError('cannot specify multiple input srt files for test cases') - if len(args.srtin) > 1 and args.gui_mode: - raise ValueError('cannot specify multiple input srt files in GUI mode') - if args.make_test_case and not args.gui_mode: # this validation not necessary for gui mode - if args.srtin is None or args.srtout is None: - raise ValueError('need to specify input and output srt files for test cases') - if args.overwrite_input: - if args.extract_subs_from_stream is not None: - raise ValueError('input overwriting not allowed for extracting subtitles from reference') + if args.reference is None: + if args.apply_offset_seconds == 0 or not args.srtin: + raise ValueError( + "`reference` required unless `--apply-offset-seconds` specified" + ) + if args.apply_offset_seconds != 0: + if not args.srtin: + args.srtin = [args.reference] if not args.srtin: raise ValueError( - 'need to specify input srt if --overwrite-input is specified since we cannot overwrite stdin' + "at least one of `srtin` or `reference` must be specified to apply offset seconds" + ) + if args.srtin: + if len(args.srtin) > 1 and not args.overwrite_input: + raise ValueError( + "cannot specify multiple input srt files without overwriting" + ) + if len(args.srtin) > 1 and args.make_test_case: + raise ValueError("cannot specify multiple input srt files for test cases") + if len(args.srtin) > 1 and args.gui_mode: + raise ValueError("cannot specify multiple input srt files in GUI mode") + if ( + args.make_test_case and not args.gui_mode + ): # this validation not necessary for gui mode + if not args.srtin or args.srtout is None: + raise ValueError( + "need to specify input and output srt files for test cases" + ) + if args.overwrite_input: + if args.extract_subs_from_stream is not None: + raise ValueError( + "input overwriting not allowed for extracting subtitles from reference" + ) + if not args.srtin: + raise ValueError( + "need to specify input srt if --overwrite-input is specified since we cannot overwrite stdin" ) if args.srtout is not None: raise ValueError( - 'overwrite input set but output file specified; refusing to run in case this was not intended' + "overwrite input set but output file specified; refusing to run in case this was not intended" ) if args.extract_subs_from_stream is not None: if args.make_test_case: - raise ValueError('test case is for sync and not subtitle extraction') + raise ValueError("test case is for sync and not subtitle extraction") if args.srtin: - raise ValueError('stream specified for reference subtitle extraction; -i flag for sync input not allowed') + raise ValueError( + "stream specified for reference subtitle extraction; -i flag for sync input not allowed" + ) -def validate_file_permissions(args): - error_string_template = 'unable to {action} {file}; try ensuring file exists and has correct permissions' - if not os.access(args.reference, os.R_OK): - raise ValueError(error_string_template.format(action='read reference', file=args.reference)) - for srtin in args.srtin: - if srtin is not None and not os.access(srtin, os.R_OK): - raise ValueError(error_string_template.format(action='read input subtitles', file=srtin)) - if args.srtout is not None and os.path.exists(args.srtout) and not os.access(args.srtout, os.W_OK): - raise ValueError(error_string_template.format(action='write output subtitles', file=args.srtout)) +def validate_file_permissions(args: argparse.Namespace) -> None: + error_string_template = "unable to {action} {file}; try ensuring file exists and has correct permissions" + if args.reference is not None and not os.access(args.reference, os.R_OK): + raise ValueError( + error_string_template.format(action="read reference", file=args.reference) + ) + if args.srtin: + for srtin in args.srtin: + if srtin is not None and not os.access(srtin, os.R_OK): + raise ValueError( + error_string_template.format( + action="read input subtitles", file=srtin + ) + ) + if ( + args.srtout is not None + and os.path.exists(args.srtout) + and not os.access(args.srtout, os.W_OK) + ): + raise ValueError( + error_string_template.format( + action="write output subtitles", file=args.srtout + ) + ) if args.make_test_case or args.serialize_speech: - npy_savename = os.path.splitext(args.reference)[0] + '.npz' + npy_savename = os.path.splitext(args.reference)[0] + ".npz" if os.path.exists(npy_savename) and not os.access(npy_savename, os.W_OK): - raise ValueError('unable to write test case file archive %s (try checking permissions)' % npy_savename) + raise ValueError( + "unable to write test case file archive %s (try checking permissions)" + % npy_savename + ) -def run(args): - result = { - 'retval': 0, - 'offset_seconds': None, - 'framerate_scale_factor': None, - 'sync_was_successful': None - } +def _setup_logging( + args: argparse.Namespace, +) -> Tuple[Optional[str], Optional[logging.FileHandler]]: + log_handler = None + log_path = None + if args.make_test_case or args.log_dir_path is not None: + log_path = "ffsubsync.log" + if args.log_dir_path is not None and os.path.isdir(args.log_dir_path): + log_path = os.path.join(args.log_dir_path, log_path) + log_handler = logging.FileHandler(log_path) + logger.addHandler(log_handler) + logger.info("this log will be written to %s", os.path.abspath(log_path)) + return log_path, log_handler + + +def _npy_savename(args: argparse.Namespace) -> str: + return os.path.splitext(args.reference)[0] + ".npz" + + +def _run_impl(args: argparse.Namespace, result: Dict[str, Any]) -> bool: + if args.extract_subs_from_stream is not None: + result["retval"] = extract_subtitles_from_reference(args) + return True + if args.srtin is not None and ( + args.reference is None + or (len(args.srtin) == 1 and args.srtin[0] == args.reference) + ): + return try_sync(args, None, result) + reference_pipe = make_reference_pipe(args) + logger.info("extracting speech segments from reference '%s'...", args.reference) + reference_pipe.fit(args.reference) + logger.info("...done") + if args.make_test_case or args.serialize_speech: + logger.info("serializing speech...") + np.savez_compressed( + _npy_savename(args), speech=reference_pipe.transform(args.reference) + ) + logger.info("...done") + if not args.srtin: + logger.info( + "unsynchronized subtitle file not specified; skipping synchronization" + ) + return False + return try_sync(args, reference_pipe, result) + + +def validate_and_transform_args( + parser_or_args: Union[argparse.ArgumentParser, argparse.Namespace] +) -> Optional[argparse.Namespace]: + if isinstance(parser_or_args, argparse.Namespace): + parser = None + args = parser_or_args + else: + parser = parser_or_args + args = parser.parse_args() try: validate_args(args) except ValueError as e: logger.error(e) - result['retval'] = 1 - return result + if parser is not None: + parser.print_usage() + return None if args.gui_mode and args.srtout is None: - args.srtout = '{}.synced.srt'.format(os.path.splitext(args.srtin[0])[0]) + args.srtout = "{}.synced.srt".format(os.path.splitext(args.srtin[0])[0]) try: validate_file_permissions(args) except ValueError as e: logger.error(e) - result['retval'] = 1 - return result + return None ref_format = _ref_format(args.reference) if args.merge_with_reference and ref_format not in SUBTITLE_EXTENSIONS: - logger.error('merging synced output with reference only valid ' - 'when reference composed of subtitles') - result['retval'] = 1 + logger.error( + "merging synced output with reference only valid " + "when reference composed of subtitles" + ) + return None + return args + + +def run( + parser_or_args: Union[argparse.ArgumentParser, argparse.Namespace] +) -> Dict[str, Any]: + sync_was_successful = False + result = { + "retval": 0, + "offset_seconds": None, + "framerate_scale_factor": None, + } + args = validate_and_transform_args(parser_or_args) + if args is None: + result["retval"] = 1 return result - log_handler = None - log_path = None - if args.make_test_case: - log_path = 'ffsubsync.log' - if args.log_dir_path and os.path.isdir(args.log_dir_path): - log_path = os.path.join(args.log_dir_path, log_path) - log_handler = logging.FileHandler(log_path) - logger.addHandler(log_handler) - if args.extract_subs_from_stream is not None: - result['retval'] = extract_subtitles_from_reference(args) - return result - reference_pipe = make_reference_pipe(args) - logger.info("extracting speech segments from reference '%s'...", args.reference) - reference_pipe.fit(args.reference) - logger.info('...done') - npy_savename = None - if args.make_test_case or args.serialize_speech: - logger.info('serializing speech...') - npy_savename = os.path.splitext(args.reference)[0] + '.npz' - np.savez_compressed(npy_savename, speech=reference_pipe.transform(args.reference)) - logger.info('...done') - if args.srtin[0] is None: - logger.info('unsynchronized subtitle file not specified; skipping synchronization') + log_path, log_handler = _setup_logging(args) + try: + sync_was_successful = _run_impl(args, result) + result["sync_was_successful"] = sync_was_successful + finally: + if log_handler is None or log_path is None: return result - sync_was_successful = try_sync(args, reference_pipe, result) - if log_handler is not None and log_path is not None: - assert args.make_test_case - log_handler.close() - logger.removeHandler(log_handler) try: - result['retval'] += make_test_case(args, npy_savename, sync_was_successful) + log_handler.close() + logger.removeHandler(log_handler) + if args.make_test_case: + result["retval"] += make_test_case( + args, _npy_savename(args), sync_was_successful + ) finally: - os.remove(log_path) - return result + if args.log_dir_path is None or not os.path.isdir(args.log_dir_path): + os.remove(log_path) + return result -def add_main_args_for_cli(parser): +def add_main_args_for_cli(parser: argparse.ArgumentParser) -> None: parser.add_argument( - 'reference', - help='Reference (video, subtitles, or a numpy array with VAD speech) to which to synchronize input subtitles.' + "reference", + nargs="?", + help="Reference (video, subtitles, or a numpy array with VAD speech) to which to synchronize input subtitles.", ) - parser.add_argument('-i', '--srtin', nargs='*', help='Input subtitles file (default=stdin).') - parser.add_argument('-o', '--srtout', help='Output subtitles file (default=stdout).') - parser.add_argument('--merge-with-reference', '--merge', action='store_true', - help='Merge reference subtitles with synced output subtitles.') - parser.add_argument('--make-test-case', '--create-test-case', action='store_true', - help='If specified, serialize reference speech to a numpy array, ' - 'and create an archive with input/output subtitles ' - 'and serialized speech.') parser.add_argument( - '--reference-stream', '--refstream', '--reference-track', '--reftrack', + "-i", "--srtin", nargs="*", help="Input subtitles file (default=stdin)." + ) + parser.add_argument( + "-o", "--srtout", help="Output subtitles file (default=stdout)." + ) + parser.add_argument( + "--merge-with-reference", + "--merge", + action="store_true", + help="Merge reference subtitles with synced output subtitles.", + ) + parser.add_argument( + "--make-test-case", + "--create-test-case", + action="store_true", + help="If specified, serialize reference speech to a numpy array, " + "and create an archive with input/output subtitles " + "and serialized speech.", + ) + parser.add_argument( + "--reference-stream", + "--refstream", + "--reference-track", + "--reftrack", default=None, - help='Which stream/track in the video file to use as reference, ' - 'formatted according to ffmpeg conventions. For example, 0:s:0 ' - 'uses the first subtitle track; 0:a:3 would use the third audio track. ' - 'You can also drop the leading `0:`; i.e. use s:0 or a:3, respectively. ' - 'Example: `ffs ref.mkv -i in.srt -o out.srt --reference-stream s:2`' + help="Which stream/track in the video file to use as reference, " + "formatted according to ffmpeg conventions. For example, 0:s:0 " + "uses the first subtitle track; 0:a:3 would use the third audio track. " + "You can also drop the leading `0:`; i.e. use s:0 or a:3, respectively. " + "Example: `ffs ref.mkv -i in.srt -o out.srt --reference-stream s:2`", ) -def add_cli_only_args(parser): - parser.add_argument('-v', '--version', action='version', - version='{package} {version}'.format(package=__package__, version=get_version())) - parser.add_argument('--overwrite-input', action='store_true', - help='If specified, will overwrite the input srt instead of writing the output to a new file.') - parser.add_argument('--encoding', default=DEFAULT_ENCODING, - help='What encoding to use for reading input subtitles ' - '(default=%s).' % DEFAULT_ENCODING) - parser.add_argument('--max-subtitle-seconds', type=float, default=DEFAULT_MAX_SUBTITLE_SECONDS, - help='Maximum duration for a subtitle to appear on-screen ' - '(default=%.3f seconds).' % DEFAULT_MAX_SUBTITLE_SECONDS) - parser.add_argument('--start-seconds', type=int, default=DEFAULT_START_SECONDS, - help='Start time for processing ' - '(default=%d seconds).' % DEFAULT_START_SECONDS) - parser.add_argument('--max-offset-seconds', type=float, default=DEFAULT_MAX_OFFSET_SECONDS, - help='The max allowed offset seconds for any subtitle segment ' - '(default=%d seconds).' % DEFAULT_MAX_OFFSET_SECONDS) - parser.add_argument('--apply-offset-seconds', type=float, default=DEFAULT_APPLY_OFFSET_SECONDS, - help='Apply a predefined offset in seconds to all subtitle segments ' - '(default=%d seconds).' % DEFAULT_APPLY_OFFSET_SECONDS) - parser.add_argument('--frame-rate', type=int, default=DEFAULT_FRAME_RATE, - help='Frame rate for audio extraction (default=%d).' % DEFAULT_FRAME_RATE) - parser.add_argument('--skip-infer-framerate-ratio', action='store_true', - help='If set, do not try to infer framerate ratio based on duration ratio.') - parser.add_argument('--non-speech-label', type=float, default=DEFAULT_NON_SPEECH_LABEL, - help='Label to use for frames detected as non-speech (default=%f)' % DEFAULT_NON_SPEECH_LABEL) - parser.add_argument('--output-encoding', default='utf-8', - help='What encoding to use for writing output subtitles ' - '(default=utf-8). Can indicate "same" to use same ' - 'encoding as that of the input.') - parser.add_argument('--reference-encoding', - help='What encoding to use for reading / writing reference subtitles ' - '(if applicable, default=infer).') - parser.add_argument('--vad', choices=['subs_then_webrtc', 'webrtc', 'subs_then_auditok', 'auditok'], - default=None, - help='Which voice activity detector to use for speech extraction ' - '(if using video / audio as a reference, default={}).'.format(DEFAULT_VAD)) - parser.add_argument('--no-fix-framerate', action='store_true', - help='If specified, subsync will not attempt to correct a framerate ' - 'mismatch between reference and subtitles.') - parser.add_argument('--serialize-speech', action='store_true', - help='If specified, serialize reference speech to a numpy array.') - parser.add_argument('--extract-subs-from-stream', default=None, - help='If specified, do not attempt sync; instead, just extract subtitles' - ' from the specified stream using the reference.') +def add_cli_only_args(parser: argparse.ArgumentParser) -> None: parser.add_argument( - '--ffmpeg-path', '--ffmpegpath', default=None, - help='Where to look for ffmpeg and ffprobe. Uses the system PATH by default.' + "-v", + "--version", + action="version", + version="{package} {version}".format( + package=__package__, version=get_version() + ), ) - parser.add_argument('--log-dir-path', default=None, help='Where to save ffsubsync.log file (must be an existing ' - 'directory).') - parser.add_argument('--vlc-mode', action='store_true', help=argparse.SUPPRESS) - parser.add_argument('--gui-mode', action='store_true', help=argparse.SUPPRESS) - parser.add_argument('--skip-sync', action='store_true', help=argparse.SUPPRESS) - parser.add_argument('--gss', action='store_true', help=argparse.SUPPRESS) + parser.add_argument( + "--overwrite-input", + action="store_true", + help="If specified, will overwrite the input srt instead of writing the output to a new file.", + ) + parser.add_argument( + "--encoding", + default=DEFAULT_ENCODING, + help="What encoding to use for reading input subtitles " + "(default=%s)." % DEFAULT_ENCODING, + ) + parser.add_argument( + "--max-subtitle-seconds", + type=float, + default=DEFAULT_MAX_SUBTITLE_SECONDS, + help="Maximum duration for a subtitle to appear on-screen " + "(default=%.3f seconds)." % DEFAULT_MAX_SUBTITLE_SECONDS, + ) + parser.add_argument( + "--start-seconds", + type=int, + default=DEFAULT_START_SECONDS, + help="Start time for processing " + "(default=%d seconds)." % DEFAULT_START_SECONDS, + ) + parser.add_argument( + "--max-offset-seconds", + type=float, + default=DEFAULT_MAX_OFFSET_SECONDS, + help="The max allowed offset seconds for any subtitle segment " + "(default=%d seconds)." % DEFAULT_MAX_OFFSET_SECONDS, + ) + parser.add_argument( + "--apply-offset-seconds", + type=float, + default=DEFAULT_APPLY_OFFSET_SECONDS, + help="Apply a predefined offset in seconds to all subtitle segments " + "(default=%d seconds)." % DEFAULT_APPLY_OFFSET_SECONDS, + ) + parser.add_argument( + "--frame-rate", + type=int, + default=DEFAULT_FRAME_RATE, + help="Frame rate for audio extraction (default=%d)." % DEFAULT_FRAME_RATE, + ) + parser.add_argument( + "--skip-infer-framerate-ratio", + action="store_true", + help="If set, do not try to infer framerate ratio based on duration ratio.", + ) + parser.add_argument( + "--non-speech-label", + type=float, + default=DEFAULT_NON_SPEECH_LABEL, + help="Label to use for frames detected as non-speech (default=%f)" + % DEFAULT_NON_SPEECH_LABEL, + ) + parser.add_argument( + "--output-encoding", + default="utf-8", + help="What encoding to use for writing output subtitles " + '(default=utf-8). Can indicate "same" to use same ' + "encoding as that of the input.", + ) + parser.add_argument( + "--reference-encoding", + help="What encoding to use for reading / writing reference subtitles " + "(if applicable, default=infer).", + ) + parser.add_argument( + "--vad", + choices=["subs_then_webrtc", "webrtc", "subs_then_auditok", "auditok"], + default=None, + help="Which voice activity detector to use for speech extraction " + "(if using video / audio as a reference, default={}).".format(DEFAULT_VAD), + ) + parser.add_argument( + "--no-fix-framerate", + action="store_true", + help="If specified, subsync will not attempt to correct a framerate " + "mismatch between reference and subtitles.", + ) + parser.add_argument( + "--serialize-speech", + action="store_true", + help="If specified, serialize reference speech to a numpy array.", + ) + parser.add_argument( + "--extract-subs-from-stream", + "--extract-subtitles-from-stream", + default=None, + help="If specified, do not attempt sync; instead, just extract subtitles" + " from the specified stream using the reference.", + ) + parser.add_argument( + "--suppress-output-if-offset-less-than", + type=float, + default=None, + help="If specified, do not produce output if offset below provided threshold.", + ) + parser.add_argument( + "--ffmpeg-path", + "--ffmpegpath", + default=None, + help="Where to look for ffmpeg and ffprobe. Uses the system PATH by default.", + ) + parser.add_argument( + "--log-dir-path", + default=None, + help="If provided, will save log file ffsubsync.log to this path (must be an existing directory).", + ) + parser.add_argument( + "--gss", + action="store_true", + help="If specified, use golden-section search to try to find" + "the optimal framerate ratio between video and subtitles.", + ) + parser.add_argument("--vlc-mode", action="store_true", help=argparse.SUPPRESS) + parser.add_argument("--gui-mode", action="store_true", help=argparse.SUPPRESS) + parser.add_argument("--skip-sync", action="store_true", help=argparse.SUPPRESS) -def make_parser(): - parser = argparse.ArgumentParser(description='Synchronize subtitles with video.') +def make_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser(description="Synchronize subtitles with video.") add_main_args_for_cli(parser) add_cli_only_args(parser) return parser -def main(): +def main() -> int: parser = make_parser() - args = parser.parse_args() - return run(args)['retval'] + return run(parser)["retval"] if __name__ == "__main__": diff --git a/libs/ffsubsync/ffsubsync_gui.py b/libs/ffsubsync/ffsubsync_gui.py index 9bf836512..1bdb45031 100755 --- a/libs/ffsubsync/ffsubsync_gui.py +++ b/libs/ffsubsync/ffsubsync_gui.py @@ -6,7 +6,7 @@ import sys from gooey import Gooey, GooeyParser -from .constants import ( +from ffsubsync.constants import ( RELEASE_URL, WEBSITE, DEV_WEBSITE, @@ -17,11 +17,12 @@ from .constants import ( COPYRIGHT_YEAR, SUBSYNC_RESOURCES_ENV_MAGIC, ) + # set the env magic so that we look for resources in the right place if SUBSYNC_RESOURCES_ENV_MAGIC not in os.environ: - os.environ[SUBSYNC_RESOURCES_ENV_MAGIC] = getattr(sys, '_MEIPASS', '') -from .ffsubsync import run, add_cli_only_args -from .version import get_version, update_available + os.environ[SUBSYNC_RESOURCES_ENV_MAGIC] = getattr(sys, "_MEIPASS", "") +from ffsubsync.ffsubsync import run, add_cli_only_args +from ffsubsync.version import get_version, update_available logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) @@ -29,65 +30,80 @@ logger = logging.getLogger(__name__) _menu = [ { - 'name': 'File', - 'items': [ + "name": "File", + "items": [ { - 'type': 'AboutDialog', - 'menuTitle': 'About', - 'name': PROJECT_NAME, - 'description': LONG_DESCRIPTION, - 'version': get_version(), - 'copyright': COPYRIGHT_YEAR, - 'website': WEBSITE, - 'developer': DEV_WEBSITE, - 'license': PROJECT_LICENSE, + "type": "AboutDialog", + "menuTitle": "About", + "name": PROJECT_NAME, + "description": LONG_DESCRIPTION, + "version": get_version(), + "copyright": COPYRIGHT_YEAR, + "website": WEBSITE, + "developer": DEV_WEBSITE, + "license": PROJECT_LICENSE, }, { - 'type': 'Link', - 'menuTitle': 'Download latest release', - 'url': RELEASE_URL, - } - ] + "type": "Link", + "menuTitle": "Download latest release", + "url": RELEASE_URL, + }, + ], } ] @Gooey( program_name=PROJECT_NAME, - image_dir=os.path.join(os.environ[SUBSYNC_RESOURCES_ENV_MAGIC], 'img'), + image_dir=os.path.join(os.environ[SUBSYNC_RESOURCES_ENV_MAGIC], "img"), menu=_menu, tabbed_groups=True, progress_regex=r"(\d+)%", - hide_progress_msg=True + hide_progress_msg=True, ) def make_parser(): description = DESCRIPTION if update_available(): description += '\nUpdate available! Please go to "File" -> "Download latest release" to update FFsubsync.' parser = GooeyParser(description=description) - main_group = parser.add_argument_group('Basic') + main_group = parser.add_argument_group("Basic") main_group.add_argument( - 'reference', - help='Reference (video or subtitles file) to which to synchronize input subtitles.', - widget='FileChooser' + "reference", + help="Reference (video or subtitles file) to which to synchronize input subtitles.", + widget="FileChooser", ) - main_group.add_argument('srtin', help='Input subtitles file', widget='FileChooser') - main_group.add_argument('-o', '--srtout', - help='Output subtitles file (default=${srtin}.synced.srt).', - widget='FileSaver') - advanced_group = parser.add_argument_group('Advanced') + main_group.add_argument("srtin", help="Input subtitles file", widget="FileChooser") + main_group.add_argument( + "-o", + "--srtout", + help="Output subtitles file (default=${srtin}.synced.srt).", + widget="FileSaver", + ) + advanced_group = parser.add_argument_group("Advanced") # TODO: these are shared between gui and cli; don't duplicate this code - advanced_group.add_argument('--merge-with-reference', '--merge', action='store_true', - help='Merge reference subtitles with synced output subtitles.') - advanced_group.add_argument('--make-test-case', '--create-test-case', action='store_true', - help='If specified, create a test archive a few KiB in size ' - 'to send to the developer as a debugging aid.') advanced_group.add_argument( - '--reference-stream', '--refstream', '--reference-track', '--reftrack', default=None, - help='Which stream/track in the video file to use as reference, ' - 'formatted according to ffmpeg conventions. For example, s:0 ' - 'uses the first subtitle track; a:3 would use the fourth audio track.' + "--merge-with-reference", + "--merge", + action="store_true", + help="Merge reference subtitles with synced output subtitles.", + ) + advanced_group.add_argument( + "--make-test-case", + "--create-test-case", + action="store_true", + help="If specified, create a test archive a few KiB in size " + "to send to the developer as a debugging aid.", + ) + advanced_group.add_argument( + "--reference-stream", + "--refstream", + "--reference-track", + "--reftrack", + default=None, + help="Which stream/track in the video file to use as reference, " + "formatted according to ffmpeg conventions. For example, s:0 " + "uses the first subtitle track; a:3 would use the fourth audio track.", ) return parser diff --git a/libs/ffsubsync/file_utils.py b/libs/ffsubsync/file_utils.py index ee155afa2..22fc43d70 100644 --- a/libs/ffsubsync/file_utils.py +++ b/libs/ffsubsync/file_utils.py @@ -1,17 +1,18 @@ -# -*- coding: utf-8 -*- +# -*- coding: utf-8 -*- import six import sys -class open_file(object): +class open_file: """ Context manager that opens a filename and closes it on exit, but does nothing for file-like objects. """ - def __init__(self, filename, *args, **kwargs): - self.closing = kwargs.pop('closing', False) + + def __init__(self, filename, *args, **kwargs) -> None: + self.closing = kwargs.pop("closing", False) if filename is None: - stream = sys.stdout if 'w' in args else sys.stdin + stream = sys.stdout if "w" in args else sys.stdin if six.PY3: self.fh = open(stream.fileno(), *args, **kwargs) else: diff --git a/libs/ffsubsync/generic_subtitles.py b/libs/ffsubsync/generic_subtitles.py index 8bed07d87..3e4b1ed0f 100644 --- a/libs/ffsubsync/generic_subtitles.py +++ b/libs/ffsubsync/generic_subtitles.py @@ -3,32 +3,27 @@ import copy from datetime import timedelta import logging import os +from typing import cast, Any, Dict, Iterator, List, Optional import pysubs2 import srt import six import sys + logging.basicConfig(level=logging.INFO) -logger = logging.getLogger(__name__) +logger: logging.Logger = logging.getLogger(__name__) -class SubsMixin(object): - def __init__(self, subs=None): - self.subs_ = subs - - def set_encoding(self, encoding): - self.subs_.set_encoding(encoding) - return self - - -class GenericSubtitle(object): +class GenericSubtitle: def __init__(self, start, end, inner): self.start = start self.end = end self.inner = inner - def __eq__(self, other): + def __eq__(self, other: object) -> bool: + if not isinstance(other, GenericSubtitle): + return False eq = True eq = eq and self.start == other.start eq = eq and self.end == other.end @@ -36,13 +31,15 @@ class GenericSubtitle(object): return eq @property - def content(self): + def content(self) -> str: if isinstance(self.inner, srt.Subtitle): ret = self.inner.content elif isinstance(self.inner, pysubs2.SSAEvent): ret = self.inner.text else: - raise NotImplementedError('unsupported subtitle type: %s' % type(self.inner)) + raise NotImplementedError( + "unsupported subtitle type: %s" % type(self.inner) + ) return ret def resolve_inner_timestamps(self): @@ -54,110 +51,126 @@ class GenericSubtitle(object): ret.start = pysubs2.make_time(s=self.start.total_seconds()) ret.end = pysubs2.make_time(s=self.end.total_seconds()) else: - raise NotImplementedError('unsupported subtitle type: %s' % type(self.inner)) + raise NotImplementedError( + "unsupported subtitle type: %s" % type(self.inner) + ) return ret def merge_with(self, other): assert isinstance(self.inner, type(other.inner)) inner_merged = copy.deepcopy(self.inner) if isinstance(self.inner, srt.Subtitle): - inner_merged.content = u'{}\n{}'.format(inner_merged.content, other.inner.content) - return self.__class__( - self.start, - self.end, - inner_merged + inner_merged.content = "{}\n{}".format( + inner_merged.content, other.inner.content ) + return self.__class__(self.start, self.end, inner_merged) else: - raise NotImplementedError('unsupported subtitle type: %s' % type(self.inner)) + raise NotImplementedError( + "unsupported subtitle type: %s" % type(self.inner) + ) @classmethod - def wrap_inner_subtitle(cls, sub): + def wrap_inner_subtitle(cls, sub) -> "GenericSubtitle": if isinstance(sub, srt.Subtitle): return cls(sub.start, sub.end, sub) elif isinstance(sub, pysubs2.SSAEvent): return cls( - timedelta(milliseconds=sub.start), - timedelta(milliseconds=sub.end), - sub + timedelta(milliseconds=sub.start), timedelta(milliseconds=sub.end), sub ) else: - raise NotImplementedError('unsupported subtitle type: %s' % type(sub)) + raise NotImplementedError("unsupported subtitle type: %s" % type(sub)) -class GenericSubtitlesFile(object): - def __init__(self, subs, *args, **kwargs): - sub_format = kwargs.pop('sub_format', None) +class GenericSubtitlesFile: + def __init__(self, subs: List[GenericSubtitle], *_, **kwargs: Any): + sub_format: str = cast(str, kwargs.pop("sub_format", None)) if sub_format is None: - raise ValueError('format must be specified') - encoding = kwargs.pop('encoding', None) + raise ValueError("format must be specified") + encoding: str = cast(str, kwargs.pop("encoding", None)) if encoding is None: - raise ValueError('encoding must be specified') - self.subs_ = subs - self._sub_format = sub_format - self._encoding = encoding - self._styles = kwargs.pop('styles', None) + raise ValueError("encoding must be specified") + self.subs_: List[GenericSubtitle] = subs + self._sub_format: str = sub_format + self._encoding: str = encoding + self._styles: Optional[Dict[str, pysubs2.SSAStyle]] = kwargs.pop("styles", None) + self._fonts_opaque: Optional[Dict[str, Any]] = kwargs.pop("fonts_opaque", None) + self._info: Optional[Dict[str, str]] = kwargs.pop("info", None) - def set_encoding(self, encoding): - if encoding != 'same': + def set_encoding(self, encoding: str) -> "GenericSubtitlesFile": + if encoding != "same": self._encoding = encoding return self - def __len__(self): + def __len__(self) -> int: return len(self.subs_) - def __getitem__(self, item): + def __getitem__(self, item: int) -> GenericSubtitle: return self.subs_[item] - @property - def sub_format(self): - return self._sub_format + def __iter__(self) -> Iterator[GenericSubtitle]: + return iter(self.subs_) - @property - def encoding(self): - return self._encoding - - @property - def styles(self): - return self._styles + def clone_props_for_subs( + self, new_subs: List[GenericSubtitle] + ) -> "GenericSubtitlesFile": + return GenericSubtitlesFile( + new_subs, + sub_format=self._sub_format, + encoding=self._encoding, + styles=self._styles, + fonts_opaque=self._fonts_opaque, + info=self._info, + ) def gen_raw_resolved_subs(self): for sub in self.subs_: yield sub.resolve_inner_timestamps() - def offset(self, td): + def offset(self, td: timedelta) -> "GenericSubtitlesFile": offset_subs = [] for sub in self.subs_: - offset_subs.append( - GenericSubtitle(sub.start + td, sub.end + td, sub.inner) - ) - return GenericSubtitlesFile( - offset_subs, - sub_format=self.sub_format, - encoding=self.encoding, - styles=self.styles - ) + offset_subs.append(GenericSubtitle(sub.start + td, sub.end + td, sub.inner)) + return self.clone_props_for_subs(offset_subs) - def write_file(self, fname): + def write_file(self, fname: str) -> None: # TODO: converter to go between self.subs_format and out_format if fname is None: out_format = self._sub_format else: out_format = os.path.splitext(fname)[-1][1:] subs = list(self.gen_raw_resolved_subs()) - if out_format == 'srt': - to_write = srt.compose(subs) - elif out_format in ('ssa', 'ass'): + if self._sub_format in ("ssa", "ass"): ssaf = pysubs2.SSAFile() ssaf.events = subs - ssaf.styles = self.styles + if self._styles is not None: + ssaf.styles = self._styles + if self._info is not None: + ssaf.info = self._info + if self._fonts_opaque is not None: + ssaf.fonts_opaque = self._fonts_opaque to_write = ssaf.to_string(out_format) + elif self._sub_format == "srt" and out_format in ("ssa", "ass"): + to_write = pysubs2.SSAFile.from_string(srt.compose(subs)).to_string( + out_format + ) + elif out_format == "srt": + to_write = srt.compose(subs) else: - raise NotImplementedError('unsupported output format: %s' % out_format) + raise NotImplementedError("unsupported output format: %s" % out_format) - to_write = to_write.encode(self.encoding) + to_write = to_write.encode(self._encoding) if six.PY3: - with open(fname or sys.stdout.fileno(), 'wb') as f: + with open(fname or sys.stdout.fileno(), "wb") as f: f.write(to_write) else: - with (fname and open(fname, 'wb')) or sys.stdout as f: + with (fname and open(fname, "wb")) or sys.stdout as f: f.write(to_write) + + +class SubsMixin: + def __init__(self, subs: Optional[GenericSubtitlesFile] = None) -> None: + self.subs_: Optional[GenericSubtitlesFile] = subs + + def set_encoding(self, encoding: str) -> "SubsMixin": + self.subs_.set_encoding(encoding) + return self diff --git a/libs/ffsubsync/golden_section_search.py b/libs/ffsubsync/golden_section_search.py index 3507ccd1d..0d239527d 100644 --- a/libs/ffsubsync/golden_section_search.py +++ b/libs/ffsubsync/golden_section_search.py @@ -11,6 +11,7 @@ logger = logging.getLogger(__name__) invphi = (math.sqrt(5) - 1) / 2 # 1 / phi invphi2 = (3 - math.sqrt(5)) / 2 # 1 / phi^2 + def gss(f, a, b, tol=1e-4): """Golden-section search. @@ -35,7 +36,10 @@ def gss(f, a, b, tol=1e-4): # Required steps to achieve tolerance n = int(math.ceil(math.log(tol / h) / math.log(invphi))) - logger.info('About to perform %d iterations of golden section search to find the best framerate', n) + logger.info( + "About to perform %d iterations of golden section search to find the best framerate", + n, + ) def f_wrapped(x, is_last_iter): try: @@ -45,26 +49,26 @@ def gss(f, a, b, tol=1e-4): c = a + invphi2 * h d = a + invphi * h - yc = f_wrapped(c, n==1) - yd = f_wrapped(d, n==1) + yc = f_wrapped(c, n == 1) + yd = f_wrapped(d, n == 1) - for k in range(n-1): + for k in range(n - 1): if yc < yd: b = d d = c yd = yc h = invphi * h c = a + invphi2 * h - yc = f_wrapped(c, k==n-2) + yc = f_wrapped(c, k == n - 2) else: a = c c = d yc = yd h = invphi * h d = a + invphi * h - yd = f(d, k==n-2) + yd = f(d, k == n - 2) if yc < yd: return a, d else: - return c, b \ No newline at end of file + return c, b diff --git a/libs/ffsubsync/sklearn_shim.py b/libs/ffsubsync/sklearn_shim.py index f0429382a..ac79e4f3c 100644 --- a/libs/ffsubsync/sklearn_shim.py +++ b/libs/ffsubsync/sklearn_shim.py @@ -8,14 +8,21 @@ is given as a comment above each class. """ from collections import defaultdict from itertools import islice +from typing import Any, Callable, Optional +from typing_extensions import Protocol + + +class TransformerProtocol(Protocol): + fit: Callable[..., "TransformerProtocol"] + transform: Callable[["TransformerProtocol", Any], Any] # Author: Gael Varoquaux # License: BSD 3 clause -class TransformerMixin(object): +class TransformerMixin(TransformerProtocol): """Mixin class for all transformers.""" - def fit_transform(self, X, y=None, **fit_params): + def fit_transform(self, X: Any, y: Optional[Any] = None, **fit_params: Any) -> Any: """ Fit to data, then transform it. Fits transformer to X and y with optional parameters fit_params @@ -49,7 +56,7 @@ class TransformerMixin(object): # Alexandre Gramfort # Lars Buitinck # License: BSD -class Pipeline(object): +class Pipeline: def __init__(self, steps, verbose=False): self.steps = steps self.verbose = verbose @@ -63,22 +70,29 @@ class Pipeline(object): estimator = estimators[-1] for t in transformers: - if t is None or t == 'passthrough': + if t is None or t == "passthrough": continue - if (not (hasattr(t, "fit") or hasattr(t, "fit_transform")) or not - hasattr(t, "transform")): - raise TypeError("All intermediate steps should be " - "transformers and implement fit and transform " - "or be the string 'passthrough' " - "'%s' (type %s) doesn't" % (t, type(t))) + if not (hasattr(t, "fit") or hasattr(t, "fit_transform")) or not hasattr( + t, "transform" + ): + raise TypeError( + "All intermediate steps should be " + "transformers and implement fit and transform " + "or be the string 'passthrough' " + "'%s' (type %s) doesn't" % (t, type(t)) + ) # We allow last estimator to be None as an identity transformation - if (estimator is not None and estimator != 'passthrough' - and not hasattr(estimator, "fit")): + if ( + estimator is not None + and estimator != "passthrough" + and not hasattr(estimator, "fit") + ): raise TypeError( "Last step of Pipeline should implement fit " "or be the string 'passthrough'. " - "'%s' (type %s) doesn't" % (estimator, type(estimator))) + "'%s' (type %s) doesn't" % (estimator, type(estimator)) + ) def _iter(self, with_final=True, filter_passthrough=True): """ @@ -94,10 +108,10 @@ class Pipeline(object): for idx, (name, trans) in enumerate(islice(self.steps, 0, stop)): if not filter_passthrough: yield idx, name, trans - elif trans is not None and trans != 'passthrough': + elif trans is not None and trans != "passthrough": yield idx, name, trans - def __len__(self): + def __len__(self) -> int: """ Returns the length of the Pipeline """ @@ -114,7 +128,7 @@ class Pipeline(object): """ if isinstance(ind, slice): if ind.step not in (1, None): - raise ValueError('Pipeline slicing only supports a step of 1') + raise ValueError("Pipeline slicing only supports a step of 1") return self.__class__(self.steps[ind]) try: name, est = self.steps[ind] @@ -134,16 +148,14 @@ class Pipeline(object): @property def _final_estimator(self): estimator = self.steps[-1][1] - return 'passthrough' if estimator is None else estimator + return "passthrough" if estimator is None else estimator def _log_message(self, step_idx): if not self.verbose: return None name, step = self.steps[step_idx] - return '(step %d of %d) Processing %s' % (step_idx + 1, - len(self.steps), - name) + return "(step %d of %d) Processing %s" % (step_idx + 1, len(self.steps), name) # Estimator interface @@ -152,34 +164,33 @@ class Pipeline(object): self.steps = list(self.steps) self._validate_steps() - fit_params_steps = {name: {} for name, step in self.steps - if step is not None} + fit_params_steps = {name: {} for name, step in self.steps if step is not None} for pname, pval in fit_params.items(): - if '__' not in pname: + if "__" not in pname: raise ValueError( "Pipeline.fit does not accept the {} parameter. " "You can pass parameters to specific steps of your " "pipeline using the stepname__parameter format, e.g. " "`Pipeline.fit(X, y, logisticregression__sample_weight" - "=sample_weight)`.".format(pname)) - step, param = pname.split('__', 1) + "=sample_weight)`.".format(pname) + ) + step, param = pname.split("__", 1) fit_params_steps[step][param] = pval - for (step_idx, - name, - transformer) in self._iter(with_final=False, - filter_passthrough=False): - if transformer is None or transformer == 'passthrough': + for (step_idx, name, transformer) in self._iter( + with_final=False, filter_passthrough=False + ): + if transformer is None or transformer == "passthrough": continue # Fit or load from cache the current transformer X, fitted_transformer = _fit_transform_one( - transformer, X, y, None, - **fit_params_steps[name]) + transformer, X, y, None, **fit_params_steps[name] + ) # Replace the transformer of the step with the fitted # transformer. This is necessary when loading the transformer # from the cache. self.steps[step_idx] = (name, fitted_transformer) - if self._final_estimator == 'passthrough': + if self._final_estimator == "passthrough": return X, {} return X, fit_params_steps[self.steps[-1][0]] @@ -210,7 +221,7 @@ class Pipeline(object): This estimator """ Xt, fit_params = self._fit(X, y, **fit_params) - if self._final_estimator != 'passthrough': + if self._final_estimator != "passthrough": self._final_estimator.fit(Xt, y, **fit_params) return self @@ -243,9 +254,9 @@ class Pipeline(object): """ last_step = self._final_estimator Xt, fit_params = self._fit(X, y, **fit_params) - if last_step == 'passthrough': + if last_step == "passthrough": return Xt - if hasattr(last_step, 'fit_transform'): + if hasattr(last_step, "fit_transform"): return last_step.fit_transform(Xt, y, **fit_params) else: return last_step.fit(Xt, y, **fit_params).transform(Xt) @@ -269,7 +280,7 @@ class Pipeline(object): """ # _final_estimator is None or has transform, otherwise attribute error # XXX: Handling the None case means we can't use if_delegate_has_method - if self._final_estimator != 'passthrough': + if self._final_estimator != "passthrough": self._final_estimator.transform return self._transform @@ -279,7 +290,6 @@ class Pipeline(object): Xt = transform.transform(Xt) return Xt - @property def classes_(self): return self.steps[-1][-1].classes_ @@ -287,7 +297,7 @@ class Pipeline(object): @property def _pairwise(self): # check if first estimator expects pairwise input - return getattr(self.steps[0][1], '_pairwise', False) + return getattr(self.steps[0][1], "_pairwise", False) @property def n_features_in_(self): @@ -299,8 +309,7 @@ def _name_estimators(estimators): """Generate names for estimators.""" names = [ - estimator - if isinstance(estimator, str) else type(estimator).__name__.lower() + estimator if isinstance(estimator, str) else type(estimator).__name__.lower() for estimator in estimators ] namecount = defaultdict(int) @@ -320,7 +329,7 @@ def _name_estimators(estimators): return list(zip(names, estimators)) -def make_pipeline(*steps, **kwargs): +def make_pipeline(*steps, **kwargs) -> Pipeline: """Construct a Pipeline from the given estimators. This is a shorthand for the Pipeline constructor; it does not require, and @@ -339,10 +348,11 @@ def make_pipeline(*steps, **kwargs): ------- p : Pipeline """ - verbose = kwargs.pop('verbose', False) + verbose = kwargs.pop("verbose", False) if kwargs: - raise TypeError('Unknown keyword arguments: "{}"' - .format(list(kwargs.keys())[0])) + raise TypeError( + 'Unknown keyword arguments: "{}"'.format(list(kwargs.keys())[0]) + ) return Pipeline(_name_estimators(steps), verbose=verbose) @@ -354,17 +364,13 @@ def _transform_one(transformer, X, y, weight, **fit_params): return res * weight -def _fit_transform_one(transformer, - X, - y, - weight, - **fit_params): +def _fit_transform_one(transformer, X, y, weight, **fit_params): """ Fits ``transformer`` to ``X`` and ``y``. The transformed result is returned with the fitted transformer. If ``weight`` is not ``None``, the result will be multiplied by ``weight``. """ - if hasattr(transformer, 'fit_transform'): + if hasattr(transformer, "fit_transform"): res = transformer.fit_transform(X, y, **fit_params) else: res = transformer.fit(X, y, **fit_params).transform(X) diff --git a/libs/ffsubsync/speech_transformers.py b/libs/ffsubsync/speech_transformers.py index 5ab7f3304..33b54db6a 100644 --- a/libs/ffsubsync/speech_transformers.py +++ b/libs/ffsubsync/speech_transformers.py @@ -5,148 +5,169 @@ import io import subprocess import sys from datetime import timedelta +from typing import cast, Callable, Dict, Optional, Union import ffmpeg import numpy as np -from .sklearn_shim import TransformerMixin -from .sklearn_shim import Pipeline import tqdm -from .constants import * -from .ffmpeg_utils import ffmpeg_bin_path, subprocess_args -from .subtitle_parser import make_subtitle_parser -from .subtitle_transformers import SubtitleScaler +from ffsubsync.constants import * +from ffsubsync.ffmpeg_utils import ffmpeg_bin_path, subprocess_args +from ffsubsync.generic_subtitles import GenericSubtitle +from ffsubsync.sklearn_shim import TransformerMixin +from ffsubsync.sklearn_shim import Pipeline +from ffsubsync.subtitle_parser import make_subtitle_parser +from ffsubsync.subtitle_transformers import SubtitleScaler + logging.basicConfig(level=logging.INFO) -logger = logging.getLogger(__name__) +logger: logging.Logger = logging.getLogger(__name__) def make_subtitle_speech_pipeline( - fmt='srt', - encoding=DEFAULT_ENCODING, - caching=False, - max_subtitle_seconds=DEFAULT_MAX_SUBTITLE_SECONDS, - start_seconds=DEFAULT_START_SECONDS, - scale_factor=DEFAULT_SCALE_FACTOR, - parser=None, - **kwargs -): + fmt: str = "srt", + encoding: str = DEFAULT_ENCODING, + caching: bool = False, + max_subtitle_seconds: int = DEFAULT_MAX_SUBTITLE_SECONDS, + start_seconds: int = DEFAULT_START_SECONDS, + scale_factor: float = DEFAULT_SCALE_FACTOR, + parser=None, + **kwargs, +) -> Union[Pipeline, Callable[[float], Pipeline]]: if parser is None: parser = make_subtitle_parser( fmt, encoding=encoding, caching=caching, max_subtitle_seconds=max_subtitle_seconds, - start_seconds=start_seconds + start_seconds=start_seconds, + **kwargs, ) assert parser.encoding == encoding assert parser.max_subtitle_seconds == max_subtitle_seconds assert parser.start_seconds == start_seconds def subpipe_maker(framerate_ratio): - return Pipeline([ - ('parse', parser), - ('scale', SubtitleScaler(framerate_ratio)), - ('speech_extract', SubtitleSpeechTransformer( - sample_rate=SAMPLE_RATE, - start_seconds=start_seconds, - framerate_ratio=framerate_ratio, - )) - ]) + return Pipeline( + [ + ("parse", parser), + ("scale", SubtitleScaler(framerate_ratio)), + ( + "speech_extract", + SubtitleSpeechTransformer( + sample_rate=SAMPLE_RATE, + start_seconds=start_seconds, + framerate_ratio=framerate_ratio, + ), + ), + ] + ) + if scale_factor is None: return subpipe_maker else: return subpipe_maker(scale_factor) -def _make_auditok_detector(sample_rate, frame_rate, non_speech_label): +def _make_auditok_detector( + sample_rate: int, frame_rate: int, non_speech_label: float +) -> Callable[[bytes], np.ndarray]: try: - from auditok import \ - BufferAudioSource, ADSFactory, AudioEnergyValidator, StreamTokenizer + from auditok import ( + BufferAudioSource, + ADSFactory, + AudioEnergyValidator, + StreamTokenizer, + ) except ImportError as e: - logger.error("""Error: auditok not installed! + logger.error( + """Error: auditok not installed! Consider installing it with `pip install auditok`. Note that auditok is GPLv3 licensed, which means that successfully importing it at runtime creates a derivative work that is GPLv3 licensed. For personal use this is fine, but note that any commercial use that relies on auditok must be open source as per the GPLv3!* *Not legal advice. Consult with a lawyer. - """) + """ + ) raise e bytes_per_frame = 2 frames_per_window = frame_rate // sample_rate - validator = AudioEnergyValidator( - sample_width=bytes_per_frame, energy_threshold=50 - ) + validator = AudioEnergyValidator(sample_width=bytes_per_frame, energy_threshold=50) tokenizer = StreamTokenizer( validator=validator, min_length=0.2 * sample_rate, max_length=int(5 * sample_rate), - max_continuous_silence=0.25 * sample_rate + max_continuous_silence=0.25 * sample_rate, ) - def _detect(asegment): + def _detect(asegment: bytes) -> np.ndarray: asource = BufferAudioSource( data_buffer=asegment, sampling_rate=frame_rate, sample_width=bytes_per_frame, - channels=1 + channels=1, ) - ads = ADSFactory.ads(audio_source=asource, block_dur=1./sample_rate) + ads = ADSFactory.ads(audio_source=asource, block_dur=1.0 / sample_rate) ads.open() tokens = tokenizer.tokenize(ads) length = ( - len(asegment)//bytes_per_frame + frames_per_window - 1 + len(asegment) // bytes_per_frame + frames_per_window - 1 ) // frames_per_window media_bstring = np.zeros(length + 1) for token in tokens: - media_bstring[token[1]] = 1. - media_bstring[token[2] + 1] = non_speech_label - 1. - return np.clip(np.cumsum(media_bstring)[:-1], 0., 1.) + media_bstring[token[1]] = 1.0 + media_bstring[token[2] + 1] = non_speech_label - 1.0 + return np.clip(np.cumsum(media_bstring)[:-1], 0.0, 1.0) + return _detect -def _make_webrtcvad_detector(sample_rate, frame_rate, non_speech_label): +def _make_webrtcvad_detector( + sample_rate: int, frame_rate: int, non_speech_label: float +) -> Callable[[bytes], np.ndarray]: import webrtcvad + vad = webrtcvad.Vad() vad.set_mode(3) # set non-speech pruning aggressiveness from 0 to 3 - window_duration = 1. / sample_rate # duration in seconds + window_duration = 1.0 / sample_rate # duration in seconds frames_per_window = int(window_duration * frame_rate + 0.5) bytes_per_frame = 2 - def _detect(asegment): + def _detect(asegment: bytes) -> np.ndarray: media_bstring = [] failures = 0 - for start in range(0, len(asegment) // bytes_per_frame, - frames_per_window): - stop = min(start + frames_per_window, - len(asegment) // bytes_per_frame) + for start in range(0, len(asegment) // bytes_per_frame, frames_per_window): + stop = min(start + frames_per_window, len(asegment) // bytes_per_frame) try: is_speech = vad.is_speech( - asegment[start * bytes_per_frame: stop * bytes_per_frame], - sample_rate=frame_rate) + asegment[start * bytes_per_frame : stop * bytes_per_frame], + sample_rate=frame_rate, + ) except: is_speech = False failures += 1 # webrtcvad has low recall on mode 3, so treat non-speech as "not sure" - media_bstring.append(1. if is_speech else non_speech_label) + media_bstring.append(1.0 if is_speech else non_speech_label) return np.array(media_bstring) return _detect -class ComputeSpeechFrameBoundariesMixin(object): - def __init__(self): - self.start_frame_ = None - self.end_frame_ = None +class ComputeSpeechFrameBoundariesMixin: + def __init__(self) -> None: + self.start_frame_: Optional[int] = None + self.end_frame_: Optional[int] = None @property - def num_frames(self): + def num_frames(self) -> Optional[int]: if self.start_frame_ is None or self.end_frame_ is None: return None return self.end_frame_ - self.start_frame_ - def fit_boundaries(self, speech_frames): + def fit_boundaries( + self, speech_frames: np.ndarray + ) -> "ComputeSpeechFrameBoundariesMixin": nz = np.nonzero(speech_frames > 0.5)[0] if len(nz) > 0: self.start_frame_ = np.min(nz) @@ -156,130 +177,187 @@ class ComputeSpeechFrameBoundariesMixin(object): class VideoSpeechTransformer(TransformerMixin): def __init__( - self, vad, sample_rate, frame_rate, non_speech_label, start_seconds=0, - ffmpeg_path=None, ref_stream=None, vlc_mode=False, gui_mode=False - ): + self, + vad: str, + sample_rate: int, + frame_rate: int, + non_speech_label: float, + start_seconds: int = 0, + ffmpeg_path: Optional[str] = None, + ref_stream: Optional[str] = None, + vlc_mode: bool = False, + gui_mode: bool = False, + ) -> None: super(VideoSpeechTransformer, self).__init__() - self.vad = vad - self.sample_rate = sample_rate - self.frame_rate = frame_rate - self._non_speech_label = non_speech_label - self.start_seconds = start_seconds - self.ffmpeg_path = ffmpeg_path - self.ref_stream = ref_stream - self.vlc_mode = vlc_mode - self.gui_mode = gui_mode - self.video_speech_results_ = None + self.vad: str = vad + self.sample_rate: int = sample_rate + self.frame_rate: int = frame_rate + self._non_speech_label: float = non_speech_label + self.start_seconds: int = start_seconds + self.ffmpeg_path: Optional[str] = ffmpeg_path + self.ref_stream: Optional[str] = ref_stream + self.vlc_mode: bool = vlc_mode + self.gui_mode: bool = gui_mode + self.video_speech_results_: Optional[np.ndarray] = None - def try_fit_using_embedded_subs(self, fname): + def try_fit_using_embedded_subs(self, fname: str) -> None: embedded_subs = [] embedded_subs_times = [] if self.ref_stream is None: # check first 5; should cover 99% of movies - streams_to_try = map('0:s:{}'.format, range(5)) + streams_to_try: List[str] = list(map("0:s:{}".format, range(5))) else: streams_to_try = [self.ref_stream] for stream in streams_to_try: - ffmpeg_args = [ffmpeg_bin_path('ffmpeg', self.gui_mode, ffmpeg_resources_path=self.ffmpeg_path)] - ffmpeg_args.extend([ - '-loglevel', 'fatal', - '-nostdin', - '-i', fname, - '-map', '{}'.format(stream), - '-f', 'srt', - '-' - ]) - process = subprocess.Popen(ffmpeg_args, **subprocess_args(include_stdout=True)) + ffmpeg_args = [ + ffmpeg_bin_path( + "ffmpeg", self.gui_mode, ffmpeg_resources_path=self.ffmpeg_path + ) + ] + ffmpeg_args.extend( + [ + "-loglevel", + "fatal", + "-nostdin", + "-i", + fname, + "-map", + "{}".format(stream), + "-f", + "srt", + "-", + ] + ) + process = subprocess.Popen( + ffmpeg_args, **subprocess_args(include_stdout=True) + ) output = io.BytesIO(process.communicate()[0]) if process.returncode != 0: break - pipe = make_subtitle_speech_pipeline(start_seconds=self.start_seconds).fit(output) + pipe = cast( + Pipeline, + make_subtitle_speech_pipeline(start_seconds=self.start_seconds), + ).fit(output) speech_step = pipe.steps[-1][1] embedded_subs.append(speech_step) embedded_subs_times.append(speech_step.max_time_) if len(embedded_subs) == 0: if self.ref_stream is None: - error_msg = 'Video file appears to lack subtitle stream' + error_msg = "Video file appears to lack subtitle stream" else: - error_msg = 'Stream {} not found'.format(self.ref_stream) + error_msg = "Stream {} not found".format(self.ref_stream) raise ValueError(error_msg) # use longest set of embedded subs subs_to_use = embedded_subs[int(np.argmax(embedded_subs_times))] self.video_speech_results_ = subs_to_use.subtitle_speech_results_ - def fit(self, fname, *_): - if 'subs' in self.vad and (self.ref_stream is None or self.ref_stream.startswith('0:s:')): + def fit(self, fname: str, *_) -> "VideoSpeechTransformer": + if "subs" in self.vad and ( + self.ref_stream is None or self.ref_stream.startswith("0:s:") + ): try: - logger.info('Checking video for subtitles stream...') + logger.info("Checking video for subtitles stream...") self.try_fit_using_embedded_subs(fname) - logger.info('...success!') + logger.info("...success!") return self except Exception as e: logger.info(e) try: - total_duration = float(ffmpeg.probe( - fname, cmd=ffmpeg_bin_path('ffprobe', self.gui_mode, ffmpeg_resources_path=self.ffmpeg_path) - )['format']['duration']) - self.start_seconds + total_duration = ( + float( + ffmpeg.probe( + fname, + cmd=ffmpeg_bin_path( + "ffprobe", + self.gui_mode, + ffmpeg_resources_path=self.ffmpeg_path, + ), + )["format"]["duration"] + ) + - self.start_seconds + ) except Exception as e: logger.warning(e) total_duration = None - if 'webrtc' in self.vad: - detector = _make_webrtcvad_detector(self.sample_rate, self.frame_rate, self._non_speech_label) - elif 'auditok' in self.vad: - detector = _make_auditok_detector(self.sample_rate, self.frame_rate, self._non_speech_label) + if "webrtc" in self.vad: + detector = _make_webrtcvad_detector( + self.sample_rate, self.frame_rate, self._non_speech_label + ) + elif "auditok" in self.vad: + detector = _make_auditok_detector( + self.sample_rate, self.frame_rate, self._non_speech_label + ) else: - raise ValueError('unknown vad: %s' % self.vad) + raise ValueError("unknown vad: %s" % self.vad) media_bstring = [] - ffmpeg_args = [ffmpeg_bin_path('ffmpeg', self.gui_mode, ffmpeg_resources_path=self.ffmpeg_path)] + ffmpeg_args = [ + ffmpeg_bin_path( + "ffmpeg", self.gui_mode, ffmpeg_resources_path=self.ffmpeg_path + ) + ] if self.start_seconds > 0: - ffmpeg_args.extend([ - '-ss', str(timedelta(seconds=self.start_seconds)), - ]) - ffmpeg_args.extend([ - '-loglevel', 'fatal', - '-nostdin', - '-i', fname - ]) - if self.ref_stream is not None and self.ref_stream.startswith('0:a:'): - ffmpeg_args.extend(['-map', self.ref_stream]) - ffmpeg_args.extend([ - '-f', 's16le', - '-ac', '1', - '-acodec', 'pcm_s16le', - '-ar', str(self.frame_rate), - '-' - ]) + ffmpeg_args.extend( + [ + "-ss", + str(timedelta(seconds=self.start_seconds)), + ] + ) + ffmpeg_args.extend(["-loglevel", "fatal", "-nostdin", "-i", fname]) + if self.ref_stream is not None and self.ref_stream.startswith("0:a:"): + ffmpeg_args.extend(["-map", self.ref_stream]) + ffmpeg_args.extend( + [ + "-f", + "s16le", + "-ac", + "1", + "-acodec", + "pcm_s16le", + "-ar", + str(self.frame_rate), + "-", + ] + ) process = subprocess.Popen(ffmpeg_args, **subprocess_args(include_stdout=True)) bytes_per_frame = 2 frames_per_window = bytes_per_frame * self.frame_rate // self.sample_rate windows_per_buffer = 10000 - simple_progress = 0. + simple_progress = 0.0 @contextmanager def redirect_stderr(enter_result=None): yield enter_result + tqdm_extra_args = {} should_print_redirected_stderr = self.gui_mode if self.gui_mode: try: - from contextlib import redirect_stderr - tqdm_extra_args['file'] = sys.stdout + from contextlib import redirect_stderr # type: ignore + + tqdm_extra_args["file"] = sys.stdout except ImportError: should_print_redirected_stderr = False pbar_output = io.StringIO() with redirect_stderr(pbar_output): - with tqdm.tqdm(total=total_duration, disable=self.vlc_mode, **tqdm_extra_args) as pbar: + with tqdm.tqdm( + total=total_duration, disable=self.vlc_mode, **tqdm_extra_args + ) as pbar: while True: - in_bytes = process.stdout.read(frames_per_window * windows_per_buffer) + in_bytes = process.stdout.read( + frames_per_window * windows_per_buffer + ) if not in_bytes: break newstuff = len(in_bytes) / float(bytes_per_frame) / self.frame_rate - if total_duration is not None and simple_progress + newstuff > total_duration: + if ( + total_duration is not None + and simple_progress + newstuff > total_duration + ): newstuff = total_duration - simple_progress simple_progress += newstuff pbar.update(newstuff) if self.vlc_mode and total_duration is not None: - print("%d" % int(simple_progress * 100. / total_duration)) + print("%d" % int(simple_progress * 100.0 / total_duration)) sys.stdout.flush() if should_print_redirected_stderr: assert self.gui_mode @@ -289,90 +367,103 @@ class VideoSpeechTransformer(TransformerMixin): media_bstring.append(detector(in_bytes)) if len(media_bstring) == 0: raise ValueError( - 'Unable to detect speech. Perhaps try specifying a different stream / track, or a different vad.' + "Unable to detect speech. Perhaps try specifying a different stream / track, or a different vad." ) self.video_speech_results_ = np.concatenate(media_bstring) return self - def transform(self, *_): + def transform(self, *_) -> np.ndarray: return self.video_speech_results_ -_PAIRED_NESTER = { - '(': ')', - '{': '}', - '[': ']', +_PAIRED_NESTER: Dict[str, str] = { + "(": ")", + "{": "}", + "[": "]", # FIXME: False positive sometimes when there are html tags, e.g. Hello? # '<': '>', } # TODO: need way better metadata detector -def _is_metadata(content, is_beginning_or_end): +def _is_metadata(content: str, is_beginning_or_end: bool) -> bool: content = content.strip() if len(content) == 0: return True - if content[0] in _PAIRED_NESTER.keys() and content[-1] == _PAIRED_NESTER[content[0]]: + if ( + content[0] in _PAIRED_NESTER.keys() + and content[-1] == _PAIRED_NESTER[content[0]] + ): return True if is_beginning_or_end: - if 'english' in content.lower(): + if "english" in content.lower(): return True - if ' - ' in content: + if " - " in content: return True return False class SubtitleSpeechTransformer(TransformerMixin, ComputeSpeechFrameBoundariesMixin): - def __init__(self, sample_rate, start_seconds=0, framerate_ratio=1.): + def __init__( + self, sample_rate: int, start_seconds: int = 0, framerate_ratio: float = 1.0 + ) -> None: super(SubtitleSpeechTransformer, self).__init__() - self.sample_rate = sample_rate - self.start_seconds = start_seconds - self.framerate_ratio = framerate_ratio - self.subtitle_speech_results_ = None - self.max_time_ = None + self.sample_rate: int = sample_rate + self.start_seconds: int = start_seconds + self.framerate_ratio: float = framerate_ratio + self.subtitle_speech_results_: Optional[np.ndarray] = None + self.max_time_: Optional[int] = None - def fit(self, subs, *_): + def fit(self, subs: List[GenericSubtitle], *_) -> "SubtitleSpeechTransformer": max_time = 0 for sub in subs: max_time = max(max_time, sub.end.total_seconds()) self.max_time_ = max_time - self.start_seconds samples = np.zeros(int(max_time * self.sample_rate) + 2, dtype=float) - start_frame = float('inf') + start_frame = float("inf") end_frame = 0 for i, sub in enumerate(subs): if _is_metadata(sub.content, i == 0 or i + 1 == len(subs)): continue - start = int(round((sub.start.total_seconds() - self.start_seconds) * self.sample_rate)) + start = int( + round( + (sub.start.total_seconds() - self.start_seconds) * self.sample_rate + ) + ) start_frame = min(start_frame, start) duration = sub.end.total_seconds() - sub.start.total_seconds() end = start + int(round(duration * self.sample_rate)) end_frame = max(end_frame, end) - samples[start:end] = min(1. / self.framerate_ratio, 1.) + samples[start:end] = min(1.0 / self.framerate_ratio, 1.0) self.subtitle_speech_results_ = samples self.fit_boundaries(self.subtitle_speech_results_) return self - def transform(self, *_): + def transform(self, *_) -> np.ndarray: + assert self.subtitle_speech_results_ is not None return self.subtitle_speech_results_ class DeserializeSpeechTransformer(TransformerMixin): - def __init__(self, non_speech_label): + def __init__(self, non_speech_label: float) -> None: super(DeserializeSpeechTransformer, self).__init__() - self._non_speech_label = non_speech_label - self.deserialized_speech_results_ = None + self._non_speech_label: float = non_speech_label + self.deserialized_speech_results_: Optional[np.ndarray] = None - def fit(self, fname, *_): + def fit(self, fname, *_) -> "DeserializeSpeechTransformer": speech = np.load(fname) - if hasattr(speech, 'files'): - if 'speech' in speech.files: - speech = speech['speech'] + if hasattr(speech, "files"): + if "speech" in speech.files: + speech = speech["speech"] else: - raise ValueError('could not find "speech" array in ' - 'serialized file; only contains: %s' % speech.files) - speech[speech < 1.] = self._non_speech_label + raise ValueError( + 'could not find "speech" array in ' + "serialized file; only contains: %s" % speech.files + ) + speech[speech < 1.0] = self._non_speech_label self.deserialized_speech_results_ = speech return self - def transform(self, *_): + def transform(self, *_) -> np.ndarray: + assert self.deserialized_speech_results_ is not None return self.deserialized_speech_results_ diff --git a/libs/ffsubsync/suboffset.py b/libs/ffsubsync/suboffset.py deleted file mode 100644 index bb8ebdf17..000000000 --- a/libs/ffsubsync/suboffset.py +++ /dev/null @@ -1,27 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -import logging -import sys - -from sklearn.pipeline import Pipeline - -from .subtitle_parser import GenericSubtitleParser -from .subtitle_transformers import SubtitleShifter - -logging.basicConfig(level=logging.INFO) -logger = logging.getLogger(__name__) - - -def main(): - td = float(sys.argv[3]) - pipe = Pipeline([ - ('parse', GenericSubtitleParser()), - ('offset', SubtitleShifter(td)), - ]) - pipe.fit_transform(sys.argv[1]) - pipe.steps[-1][1].write_file(sys.argv[2]) - return 0 - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/libs/ffsubsync/subtitle_parser.py b/libs/ffsubsync/subtitle_parser.py index 421be19da..ea5e6657c 100755 --- a/libs/ffsubsync/subtitle_parser.py +++ b/libs/ffsubsync/subtitle_parser.py @@ -1,41 +1,30 @@ # -*- coding: utf-8 -*- from datetime import timedelta import logging +from typing import Any, Optional try: import cchardet as chardet except ImportError: - import chardet + import chardet # type: ignore import pysubs2 -from .sklearn_shim import TransformerMixin +from ffsubsync.sklearn_shim import TransformerMixin import srt -from .constants import * -from .file_utils import open_file -from .generic_subtitles import GenericSubtitle, GenericSubtitlesFile, SubsMixin +from ffsubsync.constants import * +from ffsubsync.file_utils import open_file +from ffsubsync.generic_subtitles import GenericSubtitle, GenericSubtitlesFile, SubsMixin logging.basicConfig(level=logging.INFO) -logger = logging.getLogger(__name__) +logger: logging.Logger = logging.getLogger(__name__) -def make_subtitle_parser( - fmt, - encoding=DEFAULT_ENCODING, - caching=False, - max_subtitle_seconds=DEFAULT_MAX_SUBTITLE_SECONDS, - start_seconds=DEFAULT_START_SECONDS, - **kwargs -): - return GenericSubtitleParser( - fmt=fmt, - encoding=encoding, - caching=caching, - max_subtitle_seconds=max_subtitle_seconds, - start_seconds=start_seconds - ) - - -def _preprocess_subs(subs, max_subtitle_seconds=None, start_seconds=0, tolerant=True): +def _preprocess_subs( + subs, + max_subtitle_seconds: Optional[int] = None, + start_seconds: int = 0, + tolerant: bool = True, +) -> List[GenericSubtitle]: subs_list = [] start_time = timedelta(seconds=start_seconds) max_duration = timedelta(days=1) @@ -64,54 +53,95 @@ def _preprocess_subs(subs, max_subtitle_seconds=None, start_seconds=0, tolerant= class GenericSubtitleParser(SubsMixin, TransformerMixin): - def __init__(self, fmt='srt', encoding='infer', caching=False, max_subtitle_seconds=None, start_seconds=0): + def __init__( + self, + fmt: str = "srt", + encoding: str = "infer", + caching: bool = False, + max_subtitle_seconds: Optional[int] = None, + start_seconds: int = 0, + skip_ssa_info: bool = False, + ) -> None: super(self.__class__, self).__init__() - self.sub_format = fmt - self.encoding = encoding - self.caching = caching - self.fit_fname = None - self.detected_encoding_ = None - self.sub_skippers = [] - self.max_subtitle_seconds = max_subtitle_seconds - self.start_seconds = start_seconds + self.sub_format: str = fmt + self.encoding: str = encoding + self.caching: bool = caching + self.fit_fname: Optional[str] = None + self.detected_encoding_: Optional[str] = None + self.max_subtitle_seconds: Optional[int] = max_subtitle_seconds + self.start_seconds: int = start_seconds + # FIXME: hack to get tests to pass; remove + self._skip_ssa_info: bool = skip_ssa_info - def fit(self, fname, *_): - if self.caching and self.fit_fname == ('' if fname is None else fname): + def fit(self, fname: str, *_) -> "GenericSubtitleParser": + if self.caching and self.fit_fname == ("" if fname is None else fname): return self encodings_to_try = (self.encoding,) - with open_file(fname, 'rb') as f: + with open_file(fname, "rb") as f: subs = f.read() - if self.encoding == 'infer': - encodings_to_try = (chardet.detect(subs)['encoding'],) + if self.encoding == "infer": + encodings_to_try = (chardet.detect(subs)["encoding"],) self.detected_encoding_ = encodings_to_try[0] - logger.info('detected encoding: %s' % self.detected_encoding_) + logger.info("detected encoding: %s" % self.detected_encoding_) exc = None for encoding in encodings_to_try: try: - decoded_subs = subs.decode(encoding, errors='replace').strip() - if self.sub_format == 'srt': + decoded_subs = subs.decode(encoding, errors="replace").strip() + if self.sub_format == "srt": parsed_subs = srt.parse(decoded_subs) - elif self.sub_format in ('ass', 'ssa', 'sub'): + elif self.sub_format in ("ass", "ssa", "sub"): parsed_subs = pysubs2.SSAFile.from_string(decoded_subs) else: - raise NotImplementedError('unsupported format: %s' % self.sub_format) + raise NotImplementedError( + "unsupported format: %s" % self.sub_format + ) + extra_generic_subtitle_file_kwargs = {} + if isinstance(parsed_subs, pysubs2.SSAFile): + extra_generic_subtitle_file_kwargs.update( + dict( + styles=parsed_subs.styles, + # pysubs2 on Python >= 3.6 doesn't support this + fonts_opaque=getattr(parsed_subs, "fonts_opaque", None), + info=parsed_subs.info if not self._skip_ssa_info else None, + ) + ) self.subs_ = GenericSubtitlesFile( - _preprocess_subs(parsed_subs, - max_subtitle_seconds=self.max_subtitle_seconds, - start_seconds=self.start_seconds), + _preprocess_subs( + parsed_subs, + max_subtitle_seconds=self.max_subtitle_seconds, + start_seconds=self.start_seconds, + ), sub_format=self.sub_format, encoding=encoding, - styles=parsed_subs.styles if isinstance(parsed_subs, pysubs2.SSAFile) else None + **extra_generic_subtitle_file_kwargs, ) - self.fit_fname = '' if fname is None else fname + self.fit_fname = "" if fname is None else fname if len(encodings_to_try) > 1: self.detected_encoding_ = encoding - logger.info('detected encoding: %s' % self.detected_encoding_) + logger.info("detected encoding: %s" % self.detected_encoding_) return self except Exception as e: exc = e continue raise exc - def transform(self, *_): + def transform(self, *_) -> GenericSubtitlesFile: return self.subs_ + + +def make_subtitle_parser( + fmt: str, + encoding: str = DEFAULT_ENCODING, + caching: bool = False, + max_subtitle_seconds: int = DEFAULT_MAX_SUBTITLE_SECONDS, + start_seconds: int = DEFAULT_START_SECONDS, + **kwargs: Any, +) -> GenericSubtitleParser: + return GenericSubtitleParser( + fmt=fmt, + encoding=encoding, + caching=caching, + max_subtitle_seconds=max_subtitle_seconds, + start_seconds=start_seconds, + skip_ssa_info=kwargs.get("skip_ssa_info", False), + ) diff --git a/libs/ffsubsync/subtitle_transformers.py b/libs/ffsubsync/subtitle_transformers.py index 32330f597..dcf0664f5 100644 --- a/libs/ffsubsync/subtitle_transformers.py +++ b/libs/ffsubsync/subtitle_transformers.py @@ -3,12 +3,11 @@ from datetime import timedelta import logging import numbers -from .sklearn_shim import TransformerMixin - -from .generic_subtitles import GenericSubtitle, GenericSubtitlesFile, SubsMixin +from ffsubsync.generic_subtitles import GenericSubtitle, GenericSubtitlesFile, SubsMixin +from ffsubsync.sklearn_shim import TransformerMixin logging.basicConfig(level=logging.INFO) -logger = logging.getLogger(__name__) +logger: logging.Logger = logging.getLogger(__name__) class SubtitleShifter(SubsMixin, TransformerMixin): @@ -19,7 +18,7 @@ class SubtitleShifter(SubsMixin, TransformerMixin): else: self.td_seconds = td_seconds - def fit(self, subs, *_): + def fit(self, subs: GenericSubtitlesFile, *_): self.subs_ = subs.offset(self.td_seconds) return self @@ -33,7 +32,7 @@ class SubtitleScaler(SubsMixin, TransformerMixin): super(SubsMixin, self).__init__() self.scale_factor = scale_factor - def fit(self, subs, *_): + def fit(self, subs: GenericSubtitlesFile, *_): scaled_subs = [] for sub in subs: scaled_subs.append( @@ -41,15 +40,10 @@ class SubtitleScaler(SubsMixin, TransformerMixin): # py2 doesn't support direct multiplication of timedelta w/ float timedelta(seconds=sub.start.total_seconds() * self.scale_factor), timedelta(seconds=sub.end.total_seconds() * self.scale_factor), - sub.inner + sub.inner, ) ) - self.subs_ = GenericSubtitlesFile( - scaled_subs, - sub_format=subs.sub_format, - encoding=subs.encoding, - styles=subs.styles - ) + self.subs_ = subs.clone_props_for_subs(scaled_subs) return self def transform(self, *_): @@ -57,13 +51,13 @@ class SubtitleScaler(SubsMixin, TransformerMixin): class SubtitleMerger(SubsMixin, TransformerMixin): - def __init__(self, reference_subs, first='reference'): - assert first in ('reference', 'output') + def __init__(self, reference_subs, first="reference"): + assert first in ("reference", "output") super(SubsMixin, self).__init__() self.reference_subs = reference_subs self.first = first - def fit(self, output_subs, *_): + def fit(self, output_subs: GenericSubtitlesFile, *_): def _merger_gen(a, b): ita, itb = iter(a), iter(b) cur_a = next(ita, None) @@ -118,17 +112,13 @@ class SubtitleMerger(SubsMixin, TransformerMixin): cur_b = next(itb, None) merged_subs = [] - if self.first == 'reference': + if self.first == "reference": first, second = self.reference_subs, output_subs else: first, second = output_subs, self.reference_subs for merged in _merger_gen(first, second): merged_subs.append(merged) - self.subs_ = GenericSubtitlesFile( - merged_subs, - sub_format=output_subs.sub_format, - encoding=output_subs.encoding - ) + self.subs_ = output_subs.clone_props_for_subs(merged_subs) return self def transform(self, *_): diff --git a/libs/ffsubsync/version.py b/libs/ffsubsync/version.py index 2cfdfdf70..936e12209 100644 --- a/libs/ffsubsync/version.py +++ b/libs/ffsubsync/version.py @@ -1,14 +1,17 @@ # -*- coding: utf-8 -*- import os -from .constants import SUBSYNC_RESOURCES_ENV_MAGIC -from ._version import get_versions -__version__ = get_versions()['version'] +from ffsubsync.constants import SUBSYNC_RESOURCES_ENV_MAGIC +from ffsubsync._version import get_versions + +__version__ = get_versions()["version"] del get_versions def get_version(): - if 'unknown' in __version__.lower(): - with open(os.path.join(os.environ[SUBSYNC_RESOURCES_ENV_MAGIC], '__version__')) as f: + if "unknown" in __version__.lower(): + with open( + os.path.join(os.environ[SUBSYNC_RESOURCES_ENV_MAGIC], "__version__") + ) as f: return f.read().strip() else: return __version__ @@ -17,10 +20,10 @@ def get_version(): def make_version_tuple(vstr=None): if vstr is None: vstr = __version__ - if vstr[0] == 'v': + if vstr[0] == "v": vstr = vstr[1:] components = [] - for component in vstr.split('+')[0].split('.'): + for component in vstr.split("+")[0].split("."): try: components.append(int(component)) except ValueError: @@ -32,9 +35,10 @@ def update_available(): import requests from requests.exceptions import Timeout from .constants import API_RELEASE_URL + try: resp = requests.get(API_RELEASE_URL, timeout=1) - latest_vstr = resp.json()['tag_name'] + latest_vstr = resp.json()["tag_name"] except Timeout: return False except KeyError: diff --git a/libs/flask/__init__.py b/libs/flask/__init__.py index 687475bc6..43b54683e 100644 --- a/libs/flask/__init__.py +++ b/libs/flask/__init__.py @@ -1,60 +1,46 @@ -# -*- coding: utf-8 -*- -""" - flask - ~~~~~ +from markupsafe import escape +from markupsafe import Markup +from werkzeug.exceptions import abort as abort +from werkzeug.utils import redirect as redirect - A microframework based on Werkzeug. It's extensively documented - and follows best practice patterns. +from . import json as json +from .app import Flask as Flask +from .app import Request as Request +from .app import Response as Response +from .blueprints import Blueprint as Blueprint +from .config import Config as Config +from .ctx import after_this_request as after_this_request +from .ctx import copy_current_request_context as copy_current_request_context +from .ctx import has_app_context as has_app_context +from .ctx import has_request_context as has_request_context +from .globals import _app_ctx_stack as _app_ctx_stack +from .globals import _request_ctx_stack as _request_ctx_stack +from .globals import current_app as current_app +from .globals import g as g +from .globals import request as request +from .globals import session as session +from .helpers import flash as flash +from .helpers import get_flashed_messages as get_flashed_messages +from .helpers import get_template_attribute as get_template_attribute +from .helpers import make_response as make_response +from .helpers import safe_join as safe_join +from .helpers import send_file as send_file +from .helpers import send_from_directory as send_from_directory +from .helpers import stream_with_context as stream_with_context +from .helpers import url_for as url_for +from .json import jsonify as jsonify +from .signals import appcontext_popped as appcontext_popped +from .signals import appcontext_pushed as appcontext_pushed +from .signals import appcontext_tearing_down as appcontext_tearing_down +from .signals import before_render_template as before_render_template +from .signals import got_request_exception as got_request_exception +from .signals import message_flashed as message_flashed +from .signals import request_finished as request_finished +from .signals import request_started as request_started +from .signals import request_tearing_down as request_tearing_down +from .signals import signals_available as signals_available +from .signals import template_rendered as template_rendered +from .templating import render_template as render_template +from .templating import render_template_string as render_template_string - :copyright: 2010 Pallets - :license: BSD-3-Clause -""" -# utilities we import from Werkzeug and Jinja2 that are unused -# in the module but are exported as public interface. -from jinja2 import escape -from jinja2 import Markup -from werkzeug.exceptions import abort -from werkzeug.utils import redirect - -from . import json -from ._compat import json_available -from .app import Flask -from .app import Request -from .app import Response -from .blueprints import Blueprint -from .config import Config -from .ctx import after_this_request -from .ctx import copy_current_request_context -from .ctx import has_app_context -from .ctx import has_request_context -from .globals import _app_ctx_stack -from .globals import _request_ctx_stack -from .globals import current_app -from .globals import g -from .globals import request -from .globals import session -from .helpers import flash -from .helpers import get_flashed_messages -from .helpers import get_template_attribute -from .helpers import make_response -from .helpers import safe_join -from .helpers import send_file -from .helpers import send_from_directory -from .helpers import stream_with_context -from .helpers import url_for -from .json import jsonify -from .signals import appcontext_popped -from .signals import appcontext_pushed -from .signals import appcontext_tearing_down -from .signals import before_render_template -from .signals import got_request_exception -from .signals import message_flashed -from .signals import request_finished -from .signals import request_started -from .signals import request_tearing_down -from .signals import signals_available -from .signals import template_rendered -from .templating import render_template -from .templating import render_template_string - -__version__ = "1.1.1" +__version__ = "2.0.2" diff --git a/libs/flask/__main__.py b/libs/flask/__main__.py index f61dbc0b0..4e28416e1 100644 --- a/libs/flask/__main__.py +++ b/libs/flask/__main__.py @@ -1,15 +1,3 @@ -# -*- coding: utf-8 -*- -""" - flask.__main__ - ~~~~~~~~~~~~~~ +from .cli import main - Alias for flask.run for the command line. - - :copyright: 2010 Pallets - :license: BSD-3-Clause -""" - -if __name__ == "__main__": - from .cli import main - - main(as_module=True) +main() diff --git a/libs/flask/_compat.py b/libs/flask/_compat.py deleted file mode 100644 index 76c442cab..000000000 --- a/libs/flask/_compat.py +++ /dev/null @@ -1,145 +0,0 @@ -# -*- coding: utf-8 -*- -""" - flask._compat - ~~~~~~~~~~~~~ - - Some py2/py3 compatibility support based on a stripped down - version of six so we don't have to depend on a specific version - of it. - - :copyright: 2010 Pallets - :license: BSD-3-Clause -""" -import sys - -PY2 = sys.version_info[0] == 2 -_identity = lambda x: x - -try: # Python 2 - text_type = unicode - string_types = (str, unicode) - integer_types = (int, long) -except NameError: # Python 3 - text_type = str - string_types = (str,) - integer_types = (int,) - -if not PY2: - iterkeys = lambda d: iter(d.keys()) - itervalues = lambda d: iter(d.values()) - iteritems = lambda d: iter(d.items()) - - from inspect import getfullargspec as getargspec - from io import StringIO - import collections.abc as collections_abc - - def reraise(tp, value, tb=None): - if value.__traceback__ is not tb: - raise value.with_traceback(tb) - raise value - - implements_to_string = _identity - -else: - iterkeys = lambda d: d.iterkeys() - itervalues = lambda d: d.itervalues() - iteritems = lambda d: d.iteritems() - - from inspect import getargspec - from cStringIO import StringIO - import collections as collections_abc - - exec("def reraise(tp, value, tb=None):\n raise tp, value, tb") - - def implements_to_string(cls): - cls.__unicode__ = cls.__str__ - cls.__str__ = lambda x: x.__unicode__().encode("utf-8") - return cls - - -def with_metaclass(meta, *bases): - """Create a base class with a metaclass.""" - # This requires a bit of explanation: the basic idea is to make a - # dummy metaclass for one level of class instantiation that replaces - # itself with the actual metaclass. - class metaclass(type): - def __new__(metacls, name, this_bases, d): - return meta(name, bases, d) - - return type.__new__(metaclass, "temporary_class", (), {}) - - -# Certain versions of pypy have a bug where clearing the exception stack -# breaks the __exit__ function in a very peculiar way. The second level of -# exception blocks is necessary because pypy seems to forget to check if an -# exception happened until the next bytecode instruction? -# -# Relevant PyPy bugfix commit: -# https://bitbucket.org/pypy/pypy/commits/77ecf91c635a287e88e60d8ddb0f4e9df4003301 -# According to ronan on #pypy IRC, it is released in PyPy2 2.3 and later -# versions. -# -# Ubuntu 14.04 has PyPy 2.2.1, which does exhibit this bug. -BROKEN_PYPY_CTXMGR_EXIT = False -if hasattr(sys, "pypy_version_info"): - - class _Mgr(object): - def __enter__(self): - return self - - def __exit__(self, *args): - if hasattr(sys, "exc_clear"): - # Python 3 (PyPy3) doesn't have exc_clear - sys.exc_clear() - - try: - try: - with _Mgr(): - raise AssertionError() - except: # noqa: B001 - # We intentionally use a bare except here. See the comment above - # regarding a pypy bug as to why. - raise - except TypeError: - BROKEN_PYPY_CTXMGR_EXIT = True - except AssertionError: - pass - - -try: - from os import fspath -except ImportError: - # Backwards compatibility as proposed in PEP 0519: - # https://www.python.org/dev/peps/pep-0519/#backwards-compatibility - def fspath(path): - return path.__fspath__() if hasattr(path, "__fspath__") else path - - -class _DeprecatedBool(object): - def __init__(self, name, version, value): - self.message = "'{}' is deprecated and will be removed in version {}.".format( - name, version - ) - self.value = value - - def _warn(self): - import warnings - - warnings.warn(self.message, DeprecationWarning, stacklevel=2) - - def __eq__(self, other): - self._warn() - return other == self.value - - def __ne__(self, other): - self._warn() - return other != self.value - - def __bool__(self): - self._warn() - return self.value - - __nonzero__ = __bool__ - - -json_available = _DeprecatedBool("flask.json_available", "2.0.0", True) diff --git a/libs/flask/app.py b/libs/flask/app.py index e596fe570..23b99e2ca 100644 --- a/libs/flask/app.py +++ b/libs/flask/app.py @@ -1,42 +1,32 @@ -# -*- coding: utf-8 -*- -""" - flask.app - ~~~~~~~~~ - - This module implements the central WSGI application object. - - :copyright: 2010 Pallets - :license: BSD-3-Clause -""" +import functools +import inspect +import logging import os import sys -import warnings +import typing as t +import weakref from datetime import timedelta -from functools import update_wrapper from itertools import chain from threading import Lock +from types import TracebackType from werkzeug.datastructures import Headers from werkzeug.datastructures import ImmutableDict from werkzeug.exceptions import BadRequest from werkzeug.exceptions import BadRequestKeyError -from werkzeug.exceptions import default_exceptions from werkzeug.exceptions import HTTPException from werkzeug.exceptions import InternalServerError -from werkzeug.exceptions import MethodNotAllowed +from werkzeug.local import ContextVar from werkzeug.routing import BuildError from werkzeug.routing import Map +from werkzeug.routing import MapAdapter from werkzeug.routing import RequestRedirect from werkzeug.routing import RoutingException from werkzeug.routing import Rule -from werkzeug.wrappers import BaseResponse +from werkzeug.wrappers import Response as BaseResponse from . import cli from . import json -from ._compat import integer_types -from ._compat import reraise -from ._compat import string_types -from ._compat import text_type from .config import Config from .config import ConfigAttribute from .ctx import _AppCtxGlobals @@ -46,9 +36,7 @@ from .globals import _request_ctx_stack from .globals import g from .globals import request from .globals import session -from .helpers import _endpoint_from_view_func -from .helpers import _PackageBoundObject -from .helpers import find_package +from .helpers import _split_blueprint_path from .helpers import get_debug_flag from .helpers import get_env from .helpers import get_flashed_messages @@ -57,50 +45,57 @@ from .helpers import locked_cached_property from .helpers import url_for from .json import jsonify from .logging import create_logger +from .scaffold import _endpoint_from_view_func +from .scaffold import _sentinel +from .scaffold import find_package +from .scaffold import Scaffold +from .scaffold import setupmethod from .sessions import SecureCookieSessionInterface from .signals import appcontext_tearing_down from .signals import got_request_exception from .signals import request_finished from .signals import request_started from .signals import request_tearing_down -from .templating import _default_template_ctx_processor from .templating import DispatchingJinjaLoader from .templating import Environment +from .typing import BeforeFirstRequestCallable +from .typing import ResponseReturnValue +from .typing import TeardownCallable +from .typing import TemplateFilterCallable +from .typing import TemplateGlobalCallable +from .typing import TemplateTestCallable from .wrappers import Request from .wrappers import Response -# a singleton sentinel value for parameter defaults -_sentinel = object() +if t.TYPE_CHECKING: + import typing_extensions as te + from .blueprints import Blueprint + from .testing import FlaskClient + from .testing import FlaskCliRunner + from .typing import ErrorHandlerCallable + +if sys.version_info >= (3, 8): + iscoroutinefunction = inspect.iscoroutinefunction +else: + + def iscoroutinefunction(func: t.Any) -> bool: + while inspect.ismethod(func): + func = func.__func__ + + while isinstance(func, functools.partial): + func = func.func + + return inspect.iscoroutinefunction(func) -def _make_timedelta(value): - if not isinstance(value, timedelta): - return timedelta(seconds=value) - return value +def _make_timedelta(value: t.Optional[timedelta]) -> t.Optional[timedelta]: + if value is None or isinstance(value, timedelta): + return value + + return timedelta(seconds=value) -def setupmethod(f): - """Wraps a method so that it performs a check in debug mode if the - first request was already handled. - """ - - def wrapper_func(self, *args, **kwargs): - if self.debug and self._got_first_request: - raise AssertionError( - "A setup function was called after the " - "first request was handled. This usually indicates a bug " - "in the application where a module was not imported " - "and decorators or other functionality was called too late.\n" - "To fix this make sure to import all your view modules, " - "database models and everything related at a central place " - "before the application starts serving requests." - ) - return f(self, *args, **kwargs) - - return update_wrapper(wrapper_func, f) - - -class Flask(_PackageBoundObject): +class Flask(Scaffold): """The flask object implements a WSGI application and acts as the central object. It is passed the name of the module or package of the application. Once it is created it will act as a central registry for @@ -170,9 +165,9 @@ class Flask(_PackageBoundObject): :param static_url_path: can be used to specify a different path for the static files on the web. Defaults to the name of the `static_folder` folder. - :param static_folder: the folder with static files that should be served - at `static_url_path`. Defaults to the ``'static'`` - folder in the root path of the application. + :param static_folder: The folder with static files that is served at + ``static_url_path``. Relative to the application ``root_path`` + or an absolute path. Defaults to ``'static'``. :param static_host: the host to use when adding the static route. Defaults to None. Required when using ``host_matching=True`` with a ``static_folder`` configured. @@ -192,11 +187,9 @@ class Flask(_PackageBoundObject): for loading the config are assumed to be relative to the instance path instead of the application root. - :param root_path: Flask by default will automatically calculate the path - to the root of the application. In certain situations - this cannot be achieved (for instance if the package - is a Python 3 namespace package) and needs to be - manually defined. + :param root_path: The path to the root of the application files. + This should only be set manually when it can't be detected + automatically, such as for namespace packages. """ #: The class that is used for request objects. See :class:`~flask.Request` @@ -276,13 +269,16 @@ class Flask(_PackageBoundObject): "PERMANENT_SESSION_LIFETIME", get_converter=_make_timedelta ) - #: A :class:`~datetime.timedelta` which is used as default cache_timeout - #: for the :func:`send_file` functions. The default is 12 hours. + #: A :class:`~datetime.timedelta` or number of seconds which is used + #: as the default ``max_age`` for :func:`send_file`. The default is + #: ``None``, which tells the browser to use conditional requests + #: instead of a timed cache. #: - #: This attribute can also be configured from the config with the - #: ``SEND_FILE_MAX_AGE_DEFAULT`` configuration key. This configuration - #: variable can also be set with an integer value used as seconds. - #: Defaults to ``timedelta(hours=12)`` + #: Configured with the :data:`SEND_FILE_MAX_AGE_DEFAULT` + #: configuration key. + #: + #: .. versionchanged:: 2.0 + #: Defaults to ``None`` instead of 12 hours. send_file_max_age_default = ConfigAttribute( "SEND_FILE_MAX_AGE_DEFAULT", get_converter=_make_timedelta ) @@ -316,7 +312,7 @@ class Flask(_PackageBoundObject): #: This is a ``dict`` instead of an ``ImmutableDict`` to allow #: easier configuration. #: - jinja_options = {"extensions": ["jinja2.ext.autoescape", "jinja2.ext.with_"]} + jinja_options: dict = {} #: Default configuration parameters. default_config = ImmutableDict( @@ -339,7 +335,7 @@ class Flask(_PackageBoundObject): "SESSION_COOKIE_SAMESITE": None, "SESSION_REFRESH_EACH_REQUEST": True, "MAX_CONTENT_LENGTH": None, - "SEND_FILE_MAX_AGE_DEFAULT": timedelta(hours=12), + "SEND_FILE_MAX_AGE_DEFAULT": None, "TRAP_BAD_REQUEST_ERRORS": None, "TRAP_HTTP_EXCEPTIONS": False, "EXPLAIN_TEMPLATE_LOADING": False, @@ -365,10 +361,11 @@ class Flask(_PackageBoundObject): #: .. versionadded:: 1.1.0 url_map_class = Map - #: the test client that is used with when `test_client` is used. + #: The :meth:`test_client` method creates an instance of this test + #: client class. Defaults to :class:`~flask.testing.FlaskClient`. #: #: .. versionadded:: 0.7 - test_client_class = None + test_client_class: t.Optional[t.Type["FlaskClient"]] = None #: The :class:`~click.testing.CliRunner` subclass, by default #: :class:`~flask.testing.FlaskCliRunner` that is used by @@ -376,7 +373,7 @@ class Flask(_PackageBoundObject): #: Flask app object as the first argument. #: #: .. versionadded:: 1.0 - test_cli_runner_class = None + test_cli_runner_class: t.Optional[t.Type["FlaskCliRunner"]] = None #: the session interface to use. By default an instance of #: :class:`~flask.sessions.SecureCookieSessionInterface` is used here. @@ -384,41 +381,27 @@ class Flask(_PackageBoundObject): #: .. versionadded:: 0.8 session_interface = SecureCookieSessionInterface() - # TODO remove the next three attrs when Sphinx :inherited-members: works - # https://github.com/sphinx-doc/sphinx/issues/741 - - #: The name of the package or module that this app belongs to. Do not - #: change this once it is set by the constructor. - import_name = None - - #: Location of the template files to be added to the template lookup. - #: ``None`` if templates should not be added. - template_folder = None - - #: Absolute path to the package on the filesystem. Used to look up - #: resources contained in the package. - root_path = None - def __init__( self, - import_name, - static_url_path=None, - static_folder="static", - static_host=None, - host_matching=False, - subdomain_matching=False, - template_folder="templates", - instance_path=None, - instance_relative_config=False, - root_path=None, + import_name: str, + static_url_path: t.Optional[str] = None, + static_folder: t.Optional[t.Union[str, os.PathLike]] = "static", + static_host: t.Optional[str] = None, + host_matching: bool = False, + subdomain_matching: bool = False, + template_folder: t.Optional[str] = "templates", + instance_path: t.Optional[str] = None, + instance_relative_config: bool = False, + root_path: t.Optional[str] = None, ): - _PackageBoundObject.__init__( - self, import_name, template_folder=template_folder, root_path=root_path + super().__init__( + import_name=import_name, + static_folder=static_folder, + static_url_path=static_url_path, + template_folder=template_folder, + root_path=root_path, ) - self.static_url_path = static_url_path - self.static_folder = static_folder - if instance_path is None: instance_path = self.auto_find_instance_path() elif not os.path.isabs(instance_path): @@ -437,24 +420,6 @@ class Flask(_PackageBoundObject): #: to load a config from files. self.config = self.make_config(instance_relative_config) - #: A dictionary of all view functions registered. The keys will - #: be function names which are also used to generate URLs and - #: the values are the function objects themselves. - #: To register a view function, use the :meth:`route` decorator. - self.view_functions = {} - - #: A dictionary of all registered error handlers. The key is ``None`` - #: for error handlers active on the application, otherwise the key is - #: the name of the blueprint. Each key points to another dictionary - #: where the key is the status code of the http exception. The - #: special key ``None`` points to a list of tuples where the first item - #: is the class for the instance check and the second the error handler - #: function. - #: - #: To register an error handler, use the :meth:`errorhandler` - #: decorator. - self.error_handler_spec = {} - #: A list of functions that are called when :meth:`url_for` raises a #: :exc:`~werkzeug.routing.BuildError`. Each function registered here #: is called with `error`, `endpoint` and `values`. If a function @@ -462,40 +427,16 @@ class Flask(_PackageBoundObject): #: tried. #: #: .. versionadded:: 0.9 - self.url_build_error_handlers = [] - - #: A dictionary with lists of functions that will be called at the - #: beginning of each request. The key of the dictionary is the name of - #: the blueprint this function is active for, or ``None`` for all - #: requests. To register a function, use the :meth:`before_request` - #: decorator. - self.before_request_funcs = {} + self.url_build_error_handlers: t.List[ + t.Callable[[Exception, str, dict], str] + ] = [] #: A list of functions that will be called at the beginning of the #: first request to this instance. To register a function, use the #: :meth:`before_first_request` decorator. #: #: .. versionadded:: 0.8 - self.before_first_request_funcs = [] - - #: A dictionary with lists of functions that should be called after - #: each request. The key of the dictionary is the name of the blueprint - #: this function is active for, ``None`` for all requests. This can for - #: example be used to close database connections. To register a function - #: here, use the :meth:`after_request` decorator. - self.after_request_funcs = {} - - #: A dictionary with lists of functions that are called after - #: each request, even if an exception has occurred. The key of the - #: dictionary is the name of the blueprint this function is active for, - #: ``None`` for all requests. These functions are not allowed to modify - #: the request, and their return values are ignored. If an exception - #: occurred while processing the request, it gets passed to each - #: teardown_request function. To register a function here, use the - #: :meth:`teardown_request` decorator. - #: - #: .. versionadded:: 0.7 - self.teardown_request_funcs = {} + self.before_first_request_funcs: t.List[BeforeFirstRequestCallable] = [] #: A list of functions that are called when the application context #: is destroyed. Since the application context is also torn down @@ -503,66 +444,32 @@ class Flask(_PackageBoundObject): #: from databases. #: #: .. versionadded:: 0.9 - self.teardown_appcontext_funcs = [] - - #: A dictionary with lists of functions that are called before the - #: :attr:`before_request_funcs` functions. The key of the dictionary is - #: the name of the blueprint this function is active for, or ``None`` - #: for all requests. To register a function, use - #: :meth:`url_value_preprocessor`. - #: - #: .. versionadded:: 0.7 - self.url_value_preprocessors = {} - - #: A dictionary with lists of functions that can be used as URL value - #: preprocessors. The key ``None`` here is used for application wide - #: callbacks, otherwise the key is the name of the blueprint. - #: Each of these functions has the chance to modify the dictionary - #: of URL values before they are used as the keyword arguments of the - #: view function. For each function registered this one should also - #: provide a :meth:`url_defaults` function that adds the parameters - #: automatically again that were removed that way. - #: - #: .. versionadded:: 0.7 - self.url_default_functions = {} - - #: A dictionary with list of functions that are called without argument - #: to populate the template context. The key of the dictionary is the - #: name of the blueprint this function is active for, ``None`` for all - #: requests. Each returns a dictionary that the template context is - #: updated with. To register a function here, use the - #: :meth:`context_processor` decorator. - self.template_context_processors = {None: [_default_template_ctx_processor]} + self.teardown_appcontext_funcs: t.List[TeardownCallable] = [] #: A list of shell context processor functions that should be run #: when a shell context is created. #: #: .. versionadded:: 0.11 - self.shell_context_processors = [] + self.shell_context_processors: t.List[t.Callable[[], t.Dict[str, t.Any]]] = [] - #: all the attached blueprints in a dictionary by name. Blueprints - #: can be attached multiple times so this dictionary does not tell - #: you how often they got attached. + #: Maps registered blueprint names to blueprint objects. The + #: dict retains the order the blueprints were registered in. + #: Blueprints can be registered multiple times, this dict does + #: not track how often they were attached. #: #: .. versionadded:: 0.7 - self.blueprints = {} - self._blueprint_order = [] + self.blueprints: t.Dict[str, "Blueprint"] = {} #: a place where extensions can store application specific state. For #: example this is where an extension could store database engines and - #: similar things. For backwards compatibility extensions should register - #: themselves like this:: - #: - #: if not hasattr(app, 'extensions'): - #: app.extensions = {} - #: app.extensions['extensionname'] = SomeObject() + #: similar things. #: #: The key must match the name of the extension module. For example in #: case of a "Flask-Foo" extension in `flask_foo`, the key would be #: ``'foo'``. #: #: .. versionadded:: 0.7 - self.extensions = {} + self.extensions: dict = {} #: The :class:`~werkzeug.routing.Map` for this instance. You can use #: this to change the routing converters after the class was created @@ -598,19 +505,25 @@ class Flask(_PackageBoundObject): assert ( bool(static_host) == host_matching ), "Invalid static_host/host_matching combination" + # Use a weakref to avoid creating a reference cycle between the app + # and the view function (see #3761). + self_ref = weakref.ref(self) self.add_url_rule( - self.static_url_path + "/", + f"{self.static_url_path}/", endpoint="static", host=static_host, - view_func=self.send_static_file, + view_func=lambda **kw: self_ref().send_static_file(**kw), # type: ignore # noqa: B950 ) # Set the name of the Click group in case someone wants to add # the app's commands to another CLI tool. self.cli.name = self.name + def _is_setup_finished(self) -> bool: + return self.debug and self._got_first_request + @locked_cached_property - def name(self): + def name(self) -> str: # type: ignore """The name of the application. This is usually the import name with the difference that it's guessed from the run file if the import name is main. This name is used as a display name when @@ -627,7 +540,7 @@ class Flask(_PackageBoundObject): return self.import_name @property - def propagate_exceptions(self): + def propagate_exceptions(self) -> bool: """Returns the value of the ``PROPAGATE_EXCEPTIONS`` configuration value in case it's set, otherwise a sensible default is returned. @@ -639,7 +552,7 @@ class Flask(_PackageBoundObject): return self.testing or self.debug @property - def preserve_context_on_exception(self): + def preserve_context_on_exception(self) -> bool: """Returns the value of the ``PRESERVE_CONTEXT_ON_EXCEPTION`` configuration value in case it's set, otherwise a sensible default is returned. @@ -652,7 +565,7 @@ class Flask(_PackageBoundObject): return self.debug @locked_cached_property - def logger(self): + def logger(self) -> logging.Logger: """A standard Python :class:`~logging.Logger` for the app, with the same name as :attr:`name`. @@ -679,7 +592,7 @@ class Flask(_PackageBoundObject): return create_logger(self) @locked_cached_property - def jinja_env(self): + def jinja_env(self) -> Environment: """The Jinja environment used to load templates. The environment is created the first time this property is @@ -689,7 +602,7 @@ class Flask(_PackageBoundObject): return self.create_jinja_environment() @property - def got_first_request(self): + def got_first_request(self) -> bool: """This attribute is set to ``True`` if the application started handling the first request. @@ -697,7 +610,7 @@ class Flask(_PackageBoundObject): """ return self._got_first_request - def make_config(self, instance_relative=False): + def make_config(self, instance_relative: bool = False) -> Config: """Used to create the config attribute by the Flask constructor. The `instance_relative` parameter is passed in from the constructor of Flask (there named `instance_relative_config`) and indicates if @@ -714,7 +627,7 @@ class Flask(_PackageBoundObject): defaults["DEBUG"] = get_debug_flag() return self.config_class(root_path, defaults) - def auto_find_instance_path(self): + def auto_find_instance_path(self) -> str: """Tries to locate the instance path if it was not provided to the constructor of the application class. It will basically calculate the path to a folder named ``instance`` next to your main file or @@ -725,9 +638,9 @@ class Flask(_PackageBoundObject): prefix, package_path = find_package(self.import_name) if prefix is None: return os.path.join(package_path, "instance") - return os.path.join(prefix, "var", self.name + "-instance") + return os.path.join(prefix, "var", f"{self.name}-instance") - def open_instance_resource(self, resource, mode="rb"): + def open_instance_resource(self, resource: str, mode: str = "rb") -> t.IO[t.AnyStr]: """Opens a resource from the application's instance folder (:attr:`instance_path`). Otherwise works like :meth:`open_resource`. Instance resources can also be opened for @@ -740,7 +653,7 @@ class Flask(_PackageBoundObject): return open(os.path.join(self.instance_path, resource), mode) @property - def templates_auto_reload(self): + def templates_auto_reload(self) -> bool: """Reload templates when they are changed. Used by :meth:`create_jinja_environment`. @@ -755,10 +668,10 @@ class Flask(_PackageBoundObject): return rv if rv is not None else self.debug @templates_auto_reload.setter - def templates_auto_reload(self, value): + def templates_auto_reload(self, value: bool) -> None: self.config["TEMPLATES_AUTO_RELOAD"] = value - def create_jinja_environment(self): + def create_jinja_environment(self) -> Environment: """Create the Jinja environment based on :attr:`jinja_options` and the various Jinja-related methods of the app. Changing :attr:`jinja_options` after this will have no effect. Also adds @@ -790,10 +703,10 @@ class Flask(_PackageBoundObject): session=session, g=g, ) - rv.filters["tojson"] = json.tojson_filter + rv.policies["json.dumps_function"] = json.dumps return rv - def create_global_jinja_loader(self): + def create_global_jinja_loader(self) -> DispatchingJinjaLoader: """Creates the loader for the Jinja2 environment. Can be used to override just the loader and keeping the rest unchanged. It's discouraged to override this function. Instead one should override @@ -806,7 +719,7 @@ class Flask(_PackageBoundObject): """ return DispatchingJinjaLoader(self) - def select_jinja_autoescape(self, filename): + def select_jinja_autoescape(self, filename: str) -> bool: """Returns ``True`` if autoescaping should be active for the given template name. If no template name is given, returns `True`. @@ -816,7 +729,7 @@ class Flask(_PackageBoundObject): return True return filename.endswith((".html", ".htm", ".xml", ".xhtml")) - def update_template_context(self, context): + def update_template_context(self, context: dict) -> None: """Update the template context with some commonly used variables. This injects request, session, config and g into the template context as well as everything template context processors want @@ -827,21 +740,24 @@ class Flask(_PackageBoundObject): :param context: the context as a dictionary that is updated in place to add extra variables. """ - funcs = self.template_context_processors[None] - reqctx = _request_ctx_stack.top - if reqctx is not None: - bp = reqctx.request.blueprint - if bp is not None and bp in self.template_context_processors: - funcs = chain(funcs, self.template_context_processors[bp]) + names: t.Iterable[t.Optional[str]] = (None,) + + # A template may be rendered outside a request context. + if request: + names = chain(names, reversed(request.blueprints)) + + # The values passed to render_template take precedence. Keep a + # copy to re-apply after all context functions. orig_ctx = context.copy() - for func in funcs: - context.update(func()) - # make sure the original values win. This makes it possible to - # easier add new variables in context processors without breaking - # existing views. + + for name in names: + if name in self.template_context_processors: + for func in self.template_context_processors[name]: + context.update(func()) + context.update(orig_ctx) - def make_shell_context(self): + def make_shell_context(self) -> dict: """Returns the shell context for an interactive shell for this application. This runs all the registered shell context processors. @@ -865,7 +781,7 @@ class Flask(_PackageBoundObject): env = ConfigAttribute("ENV") @property - def debug(self): + def debug(self) -> bool: """Whether debug mode is enabled. When using ``flask run`` to start the development server, an interactive debugger will be shown for unhandled exceptions, and the server will be reloaded when code @@ -882,16 +798,23 @@ class Flask(_PackageBoundObject): return self.config["DEBUG"] @debug.setter - def debug(self, value): + def debug(self, value: bool) -> None: self.config["DEBUG"] = value self.jinja_env.auto_reload = self.templates_auto_reload - def run(self, host=None, port=None, debug=None, load_dotenv=True, **options): + def run( + self, + host: t.Optional[str] = None, + port: t.Optional[int] = None, + debug: t.Optional[bool] = None, + load_dotenv: bool = True, + **options: t.Any, + ) -> None: """Runs the application on a local development server. Do not use ``run()`` in a production setting. It is not intended to meet security and performance requirements for a production server. - Instead, see :ref:`deployment` for WSGI server recommendations. + Instead, see :doc:`/deploying/index` for WSGI server recommendations. If the :attr:`debug` flag is set the server will automatically reload for code changes and show a debugger in case an exception happened. @@ -966,17 +889,24 @@ class Flask(_PackageBoundObject): if debug is not None: self.debug = bool(debug) - _host = "127.0.0.1" - _port = 5000 server_name = self.config.get("SERVER_NAME") - sn_host, sn_port = None, None + sn_host = sn_port = None if server_name: sn_host, _, sn_port = server_name.partition(":") - host = host or sn_host or _host - # pick the first value that's not None (0 is allowed) - port = int(next((p for p in (port, sn_port) if p is not None), _port)) + if not host: + if sn_host: + host = sn_host + else: + host = "127.0.0.1" + + if port or port == 0: + port = int(port) + elif sn_port: + port = int(sn_port) + else: + port = 5000 options.setdefault("use_reloader", self.debug) options.setdefault("use_debugger", self.debug) @@ -987,16 +917,16 @@ class Flask(_PackageBoundObject): from werkzeug.serving import run_simple try: - run_simple(host, port, self, **options) + run_simple(t.cast(str, host), port, self, **options) finally: # reset the first request information if the development server # reset normally. This makes it possible to restart the server # without reloader and that stuff from an interactive shell. self._got_first_request = False - def test_client(self, use_cookies=True, **kwargs): + def test_client(self, use_cookies: bool = True, **kwargs: t.Any) -> "FlaskClient": """Creates a test client for this application. For information - about unit testing head over to :ref:`testing`. + about unit testing head over to :doc:`/testing`. Note that if you are testing for assertions or exceptions in your application code, you must set ``app.testing = True`` in order for the @@ -1047,10 +977,12 @@ class Flask(_PackageBoundObject): """ cls = self.test_client_class if cls is None: - from .testing import FlaskClient as cls - return cls(self, self.response_class, use_cookies=use_cookies, **kwargs) + from .testing import FlaskClient as cls # type: ignore + return cls( # type: ignore + self, self.response_class, use_cookies=use_cookies, **kwargs + ) - def test_cli_runner(self, **kwargs): + def test_cli_runner(self, **kwargs: t.Any) -> "FlaskCliRunner": """Create a CLI runner for testing CLI commands. See :ref:`testing-cli`. @@ -1063,75 +995,12 @@ class Flask(_PackageBoundObject): cls = self.test_cli_runner_class if cls is None: - from .testing import FlaskCliRunner as cls + from .testing import FlaskCliRunner as cls # type: ignore - return cls(self, **kwargs) - - def open_session(self, request): - """Creates or opens a new session. Default implementation stores all - session data in a signed cookie. This requires that the - :attr:`secret_key` is set. Instead of overriding this method - we recommend replacing the :class:`session_interface`. - - .. deprecated: 1.0 - Will be removed in 1.1. Use ``session_interface.open_session`` - instead. - - :param request: an instance of :attr:`request_class`. - """ - - warnings.warn( - DeprecationWarning( - '"open_session" is deprecated and will be removed in 1.1. Use' - ' "session_interface.open_session" instead.' - ) - ) - return self.session_interface.open_session(self, request) - - def save_session(self, session, response): - """Saves the session if it needs updates. For the default - implementation, check :meth:`open_session`. Instead of overriding this - method we recommend replacing the :class:`session_interface`. - - .. deprecated: 1.0 - Will be removed in 1.1. Use ``session_interface.save_session`` - instead. - - :param session: the session to be saved (a - :class:`~werkzeug.contrib.securecookie.SecureCookie` - object) - :param response: an instance of :attr:`response_class` - """ - - warnings.warn( - DeprecationWarning( - '"save_session" is deprecated and will be removed in 1.1. Use' - ' "session_interface.save_session" instead.' - ) - ) - return self.session_interface.save_session(self, session, response) - - def make_null_session(self): - """Creates a new instance of a missing session. Instead of overriding - this method we recommend replacing the :class:`session_interface`. - - .. deprecated: 1.0 - Will be removed in 1.1. Use ``session_interface.make_null_session`` - instead. - - .. versionadded:: 0.7 - """ - - warnings.warn( - DeprecationWarning( - '"make_null_session" is deprecated and will be removed in 1.1. Use' - ' "session_interface.make_null_session" instead.' - ) - ) - return self.session_interface.make_null_session(self) + return cls(self, **kwargs) # type: ignore @setupmethod - def register_blueprint(self, blueprint, **options): + def register_blueprint(self, blueprint: "Blueprint", **options: t.Any) -> None: """Register a :class:`~flask.Blueprint` on the application. Keyword arguments passed to this method will override the defaults set on the blueprint. @@ -1148,94 +1017,34 @@ class Flask(_PackageBoundObject): :class:`~flask.blueprints.BlueprintSetupState`. They can be accessed in :meth:`~flask.Blueprint.record` callbacks. + .. versionchanged:: 2.0.1 + The ``name`` option can be used to change the (pre-dotted) + name the blueprint is registered with. This allows the same + blueprint to be registered multiple times with unique names + for ``url_for``. + .. versionadded:: 0.7 """ - first_registration = False + blueprint.register(self, options) - if blueprint.name in self.blueprints: - assert self.blueprints[blueprint.name] is blueprint, ( - "A name collision occurred between blueprints %r and %r. Both" - ' share the same name "%s". Blueprints that are created on the' - " fly need unique names." - % (blueprint, self.blueprints[blueprint.name], blueprint.name) - ) - else: - self.blueprints[blueprint.name] = blueprint - self._blueprint_order.append(blueprint) - first_registration = True - - blueprint.register(self, options, first_registration) - - def iter_blueprints(self): + def iter_blueprints(self) -> t.ValuesView["Blueprint"]: """Iterates over all blueprints by the order they were registered. .. versionadded:: 0.11 """ - return iter(self._blueprint_order) + return self.blueprints.values() @setupmethod def add_url_rule( self, - rule, - endpoint=None, - view_func=None, - provide_automatic_options=None, - **options - ): - """Connects a URL rule. Works exactly like the :meth:`route` - decorator. If a view_func is provided it will be registered with the - endpoint. - - Basically this example:: - - @app.route('/') - def index(): - pass - - Is equivalent to the following:: - - def index(): - pass - app.add_url_rule('/', 'index', index) - - If the view_func is not provided you will need to connect the endpoint - to a view function like so:: - - app.view_functions['index'] = index - - Internally :meth:`route` invokes :meth:`add_url_rule` so if you want - to customize the behavior via subclassing you only need to change - this method. - - For more information refer to :ref:`url-route-registrations`. - - .. versionchanged:: 0.2 - `view_func` parameter added. - - .. versionchanged:: 0.6 - ``OPTIONS`` is added automatically as method. - - :param rule: the URL rule as string - :param endpoint: the endpoint for the registered URL rule. Flask - itself assumes the name of the view function as - endpoint - :param view_func: the function to call when serving a request to the - provided endpoint - :param provide_automatic_options: controls whether the ``OPTIONS`` - method should be added automatically. This can also be controlled - by setting the ``view_func.provide_automatic_options = False`` - before adding the rule. - :param options: the options to be forwarded to the underlying - :class:`~werkzeug.routing.Rule` object. A change - to Werkzeug is handling of method options. methods - is a list of methods this rule should be limited - to (``GET``, ``POST`` etc.). By default a rule - just listens for ``GET`` (and implicitly ``HEAD``). - Starting with Flask 0.6, ``OPTIONS`` is implicitly - added and handled by the standard request handling. - """ + rule: str, + endpoint: t.Optional[str] = None, + view_func: t.Optional[t.Callable] = None, + provide_automatic_options: t.Optional[bool] = None, + **options: t.Any, + ) -> None: if endpoint is None: - endpoint = _endpoint_from_view_func(view_func) + endpoint = _endpoint_from_view_func(view_func) # type: ignore options["endpoint"] = endpoint methods = options.pop("methods", None) @@ -1244,12 +1053,12 @@ class Flask(_PackageBoundObject): # a tuple of only ``GET`` as default. if methods is None: methods = getattr(view_func, "methods", None) or ("GET",) - if isinstance(methods, string_types): + if isinstance(methods, str): raise TypeError( - "Allowed methods have to be iterables of strings, " - 'for example: @app.route(..., methods=["POST"])' + "Allowed methods must be a list of strings, for" + ' example: @app.route(..., methods=["POST"])' ) - methods = set(item.upper() for item in methods) + methods = {item.upper() for item in methods} # Methods that should always be added required_methods = set(getattr(view_func, "required_methods", ())) @@ -1272,163 +1081,22 @@ class Flask(_PackageBoundObject): methods |= required_methods rule = self.url_rule_class(rule, methods=methods, **options) - rule.provide_automatic_options = provide_automatic_options + rule.provide_automatic_options = provide_automatic_options # type: ignore self.url_map.add(rule) if view_func is not None: old_func = self.view_functions.get(endpoint) if old_func is not None and old_func != view_func: raise AssertionError( - "View function mapping is overwriting an " - "existing endpoint function: %s" % endpoint + "View function mapping is overwriting an existing" + f" endpoint function: {endpoint}" ) self.view_functions[endpoint] = view_func - def route(self, rule, **options): - """A decorator that is used to register a view function for a - given URL rule. This does the same thing as :meth:`add_url_rule` - but is intended for decorator usage:: - - @app.route('/') - def index(): - return 'Hello World' - - For more information refer to :ref:`url-route-registrations`. - - :param rule: the URL rule as string - :param endpoint: the endpoint for the registered URL rule. Flask - itself assumes the name of the view function as - endpoint - :param options: the options to be forwarded to the underlying - :class:`~werkzeug.routing.Rule` object. A change - to Werkzeug is handling of method options. methods - is a list of methods this rule should be limited - to (``GET``, ``POST`` etc.). By default a rule - just listens for ``GET`` (and implicitly ``HEAD``). - Starting with Flask 0.6, ``OPTIONS`` is implicitly - added and handled by the standard request handling. - """ - - def decorator(f): - endpoint = options.pop("endpoint", None) - self.add_url_rule(rule, endpoint, f, **options) - return f - - return decorator - @setupmethod - def endpoint(self, endpoint): - """A decorator to register a function as an endpoint. - Example:: - - @app.endpoint('example.endpoint') - def example(): - return "example" - - :param endpoint: the name of the endpoint - """ - - def decorator(f): - self.view_functions[endpoint] = f - return f - - return decorator - - @staticmethod - def _get_exc_class_and_code(exc_class_or_code): - """Get the exception class being handled. For HTTP status codes - or ``HTTPException`` subclasses, return both the exception and - status code. - - :param exc_class_or_code: Any exception class, or an HTTP status - code as an integer. - """ - if isinstance(exc_class_or_code, integer_types): - exc_class = default_exceptions[exc_class_or_code] - else: - exc_class = exc_class_or_code - - assert issubclass(exc_class, Exception) - - if issubclass(exc_class, HTTPException): - return exc_class, exc_class.code - else: - return exc_class, None - - @setupmethod - def errorhandler(self, code_or_exception): - """Register a function to handle errors by code or exception class. - - A decorator that is used to register a function given an - error code. Example:: - - @app.errorhandler(404) - def page_not_found(error): - return 'This page does not exist', 404 - - You can also register handlers for arbitrary exceptions:: - - @app.errorhandler(DatabaseError) - def special_exception_handler(error): - return 'Database connection failed', 500 - - .. versionadded:: 0.7 - Use :meth:`register_error_handler` instead of modifying - :attr:`error_handler_spec` directly, for application wide error - handlers. - - .. versionadded:: 0.7 - One can now additionally also register custom exception types - that do not necessarily have to be a subclass of the - :class:`~werkzeug.exceptions.HTTPException` class. - - :param code_or_exception: the code as integer for the handler, or - an arbitrary exception - """ - - def decorator(f): - self._register_error_handler(None, code_or_exception, f) - return f - - return decorator - - @setupmethod - def register_error_handler(self, code_or_exception, f): - """Alternative error attach function to the :meth:`errorhandler` - decorator that is more straightforward to use for non decorator - usage. - - .. versionadded:: 0.7 - """ - self._register_error_handler(None, code_or_exception, f) - - @setupmethod - def _register_error_handler(self, key, code_or_exception, f): - """ - :type key: None|str - :type code_or_exception: int|T<=Exception - :type f: callable - """ - if isinstance(code_or_exception, HTTPException): # old broken behavior - raise ValueError( - "Tried to register a handler for an exception instance {0!r}." - " Handlers can only be registered for exception classes or" - " HTTP error codes.".format(code_or_exception) - ) - - try: - exc_class, code = self._get_exc_class_and_code(code_or_exception) - except KeyError: - raise KeyError( - "'{0}' is not a recognized HTTP error code. Use a subclass of" - " HTTPException with that code instead.".format(code_or_exception) - ) - - handlers = self.error_handler_spec.setdefault(key, {}).setdefault(code, {}) - handlers[exc_class] = f - - @setupmethod - def template_filter(self, name=None): + def template_filter( + self, name: t.Optional[str] = None + ) -> t.Callable[[TemplateFilterCallable], TemplateFilterCallable]: """A decorator that is used to register custom template filter. You can specify a name for the filter, otherwise the function name will be used. Example:: @@ -1441,14 +1109,16 @@ class Flask(_PackageBoundObject): function name will be used. """ - def decorator(f): + def decorator(f: TemplateFilterCallable) -> TemplateFilterCallable: self.add_template_filter(f, name=name) return f return decorator @setupmethod - def add_template_filter(self, f, name=None): + def add_template_filter( + self, f: TemplateFilterCallable, name: t.Optional[str] = None + ) -> None: """Register a custom template filter. Works exactly like the :meth:`template_filter` decorator. @@ -1458,7 +1128,9 @@ class Flask(_PackageBoundObject): self.jinja_env.filters[name or f.__name__] = f @setupmethod - def template_test(self, name=None): + def template_test( + self, name: t.Optional[str] = None + ) -> t.Callable[[TemplateTestCallable], TemplateTestCallable]: """A decorator that is used to register custom template test. You can specify a name for the test, otherwise the function name will be used. Example:: @@ -1478,14 +1150,16 @@ class Flask(_PackageBoundObject): function name will be used. """ - def decorator(f): + def decorator(f: TemplateTestCallable) -> TemplateTestCallable: self.add_template_test(f, name=name) return f return decorator @setupmethod - def add_template_test(self, f, name=None): + def add_template_test( + self, f: TemplateTestCallable, name: t.Optional[str] = None + ) -> None: """Register a custom template test. Works exactly like the :meth:`template_test` decorator. @@ -1497,7 +1171,9 @@ class Flask(_PackageBoundObject): self.jinja_env.tests[name or f.__name__] = f @setupmethod - def template_global(self, name=None): + def template_global( + self, name: t.Optional[str] = None + ) -> t.Callable[[TemplateGlobalCallable], TemplateGlobalCallable]: """A decorator that is used to register a custom template global function. You can specify a name for the global function, otherwise the function name will be used. Example:: @@ -1512,14 +1188,16 @@ class Flask(_PackageBoundObject): function name will be used. """ - def decorator(f): + def decorator(f: TemplateGlobalCallable) -> TemplateGlobalCallable: self.add_template_global(f, name=name) return f return decorator @setupmethod - def add_template_global(self, f, name=None): + def add_template_global( + self, f: TemplateGlobalCallable, name: t.Optional[str] = None + ) -> None: """Register a custom template global function. Works exactly like the :meth:`template_global` decorator. @@ -1531,21 +1209,9 @@ class Flask(_PackageBoundObject): self.jinja_env.globals[name or f.__name__] = f @setupmethod - def before_request(self, f): - """Registers a function to run before each request. - - For example, this can be used to open a database connection, or to load - the logged in user from the session. - - The function will be called without any arguments. If it returns a - non-None value, the value is handled as if it was the return value from - the view, and further request handling is stopped. - """ - self.before_request_funcs.setdefault(None, []).append(f) - return f - - @setupmethod - def before_first_request(self, f): + def before_first_request( + self, f: BeforeFirstRequestCallable + ) -> BeforeFirstRequestCallable: """Registers a function to be run before the first request to this instance of the application. @@ -1558,60 +1224,7 @@ class Flask(_PackageBoundObject): return f @setupmethod - def after_request(self, f): - """Register a function to be run after each request. - - Your function must take one parameter, an instance of - :attr:`response_class` and return a new response object or the - same (see :meth:`process_response`). - - As of Flask 0.7 this function might not be executed at the end of the - request in case an unhandled exception occurred. - """ - self.after_request_funcs.setdefault(None, []).append(f) - return f - - @setupmethod - def teardown_request(self, f): - """Register a function to be run at the end of each request, - regardless of whether there was an exception or not. These functions - are executed when the request context is popped, even if not an - actual request was performed. - - Example:: - - ctx = app.test_request_context() - ctx.push() - ... - ctx.pop() - - When ``ctx.pop()`` is executed in the above example, the teardown - functions are called just before the request context moves from the - stack of active contexts. This becomes relevant if you are using - such constructs in tests. - - Generally teardown functions must take every necessary step to avoid - that they will fail. If they do execute code that might fail they - will have to surround the execution of these code by try/except - statements and log occurring errors. - - When a teardown function was called because of an exception it will - be passed an error object. - - The return values of teardown functions are ignored. - - .. admonition:: Debug Note - - In debug mode Flask will not tear down a request on an exception - immediately. Instead it will keep it alive so that the interactive - debugger can still access it. This behavior can be controlled - by the ``PRESERVE_CONTEXT_ON_EXCEPTION`` configuration variable. - """ - self.teardown_request_funcs.setdefault(None, []).append(f) - return f - - @setupmethod - def teardown_appcontext(self, f): + def teardown_appcontext(self, f: TeardownCallable) -> TeardownCallable: """Registers a function to be called when the application context ends. These functions are typically also called when the request context is popped. @@ -1644,13 +1257,7 @@ class Flask(_PackageBoundObject): return f @setupmethod - def context_processor(self, f): - """Registers a template context processor function.""" - self.template_context_processors[None].append(f) - return f - - @setupmethod - def shell_context_processor(self, f): + def shell_context_processor(self, f: t.Callable) -> t.Callable: """Registers a shell context processor function. .. versionadded:: 0.11 @@ -1658,58 +1265,34 @@ class Flask(_PackageBoundObject): self.shell_context_processors.append(f) return f - @setupmethod - def url_value_preprocessor(self, f): - """Register a URL value preprocessor function for all view - functions in the application. These functions will be called before the - :meth:`before_request` functions. - - The function can modify the values captured from the matched url before - they are passed to the view. For example, this can be used to pop a - common language code value and place it in ``g`` rather than pass it to - every view. - - The function is passed the endpoint name and values dict. The return - value is ignored. - """ - self.url_value_preprocessors.setdefault(None, []).append(f) - return f - - @setupmethod - def url_defaults(self, f): - """Callback function for URL defaults for all view functions of the - application. It's called with the endpoint and values and should - update the values passed in place. - """ - self.url_default_functions.setdefault(None, []).append(f) - return f - - def _find_error_handler(self, e): + def _find_error_handler( + self, e: Exception + ) -> t.Optional["ErrorHandlerCallable[Exception]"]: """Return a registered error handler for an exception in this order: blueprint handler for a specific code, app handler for a specific code, blueprint handler for an exception class, app handler for an exception class, or ``None`` if a suitable handler is not found. """ exc_class, code = self._get_exc_class_and_code(type(e)) + names = (*request.blueprints, None) - for name, c in ( - (request.blueprint, code), - (None, code), - (request.blueprint, None), - (None, None), - ): - handler_map = self.error_handler_spec.setdefault(name, {}).get(c) + for c in (code, None) if code is not None else (None,): + for name in names: + handler_map = self.error_handler_spec[name][c] - if not handler_map: - continue + if not handler_map: + continue - for cls in exc_class.__mro__: - handler = handler_map.get(cls) + for cls in exc_class.__mro__: + handler = handler_map.get(cls) - if handler is not None: - return handler + if handler is not None: + return handler + return None - def handle_http_exception(self, e): + def handle_http_exception( + self, e: HTTPException + ) -> t.Union[HTTPException, ResponseReturnValue]: """Handles an HTTP exception. By default this will invoke the registered error handlers and fall back to returning the exception as response. @@ -1721,7 +1304,7 @@ class Flask(_PackageBoundObject): .. versionchanged:: 1.0 Exceptions are looked up by code *and* by MRO, so - ``HTTPExcpetion`` subclasses can be handled with a catch-all + ``HTTPException`` subclasses can be handled with a catch-all handler for the base ``HTTPException``. .. versionadded:: 0.3 @@ -1740,9 +1323,9 @@ class Flask(_PackageBoundObject): handler = self._find_error_handler(e) if handler is None: return e - return handler(e) + return self.ensure_sync(handler)(e) - def trap_http_exception(self, e): + def trap_http_exception(self, e: Exception) -> bool: """Checks if an HTTP exception should be trapped or not. By default this will return ``False`` for all exceptions except for a bad request key error if ``TRAP_BAD_REQUEST_ERRORS`` is set to ``True``. It @@ -1777,7 +1360,9 @@ class Flask(_PackageBoundObject): return False - def handle_user_exception(self, e): + def handle_user_exception( + self, e: Exception + ) -> t.Union[HTTPException, ResponseReturnValue]: """This method is called whenever an exception occurs that should be handled. A special case is :class:`~werkzeug .exceptions.HTTPException` which is forwarded to the @@ -1792,24 +1377,10 @@ class Flask(_PackageBoundObject): .. versionadded:: 0.7 """ - exc_type, exc_value, tb = sys.exc_info() - assert exc_value is e - # ensure not to trash sys.exc_info() at that point in case someone - # wants the traceback preserved in handle_http_exception. Of course - # we cannot prevent users from trashing it themselves in a custom - # trap_http_exception method so that's their fault then. - - if isinstance(e, BadRequestKeyError): - if self.debug or self.config["TRAP_BAD_REQUEST_ERRORS"]: - e.show_exception = True - - # Werkzeug < 0.15 doesn't add the KeyError to the 400 - # message, add it in manually. - # TODO: clean up once Werkzeug >= 0.15.5 is required - if e.args[0] not in e.get_description(): - e.description = "KeyError: '{}'".format(*e.args) - elif not hasattr(BadRequestKeyError, "show_exception"): - e.args = () + if isinstance(e, BadRequestKeyError) and ( + self.debug or self.config["TRAP_BAD_REQUEST_ERRORS"] + ): + e.show_exception = True if isinstance(e, HTTPException) and not self.trap_http_exception(e): return self.handle_http_exception(e) @@ -1817,10 +1388,11 @@ class Flask(_PackageBoundObject): handler = self._find_error_handler(e) if handler is None: - reraise(exc_type, exc_value, tb) - return handler(e) + raise - def handle_exception(self, e): + return self.ensure_sync(handler)(e) + + def handle_exception(self, e: Exception) -> Response: """Handle an exception that did not have an error handler associated with it, or that was raised from an error handler. This always causes a 500 ``InternalServerError``. @@ -1837,12 +1409,6 @@ class Flask(_PackageBoundObject): always receive the ``InternalServerError``. The original unhandled exception is available as ``e.original_exception``. - .. note:: - Prior to Werkzeug 1.0.0, ``InternalServerError`` will not - always have an ``original_exception`` attribute. Use - ``getattr(e, "original_exception", None)`` to simulate the - behavior for compatibility. - .. versionchanged:: 1.1.0 Always passes the ``InternalServerError`` instance to the handler, setting ``original_exception`` to the unhandled @@ -1854,32 +1420,33 @@ class Flask(_PackageBoundObject): .. versionadded:: 0.3 """ - exc_type, exc_value, tb = sys.exc_info() + exc_info = sys.exc_info() got_request_exception.send(self, exception=e) if self.propagate_exceptions: - # if we want to repropagate the exception, we can attempt to - # raise it with the whole traceback in case we can do that - # (the function was actually called from the except part) - # otherwise, we just raise the error again - if exc_value is e: - reraise(exc_type, exc_value, tb) - else: - raise e + # Re-raise if called with an active exception, otherwise + # raise the passed in exception. + if exc_info[1] is e: + raise - self.log_exception((exc_type, exc_value, tb)) - server_error = InternalServerError() - # TODO: pass as param when Werkzeug>=1.0.0 is required - # TODO: also remove note about this from docstring and docs - server_error.original_exception = e + raise e + + self.log_exception(exc_info) + server_error: t.Union[InternalServerError, ResponseReturnValue] + server_error = InternalServerError(original_exception=e) handler = self._find_error_handler(server_error) if handler is not None: - server_error = handler(server_error) + server_error = self.ensure_sync(handler)(server_error) return self.finalize_request(server_error, from_error_handler=True) - def log_exception(self, exc_info): + def log_exception( + self, + exc_info: t.Union[ + t.Tuple[type, BaseException, TracebackType], t.Tuple[None, None, None] + ], + ) -> None: """Logs an exception. This is called by :meth:`handle_exception` if debugging is disabled and right before the handler is called. The default implementation logs the exception as error on the @@ -1888,10 +1455,10 @@ class Flask(_PackageBoundObject): .. versionadded:: 0.8 """ self.logger.error( - "Exception on %s [%s]" % (request.path, request.method), exc_info=exc_info + f"Exception on {request.path} [{request.method}]", exc_info=exc_info ) - def raise_routing_exception(self, request): + def raise_routing_exception(self, request: Request) -> "te.NoReturn": """Exceptions that are recording during routing are reraised with this method. During debug we are not reraising redirect requests for non ``GET``, ``HEAD``, or ``OPTIONS`` requests and we're raising @@ -1904,13 +1471,13 @@ class Flask(_PackageBoundObject): or not isinstance(request.routing_exception, RequestRedirect) or request.method in ("GET", "HEAD", "OPTIONS") ): - raise request.routing_exception + raise request.routing_exception # type: ignore from .debughelpers import FormDataRoutingRedirect raise FormDataRoutingRedirect(request) - def dispatch_request(self): + def dispatch_request(self) -> ResponseReturnValue: """Does the request dispatching. Matches the URL and returns the return value of the view or error handler. This does not have to be a response object. In order to convert the return value to a @@ -1932,9 +1499,9 @@ class Flask(_PackageBoundObject): ): return self.make_default_options_response() # otherwise dispatch to the handler for that endpoint - return self.view_functions[rule.endpoint](**req.view_args) + return self.ensure_sync(self.view_functions[rule.endpoint])(**req.view_args) - def full_dispatch_request(self): + def full_dispatch_request(self) -> Response: """Dispatches the request and on top of that performs request pre and postprocessing as well as HTTP exception catching and error handling. @@ -1951,7 +1518,11 @@ class Flask(_PackageBoundObject): rv = self.handle_user_exception(e) return self.finalize_request(rv) - def finalize_request(self, rv, from_error_handler=False): + def finalize_request( + self, + rv: t.Union[ResponseReturnValue, HTTPException], + from_error_handler: bool = False, + ) -> Response: """Given the return value from a view function this finalizes the request by converting it into a response and invoking the postprocessing functions. This is invoked for both normal @@ -1976,7 +1547,7 @@ class Flask(_PackageBoundObject): ) return response - def try_trigger_before_first_request_functions(self): + def try_trigger_before_first_request_functions(self) -> None: """Called before each request and will ensure that it triggers the :attr:`before_first_request_funcs` and only exactly once per application instance (which means process usually). @@ -1989,10 +1560,10 @@ class Flask(_PackageBoundObject): if self._got_first_request: return for func in self.before_first_request_funcs: - func() + self.ensure_sync(func)() self._got_first_request = True - def make_default_options_response(self): + def make_default_options_response(self) -> Response: """This method is called to create the default ``OPTIONS`` response. This can be changed through subclassing to change the default behavior of ``OPTIONS`` responses. @@ -2000,22 +1571,12 @@ class Flask(_PackageBoundObject): .. versionadded:: 0.7 """ adapter = _request_ctx_stack.top.url_adapter - if hasattr(adapter, "allowed_methods"): - methods = adapter.allowed_methods() - else: - # fallback for Werkzeug < 0.7 - methods = [] - try: - adapter.match(method="--") - except MethodNotAllowed as e: - methods = e.valid_methods - except HTTPException: - pass + methods = adapter.allowed_methods() rv = self.response_class() rv.allow.update(methods) return rv - def should_ignore_error(self, error): + def should_ignore_error(self, error: t.Optional[BaseException]) -> bool: """This is called to figure out if an error should be ignored or not as far as the teardown system is concerned. If this function returns ``True`` then the teardown handlers will not be @@ -2025,7 +1586,51 @@ class Flask(_PackageBoundObject): """ return False - def make_response(self, rv): + def ensure_sync(self, func: t.Callable) -> t.Callable: + """Ensure that the function is synchronous for WSGI workers. + Plain ``def`` functions are returned as-is. ``async def`` + functions are wrapped to run and wait for the response. + + Override this method to change how the app runs async views. + + .. versionadded:: 2.0 + """ + if iscoroutinefunction(func): + return self.async_to_sync(func) + + return func + + def async_to_sync( + self, func: t.Callable[..., t.Coroutine] + ) -> t.Callable[..., t.Any]: + """Return a sync function that will run the coroutine function. + + .. code-block:: python + + result = app.async_to_sync(func)(*args, **kwargs) + + Override this method to change how the app converts async code + to be synchronously callable. + + .. versionadded:: 2.0 + """ + try: + from asgiref.sync import async_to_sync as asgiref_async_to_sync + except ImportError: + raise RuntimeError( + "Install Flask with the 'async' extra in order to use async views." + ) from None + + # Check that Werkzeug isn't using its fallback ContextVar class. + if ContextVar.__module__ == "werkzeug.local": + raise RuntimeError( + "Async cannot be used with this combination of Python " + "and Greenlet versions." + ) + + return asgiref_async_to_sync(func) + + def make_response(self, rv: ResponseReturnValue) -> Response: """Convert the return value from a view function to an instance of :attr:`response_class`. @@ -2034,11 +1639,11 @@ class Flask(_PackageBoundObject): without returning, is not allowed. The following types are allowed for ``view_rv``: - ``str`` (``unicode`` in Python 2) + ``str`` A response object is created with the string encoded to UTF-8 as the body. - ``bytes`` (``str`` in Python 2) + ``bytes`` A response object is created with the bytes as the body. ``dict`` @@ -2094,14 +1699,14 @@ class Flask(_PackageBoundObject): # the body must not be None if rv is None: raise TypeError( - "The view function did not return a valid response. The" - " function either returned None or ended without a return" - " statement." + f"The view function for {request.endpoint!r} did not" + " return a valid response. The function either returned" + " None or ended without a return statement." ) # make sure the body is an instance of the response class if not isinstance(rv, self.response_class): - if isinstance(rv, (text_type, bytes, bytearray)): + if isinstance(rv, (str, bytes, bytearray)): # let the response class set the status and headers instead of # waiting to do it manually, so that the class can handle any # special logic @@ -2113,37 +1718,39 @@ class Flask(_PackageBoundObject): # evaluate a WSGI callable, or coerce a different response # class to the correct type try: - rv = self.response_class.force_type(rv, request.environ) + rv = self.response_class.force_type(rv, request.environ) # type: ignore # noqa: B950 except TypeError as e: - new_error = TypeError( - "{e}\nThe view function did not return a valid" - " response. The return type must be a string, dict, tuple," - " Response instance, or WSGI callable, but it was a" - " {rv.__class__.__name__}.".format(e=e, rv=rv) - ) - reraise(TypeError, new_error, sys.exc_info()[2]) + raise TypeError( + f"{e}\nThe view function did not return a valid" + " response. The return type must be a string," + " dict, tuple, Response instance, or WSGI" + f" callable, but it was a {type(rv).__name__}." + ).with_traceback(sys.exc_info()[2]) from None else: raise TypeError( "The view function did not return a valid" - " response. The return type must be a string, dict, tuple," - " Response instance, or WSGI callable, but it was a" - " {rv.__class__.__name__}.".format(rv=rv) + " response. The return type must be a string," + " dict, tuple, Response instance, or WSGI" + f" callable, but it was a {type(rv).__name__}." ) + rv = t.cast(Response, rv) # prefer the status if it was provided if status is not None: - if isinstance(status, (text_type, bytes, bytearray)): - rv.status = status + if isinstance(status, (str, bytes, bytearray)): + rv.status = status # type: ignore else: rv.status_code = status # extend existing headers with provided headers if headers: - rv.headers.extend(headers) + rv.headers.update(headers) return rv - def create_url_adapter(self, request): + def create_url_adapter( + self, request: t.Optional[Request] + ) -> t.Optional[MapAdapter]: """Creates a URL adapter for the given request. The URL adapter is created at a point where the request context is not yet set up so the request is passed explicitly. @@ -2162,11 +1769,11 @@ class Flask(_PackageBoundObject): # If subdomain matching is disabled (the default), use the # default subdomain in all cases. This should be the default # in Werkzeug but it currently does not have that feature. - subdomain = ( - (self.url_map.default_subdomain or None) - if not self.subdomain_matching - else None - ) + if not self.subdomain_matching: + subdomain = self.url_map.default_subdomain or None + else: + subdomain = None + return self.url_map.bind_to_environ( request.environ, server_name=self.config["SERVER_NAME"], @@ -2181,41 +1788,53 @@ class Flask(_PackageBoundObject): url_scheme=self.config["PREFERRED_URL_SCHEME"], ) - def inject_url_defaults(self, endpoint, values): + return None + + def inject_url_defaults(self, endpoint: str, values: dict) -> None: """Injects the URL defaults for the given endpoint directly into the values dictionary passed. This is used internally and automatically called on URL building. .. versionadded:: 0.7 """ - funcs = self.url_default_functions.get(None, ()) - if "." in endpoint: - bp = endpoint.rsplit(".", 1)[0] - funcs = chain(funcs, self.url_default_functions.get(bp, ())) - for func in funcs: - func(endpoint, values) + names: t.Iterable[t.Optional[str]] = (None,) - def handle_url_build_error(self, error, endpoint, values): - """Handle :class:`~werkzeug.routing.BuildError` on :meth:`url_for`. + # url_for may be called outside a request context, parse the + # passed endpoint instead of using request.blueprints. + if "." in endpoint: + names = chain( + names, reversed(_split_blueprint_path(endpoint.rpartition(".")[0])) + ) + + for name in names: + if name in self.url_default_functions: + for func in self.url_default_functions[name]: + func(endpoint, values) + + def handle_url_build_error( + self, error: Exception, endpoint: str, values: dict + ) -> str: + """Handle :class:`~werkzeug.routing.BuildError` on + :meth:`url_for`. """ - exc_type, exc_value, tb = sys.exc_info() for handler in self.url_build_error_handlers: try: rv = handler(error, endpoint, values) + except BuildError as e: + # make error available outside except block + error = e + else: if rv is not None: return rv - except BuildError as e: - # make error available outside except block (py3) - error = e - # At this point we want to reraise the exception. If the error is - # still the same one we can reraise it with the original traceback, - # otherwise we raise it from here. - if error is exc_value: - reraise(exc_type, exc_value, tb) + # Re-raise if called with an active exception, otherwise raise + # the passed in exception. + if error is sys.exc_info()[1]: + raise + raise error - def preprocess_request(self): + def preprocess_request(self) -> t.Optional[ResponseReturnValue]: """Called before the request is dispatched. Calls :attr:`url_value_preprocessors` registered with the app and the current blueprint (if any). Then calls :attr:`before_request_funcs` @@ -2225,24 +1844,24 @@ class Flask(_PackageBoundObject): value is handled as if it was the return value from the view, and further request handling is stopped. """ + names = (None, *reversed(request.blueprints)) - bp = _request_ctx_stack.top.request.blueprint + for name in names: + if name in self.url_value_preprocessors: + for url_func in self.url_value_preprocessors[name]: + url_func(request.endpoint, request.view_args) - funcs = self.url_value_preprocessors.get(None, ()) - if bp is not None and bp in self.url_value_preprocessors: - funcs = chain(funcs, self.url_value_preprocessors[bp]) - for func in funcs: - func(request.endpoint, request.view_args) + for name in names: + if name in self.before_request_funcs: + for before_func in self.before_request_funcs[name]: + rv = self.ensure_sync(before_func)() - funcs = self.before_request_funcs.get(None, ()) - if bp is not None and bp in self.before_request_funcs: - funcs = chain(funcs, self.before_request_funcs[bp]) - for func in funcs: - rv = func() - if rv is not None: - return rv + if rv is not None: + return rv - def process_response(self, response): + return None + + def process_response(self, response: Response) -> Response: """Can be overridden in order to modify the response object before it's sent to the WSGI server. By default this will call all the :meth:`after_request` decorated functions. @@ -2256,19 +1875,23 @@ class Flask(_PackageBoundObject): instance of :attr:`response_class`. """ ctx = _request_ctx_stack.top - bp = ctx.request.blueprint - funcs = ctx._after_request_functions - if bp is not None and bp in self.after_request_funcs: - funcs = chain(funcs, reversed(self.after_request_funcs[bp])) - if None in self.after_request_funcs: - funcs = chain(funcs, reversed(self.after_request_funcs[None])) - for handler in funcs: - response = handler(response) + + for func in ctx._after_request_functions: + response = self.ensure_sync(func)(response) + + for name in chain(request.blueprints, (None,)): + if name in self.after_request_funcs: + for func in reversed(self.after_request_funcs[name]): + response = self.ensure_sync(func)(response) + if not self.session_interface.is_null_session(ctx.session): self.session_interface.save_session(self, ctx.session, response) + return response - def do_teardown_request(self, exc=_sentinel): + def do_teardown_request( + self, exc: t.Optional[BaseException] = _sentinel # type: ignore + ) -> None: """Called after the request is dispatched and the response is returned, right before the request context is popped. @@ -2291,15 +1914,17 @@ class Flask(_PackageBoundObject): """ if exc is _sentinel: exc = sys.exc_info()[1] - funcs = reversed(self.teardown_request_funcs.get(None, ())) - bp = _request_ctx_stack.top.request.blueprint - if bp is not None and bp in self.teardown_request_funcs: - funcs = chain(funcs, reversed(self.teardown_request_funcs[bp])) - for func in funcs: - func(exc) + + for name in chain(request.blueprints, (None,)): + if name in self.teardown_request_funcs: + for func in reversed(self.teardown_request_funcs[name]): + self.ensure_sync(func)(exc) + request_tearing_down.send(self, exc=exc) - def do_teardown_appcontext(self, exc=_sentinel): + def do_teardown_appcontext( + self, exc: t.Optional[BaseException] = _sentinel # type: ignore + ) -> None: """Called right before the application context is popped. When handling a request, the application context is popped @@ -2316,11 +1941,13 @@ class Flask(_PackageBoundObject): """ if exc is _sentinel: exc = sys.exc_info()[1] + for func in reversed(self.teardown_appcontext_funcs): - func(exc) + self.ensure_sync(func)(exc) + appcontext_tearing_down.send(self, exc=exc) - def app_context(self): + def app_context(self) -> AppContext: """Create an :class:`~flask.ctx.AppContext`. Use as a ``with`` block to push the context, which will make :data:`current_app` point at this application. @@ -2341,7 +1968,7 @@ class Flask(_PackageBoundObject): """ return AppContext(self) - def request_context(self, environ): + def request_context(self, environ: dict) -> RequestContext: """Create a :class:`~flask.ctx.RequestContext` representing a WSGI environment. Use a ``with`` block to push the context, which will make :data:`request` point at this request. @@ -2357,7 +1984,7 @@ class Flask(_PackageBoundObject): """ return RequestContext(self, environ) - def test_request_context(self, *args, **kwargs): + def test_request_context(self, *args: t.Any, **kwargs: t.Any) -> RequestContext: """Create a :class:`~flask.ctx.RequestContext` for a WSGI environment created from the given values. This is mostly useful during testing, where you may want to run a function that uses @@ -2413,7 +2040,7 @@ class Flask(_PackageBoundObject): finally: builder.close() - def wsgi_app(self, environ, start_response): + def wsgi_app(self, environ: dict, start_response: t.Callable) -> t.Any: """The actual WSGI application. This is not implemented in :meth:`__call__` so that middlewares can be applied without losing a reference to the app object. Instead of doing this:: @@ -2439,7 +2066,7 @@ class Flask(_PackageBoundObject): start the response. """ ctx = self.request_context(environ) - error = None + error: t.Optional[BaseException] = None try: try: ctx.push() @@ -2456,11 +2083,9 @@ class Flask(_PackageBoundObject): error = None ctx.auto_pop(error) - def __call__(self, environ, start_response): + def __call__(self, environ: dict, start_response: t.Callable) -> t.Any: """The WSGI server calls the Flask application object as the - WSGI application. This calls :meth:`wsgi_app` which can be - wrapped to applying middleware.""" + WSGI application. This calls :meth:`wsgi_app`, which can be + wrapped to apply middleware. + """ return self.wsgi_app(environ, start_response) - - def __repr__(self): - return "<%s %r>" % (self.__class__.__name__, self.name) diff --git a/libs/flask/blueprints.py b/libs/flask/blueprints.py index 8978104d2..5c23a735c 100644 --- a/libs/flask/blueprints.py +++ b/libs/flask/blueprints.py @@ -1,31 +1,43 @@ -# -*- coding: utf-8 -*- -""" - flask.blueprints - ~~~~~~~~~~~~~~~~ - - Blueprints are the recommended way to implement larger or more - pluggable applications in Flask 0.7 and later. - - :copyright: 2010 Pallets - :license: BSD-3-Clause -""" +import os +import typing as t +from collections import defaultdict from functools import update_wrapper -from .helpers import _endpoint_from_view_func -from .helpers import _PackageBoundObject +from .scaffold import _endpoint_from_view_func +from .scaffold import _sentinel +from .scaffold import Scaffold +from .typing import AfterRequestCallable +from .typing import BeforeFirstRequestCallable +from .typing import BeforeRequestCallable +from .typing import TeardownCallable +from .typing import TemplateContextProcessorCallable +from .typing import TemplateFilterCallable +from .typing import TemplateGlobalCallable +from .typing import TemplateTestCallable +from .typing import URLDefaultCallable +from .typing import URLValuePreprocessorCallable -# a singleton sentinel value for parameter defaults -_sentinel = object() +if t.TYPE_CHECKING: + from .app import Flask + from .typing import ErrorHandlerCallable + +DeferredSetupFunction = t.Callable[["BlueprintSetupState"], t.Callable] -class BlueprintSetupState(object): +class BlueprintSetupState: """Temporary holder object for registering a blueprint with the application. An instance of this class is created by the :meth:`~flask.Blueprint.make_setup_state` method and later passed to all register callback functions. """ - def __init__(self, blueprint, app, options, first_registration): + def __init__( + self, + blueprint: "Blueprint", + app: "Flask", + options: t.Any, + first_registration: bool, + ) -> None: #: a reference to the current application self.app = app @@ -57,12 +69,21 @@ class BlueprintSetupState(object): #: blueprint. self.url_prefix = url_prefix + self.name = self.options.get("name", blueprint.name) + self.name_prefix = self.options.get("name_prefix", "") + #: A dictionary with URL defaults that is added to each and every #: URL that was defined with the blueprint. self.url_defaults = dict(self.blueprint.url_values_defaults) self.url_defaults.update(self.options.get("url_defaults", ())) - def add_url_rule(self, rule, endpoint=None, view_func=None, **options): + def add_url_rule( + self, + rule: str, + endpoint: t.Optional[str] = None, + view_func: t.Optional[t.Callable] = None, + **options: t.Any, + ) -> None: """A helper method to register a rule (and optionally a view function) to the application. The endpoint is automatically prefixed with the blueprint's name. @@ -74,20 +95,21 @@ class BlueprintSetupState(object): rule = self.url_prefix options.setdefault("subdomain", self.subdomain) if endpoint is None: - endpoint = _endpoint_from_view_func(view_func) + endpoint = _endpoint_from_view_func(view_func) # type: ignore defaults = self.url_defaults if "defaults" in options: defaults = dict(defaults, **options.pop("defaults")) + self.app.add_url_rule( rule, - "%s.%s" % (self.blueprint.name, endpoint), + f"{self.name_prefix}.{self.name}.{endpoint}".lstrip("."), view_func, defaults=defaults, - **options + **options, ) -class Blueprint(_PackageBoundObject): +class Blueprint(Scaffold): """Represents a blueprint, a collection of routes and other app-related functions that can be registered on a real application later. @@ -101,14 +123,7 @@ class Blueprint(_PackageBoundObject): that is called with :class:`~flask.blueprints.BlueprintSetupState` when the blueprint is registered on an application. - See :ref:`blueprints` for more information. - - .. versionchanged:: 1.1.0 - Blueprints have a ``cli`` group to register nested CLI commands. - The ``cli_group`` parameter controls the name of the group under - the ``flask`` command. - - .. versionadded:: 0.7 + See :doc:`/blueprints` for more information. :param name: The name of the blueprint. Will be prepended to each endpoint name. @@ -134,65 +149,69 @@ class Blueprint(_PackageBoundObject): default. :param url_defaults: A dict of default values that blueprint routes will receive by default. - :param root_path: By default, the blueprint will automatically this - based on ``import_name``. In certain situations this automatic - detection can fail, so the path can be specified manually - instead. + :param root_path: By default, the blueprint will automatically set + this based on ``import_name``. In certain situations this + automatic detection can fail, so the path can be specified + manually instead. + + .. versionchanged:: 1.1.0 + Blueprints have a ``cli`` group to register nested CLI commands. + The ``cli_group`` parameter controls the name of the group under + the ``flask`` command. + + .. versionadded:: 0.7 """ warn_on_modifications = False _got_registered_once = False - #: Blueprint local JSON decoder class to use. - #: Set to ``None`` to use the app's :class:`~flask.app.Flask.json_encoder`. + #: Blueprint local JSON encoder class to use. Set to ``None`` to use + #: the app's :class:`~flask.Flask.json_encoder`. json_encoder = None - #: Blueprint local JSON decoder class to use. - #: Set to ``None`` to use the app's :class:`~flask.app.Flask.json_decoder`. + #: Blueprint local JSON decoder class to use. Set to ``None`` to use + #: the app's :class:`~flask.Flask.json_decoder`. json_decoder = None - # TODO remove the next three attrs when Sphinx :inherited-members: works - # https://github.com/sphinx-doc/sphinx/issues/741 - - #: The name of the package or module that this app belongs to. Do not - #: change this once it is set by the constructor. - import_name = None - - #: Location of the template files to be added to the template lookup. - #: ``None`` if templates should not be added. - template_folder = None - - #: Absolute path to the package on the filesystem. Used to look up - #: resources contained in the package. - root_path = None - def __init__( self, - name, - import_name, - static_folder=None, - static_url_path=None, - template_folder=None, - url_prefix=None, - subdomain=None, - url_defaults=None, - root_path=None, - cli_group=_sentinel, + name: str, + import_name: str, + static_folder: t.Optional[t.Union[str, os.PathLike]] = None, + static_url_path: t.Optional[str] = None, + template_folder: t.Optional[str] = None, + url_prefix: t.Optional[str] = None, + subdomain: t.Optional[str] = None, + url_defaults: t.Optional[dict] = None, + root_path: t.Optional[str] = None, + cli_group: t.Optional[str] = _sentinel, # type: ignore ): - _PackageBoundObject.__init__( - self, import_name, template_folder, root_path=root_path + super().__init__( + import_name=import_name, + static_folder=static_folder, + static_url_path=static_url_path, + template_folder=template_folder, + root_path=root_path, ) + + if "." in name: + raise ValueError("'name' may not contain a dot '.' character.") + self.name = name self.url_prefix = url_prefix self.subdomain = subdomain - self.static_folder = static_folder - self.static_url_path = static_url_path - self.deferred_functions = [] + self.deferred_functions: t.List[DeferredSetupFunction] = [] + if url_defaults is None: url_defaults = {} + self.url_values_defaults = url_defaults self.cli_group = cli_group + self._blueprints: t.List[t.Tuple["Blueprint", dict]] = [] - def record(self, func): + def _is_setup_finished(self) -> bool: + return self.warn_on_modifications and self._got_registered_once + + def record(self, func: t.Callable) -> None: """Registers a function that is called when the blueprint is registered on the application. This function is called with the state as argument as returned by the :meth:`make_setup_state` @@ -203,114 +222,214 @@ class Blueprint(_PackageBoundObject): warn( Warning( - "The blueprint was already registered once " - "but is getting modified now. These changes " - "will not show up." + "The blueprint was already registered once but is" + " getting modified now. These changes will not show" + " up." ) ) self.deferred_functions.append(func) - def record_once(self, func): + def record_once(self, func: t.Callable) -> None: """Works like :meth:`record` but wraps the function in another function that will ensure the function is only called once. If the blueprint is registered a second time on the application, the function passed is not called. """ - def wrapper(state): + def wrapper(state: BlueprintSetupState) -> None: if state.first_registration: func(state) return self.record(update_wrapper(wrapper, func)) - def make_setup_state(self, app, options, first_registration=False): + def make_setup_state( + self, app: "Flask", options: dict, first_registration: bool = False + ) -> BlueprintSetupState: """Creates an instance of :meth:`~flask.blueprints.BlueprintSetupState` object that is later passed to the register callback functions. Subclasses can override this to return a subclass of the setup state. """ return BlueprintSetupState(self, app, options, first_registration) - def register(self, app, options, first_registration=False): - """Called by :meth:`Flask.register_blueprint` to register all views - and callbacks registered on the blueprint with the application. Creates - a :class:`.BlueprintSetupState` and calls each :meth:`record` callback - with it. + def register_blueprint(self, blueprint: "Blueprint", **options: t.Any) -> None: + """Register a :class:`~flask.Blueprint` on this blueprint. Keyword + arguments passed to this method will override the defaults set + on the blueprint. - :param app: The application this blueprint is being registered with. + .. versionchanged:: 2.0.1 + The ``name`` option can be used to change the (pre-dotted) + name the blueprint is registered with. This allows the same + blueprint to be registered multiple times with unique names + for ``url_for``. + + .. versionadded:: 2.0 + """ + if blueprint is self: + raise ValueError("Cannot register a blueprint on itself") + self._blueprints.append((blueprint, options)) + + def register(self, app: "Flask", options: dict) -> None: + """Called by :meth:`Flask.register_blueprint` to register all + views and callbacks registered on the blueprint with the + application. Creates a :class:`.BlueprintSetupState` and calls + each :meth:`record` callback with it. + + :param app: The application this blueprint is being registered + with. :param options: Keyword arguments forwarded from :meth:`~Flask.register_blueprint`. - :param first_registration: Whether this is the first time this - blueprint has been registered on the application. + + .. versionchanged:: 2.0.1 + Nested blueprints are registered with their dotted name. + This allows different blueprints with the same name to be + nested at different locations. + + .. versionchanged:: 2.0.1 + The ``name`` option can be used to change the (pre-dotted) + name the blueprint is registered with. This allows the same + blueprint to be registered multiple times with unique names + for ``url_for``. + + .. versionchanged:: 2.0.1 + Registering the same blueprint with the same name multiple + times is deprecated and will become an error in Flask 2.1. """ + name_prefix = options.get("name_prefix", "") + self_name = options.get("name", self.name) + name = f"{name_prefix}.{self_name}".lstrip(".") + + if name in app.blueprints: + existing_at = f" '{name}'" if self_name != name else "" + + if app.blueprints[name] is not self: + raise ValueError( + f"The name '{self_name}' is already registered for" + f" a different blueprint{existing_at}. Use 'name='" + " to provide a unique name." + ) + else: + import warnings + + warnings.warn( + f"The name '{self_name}' is already registered for" + f" this blueprint{existing_at}. Use 'name=' to" + " provide a unique name. This will become an error" + " in Flask 2.1.", + stacklevel=4, + ) + + first_bp_registration = not any(bp is self for bp in app.blueprints.values()) + first_name_registration = name not in app.blueprints + + app.blueprints[name] = self self._got_registered_once = True - state = self.make_setup_state(app, options, first_registration) + state = self.make_setup_state(app, options, first_bp_registration) if self.has_static_folder: state.add_url_rule( - self.static_url_path + "/", + f"{self.static_url_path}/", view_func=self.send_static_file, endpoint="static", ) + # Merge blueprint data into parent. + if first_bp_registration or first_name_registration: + + def extend(bp_dict, parent_dict): + for key, values in bp_dict.items(): + key = name if key is None else f"{name}.{key}" + parent_dict[key].extend(values) + + for key, value in self.error_handler_spec.items(): + key = name if key is None else f"{name}.{key}" + value = defaultdict( + dict, + { + code: { + exc_class: func for exc_class, func in code_values.items() + } + for code, code_values in value.items() + }, + ) + app.error_handler_spec[key] = value + + for endpoint, func in self.view_functions.items(): + app.view_functions[endpoint] = func + + extend(self.before_request_funcs, app.before_request_funcs) + extend(self.after_request_funcs, app.after_request_funcs) + extend( + self.teardown_request_funcs, + app.teardown_request_funcs, + ) + extend(self.url_default_functions, app.url_default_functions) + extend(self.url_value_preprocessors, app.url_value_preprocessors) + extend(self.template_context_processors, app.template_context_processors) + for deferred in self.deferred_functions: deferred(state) cli_resolved_group = options.get("cli_group", self.cli_group) - if not self.cli.commands: - return + if self.cli.commands: + if cli_resolved_group is None: + app.cli.commands.update(self.cli.commands) + elif cli_resolved_group is _sentinel: + self.cli.name = name + app.cli.add_command(self.cli) + else: + self.cli.name = cli_resolved_group + app.cli.add_command(self.cli) - if cli_resolved_group is None: - app.cli.commands.update(self.cli.commands) - elif cli_resolved_group is _sentinel: - self.cli.name = self.name - app.cli.add_command(self.cli) - else: - self.cli.name = cli_resolved_group - app.cli.add_command(self.cli) + for blueprint, bp_options in self._blueprints: + bp_options = bp_options.copy() + bp_url_prefix = bp_options.get("url_prefix") - def route(self, rule, **options): - """Like :meth:`Flask.route` but for a blueprint. The endpoint for the - :func:`url_for` function is prefixed with the name of the blueprint. - """ + if bp_url_prefix is None: + bp_url_prefix = blueprint.url_prefix - def decorator(f): - endpoint = options.pop("endpoint", f.__name__) - self.add_url_rule(rule, endpoint, f, **options) - return f + if state.url_prefix is not None and bp_url_prefix is not None: + bp_options["url_prefix"] = ( + state.url_prefix.rstrip("/") + "/" + bp_url_prefix.lstrip("/") + ) + elif bp_url_prefix is not None: + bp_options["url_prefix"] = bp_url_prefix + elif state.url_prefix is not None: + bp_options["url_prefix"] = state.url_prefix - return decorator + bp_options["name_prefix"] = name + blueprint.register(app, bp_options) - def add_url_rule(self, rule, endpoint=None, view_func=None, **options): + def add_url_rule( + self, + rule: str, + endpoint: t.Optional[str] = None, + view_func: t.Optional[t.Callable] = None, + provide_automatic_options: t.Optional[bool] = None, + **options: t.Any, + ) -> None: """Like :meth:`Flask.add_url_rule` but for a blueprint. The endpoint for the :func:`url_for` function is prefixed with the name of the blueprint. """ - if endpoint: - assert "." not in endpoint, "Blueprint endpoints should not contain dots" - if view_func and hasattr(view_func, "__name__"): - assert ( - "." not in view_func.__name__ - ), "Blueprint view function name should not contain dots" - self.record(lambda s: s.add_url_rule(rule, endpoint, view_func, **options)) + if endpoint and "." in endpoint: + raise ValueError("'endpoint' may not contain a dot '.' character.") - def endpoint(self, endpoint): - """Like :meth:`Flask.endpoint` but for a blueprint. This does not - prefix the endpoint with the blueprint name, this has to be done - explicitly by the user of this method. If the endpoint is prefixed - with a `.` it will be registered to the current blueprint, otherwise - it's an application independent endpoint. - """ + if view_func and hasattr(view_func, "__name__") and "." in view_func.__name__: + raise ValueError("'view_func' name may not contain a dot '.' character.") - def decorator(f): - def register_endpoint(state): - state.app.view_functions[endpoint] = f + self.record( + lambda s: s.add_url_rule( + rule, + endpoint, + view_func, + provide_automatic_options=provide_automatic_options, + **options, + ) + ) - self.record_once(register_endpoint) - return f - - return decorator - - def app_template_filter(self, name=None): + def app_template_filter( + self, name: t.Optional[str] = None + ) -> t.Callable[[TemplateFilterCallable], TemplateFilterCallable]: """Register a custom template filter, available application wide. Like :meth:`Flask.template_filter` but for a blueprint. @@ -318,13 +437,15 @@ class Blueprint(_PackageBoundObject): function name will be used. """ - def decorator(f): + def decorator(f: TemplateFilterCallable) -> TemplateFilterCallable: self.add_app_template_filter(f, name=name) return f return decorator - def add_app_template_filter(self, f, name=None): + def add_app_template_filter( + self, f: TemplateFilterCallable, name: t.Optional[str] = None + ) -> None: """Register a custom template filter, available application wide. Like :meth:`Flask.add_template_filter` but for a blueprint. Works exactly like the :meth:`app_template_filter` decorator. @@ -333,12 +454,14 @@ class Blueprint(_PackageBoundObject): function name will be used. """ - def register_template(state): + def register_template(state: BlueprintSetupState) -> None: state.app.jinja_env.filters[name or f.__name__] = f self.record_once(register_template) - def app_template_test(self, name=None): + def app_template_test( + self, name: t.Optional[str] = None + ) -> t.Callable[[TemplateTestCallable], TemplateTestCallable]: """Register a custom template test, available application wide. Like :meth:`Flask.template_test` but for a blueprint. @@ -348,13 +471,15 @@ class Blueprint(_PackageBoundObject): function name will be used. """ - def decorator(f): + def decorator(f: TemplateTestCallable) -> TemplateTestCallable: self.add_app_template_test(f, name=name) return f return decorator - def add_app_template_test(self, f, name=None): + def add_app_template_test( + self, f: TemplateTestCallable, name: t.Optional[str] = None + ) -> None: """Register a custom template test, available application wide. Like :meth:`Flask.add_template_test` but for a blueprint. Works exactly like the :meth:`app_template_test` decorator. @@ -365,12 +490,14 @@ class Blueprint(_PackageBoundObject): function name will be used. """ - def register_template(state): + def register_template(state: BlueprintSetupState) -> None: state.app.jinja_env.tests[name or f.__name__] = f self.record_once(register_template) - def app_template_global(self, name=None): + def app_template_global( + self, name: t.Optional[str] = None + ) -> t.Callable[[TemplateGlobalCallable], TemplateGlobalCallable]: """Register a custom template global, available application wide. Like :meth:`Flask.template_global` but for a blueprint. @@ -380,13 +507,15 @@ class Blueprint(_PackageBoundObject): function name will be used. """ - def decorator(f): + def decorator(f: TemplateGlobalCallable) -> TemplateGlobalCallable: self.add_app_template_global(f, name=name) return f return decorator - def add_app_template_global(self, f, name=None): + def add_app_template_global( + self, f: TemplateGlobalCallable, name: t.Optional[str] = None + ) -> None: """Register a custom template global, available application wide. Like :meth:`Flask.add_template_global` but for a blueprint. Works exactly like the :meth:`app_template_global` decorator. @@ -397,22 +526,12 @@ class Blueprint(_PackageBoundObject): function name will be used. """ - def register_template(state): + def register_template(state: BlueprintSetupState) -> None: state.app.jinja_env.globals[name or f.__name__] = f self.record_once(register_template) - def before_request(self, f): - """Like :meth:`Flask.before_request` but for a blueprint. This function - is only executed before each request that is handled by a function of - that blueprint. - """ - self.record_once( - lambda s: s.app.before_request_funcs.setdefault(self.name, []).append(f) - ) - return f - - def before_app_request(self, f): + def before_app_request(self, f: BeforeRequestCallable) -> BeforeRequestCallable: """Like :meth:`Flask.before_request`. Such a function is executed before each request, even if outside of a blueprint. """ @@ -421,24 +540,16 @@ class Blueprint(_PackageBoundObject): ) return f - def before_app_first_request(self, f): + def before_app_first_request( + self, f: BeforeFirstRequestCallable + ) -> BeforeFirstRequestCallable: """Like :meth:`Flask.before_first_request`. Such a function is executed before the first request to the application. """ self.record_once(lambda s: s.app.before_first_request_funcs.append(f)) return f - def after_request(self, f): - """Like :meth:`Flask.after_request` but for a blueprint. This function - is only executed after each request that is handled by a function of - that blueprint. - """ - self.record_once( - lambda s: s.app.after_request_funcs.setdefault(self.name, []).append(f) - ) - return f - - def after_app_request(self, f): + def after_app_request(self, f: AfterRequestCallable) -> AfterRequestCallable: """Like :meth:`Flask.after_request` but for a blueprint. Such a function is executed after each request, even if outside of the blueprint. """ @@ -447,19 +558,7 @@ class Blueprint(_PackageBoundObject): ) return f - def teardown_request(self, f): - """Like :meth:`Flask.teardown_request` but for a blueprint. This - function is only executed when tearing down requests handled by a - function of that blueprint. Teardown request functions are executed - when the request context is popped, even when no actual request was - performed. - """ - self.record_once( - lambda s: s.app.teardown_request_funcs.setdefault(self.name, []).append(f) - ) - return f - - def teardown_app_request(self, f): + def teardown_app_request(self, f: TeardownCallable) -> TeardownCallable: """Like :meth:`Flask.teardown_request` but for a blueprint. Such a function is executed when tearing down each request, even if outside of the blueprint. @@ -469,18 +568,9 @@ class Blueprint(_PackageBoundObject): ) return f - def context_processor(self, f): - """Like :meth:`Flask.context_processor` but for a blueprint. This - function is only executed for requests handled by a blueprint. - """ - self.record_once( - lambda s: s.app.template_context_processors.setdefault( - self.name, [] - ).append(f) - ) - return f - - def app_context_processor(self, f): + def app_context_processor( + self, f: TemplateContextProcessorCallable + ) -> TemplateContextProcessorCallable: """Like :meth:`Flask.context_processor` but for a blueprint. Such a function is executed each request, even if outside of the blueprint. """ @@ -489,81 +579,31 @@ class Blueprint(_PackageBoundObject): ) return f - def app_errorhandler(self, code): + def app_errorhandler(self, code: t.Union[t.Type[Exception], int]) -> t.Callable: """Like :meth:`Flask.errorhandler` but for a blueprint. This handler is used for all requests, even if outside of the blueprint. """ - def decorator(f): + def decorator( + f: "ErrorHandlerCallable[Exception]", + ) -> "ErrorHandlerCallable[Exception]": self.record_once(lambda s: s.app.errorhandler(code)(f)) return f return decorator - def url_value_preprocessor(self, f): - """Registers a function as URL value preprocessor for this - blueprint. It's called before the view functions are called and - can modify the url values provided. - """ - self.record_once( - lambda s: s.app.url_value_preprocessors.setdefault(self.name, []).append(f) - ) - return f - - def url_defaults(self, f): - """Callback function for URL defaults for this blueprint. It's called - with the endpoint and values and should update the values passed - in place. - """ - self.record_once( - lambda s: s.app.url_default_functions.setdefault(self.name, []).append(f) - ) - return f - - def app_url_value_preprocessor(self, f): - """Same as :meth:`url_value_preprocessor` but application wide. - """ + def app_url_value_preprocessor( + self, f: URLValuePreprocessorCallable + ) -> URLValuePreprocessorCallable: + """Same as :meth:`url_value_preprocessor` but application wide.""" self.record_once( lambda s: s.app.url_value_preprocessors.setdefault(None, []).append(f) ) return f - def app_url_defaults(self, f): - """Same as :meth:`url_defaults` but application wide. - """ + def app_url_defaults(self, f: URLDefaultCallable) -> URLDefaultCallable: + """Same as :meth:`url_defaults` but application wide.""" self.record_once( lambda s: s.app.url_default_functions.setdefault(None, []).append(f) ) return f - - def errorhandler(self, code_or_exception): - """Registers an error handler that becomes active for this blueprint - only. Please be aware that routing does not happen local to a - blueprint so an error handler for 404 usually is not handled by - a blueprint unless it is caused inside a view function. Another - special case is the 500 internal server error which is always looked - up from the application. - - Otherwise works as the :meth:`~flask.Flask.errorhandler` decorator - of the :class:`~flask.Flask` object. - """ - - def decorator(f): - self.record_once( - lambda s: s.app._register_error_handler(self.name, code_or_exception, f) - ) - return f - - return decorator - - def register_error_handler(self, code_or_exception, f): - """Non-decorator version of the :meth:`errorhandler` error attach - function, akin to the :meth:`~flask.Flask.register_error_handler` - application-wide function of the :class:`~flask.Flask` object but - for error handlers limited to this blueprint. - - .. versionadded:: 0.11 - """ - self.record_once( - lambda s: s.app._register_error_handler(self.name, code_or_exception, f) - ) diff --git a/libs/flask/cli.py b/libs/flask/cli.py index 11585455a..7ab4fa1c9 100644 --- a/libs/flask/cli.py +++ b/libs/flask/cli.py @@ -1,15 +1,3 @@ -# -*- coding: utf-8 -*- -""" - flask.cli - ~~~~~~~~~ - - A simple command line application to run flask apps. - - :copyright: 2010 Pallets - :license: BSD-3-Clause -""" -from __future__ import print_function - import ast import inspect import os @@ -17,6 +5,7 @@ import platform import re import sys import traceback +import warnings from functools import update_wrapper from operator import attrgetter from threading import Lock @@ -25,10 +14,6 @@ from threading import Thread import click from werkzeug.utils import import_string -from ._compat import getargspec -from ._compat import itervalues -from ._compat import reraise -from ._compat import text_type from .globals import current_app from .helpers import get_debug_flag from .helpers import get_env @@ -42,7 +27,7 @@ except ImportError: try: import ssl except ImportError: - ssl = None + ssl = None # type: ignore class NoAppException(click.UsageError): @@ -63,15 +48,15 @@ def find_best_app(script_info, module): return app # Otherwise find the only object that is a Flask instance. - matches = [v for v in itervalues(module.__dict__) if isinstance(v, Flask)] + matches = [v for v in module.__dict__.values() if isinstance(v, Flask)] if len(matches) == 1: return matches[0] elif len(matches) > 1: raise NoAppException( - 'Detected multiple Flask applications in module "{module}". Use ' - '"FLASK_APP={module}:name" to specify the correct ' - "one.".format(module=module.__name__) + "Detected multiple Flask applications in module" + f" {module.__name__!r}. Use 'FLASK_APP={module.__name__}:name'" + f" to specify the correct one." ) # Search for app factory functions. @@ -84,114 +69,149 @@ def find_best_app(script_info, module): if isinstance(app, Flask): return app - except TypeError: + except TypeError as e: if not _called_with_wrong_args(app_factory): raise + raise NoAppException( - 'Detected factory "{factory}" in module "{module}", but ' - "could not call it without arguments. Use " - "\"FLASK_APP='{module}:{factory}(args)'\" to specify " - "arguments.".format(factory=attr_name, module=module.__name__) - ) + f"Detected factory {attr_name!r} in module {module.__name__!r}," + " but could not call it without arguments. Use" + f" \"FLASK_APP='{module.__name__}:{attr_name}(args)'\"" + " to specify arguments." + ) from e raise NoAppException( - 'Failed to find Flask application or factory in module "{module}". ' - 'Use "FLASK_APP={module}:name to specify one.'.format(module=module.__name__) + "Failed to find Flask application or factory in module" + f" {module.__name__!r}. Use 'FLASK_APP={module.__name__}:name'" + " to specify one." ) -def call_factory(script_info, app_factory, arguments=()): +def call_factory(script_info, app_factory, args=None, kwargs=None): """Takes an app factory, a ``script_info` object and optionally a tuple of arguments. Checks for the existence of a script_info argument and calls the app_factory depending on that and the arguments provided. """ - args_spec = getargspec(app_factory) - arg_names = args_spec.args - arg_defaults = args_spec.defaults + sig = inspect.signature(app_factory) + args = [] if args is None else args + kwargs = {} if kwargs is None else kwargs - if "script_info" in arg_names: - return app_factory(*arguments, script_info=script_info) - elif arguments: - return app_factory(*arguments) - elif not arguments and len(arg_names) == 1 and arg_defaults is None: - return app_factory(script_info) + if "script_info" in sig.parameters: + warnings.warn( + "The 'script_info' argument is deprecated and will not be" + " passed to the app factory function in Flask 2.1.", + DeprecationWarning, + ) + kwargs["script_info"] = script_info - return app_factory() + if not args and len(sig.parameters) == 1: + first_parameter = next(iter(sig.parameters.values())) + + if ( + first_parameter.default is inspect.Parameter.empty + # **kwargs is reported as an empty default, ignore it + and first_parameter.kind is not inspect.Parameter.VAR_KEYWORD + ): + warnings.warn( + "Script info is deprecated and will not be passed as the" + " single argument to the app factory function in Flask" + " 2.1.", + DeprecationWarning, + ) + args.append(script_info) + + return app_factory(*args, **kwargs) -def _called_with_wrong_args(factory): +def _called_with_wrong_args(f): """Check whether calling a function raised a ``TypeError`` because the call failed or because something in the factory raised the error. - :param factory: the factory function that was called - :return: true if the call failed + :param f: The function that was called. + :return: ``True`` if the call failed. """ tb = sys.exc_info()[2] try: while tb is not None: - if tb.tb_frame.f_code is factory.__code__: - # in the factory, it was called successfully + if tb.tb_frame.f_code is f.__code__: + # In the function, it was called successfully. return False tb = tb.tb_next - # didn't reach the factory + # Didn't reach the function. return True finally: - # explicitly delete tb as it is circular referenced + # Delete tb to break a circular reference. # https://docs.python.org/2/library/sys.html#sys.exc_info del tb def find_app_by_string(script_info, module, app_name): - """Checks if the given string is a variable name or a function. If it is a - function, it checks for specified arguments and whether it takes a - ``script_info`` argument and calls the function with the appropriate - arguments. + """Check if the given string is a variable name or a function. Call + a function to get the app instance, or return the variable directly. """ from . import Flask - match = re.match(r"^ *([^ ()]+) *(?:\((.*?) *,? *\))? *$", app_name) - - if not match: + # Parse app_name as a single expression to determine if it's a valid + # attribute name or function call. + try: + expr = ast.parse(app_name.strip(), mode="eval").body + except SyntaxError: raise NoAppException( - '"{name}" is not a valid variable name or function ' - "expression.".format(name=app_name) - ) + f"Failed to parse {app_name!r} as an attribute name or function call." + ) from None - name, args = match.groups() + if isinstance(expr, ast.Name): + name = expr.id + args = kwargs = None + elif isinstance(expr, ast.Call): + # Ensure the function name is an attribute name only. + if not isinstance(expr.func, ast.Name): + raise NoAppException( + f"Function reference must be a simple name: {app_name!r}." + ) + + name = expr.func.id + + # Parse the positional and keyword arguments as literals. + try: + args = [ast.literal_eval(arg) for arg in expr.args] + kwargs = {kw.arg: ast.literal_eval(kw.value) for kw in expr.keywords} + except ValueError: + # literal_eval gives cryptic error messages, show a generic + # message with the full expression instead. + raise NoAppException( + f"Failed to parse arguments as literal values: {app_name!r}." + ) from None + else: + raise NoAppException( + f"Failed to parse {app_name!r} as an attribute name or function call." + ) try: attr = getattr(module, name) except AttributeError as e: - raise NoAppException(e.args[0]) + raise NoAppException( + f"Failed to find attribute {name!r} in {module.__name__!r}." + ) from e + # If the attribute is a function, call it with any args and kwargs + # to get the real application. if inspect.isfunction(attr): - if args: - try: - args = ast.literal_eval("({args},)".format(args=args)) - except (ValueError, SyntaxError) as e: - raise NoAppException( - "Could not parse the arguments in " - '"{app_name}".'.format(e=e, app_name=app_name) - ) - else: - args = () - try: - app = call_factory(script_info, attr, args) + app = call_factory(script_info, attr, args, kwargs) except TypeError as e: if not _called_with_wrong_args(attr): raise raise NoAppException( - '{e}\nThe factory "{app_name}" in module "{module}" could not ' - "be called with the specified arguments.".format( - e=e, app_name=app_name, module=module.__name__ - ) - ) + f"The factory {app_name!r} in module" + f" {module.__name__!r} could not be called with the" + " specified arguments." + ) from e else: app = attr @@ -199,8 +219,8 @@ def find_app_by_string(script_info, module, app_name): return app raise NoAppException( - "A valid Flask application was not obtained from " - '"{module}:{app_name}".'.format(module=module.__name__, app_name=app_name) + "A valid Flask application was not obtained from" + f" '{module.__name__}:{app_name}'." ) @@ -238,16 +258,15 @@ def locate_app(script_info, module_name, app_name, raise_if_not_found=True): try: __import__(module_name) - except ImportError: + except ImportError as e: # Reraise the ImportError if it occurred within the imported module. # Determine this by checking whether the trace has a depth > 1. - if sys.exc_info()[-1].tb_next: + if sys.exc_info()[2].tb_next: raise NoAppException( - 'While importing "{name}", an ImportError was raised:' - "\n\n{tb}".format(name=module_name, tb=traceback.format_exc()) - ) + f"While importing {module_name!r}, an ImportError was raised." + ) from e elif raise_if_not_found: - raise NoAppException('Could not import "{name}".'.format(name=module_name)) + raise NoAppException(f"Could not import {module_name!r}.") from e else: return @@ -266,14 +285,10 @@ def get_version(ctx, param, value): import werkzeug from . import __version__ - message = "Python %(python)s\nFlask %(flask)s\nWerkzeug %(werkzeug)s" click.echo( - message - % { - "python": platform.python_version(), - "flask": __version__, - "werkzeug": werkzeug.__version__, - }, + f"Python {platform.python_version()}\n" + f"Flask {__version__}\n" + f"Werkzeug {werkzeug.__version__}", color=ctx.color, ) ctx.exit() @@ -289,18 +304,22 @@ version_option = click.Option( ) -class DispatchingApp(object): +class DispatchingApp: """Special application that dispatches to a Flask application which is imported by name in a background thread. If an error happens it is recorded and shown as part of the WSGI handling which in case of the Werkzeug debugger means that it shows up in the browser. """ - def __init__(self, loader, use_eager_loading=False): + def __init__(self, loader, use_eager_loading=None): self.loader = loader self._app = None self._lock = Lock() - self._bg_loading_exc_info = None + self._bg_loading_exc = None + + if use_eager_loading is None: + use_eager_loading = os.environ.get("WERKZEUG_RUN_MAIN") != "true" + if use_eager_loading: self._load_unlocked() else: @@ -312,23 +331,24 @@ class DispatchingApp(object): with self._lock: try: self._load_unlocked() - except Exception: - self._bg_loading_exc_info = sys.exc_info() + except Exception as e: + self._bg_loading_exc = e t = Thread(target=_load_app, args=()) t.start() def _flush_bg_loading_exception(self): __traceback_hide__ = True # noqa: F841 - exc_info = self._bg_loading_exc_info - if exc_info is not None: - self._bg_loading_exc_info = None - reraise(*exc_info) + exc = self._bg_loading_exc + + if exc is not None: + self._bg_loading_exc = None + raise exc def _load_unlocked(self): __traceback_hide__ = True # noqa: F841 self._app = rv = self.loader() - self._bg_loading_exc_info = None + self._bg_loading_exc = None return rv def __call__(self, environ, start_response): @@ -344,7 +364,7 @@ class DispatchingApp(object): return rv(environ, start_response) -class ScriptInfo(object): +class ScriptInfo: """Helper object to deal with Flask applications. This is usually not necessary to interface with as it's used internally in the dispatching to click. In future versions of Flask this object will most likely play @@ -375,8 +395,6 @@ class ScriptInfo(object): if self._loaded_app is not None: return self._loaded_app - app = None - if self.create_app is not None: app = call_factory(self, self.create_app) else: @@ -464,9 +482,7 @@ class FlaskGroup(AppGroup): loading more commands from the configured Flask app. Normally a developer does not have to interface with this class but there are some very advanced use cases for which it makes sense to create an - instance of this. - - For information as of why this is useful see :ref:`custom-scripts`. + instance of this. see :ref:`custom-scripts`. :param add_default_commands: if this is True then the default run and shell commands will be added. @@ -491,7 +507,7 @@ class FlaskGroup(AppGroup): add_version_option=True, load_dotenv=True, set_debug_flag=True, - **extra + **extra, ): params = list(extra.pop("params", None) or ()) @@ -525,43 +541,41 @@ class FlaskGroup(AppGroup): def get_command(self, ctx, name): self._load_plugin_commands() + # Look up built-in and plugin commands, which should be + # available even if the app fails to load. + rv = super().get_command(ctx, name) - # We load built-in commands first as these should always be the - # same no matter what the app does. If the app does want to - # override this it needs to make a custom instance of this group - # and not attach the default commands. - # - # This also means that the script stays functional in case the - # application completely fails. - rv = AppGroup.get_command(self, ctx, name) if rv is not None: return rv info = ctx.ensure_object(ScriptInfo) + + # Look up commands provided by the app, showing an error and + # continuing if the app couldn't be loaded. try: - rv = info.load_app().cli.get_command(ctx, name) - if rv is not None: - return rv - except NoAppException: - pass + return info.load_app().cli.get_command(ctx, name) + except NoAppException as e: + click.secho(f"Error: {e.format_message()}\n", err=True, fg="red") def list_commands(self, ctx): self._load_plugin_commands() - - # The commands available is the list of both the application (if - # available) plus the builtin commands. - rv = set(click.Group.list_commands(self, ctx)) + # Start with the built-in and plugin commands. + rv = set(super().list_commands(ctx)) info = ctx.ensure_object(ScriptInfo) + + # Add commands provided by the app, showing an error and + # continuing if the app couldn't be loaded. try: rv.update(info.load_app().cli.list_commands(ctx)) + except NoAppException as e: + # When an app couldn't be loaded, show the error message + # without the traceback. + click.secho(f"Error: {e.format_message()}\n", err=True, fg="red") except Exception: - # Here we intentionally swallow all exceptions as we don't - # want the help page to break if the app does not exist. - # If someone attempts to use the command we try to create - # the app again and this will give us the error. - # However, we will not do so silently because that would confuse - # users. - traceback.print_exc() + # When any other errors occurred during loading, show the + # full traceback. + click.secho(f"{traceback.format_exc()}\n", err=True, fg="red") + return sorted(rv) def main(self, *args, **kwargs): @@ -583,7 +597,7 @@ class FlaskGroup(AppGroup): kwargs["obj"] = obj kwargs.setdefault("auto_envvar_prefix", "FLASK") - return super(FlaskGroup, self).main(*args, **kwargs) + return super().main(*args, **kwargs) def _path_is_ancestor(path, other): @@ -599,10 +613,6 @@ def load_dotenv(path=None): If an env var is already set it is not overwritten, so earlier files in the list are preferred over later files. - Changes the current working directory to the location of the first file - found, with the assumption that it is in the top level project directory - and will be where the Python path should import local packages from. - This is a no-op if `python-dotenv`_ is not installed. .. _python-dotenv: https://github.com/theskumar/python-dotenv#readme @@ -614,6 +624,9 @@ def load_dotenv(path=None): Returns ``False`` when python-dotenv is not installed, or when the given path isn't a file. + .. versionchanged:: 2.0 + When loading the env files, set the default encoding to UTF-8. + .. versionadded:: 1.0 """ if dotenv is None: @@ -631,7 +644,7 @@ def load_dotenv(path=None): # else False if path is not None: if os.path.isfile(path): - return dotenv.load_dotenv(path) + return dotenv.load_dotenv(path, encoding="utf-8") return False @@ -646,10 +659,7 @@ def load_dotenv(path=None): if new_dir is None: new_dir = os.path.dirname(path) - dotenv.load_dotenv(path) - - if new_dir and os.getcwd() != new_dir: - os.chdir(new_dir) + dotenv.load_dotenv(path, encoding="utf-8") return new_dir is not None # at least one file was located and loaded @@ -662,25 +672,25 @@ def show_server_banner(env, debug, app_import_path, eager_loading): return if app_import_path is not None: - message = ' * Serving Flask app "{0}"'.format(app_import_path) + message = f" * Serving Flask app {app_import_path!r}" if not eager_loading: message += " (lazy loading)" click.echo(message) - click.echo(" * Environment: {0}".format(env)) + click.echo(f" * Environment: {env}") if env == "production": click.secho( - " WARNING: This is a development server. " - "Do not use it in a production deployment.", + " WARNING: This is a development server. Do not use it in" + " a production deployment.", fg="red", ) click.secho(" Use a production WSGI server instead.", dim=True) if debug is not None: - click.echo(" * Debug mode: {0}".format("on" if debug else "off")) + click.echo(f" * Debug mode: {'on' if debug else 'off'}") class CertParamType(click.ParamType): @@ -709,22 +719,20 @@ class CertParamType(click.ParamType): if value == "adhoc": try: - import OpenSSL # noqa: F401 + import cryptography # noqa: F401 except ImportError: raise click.BadParameter( - "Using ad-hoc certificates requires pyOpenSSL.", ctx, param - ) + "Using ad-hoc certificates requires the cryptography library.", + ctx, + param, + ) from None return value obj = import_string(value, silent=True) - if sys.version_info < (2, 7, 9): - if obj: - return obj - else: - if isinstance(obj, ssl.SSLContext): - return obj + if isinstance(obj, ssl.SSLContext): + return obj raise @@ -735,11 +743,7 @@ def _validate_key(ctx, param, value): """ cert = ctx.params.get("cert") is_adhoc = cert == "adhoc" - - if sys.version_info < (2, 7, 9): - is_context = cert and not isinstance(cert, (text_type, bytes)) - else: - is_context = isinstance(cert, ssl.SSLContext) + is_context = ssl and isinstance(cert, ssl.SSLContext) if value is not None: if is_adhoc: @@ -772,7 +776,7 @@ class SeparatedPathType(click.Path): def convert(self, value, param, ctx): items = self.split_envvar_value(value) - super_convert = super(SeparatedPathType, self).convert + super_convert = super().convert return [super_convert(item, param, ctx) for item in items] @@ -802,7 +806,7 @@ class SeparatedPathType(click.Path): "is active if debug is enabled.", ) @click.option( - "--eager-loading/--lazy-loader", + "--eager-loading/--lazy-loading", default=None, help="Enable or disable eager loading. By default eager " "loading is enabled if the reloader is disabled.", @@ -818,7 +822,7 @@ class SeparatedPathType(click.Path): type=SeparatedPathType(), help=( "Extra files that trigger a reload on change. Multiple paths" - " are separated by '{}'.".format(os.path.pathsep) + f" are separated by {os.path.pathsep!r}." ), ) @pass_script_info @@ -841,9 +845,6 @@ def run_command( if debugger is None: debugger = debug - if eager_loading is None: - eager_loading = not reload - show_server_banner(get_env(), debug, info.app_import_path, eager_loading) app = DispatchingApp(info.load_app, use_eager_loading=eager_loading) @@ -863,10 +864,10 @@ def run_command( @click.command("shell", short_help="Run a shell in the app context.") @with_appcontext -def shell_command(): +def shell_command() -> None: """Run an interactive Python shell in the context of a given Flask application. The application will populate the default - namespace of this shell according to it's configuration. + namespace of this shell according to its configuration. This is useful for executing small snippets of management code without having to manually configure the application. @@ -875,24 +876,40 @@ def shell_command(): from .globals import _app_ctx_stack app = _app_ctx_stack.top.app - banner = "Python %s on %s\nApp: %s [%s]\nInstance: %s" % ( - sys.version, - sys.platform, - app.import_name, - app.env, - app.instance_path, + banner = ( + f"Python {sys.version} on {sys.platform}\n" + f"App: {app.import_name} [{app.env}]\n" + f"Instance: {app.instance_path}" ) - ctx = {} + ctx: dict = {} # Support the regular Python interpreter startup script if someone # is using it. startup = os.environ.get("PYTHONSTARTUP") if startup and os.path.isfile(startup): - with open(startup, "r") as f: + with open(startup) as f: eval(compile(f.read(), startup, "exec"), ctx) ctx.update(app.make_shell_context()) + # Site, customize, or startup script can set a hook to call when + # entering interactive mode. The default one sets up readline with + # tab and history completion. + interactive_hook = getattr(sys, "__interactivehook__", None) + + if interactive_hook is not None: + try: + import readline + from rlcompleter import Completer + except ImportError: + pass + else: + # rlcompleter uses __main__.__dict__ by default, which is + # flask.__main__. Use the shell context instead. + readline.set_completer(Completer(ctx).complete) + + interactive_hook() + code.interact(banner=banner, local=ctx) @@ -909,7 +926,7 @@ def shell_command(): ) @click.option("--all-methods", is_flag=True, help="Show HEAD and OPTIONS methods.") @with_appcontext -def routes_command(sort, all_methods): +def routes_command(sort: str, all_methods: bool) -> None: """Show all registered routes with endpoints and methods.""" rules = list(current_app.url_map.iter_rules()) @@ -922,9 +939,12 @@ def routes_command(sort, all_methods): if sort in ("endpoint", "rule"): rules = sorted(rules, key=attrgetter(sort)) elif sort == "methods": - rules = sorted(rules, key=lambda rule: sorted(rule.methods)) + rules = sorted(rules, key=lambda rule: sorted(rule.methods)) # type: ignore - rule_methods = [", ".join(sorted(rule.methods - ignored_methods)) for rule in rules] + rule_methods = [ + ", ".join(sorted(rule.methods - ignored_methods)) # type: ignore + for rule in rules + ] headers = ("Endpoint", "Methods", "Rule") widths = ( @@ -962,9 +982,17 @@ debug mode. ) -def main(as_module=False): - cli.main(prog_name="python -m flask" if as_module else None) +def main() -> None: + if int(click.__version__[0]) < 8: + warnings.warn( + "Using the `flask` cli with Click 7 is deprecated and" + " will not be supported starting with Flask 2.1." + " Please upgrade to Click 8 as soon as possible.", + DeprecationWarning, + ) + # TODO omit sys.argv once https://github.com/pallets/click/issues/536 is fixed + cli.main(args=sys.argv[1:]) if __name__ == "__main__": - main(as_module=True) + main() diff --git a/libs/flask/config.py b/libs/flask/config.py index 809de336f..ca769022f 100644 --- a/libs/flask/config.py +++ b/libs/flask/config.py @@ -1,32 +1,19 @@ -# -*- coding: utf-8 -*- -""" - flask.config - ~~~~~~~~~~~~ - - Implements the configuration related objects. - - :copyright: 2010 Pallets - :license: BSD-3-Clause -""" import errno import os import types +import typing as t from werkzeug.utils import import_string -from . import json -from ._compat import iteritems -from ._compat import string_types - -class ConfigAttribute(object): +class ConfigAttribute: """Makes an attribute forward to the config""" - def __init__(self, name, get_converter=None): + def __init__(self, name: str, get_converter: t.Optional[t.Callable] = None) -> None: self.__name__ = name self.get_converter = get_converter - def __get__(self, obj, type=None): + def __get__(self, obj: t.Any, owner: t.Any = None) -> t.Any: if obj is None: return self rv = obj.config[self.__name__] @@ -34,7 +21,7 @@ class ConfigAttribute(object): rv = self.get_converter(rv) return rv - def __set__(self, obj, value): + def __set__(self, obj: t.Any, value: t.Any) -> None: obj.config[self.__name__] = value @@ -82,11 +69,11 @@ class Config(dict): :param defaults: an optional dictionary of default values """ - def __init__(self, root_path, defaults=None): + def __init__(self, root_path: str, defaults: t.Optional[dict] = None) -> None: dict.__init__(self, defaults or {}) self.root_path = root_path - def from_envvar(self, variable_name, silent=False): + def from_envvar(self, variable_name: str, silent: bool = False) -> bool: """Loads a configuration from an environment variable pointing to a configuration file. This is basically just a shortcut with nicer error messages for this line of code:: @@ -96,21 +83,21 @@ class Config(dict): :param variable_name: name of the environment variable :param silent: set to ``True`` if you want silent failure for missing files. - :return: bool. ``True`` if able to load config, ``False`` otherwise. + :return: ``True`` if the file was loaded successfully. """ rv = os.environ.get(variable_name) if not rv: if silent: return False raise RuntimeError( - "The environment variable %r is not set " - "and as such configuration could not be " - "loaded. Set this variable and make it " - "point to a configuration file" % variable_name + f"The environment variable {variable_name!r} is not set" + " and as such configuration could not be loaded. Set" + " this variable and make it point to a configuration" + " file" ) return self.from_pyfile(rv, silent=silent) - def from_pyfile(self, filename, silent=False): + def from_pyfile(self, filename: str, silent: bool = False) -> bool: """Updates the values in the config from a Python file. This function behaves as if the file was imported as module with the :meth:`from_object` function. @@ -120,6 +107,7 @@ class Config(dict): root path. :param silent: set to ``True`` if you want silent failure for missing files. + :return: ``True`` if the file was loaded successfully. .. versionadded:: 0.7 `silent` parameter. @@ -130,15 +118,15 @@ class Config(dict): try: with open(filename, mode="rb") as config_file: exec(compile(config_file.read(), filename, "exec"), d.__dict__) - except IOError as e: + except OSError as e: if silent and e.errno in (errno.ENOENT, errno.EISDIR, errno.ENOTDIR): return False - e.strerror = "Unable to load configuration file (%s)" % e.strerror + e.strerror = f"Unable to load configuration file ({e.strerror})" raise self.from_object(d) return True - def from_object(self, obj): + def from_object(self, obj: t.Union[object, str]) -> None: """Updates the values from the given object. An object can be of one of the following two types: @@ -170,61 +158,99 @@ class Config(dict): :param obj: an import name or object """ - if isinstance(obj, string_types): + if isinstance(obj, str): obj = import_string(obj) for key in dir(obj): if key.isupper(): self[key] = getattr(obj, key) - def from_json(self, filename, silent=False): - """Updates the values in the config from a JSON file. This function - behaves as if the JSON object was a dictionary and passed to the - :meth:`from_mapping` function. + def from_file( + self, + filename: str, + load: t.Callable[[t.IO[t.Any]], t.Mapping], + silent: bool = False, + ) -> bool: + """Update the values in the config from a file that is loaded + using the ``load`` parameter. The loaded data is passed to the + :meth:`from_mapping` method. - :param filename: the filename of the JSON file. This can either be an - absolute filename or a filename relative to the - root path. - :param silent: set to ``True`` if you want silent failure for missing - files. + .. code-block:: python - .. versionadded:: 0.11 + import toml + app.config.from_file("config.toml", load=toml.load) + + :param filename: The path to the data file. This can be an + absolute path or relative to the config root path. + :param load: A callable that takes a file handle and returns a + mapping of loaded data from the file. + :type load: ``Callable[[Reader], Mapping]`` where ``Reader`` + implements a ``read`` method. + :param silent: Ignore the file if it doesn't exist. + :return: ``True`` if the file was loaded successfully. + + .. versionadded:: 2.0 """ filename = os.path.join(self.root_path, filename) try: - with open(filename) as json_file: - obj = json.loads(json_file.read()) - except IOError as e: + with open(filename) as f: + obj = load(f) + except OSError as e: if silent and e.errno in (errno.ENOENT, errno.EISDIR): return False - e.strerror = "Unable to load configuration file (%s)" % e.strerror + + e.strerror = f"Unable to load configuration file ({e.strerror})" raise + return self.from_mapping(obj) - def from_mapping(self, *mapping, **kwargs): - """Updates the config like :meth:`update` ignoring items with non-upper - keys. + def from_json(self, filename: str, silent: bool = False) -> bool: + """Update the values in the config from a JSON file. The loaded + data is passed to the :meth:`from_mapping` method. + + :param filename: The path to the JSON file. This can be an + absolute path or relative to the config root path. + :param silent: Ignore the file if it doesn't exist. + :return: ``True`` if the file was loaded successfully. + + .. deprecated:: 2.0.0 + Will be removed in Flask 2.1. Use :meth:`from_file` instead. + This was removed early in 2.0.0, was added back in 2.0.1. .. versionadded:: 0.11 """ - mappings = [] - if len(mapping) == 1: - if hasattr(mapping[0], "items"): - mappings.append(mapping[0].items()) - else: - mappings.append(mapping[0]) - elif len(mapping) > 1: - raise TypeError( - "expected at most 1 positional argument, got %d" % len(mapping) - ) - mappings.append(kwargs.items()) - for mapping in mappings: - for (key, value) in mapping: - if key.isupper(): - self[key] = value + import warnings + from . import json + + warnings.warn( + "'from_json' is deprecated and will be removed in Flask" + " 2.1. Use 'from_file(path, json.load)' instead.", + DeprecationWarning, + stacklevel=2, + ) + return self.from_file(filename, json.load, silent=silent) + + def from_mapping( + self, mapping: t.Optional[t.Mapping[str, t.Any]] = None, **kwargs: t.Any + ) -> bool: + """Updates the config like :meth:`update` ignoring items with non-upper + keys. + :return: Always returns ``True``. + + .. versionadded:: 0.11 + """ + mappings: t.Dict[str, t.Any] = {} + if mapping is not None: + mappings.update(mapping) + mappings.update(kwargs) + for key, value in mappings.items(): + if key.isupper(): + self[key] = value return True - def get_namespace(self, namespace, lowercase=True, trim_namespace=True): + def get_namespace( + self, namespace: str, lowercase: bool = True, trim_namespace: bool = True + ) -> t.Dict[str, t.Any]: """Returns a dictionary containing a subset of configuration options that match the specified namespace/prefix. Example usage:: @@ -253,7 +279,7 @@ class Config(dict): .. versionadded:: 0.11 """ rv = {} - for k, v in iteritems(self): + for k, v in self.items(): if not k.startswith(namespace): continue if trim_namespace: @@ -265,5 +291,5 @@ class Config(dict): rv[key] = v return rv - def __repr__(self): - return "<%s %s>" % (self.__class__.__name__, dict.__repr__(self)) + def __repr__(self) -> str: + return f"<{type(self).__name__} {dict.__repr__(self)}>" diff --git a/libs/flask/ctx.py b/libs/flask/ctx.py index 172f6a01b..5c0646352 100644 --- a/libs/flask/ctx.py +++ b/libs/flask/ctx.py @@ -1,31 +1,27 @@ -# -*- coding: utf-8 -*- -""" - flask.ctx - ~~~~~~~~~ - - Implements the objects required to keep the context. - - :copyright: 2010 Pallets - :license: BSD-3-Clause -""" import sys +import typing as t from functools import update_wrapper +from types import TracebackType from werkzeug.exceptions import HTTPException -from ._compat import BROKEN_PYPY_CTXMGR_EXIT -from ._compat import reraise from .globals import _app_ctx_stack from .globals import _request_ctx_stack from .signals import appcontext_popped from .signals import appcontext_pushed +from .typing import AfterRequestCallable + +if t.TYPE_CHECKING: + from .app import Flask + from .sessions import SessionMixin + from .wrappers import Request # a singleton sentinel value for parameter defaults _sentinel = object() -class _AppCtxGlobals(object): +class _AppCtxGlobals: """A plain object. Used as a namespace for storing data during an application context. @@ -45,7 +41,25 @@ class _AppCtxGlobals(object): .. versionadded:: 0.10 """ - def get(self, name, default=None): + # Define attr methods to let mypy know this is a namespace object + # that has arbitrary attributes. + + def __getattr__(self, name: str) -> t.Any: + try: + return self.__dict__[name] + except KeyError: + raise AttributeError(name) from None + + def __setattr__(self, name: str, value: t.Any) -> None: + self.__dict__[name] = value + + def __delattr__(self, name: str) -> None: + try: + del self.__dict__[name] + except KeyError: + raise AttributeError(name) from None + + def get(self, name: str, default: t.Optional[t.Any] = None) -> t.Any: """Get an attribute by name, or a default value. Like :meth:`dict.get`. @@ -56,12 +70,12 @@ class _AppCtxGlobals(object): """ return self.__dict__.get(name, default) - def pop(self, name, default=_sentinel): + def pop(self, name: str, default: t.Any = _sentinel) -> t.Any: """Get and remove an attribute by name. Like :meth:`dict.pop`. :param name: Name of attribute to pop. :param default: Value to return if the attribute is not present, - instead of raise a ``KeyError``. + instead of raising a ``KeyError``. .. versionadded:: 0.11 """ @@ -70,32 +84,32 @@ class _AppCtxGlobals(object): else: return self.__dict__.pop(name, default) - def setdefault(self, name, default=None): + def setdefault(self, name: str, default: t.Any = None) -> t.Any: """Get the value of an attribute if it is present, otherwise set and return a default value. Like :meth:`dict.setdefault`. :param name: Name of attribute to get. - :param: default: Value to set and return if the attribute is not + :param default: Value to set and return if the attribute is not present. .. versionadded:: 0.11 """ return self.__dict__.setdefault(name, default) - def __contains__(self, item): + def __contains__(self, item: str) -> bool: return item in self.__dict__ - def __iter__(self): + def __iter__(self) -> t.Iterator[str]: return iter(self.__dict__) - def __repr__(self): + def __repr__(self) -> str: top = _app_ctx_stack.top if top is not None: - return "" % top.app.name + return f"" return object.__repr__(self) -def after_this_request(f): +def after_this_request(f: AfterRequestCallable) -> AfterRequestCallable: """Executes a function after this request. This is useful to modify response objects. The function is passed the response object and has to return the same or a new one. @@ -120,7 +134,7 @@ def after_this_request(f): return f -def copy_current_request_context(f): +def copy_current_request_context(f: t.Callable) -> t.Callable: """A helper function that decorates a function to retain the current request context. This is useful when working with greenlets. The moment the function is decorated a copy of the request context is created and @@ -160,7 +174,7 @@ def copy_current_request_context(f): return update_wrapper(wrapper, f) -def has_request_context(): +def has_request_context() -> bool: """If you have code that wants to test if a request context is there or not this function can be used. For instance, you may want to take advantage of request information if the request object is available, but fail @@ -192,7 +206,7 @@ def has_request_context(): return _request_ctx_stack.top is not None -def has_app_context(): +def has_app_context() -> bool: """Works like :func:`has_request_context` but for the application context. You can also just do a boolean check on the :data:`current_app` object instead. @@ -202,7 +216,7 @@ def has_app_context(): return _app_ctx_stack.top is not None -class AppContext(object): +class AppContext: """The application context binds an application object implicitly to the current thread or greenlet, similar to how the :class:`RequestContext` binds request information. The application @@ -211,7 +225,7 @@ class AppContext(object): context. """ - def __init__(self, app): + def __init__(self, app: "Flask") -> None: self.app = app self.url_adapter = app.create_url_adapter(None) self.g = app.app_ctx_globals_class() @@ -220,15 +234,13 @@ class AppContext(object): # but there a basic "refcount" is enough to track them. self._refcnt = 0 - def push(self): + def push(self) -> None: """Binds the app context to the current context.""" self._refcnt += 1 - if hasattr(sys, "exc_clear"): - sys.exc_clear() _app_ctx_stack.push(self) appcontext_pushed.send(self.app) - def pop(self, exc=_sentinel): + def pop(self, exc: t.Optional[BaseException] = _sentinel) -> None: # type: ignore """Pops the app context.""" try: self._refcnt -= 1 @@ -238,21 +250,20 @@ class AppContext(object): self.app.do_teardown_appcontext(exc) finally: rv = _app_ctx_stack.pop() - assert rv is self, "Popped wrong app context. (%r instead of %r)" % (rv, self) + assert rv is self, f"Popped wrong app context. ({rv!r} instead of {self!r})" appcontext_popped.send(self.app) - def __enter__(self): + def __enter__(self) -> "AppContext": self.push() return self - def __exit__(self, exc_type, exc_value, tb): + def __exit__( + self, exc_type: type, exc_value: BaseException, tb: TracebackType + ) -> None: self.pop(exc_value) - if BROKEN_PYPY_CTXMGR_EXIT and exc_type is not None: - reraise(exc_type, exc_value, tb) - -class RequestContext(object): +class RequestContext: """The request context contains all request relevant information. It is created at the beginning of the request and pushed to the `_request_ctx_stack` and removed at the end of it. It will create the @@ -282,7 +293,13 @@ class RequestContext(object): that situation, otherwise your unittests will leak memory. """ - def __init__(self, app, environ, request=None, session=None): + def __init__( + self, + app: "Flask", + environ: dict, + request: t.Optional["Request"] = None, + session: t.Optional["SessionMixin"] = None, + ) -> None: self.app = app if request is None: request = app.request_class(environ) @@ -299,7 +316,7 @@ class RequestContext(object): # other request contexts. Now only if the last level is popped we # get rid of them. Additionally if an application context is missing # one is created implicitly so for each level we add this information - self._implicit_app_ctx_stack = [] + self._implicit_app_ctx_stack: t.List[t.Optional["AppContext"]] = [] # indicator if the context was preserved. Next time another context # is pushed the preserved context is popped. @@ -312,17 +329,17 @@ class RequestContext(object): # Functions that should be executed after the request on the response # object. These will be called before the regular "after_request" # functions. - self._after_request_functions = [] + self._after_request_functions: t.List[AfterRequestCallable] = [] @property - def g(self): + def g(self) -> AppContext: return _app_ctx_stack.top.g @g.setter - def g(self, value): + def g(self, value: AppContext) -> None: _app_ctx_stack.top.g = value - def copy(self): + def copy(self) -> "RequestContext": """Creates a copy of this request context with the same request object. This can be used to move a request context to a different greenlet. Because the actual request object is the same this cannot be used to @@ -342,17 +359,17 @@ class RequestContext(object): session=self.session, ) - def match_request(self): + def match_request(self) -> None: """Can be overridden by a subclass to hook into the matching of the request. """ try: - result = self.url_adapter.match(return_rule=True) - self.request.url_rule, self.request.view_args = result + result = self.url_adapter.match(return_rule=True) # type: ignore + self.request.url_rule, self.request.view_args = result # type: ignore except HTTPException as e: self.request.routing_exception = e - def push(self): + def push(self) -> None: """Binds the request context to the current context.""" # If an exception occurs in debug mode or if context preservation is # activated under exception situations exactly one context stays @@ -376,9 +393,6 @@ class RequestContext(object): else: self._implicit_app_ctx_stack.append(None) - if hasattr(sys, "exc_clear"): - sys.exc_clear() - _request_ctx_stack.push(self) # Open the session at the moment that the request context is available. @@ -392,10 +406,12 @@ class RequestContext(object): if self.session is None: self.session = session_interface.make_null_session(self.app) + # Match the request URL after loading the session, so that the + # session is available in custom URL converters. if self.url_adapter is not None: self.match_request() - def pop(self, exc=_sentinel): + def pop(self, exc: t.Optional[BaseException] = _sentinel) -> None: # type: ignore """Pops the request context and unbinds it by doing that. This will also trigger the execution of functions registered by the :meth:`~flask.Flask.teardown_request` decorator. @@ -404,9 +420,9 @@ class RequestContext(object): Added the `exc` argument. """ app_ctx = self._implicit_app_ctx_stack.pop() + clear_request = False try: - clear_request = False if not self._implicit_app_ctx_stack: self.preserved = False self._preserved_exc = None @@ -414,13 +430,6 @@ class RequestContext(object): exc = sys.exc_info()[1] self.app.do_teardown_request(exc) - # If this interpreter supports clearing the exception information - # we do that now. This will only go into effect on Python 2.x, - # on 3.x it disappears automatically at the end of the exception - # stack. - if hasattr(sys, "exc_clear"): - sys.exc_clear() - request_close = getattr(self.request, "close", None) if request_close is not None: request_close() @@ -437,25 +446,26 @@ class RequestContext(object): if app_ctx is not None: app_ctx.pop(exc) - assert rv is self, "Popped wrong request context. (%r instead of %r)" % ( - rv, - self, - ) + assert ( + rv is self + ), f"Popped wrong request context. ({rv!r} instead of {self!r})" - def auto_pop(self, exc): + def auto_pop(self, exc: t.Optional[BaseException]) -> None: if self.request.environ.get("flask._preserve_context") or ( exc is not None and self.app.preserve_context_on_exception ): self.preserved = True - self._preserved_exc = exc + self._preserved_exc = exc # type: ignore else: self.pop(exc) - def __enter__(self): + def __enter__(self) -> "RequestContext": self.push() return self - def __exit__(self, exc_type, exc_value, tb): + def __exit__( + self, exc_type: type, exc_value: BaseException, tb: TracebackType + ) -> None: # do not pop the request stack if we are in debug mode and an # exception happened. This will allow the debugger to still # access the request object in the interactive shell. Furthermore @@ -463,13 +473,8 @@ class RequestContext(object): # See flask.testing for how this works. self.auto_pop(exc_value) - if BROKEN_PYPY_CTXMGR_EXIT and exc_type is not None: - reraise(exc_type, exc_value, tb) - - def __repr__(self): - return "<%s '%s' [%s] of %s>" % ( - self.__class__.__name__, - self.request.url, - self.request.method, - self.app.name, + def __repr__(self) -> str: + return ( + f"<{type(self).__name__} {self.request.url!r}" + f" [{self.request.method}] of {self.app.name}>" ) diff --git a/libs/flask/debughelpers.py b/libs/flask/debughelpers.py index e475bd1a8..212f7d7ee 100644 --- a/libs/flask/debughelpers.py +++ b/libs/flask/debughelpers.py @@ -1,18 +1,7 @@ -# -*- coding: utf-8 -*- -""" - flask.debughelpers - ~~~~~~~~~~~~~~~~~~ - - Various helpers to make the development experience better. - - :copyright: 2010 Pallets - :license: BSD-3-Clause -""" import os +import typing as t from warnings import warn -from ._compat import implements_to_string -from ._compat import text_type from .app import Flask from .blueprints import Blueprint from .globals import _request_ctx_stack @@ -24,7 +13,6 @@ class UnexpectedUnicodeError(AssertionError, UnicodeError): """ -@implements_to_string class DebugFilesKeyError(KeyError, AssertionError): """Raised from request.files during debugging. The idea is that it can provide a better error message than just a generic KeyError/BadRequest. @@ -33,17 +21,18 @@ class DebugFilesKeyError(KeyError, AssertionError): def __init__(self, request, key): form_matches = request.form.getlist(key) buf = [ - 'You tried to access the file "%s" in the request.files ' - "dictionary but it does not exist. The mimetype for the request " - 'is "%s" instead of "multipart/form-data" which means that no ' - "file contents were transmitted. To fix this error you should " - 'provide enctype="multipart/form-data" in your form.' - % (key, request.mimetype) + f"You tried to access the file {key!r} in the request.files" + " dictionary but it does not exist. The mimetype for the" + f" request is {request.mimetype!r} instead of" + " 'multipart/form-data' which means that no file contents" + " were transmitted. To fix this error you should provide" + ' enctype="multipart/form-data" in your form.' ] if form_matches: + names = ", ".join(repr(x) for x in form_matches) buf.append( "\n\nThe browser instead transmitted some file names. " - "This was submitted: %s" % ", ".join('"%s"' % x for x in form_matches) + f"This was submitted: {names}" ) self.msg = "".join(buf) @@ -60,24 +49,24 @@ class FormDataRoutingRedirect(AssertionError): def __init__(self, request): exc = request.routing_exception buf = [ - "A request was sent to this URL (%s) but a redirect was " - 'issued automatically by the routing system to "%s".' - % (request.url, exc.new_url) + f"A request was sent to this URL ({request.url}) but a" + " redirect was issued automatically by the routing system" + f" to {exc.new_url!r}." ] # In case just a slash was appended we can be extra helpful - if request.base_url + "/" == exc.new_url.split("?")[0]: + if f"{request.base_url}/" == exc.new_url.split("?")[0]: buf.append( - " The URL was defined with a trailing slash so " - "Flask will automatically redirect to the URL " - "with the trailing slash if it was accessed " - "without one." + " The URL was defined with a trailing slash so Flask" + " will automatically redirect to the URL with the" + " trailing slash if it was accessed without one." ) buf.append( - " Make sure to directly send your %s-request to this URL " - "since we can't make browsers or HTTP clients redirect " - "with form data reliably or without user interaction." % request.method + " Make sure to directly send your" + f" {request.method}-request to this URL since we can't make" + " browsers or HTTP clients redirect with form data reliably" + " or without user interaction." ) buf.append("\n\nNote: this exception is only raised in debug mode") AssertionError.__init__(self, "".join(buf).encode("utf-8")) @@ -94,36 +83,37 @@ def attach_enctype_error_multidict(request): def __getitem__(self, key): try: return oldcls.__getitem__(self, key) - except KeyError: + except KeyError as e: if key not in request.form: raise - raise DebugFilesKeyError(request, key) + + raise DebugFilesKeyError(request, key) from e newcls.__name__ = oldcls.__name__ newcls.__module__ = oldcls.__module__ request.files.__class__ = newcls -def _dump_loader_info(loader): - yield "class: %s.%s" % (type(loader).__module__, type(loader).__name__) +def _dump_loader_info(loader) -> t.Generator: + yield f"class: {type(loader).__module__}.{type(loader).__name__}" for key, value in sorted(loader.__dict__.items()): if key.startswith("_"): continue if isinstance(value, (tuple, list)): - if not all(isinstance(x, (str, text_type)) for x in value): + if not all(isinstance(x, str) for x in value): continue - yield "%s:" % key + yield f"{key}:" for item in value: - yield " - %s" % item + yield f" - {item}" continue - elif not isinstance(value, (str, text_type, int, float, bool)): + elif not isinstance(value, (str, int, float, bool)): continue - yield "%s: %r" % (key, value) + yield f"{key}: {value!r}" -def explain_template_loading_attempts(app, template, attempts): +def explain_template_loading_attempts(app: Flask, template, attempts) -> None: """This should help developers understand what failed""" - info = ['Locating template "%s":' % template] + info = [f"Locating template {template!r}:"] total_found = 0 blueprint = None reqctx = _request_ctx_stack.top @@ -132,23 +122,23 @@ def explain_template_loading_attempts(app, template, attempts): for idx, (loader, srcobj, triple) in enumerate(attempts): if isinstance(srcobj, Flask): - src_info = 'application "%s"' % srcobj.import_name + src_info = f"application {srcobj.import_name!r}" elif isinstance(srcobj, Blueprint): - src_info = 'blueprint "%s" (%s)' % (srcobj.name, srcobj.import_name) + src_info = f"blueprint {srcobj.name!r} ({srcobj.import_name})" else: src_info = repr(srcobj) - info.append("% 5d: trying loader of %s" % (idx + 1, src_info)) + info.append(f"{idx + 1:5}: trying loader of {src_info}") for line in _dump_loader_info(loader): - info.append(" %s" % line) + info.append(f" {line}") if triple is None: detail = "no match" else: - detail = "found (%r)" % (triple[1] or "") + detail = f"found ({triple[1] or ''!r})" total_found += 1 - info.append(" -> %s" % detail) + info.append(f" -> {detail}") seems_fishy = False if total_found == 0: @@ -160,24 +150,23 @@ def explain_template_loading_attempts(app, template, attempts): if blueprint is not None and seems_fishy: info.append( - " The template was looked up from an endpoint that " - 'belongs to the blueprint "%s".' % blueprint + " The template was looked up from an endpoint that belongs" + f" to the blueprint {blueprint!r}." ) info.append(" Maybe you did not place a template in the right folder?") - info.append(" See http://flask.pocoo.org/docs/blueprints/#templates") + info.append(" See https://flask.palletsprojects.com/blueprints/#templates") app.logger.info("\n".join(info)) -def explain_ignored_app_run(): +def explain_ignored_app_run() -> None: if os.environ.get("WERKZEUG_RUN_MAIN") != "true": warn( Warning( - "Silently ignoring app.run() because the " - "application is run from the flask command line " - "executable. Consider putting app.run() behind an " - 'if __name__ == "__main__" guard to silence this ' - "warning." + "Silently ignoring app.run() because the application is" + " run from the flask command line executable. Consider" + ' putting app.run() behind an if __name__ == "__main__"' + " guard to silence this warning." ), stacklevel=3, ) diff --git a/libs/flask/globals.py b/libs/flask/globals.py index 6d32dcfd4..6d91c75ed 100644 --- a/libs/flask/globals.py +++ b/libs/flask/globals.py @@ -1,19 +1,14 @@ -# -*- coding: utf-8 -*- -""" - flask.globals - ~~~~~~~~~~~~~ - - Defines all the global objects that are proxies to the current - active context. - - :copyright: 2010 Pallets - :license: BSD-3-Clause -""" +import typing as t from functools import partial from werkzeug.local import LocalProxy from werkzeug.local import LocalStack +if t.TYPE_CHECKING: + from .app import Flask + from .ctx import _AppCtxGlobals + from .sessions import SessionMixin + from .wrappers import Request _request_ctx_err_msg = """\ Working outside of request context. @@ -56,7 +51,9 @@ def _find_app(): # context locals _request_ctx_stack = LocalStack() _app_ctx_stack = LocalStack() -current_app = LocalProxy(_find_app) -request = LocalProxy(partial(_lookup_req_object, "request")) -session = LocalProxy(partial(_lookup_req_object, "session")) -g = LocalProxy(partial(_lookup_app_object, "g")) +current_app: "Flask" = LocalProxy(_find_app) # type: ignore +request: "Request" = LocalProxy(partial(_lookup_req_object, "request")) # type: ignore +session: "SessionMixin" = LocalProxy( # type: ignore + partial(_lookup_req_object, "session") +) +g: "_AppCtxGlobals" = LocalProxy(partial(_lookup_app_object, "g")) # type: ignore diff --git a/libs/flask/helpers.py b/libs/flask/helpers.py index 3f401a5bd..7b8b08702 100644 --- a/libs/flask/helpers.py +++ b/libs/flask/helpers.py @@ -1,39 +1,20 @@ -# -*- coding: utf-8 -*- -""" - flask.helpers - ~~~~~~~~~~~~~ - - Implements various helpers. - - :copyright: 2010 Pallets - :license: BSD-3-Clause -""" -import io -import mimetypes import os import pkgutil -import posixpath import socket import sys -import unicodedata +import typing as t +import warnings +from datetime import datetime +from datetime import timedelta +from functools import lru_cache from functools import update_wrapper from threading import RLock -from time import time -from zlib import adler32 -from jinja2 import FileSystemLoader -from werkzeug.datastructures import Headers -from werkzeug.exceptions import BadRequest +import werkzeug.utils from werkzeug.exceptions import NotFound -from werkzeug.exceptions import RequestedRangeNotSatisfiable from werkzeug.routing import BuildError from werkzeug.urls import url_quote -from werkzeug.wsgi import wrap_file -from ._compat import fspath -from ._compat import PY2 -from ._compat import string_types -from ._compat import text_type from .globals import _app_ctx_stack from .globals import _request_ctx_stack from .globals import current_app @@ -41,19 +22,11 @@ from .globals import request from .globals import session from .signals import message_flashed -# sentinel -_missing = object() +if t.TYPE_CHECKING: + from .wrappers import Response -# what separators does this operating system provide that are not a slash? -# this is used by the send_from_directory function to ensure that nobody is -# able to access files from outside the filesystem. -_os_alt_seps = list( - sep for sep in [os.path.sep, os.path.altsep] if sep not in (None, "/") -) - - -def get_env(): +def get_env() -> str: """Get the environment the app is running in, indicated by the :envvar:`FLASK_ENV` environment variable. The default is ``'production'``. @@ -61,7 +34,7 @@ def get_env(): return os.environ.get("FLASK_ENV") or "production" -def get_debug_flag(): +def get_debug_flag() -> bool: """Get whether debug mode should be enabled for the app, indicated by the :envvar:`FLASK_DEBUG` environment variable. The default is ``True`` if :func:`.get_env` returns ``'development'``, or ``False`` @@ -75,7 +48,7 @@ def get_debug_flag(): return val.lower() not in ("0", "false", "no") -def get_load_dotenv(default=True): +def get_load_dotenv(default: bool = True) -> bool: """Get whether the user has disabled loading dotenv files by setting :envvar:`FLASK_SKIP_DOTENV`. The default is ``True``, load the files. @@ -90,15 +63,11 @@ def get_load_dotenv(default=True): return val.lower() in ("0", "false", "no") -def _endpoint_from_view_func(view_func): - """Internal helper that returns the default endpoint for a given - function. This always is the function name. - """ - assert view_func is not None, "expected view func if endpoint is not provided." - return view_func.__name__ - - -def stream_with_context(generator_or_function): +def stream_with_context( + generator_or_function: t.Union[ + t.Iterator[t.AnyStr], t.Callable[..., t.Iterator[t.AnyStr]] + ] +) -> t.Iterator[t.AnyStr]: """Request contexts disappear when the response is started on the server. This is done for efficiency reasons and to make it less likely to encounter memory leaks with badly written WSGI middlewares. The downside is that if @@ -133,16 +102,16 @@ def stream_with_context(generator_or_function): .. versionadded:: 0.9 """ try: - gen = iter(generator_or_function) + gen = iter(generator_or_function) # type: ignore except TypeError: - def decorator(*args, **kwargs): - gen = generator_or_function(*args, **kwargs) + def decorator(*args: t.Any, **kwargs: t.Any) -> t.Any: + gen = generator_or_function(*args, **kwargs) # type: ignore return stream_with_context(gen) - return update_wrapper(decorator, generator_or_function) + return update_wrapper(decorator, generator_or_function) # type: ignore - def generator(): + def generator() -> t.Generator: ctx = _request_ctx_stack.top if ctx is None: raise RuntimeError( @@ -159,11 +128,10 @@ def stream_with_context(generator_or_function): # don't need that because they are closed on their destruction # automatically. try: - for item in gen: - yield item + yield from gen finally: if hasattr(gen, "close"): - gen.close() + gen.close() # type: ignore # The trick is to start the generator. Then the code execution runs until # the first dummy None is yielded at which point the context was already @@ -174,7 +142,7 @@ def stream_with_context(generator_or_function): return wrapped_g -def make_response(*args): +def make_response(*args: t.Any) -> "Response": """Sometimes it is necessary to set additional headers in a view. Because views do not have to return response objects but can return a value that is converted into a response object by Flask itself, it becomes tricky to @@ -223,7 +191,7 @@ def make_response(*args): return current_app.make_response(args) -def url_for(endpoint, **values): +def url_for(endpoint: str, **values: t.Any) -> str: """Generates a URL to the given endpoint with the method provided. Variable arguments that are unknown to the target endpoint are appended @@ -236,7 +204,7 @@ def url_for(endpoint, **values): url_for('.index') - For more information, head over to the :ref:`Quickstart `. + See :ref:`url-building`. Configuration values ``APPLICATION_ROOT`` and ``SERVER_NAME`` are only used when generating URLs outside of a request context. @@ -262,7 +230,7 @@ def url_for(endpoint, **values): # Re-raise the BuildError, in context of original traceback. exc_type, exc_value, tb = sys.exc_info() if exc_value is error: - raise exc_type, exc_value, tb + raise exc_type(exc_value).with_traceback(tb) else: raise error # url_for will use this result, instead of raising BuildError. @@ -293,9 +261,9 @@ def url_for(endpoint, **values): :param _scheme: a string specifying the desired URL scheme. The `_external` parameter must be set to ``True`` or a :exc:`ValueError` is raised. The default behavior uses the same scheme as the current request, or - ``PREFERRED_URL_SCHEME`` from the :ref:`app configuration ` if no - request context is available. As of Werkzeug 0.10, this also can be set - to an empty string to build protocol-relative URLs. + :data:`PREFERRED_URL_SCHEME` if no request context is available. + This also can be set to an empty string to build protocol-relative + URLs. :param _anchor: if provided this is added as anchor to the URL. :param _method: if provided this explicitly specifies an HTTP method. """ @@ -317,7 +285,7 @@ def url_for(endpoint, **values): if endpoint[:1] == ".": if blueprint_name is not None: - endpoint = blueprint_name + endpoint + endpoint = f"{blueprint_name}{endpoint}" else: endpoint = endpoint[1:] @@ -370,11 +338,11 @@ def url_for(endpoint, **values): return appctx.app.handle_url_build_error(error, endpoint, values) if anchor is not None: - rv += "#" + url_quote(anchor) + rv += f"#{url_quote(anchor)}" return rv -def get_template_attribute(template_name, attribute): +def get_template_attribute(template_name: str, attribute: str) -> t.Any: """Loads a macro (or variable) a template exports. This can be used to invoke a macro from within Python code. If you for example have a template named :file:`_cider.html` with the following contents: @@ -396,7 +364,7 @@ def get_template_attribute(template_name, attribute): return getattr(current_app.jinja_env.get_template(template_name).module, attribute) -def flash(message, category="message"): +def flash(message: str, category: str = "message") -> None: """Flashes a message to the next request. In order to remove the flashed message from the session and to display it to the user, the template has to call :func:`get_flashed_messages`. @@ -422,11 +390,15 @@ def flash(message, category="message"): flashes.append((category, message)) session["_flashes"] = flashes message_flashed.send( - current_app._get_current_object(), message=message, category=category + current_app._get_current_object(), # type: ignore + message=message, + category=category, ) -def get_flashed_messages(with_categories=False, category_filter=()): +def get_flashed_messages( + with_categories: bool = False, category_filter: t.Iterable[str] = () +) -> t.Union[t.List[str], t.List[t.Tuple[str, str]]]: """Pulls all flashed messages from the session and returns them. Further calls in the same request to the function will return the same messages. By default just the messages are returned, @@ -443,7 +415,7 @@ def get_flashed_messages(with_categories=False, category_filter=()): * `category_filter` filters the messages down to only those matching the provided categories. - See :ref:`message-flashing-pattern` for examples. + See :doc:`/patterns/flashing` for examples. .. versionchanged:: 0.3 `with_categories` parameter added. @@ -452,7 +424,8 @@ def get_flashed_messages(with_categories=False, category_filter=()): `category_filter` parameter added. :param with_categories: set to ``True`` to also receive categories. - :param category_filter: whitelist of categories to limit return values + :param category_filter: filter of categories to limit return values. Only + categories in the list will be returned. """ flashes = _request_ctx_stack.top.flashes if flashes is None: @@ -466,688 +439,398 @@ def get_flashed_messages(with_categories=False, category_filter=()): return flashes +def _prepare_send_file_kwargs( + download_name: t.Optional[str] = None, + attachment_filename: t.Optional[str] = None, + etag: t.Optional[t.Union[bool, str]] = None, + add_etags: t.Optional[t.Union[bool]] = None, + max_age: t.Optional[ + t.Union[int, t.Callable[[t.Optional[str]], t.Optional[int]]] + ] = None, + cache_timeout: t.Optional[int] = None, + **kwargs: t.Any, +) -> t.Dict[str, t.Any]: + if attachment_filename is not None: + warnings.warn( + "The 'attachment_filename' parameter has been renamed to" + " 'download_name'. The old name will be removed in Flask" + " 2.1.", + DeprecationWarning, + stacklevel=3, + ) + download_name = attachment_filename + + if cache_timeout is not None: + warnings.warn( + "The 'cache_timeout' parameter has been renamed to" + " 'max_age'. The old name will be removed in Flask 2.1.", + DeprecationWarning, + stacklevel=3, + ) + max_age = cache_timeout + + if add_etags is not None: + warnings.warn( + "The 'add_etags' parameter has been renamed to 'etag'. The" + " old name will be removed in Flask 2.1.", + DeprecationWarning, + stacklevel=3, + ) + etag = add_etags + + if max_age is None: + max_age = current_app.get_send_file_max_age + + kwargs.update( + environ=request.environ, + download_name=download_name, + etag=etag, + max_age=max_age, + use_x_sendfile=current_app.use_x_sendfile, + response_class=current_app.response_class, + _root_path=current_app.root_path, # type: ignore + ) + return kwargs + + def send_file( - filename_or_fp, - mimetype=None, - as_attachment=False, - attachment_filename=None, - add_etags=True, - cache_timeout=None, - conditional=False, - last_modified=None, + path_or_file: t.Union[os.PathLike, str, t.BinaryIO], + mimetype: t.Optional[str] = None, + as_attachment: bool = False, + download_name: t.Optional[str] = None, + attachment_filename: t.Optional[str] = None, + conditional: bool = True, + etag: t.Union[bool, str] = True, + add_etags: t.Optional[bool] = None, + last_modified: t.Optional[t.Union[datetime, int, float]] = None, + max_age: t.Optional[ + t.Union[int, t.Callable[[t.Optional[str]], t.Optional[int]]] + ] = None, + cache_timeout: t.Optional[int] = None, ): - """Sends the contents of a file to the client. This will use the - most efficient method available and configured. By default it will - try to use the WSGI server's file_wrapper support. Alternatively - you can set the application's :attr:`~Flask.use_x_sendfile` attribute - to ``True`` to directly emit an ``X-Sendfile`` header. This however - requires support of the underlying webserver for ``X-Sendfile``. + """Send the contents of a file to the client. - By default it will try to guess the mimetype for you, but you can - also explicitly provide one. For extra security you probably want - to send certain files as attachment (HTML for instance). The mimetype - guessing requires a `filename` or an `attachment_filename` to be - provided. + The first argument can be a file path or a file-like object. Paths + are preferred in most cases because Werkzeug can manage the file and + get extra information from the path. Passing a file-like object + requires that the file is opened in binary mode, and is mostly + useful when building a file in memory with :class:`io.BytesIO`. - ETags will also be attached automatically if a `filename` is provided. You - can turn this off by setting `add_etags=False`. + Never pass file paths provided by a user. The path is assumed to be + trusted, so a user could craft a path to access a file you didn't + intend. Use :func:`send_from_directory` to safely serve + user-requested paths from within a directory. - If `conditional=True` and `filename` is provided, this method will try to - upgrade the response stream to support range requests. This will allow - the request to be answered with partial content response. + If the WSGI server sets a ``file_wrapper`` in ``environ``, it is + used, otherwise Werkzeug's built-in wrapper is used. Alternatively, + if the HTTP server supports ``X-Sendfile``, configuring Flask with + ``USE_X_SENDFILE = True`` will tell the server to send the given + path, which is much more efficient than reading it in Python. - Please never pass filenames to this function from user sources; - you should use :func:`send_from_directory` instead. + :param path_or_file: The path to the file to send, relative to the + current working directory if a relative path is given. + Alternatively, a file-like object opened in binary mode. Make + sure the file pointer is seeked to the start of the data. + :param mimetype: The MIME type to send for the file. If not + provided, it will try to detect it from the file name. + :param as_attachment: Indicate to a browser that it should offer to + save the file instead of displaying it. + :param download_name: The default name browsers will use when saving + the file. Defaults to the passed file name. + :param conditional: Enable conditional and range responses based on + request headers. Requires passing a file path and ``environ``. + :param etag: Calculate an ETag for the file, which requires passing + a file path. Can also be a string to use instead. + :param last_modified: The last modified time to send for the file, + in seconds. If not provided, it will try to detect it from the + file path. + :param max_age: How long the client should cache the file, in + seconds. If set, ``Cache-Control`` will be ``public``, otherwise + it will be ``no-cache`` to prefer conditional caching. - .. versionadded:: 0.2 + .. versionchanged:: 2.0 + ``download_name`` replaces the ``attachment_filename`` + parameter. If ``as_attachment=False``, it is passed with + ``Content-Disposition: inline`` instead. - .. versionadded:: 0.5 - The `add_etags`, `cache_timeout` and `conditional` parameters were - added. The default behavior is now to attach etags. + .. versionchanged:: 2.0 + ``max_age`` replaces the ``cache_timeout`` parameter. + ``conditional`` is enabled and ``max_age`` is not set by + default. - .. versionchanged:: 0.7 - mimetype guessing and etag support for file objects was - deprecated because it was unreliable. Pass a filename if you are - able to, otherwise attach an etag yourself. This functionality - will be removed in Flask 1.0 + .. versionchanged:: 2.0 + ``etag`` replaces the ``add_etags`` parameter. It can be a + string to use instead of generating one. - .. versionchanged:: 0.9 - cache_timeout pulls its default from application config, when None. + .. versionchanged:: 2.0 + Passing a file-like object that inherits from + :class:`~io.TextIOBase` will raise a :exc:`ValueError` rather + than sending an empty file. - .. versionchanged:: 0.12 - The filename is no longer automatically inferred from file objects. If - you want to use automatic mimetype and etag support, pass a filepath via - `filename_or_fp` or `attachment_filename`. + .. versionadded:: 2.0 + Moved the implementation to Werkzeug. This is now a wrapper to + pass some Flask-specific arguments. - .. versionchanged:: 0.12 - The `attachment_filename` is preferred over `filename` for MIME-type - detection. + .. versionchanged:: 1.1 + ``filename`` may be a :class:`~os.PathLike` object. - .. versionchanged:: 1.0 - UTF-8 filenames, as specified in `RFC 2231`_, are supported. - - .. _RFC 2231: https://tools.ietf.org/html/rfc2231#section-4 + .. versionchanged:: 1.1 + Passing a :class:`~io.BytesIO` object supports range requests. .. versionchanged:: 1.0.3 Filenames are encoded with ASCII instead of Latin-1 for broader compatibility with WSGI servers. - .. versionchanged:: 1.1 - Filename may be a :class:`~os.PathLike` object. + .. versionchanged:: 1.0 + UTF-8 filenames as specified in :rfc:`2231` are supported. - .. versionadded:: 1.1 - Partial content supports :class:`~io.BytesIO`. + .. versionchanged:: 0.12 + The filename is no longer automatically inferred from file + objects. If you want to use automatic MIME and etag support, + pass a filename via ``filename_or_fp`` or + ``attachment_filename``. - :param filename_or_fp: the filename of the file to send. - This is relative to the :attr:`~Flask.root_path` - if a relative path is specified. - Alternatively a file object might be provided in - which case ``X-Sendfile`` might not work and fall - back to the traditional method. Make sure that the - file pointer is positioned at the start of data to - send before calling :func:`send_file`. - :param mimetype: the mimetype of the file if provided. If a file path is - given, auto detection happens as fallback, otherwise an - error will be raised. - :param as_attachment: set to ``True`` if you want to send this file with - a ``Content-Disposition: attachment`` header. - :param attachment_filename: the filename for the attachment if it - differs from the file's filename. - :param add_etags: set to ``False`` to disable attaching of etags. - :param conditional: set to ``True`` to enable conditional responses. + .. versionchanged:: 0.12 + ``attachment_filename`` is preferred over ``filename`` for MIME + detection. - :param cache_timeout: the timeout in seconds for the headers. When ``None`` - (default), this value is set by - :meth:`~Flask.get_send_file_max_age` of - :data:`~flask.current_app`. - :param last_modified: set the ``Last-Modified`` header to this value, - a :class:`~datetime.datetime` or timestamp. - If a file was passed, this overrides its mtime. + .. versionchanged:: 0.9 + ``cache_timeout`` defaults to + :meth:`Flask.get_send_file_max_age`. + + .. versionchanged:: 0.7 + MIME guessing and etag support for file-like objects was + deprecated because it was unreliable. Pass a filename if you are + able to, otherwise attach an etag yourself. + + .. versionchanged:: 0.5 + The ``add_etags``, ``cache_timeout`` and ``conditional`` + parameters were added. The default behavior is to add etags. + + .. versionadded:: 0.2 """ - mtime = None - fsize = None - - if hasattr(filename_or_fp, "__fspath__"): - filename_or_fp = fspath(filename_or_fp) - - if isinstance(filename_or_fp, string_types): - filename = filename_or_fp - if not os.path.isabs(filename): - filename = os.path.join(current_app.root_path, filename) - file = None - if attachment_filename is None: - attachment_filename = os.path.basename(filename) - else: - file = filename_or_fp - filename = None - - if mimetype is None: - if attachment_filename is not None: - mimetype = ( - mimetypes.guess_type(attachment_filename)[0] - or "application/octet-stream" - ) - - if mimetype is None: - raise ValueError( - "Unable to infer MIME-type because no filename is available. " - "Please set either `attachment_filename`, pass a filepath to " - "`filename_or_fp` or set your own MIME-type via `mimetype`." - ) - - headers = Headers() - if as_attachment: - if attachment_filename is None: - raise TypeError("filename unavailable, required for sending as attachment") - - if not isinstance(attachment_filename, text_type): - attachment_filename = attachment_filename.decode("utf-8") - - try: - attachment_filename = attachment_filename.encode("ascii") - except UnicodeEncodeError: - filenames = { - "filename": unicodedata.normalize("NFKD", attachment_filename).encode( - "ascii", "ignore" - ), - "filename*": "UTF-8''%s" % url_quote(attachment_filename, safe=b""), - } - else: - filenames = {"filename": attachment_filename} - - headers.add("Content-Disposition", "attachment", **filenames) - - if current_app.use_x_sendfile and filename: - if file is not None: - file.close() - headers["X-Sendfile"] = filename - fsize = os.path.getsize(filename) - headers["Content-Length"] = fsize - data = None - else: - if file is None: - file = open(filename, "rb") - mtime = os.path.getmtime(filename) - fsize = os.path.getsize(filename) - headers["Content-Length"] = fsize - elif isinstance(file, io.BytesIO): - try: - fsize = file.getbuffer().nbytes - except AttributeError: - # Python 2 doesn't have getbuffer - fsize = len(file.getvalue()) - headers["Content-Length"] = fsize - data = wrap_file(request.environ, file) - - rv = current_app.response_class( - data, mimetype=mimetype, headers=headers, direct_passthrough=True + return werkzeug.utils.send_file( + **_prepare_send_file_kwargs( + path_or_file=path_or_file, + environ=request.environ, + mimetype=mimetype, + as_attachment=as_attachment, + download_name=download_name, + attachment_filename=attachment_filename, + conditional=conditional, + etag=etag, + add_etags=add_etags, + last_modified=last_modified, + max_age=max_age, + cache_timeout=cache_timeout, + ) ) - if last_modified is not None: - rv.last_modified = last_modified - elif mtime is not None: - rv.last_modified = mtime - rv.cache_control.public = True - if cache_timeout is None: - cache_timeout = current_app.get_send_file_max_age(filename) - if cache_timeout is not None: - rv.cache_control.max_age = cache_timeout - rv.expires = int(time() + cache_timeout) +def safe_join(directory: str, *pathnames: str) -> str: + """Safely join zero or more untrusted path components to a base + directory to avoid escaping the base directory. - if add_etags and filename is not None: - from warnings import warn - - try: - rv.set_etag( - "%s-%s-%s" - % ( - os.path.getmtime(filename), - os.path.getsize(filename), - adler32( - filename.encode("utf-8") - if isinstance(filename, text_type) - else filename - ) - & 0xFFFFFFFF, - ) - ) - except OSError: - warn( - "Access %s failed, maybe it does not exist, so ignore etags in " - "headers" % filename, - stacklevel=2, - ) - - if conditional: - try: - rv = rv.make_conditional(request, accept_ranges=True, complete_length=fsize) - except RequestedRangeNotSatisfiable: - if file is not None: - file.close() - raise - # make sure we don't send x-sendfile for servers that - # ignore the 304 status code for x-sendfile. - if rv.status_code == 304: - rv.headers.pop("x-sendfile", None) - return rv - - -def safe_join(directory, *pathnames): - """Safely join `directory` and zero or more untrusted `pathnames` - components. - - Example usage:: - - @app.route('/wiki/') - def wiki_page(filename): - filename = safe_join(app.config['WIKI_FOLDER'], filename) - with open(filename, 'rb') as fd: - content = fd.read() # Read and process the file content... - - :param directory: the trusted base directory. - :param pathnames: the untrusted pathnames relative to that directory. - :raises: :class:`~werkzeug.exceptions.NotFound` if one or more passed - paths fall out of its boundaries. + :param directory: The trusted base directory. + :param pathnames: The untrusted path components relative to the + base directory. + :return: A safe path, otherwise ``None``. """ + warnings.warn( + "'flask.helpers.safe_join' is deprecated and will be removed in" + " Flask 2.1. Use 'werkzeug.utils.safe_join' instead.", + DeprecationWarning, + stacklevel=2, + ) + path = werkzeug.utils.safe_join(directory, *pathnames) - parts = [directory] + if path is None: + raise NotFound() - for filename in pathnames: - if filename != "": - filename = posixpath.normpath(filename) - - if ( - any(sep in filename for sep in _os_alt_seps) - or os.path.isabs(filename) - or filename == ".." - or filename.startswith("../") - ): - raise NotFound() - - parts.append(filename) - - return posixpath.join(*parts) + return path -def send_from_directory(directory, filename, **options): - """Send a file from a given directory with :func:`send_file`. This - is a secure way to quickly expose static files from an upload folder - or something similar. +def send_from_directory( + directory: t.Union[os.PathLike, str], + path: t.Union[os.PathLike, str], + filename: t.Optional[str] = None, + **kwargs: t.Any, +) -> "Response": + """Send a file from within a directory using :func:`send_file`. - Example usage:: + .. code-block:: python - @app.route('/uploads/') - def download_file(filename): - return send_from_directory(app.config['UPLOAD_FOLDER'], - filename, as_attachment=True) + @app.route("/uploads/") + def download_file(name): + return send_from_directory( + app.config['UPLOAD_FOLDER'], name, as_attachment=True + ) - .. admonition:: Sending files and Performance + This is a secure way to serve files from a folder, such as static + files or uploads. Uses :func:`~werkzeug.security.safe_join` to + ensure the path coming from the client is not maliciously crafted to + point outside the specified directory. - It is strongly recommended to activate either ``X-Sendfile`` support in - your webserver or (if no authentication happens) to tell the webserver - to serve files for the given path on its own without calling into the - web application for improved performance. + If the final path does not point to an existing regular file, + raises a 404 :exc:`~werkzeug.exceptions.NotFound` error. + + :param directory: The directory that ``path`` must be located under. + :param path: The path to the file to send, relative to + ``directory``. + :param kwargs: Arguments to pass to :func:`send_file`. + + .. versionchanged:: 2.0 + ``path`` replaces the ``filename`` parameter. + + .. versionadded:: 2.0 + Moved the implementation to Werkzeug. This is now a wrapper to + pass some Flask-specific arguments. .. versionadded:: 0.5 - - :param directory: the directory where all the files are stored. - :param filename: the filename relative to that directory to - download. - :param options: optional keyword arguments that are directly - forwarded to :func:`send_file`. """ - filename = fspath(filename) - directory = fspath(directory) - filename = safe_join(directory, filename) - if not os.path.isabs(filename): - filename = os.path.join(current_app.root_path, filename) - try: - if not os.path.isfile(filename): - raise NotFound() - except (TypeError, ValueError): - raise BadRequest() - options.setdefault("conditional", True) - return send_file(filename, **options) + if filename is not None: + warnings.warn( + "The 'filename' parameter has been renamed to 'path'. The" + " old name will be removed in Flask 2.1.", + DeprecationWarning, + stacklevel=2, + ) + path = filename + + return werkzeug.utils.send_from_directory( # type: ignore + directory, path, **_prepare_send_file_kwargs(**kwargs) + ) -def get_root_path(import_name): - """Returns the path to a package or cwd if that cannot be found. This - returns the path of a package or the folder that contains a module. +def get_root_path(import_name: str) -> str: + """Find the root path of a package, or the path that contains a + module. If it cannot be found, returns the current working + directory. - Not to be confused with the package path returned by :func:`find_package`. + Not to be confused with the value returned by :func:`find_package`. + + :meta private: """ - # Module already imported and has a file attribute. Use that first. + # Module already imported and has a file attribute. Use that first. mod = sys.modules.get(import_name) + if mod is not None and hasattr(mod, "__file__"): return os.path.dirname(os.path.abspath(mod.__file__)) # Next attempt: check the loader. loader = pkgutil.get_loader(import_name) - # Loader does not exist or we're referring to an unloaded main module - # or a main module without path (interactive sessions), go with the - # current working directory. + # Loader does not exist or we're referring to an unloaded main + # module or a main module without path (interactive sessions), go + # with the current working directory. if loader is None or import_name == "__main__": return os.getcwd() - # For .egg, zipimporter does not have get_filename until Python 2.7. - # Some other loaders might exhibit the same behavior. if hasattr(loader, "get_filename"): - filepath = loader.get_filename(import_name) + filepath = loader.get_filename(import_name) # type: ignore else: # Fall back to imports. __import__(import_name) mod = sys.modules[import_name] filepath = getattr(mod, "__file__", None) - # If we don't have a filepath it might be because we are a - # namespace package. In this case we pick the root path from the - # first module that is contained in our package. + # If we don't have a file path it might be because it is a + # namespace package. In this case pick the root path from the + # first module that is contained in the package. if filepath is None: raise RuntimeError( - "No root path can be found for the provided " - 'module "%s". This can happen because the ' - "module came from an import hook that does " - "not provide file name information or because " - "it's a namespace package. In this case " - "the root path needs to be explicitly " - "provided." % import_name + "No root path can be found for the provided module" + f" {import_name!r}. This can happen because the module" + " came from an import hook that does not provide file" + " name information or because it's a namespace package." + " In this case the root path needs to be explicitly" + " provided." ) # filepath is import_name.py for a module, or __init__.py for a package. return os.path.dirname(os.path.abspath(filepath)) -def _matching_loader_thinks_module_is_package(loader, mod_name): - """Given the loader that loaded a module and the module this function - attempts to figure out if the given module is actually a package. - """ - # If the loader can tell us if something is a package, we can - # directly ask the loader. - if hasattr(loader, "is_package"): - return loader.is_package(mod_name) - # importlib's namespace loaders do not have this functionality but - # all the modules it loads are packages, so we can take advantage of - # this information. - elif ( - loader.__class__.__module__ == "_frozen_importlib" - and loader.__class__.__name__ == "NamespaceLoader" - ): - return True - # Otherwise we need to fail with an error that explains what went - # wrong. - raise AttributeError( - ( - "%s.is_package() method is missing but is required by Flask of " - "PEP 302 import hooks. If you do not use import hooks and " - "you encounter this error please file a bug against Flask." - ) - % loader.__class__.__name__ - ) +class locked_cached_property(werkzeug.utils.cached_property): + """A :func:`property` that is only evaluated once. Like + :class:`werkzeug.utils.cached_property` except access uses a lock + for thread safety. - -def _find_package_path(root_mod_name): - """Find the path where the module's root exists in""" - if sys.version_info >= (3, 4): - import importlib.util - - try: - spec = importlib.util.find_spec(root_mod_name) - if spec is None: - raise ValueError("not found") - # ImportError: the machinery told us it does not exist - # ValueError: - # - the module name was invalid - # - the module name is __main__ - # - *we* raised `ValueError` due to `spec` being `None` - except (ImportError, ValueError): - pass # handled below - else: - # namespace package - if spec.origin in {"namespace", None}: - return os.path.dirname(next(iter(spec.submodule_search_locations))) - # a package (with __init__.py) - elif spec.submodule_search_locations: - return os.path.dirname(os.path.dirname(spec.origin)) - # just a normal module - else: - return os.path.dirname(spec.origin) - - # we were unable to find the `package_path` using PEP 451 loaders - loader = pkgutil.get_loader(root_mod_name) - if loader is None or root_mod_name == "__main__": - # import name is not found, or interactive/main module - return os.getcwd() - else: - # For .egg, zipimporter does not have get_filename until Python 2.7. - if hasattr(loader, "get_filename"): - filename = loader.get_filename(root_mod_name) - elif hasattr(loader, "archive"): - # zipimporter's loader.archive points to the .egg or .zip - # archive filename is dropped in call to dirname below. - filename = loader.archive - else: - # At least one loader is missing both get_filename and archive: - # Google App Engine's HardenedModulesHook - # - # Fall back to imports. - __import__(root_mod_name) - filename = sys.modules[root_mod_name].__file__ - package_path = os.path.abspath(os.path.dirname(filename)) - - # In case the root module is a package we need to chop of the - # rightmost part. This needs to go through a helper function - # because of python 3.3 namespace packages. - if _matching_loader_thinks_module_is_package(loader, root_mod_name): - package_path = os.path.dirname(package_path) - - return package_path - - -def find_package(import_name): - """Finds a package and returns the prefix (or None if the package is - not installed) as well as the folder that contains the package or - module as a tuple. The package path returned is the module that would - have to be added to the pythonpath in order to make it possible to - import the module. The prefix is the path below which a UNIX like - folder structure exists (lib, share etc.). - """ - root_mod_name, _, _ = import_name.partition(".") - package_path = _find_package_path(root_mod_name) - site_parent, site_folder = os.path.split(package_path) - py_prefix = os.path.abspath(sys.prefix) - if package_path.startswith(py_prefix): - return py_prefix, package_path - elif site_folder.lower() == "site-packages": - parent, folder = os.path.split(site_parent) - # Windows like installations - if folder.lower() == "lib": - base_dir = parent - # UNIX like installations - elif os.path.basename(parent).lower() == "lib": - base_dir = os.path.dirname(parent) - else: - base_dir = site_parent - return base_dir, package_path - return None, package_path - - -class locked_cached_property(object): - """A decorator that converts a function into a lazy property. The - function wrapped is called the first time to retrieve the result - and then that calculated result is used the next time you access - the value. Works like the one in Werkzeug but has a lock for - thread safety. + .. versionchanged:: 2.0 + Inherits from Werkzeug's ``cached_property`` (and ``property``). """ - def __init__(self, func, name=None, doc=None): - self.__name__ = name or func.__name__ - self.__module__ = func.__module__ - self.__doc__ = doc or func.__doc__ - self.func = func + def __init__( + self, + fget: t.Callable[[t.Any], t.Any], + name: t.Optional[str] = None, + doc: t.Optional[str] = None, + ) -> None: + super().__init__(fget, name=name, doc=doc) self.lock = RLock() - def __get__(self, obj, type=None): + def __get__(self, obj: object, type: type = None) -> t.Any: # type: ignore if obj is None: return self + with self.lock: - value = obj.__dict__.get(self.__name__, _missing) - if value is _missing: - value = self.func(obj) - obj.__dict__[self.__name__] = value - return value + return super().__get__(obj, type=type) + + def __set__(self, obj: object, value: t.Any) -> None: + with self.lock: + super().__set__(obj, value) + + def __delete__(self, obj: object) -> None: + with self.lock: + super().__delete__(obj) -class _PackageBoundObject(object): - #: The name of the package or module that this app belongs to. Do not - #: change this once it is set by the constructor. - import_name = None - - #: Location of the template files to be added to the template lookup. - #: ``None`` if templates should not be added. - template_folder = None - - #: Absolute path to the package on the filesystem. Used to look up - #: resources contained in the package. - root_path = None - - def __init__(self, import_name, template_folder=None, root_path=None): - self.import_name = import_name - self.template_folder = template_folder - - if root_path is None: - root_path = get_root_path(self.import_name) - - self.root_path = root_path - self._static_folder = None - self._static_url_path = None - - # circular import - from .cli import AppGroup - - #: The Click command group for registration of CLI commands - #: on the application and associated blueprints. These commands - #: are accessible via the :command:`flask` command once the - #: application has been discovered and blueprints registered. - self.cli = AppGroup() - - @property - def static_folder(self): - """The absolute path to the configured static folder.""" - if self._static_folder is not None: - return os.path.join(self.root_path, self._static_folder) - - @static_folder.setter - def static_folder(self, value): - self._static_folder = value - - @property - def static_url_path(self): - """The URL prefix that the static route will be accessible from. - - If it was not configured during init, it is derived from - :attr:`static_folder`. - """ - if self._static_url_path is not None: - return self._static_url_path - - if self.static_folder is not None: - basename = os.path.basename(self.static_folder) - return ("/" + basename).rstrip("/") - - @static_url_path.setter - def static_url_path(self, value): - if value is not None: - value = value.rstrip("/") - - self._static_url_path = value - - @property - def has_static_folder(self): - """This is ``True`` if the package bound object's container has a - folder for static files. - - .. versionadded:: 0.5 - """ - return self.static_folder is not None - - @locked_cached_property - def jinja_loader(self): - """The Jinja loader for this package bound object. - - .. versionadded:: 0.5 - """ - if self.template_folder is not None: - return FileSystemLoader(os.path.join(self.root_path, self.template_folder)) - - def get_send_file_max_age(self, filename): - """Provides default cache_timeout for the :func:`send_file` functions. - - By default, this function returns ``SEND_FILE_MAX_AGE_DEFAULT`` from - the configuration of :data:`~flask.current_app`. - - Static file functions such as :func:`send_from_directory` use this - function, and :func:`send_file` calls this function on - :data:`~flask.current_app` when the given cache_timeout is ``None``. If a - cache_timeout is given in :func:`send_file`, that timeout is used; - otherwise, this method is called. - - This allows subclasses to change the behavior when sending files based - on the filename. For example, to set the cache timeout for .js files - to 60 seconds:: - - class MyFlask(flask.Flask): - def get_send_file_max_age(self, name): - if name.lower().endswith('.js'): - return 60 - return flask.Flask.get_send_file_max_age(self, name) - - .. versionadded:: 0.9 - """ - return total_seconds(current_app.send_file_max_age_default) - - def send_static_file(self, filename): - """Function used internally to send static files from the static - folder to the browser. - - .. versionadded:: 0.5 - """ - if not self.has_static_folder: - raise RuntimeError("No static folder for this object") - # Ensure get_send_file_max_age is called in all cases. - # Here, we ensure get_send_file_max_age is called for Blueprints. - cache_timeout = self.get_send_file_max_age(filename) - return send_from_directory( - self.static_folder, filename, cache_timeout=cache_timeout - ) - - def open_resource(self, resource, mode="rb"): - """Opens a resource from the application's resource folder. To see - how this works, consider the following folder structure:: - - /myapplication.py - /schema.sql - /static - /style.css - /templates - /layout.html - /index.html - - If you want to open the :file:`schema.sql` file you would do the - following:: - - with app.open_resource('schema.sql') as f: - contents = f.read() - do_something_with(contents) - - :param resource: the name of the resource. To access resources within - subfolders use forward slashes as separator. - :param mode: Open file in this mode. Only reading is supported, - valid values are "r" (or "rt") and "rb". - """ - if mode not in {"r", "rt", "rb"}: - raise ValueError("Resources can only be opened for reading") - - return open(os.path.join(self.root_path, resource), mode) - - -def total_seconds(td): +def total_seconds(td: timedelta) -> int: """Returns the total seconds from a timedelta object. :param timedelta td: the timedelta to be converted in seconds :returns: number of seconds :rtype: int + + .. deprecated:: 2.0 + Will be removed in Flask 2.1. Use + :meth:`timedelta.total_seconds` instead. """ + warnings.warn( + "'total_seconds' is deprecated and will be removed in Flask" + " 2.1. Use 'timedelta.total_seconds' instead.", + DeprecationWarning, + stacklevel=2, + ) return td.days * 60 * 60 * 24 + td.seconds -def is_ip(value): +def is_ip(value: str) -> bool: """Determine if the given string is an IP address. - Python 2 on Windows doesn't provide ``inet_pton``, so this only - checks IPv4 addresses in that environment. - :param value: value to check :type value: str :return: True if string is an IP address :rtype: bool """ - if PY2 and os.name == "nt": - try: - socket.inet_aton(value) - return True - except socket.error: - return False - for family in (socket.AF_INET, socket.AF_INET6): try: socket.inet_pton(family, value) - except socket.error: + except OSError: pass else: return True return False + + +@lru_cache(maxsize=None) +def _split_blueprint_path(name: str) -> t.List[str]: + out: t.List[str] = [name] + + if "." in name: + out.extend(_split_blueprint_path(name.rpartition(".")[0])) + + return out diff --git a/libs/flask/json/__init__.py b/libs/flask/json/__init__.py index a141068b1..10d5123a3 100644 --- a/libs/flask/json/__init__.py +++ b/libs/flask/json/__init__.py @@ -1,357 +1,342 @@ -# -*- coding: utf-8 -*- -""" -flask.json -~~~~~~~~~~ - -:copyright: 2010 Pallets -:license: BSD-3-Clause -""" -import codecs +import decimal import io +import json as _json +import typing as t import uuid +import warnings from datetime import date -from datetime import datetime -from itsdangerous import json as _json -from jinja2 import Markup +from jinja2.utils import htmlsafe_json_dumps as _jinja_htmlsafe_dumps from werkzeug.http import http_date -from .._compat import PY2 -from .._compat import text_type from ..globals import current_app from ..globals import request +if t.TYPE_CHECKING: + from ..app import Flask + from ..wrappers import Response + try: import dataclasses except ImportError: - dataclasses = None - -# Figure out if simplejson escapes slashes. This behavior was changed -# from one version to another without reason. -_slash_escape = "\\/" not in _json.dumps("/") - - -__all__ = [ - "dump", - "dumps", - "load", - "loads", - "htmlsafe_dump", - "htmlsafe_dumps", - "JSONDecoder", - "JSONEncoder", - "jsonify", -] - - -def _wrap_reader_for_text(fp, encoding): - if isinstance(fp.read(0), bytes): - fp = io.TextIOWrapper(io.BufferedReader(fp), encoding) - return fp - - -def _wrap_writer_for_text(fp, encoding): - try: - fp.write("") - except TypeError: - fp = io.TextIOWrapper(fp, encoding) - return fp + # Python < 3.7 + dataclasses = None # type: ignore class JSONEncoder(_json.JSONEncoder): - """The default Flask JSON encoder. This one extends the default - encoder by also supporting ``datetime``, ``UUID``, ``dataclasses``, - and ``Markup`` objects. + """The default JSON encoder. Handles extra types compared to the + built-in :class:`json.JSONEncoder`. - ``datetime`` objects are serialized as RFC 822 datetime strings. - This is the same as the HTTP date format. + - :class:`datetime.datetime` and :class:`datetime.date` are + serialized to :rfc:`822` strings. This is the same as the HTTP + date format. + - :class:`uuid.UUID` is serialized to a string. + - :class:`dataclasses.dataclass` is passed to + :func:`dataclasses.asdict`. + - :class:`~markupsafe.Markup` (or any object with a ``__html__`` + method) will call the ``__html__`` method to get a string. - In order to support more data types, override the :meth:`default` - method. + Assign a subclass of this to :attr:`flask.Flask.json_encoder` or + :attr:`flask.Blueprint.json_encoder` to override the default. """ - def default(self, o): - """Implement this method in a subclass such that it returns a - serializable object for ``o``, or calls the base implementation (to - raise a :exc:`TypeError`). - - For example, to support arbitrary iterators, you could implement - default like this:: - - def default(self, o): - try: - iterable = iter(o) - except TypeError: - pass - else: - return list(iterable) - return JSONEncoder.default(self, o) + def default(self, o: t.Any) -> t.Any: + """Convert ``o`` to a JSON serializable type. See + :meth:`json.JSONEncoder.default`. Python does not support + overriding how basic types like ``str`` or ``list`` are + serialized, they are handled before this method. """ - if isinstance(o, datetime): - return http_date(o.utctimetuple()) if isinstance(o, date): - return http_date(o.timetuple()) - if isinstance(o, uuid.UUID): + return http_date(o) + if isinstance(o, (decimal.Decimal, uuid.UUID)): return str(o) if dataclasses and dataclasses.is_dataclass(o): return dataclasses.asdict(o) if hasattr(o, "__html__"): - return text_type(o.__html__()) - return _json.JSONEncoder.default(self, o) + return str(o.__html__()) + return super().default(o) class JSONDecoder(_json.JSONDecoder): - """The default JSON decoder. This one does not change the behavior from - the default simplejson decoder. Consult the :mod:`json` documentation - for more information. This decoder is not only used for the load - functions of this module but also :attr:`~flask.Request`. + """The default JSON decoder. + + This does not change any behavior from the built-in + :class:`json.JSONDecoder`. + + Assign a subclass of this to :attr:`flask.Flask.json_decoder` or + :attr:`flask.Blueprint.json_decoder` to override the default. """ -def _dump_arg_defaults(kwargs, app=None): +def _dump_arg_defaults( + kwargs: t.Dict[str, t.Any], app: t.Optional["Flask"] = None +) -> None: """Inject default arguments for dump functions.""" if app is None: app = current_app if app: - bp = app.blueprints.get(request.blueprint) if request else None - kwargs.setdefault( - "cls", bp.json_encoder if bp and bp.json_encoder else app.json_encoder - ) - - if not app.config["JSON_AS_ASCII"]: - kwargs.setdefault("ensure_ascii", False) + cls = app.json_encoder + bp = app.blueprints.get(request.blueprint) if request else None # type: ignore + if bp is not None and bp.json_encoder is not None: + cls = bp.json_encoder + kwargs.setdefault("cls", cls) + kwargs.setdefault("ensure_ascii", app.config["JSON_AS_ASCII"]) kwargs.setdefault("sort_keys", app.config["JSON_SORT_KEYS"]) else: kwargs.setdefault("sort_keys", True) kwargs.setdefault("cls", JSONEncoder) -def _load_arg_defaults(kwargs, app=None): +def _load_arg_defaults( + kwargs: t.Dict[str, t.Any], app: t.Optional["Flask"] = None +) -> None: """Inject default arguments for load functions.""" if app is None: app = current_app if app: - bp = app.blueprints.get(request.blueprint) if request else None - kwargs.setdefault( - "cls", bp.json_decoder if bp and bp.json_decoder else app.json_decoder - ) + cls = app.json_decoder + bp = app.blueprints.get(request.blueprint) if request else None # type: ignore + if bp is not None and bp.json_decoder is not None: + cls = bp.json_decoder + + kwargs.setdefault("cls", cls) else: kwargs.setdefault("cls", JSONDecoder) -def detect_encoding(data): - """Detect which UTF codec was used to encode the given bytes. +def dumps(obj: t.Any, app: t.Optional["Flask"] = None, **kwargs: t.Any) -> str: + """Serialize an object to a string of JSON. - The latest JSON standard (:rfc:`8259`) suggests that only UTF-8 is - accepted. Older documents allowed 8, 16, or 32. 16 and 32 can be big - or little endian. Some editors or libraries may prepend a BOM. - - :param data: Bytes in unknown UTF encoding. - :return: UTF encoding name - """ - head = data[:4] - - if head[:3] == codecs.BOM_UTF8: - return "utf-8-sig" - - if b"\x00" not in head: - return "utf-8" - - if head in (codecs.BOM_UTF32_BE, codecs.BOM_UTF32_LE): - return "utf-32" - - if head[:2] in (codecs.BOM_UTF16_BE, codecs.BOM_UTF16_LE): - return "utf-16" - - if len(head) == 4: - if head[:3] == b"\x00\x00\x00": - return "utf-32-be" - - if head[::2] == b"\x00\x00": - return "utf-16-be" - - if head[1:] == b"\x00\x00\x00": - return "utf-32-le" - - if head[1::2] == b"\x00\x00": - return "utf-16-le" - - if len(head) == 2: - return "utf-16-be" if head.startswith(b"\x00") else "utf-16-le" - - return "utf-8" - - -def dumps(obj, app=None, **kwargs): - """Serialize ``obj`` to a JSON-formatted string. If there is an - app context pushed, use the current app's configured encoder - (:attr:`~flask.Flask.json_encoder`), or fall back to the default - :class:`JSONEncoder`. - - Takes the same arguments as the built-in :func:`json.dumps`, and - does some extra configuration based on the application. If the - simplejson package is installed, it is preferred. + Takes the same arguments as the built-in :func:`json.dumps`, with + some defaults from application configuration. :param obj: Object to serialize to JSON. - :param app: App instance to use to configure the JSON encoder. - Uses ``current_app`` if not given, and falls back to the default - encoder when not in an app context. + :param app: Use this app's config instead of the active app context + or defaults. :param kwargs: Extra arguments passed to :func:`json.dumps`. - .. versionchanged:: 1.0.3 + .. versionchanged:: 2.0.2 + :class:`decimal.Decimal` is supported by converting to a string. + .. versionchanged:: 2.0 + ``encoding`` is deprecated and will be removed in Flask 2.1. + + .. versionchanged:: 1.0.3 ``app`` can be passed directly, rather than requiring an app context for configuration. """ _dump_arg_defaults(kwargs, app=app) encoding = kwargs.pop("encoding", None) rv = _json.dumps(obj, **kwargs) - if encoding is not None and isinstance(rv, text_type): - rv = rv.encode(encoding) + + if encoding is not None: + warnings.warn( + "'encoding' is deprecated and will be removed in Flask 2.1.", + DeprecationWarning, + stacklevel=2, + ) + + if isinstance(rv, str): + return rv.encode(encoding) # type: ignore + return rv -def dump(obj, fp, app=None, **kwargs): - """Like :func:`dumps` but writes into a file object.""" +def dump( + obj: t.Any, fp: t.IO[str], app: t.Optional["Flask"] = None, **kwargs: t.Any +) -> None: + """Serialize an object to JSON written to a file object. + + Takes the same arguments as the built-in :func:`json.dump`, with + some defaults from application configuration. + + :param obj: Object to serialize to JSON. + :param fp: File object to write JSON to. + :param app: Use this app's config instead of the active app context + or defaults. + :param kwargs: Extra arguments passed to :func:`json.dump`. + + .. versionchanged:: 2.0 + Writing to a binary file, and the ``encoding`` argument, is + deprecated and will be removed in Flask 2.1. + """ _dump_arg_defaults(kwargs, app=app) encoding = kwargs.pop("encoding", None) - if encoding is not None: - fp = _wrap_writer_for_text(fp, encoding) + show_warning = encoding is not None + + try: + fp.write("") + except TypeError: + show_warning = True + fp = io.TextIOWrapper(fp, encoding or "utf-8") # type: ignore + + if show_warning: + warnings.warn( + "Writing to a binary file, and the 'encoding' argument, is" + " deprecated and will be removed in Flask 2.1.", + DeprecationWarning, + stacklevel=2, + ) + _json.dump(obj, fp, **kwargs) -def loads(s, app=None, **kwargs): - """Deserialize an object from a JSON-formatted string ``s``. If - there is an app context pushed, use the current app's configured - decoder (:attr:`~flask.Flask.json_decoder`), or fall back to the - default :class:`JSONDecoder`. +def loads(s: str, app: t.Optional["Flask"] = None, **kwargs: t.Any) -> t.Any: + """Deserialize an object from a string of JSON. - Takes the same arguments as the built-in :func:`json.loads`, and - does some extra configuration based on the application. If the - simplejson package is installed, it is preferred. + Takes the same arguments as the built-in :func:`json.loads`, with + some defaults from application configuration. :param s: JSON string to deserialize. - :param app: App instance to use to configure the JSON decoder. - Uses ``current_app`` if not given, and falls back to the default - encoder when not in an app context. - :param kwargs: Extra arguments passed to :func:`json.dumps`. + :param app: Use this app's config instead of the active app context + or defaults. + :param kwargs: Extra arguments passed to :func:`json.loads`. + + .. versionchanged:: 2.0 + ``encoding`` is deprecated and will be removed in Flask 2.1. The + data must be a string or UTF-8 bytes. .. versionchanged:: 1.0.3 - ``app`` can be passed directly, rather than requiring an app context for configuration. """ _load_arg_defaults(kwargs, app=app) - if isinstance(s, bytes): - encoding = kwargs.pop("encoding", None) - if encoding is None: - encoding = detect_encoding(s) - s = s.decode(encoding) + encoding = kwargs.pop("encoding", None) + + if encoding is not None: + warnings.warn( + "'encoding' is deprecated and will be removed in Flask 2.1." + " The data must be a string or UTF-8 bytes.", + DeprecationWarning, + stacklevel=2, + ) + + if isinstance(s, bytes): + s = s.decode(encoding) + return _json.loads(s, **kwargs) -def load(fp, app=None, **kwargs): - """Like :func:`loads` but reads from a file object.""" +def load(fp: t.IO[str], app: t.Optional["Flask"] = None, **kwargs: t.Any) -> t.Any: + """Deserialize an object from JSON read from a file object. + + Takes the same arguments as the built-in :func:`json.load`, with + some defaults from application configuration. + + :param fp: File object to read JSON from. + :param app: Use this app's config instead of the active app context + or defaults. + :param kwargs: Extra arguments passed to :func:`json.load`. + + .. versionchanged:: 2.0 + ``encoding`` is deprecated and will be removed in Flask 2.1. The + file must be text mode, or binary mode with UTF-8 bytes. + """ _load_arg_defaults(kwargs, app=app) - if not PY2: - fp = _wrap_reader_for_text(fp, kwargs.pop("encoding", None) or "utf-8") + encoding = kwargs.pop("encoding", None) + + if encoding is not None: + warnings.warn( + "'encoding' is deprecated and will be removed in Flask 2.1." + " The file must be text mode, or binary mode with UTF-8" + " bytes.", + DeprecationWarning, + stacklevel=2, + ) + + if isinstance(fp.read(0), bytes): + fp = io.TextIOWrapper(fp, encoding) # type: ignore + return _json.load(fp, **kwargs) -def htmlsafe_dumps(obj, **kwargs): - """Works exactly like :func:`dumps` but is safe for use in ``", - "output": "<script>alert('XSS');</script>" - }, - - { - "name": "allow_colons_in_path_component", - "input": "foo", - "output": "foo" - }, - - { - "name": "background_attribute", - "input": "

", - "output": "
" - }, - - { - "name": "bgsound", - "input": "", - "output": "<bgsound src=\"javascript:alert('XSS');\"></bgsound>" - }, - - { - "name": "div_background_image_unicode_encoded", - "input": "
foo
", - "output": "
foo
" - }, - - { - "name": "div_expression", - "input": "
foo
", - "output": "
foo
" - }, - - { - "name": "double_open_angle_brackets", - "input": "", - "output": "" - }, - - { - "name": "img_dynsrc_lowsrc", - "input": "", - "output": "" - }, - - { - "name": "img_vbscript", - "input": "", - "output": "" - }, - - { - "name": "input_image", - "input": "", - "output": "" - }, - - { - "name": "link_stylesheets", - "input": "", - "output": "<link href=\"javascript:alert('XSS');\" rel=\"stylesheet\">" - }, - - { - "name": "link_stylesheets_2", - "input": "", - "output": "<link href=\"http://ha.ckers.org/xss.css\" rel=\"stylesheet\">" - }, - - { - "name": "list_style_image", - "input": "
  • foo
  • ", - "output": "
  • foo
  • " - }, - - { - "name": "no_closing_script_tags", - "input": "", - "output": "<script src=\"http://ha.ckers.org/xss.js\" xss=\"\"></script>" - }, - - { - "name": "non_alpha_non_digit_2", - "input": "foo", - "output": "foo" - }, - - { - "name": "non_alpha_non_digit_3", - "input": "", - "output": "" - }, - - { - "name": "non_alpha_non_digit_II", - "input": "foo", - "output": "foo" - }, - - { - "name": "non_alpha_non_digit_III", - "input": "foo", - "output": "foo" - }, - - { - "name": "platypus", - "input": "never trust your upstream platypus", - "output": "never trust your upstream platypus" - }, - - { - "name": "protocol_resolution_in_script_tag", - "input": "", - "output": "<script src=\"//ha.ckers.org/.j\"></script>" - }, - - { - "name": "should_allow_anchors", - "input": "", - "output": "<script>baz</script>" - }, - - { - "name": "should_allow_image_alt_attribute", - "input": "foo", - "output": "foo" - }, - - { - "name": "should_allow_image_height_attribute", - "input": "", - "output": "" - }, - - { - "name": "should_allow_image_src_attribute", - "input": "", - "output": "" - }, - - { - "name": "should_allow_image_width_attribute", - "input": "", - "output": "" - }, - - { - "name": "should_handle_blank_text", - "input": "", - "output": "" - }, - - { - "name": "should_handle_malformed_image_tags", - "input": "\">", - "output": "<script>alert(\"XSS\")</script>\">" - }, - - { - "name": "should_handle_non_html", - "input": "abc", - "output": "abc" - }, - - { - "name": "should_not_fall_for_ridiculous_hack", - "input": "", - "output": "" - }, - - { - "name": "should_not_fall_for_xss_image_hack_0", - "input": "", - "output": "" - }, - - { - "name": "should_not_fall_for_xss_image_hack_1", - "input": "", - "output": "" - }, - - { - "name": "should_not_fall_for_xss_image_hack_10", - "input": "", - "output": "" - }, - - { - "name": "should_not_fall_for_xss_image_hack_11", - "input": "", - "output": "" - }, - - { - "name": "should_not_fall_for_xss_image_hack_12", - "input": "", - "output": "" - }, - - { - "name": "should_not_fall_for_xss_image_hack_13", - "input": "", - "output": "" - }, - - { - "name": "should_not_fall_for_xss_image_hack_14", - "input": "", - "output": "" - }, - - { - "name": "should_not_fall_for_xss_image_hack_2", - "input": "", - "output": "" - }, - - { - "name": "should_not_fall_for_xss_image_hack_3", - "input": "", - "output": "" - }, - - { - "name": "should_not_fall_for_xss_image_hack_4", - "input": "", - "output": "" - }, - - { - "name": "should_not_fall_for_xss_image_hack_5", - "input": "", - "output": "" - }, - - { - "name": "should_not_fall_for_xss_image_hack_6", - "input": "", - "output": "" - }, - - { - "name": "should_not_fall_for_xss_image_hack_7", - "input": "", - "output": "" - }, - - { - "name": "should_not_fall_for_xss_image_hack_8", - "input": "", - "output": "" - }, - - { - "name": "should_not_fall_for_xss_image_hack_9", - "input": "", - "output": "" - }, - - { - "name": "should_sanitize_half_open_scripts", - "input": "", - "output": "<script src=\"http://ha.ckers.org/xss.js\" xss=\"\"></script>" - }, - - { - "name": "should_sanitize_script_tag_with_multiple_open_brackets", - "input": "<", - "output": "<<script>alert(\"XSS\");//<</script>" - }, - - { - "name": "should_sanitize_script_tag_with_multiple_open_brackets_2", - "input": "