From c4c6e989c0164c8e3b78af09492a7f80d121b5c6 Mon Sep 17 00:00:00 2001 From: JackDandy Date: Wed, 5 Jun 2024 08:47:14 +0100 Subject: [PATCH 01/29] =?UTF-8?q?Update=20Requests=20library=202.31.0=20(8?= =?UTF-8?q?812812)=20=E2=86=92=202.32.3=20(0e322af).?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGES.md | 7 +- lib/requests/__init__.py | 6 +- lib/requests/__version__.py | 4 +- lib/requests/adapters.py | 224 +++++++++++++++++++++++++++++++---- lib/requests/compat.py | 25 +++- lib/requests/exceptions.py | 10 ++ lib/requests/models.py | 7 +- lib/requests/packages.py | 23 ++-- lib/requests/status_codes.py | 10 +- lib/requests/utils.py | 9 +- 10 files changed, 273 insertions(+), 52 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 0c8844e3..265182b0 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,4 +1,9 @@ -### 3.31.0 (2024-06-05 08:00:00 UTC) +### 3.32.0 (2024-xx-xx xx:xx:00 UTC) + +* Update Requests library 2.31.0 (8812812) to 2.32.3 (0e322af) + + +### 3.31.0 (2024-06-05 08:00:00 UTC) * Update Apprise 1.3.0 (6458ab0) to 1.6.0 (0c0d5da) * Update attr 22.2.0 (683d056) to 23.1.0 (67e4ff2) diff --git a/lib/requests/__init__.py b/lib/requests/__init__.py index 300a16c5..051cda13 100644 --- a/lib/requests/__init__.py +++ b/lib/requests/__init__.py @@ -83,7 +83,11 @@ def check_compatibility(urllib3_version, chardet_version, charset_normalizer_ver # charset_normalizer >= 2.0.0 < 4.0.0 assert (2, 0, 0) <= (major, minor, patch) < (4, 0, 0) else: - raise Exception("You need either charset_normalizer or chardet installed") + warnings.warn( + "Unable to find acceptable character detection dependency " + "(chardet or charset_normalizer).", + RequestsDependencyWarning, + ) def _check_cryptography(cryptography_version): diff --git a/lib/requests/__version__.py b/lib/requests/__version__.py index d206427e..2c105aca 100644 --- a/lib/requests/__version__.py +++ b/lib/requests/__version__.py @@ -5,8 +5,8 @@ __title__ = "requests" __description__ = "Python HTTP for Humans." __url__ = "https://requests.readthedocs.io" -__version__ = "2.31.0" -__build__ = 0x023100 +__version__ = "2.32.3" +__build__ = 0x023203 __author__ = "Kenneth Reitz" __author_email__ = "me@kennethreitz.org" __license__ = "Apache-2.0" diff --git a/lib/requests/adapters.py b/lib/requests/adapters.py index eb240fa9..9a58b160 100644 --- a/lib/requests/adapters.py +++ b/lib/requests/adapters.py @@ -8,6 +8,8 @@ and maintain connections. import os.path import socket # noqa: F401 +import typing +import warnings from urllib3.exceptions import ClosedPoolError, ConnectTimeoutError from urllib3.exceptions import HTTPError as _HTTPError @@ -25,6 +27,7 @@ from urllib3.poolmanager import PoolManager, proxy_from_url from urllib3.util import Timeout as TimeoutSauce from urllib3.util import parse_url from urllib3.util.retry import Retry +from urllib3.util.ssl_ import create_urllib3_context from .auth import _basic_auth_str from .compat import basestring, urlparse @@ -61,12 +64,76 @@ except ImportError: raise InvalidSchema("Missing dependencies for SOCKS support.") +if typing.TYPE_CHECKING: + from .models import PreparedRequest + + DEFAULT_POOLBLOCK = False DEFAULT_POOLSIZE = 10 DEFAULT_RETRIES = 0 DEFAULT_POOL_TIMEOUT = None +try: + import ssl # noqa: F401 + + _preloaded_ssl_context = create_urllib3_context() + _preloaded_ssl_context.load_verify_locations( + extract_zipped_paths(DEFAULT_CA_BUNDLE_PATH) + ) +except ImportError: + # Bypass default SSLContext creation when Python + # interpreter isn't built with the ssl module. + _preloaded_ssl_context = None + + +def _urllib3_request_context( + request: "PreparedRequest", + verify: "bool | str | None", + client_cert: "typing.Tuple[str, str] | str | None", + poolmanager: "PoolManager", +) -> "(typing.Dict[str, typing.Any], typing.Dict[str, typing.Any])": + host_params = {} + pool_kwargs = {} + parsed_request_url = urlparse(request.url) + scheme = parsed_request_url.scheme.lower() + port = parsed_request_url.port + + # Determine if we have and should use our default SSLContext + # to optimize performance on standard requests. + poolmanager_kwargs = getattr(poolmanager, "connection_pool_kw", {}) + has_poolmanager_ssl_context = poolmanager_kwargs.get("ssl_context") + should_use_default_ssl_context = ( + _preloaded_ssl_context is not None and not has_poolmanager_ssl_context + ) + + cert_reqs = "CERT_REQUIRED" + if verify is False: + cert_reqs = "CERT_NONE" + elif verify is True and should_use_default_ssl_context: + pool_kwargs["ssl_context"] = _preloaded_ssl_context + elif isinstance(verify, str): + if not os.path.isdir(verify): + pool_kwargs["ca_certs"] = verify + else: + pool_kwargs["ca_cert_dir"] = verify + pool_kwargs["cert_reqs"] = cert_reqs + if client_cert is not None: + if isinstance(client_cert, tuple) and len(client_cert) == 2: + pool_kwargs["cert_file"] = client_cert[0] + pool_kwargs["key_file"] = client_cert[1] + else: + # According to our docs, we allow users to specify just the client + # cert path + pool_kwargs["cert_file"] = client_cert + host_params = { + "scheme": scheme, + "host": parsed_request_url.hostname, + "port": port, + } + return host_params, pool_kwargs + + class BaseAdapter: """The Base Transport Adapter""" @@ -247,27 +314,26 @@ class HTTPAdapter(BaseAdapter): :param cert: The SSL certificate to verify. """ if url.lower().startswith("https") and verify: - cert_loc = None - - # Allow self-specified cert location. - if verify is not True: - cert_loc = verify - - if not cert_loc: - cert_loc = extract_zipped_paths(DEFAULT_CA_BUNDLE_PATH) - - if not cert_loc or not os.path.exists(cert_loc): - raise OSError( - f"Could not find a suitable TLS CA certificate bundle, " - f"invalid path: {cert_loc}" - ) - conn.cert_reqs = "CERT_REQUIRED" - if not os.path.isdir(cert_loc): - conn.ca_certs = cert_loc - else: - conn.ca_cert_dir = cert_loc + # Only load the CA certificates if 'verify' is a string indicating the CA bundle to use. + # Otherwise, if verify is a boolean, we don't load anything since + # the connection will be using a context with the default certificates already loaded, + # and this avoids a call to the slow load_verify_locations() + if verify is not True: + # `verify` must be a str with a path then + cert_loc = verify + + if not os.path.exists(cert_loc): + raise OSError( + f"Could not find a suitable TLS CA certificate bundle, " + f"invalid path: {cert_loc}" + ) + + if not os.path.isdir(cert_loc): + conn.ca_certs = cert_loc + else: + conn.ca_cert_dir = cert_loc else: conn.cert_reqs = "CERT_NONE" conn.ca_certs = None @@ -327,8 +393,110 @@ class HTTPAdapter(BaseAdapter): return response + def build_connection_pool_key_attributes(self, request, verify, cert=None): + """Build the PoolKey attributes used by urllib3 to return a connection. + + This looks at the PreparedRequest, the user-specified verify value, + and the value of the cert parameter to determine what PoolKey values + to use to select a connection from a given urllib3 Connection Pool. + + The SSL related pool key arguments are not consistently set. As of + this writing, use the following to determine what keys may be in that + dictionary: + + * If ``verify`` is ``True``, ``"ssl_context"`` will be set and will be the + default Requests SSL Context + * If ``verify`` is ``False``, ``"ssl_context"`` will not be set but + ``"cert_reqs"`` will be set + * If ``verify`` is a string, (i.e., it is a user-specified trust bundle) + ``"ca_certs"`` will be set if the string is not a directory recognized + by :py:func:`os.path.isdir`, otherwise ``"ca_certs_dir"`` will be + set. + * If ``"cert"`` is specified, ``"cert_file"`` will always be set. If + ``"cert"`` is a tuple with a second item, ``"key_file"`` will also + be present + + To override these settings, one may subclass this class, call this + method and use the above logic to change parameters as desired. For + example, if one wishes to use a custom :py:class:`ssl.SSLContext` one + must both set ``"ssl_context"`` and based on what else they require, + alter the other keys to ensure the desired behaviour. + + :param request: + The PreparedReqest being sent over the connection. + :type request: + :class:`~requests.models.PreparedRequest` + :param verify: + Either a boolean, in which case it controls whether + we verify the server's TLS certificate, or a string, in which case it + must be a path to a CA bundle to use. + :param cert: + (optional) Any user-provided SSL certificate for client + authentication (a.k.a., mTLS). This may be a string (i.e., just + the path to a file which holds both certificate and key) or a + tuple of length 2 with the certificate file path and key file + path. + :returns: + A tuple of two dictionaries. The first is the "host parameters" + portion of the Pool Key including scheme, hostname, and port. The + second is a dictionary of SSLContext related parameters. + """ + return _urllib3_request_context(request, verify, cert, self.poolmanager) + + def get_connection_with_tls_context(self, request, verify, proxies=None, cert=None): + """Returns a urllib3 connection for the given request and TLS settings. + This should not be called from user code, and is only exposed for use + when subclassing the :class:`HTTPAdapter `. + + :param request: + The :class:`PreparedRequest ` object to be sent + over the connection. + :param verify: + Either a boolean, in which case it controls whether we verify the + server's TLS certificate, or a string, in which case it must be a + path to a CA bundle to use. + :param proxies: + (optional) The proxies dictionary to apply to the request. + :param cert: + (optional) Any user-provided SSL certificate to be used for client + authentication (a.k.a., mTLS). + :rtype: + urllib3.ConnectionPool + """ + proxy = select_proxy(request.url, proxies) + try: + host_params, pool_kwargs = self.build_connection_pool_key_attributes( + request, + verify, + cert, + ) + except ValueError as e: + raise InvalidURL(e, request=request) + if proxy: + proxy = prepend_scheme_if_needed(proxy, "http") + proxy_url = parse_url(proxy) + if not proxy_url.host: + raise InvalidProxyURL( + "Please check proxy URL. It is malformed " + "and could be missing the host." + ) + proxy_manager = self.proxy_manager_for(proxy) + conn = proxy_manager.connection_from_host( + **host_params, pool_kwargs=pool_kwargs + ) + else: + # Only scheme should be lower case + conn = self.poolmanager.connection_from_host( + **host_params, pool_kwargs=pool_kwargs + ) + + return conn + def get_connection(self, url, proxies=None): - """Returns a urllib3 connection for the given URL. This should not be + """DEPRECATED: Users should move to `get_connection_with_tls_context` + for all subclasses of HTTPAdapter using Requests>=2.32.2. + + Returns a urllib3 connection for the given URL. This should not be called from user code, and is only exposed for use when subclassing the :class:`HTTPAdapter `. @@ -336,6 +504,15 @@ class HTTPAdapter(BaseAdapter): :param proxies: (optional) A Requests-style dictionary of proxies used on this request. :rtype: urllib3.ConnectionPool """ + warnings.warn( + ( + "`get_connection` has been deprecated in favor of " + "`get_connection_with_tls_context`. Custom HTTPAdapter subclasses " + "will need to migrate for Requests>=2.32.2. Please see " + "https://github.com/psf/requests/pull/6710 for more details." + ), + DeprecationWarning, + ) proxy = select_proxy(url, proxies) if proxy: @@ -390,6 +567,9 @@ class HTTPAdapter(BaseAdapter): using_socks_proxy = proxy_scheme.startswith("socks") url = request.path_url + if url.startswith("//"): # Don't confuse urllib3 + url = f"/{url.lstrip('/')}" + if is_proxied_http_request and not using_socks_proxy: url = urldefragauth(request.url) @@ -450,7 +630,9 @@ class HTTPAdapter(BaseAdapter): """ try: - conn = self.get_connection(request.url, proxies) + conn = self.get_connection_with_tls_context( + request, verify, proxies=proxies, cert=cert + ) except LocationValueError as e: raise InvalidURL(e, request=request) diff --git a/lib/requests/compat.py b/lib/requests/compat.py index 6776163c..095de1b6 100644 --- a/lib/requests/compat.py +++ b/lib/requests/compat.py @@ -7,13 +7,28 @@ between Python 2 and Python 3. It remains for backwards compatibility until the next major version. """ -try: - import chardet -except ImportError: - import charset_normalizer as chardet - +import importlib import sys +# ------------------- +# Character Detection +# ------------------- + + +def _resolve_char_detection(): + """Find supported character detection libraries.""" + chardet = None + for lib in ("chardet", "charset_normalizer"): + if chardet is None: + try: + chardet = importlib.import_module(lib) + except ImportError: + pass + return chardet + + +chardet = _resolve_char_detection() + # ------- # Pythons # ------- diff --git a/lib/requests/exceptions.py b/lib/requests/exceptions.py index e1cedf88..83986b48 100644 --- a/lib/requests/exceptions.py +++ b/lib/requests/exceptions.py @@ -41,6 +41,16 @@ class JSONDecodeError(InvalidJSONError, CompatJSONDecodeError): CompatJSONDecodeError.__init__(self, *args) InvalidJSONError.__init__(self, *self.args, **kwargs) + def __reduce__(self): + """ + The __reduce__ method called when pickling the object must + be the one from the JSONDecodeError (be it json/simplejson) + as it expects all the arguments for instantiation, not just + one like the IOError, and the MRO would by default call the + __reduce__ method from the IOError due to the inheritance order. + """ + return CompatJSONDecodeError.__reduce__(self) + class HTTPError(RequestException): """An HTTP error occurred.""" diff --git a/lib/requests/models.py b/lib/requests/models.py index 44556394..8f56ca7d 100644 --- a/lib/requests/models.py +++ b/lib/requests/models.py @@ -789,7 +789,12 @@ class Response: @property def apparent_encoding(self): """The apparent encoding, provided by the charset_normalizer or chardet libraries.""" - return chardet.detect(self.content)["encoding"] + if chardet is not None: + return chardet.detect(self.content)["encoding"] + else: + # If no character detection library is available, we'll fall back + # to a standard Python utf-8 str. + return "utf-8" def iter_content(self, chunk_size=1, decode_unicode=False): """Iterates over the response data. When stream=True is set on the diff --git a/lib/requests/packages.py b/lib/requests/packages.py index 77c45c9e..5ab3d8e2 100644 --- a/lib/requests/packages.py +++ b/lib/requests/packages.py @@ -1,13 +1,6 @@ import sys -try: - import chardet -except ImportError: - import warnings - - import charset_normalizer as chardet - - warnings.filterwarnings("ignore", "Trying to detect", module="charset_normalizer") +from .compat import chardet # This code exists for backwards compatibility reasons. # I don't like it either. Just look the other way. :) @@ -20,9 +13,11 @@ for package in ("urllib3", "idna"): if mod == package or mod.startswith(f"{package}."): sys.modules[f"requests.packages.{mod}"] = sys.modules[mod] -target = chardet.__name__ -for mod in list(sys.modules): - if mod == target or mod.startswith(f"{target}."): - target = target.replace(target, "chardet") - sys.modules[f"requests.packages.{target}"] = sys.modules[mod] -# Kinda cool, though, right? +if chardet is not None: + target = chardet.__name__ + for mod in list(sys.modules): + if mod == target or mod.startswith(f"{target}."): + imported_mod = sys.modules[mod] + sys.modules[f"requests.packages.{mod}"] = imported_mod + mod = mod.replace(target, "chardet") + sys.modules[f"requests.packages.{mod}"] = imported_mod diff --git a/lib/requests/status_codes.py b/lib/requests/status_codes.py index 4bd072be..c7945a2f 100644 --- a/lib/requests/status_codes.py +++ b/lib/requests/status_codes.py @@ -24,7 +24,7 @@ _codes = { # Informational. 100: ("continue",), 101: ("switching_protocols",), - 102: ("processing",), + 102: ("processing", "early-hints"), 103: ("checkpoint",), 122: ("uri_too_long", "request_uri_too_long"), 200: ("ok", "okay", "all_ok", "all_okay", "all_good", "\\o/", "✓"), @@ -65,8 +65,8 @@ _codes = { 410: ("gone",), 411: ("length_required",), 412: ("precondition_failed", "precondition"), - 413: ("request_entity_too_large",), - 414: ("request_uri_too_large",), + 413: ("request_entity_too_large", "content_too_large"), + 414: ("request_uri_too_large", "uri_too_long"), 415: ("unsupported_media_type", "unsupported_media", "media_type"), 416: ( "requested_range_not_satisfiable", @@ -76,10 +76,10 @@ _codes = { 417: ("expectation_failed",), 418: ("im_a_teapot", "teapot", "i_am_a_teapot"), 421: ("misdirected_request",), - 422: ("unprocessable_entity", "unprocessable"), + 422: ("unprocessable_entity", "unprocessable", "unprocessable_content"), 423: ("locked",), 424: ("failed_dependency", "dependency"), - 425: ("unordered_collection", "unordered"), + 425: ("unordered_collection", "unordered", "too_early"), 426: ("upgrade_required", "upgrade"), 428: ("precondition_required", "precondition"), 429: ("too_many_requests", "too_many"), diff --git a/lib/requests/utils.py b/lib/requests/utils.py index 1ae77d68..ae6c42f6 100644 --- a/lib/requests/utils.py +++ b/lib/requests/utils.py @@ -97,6 +97,8 @@ if sys.platform == "win32": # '' string by the localhost entry and the corresponding # canonical entry. proxyOverride = proxyOverride.split(";") + # filter out empty strings to avoid re.match return true in the following code. + proxyOverride = filter(None, proxyOverride) # now check if we match one of the registry values. for test in proxyOverride: if test == "": @@ -134,6 +136,9 @@ def super_len(o): total_length = None current_position = 0 + if isinstance(o, str): + o = o.encode("utf-8") + if hasattr(o, "__len__"): total_length = len(o) @@ -859,7 +864,7 @@ def select_proxy(url, proxies): def resolve_proxies(request, proxies, trust_env=True): """This method takes proxy information from a request and configuration input to resolve a mapping of target proxies. This will consider settings - such a NO_PROXY to strip proxy configurations. + such as NO_PROXY to strip proxy configurations. :param request: Request or PreparedRequest :param proxies: A dictionary of schemes or schemes and hosts to proxy URLs @@ -1051,7 +1056,7 @@ def _validate_header_part(header, header_part, header_validator_index): if not validator.match(header_part): header_kind = "name" if header_validator_index == 0 else "value" raise InvalidHeader( - f"Invalid leading whitespace, reserved character(s), or return" + f"Invalid leading whitespace, reserved character(s), or return " f"character(s) in header {header_kind}: {header_part!r}" ) From 2a80663c8119dad6dfd3ba490a2781068642b929 Mon Sep 17 00:00:00 2001 From: JackDandy Date: Wed, 5 Jun 2024 08:55:43 +0100 Subject: [PATCH 02/29] =?UTF-8?q?Update=20urllib3=202.0.7=20(56f01e0)=20?= =?UTF-8?q?=E2=86=92=202.2.1=20(54d6edf).?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGES.md | 1 + lib/urllib3/__init__.py | 83 +- lib/urllib3/_base_connection.py | 3 +- lib/urllib3/_collections.py | 6 +- lib/urllib3/_request_methods.py | 66 +- lib/urllib3/_version.py | 2 +- lib/urllib3/connection.py | 52 +- lib/urllib3/connectionpool.py | 19 +- .../contrib/_securetransport/__init__.py | 0 .../contrib/_securetransport/bindings.py | 430 --------- .../contrib/_securetransport/low_level.py | 474 --------- lib/urllib3/contrib/pyopenssl.py | 12 +- lib/urllib3/contrib/securetransport.py | 913 ------------------ lib/urllib3/contrib/socks.py | 23 +- lib/urllib3/exceptions.py | 7 +- lib/urllib3/fields.py | 8 +- lib/urllib3/http2.py | 229 +++++ lib/urllib3/poolmanager.py | 5 +- lib/urllib3/response.py | 147 ++- lib/urllib3/util/__init__.py | 2 - lib/urllib3/util/request.py | 8 +- lib/urllib3/util/ssl_.py | 16 +- lib/urllib3/util/ssltransport.py | 2 +- lib/urllib3/util/timeout.py | 6 +- 24 files changed, 579 insertions(+), 1935 deletions(-) delete mode 100644 lib/urllib3/contrib/_securetransport/__init__.py delete mode 100644 lib/urllib3/contrib/_securetransport/bindings.py delete mode 100644 lib/urllib3/contrib/_securetransport/low_level.py delete mode 100644 lib/urllib3/contrib/securetransport.py create mode 100644 lib/urllib3/http2.py diff --git a/CHANGES.md b/CHANGES.md index 265182b0..0c31e938 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,6 +1,7 @@ ### 3.32.0 (2024-xx-xx xx:xx:00 UTC) * Update Requests library 2.31.0 (8812812) to 2.32.3 (0e322af) +* Update urllib3 2.0.7 (56f01e0) to 2.2.1 (54d6edf) ### 3.31.0 (2024-06-05 08:00:00 UTC) diff --git a/lib/urllib3/__init__.py b/lib/urllib3/__init__.py index 32c1f002..3fe782c8 100644 --- a/lib/urllib3/__init__.py +++ b/lib/urllib3/__init__.py @@ -6,6 +6,7 @@ from __future__ import annotations # Set default logging handler to avoid "No handler found" warnings. import logging +import sys import typing import warnings from logging import NullHandler @@ -32,35 +33,18 @@ except ImportError: else: if not ssl.OPENSSL_VERSION.startswith("OpenSSL "): # Defensive: warnings.warn( - "urllib3 v2.0 only supports OpenSSL 1.1.1+, currently " + "urllib3 v2 only supports OpenSSL 1.1.1+, currently " f"the 'ssl' module is compiled with {ssl.OPENSSL_VERSION!r}. " "See: https://github.com/urllib3/urllib3/issues/3020", exceptions.NotOpenSSLWarning, ) elif ssl.OPENSSL_VERSION_INFO < (1, 1, 1): # Defensive: raise ImportError( - "urllib3 v2.0 only supports OpenSSL 1.1.1+, currently " + "urllib3 v2 only supports OpenSSL 1.1.1+, currently " f"the 'ssl' module is compiled with {ssl.OPENSSL_VERSION!r}. " "See: https://github.com/urllib3/urllib3/issues/2168" ) -# === NOTE TO REPACKAGERS AND VENDORS === -# Please delete this block, this logic is only -# for urllib3 being distributed via PyPI. -# See: https://github.com/urllib3/urllib3/issues/2680 -try: - import urllib3_secure_extra # type: ignore # noqa: F401 -except ModuleNotFoundError: - pass -else: - warnings.warn( - "'urllib3[secure]' extra is deprecated and will be removed " - "in urllib3 v2.1.0. Read more in this issue: " - "https://github.com/urllib3/urllib3/issues/2680", - category=DeprecationWarning, - stacklevel=2, - ) - __author__ = "Andrey Petrov (andrey.petrov@shazow.net)" __license__ = "MIT" __version__ = __version__ @@ -149,6 +133,61 @@ def request( Therefore, its side effects could be shared across dependencies relying on it. To avoid side effects create a new ``PoolManager`` instance and use it instead. The method does not accept low-level ``**urlopen_kw`` keyword arguments. + + :param method: + HTTP request method (such as GET, POST, PUT, etc.) + + :param url: + The URL to perform the request on. + + :param body: + Data to send in the request body, either :class:`str`, :class:`bytes`, + an iterable of :class:`str`/:class:`bytes`, or a file-like object. + + :param fields: + Data to encode and send in the request body. + + :param headers: + Dictionary of custom headers to send, such as User-Agent, + If-None-Match, etc. + + :param bool preload_content: + If True, the response's body will be preloaded into memory. + + :param bool decode_content: + If True, will attempt to decode the body based on the + 'content-encoding' header. + + :param redirect: + If True, automatically handle redirects (status codes 301, 302, + 303, 307, 308). Each redirect counts as a retry. Disabling retries + will disable redirect, too. + + :param retries: + Configure the number of retries to allow before raising a + :class:`~urllib3.exceptions.MaxRetryError` exception. + + If ``None`` (default) will retry 3 times, see ``Retry.DEFAULT``. Pass a + :class:`~urllib3.util.retry.Retry` object for fine-grained control + over different types of retries. + Pass an integer number to retry connection errors that many times, + but no other types of errors. Pass zero to never retry. + + If ``False``, then retries are disabled and any exception is raised + immediately. Also, instead of raising a MaxRetryError on redirects, + the redirect response will be returned. + + :type retries: :class:`~urllib3.util.retry.Retry`, False, or an int. + + :param timeout: + If specified, overrides the default timeout for this one + request. It may be a float (in seconds) or an instance of + :class:`urllib3.util.Timeout`. + + :param json: + Data to encode and send as JSON with UTF-encoded in the request body. + The ``"Content-Type"`` header will be set to ``"application/json"`` + unless specified otherwise. """ return _DEFAULT_POOL.request( @@ -164,3 +203,9 @@ def request( timeout=timeout, json=json, ) + + +if sys.platform == "emscripten": + from .contrib.emscripten import inject_into_urllib3 # noqa: 401 + + inject_into_urllib3() diff --git a/lib/urllib3/_base_connection.py b/lib/urllib3/_base_connection.py index 25b633af..bb349c74 100644 --- a/lib/urllib3/_base_connection.py +++ b/lib/urllib3/_base_connection.py @@ -28,8 +28,7 @@ class _ResponseOptions(typing.NamedTuple): if typing.TYPE_CHECKING: import ssl - - from typing_extensions import Literal, Protocol + from typing import Literal, Protocol from .response import BaseHTTPResponse diff --git a/lib/urllib3/_collections.py b/lib/urllib3/_collections.py index 8bdfb767..55b03247 100644 --- a/lib/urllib3/_collections.py +++ b/lib/urllib3/_collections.py @@ -8,7 +8,9 @@ from threading import RLock if typing.TYPE_CHECKING: # We can only import Protocol if TYPE_CHECKING because it's a development # dependency, and is not available at runtime. - from typing_extensions import Protocol, Self + from typing import Protocol + + from typing_extensions import Self class HasGettableStringKeys(Protocol): def keys(self) -> typing.Iterator[str]: @@ -239,7 +241,7 @@ class HTTPHeaderDict(typing.MutableMapping[str, str]): def __init__(self, headers: ValidHTTPHeaderSource | None = None, **kwargs: str): super().__init__() - self._container = {} # 'dict' is insert-ordered in Python 3.7+ + self._container = {} # 'dict' is insert-ordered if headers is not None: if isinstance(headers, HTTPHeaderDict): self._copy_from(headers) diff --git a/lib/urllib3/_request_methods.py b/lib/urllib3/_request_methods.py index 1d0f3465..632042f0 100644 --- a/lib/urllib3/_request_methods.py +++ b/lib/urllib3/_request_methods.py @@ -85,6 +85,30 @@ class RequestMethods: option to drop down to more specific methods when necessary, such as :meth:`request_encode_url`, :meth:`request_encode_body`, or even the lowest level :meth:`urlopen`. + + :param method: + HTTP request method (such as GET, POST, PUT, etc.) + + :param url: + The URL to perform the request on. + + :param body: + Data to send in the request body, either :class:`str`, :class:`bytes`, + an iterable of :class:`str`/:class:`bytes`, or a file-like object. + + :param fields: + Data to encode and send in the request body. Values are processed + by :func:`urllib.parse.urlencode`. + + :param headers: + Dictionary of custom headers to send, such as User-Agent, + If-None-Match, etc. If None, pool headers are used. If provided, + these headers completely replace any pool-specific headers. + + :param json: + Data to encode and send as JSON with UTF-encoded in the request body. + The ``"Content-Type"`` header will be set to ``"application/json"`` + unless specified otherwise. """ method = method.upper() @@ -95,9 +119,11 @@ class RequestMethods: if json is not None: if headers is None: - headers = self.headers.copy() # type: ignore + headers = self.headers + if not ("content-type" in map(str.lower, headers.keys())): - headers["Content-Type"] = "application/json" # type: ignore + headers = HTTPHeaderDict(headers) + headers["Content-Type"] = "application/json" body = _json.dumps(json, separators=(",", ":"), ensure_ascii=False).encode( "utf-8" @@ -130,6 +156,20 @@ class RequestMethods: """ Make a request using :meth:`urlopen` with the ``fields`` encoded in the url. This is useful for request methods like GET, HEAD, DELETE, etc. + + :param method: + HTTP request method (such as GET, POST, PUT, etc.) + + :param url: + The URL to perform the request on. + + :param fields: + Data to encode and send in the request body. + + :param headers: + Dictionary of custom headers to send, such as User-Agent, + If-None-Match, etc. If None, pool headers are used. If provided, + these headers completely replace any pool-specific headers. """ if headers is None: headers = self.headers @@ -186,6 +226,28 @@ class RequestMethods: be overwritten because it depends on the dynamic random boundary string which is used to compose the body of the request. The random boundary string can be explicitly set with the ``multipart_boundary`` parameter. + + :param method: + HTTP request method (such as GET, POST, PUT, etc.) + + :param url: + The URL to perform the request on. + + :param fields: + Data to encode and send in the request body. + + :param headers: + Dictionary of custom headers to send, such as User-Agent, + If-None-Match, etc. If None, pool headers are used. If provided, + these headers completely replace any pool-specific headers. + + :param encode_multipart: + If True, encode the ``fields`` using the multipart/form-data MIME + format. + + :param multipart_boundary: + If not specified, then a random boundary will be generated using + :func:`urllib3.filepost.choose_boundary`. """ if headers is None: headers = self.headers diff --git a/lib/urllib3/_version.py b/lib/urllib3/_version.py index e2b88f1d..095cf3c1 100644 --- a/lib/urllib3/_version.py +++ b/lib/urllib3/_version.py @@ -1,4 +1,4 @@ # This file is protected via CODEOWNERS from __future__ import annotations -__version__ = "2.0.7" +__version__ = "2.2.1" diff --git a/lib/urllib3/connection.py b/lib/urllib3/connection.py index 4a71225c..aa5c547c 100644 --- a/lib/urllib3/connection.py +++ b/lib/urllib3/connection.py @@ -14,7 +14,7 @@ from http.client import ResponseNotReady from socket import timeout as SocketTimeout if typing.TYPE_CHECKING: - from typing_extensions import Literal + from typing import Literal from .response import HTTPResponse from .util.ssl_ import _TYPE_PEER_CERT_RET_DICT @@ -73,7 +73,7 @@ port_by_scheme = {"http": 80, "https": 443} # When it comes time to update this value as a part of regular maintenance # (ie test_recent_date is failing) update it to ~6 months before the current date. -RECENT_DATE = datetime.date(2022, 1, 1) +RECENT_DATE = datetime.date(2023, 6, 1) _CONTAINS_CONTROL_CHAR_RE = re.compile(r"[^-!#$%&'*+.^_`|~0-9a-zA-Z]") @@ -160,11 +160,6 @@ class HTTPConnection(_HTTPConnection): self._tunnel_port: int | None = None self._tunnel_scheme: str | None = None - # https://github.com/python/mypy/issues/4125 - # Mypy treats this as LSP violation, which is considered a bug. - # If `host` is made a property it violates LSP, because a writeable attribute is overridden with a read-only one. - # However, there is also a `host` setter so LSP is not violated. - # Potentially, a `@host.deleter` might be needed depending on how this issue will be fixed. @property def host(self) -> str: """ @@ -253,6 +248,9 @@ class HTTPConnection(_HTTPConnection): # not using tunnelling. self._has_connected_to_proxy = bool(self.proxy) + if self._has_connected_to_proxy: + self.proxy_is_verified = False + @property def is_closed(self) -> bool: return self.sock is None @@ -267,6 +265,13 @@ class HTTPConnection(_HTTPConnection): def has_connected_to_proxy(self) -> bool: return self._has_connected_to_proxy + @property + def proxy_is_forwarding(self) -> bool: + """ + Return True if a forwarding proxy is configured, else return False + """ + return bool(self.proxy) and self._tunnel_host is None + def close(self) -> None: try: super().close() @@ -302,7 +307,7 @@ class HTTPConnection(_HTTPConnection): method, url, skip_host=skip_host, skip_accept_encoding=skip_accept_encoding ) - def putheader(self, header: str, *values: str) -> None: + def putheader(self, header: str, *values: str) -> None: # type: ignore[override] """""" if not any(isinstance(v, str) and v == SKIP_HEADER for v in values): super().putheader(header, *values) @@ -616,8 +621,11 @@ class HTTPSConnection(HTTPConnection): if self._tunnel_host is not None: # We're tunneling to an HTTPS origin so need to do TLS-in-TLS. if self._tunnel_scheme == "https": + # _connect_tls_proxy will verify and assign proxy_is_verified self.sock = sock = self._connect_tls_proxy(self.host, sock) tls_in_tls = True + elif self._tunnel_scheme == "http": + self.proxy_is_verified = False # If we're tunneling it means we're connected to our proxy. self._has_connected_to_proxy = True @@ -639,6 +647,9 @@ class HTTPSConnection(HTTPConnection): SystemTimeWarning, ) + # Remove trailing '.' from fqdn hostnames to allow certificate validation + server_hostname_rm_dot = server_hostname.rstrip(".") + sock_and_verified = _ssl_wrap_socket_and_match_hostname( sock=sock, cert_reqs=self.cert_reqs, @@ -651,20 +662,33 @@ class HTTPSConnection(HTTPConnection): cert_file=self.cert_file, key_file=self.key_file, key_password=self.key_password, - server_hostname=server_hostname, + server_hostname=server_hostname_rm_dot, ssl_context=self.ssl_context, tls_in_tls=tls_in_tls, assert_hostname=self.assert_hostname, assert_fingerprint=self.assert_fingerprint, ) self.sock = sock_and_verified.socket - self.is_verified = sock_and_verified.is_verified + + # Forwarding proxies can never have a verified target since + # the proxy is the one doing the verification. Should instead + # use a CONNECT tunnel in order to verify the target. + # See: https://github.com/urllib3/urllib3/issues/3267. + if self.proxy_is_forwarding: + self.is_verified = False + else: + self.is_verified = sock_and_verified.is_verified # If there's a proxy to be connected to we are fully connected. # This is set twice (once above and here) due to forwarding proxies # not using tunnelling. self._has_connected_to_proxy = bool(self.proxy) + # Set `self.proxy_is_verified` unless it's already set while + # establishing a tunnel. + if self._has_connected_to_proxy and self.proxy_is_verified is None: + self.proxy_is_verified = sock_and_verified.is_verified + def _connect_tls_proxy(self, hostname: str, sock: socket.socket) -> ssl.SSLSocket: """ Establish a TLS connection to the proxy using the provided SSL context. @@ -757,10 +781,9 @@ def _ssl_wrap_socket_and_match_hostname( ): context.check_hostname = False - # Try to load OS default certs if none are given. - # We need to do the hasattr() check for our custom - # pyOpenSSL and SecureTransport SSLContext objects - # because neither support load_default_certs(). + # Try to load OS default certs if none are given. We need to do the hasattr() check + # for custom pyOpenSSL SSLContext objects because they don't support + # load_default_certs(). if ( not ca_certs and not ca_cert_dir @@ -865,6 +888,7 @@ def _wrap_proxy_error(err: Exception, proxy_scheme: str | None) -> ProxyError: is_likely_http_proxy = ( "wrong version number" in error_normalized or "unknown protocol" in error_normalized + or "record layer failure" in error_normalized ) http_proxy_warning = ( ". Your proxy appears to only use HTTP and not HTTPS, " diff --git a/lib/urllib3/connectionpool.py b/lib/urllib3/connectionpool.py index c6ca3902..bd58ff14 100644 --- a/lib/urllib3/connectionpool.py +++ b/lib/urllib3/connectionpool.py @@ -53,8 +53,7 @@ from .util.util import to_str if typing.TYPE_CHECKING: import ssl - - from typing_extensions import Literal + from typing import Literal from ._base_connection import BaseHTTPConnection, BaseHTTPSConnection @@ -512,9 +511,10 @@ class HTTPConnectionPool(ConnectionPool, RequestMethods): pass except OSError as e: # MacOS/Linux - # EPROTOTYPE is needed on macOS + # EPROTOTYPE and ECONNRESET are needed on macOS # https://erickt.github.io/blog/2014/11/19/adventures-in-debugging-a-potential-osx-kernel-bug/ - if e.errno != errno.EPROTOTYPE: + # Condition changed later to emit ECONNRESET instead of only EPROTOTYPE. + if e.errno != errno.EPROTOTYPE and e.errno != errno.ECONNRESET: raise # Reset the timeout for the recv() on the socket @@ -544,6 +544,8 @@ class HTTPConnectionPool(ConnectionPool, RequestMethods): response._connection = response_conn # type: ignore[attr-defined] response._pool = self # type: ignore[attr-defined] + # emscripten connection doesn't have _http_vsn_str + http_version = getattr(conn, "_http_vsn_str", "HTTP/?") log.debug( '%s://%s:%s "%s %s %s" %s %s', self.scheme, @@ -552,9 +554,9 @@ class HTTPConnectionPool(ConnectionPool, RequestMethods): method, url, # HTTP version - conn._http_vsn_str, # type: ignore[attr-defined] + http_version, response.status, - response.length_remaining, # type: ignore[attr-defined] + response.length_remaining, ) return response @@ -647,7 +649,7 @@ class HTTPConnectionPool(ConnectionPool, RequestMethods): Configure the number of retries to allow before raising a :class:`~urllib3.exceptions.MaxRetryError` exception. - Pass ``None`` to retry until you receive a response. Pass a + If ``None`` (default) will retry 3 times, see ``Retry.DEFAULT``. Pass a :class:`~urllib3.util.retry.Retry` object for fine-grained control over different types of retries. Pass an integer number to retry connection errors that many times, @@ -1096,7 +1098,8 @@ class HTTPSConnectionPool(HTTPConnectionPool): if conn.is_closed: conn.connect() - if not conn.is_verified: + # TODO revise this, see https://github.com/urllib3/urllib3/issues/2791 + if not conn.is_verified and not conn.proxy_is_verified: warnings.warn( ( f"Unverified HTTPS request is being made to host '{conn.host}'. " diff --git a/lib/urllib3/contrib/_securetransport/__init__.py b/lib/urllib3/contrib/_securetransport/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/lib/urllib3/contrib/_securetransport/bindings.py b/lib/urllib3/contrib/_securetransport/bindings.py deleted file mode 100644 index 3e4cd466..00000000 --- a/lib/urllib3/contrib/_securetransport/bindings.py +++ /dev/null @@ -1,430 +0,0 @@ -# type: ignore - -""" -This module uses ctypes to bind a whole bunch of functions and constants from -SecureTransport. The goal here is to provide the low-level API to -SecureTransport. These are essentially the C-level functions and constants, and -they're pretty gross to work with. - -This code is a bastardised version of the code found in Will Bond's oscrypto -library. An enormous debt is owed to him for blazing this trail for us. For -that reason, this code should be considered to be covered both by urllib3's -license and by oscrypto's: - - Copyright (c) 2015-2016 Will Bond - - Permission is hereby granted, free of charge, to any person obtaining a - copy of this software and associated documentation files (the "Software"), - to deal in the Software without restriction, including without limitation - the rights to use, copy, modify, merge, publish, distribute, sublicense, - and/or sell copies of the Software, and to permit persons to whom the - Software is furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in - all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER - DEALINGS IN THE SOFTWARE. -""" - -from __future__ import annotations - -import platform -from ctypes import ( - CDLL, - CFUNCTYPE, - POINTER, - c_bool, - c_byte, - c_char_p, - c_int32, - c_long, - c_size_t, - c_uint32, - c_ulong, - c_void_p, -) -from ctypes.util import find_library - -if platform.system() != "Darwin": - raise ImportError("Only macOS is supported") - -version = platform.mac_ver()[0] -version_info = tuple(map(int, version.split("."))) -if version_info < (10, 8): - raise OSError( - f"Only OS X 10.8 and newer are supported, not {version_info[0]}.{version_info[1]}" - ) - - -def load_cdll(name: str, macos10_16_path: str) -> CDLL: - """Loads a CDLL by name, falling back to known path on 10.16+""" - try: - # Big Sur is technically 11 but we use 10.16 due to the Big Sur - # beta being labeled as 10.16. - path: str | None - if version_info >= (10, 16): - path = macos10_16_path - else: - path = find_library(name) - if not path: - raise OSError # Caught and reraised as 'ImportError' - return CDLL(path, use_errno=True) - except OSError: - raise ImportError(f"The library {name} failed to load") from None - - -Security = load_cdll( - "Security", "/System/Library/Frameworks/Security.framework/Security" -) -CoreFoundation = load_cdll( - "CoreFoundation", - "/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation", -) - - -Boolean = c_bool -CFIndex = c_long -CFStringEncoding = c_uint32 -CFData = c_void_p -CFString = c_void_p -CFArray = c_void_p -CFMutableArray = c_void_p -CFDictionary = c_void_p -CFError = c_void_p -CFType = c_void_p -CFTypeID = c_ulong - -CFTypeRef = POINTER(CFType) -CFAllocatorRef = c_void_p - -OSStatus = c_int32 - -CFDataRef = POINTER(CFData) -CFStringRef = POINTER(CFString) -CFArrayRef = POINTER(CFArray) -CFMutableArrayRef = POINTER(CFMutableArray) -CFDictionaryRef = POINTER(CFDictionary) -CFArrayCallBacks = c_void_p -CFDictionaryKeyCallBacks = c_void_p -CFDictionaryValueCallBacks = c_void_p - -SecCertificateRef = POINTER(c_void_p) -SecExternalFormat = c_uint32 -SecExternalItemType = c_uint32 -SecIdentityRef = POINTER(c_void_p) -SecItemImportExportFlags = c_uint32 -SecItemImportExportKeyParameters = c_void_p -SecKeychainRef = POINTER(c_void_p) -SSLProtocol = c_uint32 -SSLCipherSuite = c_uint32 -SSLContextRef = POINTER(c_void_p) -SecTrustRef = POINTER(c_void_p) -SSLConnectionRef = c_uint32 -SecTrustResultType = c_uint32 -SecTrustOptionFlags = c_uint32 -SSLProtocolSide = c_uint32 -SSLConnectionType = c_uint32 -SSLSessionOption = c_uint32 - - -try: - Security.SecItemImport.argtypes = [ - CFDataRef, - CFStringRef, - POINTER(SecExternalFormat), - POINTER(SecExternalItemType), - SecItemImportExportFlags, - POINTER(SecItemImportExportKeyParameters), - SecKeychainRef, - POINTER(CFArrayRef), - ] - Security.SecItemImport.restype = OSStatus - - Security.SecCertificateGetTypeID.argtypes = [] - Security.SecCertificateGetTypeID.restype = CFTypeID - - Security.SecIdentityGetTypeID.argtypes = [] - Security.SecIdentityGetTypeID.restype = CFTypeID - - Security.SecKeyGetTypeID.argtypes = [] - Security.SecKeyGetTypeID.restype = CFTypeID - - Security.SecCertificateCreateWithData.argtypes = [CFAllocatorRef, CFDataRef] - Security.SecCertificateCreateWithData.restype = SecCertificateRef - - Security.SecCertificateCopyData.argtypes = [SecCertificateRef] - Security.SecCertificateCopyData.restype = CFDataRef - - Security.SecCopyErrorMessageString.argtypes = [OSStatus, c_void_p] - Security.SecCopyErrorMessageString.restype = CFStringRef - - Security.SecIdentityCreateWithCertificate.argtypes = [ - CFTypeRef, - SecCertificateRef, - POINTER(SecIdentityRef), - ] - Security.SecIdentityCreateWithCertificate.restype = OSStatus - - Security.SecKeychainCreate.argtypes = [ - c_char_p, - c_uint32, - c_void_p, - Boolean, - c_void_p, - POINTER(SecKeychainRef), - ] - Security.SecKeychainCreate.restype = OSStatus - - Security.SecKeychainDelete.argtypes = [SecKeychainRef] - Security.SecKeychainDelete.restype = OSStatus - - Security.SecPKCS12Import.argtypes = [ - CFDataRef, - CFDictionaryRef, - POINTER(CFArrayRef), - ] - Security.SecPKCS12Import.restype = OSStatus - - SSLReadFunc = CFUNCTYPE(OSStatus, SSLConnectionRef, c_void_p, POINTER(c_size_t)) - SSLWriteFunc = CFUNCTYPE( - OSStatus, SSLConnectionRef, POINTER(c_byte), POINTER(c_size_t) - ) - - Security.SSLSetIOFuncs.argtypes = [SSLContextRef, SSLReadFunc, SSLWriteFunc] - Security.SSLSetIOFuncs.restype = OSStatus - - Security.SSLSetPeerID.argtypes = [SSLContextRef, c_char_p, c_size_t] - Security.SSLSetPeerID.restype = OSStatus - - Security.SSLSetCertificate.argtypes = [SSLContextRef, CFArrayRef] - Security.SSLSetCertificate.restype = OSStatus - - Security.SSLSetCertificateAuthorities.argtypes = [SSLContextRef, CFTypeRef, Boolean] - Security.SSLSetCertificateAuthorities.restype = OSStatus - - Security.SSLSetConnection.argtypes = [SSLContextRef, SSLConnectionRef] - Security.SSLSetConnection.restype = OSStatus - - Security.SSLSetPeerDomainName.argtypes = [SSLContextRef, c_char_p, c_size_t] - Security.SSLSetPeerDomainName.restype = OSStatus - - Security.SSLHandshake.argtypes = [SSLContextRef] - Security.SSLHandshake.restype = OSStatus - - Security.SSLRead.argtypes = [SSLContextRef, c_char_p, c_size_t, POINTER(c_size_t)] - Security.SSLRead.restype = OSStatus - - Security.SSLWrite.argtypes = [SSLContextRef, c_char_p, c_size_t, POINTER(c_size_t)] - Security.SSLWrite.restype = OSStatus - - Security.SSLClose.argtypes = [SSLContextRef] - Security.SSLClose.restype = OSStatus - - Security.SSLGetNumberSupportedCiphers.argtypes = [SSLContextRef, POINTER(c_size_t)] - Security.SSLGetNumberSupportedCiphers.restype = OSStatus - - Security.SSLGetSupportedCiphers.argtypes = [ - SSLContextRef, - POINTER(SSLCipherSuite), - POINTER(c_size_t), - ] - Security.SSLGetSupportedCiphers.restype = OSStatus - - Security.SSLSetEnabledCiphers.argtypes = [ - SSLContextRef, - POINTER(SSLCipherSuite), - c_size_t, - ] - Security.SSLSetEnabledCiphers.restype = OSStatus - - Security.SSLGetNumberEnabledCiphers.argtype = [SSLContextRef, POINTER(c_size_t)] - Security.SSLGetNumberEnabledCiphers.restype = OSStatus - - Security.SSLGetEnabledCiphers.argtypes = [ - SSLContextRef, - POINTER(SSLCipherSuite), - POINTER(c_size_t), - ] - Security.SSLGetEnabledCiphers.restype = OSStatus - - Security.SSLGetNegotiatedCipher.argtypes = [SSLContextRef, POINTER(SSLCipherSuite)] - Security.SSLGetNegotiatedCipher.restype = OSStatus - - Security.SSLGetNegotiatedProtocolVersion.argtypes = [ - SSLContextRef, - POINTER(SSLProtocol), - ] - Security.SSLGetNegotiatedProtocolVersion.restype = OSStatus - - Security.SSLCopyPeerTrust.argtypes = [SSLContextRef, POINTER(SecTrustRef)] - Security.SSLCopyPeerTrust.restype = OSStatus - - Security.SecTrustSetAnchorCertificates.argtypes = [SecTrustRef, CFArrayRef] - Security.SecTrustSetAnchorCertificates.restype = OSStatus - - Security.SecTrustSetAnchorCertificatesOnly.argstypes = [SecTrustRef, Boolean] - Security.SecTrustSetAnchorCertificatesOnly.restype = OSStatus - - Security.SecTrustEvaluate.argtypes = [SecTrustRef, POINTER(SecTrustResultType)] - Security.SecTrustEvaluate.restype = OSStatus - - Security.SecTrustGetCertificateCount.argtypes = [SecTrustRef] - Security.SecTrustGetCertificateCount.restype = CFIndex - - Security.SecTrustGetCertificateAtIndex.argtypes = [SecTrustRef, CFIndex] - Security.SecTrustGetCertificateAtIndex.restype = SecCertificateRef - - Security.SSLCreateContext.argtypes = [ - CFAllocatorRef, - SSLProtocolSide, - SSLConnectionType, - ] - Security.SSLCreateContext.restype = SSLContextRef - - Security.SSLSetSessionOption.argtypes = [SSLContextRef, SSLSessionOption, Boolean] - Security.SSLSetSessionOption.restype = OSStatus - - Security.SSLSetProtocolVersionMin.argtypes = [SSLContextRef, SSLProtocol] - Security.SSLSetProtocolVersionMin.restype = OSStatus - - Security.SSLSetProtocolVersionMax.argtypes = [SSLContextRef, SSLProtocol] - Security.SSLSetProtocolVersionMax.restype = OSStatus - - try: - Security.SSLSetALPNProtocols.argtypes = [SSLContextRef, CFArrayRef] - Security.SSLSetALPNProtocols.restype = OSStatus - except AttributeError: - # Supported only in 10.12+ - pass - - Security.SecCopyErrorMessageString.argtypes = [OSStatus, c_void_p] - Security.SecCopyErrorMessageString.restype = CFStringRef - - Security.SSLReadFunc = SSLReadFunc - Security.SSLWriteFunc = SSLWriteFunc - Security.SSLContextRef = SSLContextRef - Security.SSLProtocol = SSLProtocol - Security.SSLCipherSuite = SSLCipherSuite - Security.SecIdentityRef = SecIdentityRef - Security.SecKeychainRef = SecKeychainRef - Security.SecTrustRef = SecTrustRef - Security.SecTrustResultType = SecTrustResultType - Security.SecExternalFormat = SecExternalFormat - Security.OSStatus = OSStatus - - Security.kSecImportExportPassphrase = CFStringRef.in_dll( - Security, "kSecImportExportPassphrase" - ) - Security.kSecImportItemIdentity = CFStringRef.in_dll( - Security, "kSecImportItemIdentity" - ) - - # CoreFoundation time! - CoreFoundation.CFRetain.argtypes = [CFTypeRef] - CoreFoundation.CFRetain.restype = CFTypeRef - - CoreFoundation.CFRelease.argtypes = [CFTypeRef] - CoreFoundation.CFRelease.restype = None - - CoreFoundation.CFGetTypeID.argtypes = [CFTypeRef] - CoreFoundation.CFGetTypeID.restype = CFTypeID - - CoreFoundation.CFStringCreateWithCString.argtypes = [ - CFAllocatorRef, - c_char_p, - CFStringEncoding, - ] - CoreFoundation.CFStringCreateWithCString.restype = CFStringRef - - CoreFoundation.CFStringGetCStringPtr.argtypes = [CFStringRef, CFStringEncoding] - CoreFoundation.CFStringGetCStringPtr.restype = c_char_p - - CoreFoundation.CFStringGetCString.argtypes = [ - CFStringRef, - c_char_p, - CFIndex, - CFStringEncoding, - ] - CoreFoundation.CFStringGetCString.restype = c_bool - - CoreFoundation.CFDataCreate.argtypes = [CFAllocatorRef, c_char_p, CFIndex] - CoreFoundation.CFDataCreate.restype = CFDataRef - - CoreFoundation.CFDataGetLength.argtypes = [CFDataRef] - CoreFoundation.CFDataGetLength.restype = CFIndex - - CoreFoundation.CFDataGetBytePtr.argtypes = [CFDataRef] - CoreFoundation.CFDataGetBytePtr.restype = c_void_p - - CoreFoundation.CFDictionaryCreate.argtypes = [ - CFAllocatorRef, - POINTER(CFTypeRef), - POINTER(CFTypeRef), - CFIndex, - CFDictionaryKeyCallBacks, - CFDictionaryValueCallBacks, - ] - CoreFoundation.CFDictionaryCreate.restype = CFDictionaryRef - - CoreFoundation.CFDictionaryGetValue.argtypes = [CFDictionaryRef, CFTypeRef] - CoreFoundation.CFDictionaryGetValue.restype = CFTypeRef - - CoreFoundation.CFArrayCreate.argtypes = [ - CFAllocatorRef, - POINTER(CFTypeRef), - CFIndex, - CFArrayCallBacks, - ] - CoreFoundation.CFArrayCreate.restype = CFArrayRef - - CoreFoundation.CFArrayCreateMutable.argtypes = [ - CFAllocatorRef, - CFIndex, - CFArrayCallBacks, - ] - CoreFoundation.CFArrayCreateMutable.restype = CFMutableArrayRef - - CoreFoundation.CFArrayAppendValue.argtypes = [CFMutableArrayRef, c_void_p] - CoreFoundation.CFArrayAppendValue.restype = None - - CoreFoundation.CFArrayGetCount.argtypes = [CFArrayRef] - CoreFoundation.CFArrayGetCount.restype = CFIndex - - CoreFoundation.CFArrayGetValueAtIndex.argtypes = [CFArrayRef, CFIndex] - CoreFoundation.CFArrayGetValueAtIndex.restype = c_void_p - - CoreFoundation.kCFAllocatorDefault = CFAllocatorRef.in_dll( - CoreFoundation, "kCFAllocatorDefault" - ) - CoreFoundation.kCFTypeArrayCallBacks = c_void_p.in_dll( - CoreFoundation, "kCFTypeArrayCallBacks" - ) - CoreFoundation.kCFTypeDictionaryKeyCallBacks = c_void_p.in_dll( - CoreFoundation, "kCFTypeDictionaryKeyCallBacks" - ) - CoreFoundation.kCFTypeDictionaryValueCallBacks = c_void_p.in_dll( - CoreFoundation, "kCFTypeDictionaryValueCallBacks" - ) - - CoreFoundation.CFTypeRef = CFTypeRef - CoreFoundation.CFArrayRef = CFArrayRef - CoreFoundation.CFStringRef = CFStringRef - CoreFoundation.CFDictionaryRef = CFDictionaryRef - -except AttributeError: - raise ImportError("Error initializing ctypes") from None - - -class CFConst: - """ - A class object that acts as essentially a namespace for CoreFoundation - constants. - """ - - kCFStringEncodingUTF8 = CFStringEncoding(0x08000100) diff --git a/lib/urllib3/contrib/_securetransport/low_level.py b/lib/urllib3/contrib/_securetransport/low_level.py deleted file mode 100644 index e2356997..00000000 --- a/lib/urllib3/contrib/_securetransport/low_level.py +++ /dev/null @@ -1,474 +0,0 @@ -""" -Low-level helpers for the SecureTransport bindings. - -These are Python functions that are not directly related to the high-level APIs -but are necessary to get them to work. They include a whole bunch of low-level -CoreFoundation messing about and memory management. The concerns in this module -are almost entirely about trying to avoid memory leaks and providing -appropriate and useful assistance to the higher-level code. -""" -from __future__ import annotations - -import base64 -import ctypes -import itertools -import os -import re -import ssl -import struct -import tempfile -import typing - -from .bindings import ( # type: ignore[attr-defined] - CFArray, - CFConst, - CFData, - CFDictionary, - CFMutableArray, - CFString, - CFTypeRef, - CoreFoundation, - SecKeychainRef, - Security, -) - -# This regular expression is used to grab PEM data out of a PEM bundle. -_PEM_CERTS_RE = re.compile( - b"-----BEGIN CERTIFICATE-----\n(.*?)\n-----END CERTIFICATE-----", re.DOTALL -) - - -def _cf_data_from_bytes(bytestring: bytes) -> CFData: - """ - Given a bytestring, create a CFData object from it. This CFData object must - be CFReleased by the caller. - """ - return CoreFoundation.CFDataCreate( - CoreFoundation.kCFAllocatorDefault, bytestring, len(bytestring) - ) - - -def _cf_dictionary_from_tuples( - tuples: list[tuple[typing.Any, typing.Any]] -) -> CFDictionary: - """ - Given a list of Python tuples, create an associated CFDictionary. - """ - dictionary_size = len(tuples) - - # We need to get the dictionary keys and values out in the same order. - keys = (t[0] for t in tuples) - values = (t[1] for t in tuples) - cf_keys = (CoreFoundation.CFTypeRef * dictionary_size)(*keys) - cf_values = (CoreFoundation.CFTypeRef * dictionary_size)(*values) - - return CoreFoundation.CFDictionaryCreate( - CoreFoundation.kCFAllocatorDefault, - cf_keys, - cf_values, - dictionary_size, - CoreFoundation.kCFTypeDictionaryKeyCallBacks, - CoreFoundation.kCFTypeDictionaryValueCallBacks, - ) - - -def _cfstr(py_bstr: bytes) -> CFString: - """ - Given a Python binary data, create a CFString. - The string must be CFReleased by the caller. - """ - c_str = ctypes.c_char_p(py_bstr) - cf_str = CoreFoundation.CFStringCreateWithCString( - CoreFoundation.kCFAllocatorDefault, - c_str, - CFConst.kCFStringEncodingUTF8, - ) - return cf_str - - -def _create_cfstring_array(lst: list[bytes]) -> CFMutableArray: - """ - Given a list of Python binary data, create an associated CFMutableArray. - The array must be CFReleased by the caller. - - Raises an ssl.SSLError on failure. - """ - cf_arr = None - try: - cf_arr = CoreFoundation.CFArrayCreateMutable( - CoreFoundation.kCFAllocatorDefault, - 0, - ctypes.byref(CoreFoundation.kCFTypeArrayCallBacks), - ) - if not cf_arr: - raise MemoryError("Unable to allocate memory!") - for item in lst: - cf_str = _cfstr(item) - if not cf_str: - raise MemoryError("Unable to allocate memory!") - try: - CoreFoundation.CFArrayAppendValue(cf_arr, cf_str) - finally: - CoreFoundation.CFRelease(cf_str) - except BaseException as e: - if cf_arr: - CoreFoundation.CFRelease(cf_arr) - raise ssl.SSLError(f"Unable to allocate array: {e}") from None - return cf_arr - - -def _cf_string_to_unicode(value: CFString) -> str | None: - """ - Creates a Unicode string from a CFString object. Used entirely for error - reporting. - - Yes, it annoys me quite a lot that this function is this complex. - """ - value_as_void_p = ctypes.cast(value, ctypes.POINTER(ctypes.c_void_p)) - - string = CoreFoundation.CFStringGetCStringPtr( - value_as_void_p, CFConst.kCFStringEncodingUTF8 - ) - if string is None: - buffer = ctypes.create_string_buffer(1024) - result = CoreFoundation.CFStringGetCString( - value_as_void_p, buffer, 1024, CFConst.kCFStringEncodingUTF8 - ) - if not result: - raise OSError("Error copying C string from CFStringRef") - string = buffer.value - if string is not None: - string = string.decode("utf-8") - return string # type: ignore[no-any-return] - - -def _assert_no_error( - error: int, exception_class: type[BaseException] | None = None -) -> None: - """ - Checks the return code and throws an exception if there is an error to - report - """ - if error == 0: - return - - cf_error_string = Security.SecCopyErrorMessageString(error, None) - output = _cf_string_to_unicode(cf_error_string) - CoreFoundation.CFRelease(cf_error_string) - - if output is None or output == "": - output = f"OSStatus {error}" - - if exception_class is None: - exception_class = ssl.SSLError - - raise exception_class(output) - - -def _cert_array_from_pem(pem_bundle: bytes) -> CFArray: - """ - Given a bundle of certs in PEM format, turns them into a CFArray of certs - that can be used to validate a cert chain. - """ - # Normalize the PEM bundle's line endings. - pem_bundle = pem_bundle.replace(b"\r\n", b"\n") - - der_certs = [ - base64.b64decode(match.group(1)) for match in _PEM_CERTS_RE.finditer(pem_bundle) - ] - if not der_certs: - raise ssl.SSLError("No root certificates specified") - - cert_array = CoreFoundation.CFArrayCreateMutable( - CoreFoundation.kCFAllocatorDefault, - 0, - ctypes.byref(CoreFoundation.kCFTypeArrayCallBacks), - ) - if not cert_array: - raise ssl.SSLError("Unable to allocate memory!") - - try: - for der_bytes in der_certs: - certdata = _cf_data_from_bytes(der_bytes) - if not certdata: - raise ssl.SSLError("Unable to allocate memory!") - cert = Security.SecCertificateCreateWithData( - CoreFoundation.kCFAllocatorDefault, certdata - ) - CoreFoundation.CFRelease(certdata) - if not cert: - raise ssl.SSLError("Unable to build cert object!") - - CoreFoundation.CFArrayAppendValue(cert_array, cert) - CoreFoundation.CFRelease(cert) - except Exception: - # We need to free the array before the exception bubbles further. - # We only want to do that if an error occurs: otherwise, the caller - # should free. - CoreFoundation.CFRelease(cert_array) - raise - - return cert_array - - -def _is_cert(item: CFTypeRef) -> bool: - """ - Returns True if a given CFTypeRef is a certificate. - """ - expected = Security.SecCertificateGetTypeID() - return CoreFoundation.CFGetTypeID(item) == expected # type: ignore[no-any-return] - - -def _is_identity(item: CFTypeRef) -> bool: - """ - Returns True if a given CFTypeRef is an identity. - """ - expected = Security.SecIdentityGetTypeID() - return CoreFoundation.CFGetTypeID(item) == expected # type: ignore[no-any-return] - - -def _temporary_keychain() -> tuple[SecKeychainRef, str]: - """ - This function creates a temporary Mac keychain that we can use to work with - credentials. This keychain uses a one-time password and a temporary file to - store the data. We expect to have one keychain per socket. The returned - SecKeychainRef must be freed by the caller, including calling - SecKeychainDelete. - - Returns a tuple of the SecKeychainRef and the path to the temporary - directory that contains it. - """ - # Unfortunately, SecKeychainCreate requires a path to a keychain. This - # means we cannot use mkstemp to use a generic temporary file. Instead, - # we're going to create a temporary directory and a filename to use there. - # This filename will be 8 random bytes expanded into base64. We also need - # some random bytes to password-protect the keychain we're creating, so we - # ask for 40 random bytes. - random_bytes = os.urandom(40) - filename = base64.b16encode(random_bytes[:8]).decode("utf-8") - password = base64.b16encode(random_bytes[8:]) # Must be valid UTF-8 - tempdirectory = tempfile.mkdtemp() - - keychain_path = os.path.join(tempdirectory, filename).encode("utf-8") - - # We now want to create the keychain itself. - keychain = Security.SecKeychainRef() - status = Security.SecKeychainCreate( - keychain_path, len(password), password, False, None, ctypes.byref(keychain) - ) - _assert_no_error(status) - - # Having created the keychain, we want to pass it off to the caller. - return keychain, tempdirectory - - -def _load_items_from_file( - keychain: SecKeychainRef, path: str -) -> tuple[list[CFTypeRef], list[CFTypeRef]]: - """ - Given a single file, loads all the trust objects from it into arrays and - the keychain. - Returns a tuple of lists: the first list is a list of identities, the - second a list of certs. - """ - certificates = [] - identities = [] - result_array = None - - with open(path, "rb") as f: - raw_filedata = f.read() - - try: - filedata = CoreFoundation.CFDataCreate( - CoreFoundation.kCFAllocatorDefault, raw_filedata, len(raw_filedata) - ) - result_array = CoreFoundation.CFArrayRef() - result = Security.SecItemImport( - filedata, # cert data - None, # Filename, leaving it out for now - None, # What the type of the file is, we don't care - None, # what's in the file, we don't care - 0, # import flags - None, # key params, can include passphrase in the future - keychain, # The keychain to insert into - ctypes.byref(result_array), # Results - ) - _assert_no_error(result) - - # A CFArray is not very useful to us as an intermediary - # representation, so we are going to extract the objects we want - # and then free the array. We don't need to keep hold of keys: the - # keychain already has them! - result_count = CoreFoundation.CFArrayGetCount(result_array) - for index in range(result_count): - item = CoreFoundation.CFArrayGetValueAtIndex(result_array, index) - item = ctypes.cast(item, CoreFoundation.CFTypeRef) - - if _is_cert(item): - CoreFoundation.CFRetain(item) - certificates.append(item) - elif _is_identity(item): - CoreFoundation.CFRetain(item) - identities.append(item) - finally: - if result_array: - CoreFoundation.CFRelease(result_array) - - CoreFoundation.CFRelease(filedata) - - return (identities, certificates) - - -def _load_client_cert_chain(keychain: SecKeychainRef, *paths: str | None) -> CFArray: - """ - Load certificates and maybe keys from a number of files. Has the end goal - of returning a CFArray containing one SecIdentityRef, and then zero or more - SecCertificateRef objects, suitable for use as a client certificate trust - chain. - """ - # Ok, the strategy. - # - # This relies on knowing that macOS will not give you a SecIdentityRef - # unless you have imported a key into a keychain. This is a somewhat - # artificial limitation of macOS (for example, it doesn't necessarily - # affect iOS), but there is nothing inside Security.framework that lets you - # get a SecIdentityRef without having a key in a keychain. - # - # So the policy here is we take all the files and iterate them in order. - # Each one will use SecItemImport to have one or more objects loaded from - # it. We will also point at a keychain that macOS can use to work with the - # private key. - # - # Once we have all the objects, we'll check what we actually have. If we - # already have a SecIdentityRef in hand, fab: we'll use that. Otherwise, - # we'll take the first certificate (which we assume to be our leaf) and - # ask the keychain to give us a SecIdentityRef with that cert's associated - # key. - # - # We'll then return a CFArray containing the trust chain: one - # SecIdentityRef and then zero-or-more SecCertificateRef objects. The - # responsibility for freeing this CFArray will be with the caller. This - # CFArray must remain alive for the entire connection, so in practice it - # will be stored with a single SSLSocket, along with the reference to the - # keychain. - certificates = [] - identities = [] - - # Filter out bad paths. - filtered_paths = (path for path in paths if path) - - try: - for file_path in filtered_paths: - new_identities, new_certs = _load_items_from_file(keychain, file_path) - identities.extend(new_identities) - certificates.extend(new_certs) - - # Ok, we have everything. The question is: do we have an identity? If - # not, we want to grab one from the first cert we have. - if not identities: - new_identity = Security.SecIdentityRef() - status = Security.SecIdentityCreateWithCertificate( - keychain, certificates[0], ctypes.byref(new_identity) - ) - _assert_no_error(status) - identities.append(new_identity) - - # We now want to release the original certificate, as we no longer - # need it. - CoreFoundation.CFRelease(certificates.pop(0)) - - # We now need to build a new CFArray that holds the trust chain. - trust_chain = CoreFoundation.CFArrayCreateMutable( - CoreFoundation.kCFAllocatorDefault, - 0, - ctypes.byref(CoreFoundation.kCFTypeArrayCallBacks), - ) - for item in itertools.chain(identities, certificates): - # ArrayAppendValue does a CFRetain on the item. That's fine, - # because the finally block will release our other refs to them. - CoreFoundation.CFArrayAppendValue(trust_chain, item) - - return trust_chain - finally: - for obj in itertools.chain(identities, certificates): - CoreFoundation.CFRelease(obj) - - -TLS_PROTOCOL_VERSIONS = { - "SSLv2": (0, 2), - "SSLv3": (3, 0), - "TLSv1": (3, 1), - "TLSv1.1": (3, 2), - "TLSv1.2": (3, 3), -} - - -def _build_tls_unknown_ca_alert(version: str) -> bytes: - """ - Builds a TLS alert record for an unknown CA. - """ - ver_maj, ver_min = TLS_PROTOCOL_VERSIONS[version] - severity_fatal = 0x02 - description_unknown_ca = 0x30 - msg = struct.pack(">BB", severity_fatal, description_unknown_ca) - msg_len = len(msg) - record_type_alert = 0x15 - record = struct.pack(">BBBH", record_type_alert, ver_maj, ver_min, msg_len) + msg - return record - - -class SecurityConst: - """ - A class object that acts as essentially a namespace for Security constants. - """ - - kSSLSessionOptionBreakOnServerAuth = 0 - - kSSLProtocol2 = 1 - kSSLProtocol3 = 2 - kTLSProtocol1 = 4 - kTLSProtocol11 = 7 - kTLSProtocol12 = 8 - # SecureTransport does not support TLS 1.3 even if there's a constant for it - kTLSProtocol13 = 10 - kTLSProtocolMaxSupported = 999 - - kSSLClientSide = 1 - kSSLStreamType = 0 - - kSecFormatPEMSequence = 10 - - kSecTrustResultInvalid = 0 - kSecTrustResultProceed = 1 - # This gap is present on purpose: this was kSecTrustResultConfirm, which - # is deprecated. - kSecTrustResultDeny = 3 - kSecTrustResultUnspecified = 4 - kSecTrustResultRecoverableTrustFailure = 5 - kSecTrustResultFatalTrustFailure = 6 - kSecTrustResultOtherError = 7 - - errSSLProtocol = -9800 - errSSLWouldBlock = -9803 - errSSLClosedGraceful = -9805 - errSSLClosedNoNotify = -9816 - errSSLClosedAbort = -9806 - - errSSLXCertChainInvalid = -9807 - errSSLCrypto = -9809 - errSSLInternal = -9810 - errSSLCertExpired = -9814 - errSSLCertNotYetValid = -9815 - errSSLUnknownRootCert = -9812 - errSSLNoRootCert = -9813 - errSSLHostNameMismatch = -9843 - errSSLPeerHandshakeFail = -9824 - errSSLPeerUserCancelled = -9839 - errSSLWeakPeerEphemeralDHKey = -9850 - errSSLServerAuthCompleted = -9841 - errSSLRecordOverflow = -9847 - - errSecVerifyFailed = -67808 - errSecNoTrustSettings = -25263 - errSecItemNotFound = -25300 - errSecInvalidTrustSettings = -25262 diff --git a/lib/urllib3/contrib/pyopenssl.py b/lib/urllib3/contrib/pyopenssl.py index 74b35883..b89a6dab 100644 --- a/lib/urllib3/contrib/pyopenssl.py +++ b/lib/urllib3/contrib/pyopenssl.py @@ -8,10 +8,10 @@ This needs the following packages installed: * `pyOpenSSL`_ (tested with 16.0.0) * `cryptography`_ (minimum 1.3.4, from pyopenssl) -* `idna`_ (minimum 2.0, from cryptography) +* `idna`_ (minimum 2.0) -However, pyOpenSSL depends on cryptography, which depends on idna, so while we -use all three directly here we end up having relatively few packages required. +However, pyOpenSSL depends on cryptography, so while we use all three directly here we +end up having relatively few packages required. You can install them with the following command: @@ -40,7 +40,7 @@ like this: from __future__ import annotations -import OpenSSL.SSL # type: ignore[import] +import OpenSSL.SSL # type: ignore[import-untyped] from cryptography import x509 try: @@ -61,13 +61,13 @@ from socket import timeout from .. import util if typing.TYPE_CHECKING: - from OpenSSL.crypto import X509 # type: ignore[import] + from OpenSSL.crypto import X509 # type: ignore[import-untyped] __all__ = ["inject_into_urllib3", "extract_from_urllib3"] # Map from urllib3 to PyOpenSSL compatible parameter-values. -_openssl_versions = { +_openssl_versions: dict[int, int] = { util.ssl_.PROTOCOL_TLS: OpenSSL.SSL.SSLv23_METHOD, # type: ignore[attr-defined] util.ssl_.PROTOCOL_TLS_CLIENT: OpenSSL.SSL.SSLv23_METHOD, # type: ignore[attr-defined] ssl.PROTOCOL_TLSv1: OpenSSL.SSL.TLSv1_METHOD, diff --git a/lib/urllib3/contrib/securetransport.py b/lib/urllib3/contrib/securetransport.py deleted file mode 100644 index 11beb3df..00000000 --- a/lib/urllib3/contrib/securetransport.py +++ /dev/null @@ -1,913 +0,0 @@ -""" -SecureTranport support for urllib3 via ctypes. - -This makes platform-native TLS available to urllib3 users on macOS without the -use of a compiler. This is an important feature because the Python Package -Index is moving to become a TLSv1.2-or-higher server, and the default OpenSSL -that ships with macOS is not capable of doing TLSv1.2. The only way to resolve -this is to give macOS users an alternative solution to the problem, and that -solution is to use SecureTransport. - -We use ctypes here because this solution must not require a compiler. That's -because pip is not allowed to require a compiler either. - -This is not intended to be a seriously long-term solution to this problem. -The hope is that PEP 543 will eventually solve this issue for us, at which -point we can retire this contrib module. But in the short term, we need to -solve the impending tire fire that is Python on Mac without this kind of -contrib module. So...here we are. - -To use this module, simply import and inject it:: - - import urllib3.contrib.securetransport - urllib3.contrib.securetransport.inject_into_urllib3() - -Happy TLSing! - -This code is a bastardised version of the code found in Will Bond's oscrypto -library. An enormous debt is owed to him for blazing this trail for us. For -that reason, this code should be considered to be covered both by urllib3's -license and by oscrypto's: - -.. code-block:: - - Copyright (c) 2015-2016 Will Bond - - Permission is hereby granted, free of charge, to any person obtaining a - copy of this software and associated documentation files (the "Software"), - to deal in the Software without restriction, including without limitation - the rights to use, copy, modify, merge, publish, distribute, sublicense, - and/or sell copies of the Software, and to permit persons to whom the - Software is furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in - all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER - DEALINGS IN THE SOFTWARE. -""" - -from __future__ import annotations - -import contextlib -import ctypes -import errno -import os.path -import shutil -import socket -import ssl -import struct -import threading -import typing -import warnings -import weakref -from socket import socket as socket_cls - -from .. import util -from ._securetransport.bindings import ( # type: ignore[attr-defined] - CoreFoundation, - Security, -) -from ._securetransport.low_level import ( - SecurityConst, - _assert_no_error, - _build_tls_unknown_ca_alert, - _cert_array_from_pem, - _create_cfstring_array, - _load_client_cert_chain, - _temporary_keychain, -) - -warnings.warn( - "'urllib3.contrib.securetransport' module is deprecated and will be removed " - "in urllib3 v2.1.0. Read more in this issue: " - "https://github.com/urllib3/urllib3/issues/2681", - category=DeprecationWarning, - stacklevel=2, -) - -if typing.TYPE_CHECKING: - from typing_extensions import Literal - -__all__ = ["inject_into_urllib3", "extract_from_urllib3"] - -orig_util_SSLContext = util.ssl_.SSLContext - -# This dictionary is used by the read callback to obtain a handle to the -# calling wrapped socket. This is a pretty silly approach, but for now it'll -# do. I feel like I should be able to smuggle a handle to the wrapped socket -# directly in the SSLConnectionRef, but for now this approach will work I -# guess. -# -# We need to lock around this structure for inserts, but we don't do it for -# reads/writes in the callbacks. The reasoning here goes as follows: -# -# 1. It is not possible to call into the callbacks before the dictionary is -# populated, so once in the callback the id must be in the dictionary. -# 2. The callbacks don't mutate the dictionary, they only read from it, and -# so cannot conflict with any of the insertions. -# -# This is good: if we had to lock in the callbacks we'd drastically slow down -# the performance of this code. -_connection_refs: weakref.WeakValueDictionary[ - int, WrappedSocket -] = weakref.WeakValueDictionary() -_connection_ref_lock = threading.Lock() - -# Limit writes to 16kB. This is OpenSSL's limit, but we'll cargo-cult it over -# for no better reason than we need *a* limit, and this one is right there. -SSL_WRITE_BLOCKSIZE = 16384 - -# Basically this is simple: for PROTOCOL_SSLv23 we turn it into a low of -# TLSv1 and a high of TLSv1.2. For everything else, we pin to that version. -# TLSv1 to 1.2 are supported on macOS 10.8+ -_protocol_to_min_max = { - util.ssl_.PROTOCOL_TLS: (SecurityConst.kTLSProtocol1, SecurityConst.kTLSProtocol12), # type: ignore[attr-defined] - util.ssl_.PROTOCOL_TLS_CLIENT: ( # type: ignore[attr-defined] - SecurityConst.kTLSProtocol1, - SecurityConst.kTLSProtocol12, - ), -} - -if hasattr(ssl, "PROTOCOL_SSLv2"): - _protocol_to_min_max[ssl.PROTOCOL_SSLv2] = ( - SecurityConst.kSSLProtocol2, - SecurityConst.kSSLProtocol2, - ) -if hasattr(ssl, "PROTOCOL_SSLv3"): - _protocol_to_min_max[ssl.PROTOCOL_SSLv3] = ( - SecurityConst.kSSLProtocol3, - SecurityConst.kSSLProtocol3, - ) -if hasattr(ssl, "PROTOCOL_TLSv1"): - _protocol_to_min_max[ssl.PROTOCOL_TLSv1] = ( - SecurityConst.kTLSProtocol1, - SecurityConst.kTLSProtocol1, - ) -if hasattr(ssl, "PROTOCOL_TLSv1_1"): - _protocol_to_min_max[ssl.PROTOCOL_TLSv1_1] = ( - SecurityConst.kTLSProtocol11, - SecurityConst.kTLSProtocol11, - ) -if hasattr(ssl, "PROTOCOL_TLSv1_2"): - _protocol_to_min_max[ssl.PROTOCOL_TLSv1_2] = ( - SecurityConst.kTLSProtocol12, - SecurityConst.kTLSProtocol12, - ) - - -_tls_version_to_st: dict[int, int] = { - ssl.TLSVersion.MINIMUM_SUPPORTED: SecurityConst.kTLSProtocol1, - ssl.TLSVersion.TLSv1: SecurityConst.kTLSProtocol1, - ssl.TLSVersion.TLSv1_1: SecurityConst.kTLSProtocol11, - ssl.TLSVersion.TLSv1_2: SecurityConst.kTLSProtocol12, - ssl.TLSVersion.MAXIMUM_SUPPORTED: SecurityConst.kTLSProtocol12, -} - - -def inject_into_urllib3() -> None: - """ - Monkey-patch urllib3 with SecureTransport-backed SSL-support. - """ - util.SSLContext = SecureTransportContext # type: ignore[assignment] - util.ssl_.SSLContext = SecureTransportContext # type: ignore[assignment] - util.IS_SECURETRANSPORT = True - util.ssl_.IS_SECURETRANSPORT = True - - -def extract_from_urllib3() -> None: - """ - Undo monkey-patching by :func:`inject_into_urllib3`. - """ - util.SSLContext = orig_util_SSLContext - util.ssl_.SSLContext = orig_util_SSLContext - util.IS_SECURETRANSPORT = False - util.ssl_.IS_SECURETRANSPORT = False - - -def _read_callback( - connection_id: int, data_buffer: int, data_length_pointer: bytearray -) -> int: - """ - SecureTransport read callback. This is called by ST to request that data - be returned from the socket. - """ - wrapped_socket = None - try: - wrapped_socket = _connection_refs.get(connection_id) - if wrapped_socket is None: - return SecurityConst.errSSLInternal - base_socket = wrapped_socket.socket - - requested_length = data_length_pointer[0] - - timeout = wrapped_socket.gettimeout() - error = None - read_count = 0 - - try: - while read_count < requested_length: - if timeout is None or timeout >= 0: - if not util.wait_for_read(base_socket, timeout): - raise OSError(errno.EAGAIN, "timed out") - - remaining = requested_length - read_count - buffer = (ctypes.c_char * remaining).from_address( - data_buffer + read_count - ) - chunk_size = base_socket.recv_into(buffer, remaining) - read_count += chunk_size - if not chunk_size: - if not read_count: - return SecurityConst.errSSLClosedGraceful - break - except OSError as e: - error = e.errno - - if error is not None and error != errno.EAGAIN: - data_length_pointer[0] = read_count - if error == errno.ECONNRESET or error == errno.EPIPE: - return SecurityConst.errSSLClosedAbort - raise - - data_length_pointer[0] = read_count - - if read_count != requested_length: - return SecurityConst.errSSLWouldBlock - - return 0 - except Exception as e: - if wrapped_socket is not None: - wrapped_socket._exception = e - return SecurityConst.errSSLInternal - - -def _write_callback( - connection_id: int, data_buffer: int, data_length_pointer: bytearray -) -> int: - """ - SecureTransport write callback. This is called by ST to request that data - actually be sent on the network. - """ - wrapped_socket = None - try: - wrapped_socket = _connection_refs.get(connection_id) - if wrapped_socket is None: - return SecurityConst.errSSLInternal - base_socket = wrapped_socket.socket - - bytes_to_write = data_length_pointer[0] - data = ctypes.string_at(data_buffer, bytes_to_write) - - timeout = wrapped_socket.gettimeout() - error = None - sent = 0 - - try: - while sent < bytes_to_write: - if timeout is None or timeout >= 0: - if not util.wait_for_write(base_socket, timeout): - raise OSError(errno.EAGAIN, "timed out") - chunk_sent = base_socket.send(data) - sent += chunk_sent - - # This has some needless copying here, but I'm not sure there's - # much value in optimising this data path. - data = data[chunk_sent:] - except OSError as e: - error = e.errno - - if error is not None and error != errno.EAGAIN: - data_length_pointer[0] = sent - if error == errno.ECONNRESET or error == errno.EPIPE: - return SecurityConst.errSSLClosedAbort - raise - - data_length_pointer[0] = sent - - if sent != bytes_to_write: - return SecurityConst.errSSLWouldBlock - - return 0 - except Exception as e: - if wrapped_socket is not None: - wrapped_socket._exception = e - return SecurityConst.errSSLInternal - - -# We need to keep these two objects references alive: if they get GC'd while -# in use then SecureTransport could attempt to call a function that is in freed -# memory. That would be...uh...bad. Yeah, that's the word. Bad. -_read_callback_pointer = Security.SSLReadFunc(_read_callback) -_write_callback_pointer = Security.SSLWriteFunc(_write_callback) - - -class WrappedSocket: - """ - API-compatibility wrapper for Python's OpenSSL wrapped socket object. - """ - - def __init__(self, socket: socket_cls) -> None: - self.socket = socket - self.context = None - self._io_refs = 0 - self._closed = False - self._real_closed = False - self._exception: Exception | None = None - self._keychain = None - self._keychain_dir: str | None = None - self._client_cert_chain = None - - # We save off the previously-configured timeout and then set it to - # zero. This is done because we use select and friends to handle the - # timeouts, but if we leave the timeout set on the lower socket then - # Python will "kindly" call select on that socket again for us. Avoid - # that by forcing the timeout to zero. - self._timeout = self.socket.gettimeout() - self.socket.settimeout(0) - - @contextlib.contextmanager - def _raise_on_error(self) -> typing.Generator[None, None, None]: - """ - A context manager that can be used to wrap calls that do I/O from - SecureTransport. If any of the I/O callbacks hit an exception, this - context manager will correctly propagate the exception after the fact. - This avoids silently swallowing those exceptions. - - It also correctly forces the socket closed. - """ - self._exception = None - - # We explicitly don't catch around this yield because in the unlikely - # event that an exception was hit in the block we don't want to swallow - # it. - yield - if self._exception is not None: - exception, self._exception = self._exception, None - self._real_close() - raise exception - - def _set_alpn_protocols(self, protocols: list[bytes] | None) -> None: - """ - Sets up the ALPN protocols on the context. - """ - if not protocols: - return - protocols_arr = _create_cfstring_array(protocols) - try: - result = Security.SSLSetALPNProtocols(self.context, protocols_arr) - _assert_no_error(result) - finally: - CoreFoundation.CFRelease(protocols_arr) - - def _custom_validate(self, verify: bool, trust_bundle: bytes | None) -> None: - """ - Called when we have set custom validation. We do this in two cases: - first, when cert validation is entirely disabled; and second, when - using a custom trust DB. - Raises an SSLError if the connection is not trusted. - """ - # If we disabled cert validation, just say: cool. - if not verify or trust_bundle is None: - return - - successes = ( - SecurityConst.kSecTrustResultUnspecified, - SecurityConst.kSecTrustResultProceed, - ) - try: - trust_result = self._evaluate_trust(trust_bundle) - if trust_result in successes: - return - reason = f"error code: {int(trust_result)}" - exc = None - except Exception as e: - # Do not trust on error - reason = f"exception: {e!r}" - exc = e - - # SecureTransport does not send an alert nor shuts down the connection. - rec = _build_tls_unknown_ca_alert(self.version()) - self.socket.sendall(rec) - # close the connection immediately - # l_onoff = 1, activate linger - # l_linger = 0, linger for 0 seoncds - opts = struct.pack("ii", 1, 0) - self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_LINGER, opts) - self._real_close() - raise ssl.SSLError(f"certificate verify failed, {reason}") from exc - - def _evaluate_trust(self, trust_bundle: bytes) -> int: - # We want data in memory, so load it up. - if os.path.isfile(trust_bundle): - with open(trust_bundle, "rb") as f: - trust_bundle = f.read() - - cert_array = None - trust = Security.SecTrustRef() - - try: - # Get a CFArray that contains the certs we want. - cert_array = _cert_array_from_pem(trust_bundle) - - # Ok, now the hard part. We want to get the SecTrustRef that ST has - # created for this connection, shove our CAs into it, tell ST to - # ignore everything else it knows, and then ask if it can build a - # chain. This is a buuuunch of code. - result = Security.SSLCopyPeerTrust(self.context, ctypes.byref(trust)) - _assert_no_error(result) - if not trust: - raise ssl.SSLError("Failed to copy trust reference") - - result = Security.SecTrustSetAnchorCertificates(trust, cert_array) - _assert_no_error(result) - - result = Security.SecTrustSetAnchorCertificatesOnly(trust, True) - _assert_no_error(result) - - trust_result = Security.SecTrustResultType() - result = Security.SecTrustEvaluate(trust, ctypes.byref(trust_result)) - _assert_no_error(result) - finally: - if trust: - CoreFoundation.CFRelease(trust) - - if cert_array is not None: - CoreFoundation.CFRelease(cert_array) - - return trust_result.value # type: ignore[no-any-return] - - def handshake( - self, - server_hostname: bytes | str | None, - verify: bool, - trust_bundle: bytes | None, - min_version: int, - max_version: int, - client_cert: str | None, - client_key: str | None, - client_key_passphrase: typing.Any, - alpn_protocols: list[bytes] | None, - ) -> None: - """ - Actually performs the TLS handshake. This is run automatically by - wrapped socket, and shouldn't be needed in user code. - """ - # First, we do the initial bits of connection setup. We need to create - # a context, set its I/O funcs, and set the connection reference. - self.context = Security.SSLCreateContext( - None, SecurityConst.kSSLClientSide, SecurityConst.kSSLStreamType - ) - result = Security.SSLSetIOFuncs( - self.context, _read_callback_pointer, _write_callback_pointer - ) - _assert_no_error(result) - - # Here we need to compute the handle to use. We do this by taking the - # id of self modulo 2**31 - 1. If this is already in the dictionary, we - # just keep incrementing by one until we find a free space. - with _connection_ref_lock: - handle = id(self) % 2147483647 - while handle in _connection_refs: - handle = (handle + 1) % 2147483647 - _connection_refs[handle] = self - - result = Security.SSLSetConnection(self.context, handle) - _assert_no_error(result) - - # If we have a server hostname, we should set that too. - # RFC6066 Section 3 tells us not to use SNI when the host is an IP, but we have - # to do it anyway to match server_hostname against the server certificate - if server_hostname: - if not isinstance(server_hostname, bytes): - server_hostname = server_hostname.encode("utf-8") - - result = Security.SSLSetPeerDomainName( - self.context, server_hostname, len(server_hostname) - ) - _assert_no_error(result) - - # Setup the ALPN protocols. - self._set_alpn_protocols(alpn_protocols) - - # Set the minimum and maximum TLS versions. - result = Security.SSLSetProtocolVersionMin(self.context, min_version) - _assert_no_error(result) - - result = Security.SSLSetProtocolVersionMax(self.context, max_version) - _assert_no_error(result) - - # If there's a trust DB, we need to use it. We do that by telling - # SecureTransport to break on server auth. We also do that if we don't - # want to validate the certs at all: we just won't actually do any - # authing in that case. - if not verify or trust_bundle is not None: - result = Security.SSLSetSessionOption( - self.context, SecurityConst.kSSLSessionOptionBreakOnServerAuth, True - ) - _assert_no_error(result) - - # If there's a client cert, we need to use it. - if client_cert: - self._keychain, self._keychain_dir = _temporary_keychain() - self._client_cert_chain = _load_client_cert_chain( - self._keychain, client_cert, client_key - ) - result = Security.SSLSetCertificate(self.context, self._client_cert_chain) - _assert_no_error(result) - - while True: - with self._raise_on_error(): - result = Security.SSLHandshake(self.context) - - if result == SecurityConst.errSSLWouldBlock: - raise socket.timeout("handshake timed out") - elif result == SecurityConst.errSSLServerAuthCompleted: - self._custom_validate(verify, trust_bundle) - continue - else: - _assert_no_error(result) - break - - def fileno(self) -> int: - return self.socket.fileno() - - # Copy-pasted from Python 3.5 source code - def _decref_socketios(self) -> None: - if self._io_refs > 0: - self._io_refs -= 1 - if self._closed: - self.close() - - def recv(self, bufsiz: int) -> bytes: - buffer = ctypes.create_string_buffer(bufsiz) - bytes_read = self.recv_into(buffer, bufsiz) - data = buffer[:bytes_read] - return typing.cast(bytes, data) - - def recv_into( - self, buffer: ctypes.Array[ctypes.c_char], nbytes: int | None = None - ) -> int: - # Read short on EOF. - if self._real_closed: - return 0 - - if nbytes is None: - nbytes = len(buffer) - - buffer = (ctypes.c_char * nbytes).from_buffer(buffer) - processed_bytes = ctypes.c_size_t(0) - - with self._raise_on_error(): - result = Security.SSLRead( - self.context, buffer, nbytes, ctypes.byref(processed_bytes) - ) - - # There are some result codes that we want to treat as "not always - # errors". Specifically, those are errSSLWouldBlock, - # errSSLClosedGraceful, and errSSLClosedNoNotify. - if result == SecurityConst.errSSLWouldBlock: - # If we didn't process any bytes, then this was just a time out. - # However, we can get errSSLWouldBlock in situations when we *did* - # read some data, and in those cases we should just read "short" - # and return. - if processed_bytes.value == 0: - # Timed out, no data read. - raise socket.timeout("recv timed out") - elif result in ( - SecurityConst.errSSLClosedGraceful, - SecurityConst.errSSLClosedNoNotify, - ): - # The remote peer has closed this connection. We should do so as - # well. Note that we don't actually return here because in - # principle this could actually be fired along with return data. - # It's unlikely though. - self._real_close() - else: - _assert_no_error(result) - - # Ok, we read and probably succeeded. We should return whatever data - # was actually read. - return processed_bytes.value - - def settimeout(self, timeout: float) -> None: - self._timeout = timeout - - def gettimeout(self) -> float | None: - return self._timeout - - def send(self, data: bytes) -> int: - processed_bytes = ctypes.c_size_t(0) - - with self._raise_on_error(): - result = Security.SSLWrite( - self.context, data, len(data), ctypes.byref(processed_bytes) - ) - - if result == SecurityConst.errSSLWouldBlock and processed_bytes.value == 0: - # Timed out - raise socket.timeout("send timed out") - else: - _assert_no_error(result) - - # We sent, and probably succeeded. Tell them how much we sent. - return processed_bytes.value - - def sendall(self, data: bytes) -> None: - total_sent = 0 - while total_sent < len(data): - sent = self.send(data[total_sent : total_sent + SSL_WRITE_BLOCKSIZE]) - total_sent += sent - - def shutdown(self) -> None: - with self._raise_on_error(): - Security.SSLClose(self.context) - - def close(self) -> None: - self._closed = True - # TODO: should I do clean shutdown here? Do I have to? - if self._io_refs <= 0: - self._real_close() - - def _real_close(self) -> None: - self._real_closed = True - if self.context: - CoreFoundation.CFRelease(self.context) - self.context = None - if self._client_cert_chain: - CoreFoundation.CFRelease(self._client_cert_chain) - self._client_cert_chain = None - if self._keychain: - Security.SecKeychainDelete(self._keychain) - CoreFoundation.CFRelease(self._keychain) - shutil.rmtree(self._keychain_dir) - self._keychain = self._keychain_dir = None - return self.socket.close() - - def getpeercert(self, binary_form: bool = False) -> bytes | None: - # Urgh, annoying. - # - # Here's how we do this: - # - # 1. Call SSLCopyPeerTrust to get hold of the trust object for this - # connection. - # 2. Call SecTrustGetCertificateAtIndex for index 0 to get the leaf. - # 3. To get the CN, call SecCertificateCopyCommonName and process that - # string so that it's of the appropriate type. - # 4. To get the SAN, we need to do something a bit more complex: - # a. Call SecCertificateCopyValues to get the data, requesting - # kSecOIDSubjectAltName. - # b. Mess about with this dictionary to try to get the SANs out. - # - # This is gross. Really gross. It's going to be a few hundred LoC extra - # just to repeat something that SecureTransport can *already do*. So my - # operating assumption at this time is that what we want to do is - # instead to just flag to urllib3 that it shouldn't do its own hostname - # validation when using SecureTransport. - if not binary_form: - raise ValueError("SecureTransport only supports dumping binary certs") - trust = Security.SecTrustRef() - certdata = None - der_bytes = None - - try: - # Grab the trust store. - result = Security.SSLCopyPeerTrust(self.context, ctypes.byref(trust)) - _assert_no_error(result) - if not trust: - # Probably we haven't done the handshake yet. No biggie. - return None - - cert_count = Security.SecTrustGetCertificateCount(trust) - if not cert_count: - # Also a case that might happen if we haven't handshaked. - # Handshook? Handshaken? - return None - - leaf = Security.SecTrustGetCertificateAtIndex(trust, 0) - assert leaf - - # Ok, now we want the DER bytes. - certdata = Security.SecCertificateCopyData(leaf) - assert certdata - - data_length = CoreFoundation.CFDataGetLength(certdata) - data_buffer = CoreFoundation.CFDataGetBytePtr(certdata) - der_bytes = ctypes.string_at(data_buffer, data_length) - finally: - if certdata: - CoreFoundation.CFRelease(certdata) - if trust: - CoreFoundation.CFRelease(trust) - - return der_bytes - - def version(self) -> str: - protocol = Security.SSLProtocol() - result = Security.SSLGetNegotiatedProtocolVersion( - self.context, ctypes.byref(protocol) - ) - _assert_no_error(result) - if protocol.value == SecurityConst.kTLSProtocol13: - raise ssl.SSLError("SecureTransport does not support TLS 1.3") - elif protocol.value == SecurityConst.kTLSProtocol12: - return "TLSv1.2" - elif protocol.value == SecurityConst.kTLSProtocol11: - return "TLSv1.1" - elif protocol.value == SecurityConst.kTLSProtocol1: - return "TLSv1" - elif protocol.value == SecurityConst.kSSLProtocol3: - return "SSLv3" - elif protocol.value == SecurityConst.kSSLProtocol2: - return "SSLv2" - else: - raise ssl.SSLError(f"Unknown TLS version: {protocol!r}") - - -def makefile( - self: socket_cls, - mode: ( - Literal["r"] | Literal["w"] | Literal["rw"] | Literal["wr"] | Literal[""] - ) = "r", - buffering: int | None = None, - *args: typing.Any, - **kwargs: typing.Any, -) -> typing.BinaryIO | typing.TextIO: - # We disable buffering with SecureTransport because it conflicts with - # the buffering that ST does internally (see issue #1153 for more). - buffering = 0 - return socket_cls.makefile(self, mode, buffering, *args, **kwargs) - - -WrappedSocket.makefile = makefile # type: ignore[attr-defined] - - -class SecureTransportContext: - """ - I am a wrapper class for the SecureTransport library, to translate the - interface of the standard library ``SSLContext`` object to calls into - SecureTransport. - """ - - def __init__(self, protocol: int) -> None: - self._minimum_version: int = ssl.TLSVersion.MINIMUM_SUPPORTED - self._maximum_version: int = ssl.TLSVersion.MAXIMUM_SUPPORTED - if protocol not in (None, ssl.PROTOCOL_TLS, ssl.PROTOCOL_TLS_CLIENT): - self._min_version, self._max_version = _protocol_to_min_max[protocol] - - self._options = 0 - self._verify = False - self._trust_bundle: bytes | None = None - self._client_cert: str | None = None - self._client_key: str | None = None - self._client_key_passphrase = None - self._alpn_protocols: list[bytes] | None = None - - @property - def check_hostname(self) -> Literal[True]: - """ - SecureTransport cannot have its hostname checking disabled. For more, - see the comment on getpeercert() in this file. - """ - return True - - @check_hostname.setter - def check_hostname(self, value: typing.Any) -> None: - """ - SecureTransport cannot have its hostname checking disabled. For more, - see the comment on getpeercert() in this file. - """ - - @property - def options(self) -> int: - # TODO: Well, crap. - # - # So this is the bit of the code that is the most likely to cause us - # trouble. Essentially we need to enumerate all of the SSL options that - # users might want to use and try to see if we can sensibly translate - # them, or whether we should just ignore them. - return self._options - - @options.setter - def options(self, value: int) -> None: - # TODO: Update in line with above. - self._options = value - - @property - def verify_mode(self) -> int: - return ssl.CERT_REQUIRED if self._verify else ssl.CERT_NONE - - @verify_mode.setter - def verify_mode(self, value: int) -> None: - self._verify = value == ssl.CERT_REQUIRED - - def set_default_verify_paths(self) -> None: - # So, this has to do something a bit weird. Specifically, what it does - # is nothing. - # - # This means that, if we had previously had load_verify_locations - # called, this does not undo that. We need to do that because it turns - # out that the rest of the urllib3 code will attempt to load the - # default verify paths if it hasn't been told about any paths, even if - # the context itself was sometime earlier. We resolve that by just - # ignoring it. - pass - - def load_default_certs(self) -> None: - return self.set_default_verify_paths() - - def set_ciphers(self, ciphers: typing.Any) -> None: - raise ValueError("SecureTransport doesn't support custom cipher strings") - - def load_verify_locations( - self, - cafile: str | None = None, - capath: str | None = None, - cadata: bytes | None = None, - ) -> None: - # OK, we only really support cadata and cafile. - if capath is not None: - raise ValueError("SecureTransport does not support cert directories") - - # Raise if cafile does not exist. - if cafile is not None: - with open(cafile): - pass - - self._trust_bundle = cafile or cadata # type: ignore[assignment] - - def load_cert_chain( - self, - certfile: str, - keyfile: str | None = None, - password: str | None = None, - ) -> None: - self._client_cert = certfile - self._client_key = keyfile - self._client_cert_passphrase = password - - def set_alpn_protocols(self, protocols: list[str | bytes]) -> None: - """ - Sets the ALPN protocols that will later be set on the context. - - Raises a NotImplementedError if ALPN is not supported. - """ - if not hasattr(Security, "SSLSetALPNProtocols"): - raise NotImplementedError( - "SecureTransport supports ALPN only in macOS 10.12+" - ) - self._alpn_protocols = [util.util.to_bytes(p, "ascii") for p in protocols] - - def wrap_socket( - self, - sock: socket_cls, - server_side: bool = False, - do_handshake_on_connect: bool = True, - suppress_ragged_eofs: bool = True, - server_hostname: bytes | str | None = None, - ) -> WrappedSocket: - # So, what do we do here? Firstly, we assert some properties. This is a - # stripped down shim, so there is some functionality we don't support. - # See PEP 543 for the real deal. - assert not server_side - assert do_handshake_on_connect - assert suppress_ragged_eofs - - # Ok, we're good to go. Now we want to create the wrapped socket object - # and store it in the appropriate place. - wrapped_socket = WrappedSocket(sock) - - # Now we can handshake - wrapped_socket.handshake( - server_hostname, - self._verify, - self._trust_bundle, - _tls_version_to_st[self._minimum_version], - _tls_version_to_st[self._maximum_version], - self._client_cert, - self._client_key, - self._client_key_passphrase, - self._alpn_protocols, - ) - return wrapped_socket - - @property - def minimum_version(self) -> int: - return self._minimum_version - - @minimum_version.setter - def minimum_version(self, minimum_version: int) -> None: - self._minimum_version = minimum_version - - @property - def maximum_version(self) -> int: - return self._maximum_version - - @maximum_version.setter - def maximum_version(self, maximum_version: int) -> None: - self._maximum_version = maximum_version diff --git a/lib/urllib3/contrib/socks.py b/lib/urllib3/contrib/socks.py index 5e552dda..5a803916 100644 --- a/lib/urllib3/contrib/socks.py +++ b/lib/urllib3/contrib/socks.py @@ -41,7 +41,7 @@ with the proxy: from __future__ import annotations try: - import socks # type: ignore[import] + import socks # type: ignore[import-not-found] except ImportError: import warnings @@ -51,7 +51,7 @@ except ImportError: ( "SOCKS support in urllib3 requires the installation of optional " "dependencies: specifically, PySocks. For more information, see " - "https://urllib3.readthedocs.io/en/latest/contrib.html#socks-proxies" + "https://urllib3.readthedocs.io/en/latest/advanced-usage.html#socks-proxies" ), DependencyWarning, ) @@ -71,19 +71,16 @@ try: except ImportError: ssl = None # type: ignore[assignment] -try: - from typing import TypedDict +from typing import TypedDict - class _TYPE_SOCKS_OPTIONS(TypedDict): - socks_version: int - proxy_host: str | None - proxy_port: str | None - username: str | None - password: str | None - rdns: bool -except ImportError: # Python 3.7 - _TYPE_SOCKS_OPTIONS = typing.Dict[str, typing.Any] # type: ignore[misc, assignment] +class _TYPE_SOCKS_OPTIONS(TypedDict): + socks_version: int + proxy_host: str | None + proxy_port: str | None + username: str | None + password: str | None + rdns: bool class SOCKSConnection(HTTPConnection): diff --git a/lib/urllib3/exceptions.py b/lib/urllib3/exceptions.py index 5bb92369..b0792f00 100644 --- a/lib/urllib3/exceptions.py +++ b/lib/urllib3/exceptions.py @@ -252,13 +252,16 @@ class IncompleteRead(HTTPError, httplib_IncompleteRead): for ``partial`` to avoid creating large objects on streamed reads. """ + partial: int # type: ignore[assignment] + expected: int + def __init__(self, partial: int, expected: int) -> None: - self.partial = partial # type: ignore[assignment] + self.partial = partial self.expected = expected def __repr__(self) -> str: return "IncompleteRead(%i bytes read, %i more expected)" % ( - self.partial, # type: ignore[str-format] + self.partial, self.expected, ) diff --git a/lib/urllib3/fields.py b/lib/urllib3/fields.py index 51d898e2..3e258a5d 100644 --- a/lib/urllib3/fields.py +++ b/lib/urllib3/fields.py @@ -225,13 +225,9 @@ class RequestField: if isinstance(value, tuple): if len(value) == 3: - filename, data, content_type = typing.cast( - typing.Tuple[str, _TYPE_FIELD_VALUE, str], value - ) + filename, data, content_type = value else: - filename, data = typing.cast( - typing.Tuple[str, _TYPE_FIELD_VALUE], value - ) + filename, data = value content_type = guess_content_type(filename) else: filename = None diff --git a/lib/urllib3/http2.py b/lib/urllib3/http2.py new file mode 100644 index 00000000..15fa9d91 --- /dev/null +++ b/lib/urllib3/http2.py @@ -0,0 +1,229 @@ +from __future__ import annotations + +import threading +import types +import typing + +import h2.config # type: ignore[import-untyped] +import h2.connection # type: ignore[import-untyped] +import h2.events # type: ignore[import-untyped] + +import urllib3.connection +import urllib3.util.ssl_ +from urllib3.response import BaseHTTPResponse + +from ._collections import HTTPHeaderDict +from .connection import HTTPSConnection +from .connectionpool import HTTPSConnectionPool + +orig_HTTPSConnection = HTTPSConnection + +T = typing.TypeVar("T") + + +class _LockedObject(typing.Generic[T]): + """ + A wrapper class that hides a specific object behind a lock. + + The goal here is to provide a simple way to protect access to an object + that cannot safely be simultaneously accessed from multiple threads. The + intended use of this class is simple: take hold of it with a context + manager, which returns the protected object. + """ + + def __init__(self, obj: T): + self.lock = threading.RLock() + self._obj = obj + + def __enter__(self) -> T: + self.lock.acquire() + return self._obj + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: types.TracebackType | None, + ) -> None: + self.lock.release() + + +class HTTP2Connection(HTTPSConnection): + def __init__( + self, host: str, port: int | None = None, **kwargs: typing.Any + ) -> None: + self._h2_conn = self._new_h2_conn() + self._h2_stream: int | None = None + self._h2_headers: list[tuple[bytes, bytes]] = [] + + if "proxy" in kwargs or "proxy_config" in kwargs: # Defensive: + raise NotImplementedError("Proxies aren't supported with HTTP/2") + + super().__init__(host, port, **kwargs) + + def _new_h2_conn(self) -> _LockedObject[h2.connection.H2Connection]: + config = h2.config.H2Configuration(client_side=True) + return _LockedObject(h2.connection.H2Connection(config=config)) + + def connect(self) -> None: + super().connect() + + with self._h2_conn as h2_conn: + h2_conn.initiate_connection() + self.sock.sendall(h2_conn.data_to_send()) + + def putrequest( + self, + method: str, + url: str, + skip_host: bool = False, + skip_accept_encoding: bool = False, + ) -> None: + with self._h2_conn as h2_conn: + self._request_url = url + self._h2_stream = h2_conn.get_next_available_stream_id() + + if ":" in self.host: + authority = f"[{self.host}]:{self.port or 443}" + else: + authority = f"{self.host}:{self.port or 443}" + + self._h2_headers.extend( + ( + (b":scheme", b"https"), + (b":method", method.encode()), + (b":authority", authority.encode()), + (b":path", url.encode()), + ) + ) + + def putheader(self, header: str, *values: str) -> None: # type: ignore[override] + for value in values: + self._h2_headers.append( + (header.encode("utf-8").lower(), value.encode("utf-8")) + ) + + def endheaders(self) -> None: # type: ignore[override] + with self._h2_conn as h2_conn: + h2_conn.send_headers( + stream_id=self._h2_stream, + headers=self._h2_headers, + end_stream=True, + ) + if data_to_send := h2_conn.data_to_send(): + self.sock.sendall(data_to_send) + + def send(self, data: bytes) -> None: # type: ignore[override] # Defensive: + if not data: + return + raise NotImplementedError("Sending data isn't supported yet") + + def getresponse( # type: ignore[override] + self, + ) -> HTTP2Response: + status = None + data = bytearray() + with self._h2_conn as h2_conn: + end_stream = False + while not end_stream: + # TODO: Arbitrary read value. + if received_data := self.sock.recv(65535): + events = h2_conn.receive_data(received_data) + for event in events: + if isinstance(event, h2.events.ResponseReceived): + headers = HTTPHeaderDict() + for header, value in event.headers: + if header == b":status": + status = int(value.decode()) + else: + headers.add( + header.decode("ascii"), value.decode("ascii") + ) + + elif isinstance(event, h2.events.DataReceived): + data += event.data + h2_conn.acknowledge_received_data( + event.flow_controlled_length, event.stream_id + ) + + elif isinstance(event, h2.events.StreamEnded): + end_stream = True + + if data_to_send := h2_conn.data_to_send(): + self.sock.sendall(data_to_send) + + # We always close to not have to handle connection management. + self.close() + + assert status is not None + return HTTP2Response( + status=status, + headers=headers, + request_url=self._request_url, + data=bytes(data), + ) + + def close(self) -> None: + with self._h2_conn as h2_conn: + try: + h2_conn.close_connection() + if data := h2_conn.data_to_send(): + self.sock.sendall(data) + except Exception: + pass + + # Reset all our HTTP/2 connection state. + self._h2_conn = self._new_h2_conn() + self._h2_stream = None + self._h2_headers = [] + + super().close() + + +class HTTP2Response(BaseHTTPResponse): + # TODO: This is a woefully incomplete response object, but works for non-streaming. + def __init__( + self, + status: int, + headers: HTTPHeaderDict, + request_url: str, + data: bytes, + decode_content: bool = False, # TODO: support decoding + ) -> None: + super().__init__( + status=status, + headers=headers, + # Following CPython, we map HTTP versions to major * 10 + minor integers + version=20, + # No reason phrase in HTTP/2 + reason=None, + decode_content=decode_content, + request_url=request_url, + ) + self._data = data + self.length_remaining = 0 + + @property + def data(self) -> bytes: + return self._data + + def get_redirect_location(self) -> None: + return None + + def close(self) -> None: + pass + + +def inject_into_urllib3() -> None: + HTTPSConnectionPool.ConnectionCls = HTTP2Connection + urllib3.connection.HTTPSConnection = HTTP2Connection # type: ignore[misc] + + # TODO: Offer 'http/1.1' as well, but for testing purposes this is handy. + urllib3.util.ssl_.ALPN_PROTOCOLS = ["h2"] + + +def extract_from_urllib3() -> None: + HTTPSConnectionPool.ConnectionCls = orig_HTTPSConnection + urllib3.connection.HTTPSConnection = orig_HTTPSConnection # type: ignore[misc] + + urllib3.util.ssl_.ALPN_PROTOCOLS = ["http/1.1"] diff --git a/lib/urllib3/poolmanager.py b/lib/urllib3/poolmanager.py index 3c92a14d..32da0a00 100644 --- a/lib/urllib3/poolmanager.py +++ b/lib/urllib3/poolmanager.py @@ -26,8 +26,7 @@ from .util.url import Url, parse_url if typing.TYPE_CHECKING: import ssl - - from typing_extensions import Literal + from typing import Literal __all__ = ["PoolManager", "ProxyManager", "proxy_from_url"] @@ -39,6 +38,7 @@ SSL_KEYWORDS = ( "cert_file", "cert_reqs", "ca_certs", + "ca_cert_data", "ssl_version", "ssl_minimum_version", "ssl_maximum_version", @@ -74,6 +74,7 @@ class PoolKey(typing.NamedTuple): key_cert_file: str | None key_cert_reqs: str | None key_ca_certs: str | None + key_ca_cert_data: str | bytes | None key_ssl_version: int | str | None key_ssl_minimum_version: ssl.TLSVersion | None key_ssl_maximum_version: ssl.TLSVersion | None diff --git a/lib/urllib3/response.py b/lib/urllib3/response.py index 12097ea9..d31fac9b 100644 --- a/lib/urllib3/response.py +++ b/lib/urllib3/response.py @@ -14,16 +14,19 @@ from http.client import HTTPMessage as _HttplibHTTPMessage from http.client import HTTPResponse as _HttplibHTTPResponse from socket import timeout as SocketTimeout +if typing.TYPE_CHECKING: + from ._base_connection import BaseHTTPConnection + try: try: - import brotlicffi as brotli # type: ignore[import] + import brotlicffi as brotli # type: ignore[import-not-found] except ImportError: - import brotli # type: ignore[import] + import brotli # type: ignore[import-not-found] except ImportError: brotli = None try: - import zstandard as zstd # type: ignore[import] + import zstandard as zstd # type: ignore[import-not-found] # The package 'zstandard' added the 'eof' property starting # in v0.18.0 which we require to ensure a complete and @@ -58,7 +61,7 @@ from .util.response import is_fp_closed, is_response_to_head from .util.retry import Retry if typing.TYPE_CHECKING: - from typing_extensions import Literal + from typing import Literal from .connectionpool import HTTPConnectionPool @@ -208,7 +211,9 @@ def _get_decoder(mode: str) -> ContentDecoder: if "," in mode: return MultiDecoder(mode) - if mode == "gzip": + # According to RFC 9110 section 8.4.1.3, recipients should + # consider x-gzip equivalent to gzip + if mode in ("gzip", "x-gzip"): return GzipDecoder() if brotli is not None and mode == "br": @@ -278,9 +283,23 @@ class BytesQueueBuffer: return ret.getvalue() + def get_all(self) -> bytes: + buffer = self.buffer + if not buffer: + assert self._size == 0 + return b"" + if len(buffer) == 1: + result = buffer.pop() + else: + ret = io.BytesIO() + ret.writelines(buffer.popleft() for _ in range(len(buffer))) + result = ret.getvalue() + self._size = 0 + return result + class BaseHTTPResponse(io.IOBase): - CONTENT_DECODERS = ["gzip", "deflate"] + CONTENT_DECODERS = ["gzip", "x-gzip", "deflate"] if brotli is not None: CONTENT_DECODERS += ["br"] if zstd is not None: @@ -325,6 +344,7 @@ class BaseHTTPResponse(io.IOBase): self.chunked = True self._decoder: ContentDecoder | None = None + self.length_remaining: int | None def get_redirect_location(self) -> str | None | Literal[False]: """ @@ -364,7 +384,7 @@ class BaseHTTPResponse(io.IOBase): raise NotImplementedError() @property - def connection(self) -> HTTPConnection | None: + def connection(self) -> BaseHTTPConnection | None: raise NotImplementedError() @property @@ -391,6 +411,13 @@ class BaseHTTPResponse(io.IOBase): ) -> bytes: raise NotImplementedError() + def read1( + self, + amt: int | None = None, + decode_content: bool | None = None, + ) -> bytes: + raise NotImplementedError() + def read_chunked( self, amt: int | None = None, @@ -722,8 +749,18 @@ class HTTPResponse(BaseHTTPResponse): raise ReadTimeoutError(self._pool, None, "Read timed out.") from e # type: ignore[arg-type] + except IncompleteRead as e: + if ( + e.expected is not None + and e.partial is not None + and e.expected == -e.partial + ): + arg = "Response may not contain content." + else: + arg = f"Connection broken: {e!r}" + raise ProtocolError(arg, e) from e + except (HTTPException, OSError) as e: - # This includes IncompleteRead. raise ProtocolError(f"Connection broken: {e!r}", e) from e # If no exception is thrown, we should avoid cleaning up @@ -750,7 +787,12 @@ class HTTPResponse(BaseHTTPResponse): if self._original_response and self._original_response.isclosed(): self.release_conn() - def _fp_read(self, amt: int | None = None) -> bytes: + def _fp_read( + self, + amt: int | None = None, + *, + read1: bool = False, + ) -> bytes: """ Read a response with the thought that reading the number of bytes larger than can fit in a 32-bit int at a time via SSL in some @@ -767,13 +809,15 @@ class HTTPResponse(BaseHTTPResponse): assert self._fp c_int_max = 2**31 - 1 if ( - ( - (amt and amt > c_int_max) - or (self.length_remaining and self.length_remaining > c_int_max) + (amt and amt > c_int_max) + or ( + amt is None + and self.length_remaining + and self.length_remaining > c_int_max ) - and not util.IS_SECURETRANSPORT - and (util.IS_PYOPENSSL or sys.version_info < (3, 10)) - ): + ) and (util.IS_PYOPENSSL or sys.version_info < (3, 10)): + if read1: + return self._fp.read1(c_int_max) buffer = io.BytesIO() # Besides `max_chunk_amt` being a maximum chunk size, it # affects memory overhead of reading a response by this @@ -794,6 +838,8 @@ class HTTPResponse(BaseHTTPResponse): buffer.write(data) del data # to reduce peak memory usage by `max_chunk_amt`. return buffer.getvalue() + elif read1: + return self._fp.read1(amt) if amt is not None else self._fp.read1() else: # StringIO doesn't like amt=None return self._fp.read(amt) if amt is not None else self._fp.read() @@ -801,6 +847,8 @@ class HTTPResponse(BaseHTTPResponse): def _raw_read( self, amt: int | None = None, + *, + read1: bool = False, ) -> bytes: """ Reads `amt` of bytes from the socket. @@ -811,7 +859,7 @@ class HTTPResponse(BaseHTTPResponse): fp_closed = getattr(self._fp, "closed", False) with self._error_catcher(): - data = self._fp_read(amt) if not fp_closed else b"" + data = self._fp_read(amt, read1=read1) if not fp_closed else b"" if amt is not None and amt != 0 and not data: # Platform-specific: Buggy versions of Python. # Close the connection when no data is returned @@ -833,6 +881,14 @@ class HTTPResponse(BaseHTTPResponse): # raised during streaming, so all calls with incorrect # Content-Length are caught. raise IncompleteRead(self._fp_bytes_read, self.length_remaining) + elif read1 and ( + (amt != 0 and not data) or self.length_remaining == len(data) + ): + # All data has been read, but `self._fp.read1` in + # CPython 3.12 and older doesn't always close + # `http.client.HTTPResponse`, so we close it here. + # See https://github.com/python/cpython/issues/113199 + self._fp.close() if data: self._fp_bytes_read += len(data) @@ -911,6 +967,57 @@ class HTTPResponse(BaseHTTPResponse): return data + def read1( + self, + amt: int | None = None, + decode_content: bool | None = None, + ) -> bytes: + """ + Similar to ``http.client.HTTPResponse.read1`` and documented + in :meth:`io.BufferedReader.read1`, but with an additional parameter: + ``decode_content``. + + :param amt: + How much of the content to read. + + :param decode_content: + If True, will attempt to decode the body based on the + 'content-encoding' header. + """ + if decode_content is None: + decode_content = self.decode_content + # try and respond without going to the network + if self._has_decoded_content: + if not decode_content: + raise RuntimeError( + "Calling read1(decode_content=False) is not supported after " + "read1(decode_content=True) was called." + ) + if len(self._decoded_buffer) > 0: + if amt is None: + return self._decoded_buffer.get_all() + return self._decoded_buffer.get(amt) + if amt == 0: + return b"" + + # FIXME, this method's type doesn't say returning None is possible + data = self._raw_read(amt, read1=True) + if not decode_content or data is None: + return data + + self._init_decoder() + while True: + flush_decoder = not data + decoded_data = self._decode(data, decode_content, flush_decoder) + self._decoded_buffer.put(decoded_data) + if decoded_data or flush_decoder: + break + data = self._raw_read(8192, read1=True) + + if amt is None: + return self._decoded_buffer.get_all() + return self._decoded_buffer.get(amt) + def stream( self, amt: int | None = 2**16, decode_content: bool | None = None ) -> typing.Generator[bytes, None, None]: @@ -1003,9 +1110,13 @@ class HTTPResponse(BaseHTTPResponse): try: self.chunk_left = int(line, 16) except ValueError: - # Invalid chunked protocol response, abort. self.close() - raise InvalidChunkLength(self, line) from None + if line: + # Invalid chunked protocol response, abort. + raise InvalidChunkLength(self, line) from None + else: + # Truncated at start of next chunk + raise ProtocolError("Response ended prematurely") from None def _handle_chunk(self, amt: int | None) -> bytes: returned_chunk = None diff --git a/lib/urllib3/util/__init__.py b/lib/urllib3/util/__init__.py index ff56c55b..53412603 100644 --- a/lib/urllib3/util/__init__.py +++ b/lib/urllib3/util/__init__.py @@ -8,7 +8,6 @@ from .retry import Retry from .ssl_ import ( ALPN_PROTOCOLS, IS_PYOPENSSL, - IS_SECURETRANSPORT, SSLContext, assert_fingerprint, create_urllib3_context, @@ -22,7 +21,6 @@ from .wait import wait_for_read, wait_for_write __all__ = ( "IS_PYOPENSSL", - "IS_SECURETRANSPORT", "SSLContext", "ALPN_PROTOCOLS", "Retry", diff --git a/lib/urllib3/util/request.py b/lib/urllib3/util/request.py index 7d6866f3..fe0e3485 100644 --- a/lib/urllib3/util/request.py +++ b/lib/urllib3/util/request.py @@ -9,7 +9,7 @@ from ..exceptions import UnrewindableBodyError from .util import to_bytes if typing.TYPE_CHECKING: - from typing_extensions import Final + from typing import Final # Pass as a value within ``headers`` to skip # emitting some HTTP headers that are added automatically. @@ -21,15 +21,15 @@ SKIPPABLE_HEADERS = frozenset(["accept-encoding", "host", "user-agent"]) ACCEPT_ENCODING = "gzip,deflate" try: try: - import brotlicffi as _unused_module_brotli # type: ignore[import] # noqa: F401 + import brotlicffi as _unused_module_brotli # type: ignore[import-not-found] # noqa: F401 except ImportError: - import brotli as _unused_module_brotli # type: ignore[import] # noqa: F401 + import brotli as _unused_module_brotli # type: ignore[import-not-found] # noqa: F401 except ImportError: pass else: ACCEPT_ENCODING += ",br" try: - import zstandard as _unused_module_zstd # type: ignore[import] # noqa: F401 + import zstandard as _unused_module_zstd # type: ignore[import-not-found] # noqa: F401 except ImportError: pass else: diff --git a/lib/urllib3/util/ssl_.py b/lib/urllib3/util/ssl_.py index e35e3940..b14cf27b 100644 --- a/lib/urllib3/util/ssl_.py +++ b/lib/urllib3/util/ssl_.py @@ -16,7 +16,6 @@ SSLContext = None SSLTransport = None HAS_NEVER_CHECK_COMMON_NAME = False IS_PYOPENSSL = False -IS_SECURETRANSPORT = False ALPN_PROTOCOLS = ["http/1.1"] _TYPE_VERSION_INFO = typing.Tuple[int, int, int, str, int] @@ -42,7 +41,7 @@ def _is_bpo_43522_fixed( """ if implementation_name == "pypy": # https://foss.heptapod.net/pypy/pypy/-/issues/3129 - return pypy_version_info >= (7, 3, 8) and version_info >= (3, 8) # type: ignore[operator] + return pypy_version_info >= (7, 3, 8) # type: ignore[operator] elif implementation_name == "cpython": major_minor = version_info[:2] micro = version_info[2] @@ -79,8 +78,7 @@ def _is_has_never_check_common_name_reliable( if typing.TYPE_CHECKING: from ssl import VerifyMode - - from typing_extensions import Literal, TypedDict + from typing import Literal, TypedDict from .ssltransport import SSLTransport as SSLTransportType @@ -321,13 +319,9 @@ def create_urllib3_context( # Enable post-handshake authentication for TLS 1.3, see GH #1634. PHA is # necessary for conditional client cert authentication with TLS 1.3. - # The attribute is None for OpenSSL <= 1.1.0 or does not exist in older - # versions of Python. We only enable on Python 3.7.4+ or if certificate - # verification is enabled to work around Python issue #37428 - # See: https://bugs.python.org/issue37428 - if (cert_reqs == ssl.CERT_REQUIRED or sys.version_info >= (3, 7, 4)) and getattr( - context, "post_handshake_auth", None - ) is not None: + # The attribute is None for OpenSSL <= 1.1.0 or does not exist when using + # an SSLContext created by pyOpenSSL. + if getattr(context, "post_handshake_auth", None) is not None: context.post_handshake_auth = True # The order of the below lines setting verify_mode and check_hostname diff --git a/lib/urllib3/util/ssltransport.py b/lib/urllib3/util/ssltransport.py index 5ec86473..fa9f2b37 100644 --- a/lib/urllib3/util/ssltransport.py +++ b/lib/urllib3/util/ssltransport.py @@ -8,7 +8,7 @@ import typing from ..exceptions import ProxySchemeUnsupported if typing.TYPE_CHECKING: - from typing_extensions import Literal + from typing import Literal from .ssl_ import _TYPE_PEER_CERT_RET, _TYPE_PEER_CERT_RET_DICT diff --git a/lib/urllib3/util/timeout.py b/lib/urllib3/util/timeout.py index ec090f69..4bb1be11 100644 --- a/lib/urllib3/util/timeout.py +++ b/lib/urllib3/util/timeout.py @@ -8,7 +8,7 @@ from socket import getdefaulttimeout from ..exceptions import TimeoutStateError if typing.TYPE_CHECKING: - from typing_extensions import Final + from typing import Final class _TYPE_DEFAULT(Enum): @@ -101,10 +101,6 @@ class Timeout: the case; if a server streams one byte every fifteen seconds, a timeout of 20 seconds will not trigger, even though the request will take several minutes to complete. - - If your goal is to cut off any request after a set amount of wall clock - time, consider having a second "watcher" thread to cut off a slow - request. """ #: A sentinel object representing the default timeout value From 958318491c6e40b15aff28227d48035b7204ab57 Mon Sep 17 00:00:00 2001 From: JackDandy Date: Wed, 5 Jun 2024 09:06:55 +0100 Subject: [PATCH 03/29] =?UTF-8?q?Update=20idna=20library=203.4=20(cab054c)?= =?UTF-8?q?=20=E2=86=92=203.7=20(1d365e1).?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGES.md | 1 + lib/idna/codec.py | 4 +- lib/idna/core.py | 25 +- lib/idna/idnadata.py | 2201 +++++++++++++++++++++++++++++++++++++- lib/idna/package_data.py | 2 +- lib/idna/py.typed | 0 lib/idna/uts46data.py | 454 ++++---- 7 files changed, 2389 insertions(+), 298 deletions(-) create mode 100644 lib/idna/py.typed diff --git a/CHANGES.md b/CHANGES.md index 0c31e938..4b9649cb 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,6 @@ ### 3.32.0 (2024-xx-xx xx:xx:00 UTC) +* Update idna library 3.4 (cab054c) to 3.7 (1d365e1) * Update Requests library 2.31.0 (8812812) to 2.32.3 (0e322af) * Update urllib3 2.0.7 (56f01e0) to 2.2.1 (54d6edf) diff --git a/lib/idna/codec.py b/lib/idna/codec.py index eaeada58..c855a4de 100644 --- a/lib/idna/codec.py +++ b/lib/idna/codec.py @@ -107,8 +107,8 @@ def search_function(name: str) -> Optional[codecs.CodecInfo]: return None return codecs.CodecInfo( name=name, - encode=Codec().encode, # type: ignore - decode=Codec().decode, # type: ignore + encode=Codec().encode, + decode=Codec().decode, incrementalencoder=IncrementalEncoder, incrementaldecoder=IncrementalDecoder, streamwriter=StreamWriter, diff --git a/lib/idna/core.py b/lib/idna/core.py index 0bd89a3c..0dae61ac 100644 --- a/lib/idna/core.py +++ b/lib/idna/core.py @@ -150,9 +150,11 @@ def valid_contextj(label: str, pos: int) -> bool: joining_type = idnadata.joining_types.get(ord(label[i])) if joining_type == ord('T'): continue - if joining_type in [ord('L'), ord('D')]: + elif joining_type in [ord('L'), ord('D')]: ok = True break + else: + break if not ok: return False @@ -162,9 +164,11 @@ def valid_contextj(label: str, pos: int) -> bool: joining_type = idnadata.joining_types.get(ord(label[i])) if joining_type == ord('T'): continue - if joining_type in [ord('R'), ord('D')]: + elif joining_type in [ord('R'), ord('D')]: ok = True break + else: + break return ok if cp_value == 0x200d: @@ -236,12 +240,8 @@ def check_label(label: Union[str, bytes, bytearray]) -> None: if intranges_contain(cp_value, idnadata.codepoint_classes['PVALID']): continue elif intranges_contain(cp_value, idnadata.codepoint_classes['CONTEXTJ']): - try: - if not valid_contextj(label, pos): - raise InvalidCodepointContext('Joiner {} not allowed at position {} in {}'.format( - _unot(cp_value), pos+1, repr(label))) - except ValueError: - raise IDNAError('Unknown codepoint adjacent to joiner {} at position {} in {}'.format( + if not valid_contextj(label, pos): + raise InvalidCodepointContext('Joiner {} not allowed at position {} in {}'.format( _unot(cp_value), pos+1, repr(label))) elif intranges_contain(cp_value, idnadata.codepoint_classes['CONTEXTO']): if not valid_contexto(label, pos): @@ -262,13 +262,8 @@ def alabel(label: str) -> bytes: except UnicodeEncodeError: pass - if not label: - raise IDNAError('No Input') - - label = str(label) check_label(label) - label_bytes = _punycode(label) - label_bytes = _alabel_prefix + label_bytes + label_bytes = _alabel_prefix + _punycode(label) if not valid_label_length(label_bytes): raise IDNAError('Label too long') @@ -318,7 +313,7 @@ def uts46_remap(domain: str, std3_rules: bool = True, transitional: bool = False status = uts46row[1] replacement = None # type: Optional[str] if len(uts46row) == 3: - replacement = uts46row[2] # type: ignore + replacement = uts46row[2] if (status == 'V' or (status == 'D' and not transitional) or (status == '3' and not std3_rules and replacement is None)): diff --git a/lib/idna/idnadata.py b/lib/idna/idnadata.py index f9bc0d85..c61dcf97 100644 --- a/lib/idna/idnadata.py +++ b/lib/idna/idnadata.py @@ -1,6 +1,6 @@ # This file is automatically generated by tools/idna-data -__version__ = '15.0.0' +__version__ = '15.1.0' scripts = { 'Greek': ( 0x37000000374, @@ -59,6 +59,7 @@ scripts = { 0x2b7400002b81e, 0x2b8200002cea2, 0x2ceb00002ebe1, + 0x2ebf00002ee5e, 0x2f8000002fa1e, 0x300000003134b, 0x31350000323b0, @@ -100,16 +101,190 @@ scripts = { ), } joining_types = { - 0x600: 85, - 0x601: 85, - 0x602: 85, - 0x603: 85, - 0x604: 85, - 0x605: 85, - 0x608: 85, - 0x60b: 85, + 0xad: 84, + 0x300: 84, + 0x301: 84, + 0x302: 84, + 0x303: 84, + 0x304: 84, + 0x305: 84, + 0x306: 84, + 0x307: 84, + 0x308: 84, + 0x309: 84, + 0x30a: 84, + 0x30b: 84, + 0x30c: 84, + 0x30d: 84, + 0x30e: 84, + 0x30f: 84, + 0x310: 84, + 0x311: 84, + 0x312: 84, + 0x313: 84, + 0x314: 84, + 0x315: 84, + 0x316: 84, + 0x317: 84, + 0x318: 84, + 0x319: 84, + 0x31a: 84, + 0x31b: 84, + 0x31c: 84, + 0x31d: 84, + 0x31e: 84, + 0x31f: 84, + 0x320: 84, + 0x321: 84, + 0x322: 84, + 0x323: 84, + 0x324: 84, + 0x325: 84, + 0x326: 84, + 0x327: 84, + 0x328: 84, + 0x329: 84, + 0x32a: 84, + 0x32b: 84, + 0x32c: 84, + 0x32d: 84, + 0x32e: 84, + 0x32f: 84, + 0x330: 84, + 0x331: 84, + 0x332: 84, + 0x333: 84, + 0x334: 84, + 0x335: 84, + 0x336: 84, + 0x337: 84, + 0x338: 84, + 0x339: 84, + 0x33a: 84, + 0x33b: 84, + 0x33c: 84, + 0x33d: 84, + 0x33e: 84, + 0x33f: 84, + 0x340: 84, + 0x341: 84, + 0x342: 84, + 0x343: 84, + 0x344: 84, + 0x345: 84, + 0x346: 84, + 0x347: 84, + 0x348: 84, + 0x349: 84, + 0x34a: 84, + 0x34b: 84, + 0x34c: 84, + 0x34d: 84, + 0x34e: 84, + 0x34f: 84, + 0x350: 84, + 0x351: 84, + 0x352: 84, + 0x353: 84, + 0x354: 84, + 0x355: 84, + 0x356: 84, + 0x357: 84, + 0x358: 84, + 0x359: 84, + 0x35a: 84, + 0x35b: 84, + 0x35c: 84, + 0x35d: 84, + 0x35e: 84, + 0x35f: 84, + 0x360: 84, + 0x361: 84, + 0x362: 84, + 0x363: 84, + 0x364: 84, + 0x365: 84, + 0x366: 84, + 0x367: 84, + 0x368: 84, + 0x369: 84, + 0x36a: 84, + 0x36b: 84, + 0x36c: 84, + 0x36d: 84, + 0x36e: 84, + 0x36f: 84, + 0x483: 84, + 0x484: 84, + 0x485: 84, + 0x486: 84, + 0x487: 84, + 0x488: 84, + 0x489: 84, + 0x591: 84, + 0x592: 84, + 0x593: 84, + 0x594: 84, + 0x595: 84, + 0x596: 84, + 0x597: 84, + 0x598: 84, + 0x599: 84, + 0x59a: 84, + 0x59b: 84, + 0x59c: 84, + 0x59d: 84, + 0x59e: 84, + 0x59f: 84, + 0x5a0: 84, + 0x5a1: 84, + 0x5a2: 84, + 0x5a3: 84, + 0x5a4: 84, + 0x5a5: 84, + 0x5a6: 84, + 0x5a7: 84, + 0x5a8: 84, + 0x5a9: 84, + 0x5aa: 84, + 0x5ab: 84, + 0x5ac: 84, + 0x5ad: 84, + 0x5ae: 84, + 0x5af: 84, + 0x5b0: 84, + 0x5b1: 84, + 0x5b2: 84, + 0x5b3: 84, + 0x5b4: 84, + 0x5b5: 84, + 0x5b6: 84, + 0x5b7: 84, + 0x5b8: 84, + 0x5b9: 84, + 0x5ba: 84, + 0x5bb: 84, + 0x5bc: 84, + 0x5bd: 84, + 0x5bf: 84, + 0x5c1: 84, + 0x5c2: 84, + 0x5c4: 84, + 0x5c5: 84, + 0x5c7: 84, + 0x610: 84, + 0x611: 84, + 0x612: 84, + 0x613: 84, + 0x614: 84, + 0x615: 84, + 0x616: 84, + 0x617: 84, + 0x618: 84, + 0x619: 84, + 0x61a: 84, + 0x61c: 84, 0x620: 68, - 0x621: 85, 0x622: 82, 0x623: 82, 0x624: 82, @@ -151,12 +326,33 @@ joining_types = { 0x648: 82, 0x649: 68, 0x64a: 68, + 0x64b: 84, + 0x64c: 84, + 0x64d: 84, + 0x64e: 84, + 0x64f: 84, + 0x650: 84, + 0x651: 84, + 0x652: 84, + 0x653: 84, + 0x654: 84, + 0x655: 84, + 0x656: 84, + 0x657: 84, + 0x658: 84, + 0x659: 84, + 0x65a: 84, + 0x65b: 84, + 0x65c: 84, + 0x65d: 84, + 0x65e: 84, + 0x65f: 84, 0x66e: 68, 0x66f: 68, + 0x670: 84, 0x671: 82, 0x672: 82, 0x673: 82, - 0x674: 85, 0x675: 82, 0x676: 82, 0x677: 82, @@ -253,7 +449,25 @@ joining_types = { 0x6d2: 82, 0x6d3: 82, 0x6d5: 82, - 0x6dd: 85, + 0x6d6: 84, + 0x6d7: 84, + 0x6d8: 84, + 0x6d9: 84, + 0x6da: 84, + 0x6db: 84, + 0x6dc: 84, + 0x6df: 84, + 0x6e0: 84, + 0x6e1: 84, + 0x6e2: 84, + 0x6e3: 84, + 0x6e4: 84, + 0x6e7: 84, + 0x6e8: 84, + 0x6ea: 84, + 0x6eb: 84, + 0x6ec: 84, + 0x6ed: 84, 0x6ee: 82, 0x6ef: 82, 0x6fa: 68, @@ -262,6 +476,7 @@ joining_types = { 0x6ff: 68, 0x70f: 84, 0x710: 82, + 0x711: 84, 0x712: 68, 0x713: 68, 0x714: 68, @@ -292,6 +507,33 @@ joining_types = { 0x72d: 68, 0x72e: 68, 0x72f: 82, + 0x730: 84, + 0x731: 84, + 0x732: 84, + 0x733: 84, + 0x734: 84, + 0x735: 84, + 0x736: 84, + 0x737: 84, + 0x738: 84, + 0x739: 84, + 0x73a: 84, + 0x73b: 84, + 0x73c: 84, + 0x73d: 84, + 0x73e: 84, + 0x73f: 84, + 0x740: 84, + 0x741: 84, + 0x742: 84, + 0x743: 84, + 0x744: 84, + 0x745: 84, + 0x746: 84, + 0x747: 84, + 0x748: 84, + 0x749: 84, + 0x74a: 84, 0x74d: 82, 0x74e: 68, 0x74f: 68, @@ -343,6 +585,17 @@ joining_types = { 0x77d: 68, 0x77e: 68, 0x77f: 68, + 0x7a6: 84, + 0x7a7: 84, + 0x7a8: 84, + 0x7a9: 84, + 0x7aa: 84, + 0x7ab: 84, + 0x7ac: 84, + 0x7ad: 84, + 0x7ae: 84, + 0x7af: 84, + 0x7b0: 84, 0x7ca: 68, 0x7cb: 68, 0x7cc: 68, @@ -376,7 +629,38 @@ joining_types = { 0x7e8: 68, 0x7e9: 68, 0x7ea: 68, + 0x7eb: 84, + 0x7ec: 84, + 0x7ed: 84, + 0x7ee: 84, + 0x7ef: 84, + 0x7f0: 84, + 0x7f1: 84, + 0x7f2: 84, + 0x7f3: 84, 0x7fa: 67, + 0x7fd: 84, + 0x816: 84, + 0x817: 84, + 0x818: 84, + 0x819: 84, + 0x81b: 84, + 0x81c: 84, + 0x81d: 84, + 0x81e: 84, + 0x81f: 84, + 0x820: 84, + 0x821: 84, + 0x822: 84, + 0x823: 84, + 0x825: 84, + 0x826: 84, + 0x827: 84, + 0x829: 84, + 0x82a: 84, + 0x82b: 84, + 0x82c: 84, + 0x82d: 84, 0x840: 82, 0x841: 68, 0x842: 68, @@ -402,13 +686,14 @@ joining_types = { 0x856: 82, 0x857: 82, 0x858: 82, + 0x859: 84, + 0x85a: 84, + 0x85b: 84, 0x860: 68, - 0x861: 85, 0x862: 68, 0x863: 68, 0x864: 68, 0x865: 68, - 0x866: 85, 0x867: 82, 0x868: 68, 0x869: 82, @@ -436,16 +721,20 @@ joining_types = { 0x884: 67, 0x885: 67, 0x886: 68, - 0x887: 85, - 0x888: 85, 0x889: 68, 0x88a: 68, 0x88b: 68, 0x88c: 68, 0x88d: 68, 0x88e: 82, - 0x890: 85, - 0x891: 85, + 0x898: 84, + 0x899: 84, + 0x89a: 84, + 0x89b: 84, + 0x89c: 84, + 0x89d: 84, + 0x89e: 84, + 0x89f: 84, 0x8a0: 68, 0x8a1: 68, 0x8a2: 68, @@ -459,7 +748,6 @@ joining_types = { 0x8aa: 82, 0x8ab: 82, 0x8ac: 82, - 0x8ad: 85, 0x8ae: 82, 0x8af: 68, 0x8b0: 68, @@ -487,11 +775,357 @@ joining_types = { 0x8c6: 68, 0x8c7: 68, 0x8c8: 68, - 0x8e2: 85, - 0x1806: 85, + 0x8ca: 84, + 0x8cb: 84, + 0x8cc: 84, + 0x8cd: 84, + 0x8ce: 84, + 0x8cf: 84, + 0x8d0: 84, + 0x8d1: 84, + 0x8d2: 84, + 0x8d3: 84, + 0x8d4: 84, + 0x8d5: 84, + 0x8d6: 84, + 0x8d7: 84, + 0x8d8: 84, + 0x8d9: 84, + 0x8da: 84, + 0x8db: 84, + 0x8dc: 84, + 0x8dd: 84, + 0x8de: 84, + 0x8df: 84, + 0x8e0: 84, + 0x8e1: 84, + 0x8e3: 84, + 0x8e4: 84, + 0x8e5: 84, + 0x8e6: 84, + 0x8e7: 84, + 0x8e8: 84, + 0x8e9: 84, + 0x8ea: 84, + 0x8eb: 84, + 0x8ec: 84, + 0x8ed: 84, + 0x8ee: 84, + 0x8ef: 84, + 0x8f0: 84, + 0x8f1: 84, + 0x8f2: 84, + 0x8f3: 84, + 0x8f4: 84, + 0x8f5: 84, + 0x8f6: 84, + 0x8f7: 84, + 0x8f8: 84, + 0x8f9: 84, + 0x8fa: 84, + 0x8fb: 84, + 0x8fc: 84, + 0x8fd: 84, + 0x8fe: 84, + 0x8ff: 84, + 0x900: 84, + 0x901: 84, + 0x902: 84, + 0x93a: 84, + 0x93c: 84, + 0x941: 84, + 0x942: 84, + 0x943: 84, + 0x944: 84, + 0x945: 84, + 0x946: 84, + 0x947: 84, + 0x948: 84, + 0x94d: 84, + 0x951: 84, + 0x952: 84, + 0x953: 84, + 0x954: 84, + 0x955: 84, + 0x956: 84, + 0x957: 84, + 0x962: 84, + 0x963: 84, + 0x981: 84, + 0x9bc: 84, + 0x9c1: 84, + 0x9c2: 84, + 0x9c3: 84, + 0x9c4: 84, + 0x9cd: 84, + 0x9e2: 84, + 0x9e3: 84, + 0x9fe: 84, + 0xa01: 84, + 0xa02: 84, + 0xa3c: 84, + 0xa41: 84, + 0xa42: 84, + 0xa47: 84, + 0xa48: 84, + 0xa4b: 84, + 0xa4c: 84, + 0xa4d: 84, + 0xa51: 84, + 0xa70: 84, + 0xa71: 84, + 0xa75: 84, + 0xa81: 84, + 0xa82: 84, + 0xabc: 84, + 0xac1: 84, + 0xac2: 84, + 0xac3: 84, + 0xac4: 84, + 0xac5: 84, + 0xac7: 84, + 0xac8: 84, + 0xacd: 84, + 0xae2: 84, + 0xae3: 84, + 0xafa: 84, + 0xafb: 84, + 0xafc: 84, + 0xafd: 84, + 0xafe: 84, + 0xaff: 84, + 0xb01: 84, + 0xb3c: 84, + 0xb3f: 84, + 0xb41: 84, + 0xb42: 84, + 0xb43: 84, + 0xb44: 84, + 0xb4d: 84, + 0xb55: 84, + 0xb56: 84, + 0xb62: 84, + 0xb63: 84, + 0xb82: 84, + 0xbc0: 84, + 0xbcd: 84, + 0xc00: 84, + 0xc04: 84, + 0xc3c: 84, + 0xc3e: 84, + 0xc3f: 84, + 0xc40: 84, + 0xc46: 84, + 0xc47: 84, + 0xc48: 84, + 0xc4a: 84, + 0xc4b: 84, + 0xc4c: 84, + 0xc4d: 84, + 0xc55: 84, + 0xc56: 84, + 0xc62: 84, + 0xc63: 84, + 0xc81: 84, + 0xcbc: 84, + 0xcbf: 84, + 0xcc6: 84, + 0xccc: 84, + 0xccd: 84, + 0xce2: 84, + 0xce3: 84, + 0xd00: 84, + 0xd01: 84, + 0xd3b: 84, + 0xd3c: 84, + 0xd41: 84, + 0xd42: 84, + 0xd43: 84, + 0xd44: 84, + 0xd4d: 84, + 0xd62: 84, + 0xd63: 84, + 0xd81: 84, + 0xdca: 84, + 0xdd2: 84, + 0xdd3: 84, + 0xdd4: 84, + 0xdd6: 84, + 0xe31: 84, + 0xe34: 84, + 0xe35: 84, + 0xe36: 84, + 0xe37: 84, + 0xe38: 84, + 0xe39: 84, + 0xe3a: 84, + 0xe47: 84, + 0xe48: 84, + 0xe49: 84, + 0xe4a: 84, + 0xe4b: 84, + 0xe4c: 84, + 0xe4d: 84, + 0xe4e: 84, + 0xeb1: 84, + 0xeb4: 84, + 0xeb5: 84, + 0xeb6: 84, + 0xeb7: 84, + 0xeb8: 84, + 0xeb9: 84, + 0xeba: 84, + 0xebb: 84, + 0xebc: 84, + 0xec8: 84, + 0xec9: 84, + 0xeca: 84, + 0xecb: 84, + 0xecc: 84, + 0xecd: 84, + 0xece: 84, + 0xf18: 84, + 0xf19: 84, + 0xf35: 84, + 0xf37: 84, + 0xf39: 84, + 0xf71: 84, + 0xf72: 84, + 0xf73: 84, + 0xf74: 84, + 0xf75: 84, + 0xf76: 84, + 0xf77: 84, + 0xf78: 84, + 0xf79: 84, + 0xf7a: 84, + 0xf7b: 84, + 0xf7c: 84, + 0xf7d: 84, + 0xf7e: 84, + 0xf80: 84, + 0xf81: 84, + 0xf82: 84, + 0xf83: 84, + 0xf84: 84, + 0xf86: 84, + 0xf87: 84, + 0xf8d: 84, + 0xf8e: 84, + 0xf8f: 84, + 0xf90: 84, + 0xf91: 84, + 0xf92: 84, + 0xf93: 84, + 0xf94: 84, + 0xf95: 84, + 0xf96: 84, + 0xf97: 84, + 0xf99: 84, + 0xf9a: 84, + 0xf9b: 84, + 0xf9c: 84, + 0xf9d: 84, + 0xf9e: 84, + 0xf9f: 84, + 0xfa0: 84, + 0xfa1: 84, + 0xfa2: 84, + 0xfa3: 84, + 0xfa4: 84, + 0xfa5: 84, + 0xfa6: 84, + 0xfa7: 84, + 0xfa8: 84, + 0xfa9: 84, + 0xfaa: 84, + 0xfab: 84, + 0xfac: 84, + 0xfad: 84, + 0xfae: 84, + 0xfaf: 84, + 0xfb0: 84, + 0xfb1: 84, + 0xfb2: 84, + 0xfb3: 84, + 0xfb4: 84, + 0xfb5: 84, + 0xfb6: 84, + 0xfb7: 84, + 0xfb8: 84, + 0xfb9: 84, + 0xfba: 84, + 0xfbb: 84, + 0xfbc: 84, + 0xfc6: 84, + 0x102d: 84, + 0x102e: 84, + 0x102f: 84, + 0x1030: 84, + 0x1032: 84, + 0x1033: 84, + 0x1034: 84, + 0x1035: 84, + 0x1036: 84, + 0x1037: 84, + 0x1039: 84, + 0x103a: 84, + 0x103d: 84, + 0x103e: 84, + 0x1058: 84, + 0x1059: 84, + 0x105e: 84, + 0x105f: 84, + 0x1060: 84, + 0x1071: 84, + 0x1072: 84, + 0x1073: 84, + 0x1074: 84, + 0x1082: 84, + 0x1085: 84, + 0x1086: 84, + 0x108d: 84, + 0x109d: 84, + 0x135d: 84, + 0x135e: 84, + 0x135f: 84, + 0x1712: 84, + 0x1713: 84, + 0x1714: 84, + 0x1732: 84, + 0x1733: 84, + 0x1752: 84, + 0x1753: 84, + 0x1772: 84, + 0x1773: 84, + 0x17b4: 84, + 0x17b5: 84, + 0x17b7: 84, + 0x17b8: 84, + 0x17b9: 84, + 0x17ba: 84, + 0x17bb: 84, + 0x17bc: 84, + 0x17bd: 84, + 0x17c6: 84, + 0x17c9: 84, + 0x17ca: 84, + 0x17cb: 84, + 0x17cc: 84, + 0x17cd: 84, + 0x17ce: 84, + 0x17cf: 84, + 0x17d0: 84, + 0x17d1: 84, + 0x17d2: 84, + 0x17d3: 84, + 0x17dd: 84, 0x1807: 68, 0x180a: 67, - 0x180e: 85, + 0x180b: 84, + 0x180c: 84, + 0x180d: 84, + 0x180f: 84, 0x1820: 68, 0x1821: 68, 0x1822: 68, @@ -581,11 +1215,6 @@ joining_types = { 0x1876: 68, 0x1877: 68, 0x1878: 68, - 0x1880: 85, - 0x1881: 85, - 0x1882: 85, - 0x1883: 85, - 0x1884: 85, 0x1885: 84, 0x1886: 84, 0x1887: 68, @@ -622,14 +1251,339 @@ joining_types = { 0x18a6: 68, 0x18a7: 68, 0x18a8: 68, + 0x18a9: 84, 0x18aa: 68, - 0x200c: 85, + 0x1920: 84, + 0x1921: 84, + 0x1922: 84, + 0x1927: 84, + 0x1928: 84, + 0x1932: 84, + 0x1939: 84, + 0x193a: 84, + 0x193b: 84, + 0x1a17: 84, + 0x1a18: 84, + 0x1a1b: 84, + 0x1a56: 84, + 0x1a58: 84, + 0x1a59: 84, + 0x1a5a: 84, + 0x1a5b: 84, + 0x1a5c: 84, + 0x1a5d: 84, + 0x1a5e: 84, + 0x1a60: 84, + 0x1a62: 84, + 0x1a65: 84, + 0x1a66: 84, + 0x1a67: 84, + 0x1a68: 84, + 0x1a69: 84, + 0x1a6a: 84, + 0x1a6b: 84, + 0x1a6c: 84, + 0x1a73: 84, + 0x1a74: 84, + 0x1a75: 84, + 0x1a76: 84, + 0x1a77: 84, + 0x1a78: 84, + 0x1a79: 84, + 0x1a7a: 84, + 0x1a7b: 84, + 0x1a7c: 84, + 0x1a7f: 84, + 0x1ab0: 84, + 0x1ab1: 84, + 0x1ab2: 84, + 0x1ab3: 84, + 0x1ab4: 84, + 0x1ab5: 84, + 0x1ab6: 84, + 0x1ab7: 84, + 0x1ab8: 84, + 0x1ab9: 84, + 0x1aba: 84, + 0x1abb: 84, + 0x1abc: 84, + 0x1abd: 84, + 0x1abe: 84, + 0x1abf: 84, + 0x1ac0: 84, + 0x1ac1: 84, + 0x1ac2: 84, + 0x1ac3: 84, + 0x1ac4: 84, + 0x1ac5: 84, + 0x1ac6: 84, + 0x1ac7: 84, + 0x1ac8: 84, + 0x1ac9: 84, + 0x1aca: 84, + 0x1acb: 84, + 0x1acc: 84, + 0x1acd: 84, + 0x1ace: 84, + 0x1b00: 84, + 0x1b01: 84, + 0x1b02: 84, + 0x1b03: 84, + 0x1b34: 84, + 0x1b36: 84, + 0x1b37: 84, + 0x1b38: 84, + 0x1b39: 84, + 0x1b3a: 84, + 0x1b3c: 84, + 0x1b42: 84, + 0x1b6b: 84, + 0x1b6c: 84, + 0x1b6d: 84, + 0x1b6e: 84, + 0x1b6f: 84, + 0x1b70: 84, + 0x1b71: 84, + 0x1b72: 84, + 0x1b73: 84, + 0x1b80: 84, + 0x1b81: 84, + 0x1ba2: 84, + 0x1ba3: 84, + 0x1ba4: 84, + 0x1ba5: 84, + 0x1ba8: 84, + 0x1ba9: 84, + 0x1bab: 84, + 0x1bac: 84, + 0x1bad: 84, + 0x1be6: 84, + 0x1be8: 84, + 0x1be9: 84, + 0x1bed: 84, + 0x1bef: 84, + 0x1bf0: 84, + 0x1bf1: 84, + 0x1c2c: 84, + 0x1c2d: 84, + 0x1c2e: 84, + 0x1c2f: 84, + 0x1c30: 84, + 0x1c31: 84, + 0x1c32: 84, + 0x1c33: 84, + 0x1c36: 84, + 0x1c37: 84, + 0x1cd0: 84, + 0x1cd1: 84, + 0x1cd2: 84, + 0x1cd4: 84, + 0x1cd5: 84, + 0x1cd6: 84, + 0x1cd7: 84, + 0x1cd8: 84, + 0x1cd9: 84, + 0x1cda: 84, + 0x1cdb: 84, + 0x1cdc: 84, + 0x1cdd: 84, + 0x1cde: 84, + 0x1cdf: 84, + 0x1ce0: 84, + 0x1ce2: 84, + 0x1ce3: 84, + 0x1ce4: 84, + 0x1ce5: 84, + 0x1ce6: 84, + 0x1ce7: 84, + 0x1ce8: 84, + 0x1ced: 84, + 0x1cf4: 84, + 0x1cf8: 84, + 0x1cf9: 84, + 0x1dc0: 84, + 0x1dc1: 84, + 0x1dc2: 84, + 0x1dc3: 84, + 0x1dc4: 84, + 0x1dc5: 84, + 0x1dc6: 84, + 0x1dc7: 84, + 0x1dc8: 84, + 0x1dc9: 84, + 0x1dca: 84, + 0x1dcb: 84, + 0x1dcc: 84, + 0x1dcd: 84, + 0x1dce: 84, + 0x1dcf: 84, + 0x1dd0: 84, + 0x1dd1: 84, + 0x1dd2: 84, + 0x1dd3: 84, + 0x1dd4: 84, + 0x1dd5: 84, + 0x1dd6: 84, + 0x1dd7: 84, + 0x1dd8: 84, + 0x1dd9: 84, + 0x1dda: 84, + 0x1ddb: 84, + 0x1ddc: 84, + 0x1ddd: 84, + 0x1dde: 84, + 0x1ddf: 84, + 0x1de0: 84, + 0x1de1: 84, + 0x1de2: 84, + 0x1de3: 84, + 0x1de4: 84, + 0x1de5: 84, + 0x1de6: 84, + 0x1de7: 84, + 0x1de8: 84, + 0x1de9: 84, + 0x1dea: 84, + 0x1deb: 84, + 0x1dec: 84, + 0x1ded: 84, + 0x1dee: 84, + 0x1def: 84, + 0x1df0: 84, + 0x1df1: 84, + 0x1df2: 84, + 0x1df3: 84, + 0x1df4: 84, + 0x1df5: 84, + 0x1df6: 84, + 0x1df7: 84, + 0x1df8: 84, + 0x1df9: 84, + 0x1dfa: 84, + 0x1dfb: 84, + 0x1dfc: 84, + 0x1dfd: 84, + 0x1dfe: 84, + 0x1dff: 84, + 0x200b: 84, 0x200d: 67, - 0x202f: 85, - 0x2066: 85, - 0x2067: 85, - 0x2068: 85, - 0x2069: 85, + 0x200e: 84, + 0x200f: 84, + 0x202a: 84, + 0x202b: 84, + 0x202c: 84, + 0x202d: 84, + 0x202e: 84, + 0x2060: 84, + 0x2061: 84, + 0x2062: 84, + 0x2063: 84, + 0x2064: 84, + 0x206a: 84, + 0x206b: 84, + 0x206c: 84, + 0x206d: 84, + 0x206e: 84, + 0x206f: 84, + 0x20d0: 84, + 0x20d1: 84, + 0x20d2: 84, + 0x20d3: 84, + 0x20d4: 84, + 0x20d5: 84, + 0x20d6: 84, + 0x20d7: 84, + 0x20d8: 84, + 0x20d9: 84, + 0x20da: 84, + 0x20db: 84, + 0x20dc: 84, + 0x20dd: 84, + 0x20de: 84, + 0x20df: 84, + 0x20e0: 84, + 0x20e1: 84, + 0x20e2: 84, + 0x20e3: 84, + 0x20e4: 84, + 0x20e5: 84, + 0x20e6: 84, + 0x20e7: 84, + 0x20e8: 84, + 0x20e9: 84, + 0x20ea: 84, + 0x20eb: 84, + 0x20ec: 84, + 0x20ed: 84, + 0x20ee: 84, + 0x20ef: 84, + 0x20f0: 84, + 0x2cef: 84, + 0x2cf0: 84, + 0x2cf1: 84, + 0x2d7f: 84, + 0x2de0: 84, + 0x2de1: 84, + 0x2de2: 84, + 0x2de3: 84, + 0x2de4: 84, + 0x2de5: 84, + 0x2de6: 84, + 0x2de7: 84, + 0x2de8: 84, + 0x2de9: 84, + 0x2dea: 84, + 0x2deb: 84, + 0x2dec: 84, + 0x2ded: 84, + 0x2dee: 84, + 0x2def: 84, + 0x2df0: 84, + 0x2df1: 84, + 0x2df2: 84, + 0x2df3: 84, + 0x2df4: 84, + 0x2df5: 84, + 0x2df6: 84, + 0x2df7: 84, + 0x2df8: 84, + 0x2df9: 84, + 0x2dfa: 84, + 0x2dfb: 84, + 0x2dfc: 84, + 0x2dfd: 84, + 0x2dfe: 84, + 0x2dff: 84, + 0x302a: 84, + 0x302b: 84, + 0x302c: 84, + 0x302d: 84, + 0x3099: 84, + 0x309a: 84, + 0xa66f: 84, + 0xa670: 84, + 0xa671: 84, + 0xa672: 84, + 0xa674: 84, + 0xa675: 84, + 0xa676: 84, + 0xa677: 84, + 0xa678: 84, + 0xa679: 84, + 0xa67a: 84, + 0xa67b: 84, + 0xa67c: 84, + 0xa67d: 84, + 0xa69e: 84, + 0xa69f: 84, + 0xa6f0: 84, + 0xa6f1: 84, + 0xa802: 84, + 0xa806: 84, + 0xa80b: 84, + 0xa825: 84, + 0xa826: 84, + 0xa82c: 84, 0xa840: 68, 0xa841: 68, 0xa842: 68, @@ -681,20 +1635,151 @@ joining_types = { 0xa870: 68, 0xa871: 68, 0xa872: 76, - 0xa873: 85, + 0xa8c4: 84, + 0xa8c5: 84, + 0xa8e0: 84, + 0xa8e1: 84, + 0xa8e2: 84, + 0xa8e3: 84, + 0xa8e4: 84, + 0xa8e5: 84, + 0xa8e6: 84, + 0xa8e7: 84, + 0xa8e8: 84, + 0xa8e9: 84, + 0xa8ea: 84, + 0xa8eb: 84, + 0xa8ec: 84, + 0xa8ed: 84, + 0xa8ee: 84, + 0xa8ef: 84, + 0xa8f0: 84, + 0xa8f1: 84, + 0xa8ff: 84, + 0xa926: 84, + 0xa927: 84, + 0xa928: 84, + 0xa929: 84, + 0xa92a: 84, + 0xa92b: 84, + 0xa92c: 84, + 0xa92d: 84, + 0xa947: 84, + 0xa948: 84, + 0xa949: 84, + 0xa94a: 84, + 0xa94b: 84, + 0xa94c: 84, + 0xa94d: 84, + 0xa94e: 84, + 0xa94f: 84, + 0xa950: 84, + 0xa951: 84, + 0xa980: 84, + 0xa981: 84, + 0xa982: 84, + 0xa9b3: 84, + 0xa9b6: 84, + 0xa9b7: 84, + 0xa9b8: 84, + 0xa9b9: 84, + 0xa9bc: 84, + 0xa9bd: 84, + 0xa9e5: 84, + 0xaa29: 84, + 0xaa2a: 84, + 0xaa2b: 84, + 0xaa2c: 84, + 0xaa2d: 84, + 0xaa2e: 84, + 0xaa31: 84, + 0xaa32: 84, + 0xaa35: 84, + 0xaa36: 84, + 0xaa43: 84, + 0xaa4c: 84, + 0xaa7c: 84, + 0xaab0: 84, + 0xaab2: 84, + 0xaab3: 84, + 0xaab4: 84, + 0xaab7: 84, + 0xaab8: 84, + 0xaabe: 84, + 0xaabf: 84, + 0xaac1: 84, + 0xaaec: 84, + 0xaaed: 84, + 0xaaf6: 84, + 0xabe5: 84, + 0xabe8: 84, + 0xabed: 84, + 0xfb1e: 84, + 0xfe00: 84, + 0xfe01: 84, + 0xfe02: 84, + 0xfe03: 84, + 0xfe04: 84, + 0xfe05: 84, + 0xfe06: 84, + 0xfe07: 84, + 0xfe08: 84, + 0xfe09: 84, + 0xfe0a: 84, + 0xfe0b: 84, + 0xfe0c: 84, + 0xfe0d: 84, + 0xfe0e: 84, + 0xfe0f: 84, + 0xfe20: 84, + 0xfe21: 84, + 0xfe22: 84, + 0xfe23: 84, + 0xfe24: 84, + 0xfe25: 84, + 0xfe26: 84, + 0xfe27: 84, + 0xfe28: 84, + 0xfe29: 84, + 0xfe2a: 84, + 0xfe2b: 84, + 0xfe2c: 84, + 0xfe2d: 84, + 0xfe2e: 84, + 0xfe2f: 84, + 0xfeff: 84, + 0xfff9: 84, + 0xfffa: 84, + 0xfffb: 84, + 0x101fd: 84, + 0x102e0: 84, + 0x10376: 84, + 0x10377: 84, + 0x10378: 84, + 0x10379: 84, + 0x1037a: 84, + 0x10a01: 84, + 0x10a02: 84, + 0x10a03: 84, + 0x10a05: 84, + 0x10a06: 84, + 0x10a0c: 84, + 0x10a0d: 84, + 0x10a0e: 84, + 0x10a0f: 84, + 0x10a38: 84, + 0x10a39: 84, + 0x10a3a: 84, + 0x10a3f: 84, 0x10ac0: 68, 0x10ac1: 68, 0x10ac2: 68, 0x10ac3: 68, 0x10ac4: 68, 0x10ac5: 82, - 0x10ac6: 85, 0x10ac7: 82, - 0x10ac8: 85, 0x10ac9: 82, 0x10aca: 82, - 0x10acb: 85, - 0x10acc: 85, 0x10acd: 76, 0x10ace: 82, 0x10acf: 82, @@ -716,9 +1801,9 @@ joining_types = { 0x10adf: 68, 0x10ae0: 68, 0x10ae1: 82, - 0x10ae2: 85, - 0x10ae3: 85, 0x10ae4: 82, + 0x10ae5: 84, + 0x10ae6: 84, 0x10aeb: 68, 0x10aec: 68, 0x10aed: 68, @@ -748,7 +1833,6 @@ joining_types = { 0x10bac: 82, 0x10bad: 68, 0x10bae: 68, - 0x10baf: 85, 0x10d00: 76, 0x10d01: 68, 0x10d02: 68, @@ -785,6 +1869,15 @@ joining_types = { 0x10d21: 68, 0x10d22: 82, 0x10d23: 68, + 0x10d24: 84, + 0x10d25: 84, + 0x10d26: 84, + 0x10d27: 84, + 0x10eab: 84, + 0x10eac: 84, + 0x10efd: 84, + 0x10efe: 84, + 0x10eff: 84, 0x10f30: 68, 0x10f31: 68, 0x10f32: 68, @@ -806,7 +1899,17 @@ joining_types = { 0x10f42: 68, 0x10f43: 68, 0x10f44: 68, - 0x10f45: 85, + 0x10f46: 84, + 0x10f47: 84, + 0x10f48: 84, + 0x10f49: 84, + 0x10f4a: 84, + 0x10f4b: 84, + 0x10f4c: 84, + 0x10f4d: 84, + 0x10f4e: 84, + 0x10f4f: 84, + 0x10f50: 84, 0x10f51: 68, 0x10f52: 68, 0x10f53: 68, @@ -829,14 +1932,16 @@ joining_types = { 0x10f7f: 68, 0x10f80: 68, 0x10f81: 68, + 0x10f82: 84, + 0x10f83: 84, + 0x10f84: 84, + 0x10f85: 84, 0x10fb0: 68, - 0x10fb1: 85, 0x10fb2: 68, 0x10fb3: 68, 0x10fb4: 82, 0x10fb5: 82, 0x10fb6: 82, - 0x10fb7: 85, 0x10fb8: 68, 0x10fb9: 82, 0x10fba: 82, @@ -845,20 +1950,668 @@ joining_types = { 0x10fbd: 82, 0x10fbe: 68, 0x10fbf: 68, - 0x10fc0: 85, 0x10fc1: 68, 0x10fc2: 82, 0x10fc3: 82, 0x10fc4: 68, - 0x10fc5: 85, - 0x10fc6: 85, - 0x10fc7: 85, - 0x10fc8: 85, 0x10fc9: 82, 0x10fca: 68, 0x10fcb: 76, - 0x110bd: 85, - 0x110cd: 85, + 0x11001: 84, + 0x11038: 84, + 0x11039: 84, + 0x1103a: 84, + 0x1103b: 84, + 0x1103c: 84, + 0x1103d: 84, + 0x1103e: 84, + 0x1103f: 84, + 0x11040: 84, + 0x11041: 84, + 0x11042: 84, + 0x11043: 84, + 0x11044: 84, + 0x11045: 84, + 0x11046: 84, + 0x11070: 84, + 0x11073: 84, + 0x11074: 84, + 0x1107f: 84, + 0x11080: 84, + 0x11081: 84, + 0x110b3: 84, + 0x110b4: 84, + 0x110b5: 84, + 0x110b6: 84, + 0x110b9: 84, + 0x110ba: 84, + 0x110c2: 84, + 0x11100: 84, + 0x11101: 84, + 0x11102: 84, + 0x11127: 84, + 0x11128: 84, + 0x11129: 84, + 0x1112a: 84, + 0x1112b: 84, + 0x1112d: 84, + 0x1112e: 84, + 0x1112f: 84, + 0x11130: 84, + 0x11131: 84, + 0x11132: 84, + 0x11133: 84, + 0x11134: 84, + 0x11173: 84, + 0x11180: 84, + 0x11181: 84, + 0x111b6: 84, + 0x111b7: 84, + 0x111b8: 84, + 0x111b9: 84, + 0x111ba: 84, + 0x111bb: 84, + 0x111bc: 84, + 0x111bd: 84, + 0x111be: 84, + 0x111c9: 84, + 0x111ca: 84, + 0x111cb: 84, + 0x111cc: 84, + 0x111cf: 84, + 0x1122f: 84, + 0x11230: 84, + 0x11231: 84, + 0x11234: 84, + 0x11236: 84, + 0x11237: 84, + 0x1123e: 84, + 0x11241: 84, + 0x112df: 84, + 0x112e3: 84, + 0x112e4: 84, + 0x112e5: 84, + 0x112e6: 84, + 0x112e7: 84, + 0x112e8: 84, + 0x112e9: 84, + 0x112ea: 84, + 0x11300: 84, + 0x11301: 84, + 0x1133b: 84, + 0x1133c: 84, + 0x11340: 84, + 0x11366: 84, + 0x11367: 84, + 0x11368: 84, + 0x11369: 84, + 0x1136a: 84, + 0x1136b: 84, + 0x1136c: 84, + 0x11370: 84, + 0x11371: 84, + 0x11372: 84, + 0x11373: 84, + 0x11374: 84, + 0x11438: 84, + 0x11439: 84, + 0x1143a: 84, + 0x1143b: 84, + 0x1143c: 84, + 0x1143d: 84, + 0x1143e: 84, + 0x1143f: 84, + 0x11442: 84, + 0x11443: 84, + 0x11444: 84, + 0x11446: 84, + 0x1145e: 84, + 0x114b3: 84, + 0x114b4: 84, + 0x114b5: 84, + 0x114b6: 84, + 0x114b7: 84, + 0x114b8: 84, + 0x114ba: 84, + 0x114bf: 84, + 0x114c0: 84, + 0x114c2: 84, + 0x114c3: 84, + 0x115b2: 84, + 0x115b3: 84, + 0x115b4: 84, + 0x115b5: 84, + 0x115bc: 84, + 0x115bd: 84, + 0x115bf: 84, + 0x115c0: 84, + 0x115dc: 84, + 0x115dd: 84, + 0x11633: 84, + 0x11634: 84, + 0x11635: 84, + 0x11636: 84, + 0x11637: 84, + 0x11638: 84, + 0x11639: 84, + 0x1163a: 84, + 0x1163d: 84, + 0x1163f: 84, + 0x11640: 84, + 0x116ab: 84, + 0x116ad: 84, + 0x116b0: 84, + 0x116b1: 84, + 0x116b2: 84, + 0x116b3: 84, + 0x116b4: 84, + 0x116b5: 84, + 0x116b7: 84, + 0x1171d: 84, + 0x1171e: 84, + 0x1171f: 84, + 0x11722: 84, + 0x11723: 84, + 0x11724: 84, + 0x11725: 84, + 0x11727: 84, + 0x11728: 84, + 0x11729: 84, + 0x1172a: 84, + 0x1172b: 84, + 0x1182f: 84, + 0x11830: 84, + 0x11831: 84, + 0x11832: 84, + 0x11833: 84, + 0x11834: 84, + 0x11835: 84, + 0x11836: 84, + 0x11837: 84, + 0x11839: 84, + 0x1183a: 84, + 0x1193b: 84, + 0x1193c: 84, + 0x1193e: 84, + 0x11943: 84, + 0x119d4: 84, + 0x119d5: 84, + 0x119d6: 84, + 0x119d7: 84, + 0x119da: 84, + 0x119db: 84, + 0x119e0: 84, + 0x11a01: 84, + 0x11a02: 84, + 0x11a03: 84, + 0x11a04: 84, + 0x11a05: 84, + 0x11a06: 84, + 0x11a07: 84, + 0x11a08: 84, + 0x11a09: 84, + 0x11a0a: 84, + 0x11a33: 84, + 0x11a34: 84, + 0x11a35: 84, + 0x11a36: 84, + 0x11a37: 84, + 0x11a38: 84, + 0x11a3b: 84, + 0x11a3c: 84, + 0x11a3d: 84, + 0x11a3e: 84, + 0x11a47: 84, + 0x11a51: 84, + 0x11a52: 84, + 0x11a53: 84, + 0x11a54: 84, + 0x11a55: 84, + 0x11a56: 84, + 0x11a59: 84, + 0x11a5a: 84, + 0x11a5b: 84, + 0x11a8a: 84, + 0x11a8b: 84, + 0x11a8c: 84, + 0x11a8d: 84, + 0x11a8e: 84, + 0x11a8f: 84, + 0x11a90: 84, + 0x11a91: 84, + 0x11a92: 84, + 0x11a93: 84, + 0x11a94: 84, + 0x11a95: 84, + 0x11a96: 84, + 0x11a98: 84, + 0x11a99: 84, + 0x11c30: 84, + 0x11c31: 84, + 0x11c32: 84, + 0x11c33: 84, + 0x11c34: 84, + 0x11c35: 84, + 0x11c36: 84, + 0x11c38: 84, + 0x11c39: 84, + 0x11c3a: 84, + 0x11c3b: 84, + 0x11c3c: 84, + 0x11c3d: 84, + 0x11c3f: 84, + 0x11c92: 84, + 0x11c93: 84, + 0x11c94: 84, + 0x11c95: 84, + 0x11c96: 84, + 0x11c97: 84, + 0x11c98: 84, + 0x11c99: 84, + 0x11c9a: 84, + 0x11c9b: 84, + 0x11c9c: 84, + 0x11c9d: 84, + 0x11c9e: 84, + 0x11c9f: 84, + 0x11ca0: 84, + 0x11ca1: 84, + 0x11ca2: 84, + 0x11ca3: 84, + 0x11ca4: 84, + 0x11ca5: 84, + 0x11ca6: 84, + 0x11ca7: 84, + 0x11caa: 84, + 0x11cab: 84, + 0x11cac: 84, + 0x11cad: 84, + 0x11cae: 84, + 0x11caf: 84, + 0x11cb0: 84, + 0x11cb2: 84, + 0x11cb3: 84, + 0x11cb5: 84, + 0x11cb6: 84, + 0x11d31: 84, + 0x11d32: 84, + 0x11d33: 84, + 0x11d34: 84, + 0x11d35: 84, + 0x11d36: 84, + 0x11d3a: 84, + 0x11d3c: 84, + 0x11d3d: 84, + 0x11d3f: 84, + 0x11d40: 84, + 0x11d41: 84, + 0x11d42: 84, + 0x11d43: 84, + 0x11d44: 84, + 0x11d45: 84, + 0x11d47: 84, + 0x11d90: 84, + 0x11d91: 84, + 0x11d95: 84, + 0x11d97: 84, + 0x11ef3: 84, + 0x11ef4: 84, + 0x11f00: 84, + 0x11f01: 84, + 0x11f36: 84, + 0x11f37: 84, + 0x11f38: 84, + 0x11f39: 84, + 0x11f3a: 84, + 0x11f40: 84, + 0x11f42: 84, + 0x13430: 84, + 0x13431: 84, + 0x13432: 84, + 0x13433: 84, + 0x13434: 84, + 0x13435: 84, + 0x13436: 84, + 0x13437: 84, + 0x13438: 84, + 0x13439: 84, + 0x1343a: 84, + 0x1343b: 84, + 0x1343c: 84, + 0x1343d: 84, + 0x1343e: 84, + 0x1343f: 84, + 0x13440: 84, + 0x13447: 84, + 0x13448: 84, + 0x13449: 84, + 0x1344a: 84, + 0x1344b: 84, + 0x1344c: 84, + 0x1344d: 84, + 0x1344e: 84, + 0x1344f: 84, + 0x13450: 84, + 0x13451: 84, + 0x13452: 84, + 0x13453: 84, + 0x13454: 84, + 0x13455: 84, + 0x16af0: 84, + 0x16af1: 84, + 0x16af2: 84, + 0x16af3: 84, + 0x16af4: 84, + 0x16b30: 84, + 0x16b31: 84, + 0x16b32: 84, + 0x16b33: 84, + 0x16b34: 84, + 0x16b35: 84, + 0x16b36: 84, + 0x16f4f: 84, + 0x16f8f: 84, + 0x16f90: 84, + 0x16f91: 84, + 0x16f92: 84, + 0x16fe4: 84, + 0x1bc9d: 84, + 0x1bc9e: 84, + 0x1bca0: 84, + 0x1bca1: 84, + 0x1bca2: 84, + 0x1bca3: 84, + 0x1cf00: 84, + 0x1cf01: 84, + 0x1cf02: 84, + 0x1cf03: 84, + 0x1cf04: 84, + 0x1cf05: 84, + 0x1cf06: 84, + 0x1cf07: 84, + 0x1cf08: 84, + 0x1cf09: 84, + 0x1cf0a: 84, + 0x1cf0b: 84, + 0x1cf0c: 84, + 0x1cf0d: 84, + 0x1cf0e: 84, + 0x1cf0f: 84, + 0x1cf10: 84, + 0x1cf11: 84, + 0x1cf12: 84, + 0x1cf13: 84, + 0x1cf14: 84, + 0x1cf15: 84, + 0x1cf16: 84, + 0x1cf17: 84, + 0x1cf18: 84, + 0x1cf19: 84, + 0x1cf1a: 84, + 0x1cf1b: 84, + 0x1cf1c: 84, + 0x1cf1d: 84, + 0x1cf1e: 84, + 0x1cf1f: 84, + 0x1cf20: 84, + 0x1cf21: 84, + 0x1cf22: 84, + 0x1cf23: 84, + 0x1cf24: 84, + 0x1cf25: 84, + 0x1cf26: 84, + 0x1cf27: 84, + 0x1cf28: 84, + 0x1cf29: 84, + 0x1cf2a: 84, + 0x1cf2b: 84, + 0x1cf2c: 84, + 0x1cf2d: 84, + 0x1cf30: 84, + 0x1cf31: 84, + 0x1cf32: 84, + 0x1cf33: 84, + 0x1cf34: 84, + 0x1cf35: 84, + 0x1cf36: 84, + 0x1cf37: 84, + 0x1cf38: 84, + 0x1cf39: 84, + 0x1cf3a: 84, + 0x1cf3b: 84, + 0x1cf3c: 84, + 0x1cf3d: 84, + 0x1cf3e: 84, + 0x1cf3f: 84, + 0x1cf40: 84, + 0x1cf41: 84, + 0x1cf42: 84, + 0x1cf43: 84, + 0x1cf44: 84, + 0x1cf45: 84, + 0x1cf46: 84, + 0x1d167: 84, + 0x1d168: 84, + 0x1d169: 84, + 0x1d173: 84, + 0x1d174: 84, + 0x1d175: 84, + 0x1d176: 84, + 0x1d177: 84, + 0x1d178: 84, + 0x1d179: 84, + 0x1d17a: 84, + 0x1d17b: 84, + 0x1d17c: 84, + 0x1d17d: 84, + 0x1d17e: 84, + 0x1d17f: 84, + 0x1d180: 84, + 0x1d181: 84, + 0x1d182: 84, + 0x1d185: 84, + 0x1d186: 84, + 0x1d187: 84, + 0x1d188: 84, + 0x1d189: 84, + 0x1d18a: 84, + 0x1d18b: 84, + 0x1d1aa: 84, + 0x1d1ab: 84, + 0x1d1ac: 84, + 0x1d1ad: 84, + 0x1d242: 84, + 0x1d243: 84, + 0x1d244: 84, + 0x1da00: 84, + 0x1da01: 84, + 0x1da02: 84, + 0x1da03: 84, + 0x1da04: 84, + 0x1da05: 84, + 0x1da06: 84, + 0x1da07: 84, + 0x1da08: 84, + 0x1da09: 84, + 0x1da0a: 84, + 0x1da0b: 84, + 0x1da0c: 84, + 0x1da0d: 84, + 0x1da0e: 84, + 0x1da0f: 84, + 0x1da10: 84, + 0x1da11: 84, + 0x1da12: 84, + 0x1da13: 84, + 0x1da14: 84, + 0x1da15: 84, + 0x1da16: 84, + 0x1da17: 84, + 0x1da18: 84, + 0x1da19: 84, + 0x1da1a: 84, + 0x1da1b: 84, + 0x1da1c: 84, + 0x1da1d: 84, + 0x1da1e: 84, + 0x1da1f: 84, + 0x1da20: 84, + 0x1da21: 84, + 0x1da22: 84, + 0x1da23: 84, + 0x1da24: 84, + 0x1da25: 84, + 0x1da26: 84, + 0x1da27: 84, + 0x1da28: 84, + 0x1da29: 84, + 0x1da2a: 84, + 0x1da2b: 84, + 0x1da2c: 84, + 0x1da2d: 84, + 0x1da2e: 84, + 0x1da2f: 84, + 0x1da30: 84, + 0x1da31: 84, + 0x1da32: 84, + 0x1da33: 84, + 0x1da34: 84, + 0x1da35: 84, + 0x1da36: 84, + 0x1da3b: 84, + 0x1da3c: 84, + 0x1da3d: 84, + 0x1da3e: 84, + 0x1da3f: 84, + 0x1da40: 84, + 0x1da41: 84, + 0x1da42: 84, + 0x1da43: 84, + 0x1da44: 84, + 0x1da45: 84, + 0x1da46: 84, + 0x1da47: 84, + 0x1da48: 84, + 0x1da49: 84, + 0x1da4a: 84, + 0x1da4b: 84, + 0x1da4c: 84, + 0x1da4d: 84, + 0x1da4e: 84, + 0x1da4f: 84, + 0x1da50: 84, + 0x1da51: 84, + 0x1da52: 84, + 0x1da53: 84, + 0x1da54: 84, + 0x1da55: 84, + 0x1da56: 84, + 0x1da57: 84, + 0x1da58: 84, + 0x1da59: 84, + 0x1da5a: 84, + 0x1da5b: 84, + 0x1da5c: 84, + 0x1da5d: 84, + 0x1da5e: 84, + 0x1da5f: 84, + 0x1da60: 84, + 0x1da61: 84, + 0x1da62: 84, + 0x1da63: 84, + 0x1da64: 84, + 0x1da65: 84, + 0x1da66: 84, + 0x1da67: 84, + 0x1da68: 84, + 0x1da69: 84, + 0x1da6a: 84, + 0x1da6b: 84, + 0x1da6c: 84, + 0x1da75: 84, + 0x1da84: 84, + 0x1da9b: 84, + 0x1da9c: 84, + 0x1da9d: 84, + 0x1da9e: 84, + 0x1da9f: 84, + 0x1daa1: 84, + 0x1daa2: 84, + 0x1daa3: 84, + 0x1daa4: 84, + 0x1daa5: 84, + 0x1daa6: 84, + 0x1daa7: 84, + 0x1daa8: 84, + 0x1daa9: 84, + 0x1daaa: 84, + 0x1daab: 84, + 0x1daac: 84, + 0x1daad: 84, + 0x1daae: 84, + 0x1daaf: 84, + 0x1e000: 84, + 0x1e001: 84, + 0x1e002: 84, + 0x1e003: 84, + 0x1e004: 84, + 0x1e005: 84, + 0x1e006: 84, + 0x1e008: 84, + 0x1e009: 84, + 0x1e00a: 84, + 0x1e00b: 84, + 0x1e00c: 84, + 0x1e00d: 84, + 0x1e00e: 84, + 0x1e00f: 84, + 0x1e010: 84, + 0x1e011: 84, + 0x1e012: 84, + 0x1e013: 84, + 0x1e014: 84, + 0x1e015: 84, + 0x1e016: 84, + 0x1e017: 84, + 0x1e018: 84, + 0x1e01b: 84, + 0x1e01c: 84, + 0x1e01d: 84, + 0x1e01e: 84, + 0x1e01f: 84, + 0x1e020: 84, + 0x1e021: 84, + 0x1e023: 84, + 0x1e024: 84, + 0x1e026: 84, + 0x1e027: 84, + 0x1e028: 84, + 0x1e029: 84, + 0x1e02a: 84, + 0x1e08f: 84, + 0x1e130: 84, + 0x1e131: 84, + 0x1e132: 84, + 0x1e133: 84, + 0x1e134: 84, + 0x1e135: 84, + 0x1e136: 84, + 0x1e2ae: 84, + 0x1e2ec: 84, + 0x1e2ed: 84, + 0x1e2ee: 84, + 0x1e2ef: 84, + 0x1e4ec: 84, + 0x1e4ed: 84, + 0x1e4ee: 84, + 0x1e4ef: 84, + 0x1e8d0: 84, + 0x1e8d1: 84, + 0x1e8d2: 84, + 0x1e8d3: 84, + 0x1e8d4: 84, + 0x1e8d5: 84, + 0x1e8d6: 84, 0x1e900: 68, 0x1e901: 68, 0x1e902: 68, @@ -927,7 +2680,351 @@ joining_types = { 0x1e941: 68, 0x1e942: 68, 0x1e943: 68, + 0x1e944: 84, + 0x1e945: 84, + 0x1e946: 84, + 0x1e947: 84, + 0x1e948: 84, + 0x1e949: 84, + 0x1e94a: 84, 0x1e94b: 84, + 0xe0001: 84, + 0xe0020: 84, + 0xe0021: 84, + 0xe0022: 84, + 0xe0023: 84, + 0xe0024: 84, + 0xe0025: 84, + 0xe0026: 84, + 0xe0027: 84, + 0xe0028: 84, + 0xe0029: 84, + 0xe002a: 84, + 0xe002b: 84, + 0xe002c: 84, + 0xe002d: 84, + 0xe002e: 84, + 0xe002f: 84, + 0xe0030: 84, + 0xe0031: 84, + 0xe0032: 84, + 0xe0033: 84, + 0xe0034: 84, + 0xe0035: 84, + 0xe0036: 84, + 0xe0037: 84, + 0xe0038: 84, + 0xe0039: 84, + 0xe003a: 84, + 0xe003b: 84, + 0xe003c: 84, + 0xe003d: 84, + 0xe003e: 84, + 0xe003f: 84, + 0xe0040: 84, + 0xe0041: 84, + 0xe0042: 84, + 0xe0043: 84, + 0xe0044: 84, + 0xe0045: 84, + 0xe0046: 84, + 0xe0047: 84, + 0xe0048: 84, + 0xe0049: 84, + 0xe004a: 84, + 0xe004b: 84, + 0xe004c: 84, + 0xe004d: 84, + 0xe004e: 84, + 0xe004f: 84, + 0xe0050: 84, + 0xe0051: 84, + 0xe0052: 84, + 0xe0053: 84, + 0xe0054: 84, + 0xe0055: 84, + 0xe0056: 84, + 0xe0057: 84, + 0xe0058: 84, + 0xe0059: 84, + 0xe005a: 84, + 0xe005b: 84, + 0xe005c: 84, + 0xe005d: 84, + 0xe005e: 84, + 0xe005f: 84, + 0xe0060: 84, + 0xe0061: 84, + 0xe0062: 84, + 0xe0063: 84, + 0xe0064: 84, + 0xe0065: 84, + 0xe0066: 84, + 0xe0067: 84, + 0xe0068: 84, + 0xe0069: 84, + 0xe006a: 84, + 0xe006b: 84, + 0xe006c: 84, + 0xe006d: 84, + 0xe006e: 84, + 0xe006f: 84, + 0xe0070: 84, + 0xe0071: 84, + 0xe0072: 84, + 0xe0073: 84, + 0xe0074: 84, + 0xe0075: 84, + 0xe0076: 84, + 0xe0077: 84, + 0xe0078: 84, + 0xe0079: 84, + 0xe007a: 84, + 0xe007b: 84, + 0xe007c: 84, + 0xe007d: 84, + 0xe007e: 84, + 0xe007f: 84, + 0xe0100: 84, + 0xe0101: 84, + 0xe0102: 84, + 0xe0103: 84, + 0xe0104: 84, + 0xe0105: 84, + 0xe0106: 84, + 0xe0107: 84, + 0xe0108: 84, + 0xe0109: 84, + 0xe010a: 84, + 0xe010b: 84, + 0xe010c: 84, + 0xe010d: 84, + 0xe010e: 84, + 0xe010f: 84, + 0xe0110: 84, + 0xe0111: 84, + 0xe0112: 84, + 0xe0113: 84, + 0xe0114: 84, + 0xe0115: 84, + 0xe0116: 84, + 0xe0117: 84, + 0xe0118: 84, + 0xe0119: 84, + 0xe011a: 84, + 0xe011b: 84, + 0xe011c: 84, + 0xe011d: 84, + 0xe011e: 84, + 0xe011f: 84, + 0xe0120: 84, + 0xe0121: 84, + 0xe0122: 84, + 0xe0123: 84, + 0xe0124: 84, + 0xe0125: 84, + 0xe0126: 84, + 0xe0127: 84, + 0xe0128: 84, + 0xe0129: 84, + 0xe012a: 84, + 0xe012b: 84, + 0xe012c: 84, + 0xe012d: 84, + 0xe012e: 84, + 0xe012f: 84, + 0xe0130: 84, + 0xe0131: 84, + 0xe0132: 84, + 0xe0133: 84, + 0xe0134: 84, + 0xe0135: 84, + 0xe0136: 84, + 0xe0137: 84, + 0xe0138: 84, + 0xe0139: 84, + 0xe013a: 84, + 0xe013b: 84, + 0xe013c: 84, + 0xe013d: 84, + 0xe013e: 84, + 0xe013f: 84, + 0xe0140: 84, + 0xe0141: 84, + 0xe0142: 84, + 0xe0143: 84, + 0xe0144: 84, + 0xe0145: 84, + 0xe0146: 84, + 0xe0147: 84, + 0xe0148: 84, + 0xe0149: 84, + 0xe014a: 84, + 0xe014b: 84, + 0xe014c: 84, + 0xe014d: 84, + 0xe014e: 84, + 0xe014f: 84, + 0xe0150: 84, + 0xe0151: 84, + 0xe0152: 84, + 0xe0153: 84, + 0xe0154: 84, + 0xe0155: 84, + 0xe0156: 84, + 0xe0157: 84, + 0xe0158: 84, + 0xe0159: 84, + 0xe015a: 84, + 0xe015b: 84, + 0xe015c: 84, + 0xe015d: 84, + 0xe015e: 84, + 0xe015f: 84, + 0xe0160: 84, + 0xe0161: 84, + 0xe0162: 84, + 0xe0163: 84, + 0xe0164: 84, + 0xe0165: 84, + 0xe0166: 84, + 0xe0167: 84, + 0xe0168: 84, + 0xe0169: 84, + 0xe016a: 84, + 0xe016b: 84, + 0xe016c: 84, + 0xe016d: 84, + 0xe016e: 84, + 0xe016f: 84, + 0xe0170: 84, + 0xe0171: 84, + 0xe0172: 84, + 0xe0173: 84, + 0xe0174: 84, + 0xe0175: 84, + 0xe0176: 84, + 0xe0177: 84, + 0xe0178: 84, + 0xe0179: 84, + 0xe017a: 84, + 0xe017b: 84, + 0xe017c: 84, + 0xe017d: 84, + 0xe017e: 84, + 0xe017f: 84, + 0xe0180: 84, + 0xe0181: 84, + 0xe0182: 84, + 0xe0183: 84, + 0xe0184: 84, + 0xe0185: 84, + 0xe0186: 84, + 0xe0187: 84, + 0xe0188: 84, + 0xe0189: 84, + 0xe018a: 84, + 0xe018b: 84, + 0xe018c: 84, + 0xe018d: 84, + 0xe018e: 84, + 0xe018f: 84, + 0xe0190: 84, + 0xe0191: 84, + 0xe0192: 84, + 0xe0193: 84, + 0xe0194: 84, + 0xe0195: 84, + 0xe0196: 84, + 0xe0197: 84, + 0xe0198: 84, + 0xe0199: 84, + 0xe019a: 84, + 0xe019b: 84, + 0xe019c: 84, + 0xe019d: 84, + 0xe019e: 84, + 0xe019f: 84, + 0xe01a0: 84, + 0xe01a1: 84, + 0xe01a2: 84, + 0xe01a3: 84, + 0xe01a4: 84, + 0xe01a5: 84, + 0xe01a6: 84, + 0xe01a7: 84, + 0xe01a8: 84, + 0xe01a9: 84, + 0xe01aa: 84, + 0xe01ab: 84, + 0xe01ac: 84, + 0xe01ad: 84, + 0xe01ae: 84, + 0xe01af: 84, + 0xe01b0: 84, + 0xe01b1: 84, + 0xe01b2: 84, + 0xe01b3: 84, + 0xe01b4: 84, + 0xe01b5: 84, + 0xe01b6: 84, + 0xe01b7: 84, + 0xe01b8: 84, + 0xe01b9: 84, + 0xe01ba: 84, + 0xe01bb: 84, + 0xe01bc: 84, + 0xe01bd: 84, + 0xe01be: 84, + 0xe01bf: 84, + 0xe01c0: 84, + 0xe01c1: 84, + 0xe01c2: 84, + 0xe01c3: 84, + 0xe01c4: 84, + 0xe01c5: 84, + 0xe01c6: 84, + 0xe01c7: 84, + 0xe01c8: 84, + 0xe01c9: 84, + 0xe01ca: 84, + 0xe01cb: 84, + 0xe01cc: 84, + 0xe01cd: 84, + 0xe01ce: 84, + 0xe01cf: 84, + 0xe01d0: 84, + 0xe01d1: 84, + 0xe01d2: 84, + 0xe01d3: 84, + 0xe01d4: 84, + 0xe01d5: 84, + 0xe01d6: 84, + 0xe01d7: 84, + 0xe01d8: 84, + 0xe01d9: 84, + 0xe01da: 84, + 0xe01db: 84, + 0xe01dc: 84, + 0xe01dd: 84, + 0xe01de: 84, + 0xe01df: 84, + 0xe01e0: 84, + 0xe01e1: 84, + 0xe01e2: 84, + 0xe01e3: 84, + 0xe01e4: 84, + 0xe01e5: 84, + 0xe01e6: 84, + 0xe01e7: 84, + 0xe01e8: 84, + 0xe01e9: 84, + 0xe01ea: 84, + 0xe01eb: 84, + 0xe01ec: 84, + 0xe01ed: 84, + 0xe01ee: 84, + 0xe01ef: 84, } codepoint_classes = { 'PVALID': ( @@ -2109,7 +4206,6 @@ codepoint_classes = { 0x1e01b0001e022, 0x1e0230001e025, 0x1e0260001e02b, - 0x1e0300001e06e, 0x1e08f0001e090, 0x1e1000001e12d, 0x1e1300001e13e, @@ -2131,6 +4227,7 @@ codepoint_classes = { 0x2b7400002b81e, 0x2b8200002cea2, 0x2ceb00002ebe1, + 0x2ebf00002ee5e, 0x300000003134b, 0x31350000323b0, ), diff --git a/lib/idna/package_data.py b/lib/idna/package_data.py index 8501893b..ed811133 100644 --- a/lib/idna/package_data.py +++ b/lib/idna/package_data.py @@ -1,2 +1,2 @@ -__version__ = '3.4' +__version__ = '3.7' diff --git a/lib/idna/py.typed b/lib/idna/py.typed new file mode 100644 index 00000000..e69de29b diff --git a/lib/idna/uts46data.py b/lib/idna/uts46data.py index 186796c1..6a1eddbf 100644 --- a/lib/idna/uts46data.py +++ b/lib/idna/uts46data.py @@ -7,7 +7,7 @@ from typing import List, Tuple, Union """IDNA Mapping Table from UTS46.""" -__version__ = '15.0.0' +__version__ = '15.1.0' def _seg_0() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: return [ (0x0, '3'), @@ -1899,7 +1899,7 @@ def _seg_18() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: (0x1E9A, 'M', 'aʾ'), (0x1E9B, 'M', 'ṡ'), (0x1E9C, 'V'), - (0x1E9E, 'M', 'ss'), + (0x1E9E, 'M', 'ß'), (0x1E9F, 'V'), (0x1EA0, 'M', 'ạ'), (0x1EA1, 'V'), @@ -2418,10 +2418,6 @@ def _seg_23() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: (0x222F, 'M', '∮∮'), (0x2230, 'M', '∮∮∮'), (0x2231, 'V'), - (0x2260, '3'), - (0x2261, 'V'), - (0x226E, '3'), - (0x2270, 'V'), (0x2329, 'M', '〈'), (0x232A, 'M', '〉'), (0x232B, 'V'), @@ -2502,14 +2498,14 @@ def _seg_23() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: (0x24BA, 'M', 'e'), (0x24BB, 'M', 'f'), (0x24BC, 'M', 'g'), - ] - -def _seg_24() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: - return [ (0x24BD, 'M', 'h'), (0x24BE, 'M', 'i'), (0x24BF, 'M', 'j'), (0x24C0, 'M', 'k'), + ] + +def _seg_24() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ (0x24C1, 'M', 'l'), (0x24C2, 'M', 'm'), (0x24C3, 'M', 'n'), @@ -2606,14 +2602,14 @@ def _seg_24() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: (0x2C26, 'M', 'ⱖ'), (0x2C27, 'M', 'ⱗ'), (0x2C28, 'M', 'ⱘ'), - ] - -def _seg_25() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: - return [ (0x2C29, 'M', 'ⱙ'), (0x2C2A, 'M', 'ⱚ'), (0x2C2B, 'M', 'ⱛ'), (0x2C2C, 'M', 'ⱜ'), + ] + +def _seg_25() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ (0x2C2D, 'M', 'ⱝ'), (0x2C2E, 'M', 'ⱞ'), (0x2C2F, 'M', 'ⱟ'), @@ -2710,14 +2706,14 @@ def _seg_25() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: (0x2CC0, 'M', 'ⳁ'), (0x2CC1, 'V'), (0x2CC2, 'M', 'ⳃ'), - ] - -def _seg_26() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: - return [ (0x2CC3, 'V'), (0x2CC4, 'M', 'ⳅ'), (0x2CC5, 'V'), (0x2CC6, 'M', 'ⳇ'), + ] + +def _seg_26() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ (0x2CC7, 'V'), (0x2CC8, 'M', 'ⳉ'), (0x2CC9, 'V'), @@ -2814,14 +2810,14 @@ def _seg_26() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: (0x2F13, 'M', '勹'), (0x2F14, 'M', '匕'), (0x2F15, 'M', '匚'), - ] - -def _seg_27() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: - return [ (0x2F16, 'M', '匸'), (0x2F17, 'M', '十'), (0x2F18, 'M', '卜'), (0x2F19, 'M', '卩'), + ] + +def _seg_27() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ (0x2F1A, 'M', '厂'), (0x2F1B, 'M', '厶'), (0x2F1C, 'M', '又'), @@ -2918,14 +2914,14 @@ def _seg_27() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: (0x2F77, 'M', '糸'), (0x2F78, 'M', '缶'), (0x2F79, 'M', '网'), - ] - -def _seg_28() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: - return [ (0x2F7A, 'M', '羊'), (0x2F7B, 'M', '羽'), (0x2F7C, 'M', '老'), (0x2F7D, 'M', '而'), + ] + +def _seg_28() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ (0x2F7E, 'M', '耒'), (0x2F7F, 'M', '耳'), (0x2F80, 'M', '聿'), @@ -3022,14 +3018,14 @@ def _seg_28() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: (0x3036, 'M', '〒'), (0x3037, 'V'), (0x3038, 'M', '十'), - ] - -def _seg_29() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: - return [ (0x3039, 'M', '卄'), (0x303A, 'M', '卅'), (0x303B, 'V'), (0x3040, 'X'), + ] + +def _seg_29() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ (0x3041, 'V'), (0x3097, 'X'), (0x3099, 'V'), @@ -3126,14 +3122,14 @@ def _seg_29() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: (0x3182, 'M', 'ᇱ'), (0x3183, 'M', 'ᇲ'), (0x3184, 'M', 'ᅗ'), - ] - -def _seg_30() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: - return [ (0x3185, 'M', 'ᅘ'), (0x3186, 'M', 'ᅙ'), (0x3187, 'M', 'ᆄ'), (0x3188, 'M', 'ᆅ'), + ] + +def _seg_30() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ (0x3189, 'M', 'ᆈ'), (0x318A, 'M', 'ᆑ'), (0x318B, 'M', 'ᆒ'), @@ -3230,14 +3226,14 @@ def _seg_30() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: (0x3244, 'M', '問'), (0x3245, 'M', '幼'), (0x3246, 'M', '文'), - ] - -def _seg_31() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: - return [ (0x3247, 'M', '箏'), (0x3248, 'V'), (0x3250, 'M', 'pte'), (0x3251, 'M', '21'), + ] + +def _seg_31() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ (0x3252, 'M', '22'), (0x3253, 'M', '23'), (0x3254, 'M', '24'), @@ -3334,14 +3330,14 @@ def _seg_31() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: (0x32AF, 'M', '協'), (0x32B0, 'M', '夜'), (0x32B1, 'M', '36'), - ] - -def _seg_32() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: - return [ (0x32B2, 'M', '37'), (0x32B3, 'M', '38'), (0x32B4, 'M', '39'), (0x32B5, 'M', '40'), + ] + +def _seg_32() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ (0x32B6, 'M', '41'), (0x32B7, 'M', '42'), (0x32B8, 'M', '43'), @@ -3438,14 +3434,14 @@ def _seg_32() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: (0x3313, 'M', 'ギルダー'), (0x3314, 'M', 'キロ'), (0x3315, 'M', 'キログラム'), - ] - -def _seg_33() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: - return [ (0x3316, 'M', 'キロメートル'), (0x3317, 'M', 'キロワット'), (0x3318, 'M', 'グラム'), (0x3319, 'M', 'グラムトン'), + ] + +def _seg_33() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ (0x331A, 'M', 'クルゼイロ'), (0x331B, 'M', 'クローネ'), (0x331C, 'M', 'ケース'), @@ -3542,14 +3538,14 @@ def _seg_33() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: (0x3377, 'M', 'dm'), (0x3378, 'M', 'dm2'), (0x3379, 'M', 'dm3'), - ] - -def _seg_34() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: - return [ (0x337A, 'M', 'iu'), (0x337B, 'M', '平成'), (0x337C, 'M', '昭和'), (0x337D, 'M', '大正'), + ] + +def _seg_34() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ (0x337E, 'M', '明治'), (0x337F, 'M', '株式会社'), (0x3380, 'M', 'pa'), @@ -3646,14 +3642,14 @@ def _seg_34() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: (0x33DB, 'M', 'sr'), (0x33DC, 'M', 'sv'), (0x33DD, 'M', 'wb'), - ] - -def _seg_35() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: - return [ (0x33DE, 'M', 'v∕m'), (0x33DF, 'M', 'a∕m'), (0x33E0, 'M', '1日'), (0x33E1, 'M', '2日'), + ] + +def _seg_35() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ (0x33E2, 'M', '3日'), (0x33E3, 'M', '4日'), (0x33E4, 'M', '5日'), @@ -3750,14 +3746,14 @@ def _seg_35() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: (0xA68B, 'V'), (0xA68C, 'M', 'ꚍ'), (0xA68D, 'V'), - ] - -def _seg_36() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: - return [ (0xA68E, 'M', 'ꚏ'), (0xA68F, 'V'), (0xA690, 'M', 'ꚑ'), (0xA691, 'V'), + ] + +def _seg_36() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ (0xA692, 'M', 'ꚓ'), (0xA693, 'V'), (0xA694, 'M', 'ꚕ'), @@ -3854,14 +3850,14 @@ def _seg_36() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: (0xA779, 'M', 'ꝺ'), (0xA77A, 'V'), (0xA77B, 'M', 'ꝼ'), - ] - -def _seg_37() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: - return [ (0xA77C, 'V'), (0xA77D, 'M', 'ᵹ'), (0xA77E, 'M', 'ꝿ'), (0xA77F, 'V'), + ] + +def _seg_37() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ (0xA780, 'M', 'ꞁ'), (0xA781, 'V'), (0xA782, 'M', 'ꞃ'), @@ -3958,14 +3954,14 @@ def _seg_37() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: (0xA878, 'X'), (0xA880, 'V'), (0xA8C6, 'X'), - ] - -def _seg_38() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: - return [ (0xA8CE, 'V'), (0xA8DA, 'X'), (0xA8E0, 'V'), (0xA954, 'X'), + ] + +def _seg_38() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ (0xA95F, 'V'), (0xA97D, 'X'), (0xA980, 'V'), @@ -4062,14 +4058,14 @@ def _seg_38() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: (0xABA8, 'M', 'Ꮨ'), (0xABA9, 'M', 'Ꮩ'), (0xABAA, 'M', 'Ꮪ'), - ] - -def _seg_39() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: - return [ (0xABAB, 'M', 'Ꮫ'), (0xABAC, 'M', 'Ꮬ'), (0xABAD, 'M', 'Ꮭ'), (0xABAE, 'M', 'Ꮮ'), + ] + +def _seg_39() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ (0xABAF, 'M', 'Ꮯ'), (0xABB0, 'M', 'Ꮰ'), (0xABB1, 'M', 'Ꮱ'), @@ -4166,14 +4162,14 @@ def _seg_39() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: (0xF943, 'M', '弄'), (0xF944, 'M', '籠'), (0xF945, 'M', '聾'), - ] - -def _seg_40() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: - return [ (0xF946, 'M', '牢'), (0xF947, 'M', '磊'), (0xF948, 'M', '賂'), (0xF949, 'M', '雷'), + ] + +def _seg_40() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ (0xF94A, 'M', '壘'), (0xF94B, 'M', '屢'), (0xF94C, 'M', '樓'), @@ -4270,14 +4266,14 @@ def _seg_40() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: (0xF9A7, 'M', '獵'), (0xF9A8, 'M', '令'), (0xF9A9, 'M', '囹'), - ] - -def _seg_41() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: - return [ (0xF9AA, 'M', '寧'), (0xF9AB, 'M', '嶺'), (0xF9AC, 'M', '怜'), (0xF9AD, 'M', '玲'), + ] + +def _seg_41() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ (0xF9AE, 'M', '瑩'), (0xF9AF, 'M', '羚'), (0xF9B0, 'M', '聆'), @@ -4374,14 +4370,14 @@ def _seg_41() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: (0xFA0B, 'M', '廓'), (0xFA0C, 'M', '兀'), (0xFA0D, 'M', '嗀'), - ] - -def _seg_42() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: - return [ (0xFA0E, 'V'), (0xFA10, 'M', '塚'), (0xFA11, 'V'), (0xFA12, 'M', '晴'), + ] + +def _seg_42() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ (0xFA13, 'V'), (0xFA15, 'M', '凞'), (0xFA16, 'M', '猪'), @@ -4478,14 +4474,14 @@ def _seg_42() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: (0xFA76, 'M', '勇'), (0xFA77, 'M', '勺'), (0xFA78, 'M', '喝'), - ] - -def _seg_43() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: - return [ (0xFA79, 'M', '啕'), (0xFA7A, 'M', '喙'), (0xFA7B, 'M', '嗢'), (0xFA7C, 'M', '塚'), + ] + +def _seg_43() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ (0xFA7D, 'M', '墳'), (0xFA7E, 'M', '奄'), (0xFA7F, 'M', '奔'), @@ -4582,14 +4578,14 @@ def _seg_43() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: (0xFADA, 'X'), (0xFB00, 'M', 'ff'), (0xFB01, 'M', 'fi'), - ] - -def _seg_44() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: - return [ (0xFB02, 'M', 'fl'), (0xFB03, 'M', 'ffi'), (0xFB04, 'M', 'ffl'), (0xFB05, 'M', 'st'), + ] + +def _seg_44() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ (0xFB07, 'X'), (0xFB13, 'M', 'մն'), (0xFB14, 'M', 'մե'), @@ -4686,14 +4682,14 @@ def _seg_44() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: (0xFBDB, 'M', 'ۈ'), (0xFBDD, 'M', 'ۇٴ'), (0xFBDE, 'M', 'ۋ'), - ] - -def _seg_45() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: - return [ (0xFBE0, 'M', 'ۅ'), (0xFBE2, 'M', 'ۉ'), (0xFBE4, 'M', 'ې'), (0xFBE8, 'M', 'ى'), + ] + +def _seg_45() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ (0xFBEA, 'M', 'ئا'), (0xFBEC, 'M', 'ئە'), (0xFBEE, 'M', 'ئو'), @@ -4790,14 +4786,14 @@ def _seg_45() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: (0xFC54, 'M', 'هي'), (0xFC55, 'M', 'يج'), (0xFC56, 'M', 'يح'), - ] - -def _seg_46() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: - return [ (0xFC57, 'M', 'يخ'), (0xFC58, 'M', 'يم'), (0xFC59, 'M', 'يى'), (0xFC5A, 'M', 'يي'), + ] + +def _seg_46() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ (0xFC5B, 'M', 'ذٰ'), (0xFC5C, 'M', 'رٰ'), (0xFC5D, 'M', 'ىٰ'), @@ -4894,14 +4890,14 @@ def _seg_46() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: (0xFCB8, 'M', 'طح'), (0xFCB9, 'M', 'ظم'), (0xFCBA, 'M', 'عج'), - ] - -def _seg_47() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: - return [ (0xFCBB, 'M', 'عم'), (0xFCBC, 'M', 'غج'), (0xFCBD, 'M', 'غم'), (0xFCBE, 'M', 'فج'), + ] + +def _seg_47() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ (0xFCBF, 'M', 'فح'), (0xFCC0, 'M', 'فخ'), (0xFCC1, 'M', 'فم'), @@ -4998,14 +4994,14 @@ def _seg_47() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: (0xFD1C, 'M', 'حي'), (0xFD1D, 'M', 'جى'), (0xFD1E, 'M', 'جي'), - ] - -def _seg_48() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: - return [ (0xFD1F, 'M', 'خى'), (0xFD20, 'M', 'خي'), (0xFD21, 'M', 'صى'), (0xFD22, 'M', 'صي'), + ] + +def _seg_48() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ (0xFD23, 'M', 'ضى'), (0xFD24, 'M', 'ضي'), (0xFD25, 'M', 'شج'), @@ -5102,14 +5098,14 @@ def _seg_48() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: (0xFDA4, 'M', 'تمى'), (0xFDA5, 'M', 'جمي'), (0xFDA6, 'M', 'جحى'), - ] - -def _seg_49() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: - return [ (0xFDA7, 'M', 'جمى'), (0xFDA8, 'M', 'سخى'), (0xFDA9, 'M', 'صحي'), (0xFDAA, 'M', 'شحي'), + ] + +def _seg_49() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ (0xFDAB, 'M', 'ضحي'), (0xFDAC, 'M', 'لجي'), (0xFDAD, 'M', 'لمي'), @@ -5206,14 +5202,14 @@ def _seg_49() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: (0xFE5B, '3', '{'), (0xFE5C, '3', '}'), (0xFE5D, 'M', '〔'), - ] - -def _seg_50() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: - return [ (0xFE5E, 'M', '〕'), (0xFE5F, '3', '#'), (0xFE60, '3', '&'), (0xFE61, '3', '*'), + ] + +def _seg_50() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ (0xFE62, '3', '+'), (0xFE63, 'M', '-'), (0xFE64, '3', '<'), @@ -5310,14 +5306,14 @@ def _seg_50() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: (0xFF18, 'M', '8'), (0xFF19, 'M', '9'), (0xFF1A, '3', ':'), - ] - -def _seg_51() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: - return [ (0xFF1B, '3', ';'), (0xFF1C, '3', '<'), (0xFF1D, '3', '='), (0xFF1E, '3', '>'), + ] + +def _seg_51() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ (0xFF1F, '3', '?'), (0xFF20, '3', '@'), (0xFF21, 'M', 'a'), @@ -5414,14 +5410,14 @@ def _seg_51() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: (0xFF7C, 'M', 'シ'), (0xFF7D, 'M', 'ス'), (0xFF7E, 'M', 'セ'), - ] - -def _seg_52() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: - return [ (0xFF7F, 'M', 'ソ'), (0xFF80, 'M', 'タ'), (0xFF81, 'M', 'チ'), (0xFF82, 'M', 'ツ'), + ] + +def _seg_52() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ (0xFF83, 'M', 'テ'), (0xFF84, 'M', 'ト'), (0xFF85, 'M', 'ナ'), @@ -5518,14 +5514,14 @@ def _seg_52() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: (0xFFE7, 'X'), (0xFFE8, 'M', '│'), (0xFFE9, 'M', '←'), - ] - -def _seg_53() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: - return [ (0xFFEA, 'M', '↑'), (0xFFEB, 'M', '→'), (0xFFEC, 'M', '↓'), (0xFFED, 'M', '■'), + ] + +def _seg_53() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ (0xFFEE, 'M', '○'), (0xFFEF, 'X'), (0x10000, 'V'), @@ -5622,14 +5618,14 @@ def _seg_53() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: (0x104B3, 'M', '𐓛'), (0x104B4, 'M', '𐓜'), (0x104B5, 'M', '𐓝'), - ] - -def _seg_54() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: - return [ (0x104B6, 'M', '𐓞'), (0x104B7, 'M', '𐓟'), (0x104B8, 'M', '𐓠'), (0x104B9, 'M', '𐓡'), + ] + +def _seg_54() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ (0x104BA, 'M', '𐓢'), (0x104BB, 'M', '𐓣'), (0x104BC, 'M', '𐓤'), @@ -5726,14 +5722,14 @@ def _seg_54() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: (0x10786, 'X'), (0x10787, 'M', 'ʣ'), (0x10788, 'M', 'ꭦ'), - ] - -def _seg_55() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: - return [ (0x10789, 'M', 'ʥ'), (0x1078A, 'M', 'ʤ'), (0x1078B, 'M', 'ɖ'), (0x1078C, 'M', 'ɗ'), + ] + +def _seg_55() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ (0x1078D, 'M', 'ᶑ'), (0x1078E, 'M', 'ɘ'), (0x1078F, 'M', 'ɞ'), @@ -5830,14 +5826,14 @@ def _seg_55() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: (0x10A60, 'V'), (0x10AA0, 'X'), (0x10AC0, 'V'), - ] - -def _seg_56() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: - return [ (0x10AE7, 'X'), (0x10AEB, 'V'), (0x10AF7, 'X'), (0x10B00, 'V'), + ] + +def _seg_56() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ (0x10B36, 'X'), (0x10B39, 'V'), (0x10B56, 'X'), @@ -5934,14 +5930,14 @@ def _seg_56() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: (0x1107F, 'V'), (0x110BD, 'X'), (0x110BE, 'V'), - ] - -def _seg_57() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: - return [ (0x110C3, 'X'), (0x110D0, 'V'), (0x110E9, 'X'), (0x110F0, 'V'), + ] + +def _seg_57() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ (0x110FA, 'X'), (0x11100, 'V'), (0x11135, 'X'), @@ -6038,14 +6034,14 @@ def _seg_57() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: (0x118A4, 'M', '𑣄'), (0x118A5, 'M', '𑣅'), (0x118A6, 'M', '𑣆'), - ] - -def _seg_58() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: - return [ (0x118A7, 'M', '𑣇'), (0x118A8, 'M', '𑣈'), (0x118A9, 'M', '𑣉'), (0x118AA, 'M', '𑣊'), + ] + +def _seg_58() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ (0x118AB, 'M', '𑣋'), (0x118AC, 'M', '𑣌'), (0x118AD, 'M', '𑣍'), @@ -6142,14 +6138,14 @@ def _seg_58() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: (0x11EE0, 'V'), (0x11EF9, 'X'), (0x11F00, 'V'), - ] - -def _seg_59() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: - return [ (0x11F11, 'X'), (0x11F12, 'V'), (0x11F3B, 'X'), (0x11F3E, 'V'), + ] + +def _seg_59() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ (0x11F5A, 'X'), (0x11FB0, 'V'), (0x11FB1, 'X'), @@ -6246,14 +6242,14 @@ def _seg_59() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: (0x18D00, 'V'), (0x18D09, 'X'), (0x1AFF0, 'V'), - ] - -def _seg_60() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: - return [ (0x1AFF4, 'X'), (0x1AFF5, 'V'), (0x1AFFC, 'X'), (0x1AFFD, 'V'), + ] + +def _seg_60() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ (0x1AFFF, 'X'), (0x1B000, 'V'), (0x1B123, 'X'), @@ -6350,14 +6346,14 @@ def _seg_60() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: (0x1D41E, 'M', 'e'), (0x1D41F, 'M', 'f'), (0x1D420, 'M', 'g'), - ] - -def _seg_61() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: - return [ (0x1D421, 'M', 'h'), (0x1D422, 'M', 'i'), (0x1D423, 'M', 'j'), (0x1D424, 'M', 'k'), + ] + +def _seg_61() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ (0x1D425, 'M', 'l'), (0x1D426, 'M', 'm'), (0x1D427, 'M', 'n'), @@ -6454,14 +6450,14 @@ def _seg_61() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: (0x1D482, 'M', 'a'), (0x1D483, 'M', 'b'), (0x1D484, 'M', 'c'), - ] - -def _seg_62() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: - return [ (0x1D485, 'M', 'd'), (0x1D486, 'M', 'e'), (0x1D487, 'M', 'f'), (0x1D488, 'M', 'g'), + ] + +def _seg_62() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ (0x1D489, 'M', 'h'), (0x1D48A, 'M', 'i'), (0x1D48B, 'M', 'j'), @@ -6558,14 +6554,14 @@ def _seg_62() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: (0x1D4E9, 'M', 'z'), (0x1D4EA, 'M', 'a'), (0x1D4EB, 'M', 'b'), - ] - -def _seg_63() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: - return [ (0x1D4EC, 'M', 'c'), (0x1D4ED, 'M', 'd'), (0x1D4EE, 'M', 'e'), (0x1D4EF, 'M', 'f'), + ] + +def _seg_63() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ (0x1D4F0, 'M', 'g'), (0x1D4F1, 'M', 'h'), (0x1D4F2, 'M', 'i'), @@ -6662,14 +6658,14 @@ def _seg_63() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: (0x1D550, 'M', 'y'), (0x1D551, 'X'), (0x1D552, 'M', 'a'), - ] - -def _seg_64() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: - return [ (0x1D553, 'M', 'b'), (0x1D554, 'M', 'c'), (0x1D555, 'M', 'd'), (0x1D556, 'M', 'e'), + ] + +def _seg_64() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ (0x1D557, 'M', 'f'), (0x1D558, 'M', 'g'), (0x1D559, 'M', 'h'), @@ -6766,14 +6762,14 @@ def _seg_64() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: (0x1D5B4, 'M', 'u'), (0x1D5B5, 'M', 'v'), (0x1D5B6, 'M', 'w'), - ] - -def _seg_65() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: - return [ (0x1D5B7, 'M', 'x'), (0x1D5B8, 'M', 'y'), (0x1D5B9, 'M', 'z'), (0x1D5BA, 'M', 'a'), + ] + +def _seg_65() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ (0x1D5BB, 'M', 'b'), (0x1D5BC, 'M', 'c'), (0x1D5BD, 'M', 'd'), @@ -6870,14 +6866,14 @@ def _seg_65() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: (0x1D618, 'M', 'q'), (0x1D619, 'M', 'r'), (0x1D61A, 'M', 's'), - ] - -def _seg_66() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: - return [ (0x1D61B, 'M', 't'), (0x1D61C, 'M', 'u'), (0x1D61D, 'M', 'v'), (0x1D61E, 'M', 'w'), + ] + +def _seg_66() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ (0x1D61F, 'M', 'x'), (0x1D620, 'M', 'y'), (0x1D621, 'M', 'z'), @@ -6974,14 +6970,14 @@ def _seg_66() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: (0x1D67C, 'M', 'm'), (0x1D67D, 'M', 'n'), (0x1D67E, 'M', 'o'), - ] - -def _seg_67() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: - return [ (0x1D67F, 'M', 'p'), (0x1D680, 'M', 'q'), (0x1D681, 'M', 'r'), (0x1D682, 'M', 's'), + ] + +def _seg_67() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ (0x1D683, 'M', 't'), (0x1D684, 'M', 'u'), (0x1D685, 'M', 'v'), @@ -7078,14 +7074,14 @@ def _seg_67() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: (0x1D6E2, 'M', 'α'), (0x1D6E3, 'M', 'β'), (0x1D6E4, 'M', 'γ'), - ] - -def _seg_68() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: - return [ (0x1D6E5, 'M', 'δ'), (0x1D6E6, 'M', 'ε'), (0x1D6E7, 'M', 'ζ'), (0x1D6E8, 'M', 'η'), + ] + +def _seg_68() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ (0x1D6E9, 'M', 'θ'), (0x1D6EA, 'M', 'ι'), (0x1D6EB, 'M', 'κ'), @@ -7182,14 +7178,14 @@ def _seg_68() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: (0x1D747, 'M', 'σ'), (0x1D749, 'M', 'τ'), (0x1D74A, 'M', 'υ'), - ] - -def _seg_69() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: - return [ (0x1D74B, 'M', 'φ'), (0x1D74C, 'M', 'χ'), (0x1D74D, 'M', 'ψ'), (0x1D74E, 'M', 'ω'), + ] + +def _seg_69() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ (0x1D74F, 'M', '∂'), (0x1D750, 'M', 'ε'), (0x1D751, 'M', 'θ'), @@ -7286,14 +7282,14 @@ def _seg_69() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: (0x1D7AD, 'M', 'δ'), (0x1D7AE, 'M', 'ε'), (0x1D7AF, 'M', 'ζ'), - ] - -def _seg_70() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: - return [ (0x1D7B0, 'M', 'η'), (0x1D7B1, 'M', 'θ'), (0x1D7B2, 'M', 'ι'), (0x1D7B3, 'M', 'κ'), + ] + +def _seg_70() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ (0x1D7B4, 'M', 'λ'), (0x1D7B5, 'M', 'μ'), (0x1D7B6, 'M', 'ν'), @@ -7390,14 +7386,14 @@ def _seg_70() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: (0x1E030, 'M', 'а'), (0x1E031, 'M', 'б'), (0x1E032, 'M', 'в'), - ] - -def _seg_71() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: - return [ (0x1E033, 'M', 'г'), (0x1E034, 'M', 'д'), (0x1E035, 'M', 'е'), (0x1E036, 'M', 'ж'), + ] + +def _seg_71() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ (0x1E037, 'M', 'з'), (0x1E038, 'M', 'и'), (0x1E039, 'M', 'к'), @@ -7494,14 +7490,14 @@ def _seg_71() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: (0x1E907, 'M', '𞤩'), (0x1E908, 'M', '𞤪'), (0x1E909, 'M', '𞤫'), - ] - -def _seg_72() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: - return [ (0x1E90A, 'M', '𞤬'), (0x1E90B, 'M', '𞤭'), (0x1E90C, 'M', '𞤮'), (0x1E90D, 'M', '𞤯'), + ] + +def _seg_72() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ (0x1E90E, 'M', '𞤰'), (0x1E90F, 'M', '𞤱'), (0x1E910, 'M', '𞤲'), @@ -7598,14 +7594,14 @@ def _seg_72() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: (0x1EE48, 'X'), (0x1EE49, 'M', 'ي'), (0x1EE4A, 'X'), - ] - -def _seg_73() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: - return [ (0x1EE4B, 'M', 'ل'), (0x1EE4C, 'X'), (0x1EE4D, 'M', 'ن'), (0x1EE4E, 'M', 'س'), + ] + +def _seg_73() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ (0x1EE4F, 'M', 'ع'), (0x1EE50, 'X'), (0x1EE51, 'M', 'ص'), @@ -7702,14 +7698,14 @@ def _seg_73() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: (0x1EEB2, 'M', 'ق'), (0x1EEB3, 'M', 'ر'), (0x1EEB4, 'M', 'ش'), - ] - -def _seg_74() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: - return [ (0x1EEB5, 'M', 'ت'), (0x1EEB6, 'M', 'ث'), (0x1EEB7, 'M', 'خ'), (0x1EEB8, 'M', 'ذ'), + ] + +def _seg_74() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ (0x1EEB9, 'M', 'ض'), (0x1EEBA, 'M', 'ظ'), (0x1EEBB, 'M', 'غ'), @@ -7806,14 +7802,14 @@ def _seg_74() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: (0x1F150, 'V'), (0x1F16A, 'M', 'mc'), (0x1F16B, 'M', 'md'), - ] - -def _seg_75() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: - return [ (0x1F16C, 'M', 'mr'), (0x1F16D, 'V'), (0x1F190, 'M', 'dj'), (0x1F191, 'V'), + ] + +def _seg_75() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ (0x1F1AE, 'X'), (0x1F1E6, 'V'), (0x1F200, 'M', 'ほか'), @@ -7910,14 +7906,14 @@ def _seg_75() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: (0x1FA54, 'X'), (0x1FA60, 'V'), (0x1FA6E, 'X'), - ] - -def _seg_76() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: - return [ (0x1FA70, 'V'), (0x1FA7D, 'X'), (0x1FA80, 'V'), (0x1FA89, 'X'), + ] + +def _seg_76() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ (0x1FA90, 'V'), (0x1FABE, 'X'), (0x1FABF, 'V'), @@ -7953,6 +7949,8 @@ def _seg_76() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: (0x2CEA2, 'X'), (0x2CEB0, 'V'), (0x2EBE1, 'X'), + (0x2EBF0, 'V'), + (0x2EE5E, 'X'), (0x2F800, 'M', '丽'), (0x2F801, 'M', '丸'), (0x2F802, 'M', '乁'), @@ -8014,12 +8012,12 @@ def _seg_76() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: (0x2F83C, 'M', '咞'), (0x2F83D, 'M', '吸'), (0x2F83E, 'M', '呈'), + (0x2F83F, 'M', '周'), + (0x2F840, 'M', '咢'), ] def _seg_77() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: return [ - (0x2F83F, 'M', '周'), - (0x2F840, 'M', '咢'), (0x2F841, 'M', '哶'), (0x2F842, 'M', '唐'), (0x2F843, 'M', '啓'), @@ -8118,12 +8116,12 @@ def _seg_77() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: (0x2F8A4, 'M', '𢛔'), (0x2F8A5, 'M', '惇'), (0x2F8A6, 'M', '慈'), + (0x2F8A7, 'M', '慌'), + (0x2F8A8, 'M', '慎'), ] def _seg_78() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: return [ - (0x2F8A7, 'M', '慌'), - (0x2F8A8, 'M', '慎'), (0x2F8A9, 'M', '慌'), (0x2F8AA, 'M', '慺'), (0x2F8AB, 'M', '憎'), @@ -8222,12 +8220,12 @@ def _seg_78() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: (0x2F908, 'M', '港'), (0x2F909, 'M', '湮'), (0x2F90A, 'M', '㴳'), + (0x2F90B, 'M', '滋'), + (0x2F90C, 'M', '滇'), ] def _seg_79() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: return [ - (0x2F90B, 'M', '滋'), - (0x2F90C, 'M', '滇'), (0x2F90D, 'M', '𣻑'), (0x2F90E, 'M', '淹'), (0x2F90F, 'M', '潮'), @@ -8326,12 +8324,12 @@ def _seg_79() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: (0x2F96F, 'M', '縂'), (0x2F970, 'M', '繅'), (0x2F971, 'M', '䌴'), + (0x2F972, 'M', '𦈨'), + (0x2F973, 'M', '𦉇'), ] def _seg_80() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: return [ - (0x2F972, 'M', '𦈨'), - (0x2F973, 'M', '𦉇'), (0x2F974, 'M', '䍙'), (0x2F975, 'M', '𦋙'), (0x2F976, 'M', '罺'), @@ -8430,12 +8428,12 @@ def _seg_80() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: (0x2F9D3, 'M', '𧲨'), (0x2F9D4, 'M', '貫'), (0x2F9D5, 'M', '賁'), + (0x2F9D6, 'M', '贛'), + (0x2F9D7, 'M', '起'), ] def _seg_81() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: return [ - (0x2F9D6, 'M', '贛'), - (0x2F9D7, 'M', '起'), (0x2F9D8, 'M', '𧼯'), (0x2F9D9, 'M', '𠠄'), (0x2F9DA, 'M', '跋'), From a40b0cb46f1fd8f0f1e98bd6b04ae649faf0f571 Mon Sep 17 00:00:00 2001 From: JackDandy Date: Wed, 5 Jun 2024 09:19:49 +0100 Subject: [PATCH 04/29] =?UTF-8?q?Update=20CacheControl=200.13.1=20(783a338?= =?UTF-8?q?)=20=E2=86=92=200.14.0=20(e2be0c2).?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGES.md | 1 + lib/cachecontrol/__init__.py | 2 +- lib/cachecontrol/adapter.py | 10 +++++----- lib/cachecontrol/caches/file_cache.py | 2 +- lib/cachecontrol/controller.py | 7 ++++++- lib/cachecontrol/heuristics.py | 2 +- lib/cachecontrol/serialize.py | 4 ++-- 7 files changed, 17 insertions(+), 11 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 4b9649cb..c7d6a7b7 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,6 @@ ### 3.32.0 (2024-xx-xx xx:xx:00 UTC) +* Update CacheControl 0.13.1 (783a338) to 0.14.0 (e2be0c2) * Update idna library 3.4 (cab054c) to 3.7 (1d365e1) * Update Requests library 2.31.0 (8812812) to 2.32.3 (0e322af) * Update urllib3 2.0.7 (56f01e0) to 2.2.1 (54d6edf) diff --git a/lib/cachecontrol/__init__.py b/lib/cachecontrol/__init__.py index abbf8c06..82646f37 100644 --- a/lib/cachecontrol/__init__.py +++ b/lib/cachecontrol/__init__.py @@ -8,7 +8,7 @@ Make it easy to import from cachecontrol without long namespaces. """ __author__ = "Eric Larson" __email__ = "eric@ionrock.org" -__version__ = "0.13.1" +__version__ = "0.14.0" from cachecontrol.adapter import CacheControlAdapter from cachecontrol.controller import CacheController diff --git a/lib/cachecontrol/adapter.py b/lib/cachecontrol/adapter.py index bf4a23dd..d2e65ff0 100644 --- a/lib/cachecontrol/adapter.py +++ b/lib/cachecontrol/adapter.py @@ -125,21 +125,21 @@ class CacheControlAdapter(HTTPAdapter): else: # Wrap the response file with a wrapper that will cache the # response when the stream has been consumed. - response._fp = CallbackFileWrapper( # type: ignore[attr-defined] - response._fp, # type: ignore[attr-defined] + response._fp = CallbackFileWrapper( # type: ignore[assignment] + response._fp, # type: ignore[arg-type] functools.partial( self.controller.cache_response, request, response ), ) if response.chunked: - super_update_chunk_length = response._update_chunk_length # type: ignore[attr-defined] + super_update_chunk_length = response._update_chunk_length def _update_chunk_length(self: HTTPResponse) -> None: super_update_chunk_length() if self.chunk_left == 0: - self._fp._close() # type: ignore[attr-defined] + self._fp._close() # type: ignore[union-attr] - response._update_chunk_length = types.MethodType( # type: ignore[attr-defined] + response._update_chunk_length = types.MethodType( # type: ignore[method-assign] _update_chunk_length, response ) diff --git a/lib/cachecontrol/caches/file_cache.py b/lib/cachecontrol/caches/file_cache.py index 86213500..6981bda1 100644 --- a/lib/cachecontrol/caches/file_cache.py +++ b/lib/cachecontrol/caches/file_cache.py @@ -66,7 +66,7 @@ class _FileCacheMixin: def __init__( self, - directory: Union[str, Path], + directory: str | Path, forever: bool = False, filemode: int = 0o0600, dirmode: int = 0o0700, diff --git a/lib/cachecontrol/controller.py b/lib/cachecontrol/controller.py index 4f54a121..5e9cab72 100644 --- a/lib/cachecontrol/controller.py +++ b/lib/cachecontrol/controller.py @@ -142,6 +142,11 @@ class CacheController: """ Load a cached response, or return None if it's not available. """ + # We do not support caching of partial content: so if the request contains a + # Range header then we don't want to load anything from the cache. + if "Range" in request.headers: + return None + cache_url = request.url assert cache_url is not None cache_data = self.cache.get(cache_url) @@ -480,7 +485,7 @@ class CacheController: cached_response.headers.update( { k: v - for k, v in response.headers.items() # type: ignore[no-untyped-call] + for k, v in response.headers.items() if k.lower() not in excluded_headers } ) diff --git a/lib/cachecontrol/heuristics.py b/lib/cachecontrol/heuristics.py index 323262be..ea1badca 100644 --- a/lib/cachecontrol/heuristics.py +++ b/lib/cachecontrol/heuristics.py @@ -68,7 +68,7 @@ class OneDayCache(BaseHeuristic): if "expires" not in response.headers: date = parsedate(response.headers["date"]) - expires = expire_after(timedelta(days=1), date=datetime(*date[:6], tzinfo=timezone.utc)) # type: ignore[misc] + expires = expire_after(timedelta(days=1), date=datetime(*date[:6], tzinfo=timezone.utc)) # type: ignore[index,misc] headers["expires"] = datetime_to_header(expires) headers["cache-control"] = "public" return headers diff --git a/lib/cachecontrol/serialize.py b/lib/cachecontrol/serialize.py index 99045a0a..83bce073 100644 --- a/lib/cachecontrol/serialize.py +++ b/lib/cachecontrol/serialize.py @@ -32,13 +32,13 @@ class Serializer: # also update the response with a new file handler to be # sure it acts as though it was never read. body = response.read(decode_content=False) - response._fp = io.BytesIO(body) # type: ignore[attr-defined] + response._fp = io.BytesIO(body) # type: ignore[assignment] response.length_remaining = len(body) data = { "response": { "body": body, # Empty bytestring if body is stored separately - "headers": {str(k): str(v) for k, v in response.headers.items()}, # type: ignore[no-untyped-call] + "headers": {str(k): str(v) for k, v in response.headers.items()}, "status": response.status, "version": response.version, "reason": str(response.reason), From 5715726b2218e673ea1754e463c3d753cd7edd68 Mon Sep 17 00:00:00 2001 From: JackDandy Date: Wed, 5 Jun 2024 09:37:08 +0100 Subject: [PATCH 05/29] =?UTF-8?q?Update=20filelock=203.12.4=20(c1163ae)=20?= =?UTF-8?q?=E2=86=92=203.14.0=20(8556141).?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGES.md | 1 + lib/filelock/__init__.py | 9 ++-- lib/filelock/_api.py | 104 ++++++++++++++++++++++++++++++++++----- lib/filelock/_unix.py | 9 ++-- lib/filelock/_util.py | 13 +++-- lib/filelock/version.py | 4 +- 6 files changed, 114 insertions(+), 26 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index c7d6a7b7..837132f3 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,6 +1,7 @@ ### 3.32.0 (2024-xx-xx xx:xx:00 UTC) * Update CacheControl 0.13.1 (783a338) to 0.14.0 (e2be0c2) +* Update filelock 3.12.4 (c1163ae) to 3.14.0 (8556141) * Update idna library 3.4 (cab054c) to 3.7 (1d365e1) * Update Requests library 2.31.0 (8812812) to 2.32.3 (0e322af) * Update urllib3 2.0.7 (56f01e0) to 2.2.1 (54d6edf) diff --git a/lib/filelock/__init__.py b/lib/filelock/__init__.py index 0b8c1d2f..006299d2 100644 --- a/lib/filelock/__init__.py +++ b/lib/filelock/__init__.py @@ -5,6 +5,7 @@ A platform independent file lock that supports the with-statement. :no-value: """ + from __future__ import annotations import sys @@ -32,7 +33,7 @@ else: # pragma: win32 no cover # noqa: PLR5501 if warnings is not None: warnings.warn("only soft file lock is available", stacklevel=2) -if TYPE_CHECKING: # noqa: SIM108 +if TYPE_CHECKING: FileLock = SoftFileLock else: #: Alias for the lock, which should be used for the current platform. @@ -40,12 +41,12 @@ else: __all__ = [ - "__version__", + "AcquireReturnProxy", + "BaseFileLock", "FileLock", "SoftFileLock", "Timeout", "UnixFileLock", "WindowsFileLock", - "BaseFileLock", - "AcquireReturnProxy", + "__version__", ] diff --git a/lib/filelock/_api.py b/lib/filelock/_api.py index 8a40ccd0..fd87972c 100644 --- a/lib/filelock/_api.py +++ b/lib/filelock/_api.py @@ -9,6 +9,7 @@ from abc import ABC, abstractmethod from dataclasses import dataclass from threading import local from typing import TYPE_CHECKING, Any +from weakref import WeakValueDictionary from ._error import Timeout @@ -29,7 +30,7 @@ _LOGGER = logging.getLogger("filelock") # is not called twice when entering the with statement. If we would simply return *self*, the lock would be acquired # again in the *__enter__* method of the BaseFileLock, but not released again automatically. issue #37 (memory leak) class AcquireReturnProxy: - """A context aware object that will release the lock file when exiting.""" + """A context-aware object that will release the lock file when exiting.""" def __init__(self, lock: BaseFileLock) -> None: self.lock = lock @@ -62,6 +63,9 @@ class FileLockContext: #: The mode for the lock files mode: int + #: Whether the lock should be blocking or not + blocking: bool + #: The file descriptor for the *_lock_file* as it is returned by the os.open() function, not None when lock held lock_file_fd: int | None = None @@ -76,32 +80,74 @@ class ThreadLocalFileContext(FileLockContext, local): class BaseFileLock(ABC, contextlib.ContextDecorator): """Abstract base class for a file lock object.""" - def __init__( + _instances: WeakValueDictionary[str, BaseFileLock] + + def __new__( # noqa: PLR0913 + cls, + lock_file: str | os.PathLike[str], + timeout: float = -1, + mode: int = 0o644, + thread_local: bool = True, # noqa: ARG003, FBT001, FBT002 + *, + blocking: bool = True, # noqa: ARG003 + is_singleton: bool = False, + **kwargs: dict[str, Any], # capture remaining kwargs for subclasses # noqa: ARG003 + ) -> Self: + """Create a new lock object or if specified return the singleton instance for the lock file.""" + if not is_singleton: + return super().__new__(cls) + + instance = cls._instances.get(str(lock_file)) + if not instance: + instance = super().__new__(cls) + cls._instances[str(lock_file)] = instance + elif timeout != instance.timeout or mode != instance.mode: + msg = "Singleton lock instances cannot be initialized with differing arguments" + raise ValueError(msg) + + return instance # type: ignore[return-value] # https://github.com/python/mypy/issues/15322 + + def __init_subclass__(cls, **kwargs: dict[str, Any]) -> None: + """Setup unique state for lock subclasses.""" + super().__init_subclass__(**kwargs) + cls._instances = WeakValueDictionary() + + def __init__( # noqa: PLR0913 self, lock_file: str | os.PathLike[str], timeout: float = -1, mode: int = 0o644, thread_local: bool = True, # noqa: FBT001, FBT002 + *, + blocking: bool = True, + is_singleton: bool = False, ) -> None: """ Create a new lock object. :param lock_file: path to the file - :param timeout: default timeout when acquiring the lock, in seconds. It will be used as fallback value in - the acquire method, if no timeout value (``None``) is given. If you want to disable the timeout, set it - to a negative value. A timeout of 0 means, that there is exactly one attempt to acquire the file lock. - :param mode: file permissions for the lockfile. - :param thread_local: Whether this object's internal context should be thread local or not. - If this is set to ``False`` then the lock will be reentrant across threads. + :param timeout: default timeout when acquiring the lock, in seconds. It will be used as fallback value in \ + the acquire method, if no timeout value (``None``) is given. If you want to disable the timeout, set it \ + to a negative value. A timeout of 0 means that there is exactly one attempt to acquire the file lock. + :param mode: file permissions for the lockfile + :param thread_local: Whether this object's internal context should be thread local or not. If this is set to \ + ``False`` then the lock will be reentrant across threads. + :param blocking: whether the lock should be blocking or not + :param is_singleton: If this is set to ``True`` then only one instance of this class will be created \ + per lock file. This is useful if you want to use the lock object for reentrant locking without needing \ + to pass the same object around. + """ self._is_thread_local = thread_local + self._is_singleton = is_singleton - # Create the context. Note that external code should not work with the context directly and should instead use + # Create the context. Note that external code should not work with the context directly and should instead use # properties of this class. kwargs: dict[str, Any] = { "lock_file": os.fspath(lock_file), "timeout": timeout, "mode": mode, + "blocking": blocking, } self._context: FileLockContext = (ThreadLocalFileContext if thread_local else FileLockContext)(**kwargs) @@ -109,6 +155,11 @@ class BaseFileLock(ABC, contextlib.ContextDecorator): """:return: a flag indicating if this lock is thread local or not""" return self._is_thread_local + @property + def is_singleton(self) -> bool: + """:return: a flag indicating if this lock is singleton or not""" + return self._is_singleton + @property def lock_file(self) -> str: """:return: path to the lock file""" @@ -129,9 +180,30 @@ class BaseFileLock(ABC, contextlib.ContextDecorator): Change the default timeout value. :param value: the new value, in seconds + """ self._context.timeout = float(value) + @property + def blocking(self) -> bool: + """:return: whether the locking is blocking or not""" + return self._context.blocking + + @blocking.setter + def blocking(self, value: bool) -> None: + """ + Change the default blocking value. + + :param value: the new value as bool + + """ + self._context.blocking = value + + @property + def mode(self) -> int: + """:return: the file permissions for the lockfile""" + return self._context.mode + @abstractmethod def _acquire(self) -> None: """If the file lock could be acquired, self._context.lock_file_fd holds the file descriptor of the lock file.""" @@ -165,7 +237,7 @@ class BaseFileLock(ABC, contextlib.ContextDecorator): poll_interval: float = 0.05, *, poll_intervall: float | None = None, - blocking: bool = True, + blocking: bool | None = None, ) -> AcquireReturnProxy: """ Try to acquire the file lock. @@ -202,6 +274,9 @@ class BaseFileLock(ABC, contextlib.ContextDecorator): if timeout is None: timeout = self._context.timeout + if blocking is None: + blocking = self._context.blocking + if poll_intervall is not None: msg = "use poll_interval instead of poll_intervall" warnings.warn(msg, DeprecationWarning, stacklevel=2) @@ -237,10 +312,11 @@ class BaseFileLock(ABC, contextlib.ContextDecorator): def release(self, force: bool = False) -> None: # noqa: FBT001, FBT002 """ - Releases the file lock. Please note, that the lock is only completely released, if the lock counter is 0. Also - note, that the lock file itself is not automatically deleted. + Releases the file lock. Please note, that the lock is only completely released, if the lock counter is 0. + Also note, that the lock file itself is not automatically deleted. :param force: If true, the lock counter is ignored and the lock is released in every case/ + """ if self.is_locked: self._context.lock_counter -= 1 @@ -258,6 +334,7 @@ class BaseFileLock(ABC, contextlib.ContextDecorator): Acquire the lock. :return: the lock object + """ self.acquire() return self @@ -274,6 +351,7 @@ class BaseFileLock(ABC, contextlib.ContextDecorator): :param exc_type: the exception type if raised :param exc_value: the exception value if raised :param traceback: the exception traceback if raised + """ self.release() @@ -283,6 +361,6 @@ class BaseFileLock(ABC, contextlib.ContextDecorator): __all__ = [ - "BaseFileLock", "AcquireReturnProxy", + "BaseFileLock", ] diff --git a/lib/filelock/_unix.py b/lib/filelock/_unix.py index 93ce3be5..4ae1fbe9 100644 --- a/lib/filelock/_unix.py +++ b/lib/filelock/_unix.py @@ -4,6 +4,7 @@ import os import sys from contextlib import suppress from errno import ENOSYS +from pathlib import Path from typing import cast from ._api import BaseFileLock @@ -35,7 +36,9 @@ else: # pragma: win32 no cover def _acquire(self) -> None: ensure_directory_exists(self.lock_file) - open_flags = os.O_RDWR | os.O_CREAT | os.O_TRUNC + open_flags = os.O_RDWR | os.O_TRUNC + if not Path(self.lock_file).exists(): + open_flags |= os.O_CREAT fd = os.open(self.lock_file, open_flags, self._context.mode) with suppress(PermissionError): # This locked is not owned by this UID os.fchmod(fd, self._context.mode) @@ -44,7 +47,7 @@ else: # pragma: win32 no cover except OSError as exception: os.close(fd) if exception.errno == ENOSYS: # NotImplemented error - msg = "FileSystem does not appear to support flock; user SoftFileLock instead" + msg = "FileSystem does not appear to support flock; use SoftFileLock instead" raise NotImplementedError(msg) from exception else: self._context.lock_file_fd = fd @@ -60,6 +63,6 @@ else: # pragma: win32 no cover __all__ = [ - "has_fcntl", "UnixFileLock", + "has_fcntl", ] diff --git a/lib/filelock/_util.py b/lib/filelock/_util.py index 543c1394..c671e853 100644 --- a/lib/filelock/_util.py +++ b/lib/filelock/_util.py @@ -10,10 +10,13 @@ from pathlib import Path def raise_on_not_writable_file(filename: str) -> None: """ Raise an exception if attempting to open the file for writing would fail. - This is done so files that will never be writable can be separated from - files that are writable but currently locked + + This is done so files that will never be writable can be separated from files that are writable but currently + locked. + :param filename: file to check :raises OSError: as if the file was opened for writing. + """ try: # use stat to do exists + can write to check without race condition file_stat = os.stat(filename) # noqa: PTH116 @@ -35,13 +38,15 @@ def raise_on_not_writable_file(filename: str) -> None: def ensure_directory_exists(filename: Path | str) -> None: """ - Ensure the directory containing the file exists (create it if necessary) + Ensure the directory containing the file exists (create it if necessary). + :param filename: file. + """ Path(filename).parent.mkdir(parents=True, exist_ok=True) __all__ = [ - "raise_on_not_writable_file", "ensure_directory_exists", + "raise_on_not_writable_file", ] diff --git a/lib/filelock/version.py b/lib/filelock/version.py index e6e96d53..0afa0cef 100644 --- a/lib/filelock/version.py +++ b/lib/filelock/version.py @@ -1,4 +1,4 @@ # file generated by setuptools_scm # don't change, don't track in version control -__version__ = version = '3.12.4' -__version_tuple__ = version_tuple = (3, 12, 4) +__version__ = version = '3.14.0' +__version_tuple__ = version_tuple = (3, 14, 0) From 2561cda8ceea828cf8b1cb58ff1378634db58deb Mon Sep 17 00:00:00 2001 From: JackDandy Date: Thu, 6 Jun 2024 12:00:00 +0100 Subject: [PATCH 06/29] =?UTF-8?q?Update=20Beautiful=20Soup=204.12.2=20(30c?= =?UTF-8?q?58a1)=20=E2=86=92=204.12.3=20(7fb5175).?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGES.md | 1 + lib/bs4/__init__.py | 4 ++-- lib/bs4/builder/__init__.py | 15 ++++++++++----- lib/bs4/builder/_html5lib.py | 4 +++- lib/bs4/builder/_lxml.py | 4 +++- lib/bs4/formatter.py | 4 ++-- 6 files changed, 21 insertions(+), 11 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 837132f3..b485a039 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,6 @@ ### 3.32.0 (2024-xx-xx xx:xx:00 UTC) +* Update Beautiful Soup 4.12.2 (30c58a1) to 4.12.3 (7fb5175) * Update CacheControl 0.13.1 (783a338) to 0.14.0 (e2be0c2) * Update filelock 3.12.4 (c1163ae) to 3.14.0 (8556141) * Update idna library 3.4 (cab054c) to 3.7 (1d365e1) diff --git a/lib/bs4/__init__.py b/lib/bs4/__init__.py index 98092923..bc081008 100644 --- a/lib/bs4/__init__.py +++ b/lib/bs4/__init__.py @@ -15,8 +15,8 @@ documentation: http://www.crummy.com/software/BeautifulSoup/bs4/doc/ """ __author__ = "Leonard Richardson (leonardr@segfault.org)" -__version__ = "4.12.2" -__copyright__ = "Copyright (c) 2004-2023 Leonard Richardson" +__version__ = "4.12.3" +__copyright__ = "Copyright (c) 2004-2024 Leonard Richardson" # Use of this source code is governed by the MIT license. __license__ = "MIT" diff --git a/lib/bs4/builder/__init__.py b/lib/bs4/builder/__init__.py index fa5017f5..e182051c 100644 --- a/lib/bs4/builder/__init__.py +++ b/lib/bs4/builder/__init__.py @@ -514,15 +514,19 @@ class DetectsXMLParsedAsHTML(object): XML_PREFIX_B = b' Date: Thu, 6 Jun 2024 12:23:53 +0100 Subject: [PATCH 07/29] =?UTF-8?q?Update=20feedparser=206.0.10=20(9865dec)?= =?UTF-8?q?=20=E2=86=92=206.0.11=20(efcb89b).?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGES.md | 1 + lib/feedparser/__init__.py | 2 +- lib/feedparser/api.py | 2 +- lib/feedparser/datetimes/rfc822.py | 4 ++++ lib/feedparser/encodings.py | 1 - 5 files changed, 7 insertions(+), 3 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index b485a039..a56715ca 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,6 +2,7 @@ * Update Beautiful Soup 4.12.2 (30c58a1) to 4.12.3 (7fb5175) * Update CacheControl 0.13.1 (783a338) to 0.14.0 (e2be0c2) +* Update feedparser 6.0.10 (9865dec) to 6.0.11 (efcb89b) * Update filelock 3.12.4 (c1163ae) to 3.14.0 (8556141) * Update idna library 3.4 (cab054c) to 3.7 (1d365e1) * Update Requests library 2.31.0 (8812812) to 2.32.3 (0e322af) diff --git a/lib/feedparser/__init__.py b/lib/feedparser/__init__.py index bdf8060c..9fa58e96 100644 --- a/lib/feedparser/__init__.py +++ b/lib/feedparser/__init__.py @@ -38,7 +38,7 @@ from .util import FeedParserDict __author__ = "Kurt McKee " __license__ = "BSD 2-clause" -__version__ = "6.0.10" +__version__ = "6.0.11" # HTTP "User-Agent" header to send to servers when downloading feeds. # If you are embedding feedparser in a larger application, you should diff --git a/lib/feedparser/api.py b/lib/feedparser/api.py index fe49c56d..4de2d271 100644 --- a/lib/feedparser/api.py +++ b/lib/feedparser/api.py @@ -314,7 +314,7 @@ def _parse_file_inplace( except xml.sax.SAXNotSupportedException: pass saxparser.setContentHandler(feed_parser) - saxparser.setErrorHandler(feed_parser) + saxparser.setErrorHandler(feed_parser) # type: ignore[arg-type] source = xml.sax.xmlreader.InputSource() # If an encoding was detected, decode the file on the fly; diff --git a/lib/feedparser/datetimes/rfc822.py b/lib/feedparser/datetimes/rfc822.py index 5f300f1d..902e9894 100644 --- a/lib/feedparser/datetimes/rfc822.py +++ b/lib/feedparser/datetimes/rfc822.py @@ -91,6 +91,10 @@ def _parse_date_rfc822(date): parts.extend(("00:00:00", "0000")) # Remove the day name if parts[0][:3] in day_names: + # Comma without spaces: + # 'Fri,24 Nov 2023 18:28:36 -0000' + if "," in parts[0] and parts[0][-1] != ",": + parts.insert(1, parts[0].rpartition(",")[2]) parts = parts[1:] if len(parts) < 5: # If there are still fewer than five parts, there's not enough diff --git a/lib/feedparser/encodings.py b/lib/feedparser/encodings.py index 01f228d1..7713d7a6 100644 --- a/lib/feedparser/encodings.py +++ b/lib/feedparser/encodings.py @@ -528,7 +528,6 @@ class MissingEncoding(io.UnsupportedOperation): class StreamFactory: - """Decode on the fly a binary stream that *may* have a known encoding. If the underlying stream is seekable, it is possible to call From d8f1102e794adf90701676724384183117fc6c17 Mon Sep 17 00:00:00 2001 From: JackDandy Date: Thu, 6 Jun 2024 21:05:43 +0100 Subject: [PATCH 08/29] =?UTF-8?q?Update=20certifi=202024.02.02=20=E2=86=92?= =?UTF-8?q?=202024.06.02?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGES.md | 1 + lib/certifi/__init__.py | 2 +- lib/certifi/cacert.pem | 24 ++++++++++++++++++++++++ 3 files changed, 26 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index a56715ca..1466859b 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,6 +2,7 @@ * Update Beautiful Soup 4.12.2 (30c58a1) to 4.12.3 (7fb5175) * Update CacheControl 0.13.1 (783a338) to 0.14.0 (e2be0c2) +* Update certifi 2024.02.02 to 2024.06.02 * Update feedparser 6.0.10 (9865dec) to 6.0.11 (efcb89b) * Update filelock 3.12.4 (c1163ae) to 3.14.0 (8556141) * Update idna library 3.4 (cab054c) to 3.7 (1d365e1) diff --git a/lib/certifi/__init__.py b/lib/certifi/__init__.py index 1c91f3ec..62b27b63 100644 --- a/lib/certifi/__init__.py +++ b/lib/certifi/__init__.py @@ -1,4 +1,4 @@ from .core import contents, where __all__ = ["contents", "where"] -__version__ = "2024.02.02" +__version__ = "2024.06.02" diff --git a/lib/certifi/cacert.pem b/lib/certifi/cacert.pem index fac3c319..6e1f1a67 100644 --- a/lib/certifi/cacert.pem +++ b/lib/certifi/cacert.pem @@ -4812,3 +4812,27 @@ X273CXE2whJdV/LItM3z7gLfEdxquVeEHVlNjM7IDiPCtyaaEBRx/pOyiriA8A4Q ntOoUAw3gi/q4Iqd4Sw5/7W0cwDk90imc6y/st53BIe0o82bNSQ3+pCTE4FCxpgm dTdmQRCsu/WU48IxK63nI1bMNSWSs1A= -----END CERTIFICATE----- + +# Issuer: CN=FIRMAPROFESIONAL CA ROOT-A WEB O=Firmaprofesional SA +# Subject: CN=FIRMAPROFESIONAL CA ROOT-A WEB O=Firmaprofesional SA +# Label: "FIRMAPROFESIONAL CA ROOT-A WEB" +# Serial: 65916896770016886708751106294915943533 +# MD5 Fingerprint: 82:b2:ad:45:00:82:b0:66:63:f8:5f:c3:67:4e:ce:a3 +# SHA1 Fingerprint: a8:31:11:74:a6:14:15:0d:ca:77:dd:0e:e4:0c:5d:58:fc:a0:72:a5 +# SHA256 Fingerprint: be:f2:56:da:f2:6e:9c:69:bd:ec:16:02:35:97:98:f3:ca:f7:18:21:a0:3e:01:82:57:c5:3c:65:61:7f:3d:4a +-----BEGIN CERTIFICATE----- +MIICejCCAgCgAwIBAgIQMZch7a+JQn81QYehZ1ZMbTAKBggqhkjOPQQDAzBuMQsw +CQYDVQQGEwJFUzEcMBoGA1UECgwTRmlybWFwcm9mZXNpb25hbCBTQTEYMBYGA1UE +YQwPVkFURVMtQTYyNjM0MDY4MScwJQYDVQQDDB5GSVJNQVBST0ZFU0lPTkFMIENB +IFJPT1QtQSBXRUIwHhcNMjIwNDA2MDkwMTM2WhcNNDcwMzMxMDkwMTM2WjBuMQsw +CQYDVQQGEwJFUzEcMBoGA1UECgwTRmlybWFwcm9mZXNpb25hbCBTQTEYMBYGA1UE +YQwPVkFURVMtQTYyNjM0MDY4MScwJQYDVQQDDB5GSVJNQVBST0ZFU0lPTkFMIENB +IFJPT1QtQSBXRUIwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAARHU+osEaR3xyrq89Zf +e9MEkVz6iMYiuYMQYneEMy3pA4jU4DP37XcsSmDq5G+tbbT4TIqk5B/K6k84Si6C +cyvHZpsKjECcfIr28jlgst7L7Ljkb+qbXbdTkBgyVcUgt5SjYzBhMA8GA1UdEwEB +/wQFMAMBAf8wHwYDVR0jBBgwFoAUk+FDY1w8ndYn81LsF7Kpryz3dvgwHQYDVR0O +BBYEFJPhQ2NcPJ3WJ/NS7Beyqa8s93b4MA4GA1UdDwEB/wQEAwIBBjAKBggqhkjO +PQQDAwNoADBlAjAdfKR7w4l1M+E7qUW/Runpod3JIha3RxEL2Jq68cgLcFBTApFw +hVmpHqTm6iMxoAACMQD94vizrxa5HnPEluPBMBnYfubDl94cT7iJLzPrSA8Z94dG +XSaQpYXFuXqUPoeovQA= +-----END CERTIFICATE----- From cdeb70223220deb51a6e0d938f9ee11b8e35de06 Mon Sep 17 00:00:00 2001 From: JackDandy Date: Thu, 6 Jun 2024 21:17:05 +0100 Subject: [PATCH 09/29] =?UTF-8?q?Update=20Tornado=20Web=20Server=206.4=20(?= =?UTF-8?q?b3f2a4b)=20=E2=86=92=206.4.1=20(2a0e1d1).?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGES.md | 1 + lib/tornado/__init__.py | 4 +- lib/tornado/concurrent.py | 1 + lib/tornado/curl_httpclient.py | 20 ++++++---- lib/tornado/gen.py | 7 ++-- lib/tornado/http1connection.py | 63 +++++++++++++++++++++----------- lib/tornado/httputil.py | 7 +++- lib/tornado/iostream.py | 2 +- lib/tornado/simple_httpclient.py | 6 +-- lib/tornado/testing.py | 63 ++++++++++++-------------------- lib/tornado/websocket.py | 6 +-- 11 files changed, 97 insertions(+), 83 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 1466859b..78f56113 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -7,6 +7,7 @@ * Update filelock 3.12.4 (c1163ae) to 3.14.0 (8556141) * Update idna library 3.4 (cab054c) to 3.7 (1d365e1) * Update Requests library 2.31.0 (8812812) to 2.32.3 (0e322af) +* Update Tornado Web Server 6.4 (b3f2a4b) to 6.4.1 (2a0e1d1) * Update urllib3 2.0.7 (56f01e0) to 2.2.1 (54d6edf) diff --git a/lib/tornado/__init__.py b/lib/tornado/__init__.py index a0ae714d..f542de35 100644 --- a/lib/tornado/__init__.py +++ b/lib/tornado/__init__.py @@ -22,8 +22,8 @@ # is zero for an official release, positive for a development branch, # or negative for a release candidate or beta (after the base version # number has been incremented) -version = "6.4" -version_info = (6, 4, 0, 0) +version = "6.4.1" +version_info = (6, 4, 0, 1) import importlib import typing diff --git a/lib/tornado/concurrent.py b/lib/tornado/concurrent.py index 86bbd703..5047c538 100644 --- a/lib/tornado/concurrent.py +++ b/lib/tornado/concurrent.py @@ -118,6 +118,7 @@ def run_on_executor(*args: Any, **kwargs: Any) -> Callable: The ``callback`` argument was removed. """ + # Fully type-checking decorators is tricky, and this one is # discouraged anyway so it doesn't have all the generic magic. def run_on_executor_decorator(fn: Callable) -> Callable[..., Future]: diff --git a/lib/tornado/curl_httpclient.py b/lib/tornado/curl_httpclient.py index 23320e48..397c3a97 100644 --- a/lib/tornado/curl_httpclient.py +++ b/lib/tornado/curl_httpclient.py @@ -19,6 +19,7 @@ import collections import functools import logging import pycurl +import re import threading import time from io import BytesIO @@ -44,6 +45,8 @@ if typing.TYPE_CHECKING: curl_log = logging.getLogger("tornado.curl_httpclient") +CR_OR_LF_RE = re.compile(b"\r|\n") + class CurlAsyncHTTPClient(AsyncHTTPClient): def initialize( # type: ignore @@ -347,14 +350,15 @@ class CurlAsyncHTTPClient(AsyncHTTPClient): if "Pragma" not in request.headers: request.headers["Pragma"] = "" - curl.setopt( - pycurl.HTTPHEADER, - [ - b"%s: %s" - % (native_str(k).encode("ASCII"), native_str(v).encode("ISO8859-1")) - for k, v in request.headers.get_all() - ], - ) + encoded_headers = [ + b"%s: %s" + % (native_str(k).encode("ASCII"), native_str(v).encode("ISO8859-1")) + for k, v in request.headers.get_all() + ] + for line in encoded_headers: + if CR_OR_LF_RE.search(line): + raise ValueError("Illegal characters in header (CR or LF): %r" % line) + curl.setopt(pycurl.HTTPHEADER, encoded_headers) curl.setopt( pycurl.HEADERFUNCTION, diff --git a/lib/tornado/gen.py b/lib/tornado/gen.py index dab4fd09..0e3c7a6f 100644 --- a/lib/tornado/gen.py +++ b/lib/tornado/gen.py @@ -66,6 +66,7 @@ function to extend this mechanism. via ``singledispatch``. """ + import asyncio import builtins import collections @@ -165,13 +166,11 @@ def _fake_ctx_run(f: Callable[..., _T], *args: Any, **kw: Any) -> _T: @overload def coroutine( func: Callable[..., "Generator[Any, Any, _T]"] -) -> Callable[..., "Future[_T]"]: - ... +) -> Callable[..., "Future[_T]"]: ... @overload -def coroutine(func: Callable[..., _T]) -> Callable[..., "Future[_T]"]: - ... +def coroutine(func: Callable[..., _T]) -> Callable[..., "Future[_T]"]: ... def coroutine( diff --git a/lib/tornado/http1connection.py b/lib/tornado/http1connection.py index 96326991..5f51169d 100644 --- a/lib/tornado/http1connection.py +++ b/lib/tornado/http1connection.py @@ -38,6 +38,8 @@ from tornado.util import GzipDecompressor from typing import cast, Optional, Type, Awaitable, Callable, Union, Tuple +CR_OR_LF_RE = re.compile(b"\r|\n") + class _QuietException(Exception): def __init__(self) -> None: @@ -389,14 +391,11 @@ class HTTP1Connection(httputil.HTTPConnection): self._request_start_line = start_line lines.append(utf8("%s %s HTTP/1.1" % (start_line[0], start_line[1]))) # Client requests with a non-empty body must have either a - # Content-Length or a Transfer-Encoding. + # Content-Length or a Transfer-Encoding. If Content-Length is not + # present we'll add our Transfer-Encoding below. self._chunking_output = ( start_line.method in ("POST", "PUT", "PATCH") and "Content-Length" not in headers - and ( - "Transfer-Encoding" not in headers - or headers["Transfer-Encoding"] == "chunked" - ) ) else: assert isinstance(start_line, httputil.ResponseStartLine) @@ -418,9 +417,6 @@ class HTTP1Connection(httputil.HTTPConnection): and (start_line.code < 100 or start_line.code >= 200) # No need to chunk the output if a Content-Length is specified. and "Content-Length" not in headers - # Applications are discouraged from touching Transfer-Encoding, - # but if they do, leave it alone. - and "Transfer-Encoding" not in headers ) # If connection to a 1.1 client will be closed, inform client if ( @@ -453,8 +449,8 @@ class HTTP1Connection(httputil.HTTPConnection): ) lines.extend(line.encode("latin1") for line in header_lines) for line in lines: - if b"\n" in line: - raise ValueError("Newline in header: " + repr(line)) + if CR_OR_LF_RE.search(line): + raise ValueError("Illegal characters (CR or LF) in header: %r" % line) future = None if self.stream.closed(): future = self._write_future = Future() @@ -560,7 +556,7 @@ class HTTP1Connection(httputil.HTTPConnection): return connection_header != "close" elif ( "Content-Length" in headers - or headers.get("Transfer-Encoding", "").lower() == "chunked" + or is_transfer_encoding_chunked(headers) or getattr(start_line, "method", None) in ("HEAD", "GET") ): # start_line may be a request or response start line; only @@ -598,13 +594,6 @@ class HTTP1Connection(httputil.HTTPConnection): delegate: httputil.HTTPMessageDelegate, ) -> Optional[Awaitable[None]]: if "Content-Length" in headers: - if "Transfer-Encoding" in headers: - # Response cannot contain both Content-Length and - # Transfer-Encoding headers. - # http://tools.ietf.org/html/rfc7230#section-3.3.3 - raise httputil.HTTPInputError( - "Response with both Transfer-Encoding and Content-Length" - ) if "," in headers["Content-Length"]: # Proxies sometimes cause Content-Length headers to get # duplicated. If all the values are identical then we can @@ -631,20 +620,22 @@ class HTTP1Connection(httputil.HTTPConnection): else: content_length = None + is_chunked = is_transfer_encoding_chunked(headers) + if code == 204: # This response code is not allowed to have a non-empty body, # and has an implicit length of zero instead of read-until-close. # http://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.3 - if "Transfer-Encoding" in headers or content_length not in (None, 0): + if is_chunked or content_length not in (None, 0): raise httputil.HTTPInputError( "Response with code %d should not have body" % code ) content_length = 0 + if is_chunked: + return self._read_chunked_body(delegate) if content_length is not None: return self._read_fixed_body(content_length, delegate) - if headers.get("Transfer-Encoding", "").lower() == "chunked": - return self._read_chunked_body(delegate) if self.is_client: return self._read_body_until_close(delegate) return None @@ -867,3 +858,33 @@ def parse_hex_int(s: str) -> int: if HEXDIGITS.fullmatch(s) is None: raise ValueError("not a hexadecimal integer: %r" % s) return int(s, 16) + + +def is_transfer_encoding_chunked(headers: httputil.HTTPHeaders) -> bool: + """Returns true if the headers specify Transfer-Encoding: chunked. + + Raise httputil.HTTPInputError if any other transfer encoding is used. + """ + # Note that transfer-encoding is an area in which postel's law can lead + # us astray. If a proxy and a backend server are liberal in what they accept, + # but accept slightly different things, this can lead to mismatched framing + # and request smuggling issues. Therefore we are as strict as possible here + # (even technically going beyond the requirements of the RFCs: a value of + # ",chunked" is legal but doesn't appear in practice for legitimate traffic) + if "Transfer-Encoding" not in headers: + return False + if "Content-Length" in headers: + # Message cannot contain both Content-Length and + # Transfer-Encoding headers. + # http://tools.ietf.org/html/rfc7230#section-3.3.3 + raise httputil.HTTPInputError( + "Message with both Transfer-Encoding and Content-Length" + ) + if headers["Transfer-Encoding"].lower() == "chunked": + return True + # We do not support any transfer-encodings other than chunked, and we do not + # expect to add any support because the concept of transfer-encoding has + # been removed in HTTP/2. + raise httputil.HTTPInputError( + "Unsupported Transfer-Encoding %s" % headers["Transfer-Encoding"] + ) diff --git a/lib/tornado/httputil.py b/lib/tornado/httputil.py index b21d8046..9ce992d8 100644 --- a/lib/tornado/httputil.py +++ b/lib/tornado/httputil.py @@ -62,6 +62,9 @@ if typing.TYPE_CHECKING: from asyncio import Future # noqa: F401 import unittest # noqa: F401 +# To be used with str.strip() and related methods. +HTTP_WHITESPACE = " \t" + @lru_cache(1000) def _normalize_header(name: str) -> str: @@ -171,7 +174,7 @@ class HTTPHeaders(collections.abc.MutableMapping): # continuation of a multi-line header if self._last_key is None: raise HTTPInputError("first header line cannot start with whitespace") - new_part = " " + line.lstrip() + new_part = " " + line.lstrip(HTTP_WHITESPACE) self._as_list[self._last_key][-1] += new_part self._dict[self._last_key] += new_part else: @@ -179,7 +182,7 @@ class HTTPHeaders(collections.abc.MutableMapping): name, value = line.split(":", 1) except ValueError: raise HTTPInputError("no colon in header line") - self.add(name, value.strip()) + self.add(name, value.strip(HTTP_WHITESPACE)) @classmethod def parse(cls, headers: str) -> "HTTPHeaders": diff --git a/lib/tornado/iostream.py b/lib/tornado/iostream.py index 6cee1fe4..aea7e891 100644 --- a/lib/tornado/iostream.py +++ b/lib/tornado/iostream.py @@ -1376,7 +1376,7 @@ class SSLIOStream(IOStream): return elif err.args[0] in (ssl.SSL_ERROR_EOF, ssl.SSL_ERROR_ZERO_RETURN): return self.close(exc_info=err) - elif err.args[0] == ssl.SSL_ERROR_SSL: + elif err.args[0] in (ssl.SSL_ERROR_SSL, ssl.SSL_ERROR_SYSCALL): try: peer = self.socket.getpeername() except Exception: diff --git a/lib/tornado/simple_httpclient.py b/lib/tornado/simple_httpclient.py index 2460863f..5b2d4dcd 100644 --- a/lib/tornado/simple_httpclient.py +++ b/lib/tornado/simple_httpclient.py @@ -429,9 +429,9 @@ class _HTTPConnection(httputil.HTTPMessageDelegate): self.request.method == "POST" and "Content-Type" not in self.request.headers ): - self.request.headers[ - "Content-Type" - ] = "application/x-www-form-urlencoded" + self.request.headers["Content-Type"] = ( + "application/x-www-form-urlencoded" + ) if self.request.decompress_response: self.request.headers["Accept-Encoding"] = "gzip" req_path = (self.parsed.path or "/") + ( diff --git a/lib/tornado/testing.py b/lib/tornado/testing.py index bdbff87b..4c33b3e2 100644 --- a/lib/tornado/testing.py +++ b/lib/tornado/testing.py @@ -84,39 +84,6 @@ def get_async_test_timeout() -> float: return 5 -class _TestMethodWrapper(object): - """Wraps a test method to raise an error if it returns a value. - - This is mainly used to detect undecorated generators (if a test - method yields it must use a decorator to consume the generator), - but will also detect other kinds of return values (these are not - necessarily errors, but we alert anyway since there is no good - reason to return a value from a test). - """ - - def __init__(self, orig_method: Callable) -> None: - self.orig_method = orig_method - self.__wrapped__ = orig_method - - def __call__(self, *args: Any, **kwargs: Any) -> None: - result = self.orig_method(*args, **kwargs) - if isinstance(result, Generator) or inspect.iscoroutine(result): - raise TypeError( - "Generator and coroutine test methods should be" - " decorated with tornado.testing.gen_test" - ) - elif result is not None: - raise ValueError("Return value from test method ignored: %r" % result) - - def __getattr__(self, name: str) -> Any: - """Proxy all unknown attributes to the original method. - - This is important for some of the decorators in the `unittest` - module, such as `unittest.skipIf`. - """ - return getattr(self.orig_method, name) - - class AsyncTestCase(unittest.TestCase): """`~unittest.TestCase` subclass for testing `.IOLoop`-based asynchronous code. @@ -173,12 +140,6 @@ class AsyncTestCase(unittest.TestCase): self.__stop_args = None # type: Any self.__timeout = None # type: Optional[object] - # It's easy to forget the @gen_test decorator, but if you do - # the test will silently be ignored because nothing will consume - # the generator. Replace the test method with a wrapper that will - # make sure it's not an undecorated generator. - setattr(self, methodName, _TestMethodWrapper(getattr(self, methodName))) - # Not used in this class itself, but used by @gen_test self._test_generator = None # type: Optional[Union[Generator, Coroutine]] @@ -289,6 +250,30 @@ class AsyncTestCase(unittest.TestCase): self.__rethrow() return ret + def _callTestMethod(self, method: Callable) -> None: + """Run the given test method, raising an error if it returns non-None. + + Failure to decorate asynchronous test methods with ``@gen_test`` can lead to tests + incorrectly passing. + + Remove this override when Python 3.10 support is dropped. This check (in the form of a + DeprecationWarning) became a part of the standard library in 3.11. + + Note that ``_callTestMethod`` is not documented as a public interface. However, it is + present in all supported versions of Python (3.8+), and if it goes away in the future that's + OK because we can just remove this override as noted above. + """ + # Calling super()._callTestMethod would hide the return value, even in python 3.8-3.10 + # where the check isn't being done for us. + result = method() + if isinstance(result, Generator) or inspect.iscoroutine(result): + raise TypeError( + "Generator and coroutine test methods should be" + " decorated with tornado.testing.gen_test" + ) + elif result is not None: + raise ValueError("Return value from test method ignored: %r" % result) + def stop(self, _arg: Any = None, **kwargs: Any) -> None: """Stops the `.IOLoop`, causing one pending (or future) call to `wait()` to return. diff --git a/lib/tornado/websocket.py b/lib/tornado/websocket.py index fbfd7008..8f0e0aef 100644 --- a/lib/tornado/websocket.py +++ b/lib/tornado/websocket.py @@ -1392,9 +1392,9 @@ class WebSocketClientConnection(simple_httpclient._HTTPConnection): # from the server). # TODO: set server parameters for deflate extension # if requested in self.compression_options. - request.headers[ - "Sec-WebSocket-Extensions" - ] = "permessage-deflate; client_max_window_bits" + request.headers["Sec-WebSocket-Extensions"] = ( + "permessage-deflate; client_max_window_bits" + ) # Websocket connection is currently unable to follow redirects request.follow_redirects = False From c645ff464fd6f66d8eace2325404b6706fc386b3 Mon Sep 17 00:00:00 2001 From: JackDandy Date: Fri, 7 Jun 2024 11:46:29 +0100 Subject: [PATCH 10/29] =?UTF-8?q?Update=20dateutil=202.8.2=20(296d419)=20?= =?UTF-8?q?=E2=86=92=202.9.0.post0=20(0353b78).?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGES.md | 1 + lib/dateutil/zoneinfo/__init__.py | 8 +++----- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 78f56113..f0ab1c47 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -3,6 +3,7 @@ * Update Beautiful Soup 4.12.2 (30c58a1) to 4.12.3 (7fb5175) * Update CacheControl 0.13.1 (783a338) to 0.14.0 (e2be0c2) * Update certifi 2024.02.02 to 2024.06.02 +* Update dateutil 2.8.2 (296d419) to 2.9.0.post0 (0353b78) * Update feedparser 6.0.10 (9865dec) to 6.0.11 (efcb89b) * Update filelock 3.12.4 (c1163ae) to 3.14.0 (8556141) * Update idna library 3.4 (cab054c) to 3.7 (1d365e1) diff --git a/lib/dateutil/zoneinfo/__init__.py b/lib/dateutil/zoneinfo/__init__.py index 5628bfc3..27cc9df4 100644 --- a/lib/dateutil/zoneinfo/__init__.py +++ b/lib/dateutil/zoneinfo/__init__.py @@ -9,8 +9,6 @@ from io import BytesIO from dateutil.tz import tzfile as _tzfile -# noinspection PyPep8Naming -import encodingKludge as ek import sickgear __all__ = ["get_zonefile_instance", "gettz", "gettz_db_metadata"] @@ -27,10 +25,10 @@ class tzfile(_tzfile): def getzoneinfofile_stream(): try: # return BytesIO(get_data(__name__, ZONEFILENAME)) - zonefile = ek.ek(os.path.join, sickgear.ZONEINFO_DIR, ZONEFILENAME) - if not ek.ek(os.path.isfile, zonefile): + zonefile = os.path.join(sickgear.ZONEINFO_DIR, ZONEFILENAME) + if not os.path.isfile(zonefile): warnings.warn('Falling back to included zoneinfo file') - zonefile = ek.ek(os.path.join, ek.ek(os.path.dirname, __file__), ZONEFILENAME) + zonefile = os.path.join(os.path.dirname(__file__), ZONEFILENAME) with open(zonefile, 'rb') as f: return BytesIO(f.read()) except IOError as e: # TODO switch to FileNotFoundError? From 909fd3d24e3d6e29cb6f809c8135fca6c2c71406 Mon Sep 17 00:00:00 2001 From: JackDandy Date: Fri, 7 Jun 2024 13:27:06 +0100 Subject: [PATCH 11/29] =?UTF-8?q?Update=20imdbpie=205.6.4=20(f695e87)=20?= =?UTF-8?q?=E2=86=92=205.6.5=20(f8ed7a0).?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGES.md | 1 + lib/imdbpie/auth.py | 32 +++++++----- lib/imdbpie/facade.py | 111 +++++++++++++++++++++++++---------------- lib/imdbpie/imdbpie.py | 67 +++++++++++++------------ lib/imdbpie/objects.py | 5 +- 5 files changed, 124 insertions(+), 92 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index f0ab1c47..d6191765 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -7,6 +7,7 @@ * Update feedparser 6.0.10 (9865dec) to 6.0.11 (efcb89b) * Update filelock 3.12.4 (c1163ae) to 3.14.0 (8556141) * Update idna library 3.4 (cab054c) to 3.7 (1d365e1) +* Update imdbpie 5.6.4 (f695e87) to 5.6.5 (f8ed7a0) * Update Requests library 2.31.0 (8812812) to 2.32.3 (0e322af) * Update Tornado Web Server 6.4 (b3f2a4b) to 6.4.1 (2a0e1d1) * Update urllib3 2.0.7 (56f01e0) to 2.2.1 (54d6edf) diff --git a/lib/imdbpie/auth.py b/lib/imdbpie/auth.py index d6e802ce..a7194da8 100644 --- a/lib/imdbpie/auth.py +++ b/lib/imdbpie/auth.py @@ -50,7 +50,7 @@ class ZuluHmacAuthV3HTTPHandler(object): them into a string, separated by newlines. """ vals = sorted(['%s:%s' % (n.lower().strip(), - headers_to_sign[n].strip()) for n in headers_to_sign]) + headers_to_sign[n].strip()) for n in headers_to_sign]) return '\n'.join(vals) def headers_to_sign(self, http_request): @@ -90,14 +90,16 @@ class ZuluHmacAuthV3HTTPHandler(object): headers_to_sign = self.headers_to_sign(http_request) canonical_qs = self.canonical_query_string(http_request) canonical_headers = self.canonical_headers(headers_to_sign) - string_to_sign = '\n'.join(( - http_request.method, - http_request.path, - canonical_qs, - canonical_headers, - '', - http_request.body - )) + string_to_sign = '\n'.join( + ( + http_request.method, + http_request.path, + canonical_qs, + canonical_headers, + '', + http_request.body, + ) + ) return string_to_sign, headers_to_sign def add_auth(self, req): @@ -319,9 +321,15 @@ class Auth(object): key: val[0] for key, val in parse_qs(parsed_url.query).items() } request = HTTPRequest( - method='GET', protocol='https', host=HOST, - port=443, path=parsed_url.path, auth_path=None, params=params, - headers={'User-Agent': USER_AGENT}, body='' + method='GET', + protocol='https', + host=HOST, + port=443, + path=parsed_url.path, + auth_path=None, + params=params, + headers={'User-Agent': USER_AGENT}, + body='', ) handler.add_auth(req=request) headers = request.headers diff --git a/lib/imdbpie/facade.py b/lib/imdbpie/facade.py index 57782f32..6bdfa397 100644 --- a/lib/imdbpie/facade.py +++ b/lib/imdbpie/facade.py @@ -7,15 +7,20 @@ from dateutil.parser import parse from .imdbpie import Imdb from .objects import ( - Title, TitleEpisodes, Name, TitleName, Image, TitleRelease, - TitleSearchResult, NameSearchResult, + Title, + TitleEpisodes, + Name, + TitleName, + Image, + TitleRelease, + TitleSearchResult, + NameSearchResult, ) REGEX_IMDB_ID = re.compile(r'([a-zA-Z]{2}[0-9]{7})') class ImdbFacade(object): - def __init__(self, client=None): self._client = client or Imdb() @@ -36,8 +41,11 @@ class ImdbFacade(object): season = None episode = None return Title( - season=season, episode=episode, episodes=episodes, - runtime=runtime, **title_data + season=season, + episode=episode, + episodes=episodes, + runtime=runtime, + **title_data ) def get_name(self, imdb_id): @@ -68,8 +76,13 @@ class ImdbFacade(object): self._parse_id(f['id']) for f in filmography_data['filmography'] ) return Name( - name=name, imdb_id=imdb_id, date_of_birth=date_of_birth, - gender=gender, birth_place=birth_place, bios=bios, image=image, + name=name, + imdb_id=imdb_id, + date_of_birth=date_of_birth, + gender=gender, + birth_place=birth_place, + bios=bios, + image=image, filmography=filmography, ) @@ -77,7 +90,8 @@ class ImdbFacade(object): results = [] for result in self._client.search_for_name(query): result = NameSearchResult( - imdb_id=result['imdb_id'], name=result['name'], + imdb_id=result['imdb_id'], + name=result['name'], ) results.append(result) return tuple(results) @@ -90,8 +104,10 @@ class ImdbFacade(object): else: year = None result = TitleSearchResult( - imdb_id=result['imdb_id'], title=result['title'], - type=result['type'], year=year, + imdb_id=result['imdb_id'], + title=result['title'], + type=result['type'], + year=year, ) results.append(result) return tuple(results) @@ -102,8 +118,9 @@ class ImdbFacade(object): name=i['name'], job=i.get('job'), category=i.get('category'), - imdb_id=self._parse_id(i['id']) - ) for i in top_crew_data['writers'] + imdb_id=self._parse_id(i['id']), + ) + for i in top_crew_data['writers'] ) def _get_stars(self, principals_data): @@ -113,8 +130,9 @@ class ImdbFacade(object): job=i.get('job'), characters=tuple(i.get('characters', ())), category=i.get('category'), - imdb_id=self._parse_id(i['id']) - ) for i in principals_data + imdb_id=self._parse_id(i['id']), + ) + for i in principals_data ) def _get_creators(self, top_crew_data): @@ -123,8 +141,9 @@ class ImdbFacade(object): name=i['name'], job=i.get('job'), category=i.get('category'), - imdb_id=self._parse_id(i['id']) - ) for i in top_crew_data['writers'] + imdb_id=self._parse_id(i['id']), + ) + for i in top_crew_data['writers'] if i.get('job') == 'creator' ) @@ -134,20 +153,23 @@ class ImdbFacade(object): name=i['name'], job=i.get('job'), category=i.get('category'), - imdb_id=self._parse_id(i['id']) - ) for i in top_crew_data['directors'] + imdb_id=self._parse_id(i['id']), + ) + for i in top_crew_data['directors'] ) def _get_credits(self, credits_data): credits = [] for category in credits_data.get('credits', ()): for item in credits_data['credits'][category]: - credits.append(TitleName( - name=item['name'], - category=item.get('category'), - job=item.get('job'), - imdb_id=self._parse_id(item['id']) - )) + credits.append( + TitleName( + name=item['name'], + category=item.get('category'), + job=item.get('job'), + imdb_id=self._parse_id(item['id']), + ) + ) return tuple(credits) def _parse_id(self, string): @@ -212,22 +234,25 @@ class ImdbFacade(object): ) except KeyError: image = None - return dict( - imdb_id=imdb_id, - title=title, - year=year, - rating=rating, - type=type_, - release_date=release_date, - releases=releases, - plot_outline=plot_outline, - rating_count=rating_count, - writers=writers, - directors=directors, - creators=creators, - genres=genres, - credits=credits, - certification=certification, - image=image, - stars=stars, - ), title_aux_data + return ( + dict( + imdb_id=imdb_id, + title=title, + year=year, + rating=rating, + type=type_, + release_date=release_date, + releases=releases, + plot_outline=plot_outline, + rating_count=rating_count, + writers=writers, + directors=directors, + creators=creators, + genres=genres, + credits=credits, + certification=certification, + image=image, + stars=stars, + ), + title_aux_data, + ) diff --git a/lib/imdbpie/imdbpie.py b/lib/imdbpie/imdbpie.py index b268e643..397de2a5 100644 --- a/lib/imdbpie/imdbpie.py +++ b/lib/imdbpie/imdbpie.py @@ -11,7 +11,7 @@ from trans import trans import requests from six import text_type from six.moves import http_client as httplib -from six.moves.urllib.parse import urlencode, urljoin, quote, unquote +from six.moves.urllib.parse import urlencode, urljoin, quote from .constants import BASE_URI, SEARCH_BASE_URI from .auth import Auth @@ -52,7 +52,6 @@ _SIMPLE_GET_ENDPOINTS = { class Imdb(Auth): - def __init__(self, locale=None, exclude_episodes=False, session=None, cachedir=None): self.locale = locale or 'en_US' self.region = self.locale.split('_')[-1].upper() @@ -87,8 +86,8 @@ class Imdb(Auth): self._title_not_found() if ( - self.exclude_episodes is True and - resource['base']['titleType'] == 'tvEpisode' + self.exclude_episodes is True + and resource['base']['titleType'] == 'tvEpisode' ): raise LookupError( 'Title not found. Title was an episode and ' @@ -111,14 +110,14 @@ class Imdb(Auth): 'region': self.region, 'tconst': imdb_id, 'today': date.today().strftime('%Y-%m-%d'), - } + }, ) except LookupError: self._title_not_found() if ( - self.exclude_episodes is True and - resource['titleType'].lower() == 'tvepisode' + self.exclude_episodes is True + and resource['titleType'].lower() == 'tvepisode' ): raise LookupError( 'Title not found. Title was an episode and ' @@ -128,19 +127,24 @@ class Imdb(Auth): def _simple_get_method(self, method, path): """Return client method generated from ``_SIMPLE_GET_ENDPOINTS``.""" + def get(imdb_id): logger.info('called %s %s', method, imdb_id) self.validate_imdb_id(imdb_id) self._redirection_title_check(imdb_id) return self._get_resource(path.format(imdb_id=imdb_id)) + return get def title_exists(self, imdb_id): self.validate_imdb_id(imdb_id) page_url = 'https://www.imdb.com/title/{0}/'.format(imdb_id) - response = self.session.get(page_url, allow_redirects=False) - + response = self.session.get( + page_url, + allow_redirects=False, + headers={'User-Agent': 'Mozilla/5.0'}, + ) if response.status_code == httplib.OK: return True elif response.status_code == httplib.NOT_FOUND: @@ -165,7 +169,7 @@ class Imdb(Auth): def search_for_name(self, name): logger.info('called search_for_name %s', name) - name = re.sub(r'\W+', '_', name, flags=re.UNICODE).strip('_') + name = re.sub(r'\W+', '+', name).strip('+') search_results = self._suggest_search(name) results = [] for result in search_results.get('d', ()): @@ -181,7 +185,7 @@ class Imdb(Auth): def search_for_title(self, title): logger.info('called search_for_title %s', title) - title = re.sub(r'\W+', '_', title, flags=re.UNICODE).strip('_') + title = re.sub(r'\W+', '+', title).strip('+') search_results = self._suggest_search(title) results = [] for result in search_results.get('d', ()): @@ -238,9 +242,13 @@ class Imdb(Auth): if region: params.update({'region': region}) - return self._get(urljoin( - BASE_URI, '/template/imdb-ios-writable/tv-episodes-v2.jstl/render' - ), params=params) + return self._get( + urljoin( + BASE_URI, + '/template/imdb-ios-writable/tv-episodes-v2.jstl/render', + ), + params=params, + ) def get_title_top_crew(self, imdb_id): """ @@ -252,24 +260,21 @@ class Imdb(Auth): logger.info('called get_title_top_crew %s', imdb_id) self.validate_imdb_id(imdb_id) params = {'tconst': imdb_id} - return self._get(urljoin( - BASE_URI, - '/template/imdb-android-writable/7.3.top-crew.jstl/render' - ), params=params) + return self._get( + urljoin( + BASE_URI, + '/template/imdb-android-writable/7.3.top-crew.jstl/render', + ), + params=params, + ) @staticmethod def _parse_dirty_json(data, query=None): if query is None: match_json_within_dirty_json = r'imdb\$.+\({1}(.+)\){1}' else: - query_match = ''.join( - char if char.isalnum() else '[{0}]'.format(char) - for char in unquote(query) - ) - query_match = query_match.replace('[ ]', '.+') - match_json_within_dirty_json = ( - r'imdb\${}\((.+)\)'.format(query_match) - ) + # No need to unquote as the json is containing quoted query + match_json_within_dirty_json = r'imdb\${}\((.+)\)'.format(query) data_clean = re.match( match_json_within_dirty_json, data, re.IGNORECASE ).groups()[0] @@ -290,9 +295,8 @@ class Imdb(Auth): Redirection results have no information of use. """ imdb_id = response['data'].get('tconst') - if ( - imdb_id and - imdb_id != response['data'].get('news', {}).get('channel') + if imdb_id and imdb_id != response['data'].get('news', {}).get( + 'channel' ): return True return False @@ -309,7 +313,6 @@ class Imdb(Auth): full_url = url headers.update(self.get_auth_headers(full_url)) resp = self.session.get(url, headers=headers, params=params) - if not resp.ok: if resp.status_code == httplib.NOT_FOUND: raise LookupError('Resource {0} not found'.format(url)) @@ -320,9 +323,7 @@ class Imdb(Auth): try: resp_dict = json.loads(resp_data) except ValueError: - resp_dict = self._parse_dirty_json( - data=resp_data, query=query - ) + resp_dict = self._parse_dirty_json(data=resp_data, query=query) if resp_dict.get('error'): return None diff --git a/lib/imdbpie/objects.py b/lib/imdbpie/objects.py index bb3e51a3..d933f111 100644 --- a/lib/imdbpie/objects.py +++ b/lib/imdbpie/objects.py @@ -9,12 +9,9 @@ class Image(object): class TitleEpisodes(object): - def __init__(self, facade, imdb_id): self._facade = facade - episodes = self._facade._client.get_title_episodes( - imdb_id=imdb_id - ) + episodes = self._facade._client.get_title_episodes(imdb_id=imdb_id) self._episode_imdb_ids = [] for season in episodes['seasons']: for episode in season['episodes']: From cf118e92fc6c76853f2208dcad4902ebebd707ed Mon Sep 17 00:00:00 2001 From: JackDandy Date: Fri, 7 Jun 2024 15:38:19 +0100 Subject: [PATCH 12/29] =?UTF-8?q?Update=20attr=2023.1.0=20(67e4ff2)=20?= =?UTF-8?q?=E2=86=92=2023.2.0=20(b393d79).?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGES.md | 1 + lib/attr/__init__.py | 45 +-- lib/attr/__init__.pyi | 405 ++++++---------------- lib/attr/_cmp.py | 12 +- lib/attr/_cmp.pyi | 14 +- lib/attr/_compat.py | 111 +----- lib/attr/_funcs.py | 171 +++++----- lib/attr/_make.py | 725 ++++++++++++++++++++++++---------------- lib/attr/_next_gen.py | 12 +- lib/attr/converters.py | 71 ++-- lib/attr/converters.pyi | 2 +- lib/attr/filters.pyi | 6 +- lib/attr/setters.py | 3 +- lib/attr/setters.pyi | 3 +- lib/attr/validators.py | 111 ++---- lib/attr/validators.pyi | 48 ++- 16 files changed, 738 insertions(+), 1002 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index d6191765..a21f0bb8 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,6 @@ ### 3.32.0 (2024-xx-xx xx:xx:00 UTC) +* Update attr 23.1.0 (67e4ff2) to 23.2.0 (b393d79) * Update Beautiful Soup 4.12.2 (30c58a1) to 4.12.3 (7fb5175) * Update CacheControl 0.13.1 (783a338) to 0.14.0 (e2be0c2) * Update certifi 2024.02.02 to 2024.06.02 diff --git a/lib/attr/__init__.py b/lib/attr/__init__.py index 9226258a..f2db09f7 100644 --- a/lib/attr/__init__.py +++ b/lib/attr/__init__.py @@ -79,54 +79,21 @@ def _make_getattr(mod_name: str) -> Callable: """ def __getattr__(name: str) -> str: - dunder_to_metadata = { - "__title__": "Name", - "__copyright__": "", - "__version__": "version", - "__version_info__": "version", - "__description__": "summary", - "__uri__": "", - "__url__": "", - "__author__": "", - "__email__": "", - "__license__": "license", - } - if name not in dunder_to_metadata: + if name not in ("__version__", "__version_info__"): msg = f"module {mod_name} has no attribute {name}" raise AttributeError(msg) - import sys - import warnings - - if sys.version_info < (3, 8): - from importlib_metadata import metadata - else: + try: from importlib.metadata import metadata - - if name not in ("__version__", "__version_info__"): - warnings.warn( - f"Accessing {mod_name}.{name} is deprecated and will be " - "removed in a future release. Use importlib.metadata directly " - "to query for attrs's packaging metadata.", - DeprecationWarning, - stacklevel=2, - ) + except ImportError: + from importlib_metadata import metadata meta = metadata("attrs") - if name == "__license__": - return "MIT" - if name == "__copyright__": - return "Copyright (c) 2015 Hynek Schlawack" - if name in ("__uri__", "__url__"): - return meta["Project-URL"].split(" ", 1)[-1] + if name == "__version_info__": return VersionInfo._from_version_string(meta["version"]) - if name == "__author__": - return meta["Author-email"].rsplit(" ", 1)[0] - if name == "__email__": - return meta["Author-email"].rsplit("<", 1)[1][:-1] - return meta[dunder_to_metadata[name]] + return meta["version"] return __getattr__ diff --git a/lib/attr/__init__.pyi b/lib/attr/__init__.pyi index 0f641501..14e53a8d 100644 --- a/lib/attr/__init__.pyi +++ b/lib/attr/__init__.pyi @@ -4,17 +4,11 @@ import sys from typing import ( Any, Callable, - Dict, Generic, - List, Mapping, - Optional, Protocol, Sequence, - Tuple, - Type, TypeVar, - Union, overload, ) @@ -27,6 +21,20 @@ from . import validators as validators from ._cmp import cmp_using as cmp_using from ._typing_compat import AttrsInstance_ from ._version_info import VersionInfo +from attrs import ( + define as define, + field as field, + mutable as mutable, + frozen as frozen, + _EqOrderType, + _ValidatorType, + _ConverterType, + _ReprArgType, + _OnSetAttrType, + _OnSetAttrArgType, + _FieldTransformer, + _ValidatorArgType, +) if sys.version_info >= (3, 10): from typing import TypeGuard @@ -52,23 +60,7 @@ __copyright__: str _T = TypeVar("_T") _C = TypeVar("_C", bound=type) -_EqOrderType = Union[bool, Callable[[Any], Any]] -_ValidatorType = Callable[[Any, "Attribute[_T]", _T], Any] -_ConverterType = Callable[[Any], Any] _FilterType = Callable[["Attribute[_T]", _T], bool] -_ReprType = Callable[[Any], str] -_ReprArgType = Union[bool, _ReprType] -_OnSetAttrType = Callable[[Any, "Attribute[Any]", Any], Any] -_OnSetAttrArgType = Union[ - _OnSetAttrType, List[_OnSetAttrType], setters._NoOpType -] -_FieldTransformer = Callable[ - [type, List["Attribute[Any]"]], List["Attribute[Any]"] -] -# FIXME: in reality, if multiple validators are passed they must be in a list -# or tuple, but those are invariant and so would prevent subtypes of -# _ValidatorType from working when passed in a list or tuple. -_ValidatorArgType = Union[_ValidatorType[_T], Sequence[_ValidatorType[_T]]] # We subclass this here to keep the protocol's qualified name clean. class AttrsInstance(AttrsInstance_, Protocol): @@ -110,20 +102,20 @@ else: class Attribute(Generic[_T]): name: str - default: Optional[_T] - validator: Optional[_ValidatorType[_T]] + default: _T | None + validator: _ValidatorType[_T] | None repr: _ReprArgType cmp: _EqOrderType eq: _EqOrderType order: _EqOrderType - hash: Optional[bool] + hash: bool | None init: bool - converter: Optional[_ConverterType] - metadata: Dict[Any, Any] - type: Optional[Type[_T]] + converter: _ConverterType | None + metadata: dict[Any, Any] + type: type[_T] | None kw_only: bool on_setattr: _OnSetAttrType - alias: Optional[str] + alias: str | None def evolve(self, **changes: Any) -> "Attribute[Any]": ... @@ -156,18 +148,18 @@ def attrib( default: None = ..., validator: None = ..., repr: _ReprArgType = ..., - cmp: Optional[_EqOrderType] = ..., - hash: Optional[bool] = ..., + cmp: _EqOrderType | None = ..., + hash: bool | None = ..., init: bool = ..., - metadata: Optional[Mapping[Any, Any]] = ..., + metadata: Mapping[Any, Any] | None = ..., type: None = ..., converter: None = ..., factory: None = ..., kw_only: bool = ..., - eq: Optional[_EqOrderType] = ..., - order: Optional[_EqOrderType] = ..., - on_setattr: Optional[_OnSetAttrArgType] = ..., - alias: Optional[str] = ..., + eq: _EqOrderType | None = ..., + order: _EqOrderType | None = ..., + on_setattr: _OnSetAttrArgType | None = ..., + alias: str | None = ..., ) -> Any: ... # This form catches an explicit None or no default and infers the type from the @@ -175,149 +167,70 @@ def attrib( @overload def attrib( default: None = ..., - validator: Optional[_ValidatorArgType[_T]] = ..., + validator: _ValidatorArgType[_T] | None = ..., repr: _ReprArgType = ..., - cmp: Optional[_EqOrderType] = ..., - hash: Optional[bool] = ..., + cmp: _EqOrderType | None = ..., + hash: bool | None = ..., init: bool = ..., - metadata: Optional[Mapping[Any, Any]] = ..., - type: Optional[Type[_T]] = ..., - converter: Optional[_ConverterType] = ..., - factory: Optional[Callable[[], _T]] = ..., + metadata: Mapping[Any, Any] | None = ..., + type: type[_T] | None = ..., + converter: _ConverterType | None = ..., + factory: Callable[[], _T] | None = ..., kw_only: bool = ..., - eq: Optional[_EqOrderType] = ..., - order: Optional[_EqOrderType] = ..., - on_setattr: Optional[_OnSetAttrArgType] = ..., - alias: Optional[str] = ..., + eq: _EqOrderType | None = ..., + order: _EqOrderType | None = ..., + on_setattr: _OnSetAttrArgType | None = ..., + alias: str | None = ..., ) -> _T: ... # This form catches an explicit default argument. @overload def attrib( default: _T, - validator: Optional[_ValidatorArgType[_T]] = ..., + validator: _ValidatorArgType[_T] | None = ..., repr: _ReprArgType = ..., - cmp: Optional[_EqOrderType] = ..., - hash: Optional[bool] = ..., + cmp: _EqOrderType | None = ..., + hash: bool | None = ..., init: bool = ..., - metadata: Optional[Mapping[Any, Any]] = ..., - type: Optional[Type[_T]] = ..., - converter: Optional[_ConverterType] = ..., - factory: Optional[Callable[[], _T]] = ..., + metadata: Mapping[Any, Any] | None = ..., + type: type[_T] | None = ..., + converter: _ConverterType | None = ..., + factory: Callable[[], _T] | None = ..., kw_only: bool = ..., - eq: Optional[_EqOrderType] = ..., - order: Optional[_EqOrderType] = ..., - on_setattr: Optional[_OnSetAttrArgType] = ..., - alias: Optional[str] = ..., + eq: _EqOrderType | None = ..., + order: _EqOrderType | None = ..., + on_setattr: _OnSetAttrArgType | None = ..., + alias: str | None = ..., ) -> _T: ... # This form covers type=non-Type: e.g. forward references (str), Any @overload def attrib( - default: Optional[_T] = ..., - validator: Optional[_ValidatorArgType[_T]] = ..., + default: _T | None = ..., + validator: _ValidatorArgType[_T] | None = ..., repr: _ReprArgType = ..., - cmp: Optional[_EqOrderType] = ..., - hash: Optional[bool] = ..., + cmp: _EqOrderType | None = ..., + hash: bool | None = ..., init: bool = ..., - metadata: Optional[Mapping[Any, Any]] = ..., + metadata: Mapping[Any, Any] | None = ..., type: object = ..., - converter: Optional[_ConverterType] = ..., - factory: Optional[Callable[[], _T]] = ..., + converter: _ConverterType | None = ..., + factory: Callable[[], _T] | None = ..., kw_only: bool = ..., - eq: Optional[_EqOrderType] = ..., - order: Optional[_EqOrderType] = ..., - on_setattr: Optional[_OnSetAttrArgType] = ..., - alias: Optional[str] = ..., -) -> Any: ... -@overload -def field( - *, - default: None = ..., - validator: None = ..., - repr: _ReprArgType = ..., - hash: Optional[bool] = ..., - init: bool = ..., - metadata: Optional[Mapping[Any, Any]] = ..., - converter: None = ..., - factory: None = ..., - kw_only: bool = ..., - eq: Optional[bool] = ..., - order: Optional[bool] = ..., - on_setattr: Optional[_OnSetAttrArgType] = ..., - alias: Optional[str] = ..., - type: Optional[type] = ..., -) -> Any: ... - -# This form catches an explicit None or no default and infers the type from the -# other arguments. -@overload -def field( - *, - default: None = ..., - validator: Optional[_ValidatorArgType[_T]] = ..., - repr: _ReprArgType = ..., - hash: Optional[bool] = ..., - init: bool = ..., - metadata: Optional[Mapping[Any, Any]] = ..., - converter: Optional[_ConverterType] = ..., - factory: Optional[Callable[[], _T]] = ..., - kw_only: bool = ..., - eq: Optional[_EqOrderType] = ..., - order: Optional[_EqOrderType] = ..., - on_setattr: Optional[_OnSetAttrArgType] = ..., - alias: Optional[str] = ..., - type: Optional[type] = ..., -) -> _T: ... - -# This form catches an explicit default argument. -@overload -def field( - *, - default: _T, - validator: Optional[_ValidatorArgType[_T]] = ..., - repr: _ReprArgType = ..., - hash: Optional[bool] = ..., - init: bool = ..., - metadata: Optional[Mapping[Any, Any]] = ..., - converter: Optional[_ConverterType] = ..., - factory: Optional[Callable[[], _T]] = ..., - kw_only: bool = ..., - eq: Optional[_EqOrderType] = ..., - order: Optional[_EqOrderType] = ..., - on_setattr: Optional[_OnSetAttrArgType] = ..., - alias: Optional[str] = ..., - type: Optional[type] = ..., -) -> _T: ... - -# This form covers type=non-Type: e.g. forward references (str), Any -@overload -def field( - *, - default: Optional[_T] = ..., - validator: Optional[_ValidatorArgType[_T]] = ..., - repr: _ReprArgType = ..., - hash: Optional[bool] = ..., - init: bool = ..., - metadata: Optional[Mapping[Any, Any]] = ..., - converter: Optional[_ConverterType] = ..., - factory: Optional[Callable[[], _T]] = ..., - kw_only: bool = ..., - eq: Optional[_EqOrderType] = ..., - order: Optional[_EqOrderType] = ..., - on_setattr: Optional[_OnSetAttrArgType] = ..., - alias: Optional[str] = ..., - type: Optional[type] = ..., + eq: _EqOrderType | None = ..., + order: _EqOrderType | None = ..., + on_setattr: _OnSetAttrArgType | None = ..., + alias: str | None = ..., ) -> Any: ... @overload @dataclass_transform(order_default=True, field_specifiers=(attrib, field)) def attrs( maybe_cls: _C, - these: Optional[Dict[str, Any]] = ..., - repr_ns: Optional[str] = ..., + these: dict[str, Any] | None = ..., + repr_ns: str | None = ..., repr: bool = ..., - cmp: Optional[_EqOrderType] = ..., - hash: Optional[bool] = ..., + cmp: _EqOrderType | None = ..., + hash: bool | None = ..., init: bool = ..., slots: bool = ..., frozen: bool = ..., @@ -327,25 +240,25 @@ def attrs( kw_only: bool = ..., cache_hash: bool = ..., auto_exc: bool = ..., - eq: Optional[_EqOrderType] = ..., - order: Optional[_EqOrderType] = ..., + eq: _EqOrderType | None = ..., + order: _EqOrderType | None = ..., auto_detect: bool = ..., collect_by_mro: bool = ..., - getstate_setstate: Optional[bool] = ..., - on_setattr: Optional[_OnSetAttrArgType] = ..., - field_transformer: Optional[_FieldTransformer] = ..., + getstate_setstate: bool | None = ..., + on_setattr: _OnSetAttrArgType | None = ..., + field_transformer: _FieldTransformer | None = ..., match_args: bool = ..., - unsafe_hash: Optional[bool] = ..., + unsafe_hash: bool | None = ..., ) -> _C: ... @overload @dataclass_transform(order_default=True, field_specifiers=(attrib, field)) def attrs( maybe_cls: None = ..., - these: Optional[Dict[str, Any]] = ..., - repr_ns: Optional[str] = ..., + these: dict[str, Any] | None = ..., + repr_ns: str | None = ..., repr: bool = ..., - cmp: Optional[_EqOrderType] = ..., - hash: Optional[bool] = ..., + cmp: _EqOrderType | None = ..., + hash: bool | None = ..., init: bool = ..., slots: bool = ..., frozen: bool = ..., @@ -355,131 +268,24 @@ def attrs( kw_only: bool = ..., cache_hash: bool = ..., auto_exc: bool = ..., - eq: Optional[_EqOrderType] = ..., - order: Optional[_EqOrderType] = ..., + eq: _EqOrderType | None = ..., + order: _EqOrderType | None = ..., auto_detect: bool = ..., collect_by_mro: bool = ..., - getstate_setstate: Optional[bool] = ..., - on_setattr: Optional[_OnSetAttrArgType] = ..., - field_transformer: Optional[_FieldTransformer] = ..., + getstate_setstate: bool | None = ..., + on_setattr: _OnSetAttrArgType | None = ..., + field_transformer: _FieldTransformer | None = ..., match_args: bool = ..., - unsafe_hash: Optional[bool] = ..., + unsafe_hash: bool | None = ..., ) -> Callable[[_C], _C]: ... -@overload -@dataclass_transform(field_specifiers=(attrib, field)) -def define( - maybe_cls: _C, - *, - these: Optional[Dict[str, Any]] = ..., - repr: bool = ..., - unsafe_hash: Optional[bool] = ..., - hash: Optional[bool] = ..., - init: bool = ..., - slots: bool = ..., - frozen: bool = ..., - weakref_slot: bool = ..., - str: bool = ..., - auto_attribs: bool = ..., - kw_only: bool = ..., - cache_hash: bool = ..., - auto_exc: bool = ..., - eq: Optional[bool] = ..., - order: Optional[bool] = ..., - auto_detect: bool = ..., - getstate_setstate: Optional[bool] = ..., - on_setattr: Optional[_OnSetAttrArgType] = ..., - field_transformer: Optional[_FieldTransformer] = ..., - match_args: bool = ..., -) -> _C: ... -@overload -@dataclass_transform(field_specifiers=(attrib, field)) -def define( - maybe_cls: None = ..., - *, - these: Optional[Dict[str, Any]] = ..., - repr: bool = ..., - unsafe_hash: Optional[bool] = ..., - hash: Optional[bool] = ..., - init: bool = ..., - slots: bool = ..., - frozen: bool = ..., - weakref_slot: bool = ..., - str: bool = ..., - auto_attribs: bool = ..., - kw_only: bool = ..., - cache_hash: bool = ..., - auto_exc: bool = ..., - eq: Optional[bool] = ..., - order: Optional[bool] = ..., - auto_detect: bool = ..., - getstate_setstate: Optional[bool] = ..., - on_setattr: Optional[_OnSetAttrArgType] = ..., - field_transformer: Optional[_FieldTransformer] = ..., - match_args: bool = ..., -) -> Callable[[_C], _C]: ... - -mutable = define - -@overload -@dataclass_transform(frozen_default=True, field_specifiers=(attrib, field)) -def frozen( - maybe_cls: _C, - *, - these: Optional[Dict[str, Any]] = ..., - repr: bool = ..., - unsafe_hash: Optional[bool] = ..., - hash: Optional[bool] = ..., - init: bool = ..., - slots: bool = ..., - frozen: bool = ..., - weakref_slot: bool = ..., - str: bool = ..., - auto_attribs: bool = ..., - kw_only: bool = ..., - cache_hash: bool = ..., - auto_exc: bool = ..., - eq: Optional[bool] = ..., - order: Optional[bool] = ..., - auto_detect: bool = ..., - getstate_setstate: Optional[bool] = ..., - on_setattr: Optional[_OnSetAttrArgType] = ..., - field_transformer: Optional[_FieldTransformer] = ..., - match_args: bool = ..., -) -> _C: ... -@overload -@dataclass_transform(frozen_default=True, field_specifiers=(attrib, field)) -def frozen( - maybe_cls: None = ..., - *, - these: Optional[Dict[str, Any]] = ..., - repr: bool = ..., - unsafe_hash: Optional[bool] = ..., - hash: Optional[bool] = ..., - init: bool = ..., - slots: bool = ..., - frozen: bool = ..., - weakref_slot: bool = ..., - str: bool = ..., - auto_attribs: bool = ..., - kw_only: bool = ..., - cache_hash: bool = ..., - auto_exc: bool = ..., - eq: Optional[bool] = ..., - order: Optional[bool] = ..., - auto_detect: bool = ..., - getstate_setstate: Optional[bool] = ..., - on_setattr: Optional[_OnSetAttrArgType] = ..., - field_transformer: Optional[_FieldTransformer] = ..., - match_args: bool = ..., -) -> Callable[[_C], _C]: ... -def fields(cls: Type[AttrsInstance]) -> Any: ... -def fields_dict(cls: Type[AttrsInstance]) -> Dict[str, Attribute[Any]]: ... +def fields(cls: type[AttrsInstance]) -> Any: ... +def fields_dict(cls: type[AttrsInstance]) -> dict[str, Attribute[Any]]: ... def validate(inst: AttrsInstance) -> None: ... def resolve_types( cls: _A, - globalns: Optional[Dict[str, Any]] = ..., - localns: Optional[Dict[str, Any]] = ..., - attribs: Optional[List[Attribute[Any]]] = ..., + globalns: dict[str, Any] | None = ..., + localns: dict[str, Any] | None = ..., + attribs: list[Attribute[Any]] | None = ..., include_extras: bool = ..., ) -> _A: ... @@ -488,12 +294,13 @@ def resolve_types( # [attr.ib()])` is valid def make_class( name: str, - attrs: Union[List[str], Tuple[str, ...], Dict[str, Any]], - bases: Tuple[type, ...] = ..., - repr_ns: Optional[str] = ..., + attrs: list[str] | tuple[str, ...] | dict[str, Any], + bases: tuple[type, ...] = ..., + class_body: dict[str, Any] | None = ..., + repr_ns: str | None = ..., repr: bool = ..., - cmp: Optional[_EqOrderType] = ..., - hash: Optional[bool] = ..., + cmp: _EqOrderType | None = ..., + hash: bool | None = ..., init: bool = ..., slots: bool = ..., frozen: bool = ..., @@ -503,11 +310,11 @@ def make_class( kw_only: bool = ..., cache_hash: bool = ..., auto_exc: bool = ..., - eq: Optional[_EqOrderType] = ..., - order: Optional[_EqOrderType] = ..., + eq: _EqOrderType | None = ..., + order: _EqOrderType | None = ..., collect_by_mro: bool = ..., - on_setattr: Optional[_OnSetAttrArgType] = ..., - field_transformer: Optional[_FieldTransformer] = ..., + on_setattr: _OnSetAttrArgType | None = ..., + field_transformer: _FieldTransformer | None = ..., ) -> type: ... # _funcs -- @@ -521,24 +328,22 @@ def make_class( def asdict( inst: AttrsInstance, recurse: bool = ..., - filter: Optional[_FilterType[Any]] = ..., - dict_factory: Type[Mapping[Any, Any]] = ..., + filter: _FilterType[Any] | None = ..., + dict_factory: type[Mapping[Any, Any]] = ..., retain_collection_types: bool = ..., - value_serializer: Optional[ - Callable[[type, Attribute[Any], Any], Any] - ] = ..., - tuple_keys: Optional[bool] = ..., -) -> Dict[str, Any]: ... + value_serializer: Callable[[type, Attribute[Any], Any], Any] | None = ..., + tuple_keys: bool | None = ..., +) -> dict[str, Any]: ... # TODO: add support for returning NamedTuple from the mypy plugin def astuple( inst: AttrsInstance, recurse: bool = ..., - filter: Optional[_FilterType[Any]] = ..., - tuple_factory: Type[Sequence[Any]] = ..., + filter: _FilterType[Any] | None = ..., + tuple_factory: type[Sequence[Any]] = ..., retain_collection_types: bool = ..., -) -> Tuple[Any, ...]: ... -def has(cls: type) -> TypeGuard[Type[AttrsInstance]]: ... +) -> tuple[Any, ...]: ... +def has(cls: type) -> TypeGuard[type[AttrsInstance]]: ... def assoc(inst: _T, **changes: Any) -> _T: ... def evolve(inst: _T, **changes: Any) -> _T: ... diff --git a/lib/attr/_cmp.py b/lib/attr/_cmp.py index a4a35e08..1c4cce65 100644 --- a/lib/attr/_cmp.py +++ b/lib/attr/_cmp.py @@ -26,21 +26,21 @@ def cmp_using( The resulting class will have a full set of ordering methods if at least one of ``{lt, le, gt, ge}`` and ``eq`` are provided. - :param Optional[callable] eq: `callable` used to evaluate equality of two + :param typing.Callable | None eq: Callable used to evaluate equality of two objects. - :param Optional[callable] lt: `callable` used to evaluate whether one + :param typing.Callable | None lt: Callable used to evaluate whether one object is less than another object. - :param Optional[callable] le: `callable` used to evaluate whether one + :param typing.Callable | None le: Callable used to evaluate whether one object is less than or equal to another object. - :param Optional[callable] gt: `callable` used to evaluate whether one + :param typing.Callable | None gt: Callable used to evaluate whether one object is greater than another object. - :param Optional[callable] ge: `callable` used to evaluate whether one + :param typing.Callable | None ge: Callable used to evaluate whether one object is greater than or equal to another object. :param bool require_same_type: When `True`, equality and ordering methods will return `NotImplemented` if objects are not of the same type. - :param Optional[str] class_name: Name of class. Defaults to 'Comparable'. + :param str | None class_name: Name of class. Defaults to "Comparable". See `comparison` for more details. diff --git a/lib/attr/_cmp.pyi b/lib/attr/_cmp.pyi index f3dcdc1a..cc7893b0 100644 --- a/lib/attr/_cmp.pyi +++ b/lib/attr/_cmp.pyi @@ -1,13 +1,13 @@ -from typing import Any, Callable, Optional, Type +from typing import Any, Callable _CompareWithType = Callable[[Any, Any], bool] def cmp_using( - eq: Optional[_CompareWithType] = ..., - lt: Optional[_CompareWithType] = ..., - le: Optional[_CompareWithType] = ..., - gt: Optional[_CompareWithType] = ..., - ge: Optional[_CompareWithType] = ..., + eq: _CompareWithType | None = ..., + lt: _CompareWithType | None = ..., + le: _CompareWithType | None = ..., + gt: _CompareWithType | None = ..., + ge: _CompareWithType | None = ..., require_same_type: bool = ..., class_name: str = ..., -) -> Type: ... +) -> type: ... diff --git a/lib/attr/_compat.py b/lib/attr/_compat.py index 41fcf046..b7d7a4b8 100644 --- a/lib/attr/_compat.py +++ b/lib/attr/_compat.py @@ -4,17 +4,17 @@ import inspect import platform import sys import threading -import types -import warnings from collections.abc import Mapping, Sequence # noqa: F401 from typing import _GenericAlias PYPY = platform.python_implementation() == "PyPy" +PY_3_8_PLUS = sys.version_info[:2] >= (3, 8) PY_3_9_PLUS = sys.version_info[:2] >= (3, 9) -PY310 = sys.version_info[:2] >= (3, 10) +PY_3_10_PLUS = sys.version_info[:2] >= (3, 10) PY_3_12_PLUS = sys.version_info[:2] >= (3, 12) +PY_3_13_PLUS = sys.version_info[:2] >= (3, 13) if sys.version_info < (3, 8): @@ -26,16 +26,6 @@ else: from typing import Protocol # noqa: F401 -def just_warn(*args, **kw): - warnings.warn( - "Running interpreter doesn't sufficiently support code object " - "introspection. Some features like bare super() or accessing " - "__class__ will not work with slotted classes.", - RuntimeWarning, - stacklevel=2, - ) - - class _AnnotationExtractor: """ Extract type annotations from a callable, returning None whenever there @@ -76,101 +66,6 @@ class _AnnotationExtractor: return None -def make_set_closure_cell(): - """Return a function of two arguments (cell, value) which sets - the value stored in the closure cell `cell` to `value`. - """ - # pypy makes this easy. (It also supports the logic below, but - # why not do the easy/fast thing?) - if PYPY: - - def set_closure_cell(cell, value): - cell.__setstate__((value,)) - - return set_closure_cell - - # Otherwise gotta do it the hard way. - - try: - if sys.version_info >= (3, 8): - - def set_closure_cell(cell, value): - cell.cell_contents = value - - else: - # Create a function that will set its first cellvar to `value`. - def set_first_cellvar_to(value): - x = value - return - - # This function will be eliminated as dead code, but - # not before its reference to `x` forces `x` to be - # represented as a closure cell rather than a local. - def force_x_to_be_a_cell(): # pragma: no cover - return x - - # Extract the code object and make sure our assumptions about - # the closure behavior are correct. - co = set_first_cellvar_to.__code__ - if co.co_cellvars != ("x",) or co.co_freevars != (): - raise AssertionError # pragma: no cover - - # Convert this code object to a code object that sets the - # function's first _freevar_ (not cellvar) to the argument. - args = [co.co_argcount] - args.append(co.co_kwonlyargcount) - args.extend( - [ - co.co_nlocals, - co.co_stacksize, - co.co_flags, - co.co_code, - co.co_consts, - co.co_names, - co.co_varnames, - co.co_filename, - co.co_name, - co.co_firstlineno, - co.co_lnotab, - # These two arguments are reversed: - co.co_cellvars, - co.co_freevars, - ] - ) - set_first_freevar_code = types.CodeType(*args) - - def set_closure_cell(cell, value): - # Create a function using the set_first_freevar_code, - # whose first closure cell is `cell`. Calling it will - # change the value of that cell. - setter = types.FunctionType( - set_first_freevar_code, {}, "setter", (), (cell,) - ) - # And call it to set the cell. - setter(value) - - # Make sure it works on this interpreter: - def make_func_with_cell(): - x = None - - def func(): - return x # pragma: no cover - - return func - - cell = make_func_with_cell().__closure__[0] - set_closure_cell(cell, 100) - if cell.cell_contents != 100: - raise AssertionError # pragma: no cover - - except Exception: # noqa: BLE001 - return just_warn - else: - return set_closure_cell - - -set_closure_cell = make_set_closure_cell() - # Thread-local global to track attrs instances which are already being repr'd. # This is needed because there is no other (thread-safe) way to pass info # about the instances that are already being repr'd through the call stack diff --git a/lib/attr/_funcs.py b/lib/attr/_funcs.py index a888991d..29e86607 100644 --- a/lib/attr/_funcs.py +++ b/lib/attr/_funcs.py @@ -4,7 +4,7 @@ import copy from ._compat import PY_3_9_PLUS, get_generic_base -from ._make import NOTHING, _obj_setattr, fields +from ._make import _OBJ_SETATTR, NOTHING, fields from .exceptions import AttrsAttributeNotFoundError @@ -22,22 +22,21 @@ def asdict( Optionally recurse into other *attrs*-decorated classes. :param inst: Instance of an *attrs*-decorated class. - :param bool recurse: Recurse into classes that are also - *attrs*-decorated. - :param callable filter: A callable whose return code determines whether an - attribute or element is included (``True``) or dropped (``False``). Is - called with the `attrs.Attribute` as the first argument and the - value as the second argument. - :param callable dict_factory: A callable to produce dictionaries from. For - example, to produce ordered dictionaries instead of normal Python - dictionaries, pass in ``collections.OrderedDict``. - :param bool retain_collection_types: Do not convert to ``list`` when - encountering an attribute whose type is ``tuple`` or ``set``. Only - meaningful if ``recurse`` is ``True``. - :param Optional[callable] value_serializer: A hook that is called for every - attribute or dict key/value. It receives the current instance, field - and value and must return the (updated) value. The hook is run *after* - the optional *filter* has been applied. + :param bool recurse: Recurse into classes that are also *attrs*-decorated. + :param ~typing.Callable filter: A callable whose return code determines + whether an attribute or element is included (`True`) or dropped + (`False`). Is called with the `attrs.Attribute` as the first argument + and the value as the second argument. + :param ~typing.Callable dict_factory: A callable to produce dictionaries + from. For example, to produce ordered dictionaries instead of normal + Python dictionaries, pass in ``collections.OrderedDict``. + :param bool retain_collection_types: Do not convert to `list` when + encountering an attribute whose type is `tuple` or `set`. Only + meaningful if *recurse* is `True`. + :param typing.Callable | None value_serializer: A hook that is called for + every attribute or dict key/value. It receives the current instance, + field and value and must return the (updated) value. The hook is run + *after* the optional *filter* has been applied. :rtype: return type of *dict_factory* @@ -207,18 +206,16 @@ def astuple( Optionally recurse into other *attrs*-decorated classes. :param inst: Instance of an *attrs*-decorated class. - :param bool recurse: Recurse into classes that are also - *attrs*-decorated. - :param callable filter: A callable whose return code determines whether an - attribute or element is included (``True``) or dropped (``False``). Is - called with the `attrs.Attribute` as the first argument and the - value as the second argument. - :param callable tuple_factory: A callable to produce tuples from. For - example, to produce lists instead of tuples. - :param bool retain_collection_types: Do not convert to ``list`` - or ``dict`` when encountering an attribute which type is - ``tuple``, ``dict`` or ``set``. Only meaningful if ``recurse`` is - ``True``. + :param bool recurse: Recurse into classes that are also *attrs*-decorated. + :param ~typing.Callable filter: A callable whose return code determines + whether an attribute or element is included (`True`) or dropped + (`False`). Is called with the `attrs.Attribute` as the first argument + and the value as the second argument. + :param ~typing.Callable tuple_factory: A callable to produce tuples from. + For example, to produce lists instead of tuples. + :param bool retain_collection_types: Do not convert to `list` or `dict` + when encountering an attribute which type is `tuple`, `dict` or `set`. + Only meaningful if *recurse* is `True`. :rtype: return type of *tuple_factory* @@ -248,15 +245,17 @@ def astuple( elif isinstance(v, (tuple, list, set, frozenset)): cf = v.__class__ if retain is True else list items = [ - astuple( - j, - recurse=True, - filter=filter, - tuple_factory=tuple_factory, - retain_collection_types=retain, + ( + astuple( + j, + recurse=True, + filter=filter, + tuple_factory=tuple_factory, + retain_collection_types=retain, + ) + if has(j.__class__) + else j ) - if has(j.__class__) - else j for j in v ] try: @@ -272,20 +271,24 @@ def astuple( rv.append( df( ( - astuple( - kk, - tuple_factory=tuple_factory, - retain_collection_types=retain, - ) - if has(kk.__class__) - else kk, - astuple( - vv, - tuple_factory=tuple_factory, - retain_collection_types=retain, - ) - if has(vv.__class__) - else vv, + ( + astuple( + kk, + tuple_factory=tuple_factory, + retain_collection_types=retain, + ) + if has(kk.__class__) + else kk + ), + ( + astuple( + vv, + tuple_factory=tuple_factory, + retain_collection_types=retain, + ) + if has(vv.__class__) + else vv + ), ) for kk, vv in v.items() ) @@ -356,7 +359,7 @@ def assoc(inst, **changes): if a is NOTHING: msg = f"{k} is not an attrs attribute on {new.__class__}." raise AttrsAttributeNotFoundError(msg) - _obj_setattr(new, k, v) + _OBJ_SETATTR(new, k, v) return new @@ -365,7 +368,8 @@ def evolve(*args, **changes): Create a new instance, based on the first positional argument with *changes* applied. - :param inst: Instance of a class with *attrs* attributes. + :param inst: Instance of a class with *attrs* attributes. *inst* must be + passed as a positional argument. :param changes: Keyword changes in the new copy. :return: A copy of inst with *changes* incorporated. @@ -381,30 +385,16 @@ def evolve(*args, **changes): *inst*. It will raise a warning until at least April 2024, after which it will become an error. Always pass the instance as a positional argument. + .. versionchanged:: 24.1.0 + *inst* can't be passed as a keyword argument anymore. """ - # Try to get instance by positional argument first. - # Use changes otherwise and warn it'll break. - if args: - try: - (inst,) = args - except ValueError: - msg = f"evolve() takes 1 positional argument, but {len(args)} were given" - raise TypeError(msg) from None - else: - try: - inst = changes.pop("inst") - except KeyError: - msg = "evolve() missing 1 required positional argument: 'inst'" - raise TypeError(msg) from None - - import warnings - - warnings.warn( - "Passing the instance per keyword argument is deprecated and " - "will stop working in, or after, April 2024.", - DeprecationWarning, - stacklevel=2, + try: + (inst,) = args + except ValueError: + msg = ( + f"evolve() takes 1 positional argument, but {len(args)} were given" ) + raise TypeError(msg) from None cls = inst.__class__ attrs = fields(cls) @@ -426,25 +416,25 @@ def resolve_types( Resolve any strings and forward annotations in type annotations. This is only required if you need concrete types in `Attribute`'s *type* - field. In other words, you don't need to resolve your types if you only - use them for static type checking. + field. In other words, you don't need to resolve your types if you only use + them for static type checking. With no arguments, names will be looked up in the module in which the class - was created. If this is not what you want, e.g. if the name only exists - inside a method, you may pass *globalns* or *localns* to specify other - dictionaries in which to look up these names. See the docs of + was created. If this is not what you want, for example, if the name only + exists inside a method, you may pass *globalns* or *localns* to specify + other dictionaries in which to look up these names. See the docs of `typing.get_type_hints` for more details. :param type cls: Class to resolve. - :param Optional[dict] globalns: Dictionary containing global variables. - :param Optional[dict] localns: Dictionary containing local variables. - :param Optional[list] attribs: List of attribs for the given class. - This is necessary when calling from inside a ``field_transformer`` - since *cls* is not an *attrs* class yet. - :param bool include_extras: Resolve more accurately, if possible. - Pass ``include_extras`` to ``typing.get_hints``, if supported by the - typing module. On supported Python versions (3.9+), this resolves the - types more accurately. + :param dict | None globalns: Dictionary containing global variables. + :param dict | None localns: Dictionary containing local variables. + :param list | None attribs: List of attribs for the given class. This is + necessary when calling from inside a ``field_transformer`` since *cls* + is not an *attrs* class yet. + :param bool include_extras: Resolve more accurately, if possible. Pass + ``include_extras`` to ``typing.get_hints``, if supported by the typing + module. On supported Python versions (3.9+), this resolves the types + more accurately. :raise TypeError: If *cls* is not a class. :raise attrs.exceptions.NotAnAttrsClassError: If *cls* is not an *attrs* @@ -458,7 +448,6 @@ def resolve_types( .. versionadded:: 20.1.0 .. versionadded:: 21.1.0 *attribs* .. versionadded:: 23.1.0 *include_extras* - """ # Since calling get_type_hints is expensive we cache whether we've # done it already. @@ -474,7 +463,7 @@ def resolve_types( for field in fields(cls) if attribs is None else attribs: if field.name in hints: # Since fields have been frozen we must work around it. - _obj_setattr(field, "type", hints[field.name]) + _OBJ_SETATTR(field, "type", hints[field.name]) # We store the class we resolved so that subclasses know they haven't # been resolved. cls.__attrs_types_resolved__ = cls diff --git a/lib/attr/_make.py b/lib/attr/_make.py index fd106367..d3bfb440 100644 --- a/lib/attr/_make.py +++ b/lib/attr/_make.py @@ -3,7 +3,9 @@ import contextlib import copy import enum +import functools import inspect +import itertools import linecache import sys import types @@ -15,10 +17,10 @@ from operator import itemgetter # having the thread-local in the globals here. from . import _compat, _config, setters from ._compat import ( - PY310, + PY_3_8_PLUS, + PY_3_10_PLUS, _AnnotationExtractor, get_generic_base, - set_closure_cell, ) from .exceptions import ( DefaultAlreadySetError, @@ -29,10 +31,10 @@ from .exceptions import ( # This is used at least twice, so cache it here. -_obj_setattr = object.__setattr__ -_init_converter_pat = "__attr_converter_%s" -_init_factory_pat = "__attr_factory_%s" -_classvar_prefixes = ( +_OBJ_SETATTR = object.__setattr__ +_INIT_CONVERTER_PAT = "__attr_converter_%s" +_INIT_FACTORY_PAT = "__attr_factory_%s" +_CLASSVAR_PREFIXES = ( "typing.ClassVar", "t.ClassVar", "ClassVar", @@ -41,19 +43,19 @@ _classvar_prefixes = ( # we don't use a double-underscore prefix because that triggers # name mangling when trying to create a slot for the field # (when slots=True) -_hash_cache_field = "_attrs_cached_hash" +_HASH_CACHE_FIELD = "_attrs_cached_hash" -_empty_metadata_singleton = types.MappingProxyType({}) +_EMPTY_METADATA_SINGLETON = types.MappingProxyType({}) # Unique object for unequivocal getattr() defaults. -_sentinel = object() +_SENTINEL = object() -_ng_default_on_setattr = setters.pipe(setters.convert, setters.validate) +_DEFAULT_ON_SETATTR = setters.pipe(setters.convert, setters.validate) class _Nothing(enum.Enum): """ - Sentinel to indicate the lack of a value when ``None`` is ambiguous. + Sentinel to indicate the lack of a value when `None` is ambiguous. If extending attrs, you can use ``typing.Literal[NOTHING]`` to show that a value may be ``NOTHING``. @@ -73,7 +75,7 @@ class _Nothing(enum.Enum): NOTHING = _Nothing.NOTHING """ -Sentinel to indicate the lack of a value when ``None`` is ambiguous. +Sentinel to indicate the lack of a value when `None` is ambiguous. """ @@ -82,7 +84,7 @@ class _CacheHashWrapper(int): An integer subclass that pickles / copies as None This is used for non-slots classes with ``cache_hash=True``, to avoid - serializing a potentially (even likely) invalid hash value. Since ``None`` + serializing a potentially (even likely) invalid hash value. Since `None` is the default value for uncalculated hashes, whenever this is copied, the copy's value for the hash should automatically reset. @@ -115,8 +117,8 @@ def attrib( .. warning:: - Does *not* do anything unless the class is also decorated with - `attr.s` / `attrs.define` / et cetera! + Does *not* do anything unless the class is also decorated with `attr.s` + / `attrs.define` / and so on! Please consider using `attrs.field` in new code (``attr.ib`` will *never* go away, though). @@ -130,95 +132,103 @@ def attrib( or dicts). If a default is not set (or set manually to `attrs.NOTHING`), a value - *must* be supplied when instantiating; otherwise a `TypeError` - will be raised. + *must* be supplied when instantiating; otherwise a `TypeError` will be + raised. The default can also be set using decorator notation as shown below. - :type default: Any value + .. seealso:: `defaults` - :param callable factory: Syntactic sugar for + :param ~typing.Callable factory: Syntactic sugar for ``default=attr.Factory(factory)``. - :param validator: `callable` that is called by *attrs*-generated - ``__init__`` methods after the instance has been initialized. They - receive the initialized instance, the :func:`~attrs.Attribute`, and the - passed value. + :param ~typing.Callable | list[~typing.Callable] validator: Callable that + is called by *attrs*-generated ``__init__`` methods after the instance + has been initialized. They receive the initialized instance, the + :func:`~attrs.Attribute`, and the passed value. The return value is *not* inspected so the validator has to throw an exception itself. - If a `list` is passed, its items are treated as validators and must - all pass. + If a `list` is passed, its items are treated as validators and must all + pass. Validators can be globally disabled and re-enabled using `attrs.validators.get_disabled` / `attrs.validators.set_disabled`. The validator can also be set using decorator notation as shown below. - :type validator: `callable` or a `list` of `callable`\\ s. + .. seealso:: :ref:`validators` - :param repr: Include this attribute in the generated ``__repr__`` - method. If ``True``, include the attribute; if ``False``, omit it. By - default, the built-in ``repr()`` function is used. To override how the - attribute value is formatted, pass a ``callable`` that takes a single - value and returns a string. Note that the resulting string is used - as-is, i.e. it will be used directly *instead* of calling ``repr()`` - (the default). - :type repr: a `bool` or a `callable` to use a custom function. + :param bool | ~typing.Callable repr: Include this attribute in the + generated ``__repr__`` method. If `True`, include the attribute; if + `False`, omit it. By default, the built-in ``repr()`` function is used. + To override how the attribute value is formatted, pass a ``callable`` + that takes a single value and returns a string. Note that the resulting + string is used as-is, which means it will be used directly *instead* of + calling ``repr()`` (the default). - :param eq: If ``True`` (default), include this attribute in the - generated ``__eq__`` and ``__ne__`` methods that check two instances - for equality. To override how the attribute value is compared, - pass a ``callable`` that takes a single value and returns the value - to be compared. - :type eq: a `bool` or a `callable`. + :param bool | ~typing.Callable eq: If `True` (default), include this + attribute in the generated ``__eq__`` and ``__ne__`` methods that check + two instances for equality. To override how the attribute value is + compared, pass a callable that takes a single value and returns the + value to be compared. - :param order: If ``True`` (default), include this attributes in the - generated ``__lt__``, ``__le__``, ``__gt__`` and ``__ge__`` methods. - To override how the attribute value is ordered, - pass a ``callable`` that takes a single value and returns the value - to be ordered. - :type order: a `bool` or a `callable`. + .. seealso:: `comparison` + + :param bool | ~typing.Callable order: If `True` (default), include this + attributes in the generated ``__lt__``, ``__le__``, ``__gt__`` and + ``__ge__`` methods. To override how the attribute value is ordered, + pass a callable that takes a single value and returns the value to be + ordered. + + .. seealso:: `comparison` :param cmp: Setting *cmp* is equivalent to setting *eq* and *order* to the same value. Must not be mixed with *eq* or *order*. + + .. seealso:: `comparison` :type cmp: a `bool` or a `callable`. - :param Optional[bool] hash: Include this attribute in the generated - ``__hash__`` method. If ``None`` (default), mirror *eq*'s value. This + :param bool | None hash: Include this attribute in the generated + ``__hash__`` method. If `None` (default), mirror *eq*'s value. This is the correct behavior according the Python spec. Setting this value - to anything else than ``None`` is *discouraged*. + to anything else than `None` is *discouraged*. + + .. seealso:: `hashing` :param bool init: Include this attribute in the generated ``__init__`` - method. It is possible to set this to ``False`` and set a default - value. In that case this attributed is unconditionally initialized - with the specified default value or factory. - :param callable converter: `callable` that is called by - *attrs*-generated ``__init__`` methods to convert attribute's value - to the desired format. It is given the passed-in value, and the - returned value will be used as the new value of the attribute. The - value is converted before being passed to the validator, if any. - :param metadata: An arbitrary mapping, to be used by third-party - components. See `extending-metadata`. + method. It is possible to set this to `False` and set a default value. + In that case this attributed is unconditionally initialized with the + specified default value or factory. + + .. seealso:: `init` + :param typing.Callable converter: `callable` that is called by + *attrs*-generated ``__init__`` methods to convert attribute's value to + the desired format. It is given the passed-in value, and the returned + value will be used as the new value of the attribute. The value is + converted before being passed to the validator, if any. + + .. seealso:: :ref:`converters` + :param dict | None metadata: An arbitrary mapping, to be used by + third-party components. See `extending-metadata`. :param type: The type of the attribute. Nowadays, the preferred method to - specify the type is using a variable annotation (see :pep:`526`). - This argument is provided for backward compatibility. - Regardless of the approach used, the type will be stored on - ``Attribute.type``. + specify the type is using a variable annotation (see :pep:`526`). This + argument is provided for backward compatibility. Regardless of the + approach used, the type will be stored on ``Attribute.type``. Please note that *attrs* doesn't do anything with this metadata by - itself. You can use it as part of your own code or for - `static type checking `. - :param kw_only: Make this attribute keyword-only in the generated - ``__init__`` (if ``init`` is ``False``, this parameter is ignored). + itself. You can use it as part of your own code or for `static type + checking `. + :param bool kw_only: Make this attribute keyword-only in the generated + ``__init__`` (if ``init`` is `False`, this parameter is ignored). :param on_setattr: Allows to overwrite the *on_setattr* setting from `attr.s`. If left `None`, the *on_setattr* value from `attr.s` is used. Set to `attrs.setters.NO_OP` to run **no** `setattr` hooks for this attribute -- regardless of the setting in `attr.s`. - :type on_setattr: `callable`, or a list of callables, or `None`, or - `attrs.setters.NO_OP` - :param Optional[str] alias: Override this attribute's parameter name in the + :type on_setattr: ~typing.Callable | list[~typing.Callable] | None | + typing.Literal[attrs.setters.NO_OP] + :param str | None alias: Override this attribute's parameter name in the generated ``__init__`` method. If left `None`, default to ``name`` stripped of leading underscores. See `private-attributes`. @@ -226,7 +236,7 @@ def attrib( .. versionadded:: 16.3.0 *metadata* .. versionchanged:: 17.1.0 *validator* can be a ``list`` now. .. versionchanged:: 17.1.0 - *hash* is ``None`` and therefore mirrors *eq* by default. + *hash* is `None` and therefore mirrors *eq* by default. .. versionadded:: 17.3.0 *type* .. deprecated:: 17.4.0 *convert* .. versionadded:: 17.4.0 *converter* as a replacement for the deprecated @@ -299,17 +309,18 @@ def attrib( def _compile_and_eval(script, globs, locs=None, filename=""): """ - "Exec" the script with the given global (globs) and local (locs) variables. + Evaluate the script with the given global (globs) and local (locs) + variables. """ bytecode = compile(script, filename, "exec") eval(bytecode, globs, locs) -def _make_method(name, script, filename, globs): +def _make_method(name, script, filename, globs, locals=None): """ Create the method with the script given and return the method object. """ - locs = {} + locs = {} if locals is None else locals # In order of debuggers like PDB being able to step through the code, # we add a fake linecache entry. @@ -390,15 +401,15 @@ def _is_class_var(annot): if annot.startswith(("'", '"')) and annot.endswith(("'", '"')): annot = annot[1:-1] - return annot.startswith(_classvar_prefixes) + return annot.startswith(_CLASSVAR_PREFIXES) def _has_own_attribute(cls, attrib_name): """ Check whether *cls* defines *attrib_name* (and doesn't just inherit it). """ - attr = getattr(cls, attrib_name, _sentinel) - if attr is _sentinel: + attr = getattr(cls, attrib_name, _SENTINEL) + if attr is _SENTINEL: return False for base_cls in cls.__mro__[1:]: @@ -486,8 +497,8 @@ def _transform_attrs( If *these* is passed, use that and don't look for them on the class. - *collect_by_mro* is True, collect them in the correct MRO order, otherwise - use the old -- incorrect -- order. See #428. + If *collect_by_mro* is True, collect them in the correct MRO order, + otherwise use the old -- incorrect -- order. See #428. Return an `_Attributes`. """ @@ -587,6 +598,64 @@ def _transform_attrs( return _Attributes((AttrsClass(attrs), base_attrs, base_attr_map)) +def _make_cached_property_getattr(cached_properties, original_getattr, cls): + lines = [ + # Wrapped to get `__class__` into closure cell for super() + # (It will be replaced with the newly constructed class after construction). + "def wrapper(_cls):", + " __class__ = _cls", + " def __getattr__(self, item, cached_properties=cached_properties, original_getattr=original_getattr, _cached_setattr_get=_cached_setattr_get):", + " func = cached_properties.get(item)", + " if func is not None:", + " result = func(self)", + " _setter = _cached_setattr_get(self)", + " _setter(item, result)", + " return result", + ] + if original_getattr is not None: + lines.append( + " return original_getattr(self, item)", + ) + else: + lines.extend( + [ + " try:", + " return super().__getattribute__(item)", + " except AttributeError:", + " if not hasattr(super(), '__getattr__'):", + " raise", + " return super().__getattr__(item)", + " original_error = f\"'{self.__class__.__name__}' object has no attribute '{item}'\"", + " raise AttributeError(original_error)", + ] + ) + + lines.extend( + [ + " return __getattr__", + "__getattr__ = wrapper(_cls)", + ] + ) + + unique_filename = _generate_unique_filename(cls, "getattr") + + glob = { + "cached_properties": cached_properties, + "_cached_setattr_get": _OBJ_SETATTR.__get__, + "original_getattr": original_getattr, + } + + return _make_method( + "__getattr__", + "\n".join(lines), + unique_filename, + glob, + locals={ + "_cls": cls, + }, + ) + + def _frozen_setattrs(self, name, value): """ Attached to frozen classes as __setattr__. @@ -695,7 +764,7 @@ class _ClassBuilder: self._wrote_own_setattr = True elif on_setattr in ( - _ng_default_on_setattr, + _DEFAULT_ON_SETATTR, setters.validate, setters.convert, ): @@ -710,7 +779,7 @@ class _ClassBuilder: break if ( ( - on_setattr == _ng_default_on_setattr + on_setattr == _DEFAULT_ON_SETATTR and not (has_validator or has_converter) ) or (on_setattr == setters.validate and not has_validator) @@ -730,7 +799,7 @@ class _ClassBuilder: def __repr__(self): return f"<_ClassBuilder(cls={self._cls.__name__})>" - if PY310: + if PY_3_10_PLUS: import abc def build_class(self): @@ -771,7 +840,7 @@ class _ClassBuilder: for name in self._attr_names: if ( name not in base_names - and getattr(cls, name, _sentinel) is not _sentinel + and getattr(cls, name, _SENTINEL) is not _SENTINEL ): # An AttributeError can happen if a base class defines a # class variable and we want to set an attribute with the @@ -791,7 +860,7 @@ class _ClassBuilder: cls.__attrs_own_setattr__ = False if not self._has_custom_setattr: - cls.__setattr__ = _obj_setattr + cls.__setattr__ = _OBJ_SETATTR return cls @@ -819,7 +888,7 @@ class _ClassBuilder: if not self._has_custom_setattr: for base_cls in self._cls.__bases__: if base_cls.__dict__.get("__attrs_own_setattr__", False): - cd["__setattr__"] = _obj_setattr + cd["__setattr__"] = _OBJ_SETATTR break # Traverse the MRO to collect existing slots @@ -847,9 +916,50 @@ class _ClassBuilder: ): names += ("__weakref__",) + if PY_3_8_PLUS: + cached_properties = { + name: cached_property.func + for name, cached_property in cd.items() + if isinstance(cached_property, functools.cached_property) + } + else: + # `functools.cached_property` was introduced in 3.8. + # So can't be used before this. + cached_properties = {} + + # Collect methods with a `__class__` reference that are shadowed in the new class. + # To know to update them. + additional_closure_functions_to_update = [] + if cached_properties: + # Add cached properties to names for slotting. + names += tuple(cached_properties.keys()) + + for name in cached_properties: + # Clear out function from class to avoid clashing. + del cd[name] + + additional_closure_functions_to_update.extend( + cached_properties.values() + ) + + class_annotations = _get_annotations(self._cls) + for name, func in cached_properties.items(): + annotation = inspect.signature(func).return_annotation + if annotation is not inspect.Parameter.empty: + class_annotations[name] = annotation + + original_getattr = cd.get("__getattr__") + if original_getattr is not None: + additional_closure_functions_to_update.append(original_getattr) + + cd["__getattr__"] = _make_cached_property_getattr( + cached_properties, original_getattr, self._cls + ) + # We only add the names of attributes that aren't inherited. # Setting __slots__ to inherited attributes wastes memory. slot_names = [name for name in names if name not in base_names] + # There are slots for attributes from current class # that are defined in parent classes. # As their descriptors may be overridden by a child class, @@ -862,7 +972,8 @@ class _ClassBuilder: slot_names = [name for name in slot_names if name not in reused_slots] cd.update(reused_slots) if self._cache_hash: - slot_names.append(_hash_cache_field) + slot_names.append(_HASH_CACHE_FIELD) + cd["__slots__"] = tuple(slot_names) cd["__qualname__"] = self._cls.__qualname__ @@ -876,7 +987,9 @@ class _ClassBuilder: # compiler will bake a reference to the class in the method itself # as `method.__closure__`. Since we replace the class with a # clone, we rewrite these references so it keeps working. - for item in cls.__dict__.values(): + for item in itertools.chain( + cls.__dict__.values(), additional_closure_functions_to_update + ): if isinstance(item, (classmethod, staticmethod)): # Class- and staticmethods hide their functions inside. # These might need to be rewritten as well. @@ -898,8 +1011,7 @@ class _ClassBuilder: pass else: if match: - set_closure_cell(cell, cls) - + cell.cell_contents = cls return cls def add_repr(self, ns): @@ -941,7 +1053,7 @@ class _ClassBuilder: """ Automatically created by attrs. """ - __bound_setattr = _obj_setattr.__get__(self) + __bound_setattr = _OBJ_SETATTR.__get__(self) if isinstance(state, tuple): # Backward compatibility with attrs instances pickled with # attrs versions before v22.2.0 which stored tuples. @@ -957,7 +1069,7 @@ class _ClassBuilder: # indicate that the first call to __hash__ should be a cache # miss. if hash_caching_enabled: - __bound_setattr(_hash_cache_field, None) + __bound_setattr(_HASH_CACHE_FIELD, None) return slots_getstate, slots_setstate @@ -1071,7 +1183,7 @@ class _ClassBuilder: else: nval = hook(self, a, val) - _obj_setattr(self, name, nval) + _OBJ_SETATTR(self, name, nval) self._cls_dict["__attrs_own_setattr__"] = True self._cls_dict["__setattr__"] = self._add_method_dunders(__setattr__) @@ -1225,64 +1337,69 @@ def attrs( A class decorator that adds :term:`dunder methods` according to the specified attributes using `attr.ib` or the *these* argument. - Please consider using `attrs.define` / `attrs.frozen` in new code - (``attr.s`` will *never* go away, though). + Consider using `attrs.define` / `attrs.frozen` in new code (``attr.s`` will + *never* go away, though). - :param these: A dictionary of name to `attr.ib` mappings. This is - useful to avoid the definition of your attributes within the class body - because you can't (e.g. if you want to add ``__repr__`` methods to - Django models) or don't want to. + :param these: A dictionary of name to `attr.ib` mappings. This is useful + to avoid the definition of your attributes within the class body + because you can't (for example, if you want to add ``__repr__`` methods + to Django models) or don't want to. - If *these* is not ``None``, *attrs* will *not* search the class body + If *these* is not `None`, *attrs* will *not* search the class body for attributes and will *not* remove any attributes from it. The order is deduced from the order of the attributes inside *these*. :type these: `dict` of `str` to `attr.ib` - :param str repr_ns: When using nested classes, there's no way in Python 2 - to automatically detect that. Therefore it's possible to set the - namespace explicitly for a more meaningful ``repr`` output. + :param str repr_ns: When using nested classes, there was no way in Python 2 + to automatically detect that. This argument allows to set a custom + name for a more meaningful ``repr`` output. This argument + is pointless in Python 3 and is therefore deprecated. :param bool auto_detect: Instead of setting the *init*, *repr*, *eq*, *order*, and *hash* arguments explicitly, assume they are set to - ``True`` **unless any** of the involved methods for one of the - arguments is implemented in the *current* class (i.e. it is *not* + `True` **unless any** of the involved methods for one of the + arguments is implemented in the *current* class (meaning, it is *not* inherited from some base class). - So for example by implementing ``__eq__`` on a class yourself, - *attrs* will deduce ``eq=False`` and will create *neither* - ``__eq__`` *nor* ``__ne__`` (but Python classes come with a sensible - ``__ne__`` by default, so it *should* be enough to only implement - ``__eq__`` in most cases). + So, for example by implementing ``__eq__`` on a class yourself, *attrs* + will deduce ``eq=False`` and will create *neither* ``__eq__`` *nor* + ``__ne__`` (but Python classes come with a sensible ``__ne__`` by + default, so it *should* be enough to only implement ``__eq__`` in most + cases). .. warning:: If you prevent *attrs* from creating the ordering methods for you - (``order=False``, e.g. by implementing ``__le__``), it becomes - *your* responsibility to make sure its ordering is sound. The best - way is to use the `functools.total_ordering` decorator. + (``order=False``, for example, by implementing ``__le__``), it + becomes *your* responsibility to make sure its ordering is sound. + The best way is to use the `functools.total_ordering` decorator. - - Passing ``True`` or ``False`` to *init*, *repr*, *eq*, *order*, - *cmp*, or *hash* overrides whatever *auto_detect* would determine. + Passing `True` or `False` to *init*, *repr*, *eq*, *order*, *cmp*, + or *hash* overrides whatever *auto_detect* would determine. :param bool repr: Create a ``__repr__`` method with a human readable representation of *attrs* attributes.. :param bool str: Create a ``__str__`` method that is identical to - ``__repr__``. This is usually not necessary except for - `Exception`\ s. - :param Optional[bool] eq: If ``True`` or ``None`` (default), add ``__eq__`` + ``__repr__``. This is usually not necessary except for `Exception`\ s. + :param bool | None eq: If `True` or `None` (default), add ``__eq__`` and ``__ne__`` methods that check two instances for equality. They compare the instances as if they were tuples of their *attrs* attributes if and only if the types of both classes are *identical*! - :param Optional[bool] order: If ``True``, add ``__lt__``, ``__le__``, + + .. seealso:: `comparison` + :param bool | None order: If `True`, add ``__lt__``, ``__le__``, ``__gt__``, and ``__ge__`` methods that behave like *eq* above and - allow instances to be ordered. If ``None`` (default) mirror value of + allow instances to be ordered. If `None` (default) mirror value of *eq*. - :param Optional[bool] cmp: Setting *cmp* is equivalent to setting *eq* - and *order* to the same value. Must not be mixed with *eq* or *order*. - :param Optional[bool] unsafe_hash: If ``None`` (default), the ``__hash__`` + + .. seealso:: `comparison` + :param bool | None cmp: Setting *cmp* is equivalent to setting *eq* and + *order* to the same value. Must not be mixed with *eq* or *order*. + + .. seealso:: `comparison` + :param bool | None unsafe_hash: If `None` (default), the ``__hash__`` method is generated according how *eq* and *frozen* are set. 1. If *both* are True, *attrs* will generate a ``__hash__`` for you. @@ -1292,28 +1409,34 @@ def attrs( ``__hash__`` method of the base class will be used (if base class is ``object``, this means it will fall back to id-based hashing.). - Although not recommended, you can decide for yourself and force - *attrs* to create one (e.g. if the class is immutable even though you - didn't freeze it programmatically) by passing ``True`` or not. Both of + Although not recommended, you can decide for yourself and force *attrs* + to create one (for example, if the class is immutable even though you + didn't freeze it programmatically) by passing `True` or not. Both of these cases are rather special and should be used carefully. - See our documentation on `hashing`, Python's documentation on - `object.__hash__`, and the `GitHub issue that led to the default \ - behavior `_ for more - details. - :param Optional[bool] hash: Alias for *unsafe_hash*. *unsafe_hash* takes - precedence. - :param bool init: Create a ``__init__`` method that initializes the - *attrs* attributes. Leading underscores are stripped for the argument - name. If a ``__attrs_pre_init__`` method exists on the class, it will - be called before the class is initialized. If a ``__attrs_post_init__`` - method exists on the class, it will be called after the class is fully - initialized. + .. seealso:: - If ``init`` is ``False``, an ``__attrs_init__`` method will be - injected instead. This allows you to define a custom ``__init__`` - method that can do pre-init work such as ``super().__init__()``, - and then call ``__attrs_init__()`` and ``__attrs_post_init__()``. + - Our documentation on `hashing`, + - Python's documentation on `object.__hash__`, + - and the `GitHub issue that led to the default \ behavior + `_ for more + details. + + :param bool | None hash: Alias for *unsafe_hash*. *unsafe_hash* takes + precedence. + :param bool init: Create a ``__init__`` method that initializes the *attrs* + attributes. Leading underscores are stripped for the argument name + (unless an alias is set on the attribute). If a ``__attrs_pre_init__`` + method exists on the class, it will be called before the class is + initialized. If a ``__attrs_post_init__`` method exists on the class, + it will be called after the class is fully initialized. + + If ``init`` is `False`, an ``__attrs_init__`` method will be injected + instead. This allows you to define a custom ``__init__`` method that + can do pre-init work such as ``super().__init__()``, and then call + ``__attrs_init__()`` and ``__attrs_post_init__()``. + + .. seealso:: `init` :param bool slots: Create a :term:`slotted class ` that's more memory-efficient. Slotted classes are generally superior to the default dict classes, but have some gotchas you should know about, so @@ -1335,53 +1458,50 @@ def attrs( 4. If a class is frozen, you cannot modify ``self`` in ``__attrs_post_init__`` or a self-written ``__init__``. You can - circumvent that limitation by using - ``object.__setattr__(self, "attribute_name", value)``. + circumvent that limitation by using ``object.__setattr__(self, + "attribute_name", value)``. 5. Subclasses of a frozen class are frozen too. :param bool weakref_slot: Make instances weak-referenceable. This has no effect unless ``slots`` is also enabled. - :param bool auto_attribs: If ``True``, collect :pep:`526`-annotated + :param bool auto_attribs: If `True`, collect :pep:`526`-annotated attributes from the class body. - In this case, you **must** annotate every field. If *attrs* - encounters a field that is set to an `attr.ib` but lacks a type - annotation, an `attr.exceptions.UnannotatedAttributeError` is - raised. Use ``field_name: typing.Any = attr.ib(...)`` if you don't - want to set a type. + In this case, you **must** annotate every field. If *attrs* encounters + a field that is set to an `attr.ib` but lacks a type annotation, an + `attrs.exceptions.UnannotatedAttributeError` is raised. Use + ``field_name: typing.Any = attr.ib(...)`` if you don't want to set a + type. - If you assign a value to those attributes (e.g. ``x: int = 42``), that - value becomes the default value like if it were passed using - ``attr.ib(default=42)``. Passing an instance of `attrs.Factory` also - works as expected in most cases (see warning below). + If you assign a value to those attributes (for example, ``x: int = + 42``), that value becomes the default value like if it were passed + using ``attr.ib(default=42)``. Passing an instance of `attrs.Factory` + also works as expected in most cases (see warning below). Attributes annotated as `typing.ClassVar`, and attributes that are neither annotated nor set to an `attr.ib` are **ignored**. .. warning:: - For features that use the attribute name to create decorators (e.g. - :ref:`validators `), you still *must* assign `attr.ib` - to them. Otherwise Python will either not find the name or try to - use the default value to call e.g. ``validator`` on it. - These errors can be quite confusing and probably the most common bug - report on our bug tracker. + For features that use the attribute name to create decorators (for + example, :ref:`validators `), you still *must* assign + `attr.ib` to them. Otherwise Python will either not find the name or + try to use the default value to call, for example, ``validator`` on + it. - :param bool kw_only: Make all attributes keyword-only - in the generated ``__init__`` (if ``init`` is ``False``, this - parameter is ignored). - :param bool cache_hash: Ensure that the object's hash code is computed - only once and stored on the object. If this is set to ``True``, - hashing must be either explicitly or implicitly enabled for this - class. If the hash code is cached, avoid any reassignments of - fields involved in hash code computation or mutations of the objects - those fields point to after object creation. If such changes occur, - the behavior of the object's hash code is undefined. - :param bool auto_exc: If the class subclasses `BaseException` - (which implicitly includes any subclass of any exception), the - following happens to behave like a well-behaved Python exceptions - class: + :param bool kw_only: Make all attributes keyword-only in the generated + ``__init__`` (if *init* is `False`, this parameter is ignored). + :param bool cache_hash: Ensure that the object's hash code is computed only + once and stored on the object. If this is set to `True`, hashing + must be either explicitly or implicitly enabled for this class. If the + hash code is cached, avoid any reassignments of fields involved in hash + code computation or mutations of the objects those fields point to + after object creation. If such changes occur, the behavior of the + object's hash code is undefined. + :param bool auto_exc: If the class subclasses `BaseException` (which + implicitly includes any subclass of any exception), the following + happens to behave like a well-behaved Python exceptions class: - the values for *eq*, *order*, and *hash* are ignored and the instances compare and hash by the instance's ids (N.B. *attrs* will @@ -1394,25 +1514,25 @@ def attrs( :param bool collect_by_mro: Setting this to `True` fixes the way *attrs* collects attributes from base classes. The default behavior is incorrect in certain cases of multiple inheritance. It should be on by - default but is kept off for backward-compatibility. + default, but is kept off for backwards-compatibility. - See issue `#428 `_ for - more details. + .. seealso:: + Issue `#428 `_ - :param Optional[bool] getstate_setstate: + :param bool | None getstate_setstate: .. note:: This is usually only interesting for slotted classes and you should probably just set *auto_detect* to `True`. - If `True`, ``__getstate__`` and - ``__setstate__`` are generated and attached to the class. This is - necessary for slotted classes to be pickleable. If left `None`, it's - `True` by default for slotted classes and ``False`` for dict classes. + If `True`, ``__getstate__`` and ``__setstate__`` are generated and + attached to the class. This is necessary for slotted classes to be + pickleable. If left `None`, it's `True` by default for slotted classes + and `False` for dict classes. - If *auto_detect* is `True`, and *getstate_setstate* is left `None`, - and **either** ``__getstate__`` or ``__setstate__`` is detected directly - on the class (i.e. not inherited), it is set to `False` (this is usually - what you want). + If *auto_detect* is `True`, and *getstate_setstate* is left `None`, and + **either** ``__getstate__`` or ``__setstate__`` is detected directly on + the class (meaning: not inherited), it is set to `False` (this is + usually what you want). :param on_setattr: A callable that is run whenever the user attempts to set an attribute (either by assignment like ``i.x = 42`` or by using @@ -1425,18 +1545,20 @@ def attrs( If a list of callables is passed, they're automatically wrapped in an `attrs.setters.pipe`. - :type on_setattr: `callable`, or a list of callables, or `None`, or - `attrs.setters.NO_OP` + :type on_setattr: ~typing.Callable | list[~typing.Callable] | None | + typing.Literal[attrs.setters.NO_OP] - :param Optional[callable] field_transformer: - A function that is called with the original class object and all - fields right before *attrs* finalizes the class. You can use - this, e.g., to automatically add converters or validators to - fields based on their types. See `transform-fields` for more details. + :param ~typing.Callable | None field_transformer: + A function that is called with the original class object and all fields + right before *attrs* finalizes the class. You can use this, for + example, to automatically add converters or validators to fields based + on their types. + + .. seealso:: `transform-fields` :param bool match_args: If `True` (default), set ``__match_args__`` on the class to support - :pep:`634` (Structural Pattern Matching). It is a tuple of all + :pep:`634` (*Structural Pattern Matching*). It is a tuple of all non-keyword-only ``__init__`` parameter names on Python 3.10 and later. Ignored on older Python versions. @@ -1445,7 +1567,7 @@ def attrs( .. versionadded:: 16.3.0 *str* .. versionadded:: 16.3.0 Support for ``__attrs_post_init__``. .. versionchanged:: 17.1.0 - *hash* supports ``None`` as value which is also the default now. + *hash* supports `None` as value which is also the default now. .. versionadded:: 17.3.0 *auto_attribs* .. versionchanged:: 18.1.0 If *these* is passed, no attributes are deleted from the class body. @@ -1476,7 +1598,18 @@ def attrs( .. versionadded:: 21.3.0 *match_args* .. versionadded:: 22.2.0 *unsafe_hash* as an alias for *hash* (for :pep:`681` compliance). + .. deprecated:: 24.1.0 *repr_ns* """ + if repr_ns is not None: + import warnings + + warnings.warn( + DeprecationWarning( + "The `repr_ns` argument is deprecated and will be removed in or after April 2025." + ), + stacklevel=2, + ) + eq_, order_ = _determine_attrs_eq_order(cmp, eq, order, None) # unsafe_hash takes precedence due to PEP 681. @@ -1580,7 +1713,7 @@ def attrs( raise TypeError(msg) if ( - PY310 + PY_3_10_PLUS and match_args and not _has_own_attribute(cls, "__match_args__") ): @@ -1589,7 +1722,7 @@ def attrs( return builder.build_class() # maybe_cls's type depends on the usage of the decorator. It's a class - # if it's used as `@attrs` but ``None`` if used as `@attrs()`. + # if it's used as `@attrs` but `None` if used as `@attrs()`. if maybe_cls is None: return wrap @@ -1674,17 +1807,17 @@ def _make_hash(cls, attrs, frozen, cache_hash): method_lines.append(indent + " " + closing_braces) if cache_hash: - method_lines.append(tab + f"if self.{_hash_cache_field} is None:") + method_lines.append(tab + f"if self.{_HASH_CACHE_FIELD} is None:") if frozen: append_hash_computation_lines( - f"object.__setattr__(self, '{_hash_cache_field}', ", tab * 2 + f"object.__setattr__(self, '{_HASH_CACHE_FIELD}', ", tab * 2 ) method_lines.append(tab * 2 + ")") # close __setattr__ else: append_hash_computation_lines( - f"self.{_hash_cache_field} = ", tab * 2 + f"self.{_HASH_CACHE_FIELD} = ", tab * 2 ) - method_lines.append(tab + f"return self.{_hash_cache_field}") + method_lines.append(tab + f"return self.{_HASH_CACHE_FIELD}") else: append_hash_computation_lines("return ", tab) @@ -1980,15 +2113,12 @@ def validate(inst): v(inst, a, getattr(inst, a.name)) -def _is_slot_cls(cls): - return "__slots__" in cls.__dict__ - - def _is_slot_attr(a_name, base_attr_map): """ Check if the attribute name comes from a slot class. """ - return a_name in base_attr_map and _is_slot_cls(base_attr_map[a_name]) + cls = base_attr_map.get(a_name) + return cls and "__slots__" in cls.__dict__ def _make_init( @@ -2057,7 +2187,7 @@ def _make_init( if needs_cached_setattr: # Save the lookup overhead in __init__ if we need to circumvent # setattr hooks. - globs["_cached_setattr_get"] = _obj_setattr.__get__ + globs["_cached_setattr_get"] = _OBJ_SETATTR.__get__ init = _make_method( "__attrs_init__" if attrs_init else "__init__", @@ -2084,7 +2214,7 @@ def _setattr_with_converter(attr_name, value_var, has_on_setattr): """ return "_setattr('%s', %s(%s))" % ( attr_name, - _init_converter_pat % (attr_name,), + _INIT_CONVERTER_PAT % (attr_name,), value_var, ) @@ -2110,11 +2240,53 @@ def _assign_with_converter(attr_name, value_var, has_on_setattr): return "self.%s = %s(%s)" % ( attr_name, - _init_converter_pat % (attr_name,), + _INIT_CONVERTER_PAT % (attr_name,), value_var, ) +def _determine_setters(frozen, slots, base_attr_map): + """ + Determine the correct setter functions based on whether a class is frozen + and/or slotted. + """ + if frozen is True: + if slots is True: + return (), _setattr, _setattr_with_converter + + # Dict frozen classes assign directly to __dict__. + # But only if the attribute doesn't come from an ancestor slot + # class. + # Note _inst_dict will be used again below if cache_hash is True + + def fmt_setter(attr_name, value_var, has_on_setattr): + if _is_slot_attr(attr_name, base_attr_map): + return _setattr(attr_name, value_var, has_on_setattr) + + return f"_inst_dict['{attr_name}'] = {value_var}" + + def fmt_setter_with_converter(attr_name, value_var, has_on_setattr): + if has_on_setattr or _is_slot_attr(attr_name, base_attr_map): + return _setattr_with_converter( + attr_name, value_var, has_on_setattr + ) + + return "_inst_dict['%s'] = %s(%s)" % ( + attr_name, + _INIT_CONVERTER_PAT % (attr_name,), + value_var, + ) + + return ( + ("_inst_dict = self.__dict__",), + fmt_setter, + fmt_setter_with_converter, + ) + + # Not frozen -- we can just assign directly. + return (), _assign, _assign_with_converter + + def _attrs_to_init_script( attrs, frozen, @@ -2137,9 +2309,7 @@ def _attrs_to_init_script( If *frozen* is True, we cannot set the attributes directly so we use a cached ``object.__setattr__``. """ - lines = [] - if pre_init: - lines.append("self.__attrs_pre_init__()") + lines = ["self.__attrs_pre_init__()"] if pre_init else [] if needs_cached_setattr: lines.append( @@ -2149,41 +2319,10 @@ def _attrs_to_init_script( "_setattr = _cached_setattr_get(self)" ) - if frozen is True: - if slots is True: - fmt_setter = _setattr - fmt_setter_with_converter = _setattr_with_converter - else: - # Dict frozen classes assign directly to __dict__. - # But only if the attribute doesn't come from an ancestor slot - # class. - # Note _inst_dict will be used again below if cache_hash is True - lines.append("_inst_dict = self.__dict__") - - def fmt_setter(attr_name, value_var, has_on_setattr): - if _is_slot_attr(attr_name, base_attr_map): - return _setattr(attr_name, value_var, has_on_setattr) - - return f"_inst_dict['{attr_name}'] = {value_var}" - - def fmt_setter_with_converter( - attr_name, value_var, has_on_setattr - ): - if has_on_setattr or _is_slot_attr(attr_name, base_attr_map): - return _setattr_with_converter( - attr_name, value_var, has_on_setattr - ) - - return "_inst_dict['%s'] = %s(%s)" % ( - attr_name, - _init_converter_pat % (attr_name,), - value_var, - ) - - else: - # Not frozen. - fmt_setter = _assign - fmt_setter_with_converter = _assign_with_converter + extra_lines, fmt_setter, fmt_setter_with_converter = _determine_setters( + frozen, slots, base_attr_map + ) + lines.extend(extra_lines) args = [] kw_only_args = [] @@ -2211,7 +2350,7 @@ def _attrs_to_init_script( if a.init is False: if has_factory: - init_factory_name = _init_factory_pat % (a.name,) + init_factory_name = _INIT_FACTORY_PAT % (a.name,) if a.converter is not None: lines.append( fmt_setter_with_converter( @@ -2220,8 +2359,9 @@ def _attrs_to_init_script( has_on_setattr, ) ) - conv_name = _init_converter_pat % (a.name,) - names_for_globals[conv_name] = a.converter + names_for_globals[_INIT_CONVERTER_PAT % (a.name,)] = ( + a.converter + ) else: lines.append( fmt_setter( @@ -2239,8 +2379,9 @@ def _attrs_to_init_script( has_on_setattr, ) ) - conv_name = _init_converter_pat % (a.name,) - names_for_globals[conv_name] = a.converter + names_for_globals[_INIT_CONVERTER_PAT % (a.name,)] = ( + a.converter + ) else: lines.append( fmt_setter( @@ -2262,9 +2403,9 @@ def _attrs_to_init_script( attr_name, arg_name, has_on_setattr ) ) - names_for_globals[ - _init_converter_pat % (a.name,) - ] = a.converter + names_for_globals[_INIT_CONVERTER_PAT % (a.name,)] = ( + a.converter + ) else: lines.append(fmt_setter(attr_name, arg_name, has_on_setattr)) @@ -2276,7 +2417,7 @@ def _attrs_to_init_script( args.append(arg) lines.append(f"if {arg_name} is not NOTHING:") - init_factory_name = _init_factory_pat % (a.name,) + init_factory_name = _INIT_FACTORY_PAT % (a.name,) if a.converter is not None: lines.append( " " @@ -2293,9 +2434,9 @@ def _attrs_to_init_script( has_on_setattr, ) ) - names_for_globals[ - _init_converter_pat % (a.name,) - ] = a.converter + names_for_globals[_INIT_CONVERTER_PAT % (a.name,)] = ( + a.converter + ) else: lines.append( " " + fmt_setter(attr_name, arg_name, has_on_setattr) @@ -2322,9 +2463,9 @@ def _attrs_to_init_script( attr_name, arg_name, has_on_setattr ) ) - names_for_globals[ - _init_converter_pat % (a.name,) - ] = a.converter + names_for_globals[_INIT_CONVERTER_PAT % (a.name,)] = ( + a.converter + ) else: lines.append(fmt_setter(attr_name, arg_name, has_on_setattr)) @@ -2365,7 +2506,7 @@ def _attrs_to_init_script( init_hash_cache = "_inst_dict['%s'] = %s" else: init_hash_cache = "self.%s = %s" - lines.append(init_hash_cache % (_hash_cache_field, "None")) + lines.append(init_hash_cache % (_HASH_CACHE_FIELD, "None")) # For exceptions we rely on BaseException.__init__ for proper # initialization. @@ -2424,20 +2565,19 @@ class Attribute: You should never instantiate this class yourself. - The class has *all* arguments of `attr.ib` (except for ``factory`` - which is only syntactic sugar for ``default=Factory(...)`` plus the - following: + The class has *all* arguments of `attr.ib` (except for ``factory`` which is + only syntactic sugar for ``default=Factory(...)`` plus the following: - ``name`` (`str`): The name of the attribute. - ``alias`` (`str`): The __init__ parameter name of the attribute, after any explicit overrides and default private-attribute-name handling. - ``inherited`` (`bool`): Whether or not that attribute has been inherited from a base class. - - ``eq_key`` and ``order_key`` (`typing.Callable` or `None`): The callables - that are used for comparing and ordering objects by this attribute, - respectively. These are set by passing a callable to `attr.ib`'s ``eq``, - ``order``, or ``cmp`` arguments. See also :ref:`comparison customization - `. + - ``eq_key`` and ``order_key`` (`typing.Callable` or `None`): The + callables that are used for comparing and ordering objects by this + attribute, respectively. These are set by passing a callable to + `attr.ib`'s ``eq``, ``order``, or ``cmp`` arguments. See also + :ref:`comparison customization `. Instances of this class are frequently used for introspection purposes like: @@ -2506,7 +2646,7 @@ class Attribute: ) # Cache this descriptor here to speed things up later. - bound_setattr = _obj_setattr.__get__(self) + bound_setattr = _OBJ_SETATTR.__get__(self) # Despite the big red warning, people *do* instantiate `Attribute` # themselves. @@ -2526,7 +2666,7 @@ class Attribute: ( types.MappingProxyType(dict(metadata)) # Shallow copy if metadata - else _empty_metadata_singleton + else _EMPTY_METADATA_SINGLETON ), ) bound_setattr("type", type) @@ -2603,16 +2743,18 @@ class Attribute: self._setattrs(zip(self.__slots__, state)) def _setattrs(self, name_values_pairs): - bound_setattr = _obj_setattr.__get__(self) + bound_setattr = _OBJ_SETATTR.__get__(self) for name, value in name_values_pairs: if name != "metadata": bound_setattr(name, value) else: bound_setattr( name, - types.MappingProxyType(dict(value)) - if value - else _empty_metadata_singleton, + ( + types.MappingProxyType(dict(value)) + if value + else _EMPTY_METADATA_SINGLETON + ), ) @@ -2799,8 +2941,8 @@ class Factory: If passed as the default value to `attrs.field`, the factory is used to generate a new value. - :param callable factory: A callable that takes either none or exactly one - mandatory positional argument depending on *takes_self*. + :param typing.Callable factory: A callable that takes either none or + exactly one mandatory positional argument depending on *takes_self*. :param bool takes_self: Pass the partially initialized instance that is being initialized as a positional argument. @@ -2846,22 +2988,26 @@ _f = [ Factory = _add_hash(_add_eq(_add_repr(Factory, attrs=_f), attrs=_f), attrs=_f) -def make_class(name, attrs, bases=(object,), **attributes_arguments): +def make_class( + name, attrs, bases=(object,), class_body=None, **attributes_arguments +): r""" A quick way to create a new class called *name* with *attrs*. :param str name: The name for the new class. - :param attrs: A list of names or a dictionary of mappings of names to - `attr.ib`\ s / `attrs.field`\ s. + :param list | dict attrs: A list of names or a dictionary of mappings of + names to `attr.ib`\ s / `attrs.field`\ s. The order is deduced from the order of the names or attributes inside *attrs*. Otherwise the order of the definition of the attributes is used. - :type attrs: `list` or `dict` :param tuple bases: Classes that the new class will subclass. + :param dict class_body: An optional dictionary of class attributes for the + new class. + :param attributes_arguments: Passed unmodified to `attr.s`. :return: A new class with *attrs*. @@ -2869,6 +3015,7 @@ def make_class(name, attrs, bases=(object,), **attributes_arguments): .. versionadded:: 17.1.0 *bases* .. versionchanged:: 18.1.0 If *attrs* is ordered, the order is retained. + .. versionchanged:: 23.2.0 *class_body* """ if isinstance(attrs, dict): cls_dict = attrs @@ -2883,6 +3030,8 @@ def make_class(name, attrs, bases=(object,), **attributes_arguments): user_init = cls_dict.pop("__init__", None) body = {} + if class_body is not None: + body.update(class_body) if pre_init is not None: body["__attrs_pre_init__"] = pre_init if post_init is not None: @@ -2939,7 +3088,8 @@ def and_(*validators): When called on a value, it runs all wrapped validators. - :param callables validators: Arbitrary number of validators. + :param ~collections.abc.Iterable[typing.Callable] validators: Arbitrary + number of validators. .. versionadded:: 17.1.0 """ @@ -2961,10 +3111,11 @@ def pipe(*converters): When called on a value, it runs all wrapped converters, returning the *last* value. - Type annotations will be inferred from the wrapped converters', if - they have any. + Type annotations will be inferred from the wrapped converters', if they + have any. - :param callables converters: Arbitrary number of converters. + :param ~collections.abc.Iterable[typing.Callable] converters: Arbitrary + number of converters. .. versionadded:: 20.1.0 """ diff --git a/lib/attr/_next_gen.py b/lib/attr/_next_gen.py index 8b44e8b3..07db702d 100644 --- a/lib/attr/_next_gen.py +++ b/lib/attr/_next_gen.py @@ -12,9 +12,9 @@ from . import setters from ._funcs import asdict as _asdict from ._funcs import astuple as _astuple from ._make import ( + _DEFAULT_ON_SETATTR, NOTHING, _frozen_setattrs, - _ng_default_on_setattr, attrib, attrs, ) @@ -52,8 +52,8 @@ def define( - Automatically detect whether or not *auto_attribs* should be `True` (c.f. *auto_attribs* parameter). - - If *frozen* is `False`, run converters and validators when setting an - attribute by default. + - Converters and validators run when attributes are set by default -- if + *frozen* is `False`. - *slots=True* .. caution:: @@ -70,7 +70,7 @@ def define( Please note that these are all defaults and you can change them as you wish. - :param Optional[bool] auto_attribs: If set to `True` or `False`, it behaves + :param bool | None auto_attribs: If set to `True` or `False`, it behaves exactly like `attr.s`. If left `None`, `attr.s` will try to guess: 1. If any attributes are annotated and no unannotated `attrs.fields`\ s @@ -124,7 +124,7 @@ def define( # By default, mutable classes convert & validate on setattr. if frozen is False and on_setattr is None: - on_setattr = _ng_default_on_setattr + on_setattr = _DEFAULT_ON_SETATTR # However, if we subclass a frozen class, we inherit the immutability # and disable on_setattr. @@ -146,7 +146,7 @@ def define( return do_it(cls, False) # maybe_cls's type depends on the usage of the decorator. It's a class - # if it's used as `@attrs` but ``None`` if used as `@attrs()`. + # if it's used as `@attrs` but `None` if used as `@attrs()`. if maybe_cls is None: return wrap diff --git a/lib/attr/converters.py b/lib/attr/converters.py index 2bf4c902..cf1fa0f7 100644 --- a/lib/attr/converters.py +++ b/lib/attr/converters.py @@ -22,12 +22,12 @@ __all__ = [ def optional(converter): """ A converter that allows an attribute to be optional. An optional attribute - is one which can be set to ``None``. + is one which can be set to `None`. - Type annotations will be inferred from the wrapped converter's, if it - has any. + Type annotations will be inferred from the wrapped converter's, if it has + any. - :param callable converter: the converter that is used for non-``None`` + :param typing.Callable converter: the converter that is used for non-`None` values. .. versionadded:: 17.1.0 @@ -53,14 +53,14 @@ def optional(converter): def default_if_none(default=NOTHING, factory=None): """ - A converter that allows to replace ``None`` values by *default* or the - result of *factory*. + A converter that allows to replace `None` values by *default* or the result + of *factory*. - :param default: Value to be used if ``None`` is passed. Passing an instance - of `attrs.Factory` is supported, however the ``takes_self`` option - is *not*. - :param callable factory: A callable that takes no parameters whose result - is used if ``None`` is passed. + :param default: Value to be used if `None` is passed. Passing an instance + of `attrs.Factory` is supported, however the ``takes_self`` option is + *not*. + :param typing.Callable factory: A callable that takes no parameters whose + result is used if `None` is passed. :raises TypeError: If **neither** *default* or *factory* is passed. :raises TypeError: If **both** *default* and *factory* are passed. @@ -104,25 +104,26 @@ def default_if_none(default=NOTHING, factory=None): def to_bool(val): """ - Convert "boolean" strings (e.g., from env. vars.) to real booleans. + Convert "boolean" strings (for example, from environment variables) to real + booleans. - Values mapping to :code:`True`: + Values mapping to `True`: - - :code:`True` - - :code:`"true"` / :code:`"t"` - - :code:`"yes"` / :code:`"y"` - - :code:`"on"` - - :code:`"1"` - - :code:`1` + - ``True`` + - ``"true"`` / ``"t"`` + - ``"yes"`` / ``"y"`` + - ``"on"`` + - ``"1"`` + - ``1`` - Values mapping to :code:`False`: + Values mapping to `False`: - - :code:`False` - - :code:`"false"` / :code:`"f"` - - :code:`"no"` / :code:`"n"` - - :code:`"off"` - - :code:`"0"` - - :code:`0` + - ``False`` + - ``"false"`` / ``"f"`` + - ``"no"`` / ``"n"`` + - ``"off"`` + - ``"0"`` + - ``0`` :raises ValueError: for any other value. @@ -130,15 +131,11 @@ def to_bool(val): """ if isinstance(val, str): val = val.lower() - truthy = {True, "true", "t", "yes", "y", "on", "1", 1} - falsy = {False, "false", "f", "no", "n", "off", "0", 0} - try: - if val in truthy: - return True - if val in falsy: - return False - except TypeError: - # Raised when "val" is not hashable (e.g., lists) - pass - msg = f"Cannot convert value to bool: {val}" + + if val in (True, "true", "t", "yes", "y", "on", "1", 1): + return True + if val in (False, "false", "f", "no", "n", "off", "0", 0): + return False + + msg = f"Cannot convert value to bool: {val!r}" raise ValueError(msg) diff --git a/lib/attr/converters.pyi b/lib/attr/converters.pyi index 5abb49f6..9ef478f2 100644 --- a/lib/attr/converters.pyi +++ b/lib/attr/converters.pyi @@ -1,6 +1,6 @@ from typing import Callable, TypeVar, overload -from . import _ConverterType +from attrs import _ConverterType _T = TypeVar("_T") diff --git a/lib/attr/filters.pyi b/lib/attr/filters.pyi index 8a02fa0f..974abdcd 100644 --- a/lib/attr/filters.pyi +++ b/lib/attr/filters.pyi @@ -1,6 +1,6 @@ -from typing import Any, Union +from typing import Any from . import Attribute, _FilterType -def include(*what: Union[type, str, Attribute[Any]]) -> _FilterType[Any]: ... -def exclude(*what: Union[type, str, Attribute[Any]]) -> _FilterType[Any]: ... +def include(*what: type | str | Attribute[Any]) -> _FilterType[Any]: ... +def exclude(*what: type | str | Attribute[Any]) -> _FilterType[Any]: ... diff --git a/lib/attr/setters.py b/lib/attr/setters.py index 12ed6750..3922adbd 100644 --- a/lib/attr/setters.py +++ b/lib/attr/setters.py @@ -69,5 +69,6 @@ def convert(instance, attrib, new_value): # Sentinel for disabling class-wide *on_setattr* hooks for certain attributes. -# autodata stopped working, so the docstring is inlined in the API docs. +# Sphinx's autodata stopped working, so the docstring is inlined in the API +# docs. NO_OP = object() diff --git a/lib/attr/setters.pyi b/lib/attr/setters.pyi index 72f7ce47..73abf36e 100644 --- a/lib/attr/setters.pyi +++ b/lib/attr/setters.pyi @@ -1,6 +1,7 @@ from typing import Any, NewType, NoReturn, TypeVar -from . import Attribute, _OnSetAttrType +from . import Attribute +from attrs import _OnSetAttrType _T = TypeVar("_T") diff --git a/lib/attr/validators.py b/lib/attr/validators.py index 18617fe6..0695cd2f 100644 --- a/lib/attr/validators.py +++ b/lib/attr/validators.py @@ -35,7 +35,6 @@ __all__ = [ "min_len", "not_", "optional", - "provides", "set_disabled", ] @@ -46,7 +45,7 @@ def set_disabled(disabled): By default, they are run. - :param disabled: If ``True``, disable running all validators. + :param disabled: If `True`, disable running all validators. :type disabled: bool .. warning:: @@ -62,7 +61,7 @@ def get_disabled(): """ Return a bool indicating whether validators are currently disabled or not. - :return: ``True`` if validators are currently disabled. + :return: `True` if validators are currently disabled. :rtype: bool .. versionadded:: 21.3.0 @@ -97,12 +96,7 @@ class _InstanceOfValidator: We use a callable class to be able to change the ``__repr__``. """ if not isinstance(value, self.type): - msg = "'{name}' must be {type!r} (got {value!r} that is a {actual!r}).".format( - name=attr.name, - type=self.type, - actual=value.__class__, - value=value, - ) + msg = f"'{attr.name}' must be {self.type!r} (got {value!r} that is a {value.__class__!r})." raise TypeError( msg, attr, @@ -116,16 +110,14 @@ class _InstanceOfValidator: def instance_of(type): """ - A validator that raises a `TypeError` if the initializer is called - with a wrong type for this particular attribute (checks are performed using + A validator that raises a `TypeError` if the initializer is called with a + wrong type for this particular attribute (checks are performed using `isinstance` therefore it's also valid to pass a tuple of types). - :param type: The type to check for. - :type type: type or tuple of type + :param type | tuple[type] type: The type to check for. - :raises TypeError: With a human readable error message, the attribute - (of type `attrs.Attribute`), the expected type, and the value it - got. + :raises TypeError: With a human readable error message, the attribute (of + type `attrs.Attribute`), the expected type, and the value it got. """ return _InstanceOfValidator(type) @@ -140,9 +132,7 @@ class _MatchesReValidator: We use a callable class to be able to change the ``__repr__``. """ if not self.match_func(value): - msg = "'{name}' must match regex {pattern!r} ({value!r} doesn't)".format( - name=attr.name, pattern=self.pattern.pattern, value=value - ) + msg = f"'{attr.name}' must match regex {self.pattern.pattern!r} ({value!r} doesn't)" raise ValueError( msg, attr, @@ -156,16 +146,16 @@ class _MatchesReValidator: def matches_re(regex, flags=0, func=None): r""" - A validator that raises `ValueError` if the initializer is called - with a string that doesn't match *regex*. + A validator that raises `ValueError` if the initializer is called with a + string that doesn't match *regex*. :param regex: a regex string or precompiled pattern to match against :param int flags: flags that will be passed to the underlying re function (default 0) - :param callable func: which underlying `re` function to call. Valid options - are `re.fullmatch`, `re.search`, and `re.match`; the default ``None`` - means `re.fullmatch`. For performance reasons, the pattern is always - precompiled using `re.compile`. + :param typing.Callable func: which underlying `re` function to call. Valid + options are `re.fullmatch`, `re.search`, and `re.match`; the default + `None` means `re.fullmatch`. For performance reasons, the pattern is + always precompiled using `re.compile`. .. versionadded:: 19.2.0 .. versionchanged:: 21.3.0 *regex* can be a pre-compiled pattern. @@ -197,56 +187,6 @@ def matches_re(regex, flags=0, func=None): return _MatchesReValidator(pattern, match_func) -@attrs(repr=False, slots=True, hash=True) -class _ProvidesValidator: - interface = attrib() - - def __call__(self, inst, attr, value): - """ - We use a callable class to be able to change the ``__repr__``. - """ - if not self.interface.providedBy(value): - msg = "'{name}' must provide {interface!r} which {value!r} doesn't.".format( - name=attr.name, interface=self.interface, value=value - ) - raise TypeError( - msg, - attr, - self.interface, - value, - ) - - def __repr__(self): - return f"" - - -def provides(interface): - """ - A validator that raises a `TypeError` if the initializer is called - with an object that does not provide the requested *interface* (checks are - performed using ``interface.providedBy(value)`` (see `zope.interface - `_). - - :param interface: The interface to check for. - :type interface: ``zope.interface.Interface`` - - :raises TypeError: With a human readable error message, the attribute - (of type `attrs.Attribute`), the expected interface, and the - value it got. - - .. deprecated:: 23.1.0 - """ - import warnings - - warnings.warn( - "attrs's zope-interface support is deprecated and will be removed in, " - "or after, April 2024.", - DeprecationWarning, - stacklevel=2, - ) - return _ProvidesValidator(interface) - - @attrs(repr=False, slots=True, hash=True) class _OptionalValidator: validator = attrib() @@ -264,11 +204,13 @@ class _OptionalValidator: def optional(validator): """ A validator that makes an attribute optional. An optional attribute is one - which can be set to ``None`` in addition to satisfying the requirements of + which can be set to `None` in addition to satisfying the requirements of the sub-validator. - :param Callable | tuple[Callable] | list[Callable] validator: A validator - (or validators) that is used for non-``None`` values. + :param validator: A validator (or validators) that is used for non-`None` + values. + :type validator: typing.Callable | tuple[typing.Callable] | + list[typing.Callable] .. versionadded:: 15.1.0 .. versionchanged:: 17.1.0 *validator* can be a list of validators. @@ -426,9 +368,7 @@ class _DeepMapping: self.value_validator(inst, attr, value[key]) def __repr__(self): - return ( - "" - ).format(key=self.key_validator, value=self.value_validator) + return f"" def deep_mapping(key_validator, value_validator, mapping_validator=None): @@ -550,7 +490,7 @@ class _MinLengthValidator: We use a callable class to be able to change the ``__repr__``. """ if len(value) < self.min_length: - msg = f"Length of '{attr.name}' must be => {self.min_length}: {len(value)}" + msg = f"Length of '{attr.name}' must be >= {self.min_length}: {len(value)}" raise ValueError(msg) def __repr__(self): @@ -640,12 +580,7 @@ class _NotValidator: ) def __repr__(self): - return ( - "" - ).format( - what=self.validator, - exc_types=self.exc_types, - ) + return f"" def not_(validator, *, msg=None, exc_types=(ValueError, TypeError)): diff --git a/lib/attr/validators.pyi b/lib/attr/validators.pyi index d194a75a..8b2617ad 100644 --- a/lib/attr/validators.pyi +++ b/lib/attr/validators.pyi @@ -5,20 +5,15 @@ from typing import ( Container, ContextManager, Iterable, - List, Mapping, Match, - Optional, Pattern, - Tuple, - Type, TypeVar, - Union, overload, ) -from . import _ValidatorType -from . import _ValidatorArgType +from attrs import _ValidatorType +from attrs import _ValidatorArgType _T = TypeVar("_T") _T1 = TypeVar("_T1") @@ -36,42 +31,41 @@ def disabled() -> ContextManager[None]: ... # To be more precise on instance_of use some overloads. # If there are more than 3 items in the tuple then we fall back to Any @overload -def instance_of(type: Type[_T]) -> _ValidatorType[_T]: ... +def instance_of(type: type[_T]) -> _ValidatorType[_T]: ... @overload -def instance_of(type: Tuple[Type[_T]]) -> _ValidatorType[_T]: ... +def instance_of(type: tuple[type[_T]]) -> _ValidatorType[_T]: ... @overload def instance_of( - type: Tuple[Type[_T1], Type[_T2]] -) -> _ValidatorType[Union[_T1, _T2]]: ... + type: tuple[type[_T1], type[_T2]] +) -> _ValidatorType[_T1 | _T2]: ... @overload def instance_of( - type: Tuple[Type[_T1], Type[_T2], Type[_T3]] -) -> _ValidatorType[Union[_T1, _T2, _T3]]: ... + type: tuple[type[_T1], type[_T2], type[_T3]] +) -> _ValidatorType[_T1 | _T2 | _T3]: ... @overload -def instance_of(type: Tuple[type, ...]) -> _ValidatorType[Any]: ... -def provides(interface: Any) -> _ValidatorType[Any]: ... +def instance_of(type: tuple[type, ...]) -> _ValidatorType[Any]: ... def optional( - validator: Union[ - _ValidatorType[_T], List[_ValidatorType[_T]], Tuple[_ValidatorType[_T]] - ] -) -> _ValidatorType[Optional[_T]]: ... + validator: ( + _ValidatorType[_T] + | list[_ValidatorType[_T]] + | tuple[_ValidatorType[_T]] + ), +) -> _ValidatorType[_T | None]: ... def in_(options: Container[_T]) -> _ValidatorType[_T]: ... def and_(*validators: _ValidatorType[_T]) -> _ValidatorType[_T]: ... def matches_re( - regex: Union[Pattern[AnyStr], AnyStr], + regex: Pattern[AnyStr] | AnyStr, flags: int = ..., - func: Optional[ - Callable[[AnyStr, AnyStr, int], Optional[Match[AnyStr]]] - ] = ..., + func: Callable[[AnyStr, AnyStr, int], Match[AnyStr] | None] | None = ..., ) -> _ValidatorType[AnyStr]: ... def deep_iterable( member_validator: _ValidatorArgType[_T], - iterable_validator: Optional[_ValidatorType[_I]] = ..., + iterable_validator: _ValidatorType[_I] | None = ..., ) -> _ValidatorType[_I]: ... def deep_mapping( key_validator: _ValidatorType[_K], value_validator: _ValidatorType[_V], - mapping_validator: Optional[_ValidatorType[_M]] = ..., + mapping_validator: _ValidatorType[_M] | None = ..., ) -> _ValidatorType[_M]: ... def is_callable() -> _ValidatorType[_T]: ... def lt(val: _T) -> _ValidatorType[_T]: ... @@ -83,6 +77,6 @@ def min_len(length: int) -> _ValidatorType[_T]: ... def not_( validator: _ValidatorType[_T], *, - msg: Optional[str] = None, - exc_types: Union[Type[Exception], Iterable[Type[Exception]]] = ..., + msg: str | None = None, + exc_types: type[Exception] | Iterable[type[Exception]] = ..., ) -> _ValidatorType[_T]: ... From 75392efb97d5196beae7080929087e4f1c894a3f Mon Sep 17 00:00:00 2001 From: JackDandy Date: Fri, 7 Jun 2024 15:47:27 +0100 Subject: [PATCH 13/29] =?UTF-8?q?Update=20profilehooks=20module=201.12.1?= =?UTF-8?q?=20(c3fc078)=20=E2=86=92=201.13.0.dev0=20(99f8a31).?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGES.md | 1 + lib/profilehooks.py | 236 +++----------------------------------------- 2 files changed, 14 insertions(+), 223 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index a21f0bb8..b94c3c4c 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -9,6 +9,7 @@ * Update filelock 3.12.4 (c1163ae) to 3.14.0 (8556141) * Update idna library 3.4 (cab054c) to 3.7 (1d365e1) * Update imdbpie 5.6.4 (f695e87) to 5.6.5 (f8ed7a0) +* Update profilehooks module 1.12.1 (c3fc078) to 1.13.0.dev0 (99f8a31) * Update Requests library 2.31.0 (8812812) to 2.32.3 (0e322af) * Update Tornado Web Server 6.4 (b3f2a4b) to 6.4.1 (2a0e1d1) * Update urllib3 2.0.7 (56f01e0) to 2.2.1 (54d6edf) diff --git a/lib/profilehooks.py b/lib/profilehooks.py index dc2d22c8..2d878f50 100644 --- a/lib/profilehooks.py +++ b/lib/profilehooks.py @@ -39,28 +39,12 @@ instead of a detailed (but costly) profile. Caveats - A thread on python-dev convinced me that hotshot produces bogus numbers. - See https://mail.python.org/pipermail/python-dev/2005-November/058264.html - I don't know what will happen if a decorated function will try to call another decorated function. All decorators probably need to explicitly support nested profiling (currently TraceFuncCoverage is the only one - that supports this, while HotShotFuncProfile has support for recursive - functions.) + that supports this.) - Profiling with hotshot creates temporary files (*.prof for profiling, - *.cprof for coverage) in the current directory. These files are not - cleaned up. Exception: when you specify a filename to the profile - decorator (to store the pstats.Stats object for later inspection), - the temporary file will be the filename you specified with '.raw' - appended at the end. - - Coverage analysis with hotshot seems to miss some executions resulting - in lower line counts and some lines errorneously marked as never - executed. For this reason coverage analysis now uses trace.py which is - slower, but more accurate. - -Copyright (c) 2004--2020 Marius Gedminas +Copyright (c) 2004--2023 Marius Gedminas Copyright (c) 2007 Hanno Schlichting Copyright (c) 2008 Florian Schulze @@ -86,8 +70,6 @@ Released under the MIT licence since December 2006: (Previously it was distributed under the GNU General Public Licence.) """ -from __future__ import print_function - import atexit import dis import functools @@ -104,19 +86,6 @@ import trace from profile import Profile -# For hotshot profiling (inaccurate!) -try: # pragma: PY2 - import hotshot - import hotshot.stats -except ImportError: - hotshot = None - - -# For hotshot coverage (inaccurate!; uses undocumented APIs; might break) -if hotshot is not None: # pragma: PY2 - import _hotshot - import hotshot.log - # For cProfile profiling (best) try: import cProfile @@ -127,38 +96,18 @@ except ImportError: __author__ = "Marius Gedminas " __copyright__ = "Copyright 2004-2020 Marius Gedminas and contributors" __license__ = "MIT" -__version__ = '1.12.1.dev0' -__date__ = "2020-08-20" +__version__ = '1.13.0.dev0' +__date__ = "2023-12-18" # registry of available profilers AVAILABLE_PROFILERS = {} -__all__ = ['coverage', 'coverage_with_hotshot', 'profile', 'timecall'] - - -# Use tokenize.open() on Python >= 3.2, fall back to open() on Python 2 -tokenize_open = getattr(tokenize, 'open', open) - - -try: - from inspect import unwrap as _unwrap -except ImportError: # pragma: PY2 - # inspect.unwrap() doesn't exist on Python 2 - def _unwrap(fn): - if not hasattr(fn, '__wrapped__'): - return fn - else: # pragma: nocover - # functools.wraps() doesn't set __wrapped__ on Python 2 either, - # so this branch will only get reached if somebody - # manually sets __wrapped__, hence the pragma: nocover. - # NB: intentionally using recursion here instead of a while loop to - # make cycles fail with a recursion error instead of looping forever. - return _unwrap(fn.__wrapped__) +__all__ = ['coverage', 'profile', 'timecall'] def _identify(fn): - fn = _unwrap(fn) + fn = inspect.unwrap(fn) funcname = fn.__name__ filename = fn.__code__.co_filename lineno = fn.__code__.co_firstlineno @@ -171,7 +120,7 @@ def _is_file_like(o): def profile(fn=None, skip=0, filename=None, immediate=False, dirs=False, sort=None, entries=40, - profiler=('cProfile', 'profile', 'hotshot'), + profiler=('cProfile', 'profile'), stdout=True): """Mark `fn` for profiling. @@ -208,7 +157,7 @@ def profile(fn=None, skip=0, filename=None, immediate=False, dirs=False, `profiler` can be used to select the preferred profiler, or specify a sequence of them, in order of preference. The default is ('cProfile'. - 'profile', 'hotshot'). + 'profile'). If `filename` is specified, the profile stats will be stored in the named file. You can load them with pstats.Stats(filename) or use a @@ -282,26 +231,7 @@ def coverage(fn): ... """ - fp = TraceFuncCoverage(fn) # or HotShotFuncCoverage - # We cannot return fp or fp.__call__ directly as that would break method - # definitions, instead we need to return a plain function. - - @functools.wraps(fn) - def new_fn(*args, **kw): - return fp(*args, **kw) - return new_fn - - -def coverage_with_hotshot(fn): # pragma: PY2 - """Mark `fn` for line coverage analysis. - - Uses the 'hotshot' module for fast coverage analysis. - - BUG: Produces inaccurate results. - - See the docstring of `coverage` for usage examples. - """ - fp = HotShotFuncCoverage(fn) + fp = TraceFuncCoverage(fn) # We cannot return fp or fp.__call__ directly as that would break method # definitions, instead we need to return a plain function. @@ -427,148 +357,8 @@ if cProfile is not None: AVAILABLE_PROFILERS['cProfile'] = CProfileFuncProfile -if hotshot is not None: # pragma: PY2 - - class HotShotFuncProfile(FuncProfile): - """Profiler for a function (uses hotshot).""" - - # This flag is shared between all instances - in_profiler = False - - def __init__(self, fn, skip=0, filename=None, immediate=False, - dirs=False, sort=None, entries=40, stdout=True): - """Creates a profiler for a function. - - Every profiler has its own log file (the name of which is derived - from the function name). - - HotShotFuncProfile registers an atexit handler that prints - profiling information to sys.stderr when the program terminates. - - The log file is not removed and remains there to clutter the - current working directory. - """ - if filename: - self.logfilename = filename + ".raw" - else: - self.logfilename = "%s.%d.prof" % (fn.__name__, os.getpid()) - super(HotShotFuncProfile, self).__init__( - fn, skip=skip, filename=filename, immediate=immediate, - dirs=dirs, sort=sort, entries=entries, stdout=stdout) - - def __call__(self, *args, **kw): - """Profile a singe call to the function.""" - self.ncalls += 1 - if self.skip > 0: - self.skip -= 1 - self.skipped += 1 - return self.fn(*args, **kw) - if HotShotFuncProfile.in_profiler: - # handle recursive calls - return self.fn(*args, **kw) - if self.profiler is None: - self.profiler = hotshot.Profile(self.logfilename) - try: - HotShotFuncProfile.in_profiler = True - return self.profiler.runcall(self.fn, *args, **kw) - finally: - HotShotFuncProfile.in_profiler = False - if self.immediate: - self.print_stats() - self.reset_stats() - - def print_stats(self): - if self.profiler is None: - self.stats = pstats.Stats(Profile()) - else: - self.profiler.close() - self.stats = hotshot.stats.load(self.logfilename) - super(HotShotFuncProfile, self).print_stats() - - def reset_stats(self): - self.profiler = None - self.ncalls = 0 - self.skipped = 0 - - AVAILABLE_PROFILERS['hotshot'] = HotShotFuncProfile - - class HotShotFuncCoverage: - """Coverage analysis for a function (uses _hotshot). - - HotShot coverage is reportedly faster than trace.py, but it appears to - have problems with exceptions; also line counts in coverage reports - are generally lower from line counts produced by TraceFuncCoverage. - Is this my bug, or is it a problem with _hotshot? - """ - - def __init__(self, fn): - """Creates a profiler for a function. - - Every profiler has its own log file (the name of which is derived - from the function name). - - HotShotFuncCoverage registers an atexit handler that prints - profiling information to sys.stderr when the program terminates. - - The log file is not removed and remains there to clutter the - current working directory. - """ - self.fn = fn - self.logfilename = "%s.%d.cprof" % (fn.__name__, os.getpid()) - self.profiler = _hotshot.coverage(self.logfilename) - self.ncalls = 0 - atexit.register(self.atexit) - - def __call__(self, *args, **kw): - """Profile a singe call to the function.""" - self.ncalls += 1 - old_trace = sys.gettrace() - try: - return self.profiler.runcall(self.fn, args, kw) - finally: # pragma: nocover - sys.settrace(old_trace) - - def atexit(self): - """Stop profiling and print profile information to sys.stderr. - - This function is registered as an atexit hook. - """ - self.profiler.close() - funcname, filename, lineno = _identify(self.fn) - print("") - print("*** COVERAGE RESULTS ***") - print("%s (%s:%s)" % (funcname, filename, lineno)) - print("function called %d times" % self.ncalls) - print("") - fs = FuncSource(self.fn) - reader = hotshot.log.LogReader(self.logfilename) - for what, (filename, lineno, funcname), tdelta in reader: - if filename != fs.filename: - continue - if what == hotshot.log.LINE: - fs.mark(lineno) - if what == hotshot.log.ENTER: - # hotshot gives us the line number of the function - # definition and never gives us a LINE event for the first - # statement in a function, so if we didn't perform this - # mapping, the first statement would be marked as never - # executed - if lineno == fs.firstlineno: - lineno = fs.firstcodelineno - fs.mark(lineno) - reader.close() - print(fs) - never_executed = fs.count_never_executed() - if never_executed: - print("%d lines were not executed." % never_executed) - - class TraceFuncCoverage: - """Coverage analysis for a function (uses trace module). - - HotShot coverage analysis is reportedly faster, but it appears to have - problems with exceptions. - """ + """Coverage analysis for a function (uses trace module).""" # Shared between all instances so that nested calls work tracer = trace.Trace(count=True, trace=False, @@ -657,11 +447,11 @@ class FuncSource: strs = self._find_docstrings(self.filename) lines = { ln - for off, ln in dis.findlinestarts(_unwrap(self.fn).__code__) + for off, ln in dis.findlinestarts(inspect.unwrap(self.fn).__code__) # skipping firstlineno because Python 3.11 adds a 'RESUME' opcode # attributed to the `def` line, but then trace.py never sees it # getting executed - if ln not in strs and ln != self.firstlineno + if ln is not None and ln not in strs and ln != self.firstlineno } for lineno in lines: self.sourcelines.setdefault(lineno, 0) @@ -676,7 +466,7 @@ class FuncSource: # Python 3.2 and removed in 3.6. strs = set() prev = token.INDENT # so module docstring is detected as docstring - with tokenize_open(filename) as f: + with tokenize.open(filename) as f: tokens = tokenize.generate_tokens(f.readline) for ttype, tstr, start, end, line in tokens: if ttype == token.STRING and prev == token.INDENT: From 4abcb950a0edfe72beaaf3c88aad53c2e77c098b Mon Sep 17 00:00:00 2001 From: JackDandy Date: Fri, 7 Jun 2024 15:56:56 +0100 Subject: [PATCH 14/29] =?UTF-8?q?Update=20diskcache=205.6.3=20(323787f)=20?= =?UTF-8?q?=E2=86=92=205.6.3=20(ebfa37c).?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGES.md | 1 + lib/diskcache/core.py | 7 +++++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index b94c3c4c..3ad10d51 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -5,6 +5,7 @@ * Update CacheControl 0.13.1 (783a338) to 0.14.0 (e2be0c2) * Update certifi 2024.02.02 to 2024.06.02 * Update dateutil 2.8.2 (296d419) to 2.9.0.post0 (0353b78) +* Update diskcache 5.6.3 (323787f) to 5.6.3 (ebfa37c) * Update feedparser 6.0.10 (9865dec) to 6.0.11 (efcb89b) * Update filelock 3.12.4 (c1163ae) to 3.14.0 (8556141) * Update idna library 3.4 (cab054c) to 3.7 (1d365e1) diff --git a/lib/diskcache/core.py b/lib/diskcache/core.py index 46f5532b..461a4359 100644 --- a/lib/diskcache/core.py +++ b/lib/diskcache/core.py @@ -532,7 +532,7 @@ class Cache: sql( 'CREATE INDEX IF NOT EXISTS Cache_expire_time ON' - ' Cache (expire_time)' + ' Cache (expire_time) WHERE expire_time IS NOT NULL' ) query = EVICTION_POLICY[self.eviction_policy]['init'] @@ -2030,7 +2030,10 @@ class Cache: """ sql = self._sql - sql('CREATE INDEX IF NOT EXISTS Cache_tag_rowid ON Cache(tag, rowid)') + sql( + 'CREATE INDEX IF NOT EXISTS Cache_tag_rowid ON Cache(tag, rowid) ' + 'WHERE tag IS NOT NULL' + ) self.reset('tag_index', 1) def drop_tag_index(self): From 1a830f244e908dee1537bb6c750f772025b2e433 Mon Sep 17 00:00:00 2001 From: JackDandy Date: Fri, 7 Jun 2024 16:35:14 +0100 Subject: [PATCH 15/29] =?UTF-8?q?Update=20pytz=202023.3/2023c=20(488d3eb)?= =?UTF-8?q?=20=E2=86=92=202024.1/2024a=20(3680953).?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGES.md | 1 + lib/pytz/__init__.py | 5 +- lib/pytz/zoneinfo/America/Godthab | Bin 1903 -> 1889 bytes lib/pytz/zoneinfo/America/Miquelon | Bin 1652 -> 1652 bytes lib/pytz/zoneinfo/America/Montreal | Bin 3494 -> 3494 bytes lib/pytz/zoneinfo/America/Nassau | Bin 3494 -> 3494 bytes lib/pytz/zoneinfo/America/Nipigon | Bin 3494 -> 3494 bytes lib/pytz/zoneinfo/America/Nuuk | Bin 1903 -> 1889 bytes lib/pytz/zoneinfo/America/Scoresbysund | Bin 1902 -> 1935 bytes lib/pytz/zoneinfo/America/Thunder_Bay | Bin 3494 -> 3494 bytes lib/pytz/zoneinfo/America/Toronto | Bin 3494 -> 3494 bytes lib/pytz/zoneinfo/Antarctica/Casey | Bin 370 -> 423 bytes lib/pytz/zoneinfo/Antarctica/Vostok | Bin 151 -> 213 bytes lib/pytz/zoneinfo/Asia/Almaty | Bin 983 -> 983 bytes lib/pytz/zoneinfo/Asia/Gaza | Bin 3808 -> 3844 bytes lib/pytz/zoneinfo/Asia/Hebron | Bin 3836 -> 3872 bytes lib/pytz/zoneinfo/Asia/Ho_Chi_Minh | Bin 337 -> 337 bytes lib/pytz/zoneinfo/Asia/Qostanay | Bin 997 -> 1025 bytes lib/pytz/zoneinfo/Asia/Saigon | Bin 337 -> 337 bytes lib/pytz/zoneinfo/Canada/Eastern | Bin 3494 -> 3494 bytes lib/pytz/zoneinfo/iso3166.tab | 17 +- lib/pytz/zoneinfo/leapseconds | 19 +- lib/pytz/zoneinfo/tzdata.zi | 3961 ++++++++++++------------ lib/pytz/zoneinfo/zone.tab | 24 +- lib/pytz/zoneinfo/zone1970.tab | 29 +- 25 files changed, 2034 insertions(+), 2022 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 3ad10d51..06091de0 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -11,6 +11,7 @@ * Update idna library 3.4 (cab054c) to 3.7 (1d365e1) * Update imdbpie 5.6.4 (f695e87) to 5.6.5 (f8ed7a0) * Update profilehooks module 1.12.1 (c3fc078) to 1.13.0.dev0 (99f8a31) +* Update pytz 2023.3/2023c (488d3eb) to 2024.1/2024a (3680953) * Update Requests library 2.31.0 (8812812) to 2.32.3 (0e322af) * Update Tornado Web Server 6.4 (b3f2a4b) to 6.4.1 (2a0e1d1) * Update urllib3 2.0.7 (56f01e0) to 2.2.1 (54d6edf) diff --git a/lib/pytz/__init__.py b/lib/pytz/__init__.py index 48e6c369..2355f8b4 100644 --- a/lib/pytz/__init__.py +++ b/lib/pytz/__init__.py @@ -22,8 +22,8 @@ from pytz.tzfile import build_tzinfo # The IANA (nee Olson) database is updated several times a year. -OLSON_VERSION = '2023c' -VERSION = '2023.3.post1' # pip compatible version number. +OLSON_VERSION = '2024a' +VERSION = '2024.1' # pip compatible version number. __version__ = VERSION OLSEN_VERSION = OLSON_VERSION # Old releases had this misspelling @@ -1311,7 +1311,6 @@ common_timezones = \ 'America/Whitehorse', 'America/Winnipeg', 'America/Yakutat', - 'America/Yellowknife', 'Antarctica/Casey', 'Antarctica/Davis', 'Antarctica/DumontDUrville', diff --git a/lib/pytz/zoneinfo/America/Godthab b/lib/pytz/zoneinfo/America/Godthab index adb7934aadf5f45928ce67e82efa92ca92391703..29958cf12a9df7cf775e3337b4894e4cf4b82f58 100644 GIT binary patch delta 54 zcmaFQ_mFRbGGoa`l?=wo6Bv(A{>UaeiJfgS3%l3ky-Yxf%@3K*Fm2w-YRrg`$_4-+ CSrVE6 delta 70 zcmaFJ_nvQpGGpmRl?+CfRNHkECigHNXJnrIkWGB@2R63J1x(qDrJL_ConZpYZeGG_ N&WIxIG1-A#5de#87;FFl diff --git a/lib/pytz/zoneinfo/America/Miquelon b/lib/pytz/zoneinfo/America/Miquelon index 5eccd861071d4eccb76fe0d8f5195bd1a7f646b7..f780ea990ff22675b5cc4e4dbe2c8690ae4e41b4 100644 GIT binary patch delta 23 ccmeyu^Mz-EF7thHjg1DeOdxvm1g0m<0BZ>dYybcN delta 23 ccmeyu^Mz-EF7q~v6&np=nLzaB2~1Cz0dxlmTmS$7 diff --git a/lib/pytz/zoneinfo/America/Montreal b/lib/pytz/zoneinfo/America/Montreal index 6752c5b05285678b86aea170f0921fc5f5e57738..170137333f9f6dba10ac3ad7f32876aaf518c4fd 100644 GIT binary patch delta 82 zcmZ1`y-a$-FR^1Et|{z)aNTsphZ_tFAKVn}`f$s5Qo?2d#`UaP$3FZA0+2k22B`tj SAax)bq!vVj)xTuh%LV{v;5Kvs delta 82 zcmZ1`y-a$-FR@h@t|=@&aNV@$!VQMj12;wUF5EIMbJ#4vxSmyO)rJ2+0FnpMAT=Nw Rqz*)b)PiWR`j>2b*#PJ9Go%0j diff --git a/lib/pytz/zoneinfo/America/Nassau b/lib/pytz/zoneinfo/America/Nassau index 6752c5b05285678b86aea170f0921fc5f5e57738..170137333f9f6dba10ac3ad7f32876aaf518c4fd 100644 GIT binary patch delta 82 zcmZ1`y-a$-FR^1Et|{z)aNTsphZ_tFAKVn}`f$s5Qo?2d#`UaP$3FZA0+2k22B`tj SAax)bq!vVj)xTuh%LV{v;5Kvs delta 82 zcmZ1`y-a$-FR@h@t|=@&aNV@$!VQMj12;wUF5EIMbJ#4vxSmyO)rJ2+0FnpMAT=Nw Rqz*)b)PiWR`j>2b*#PJ9Go%0j diff --git a/lib/pytz/zoneinfo/America/Nipigon b/lib/pytz/zoneinfo/America/Nipigon index 6752c5b05285678b86aea170f0921fc5f5e57738..170137333f9f6dba10ac3ad7f32876aaf518c4fd 100644 GIT binary patch delta 82 zcmZ1`y-a$-FR^1Et|{z)aNTsphZ_tFAKVn}`f$s5Qo?2d#`UaP$3FZA0+2k22B`tj SAax)bq!vVj)xTuh%LV{v;5Kvs delta 82 zcmZ1`y-a$-FR@h@t|=@&aNV@$!VQMj12;wUF5EIMbJ#4vxSmyO)rJ2+0FnpMAT=Nw Rqz*)b)PiWR`j>2b*#PJ9Go%0j diff --git a/lib/pytz/zoneinfo/America/Nuuk b/lib/pytz/zoneinfo/America/Nuuk index adb7934aadf5f45928ce67e82efa92ca92391703..29958cf12a9df7cf775e3337b4894e4cf4b82f58 100644 GIT binary patch delta 54 zcmaFQ_mFRbGGoa`l?=wo6Bv(A{>UaeiJfgS3%l3ky-Yxf%@3K*Fm2w-YRrg`$_4-+ CSrVE6 delta 70 zcmaFJ_nvQpGGpmRl?+CfRNHkECigHNXJnrIkWGB@2R63J1x(qDrJL_ConZpYZeGG_ N&WIxIG1-A#5de#87;FFl diff --git a/lib/pytz/zoneinfo/America/Scoresbysund b/lib/pytz/zoneinfo/America/Scoresbysund index 286d13216eb3b14bf9fec1fceebf2fe731472d39..9bf411ef5ad5f81f3863223ea75117404d5d1421 100644 GIT binary patch delta 251 zcmaFI*UzsJ5|x=|%zyznfg&Ib0;NFAxlzNANrs&RH=NAIuEq2J|HluE9RL46e!##o z*??W#n2`}=0+3{6B-W~RKBOqyLr{im^XR2qQuWP8|YiIyw H8gKys5jrGH delta 204 zcmeC@f5)d05|x=|#DD?Vfg&Ib0;NFAzEQ)FY4QO!naN`8T5dkRAq=_(MnG)Hplx6P zlxGA2kXj&vfmkCqe`R82fthKC-%KtWpj~!`HbC3#bbO8VO!W-(4Rm}B4ZtizE&#Rx B8D#(f diff --git a/lib/pytz/zoneinfo/America/Thunder_Bay b/lib/pytz/zoneinfo/America/Thunder_Bay index 6752c5b05285678b86aea170f0921fc5f5e57738..170137333f9f6dba10ac3ad7f32876aaf518c4fd 100644 GIT binary patch delta 82 zcmZ1`y-a$-FR^1Et|{z)aNTsphZ_tFAKVn}`f$s5Qo?2d#`UaP$3FZA0+2k22B`tj SAax)bq!vVj)xTuh%LV{v;5Kvs delta 82 zcmZ1`y-a$-FR@h@t|=@&aNV@$!VQMj12;wUF5EIMbJ#4vxSmyO)rJ2+0FnpMAT=Nw Rqz*)b)PiWR`j>2b*#PJ9Go%0j diff --git a/lib/pytz/zoneinfo/America/Toronto b/lib/pytz/zoneinfo/America/Toronto index 6752c5b05285678b86aea170f0921fc5f5e57738..170137333f9f6dba10ac3ad7f32876aaf518c4fd 100644 GIT binary patch delta 82 zcmZ1`y-a$-FR^1Et|{z)aNTsphZ_tFAKVn}`f$s5Qo?2d#`UaP$3FZA0+2k22B`tj SAax)bq!vVj)xTuh%LV{v;5Kvs delta 82 zcmZ1`y-a$-FR@h@t|=@&aNV@$!VQMj12;wUF5EIMbJ#4vxSmyO)rJ2+0FnpMAT=Nw Rqz*)b)PiWR`j>2b*#PJ9Go%0j diff --git a/lib/pytz/zoneinfo/Antarctica/Casey b/lib/pytz/zoneinfo/Antarctica/Casey index 4b98133d7af497a840e2630bc72a02439c4d774f..586a7653ef20d04440bfcb6645454ef215a1bba4 100644 GIT binary patch literal 423 zcmWHE%1kq2zyK^j5fBCeK_CXPdH%_rY4Ezmrr_Ow>Vx0PIST&HXD38(SvN#2;TMSB zDdG^*E87q|&D9_-`6xqt#R8iIpX~vO5luEpIx_>3EmCY!I5sjcGBGkS12G(eYyg3R z29PDQHZX8N*t!M=4B7@34BCcAOP|lhz5BOM1wpDqCp-7(IC%)Xo@||WdjTXJ6#Jd E02EY6j{pDw delta 150 zcmZ3^{E2CTxF9P70|N+y01ptePSh~rVPs-tVg_OmoY*KK!@w|W0|SSyfdPZIfdzxM ep&>{$BZRFb;1VjD<0ZxOsY=Fku I>6&o?0LBIr)c^nh diff --git a/lib/pytz/zoneinfo/Asia/Almaty b/lib/pytz/zoneinfo/Asia/Almaty index 91c916a3a5d7949cc304532fe3fb2eac117ab2ef..855abbd6e3ee338c3d46bee7b592f3d645d2c273 100644 GIT binary patch delta 134 zcmcc4ew}@SG%qIu1egFR&WRcqSW+JxQ<(U}n2~X^Hlvw{k8cQrwt*=Sn**^KSncE* hMg>%Tn^!WXS+eYkYoCHe1rxa`{yT%PTM%U>4LIxC+xl2z4acv+hSfMCtBc*n zeqCM;Z1g&Z-Z*d=HnD8R#cS-voh1rpx!mnOLWh zlm!cIp1-+Z@%g37x{G;tt_}~`qVK`By(YLP!H?6RC$#|X^}U3?cf=3w^Y4WFqB14_ z$>TJzTdZ#6d^iQ`_fA0H?@Z@phcXPk%JJ4A4OWk0VW9IQJb2Lo50)a>;AA{54AmY) zKOCA54`(il&oMUK5d@DEY@r`nTYyJn)bOZI`sv1Q9T2w}8?&huVyM{I_9mynxN;sB zCK5M^Y{C>RdE5j~Uil191y8|K*+$7PQ#cKrr<-8s?mP6;Nhu0COczRj{+VN4@XSIT z`dOcL(H9t-P0N%9J7z4*bv=aVR<63~@I8E&BV8Q5t5pxXKI}@~$mP~Z|Fbe-*<+t@ HL$3J`1v@2F delta 1028 zcmXBST}V@57zgloT8UO>r1^mk$)N8nNet2`yO2VNB#gqOZuCOJEpHN`bINU++Wa_f zZfZw!8W9NLEDJPhrkkHjH=Rx+g|I|}lTefxcAoz^H}CU%{{Q#v;!LMrhiJxwDngQ~v8U{OJ7%Pp-)GlQ~u)W+;m( z3ksF3op4p!14%Bhs)BCC@4Df~d^KzgKZL$9J%?({D4LO1-_jzlUNj@Gi4KHo?%zXR zd(lXBR{JJRHYX2D8WiesZ=fJ3?`4Aih2p*_T&O>r3Y&tnU{g+(h6YoG9yTAi4x5)x z!3{}&(QmkYl&6Iy=nQhp&|c(LmM78gGi!=hS$vB_p|LB8r$wVL6mHsj9(mK?Cfr;X zj=Z_OMDg5r_{p>Zmecv1hcNvg4rV>{r1Eyc(OZFbxnoLGXL!70d{-b~n~ zjYsZ!u6zpDay0Ux?&HXZGhFa6m#la)8GUY~J`9Udbs-j`nQe-@BH^*3HF#`g9v)A) z#ON8XRX&CL<}TRn&}nFJOFi%>?Hu|OsXyTf>k-AyT%Hz_SNg~t^Tc|{%#&w9!6R;i zy(e2>@5U>5D(#$xhN*%gcsiyRo}O=jeIcE&FFl5*g>PMiXL^f~&n#Y*Xqb(1z_Wf{ e&gK7&zr)1S#^1An3;v}YPRfpZ~FO(`0+7)t%#?JXbQIq=Tq>+q!gmK)8kUJ)E|wu0>p(?*S|!7#05 zl!h{hIm4&#B(bMVUwud&{?MJNb$di-++Ajr?dQtR6r(Cwc@@+J)BBA009T?-Mqbc& zA}9blXBox7<_)7Wu)HcL3(}*YDi{tj>Voq&LEOX@+nyjV`0ip9F!6n5=R(KwKl$-3>G|J;^n%AC z^g&^9M|#n`EceA1i}aH9UV3TF|L@;np%2O_ttI*9Ko2ul?w-&CJdB`+g)9CldVp>f tJ;3&-pcq&=MGr7NhxdWe40?b*KYDv&(U3;hAsr%m2t@>eLpB7VwAqqDlY>N3Q$u(`Z77Q1jFFlj z&>}=Hhz5tWbO?u{EutkP$OmtXNb06$LvX|kLBLBHyQkpjbwN*ZeCI&UoxlKZkf}bLweZ9nei>KNj7qTI+0$ym$yQw85m|8pmdS%OV3XXK5~!1NFMx`Tv+e~zm3;ja7_#xj zs}s3DyI+(0bBT`3=UeZFdC7;1!nCk*Cv(v|By-7Ekh%18OZ#m4%<819SO2o|pPKq? zj=_D?A@f7muI$$fnw+aYwFGwToLw9PatCLLS%H2sm=_o!J({b f;2Rm+0ql^0QGs0H%vlBWlPw=;5oO+jrvvGK>WF^6 diff --git a/lib/pytz/zoneinfo/Asia/Ho_Chi_Minh b/lib/pytz/zoneinfo/Asia/Ho_Chi_Minh index a213d290e1a920271e924506a57191f1f4bfc4c0..9c45ed991a230919d129c63c660aa8d1ad74d0ea 100644 GIT binary patch delta 25 hcmcb}bdhO-8|$YDZ}n$PW@L1qI4Ozg(}aoNq5+R=3x5Cr delta 25 hcmcb}bdhO-8|#yXxB4?CGcvkQoRq}$q+#N>XaJ1b3qt?^ diff --git a/lib/pytz/zoneinfo/Asia/Qostanay b/lib/pytz/zoneinfo/Asia/Qostanay index f8baf676496adea298f078d6d5931ba7aa64e902..2ee9ef7e9859bc95da2450e3b282fd4275cbbb39 100644 GIT binary patch delta 113 zcmaFL-pDaQnwN(G0!)Au&qR$2EU6EUDNOcZHfCg+T+i&p#=zhfz`!y2AhR?h1G@6f Z8yM#>a)9*X)WpTWWut9iYNu<;1pxiI75@ML delta 78 zcmZqVc*;IOnwOga0*rwa_e6~glM|SYCmS$2O}@h{jV`@;591t0WCfgDHrfVecDiO< E0Bdg$5dZ)H diff --git a/lib/pytz/zoneinfo/Asia/Saigon b/lib/pytz/zoneinfo/Asia/Saigon index a213d290e1a920271e924506a57191f1f4bfc4c0..9c45ed991a230919d129c63c660aa8d1ad74d0ea 100644 GIT binary patch delta 25 hcmcb}bdhO-8|$YDZ}n$PW@L1qI4Ozg(}aoNq5+R=3x5Cr delta 25 hcmcb}bdhO-8|#yXxB4?CGcvkQoRq}$q+#N>XaJ1b3qt?^ diff --git a/lib/pytz/zoneinfo/Canada/Eastern b/lib/pytz/zoneinfo/Canada/Eastern index 6752c5b05285678b86aea170f0921fc5f5e57738..170137333f9f6dba10ac3ad7f32876aaf518c4fd 100644 GIT binary patch delta 82 zcmZ1`y-a$-FR^1Et|{z)aNTsphZ_tFAKVn}`f$s5Qo?2d#`UaP$3FZA0+2k22B`tj SAax)bq!vVj)xTuh%LV{v;5Kvs delta 82 zcmZ1`y-a$-FR@h@t|=@&aNV@$!VQMj12;wUF5EIMbJ#4vxSmyO)rJ2+0FnpMAT=Nw Rqz*)b)PiWR`j>2b*#PJ9Go%0j diff --git a/lib/pytz/zoneinfo/iso3166.tab b/lib/pytz/zoneinfo/iso3166.tab index be3348d1..402c015e 100644 --- a/lib/pytz/zoneinfo/iso3166.tab +++ b/lib/pytz/zoneinfo/iso3166.tab @@ -3,17 +3,22 @@ # This file is in the public domain, so clarified as of # 2009-05-17 by Arthur David Olson. # -# From Paul Eggert (2022-11-18): +# From Paul Eggert (2023-09-06): # This file contains a table of two-letter country codes. Columns are # separated by a single tab. Lines beginning with '#' are comments. # All text uses UTF-8 encoding. The columns of the table are as follows: # # 1. ISO 3166-1 alpha-2 country code, current as of -# ISO 3166-1 N1087 (2022-09-02). See: Updates on ISO 3166-1 -# https://isotc.iso.org/livelink/livelink/Open/16944257 -# 2. The usual English name for the coded region, -# chosen so that alphabetic sorting of subsets produces helpful lists. -# This is not the same as the English name in the ISO 3166 tables. +# ISO/TC 46 N1108 (2023-04-05). See: ISO/TC 46 Documents +# https://www.iso.org/committee/48750.html?view=documents +# 2. The usual English name for the coded region. This sometimes +# departs from ISO-listed names, sometimes so that sorted subsets +# of names are useful (e.g., "Samoa (American)" and "Samoa +# (western)" rather than "American Samoa" and "Samoa"), +# sometimes to avoid confusion among non-experts (e.g., +# "Czech Republic" and "Turkey" rather than "Czechia" and "Türkiye"), +# and sometimes to omit needless detail or churn (e.g., "Netherlands" +# rather than "Netherlands (the)" or "Netherlands (Kingdom of the)"). # # The table is sorted by country code. # diff --git a/lib/pytz/zoneinfo/leapseconds b/lib/pytz/zoneinfo/leapseconds index a6a170aa..ce150bfe 100644 --- a/lib/pytz/zoneinfo/leapseconds +++ b/lib/pytz/zoneinfo/leapseconds @@ -3,13 +3,10 @@ # This file is in the public domain. # This file is generated automatically from the data in the public-domain -# NIST format leap-seconds.list file, which can be copied from -# -# or . -# The NIST file is used instead of its IERS upstream counterpart +# NIST/IERS format leap-seconds.list file, which can be copied from # -# because under US law the NIST file is public domain -# whereas the IERS file's copyright and license status is unclear. +# or, in a variant with different comments, from +# . # For more about leap-seconds.list, please see # The NTP Timescale and Leap Seconds # . @@ -72,11 +69,11 @@ Leap 2016 Dec 31 23:59:60 + S # Any additional leap seconds will come after this. # This Expires line is commented out for now, # so that pre-2020a zic implementations do not reject this file. -#Expires 2023 Dec 28 00:00:00 +#Expires 2024 Dec 28 00:00:00 # POSIX timestamps for the data in this file: -#updated 1467936000 (2016-07-08 00:00:00 UTC) -#expires 1703721600 (2023-12-28 00:00:00 UTC) +#updated 1704708379 (2024-01-08 10:06:19 UTC) +#expires 1735344000 (2024-12-28 00:00:00 UTC) -# Updated through IERS Bulletin C65 -# File expires on: 28 December 2023 +# Updated through IERS Bulletin C (https://hpiers.obspm.fr/iers/bul/bulc/bulletinc.dat) +# File expires on 28 December 2024 diff --git a/lib/pytz/zoneinfo/tzdata.zi b/lib/pytz/zoneinfo/tzdata.zi index 23d99be4..b5a03be7 100644 --- a/lib/pytz/zoneinfo/tzdata.zi +++ b/lib/pytz/zoneinfo/tzdata.zi @@ -1,4 +1,4 @@ -# version unknown-dirty +# version unknown # This zic input file is in the public domain. R d 1916 o - Jun 14 23s 1 S R d 1916 1919 - O Su>=1 23s 0 - @@ -22,27 +22,6 @@ R d 1978 o - Mar 24 1 1 S R d 1978 o - S 22 3 0 - R d 1980 o - Ap 25 0 1 S R d 1980 o - O 31 2 0 - -Z Africa/Algiers 0:12:12 - LMT 1891 Mar 16 -0:9:21 - PMT 1911 Mar 11 -0 d WE%sT 1940 F 25 2 -1 d CE%sT 1946 O 7 -0 - WET 1956 Ja 29 -1 - CET 1963 Ap 14 -0 d WE%sT 1977 O 21 -1 d CE%sT 1979 O 26 -0 d WE%sT 1981 May -1 - CET -Z Atlantic/Cape_Verde -1:34:4 - LMT 1912 Ja 1 2u --2 - -02 1942 S --2 1 -01 1945 O 15 --2 - -02 1975 N 25 2 --1 - -01 -Z Africa/Ndjamena 1:0:12 - LMT 1912 -1 - WAT 1979 O 14 -1 1 WAST 1980 Mar 8 -1 - WAT -Z Africa/Abidjan -0:16:8 - LMT 1912 -0 - GMT R K 1940 o - Jul 15 0 1 S R K 1940 o - O 1 0 0 - R K 1941 o - Ap 15 0 1 S @@ -77,21 +56,6 @@ R K 2014 o - Jul 31 24 1 S R K 2014 o - S lastTh 24 0 - R K 2023 ma - Ap lastF 0 1 S R K 2023 ma - O lastTh 24 0 - -Z Africa/Cairo 2:5:9 - LMT 1900 O -2 K EE%sT -Z Africa/Bissau -1:2:20 - LMT 1912 Ja 1 1u --1 - -01 1975 -0 - GMT -Z Africa/Nairobi 2:27:16 - LMT 1908 May -2:30 - +0230 1928 Jun 30 24 -3 - EAT 1930 Ja 4 24 -2:30 - +0230 1936 D 31 24 -2:45 - +0245 1942 Jul 31 24 -3 - EAT -Z Africa/Monrovia -0:43:8 - LMT 1882 --0:43:8 - MMT 1919 Mar --0:44:30 - MMT 1972 Ja 7 -0 - GMT R L 1951 o - O 14 2 1 S R L 1952 o - Ja 1 0 0 - R L 1953 o - O 9 2 1 S @@ -109,21 +73,10 @@ R L 1997 o - Ap 4 0 1 S R L 1997 o - O 4 0 0 - R L 2013 o - Mar lastF 1 1 S R L 2013 o - O lastF 2 0 - -Z Africa/Tripoli 0:52:44 - LMT 1920 -1 L CE%sT 1959 -2 - EET 1982 -1 L CE%sT 1990 May 4 -2 - EET 1996 S 30 -1 L CE%sT 1997 O 4 -2 - EET 2012 N 10 2 -1 L CE%sT 2013 O 25 2 -2 - EET R MU 1982 o - O 10 0 1 - R MU 1983 o - Mar 21 0 0 - R MU 2008 o - O lastSu 2 1 - R MU 2009 o - Mar lastSu 2 0 - -Z Indian/Mauritius 3:50 - LMT 1907 -4 MU +04/+05 R M 1939 o - S 12 0 1 - R M 1939 o - N 19 0 0 - R M 1940 o - F 25 0 1 - @@ -307,53 +260,15 @@ R M 2086 o - Ap 14 3 -1 - R M 2086 o - May 19 2 0 - R M 2087 o - Mar 30 3 -1 - R M 2087 o - May 11 2 0 - -Z Africa/Casablanca -0:30:20 - LMT 1913 O 26 -0 M +00/+01 1984 Mar 16 -1 - +01 1986 -0 M +00/+01 2018 O 28 3 -1 M +01/+00 -Z Africa/El_Aaiun -0:52:48 - LMT 1934 --1 - -01 1976 Ap 14 -0 M +00/+01 2018 O 28 3 -1 M +01/+00 -Z Africa/Maputo 2:10:20 - LMT 1903 Mar -2 - CAT R NA 1994 o - Mar 21 0 -1 WAT R NA 1994 2017 - S Su>=1 2 0 CAT R NA 1995 2017 - Ap Su>=1 2 -1 WAT -Z Africa/Windhoek 1:8:24 - LMT 1892 F 8 -1:30 - +0130 1903 Mar -2 - SAST 1942 S 20 2 -2 1 SAST 1943 Mar 21 2 -2 - SAST 1990 Mar 21 -2 NA %s -Z Africa/Lagos 0:13:35 - LMT 1905 Jul -0 - GMT 1908 Jul -0:13:35 - LMT 1914 -0:30 - +0030 1919 S -1 - WAT -Z Africa/Sao_Tome 0:26:56 - LMT 1884 --0:36:45 - LMT 1912 Ja 1 0u -0 - GMT 2018 Ja 1 1 -1 - WAT 2019 Ja 1 2 -0 - GMT R SA 1942 1943 - S Su>=15 2 1 - R SA 1943 1944 - Mar Su>=15 2 0 - -Z Africa/Johannesburg 1:52 - LMT 1892 F 8 -1:30 - SAST 1903 Mar -2 SA SAST R SD 1970 o - May 1 0 1 S R SD 1970 1985 - O 15 0 0 - R SD 1971 o - Ap 30 0 1 S R SD 1972 1985 - Ap lastSu 0 1 S -Z Africa/Khartoum 2:10:8 - LMT 1931 -2 SD CA%sT 2000 Ja 15 12 -3 - EAT 2017 N -2 - CAT -Z Africa/Juba 2:6:28 - LMT 1931 -2 SD CA%sT 2000 Ja 15 12 -3 - EAT 2021 F -2 - CAT R n 1939 o - Ap 15 23s 1 S R n 1939 o - N 18 23s 0 - R n 1940 o - F 25 23s 1 S @@ -379,80 +294,14 @@ R n 2005 o - May 1 0s 1 S R n 2005 o - S 30 1s 0 - R n 2006 2008 - Mar lastSu 2s 1 S R n 2006 2008 - O lastSu 2s 0 - -Z Africa/Tunis 0:40:44 - LMT 1881 May 12 -0:9:21 - PMT 1911 Mar 11 -1 n CE%sT -Z Antarctica/Casey 0 - -00 1969 -8 - +08 2009 O 18 2 -11 - +11 2010 Mar 5 2 -8 - +08 2011 O 28 2 -11 - +11 2012 F 21 17u -8 - +08 2016 O 22 -11 - +11 2018 Mar 11 4 -8 - +08 2018 O 7 4 -11 - +11 2019 Mar 17 3 -8 - +08 2019 O 4 3 -11 - +11 2020 Mar 8 3 -8 - +08 2020 O 4 0:1 -11 - +11 -Z Antarctica/Davis 0 - -00 1957 Ja 13 -7 - +07 1964 N -0 - -00 1969 F -7 - +07 2009 O 18 2 -5 - +05 2010 Mar 10 20u -7 - +07 2011 O 28 2 -5 - +05 2012 F 21 20u -7 - +07 -Z Antarctica/Mawson 0 - -00 1954 F 13 -6 - +06 2009 O 18 2 -5 - +05 R Tr 2005 ma - Mar lastSu 1u 2 +02 R Tr 2004 ma - O lastSu 1u 0 +00 -Z Antarctica/Troll 0 - -00 2005 F 12 -0 Tr %s -Z Antarctica/Rothera 0 - -00 1976 D --3 - -03 -Z Asia/Kabul 4:36:48 - LMT 1890 -4 - +04 1945 -4:30 - +0430 R AM 2011 o - Mar lastSu 2s 1 - R AM 2011 o - O lastSu 2s 0 - -Z Asia/Yerevan 2:58 - LMT 1924 May 2 -3 - +03 1957 Mar -4 R +04/+05 1991 Mar 31 2s -3 R +03/+04 1995 S 24 2s -4 - +04 1997 -4 R +04/+05 2011 -4 AM +04/+05 R AZ 1997 2015 - Mar lastSu 4 1 - R AZ 1997 2015 - O lastSu 5 0 - -Z Asia/Baku 3:19:24 - LMT 1924 May 2 -3 - +03 1957 Mar -4 R +04/+05 1991 Mar 31 2s -3 R +03/+04 1992 S lastSu 2s -4 - +04 1996 -4 E +04/+05 1997 -4 AZ +04/+05 R BD 2009 o - Jun 19 23 1 - R BD 2009 o - D 31 24 0 - -Z Asia/Dhaka 6:1:40 - LMT 1890 -5:53:20 - HMT 1941 O -6:30 - +0630 1942 May 15 -5:30 - +0530 1942 S -6:30 - +0630 1951 S 30 -6 - +06 2009 -6 BD +06/+07 -Z Asia/Thimphu 5:58:36 - LMT 1947 Au 15 -5:30 - +0530 1987 O -6 - +06 -Z Indian/Chagos 4:49:40 - LMT 1907 -5 - +05 1996 -6 - +06 -Z Asia/Yangon 6:24:47 - LMT 1880 -6:24:47 - RMT 1920 -6:30 - +0630 1942 May -9 - +09 1945 May 3 -6:30 - +0630 R Sh 1919 o - Ap 12 24 1 D R Sh 1919 o - S 30 24 0 S R Sh 1940 o - Jun 1 0 1 D @@ -470,11 +319,6 @@ R Sh 1948 1949 - S 30 24 0 S R CN 1986 o - May 4 2 1 D R CN 1986 1991 - S Su>=11 2 0 S R CN 1987 1991 - Ap Su>=11 2 1 D -Z Asia/Shanghai 8:5:43 - LMT 1901 -8 Sh C%sT 1949 May 28 -8 CN C%sT -Z Asia/Urumqi 5:50:20 - LMT 1928 -6 - +06 R HK 1946 o - Ap 21 0 1 S R HK 1946 o - D 1 3:30s 0 - R HK 1947 o - Ap 13 3:30s 1 S @@ -489,12 +333,6 @@ R HK 1965 1976 - O Su>=16 3:30 0 - R HK 1973 o - D 30 3:30 1 S R HK 1979 o - May 13 3:30 1 S R HK 1979 o - O 21 3:30 0 - -Z Asia/Hong_Kong 7:36:42 - LMT 1904 O 29 17u -8 - HKT 1941 Jun 15 3 -8 1 HKST 1941 O 1 4 -8 0:30 HKWT 1941 D 25 -9 - JST 1945 N 18 2 -8 HK HK%sT R f 1946 o - May 15 0 1 D R f 1946 o - O 1 0 0 S R f 1947 o - Ap 15 0 1 D @@ -510,10 +348,6 @@ R f 1974 1975 - Ap 1 0 1 D R f 1974 1975 - O 1 0 0 S R f 1979 o - Jul 1 0 1 D R f 1979 o - O 1 0 0 S -Z Asia/Taipei 8:6 - LMT 1896 -8 - CST 1937 O -9 - JST 1945 S 21 1 -8 f C%sT R _ 1942 1943 - Ap 30 23 1 - R _ 1942 o - N 17 23 0 - R _ 1943 o - S 30 23 0 S @@ -541,10 +375,6 @@ R _ 1973 o - D 30 3:30 1 D R _ 1975 1976 - Ap Su>=16 3:30 1 D R _ 1979 o - May 13 3:30 1 D R _ 1979 o - O Su>=16 3:30 0 S -Z Asia/Macau 7:34:10 - LMT 1904 O 30 -8 - CST 1941 D 21 23 -9 _ +09/+10 1945 S 30 24 -8 _ C%sT R CY 1975 o - Ap 13 0 1 S R CY 1975 o - O 12 0 0 - R CY 1976 o - May 15 0 1 S @@ -554,65 +384,6 @@ R CY 1977 o - S 25 0 0 - R CY 1978 o - O 2 0 0 - R CY 1979 1997 - S lastSu 0 0 - R CY 1981 1998 - Mar lastSu 0 1 S -Z Asia/Nicosia 2:13:28 - LMT 1921 N 14 -2 CY EE%sT 1998 S -2 E EE%sT -Z Asia/Famagusta 2:15:48 - LMT 1921 N 14 -2 CY EE%sT 1998 S -2 E EE%sT 2016 S 8 -3 - +03 2017 O 29 1u -2 E EE%sT -Z Asia/Tbilisi 2:59:11 - LMT 1880 -2:59:11 - TBMT 1924 May 2 -3 - +03 1957 Mar -4 R +04/+05 1991 Mar 31 2s -3 R +03/+04 1992 -3 e +03/+04 1994 S lastSu -4 e +04/+05 1996 O lastSu -4 1 +05 1997 Mar lastSu -4 e +04/+05 2004 Jun 27 -3 R +03/+04 2005 Mar lastSu 2 -4 - +04 -Z Asia/Dili 8:22:20 - LMT 1912 -8 - +08 1942 F 21 23 -9 - +09 1976 May 3 -8 - +08 2000 S 17 -9 - +09 -Z Asia/Kolkata 5:53:28 - LMT 1854 Jun 28 -5:53:20 - HMT 1870 -5:21:10 - MMT 1906 -5:30 - IST 1941 O -5:30 1 +0630 1942 May 15 -5:30 - IST 1942 S -5:30 1 +0630 1945 O 15 -5:30 - IST -Z Asia/Jakarta 7:7:12 - LMT 1867 Au 10 -7:7:12 - BMT 1923 D 31 16:40u -7:20 - +0720 1932 N -7:30 - +0730 1942 Mar 23 -9 - +09 1945 S 23 -7:30 - +0730 1948 May -8 - +08 1950 May -7:30 - +0730 1964 -7 - WIB -Z Asia/Pontianak 7:17:20 - LMT 1908 May -7:17:20 - PMT 1932 N -7:30 - +0730 1942 Ja 29 -9 - +09 1945 S 23 -7:30 - +0730 1948 May -8 - +08 1950 May -7:30 - +0730 1964 -8 - WITA 1988 -7 - WIB -Z Asia/Makassar 7:57:36 - LMT 1920 -7:57:36 - MMT 1932 N -8 - +08 1942 F 9 -9 - +09 1945 S 23 -8 - WITA -Z Asia/Jayapura 9:22:48 - LMT 1932 N -9 - +09 1944 S -9:30 - +0930 1964 -9 - WIT R i 1910 o - Ja 1 0 0 - R i 1977 o - Mar 21 23 1 - R i 1977 o - O 20 24 0 - @@ -653,11 +424,6 @@ R i 2020 o - Mar 20 24 1 - R i 2020 o - S 20 24 0 - R i 2021 2022 - Mar 21 24 1 - R i 2021 2022 - S 21 24 0 - -Z Asia/Tehran 3:25:44 - LMT 1916 -3:25:44 - TMT 1935 Jun 13 -3:30 i +0330/+0430 1977 O 20 24 -4 i +04/+05 1979 -3:30 i +0330/+0430 R IQ 1982 o - May 1 0 1 - R IQ 1982 1984 - O 1 0 0 - R IQ 1983 o - Mar 31 0 1 - @@ -666,10 +432,6 @@ R IQ 1985 1990 - S lastSu 1s 0 - R IQ 1986 1990 - Mar lastSu 1s 1 - R IQ 1991 2007 - Ap 1 3s 1 - R IQ 1991 2007 - O 1 3s 0 - -Z Asia/Baghdad 2:57:40 - LMT 1890 -2:57:36 - BMT 1918 -3 - +03 1982 May -3 IQ +03/+04 R Z 1940 o - May 31 24u 1 D R Z 1940 o - S 30 24u 0 S R Z 1940 o - N 16 24u 1 D @@ -755,15 +517,10 @@ R Z 2011 o - O 2 2 0 S R Z 2012 o - S 23 2 0 S R Z 2013 ma - Mar F>=23 2 1 D R Z 2013 ma - O lastSu 2 0 S -Z Asia/Jerusalem 2:20:54 - LMT 1880 -2:20:40 - JMT 1918 -2 Z I%sT R JP 1948 o - May Sa>=1 24 1 D R JP 1948 1951 - S Sa>=8 25 0 S R JP 1949 o - Ap Sa>=1 24 1 D R JP 1950 1951 - May Sa>=1 24 1 D -Z Asia/Tokyo 9:18:59 - LMT 1887 D 31 15u -9 JP J%sT R J 1973 o - Jun 6 0 1 S R J 1973 1975 - O 1 0 0 - R J 1974 1977 - May 1 0 1 S @@ -796,83 +553,10 @@ R J 2013 o - D 20 0 0 - R J 2014 2021 - Mar lastTh 24 1 S R J 2014 2022 - O lastF 0s 0 - R J 2022 o - F lastTh 24 1 S -Z Asia/Amman 2:23:44 - LMT 1931 -2 J EE%sT 2022 O 28 0s -3 - +03 -Z Asia/Almaty 5:7:48 - LMT 1924 May 2 -5 - +05 1930 Jun 21 -6 R +06/+07 1991 Mar 31 2s -5 R +05/+06 1992 Ja 19 2s -6 R +06/+07 2004 O 31 2s -6 - +06 -Z Asia/Qyzylorda 4:21:52 - LMT 1924 May 2 -4 - +04 1930 Jun 21 -5 - +05 1981 Ap -5 1 +06 1981 O -6 - +06 1982 Ap -5 R +05/+06 1991 Mar 31 2s -4 R +04/+05 1991 S 29 2s -5 R +05/+06 1992 Ja 19 2s -6 R +06/+07 1992 Mar 29 2s -5 R +05/+06 2004 O 31 2s -6 - +06 2018 D 21 -5 - +05 -Z Asia/Qostanay 4:14:28 - LMT 1924 May 2 -4 - +04 1930 Jun 21 -5 - +05 1981 Ap -5 1 +06 1981 O -6 - +06 1982 Ap -5 R +05/+06 1991 Mar 31 2s -4 R +04/+05 1992 Ja 19 2s -5 R +05/+06 2004 O 31 2s -6 - +06 -Z Asia/Aqtobe 3:48:40 - LMT 1924 May 2 -4 - +04 1930 Jun 21 -5 - +05 1981 Ap -5 1 +06 1981 O -6 - +06 1982 Ap -5 R +05/+06 1991 Mar 31 2s -4 R +04/+05 1992 Ja 19 2s -5 R +05/+06 2004 O 31 2s -5 - +05 -Z Asia/Aqtau 3:21:4 - LMT 1924 May 2 -4 - +04 1930 Jun 21 -5 - +05 1981 O -6 - +06 1982 Ap -5 R +05/+06 1991 Mar 31 2s -4 R +04/+05 1992 Ja 19 2s -5 R +05/+06 1994 S 25 2s -4 R +04/+05 2004 O 31 2s -5 - +05 -Z Asia/Atyrau 3:27:44 - LMT 1924 May 2 -3 - +03 1930 Jun 21 -5 - +05 1981 O -6 - +06 1982 Ap -5 R +05/+06 1991 Mar 31 2s -4 R +04/+05 1992 Ja 19 2s -5 R +05/+06 1999 Mar 28 2s -4 R +04/+05 2004 O 31 2s -5 - +05 -Z Asia/Oral 3:25:24 - LMT 1924 May 2 -3 - +03 1930 Jun 21 -5 - +05 1981 Ap -5 1 +06 1981 O -6 - +06 1982 Ap -5 R +05/+06 1989 Mar 26 2s -4 R +04/+05 1992 Ja 19 2s -5 R +05/+06 1992 Mar 29 2s -4 R +04/+05 2004 O 31 2s -5 - +05 R KG 1992 1996 - Ap Su>=7 0s 1 - R KG 1992 1996 - S lastSu 0 0 - R KG 1997 2005 - Mar lastSu 2:30 1 - R KG 1997 2004 - O lastSu 2:30 0 - -Z Asia/Bishkek 4:58:24 - LMT 1924 May 2 -5 - +05 1930 Jun 21 -6 R +06/+07 1991 Mar 31 2s -5 R +05/+06 1991 Au 31 2 -5 KG +05/+06 2005 Au 12 -6 - +06 R KR 1948 o - Jun 1 0 1 D R KR 1948 o - S 12 24 0 S R KR 1949 o - Ap 3 0 1 D @@ -887,18 +571,6 @@ R KR 1957 1960 - May Su>=1 0 1 D R KR 1957 1960 - S Sa>=17 24 0 S R KR 1987 1988 - May Su>=8 2 1 D R KR 1987 1988 - O Su>=8 3 0 S -Z Asia/Seoul 8:27:52 - LMT 1908 Ap -8:30 - KST 1912 -9 - JST 1945 S 8 -9 KR K%sT 1954 Mar 21 -8:30 KR K%sT 1961 Au 10 -9 KR K%sT -Z Asia/Pyongyang 8:23 - LMT 1908 Ap -8:30 - KST 1912 -9 - JST 1945 Au 24 -9 - KST 2015 Au 15 -8:30 - KST 2018 May 4 23:30 -9 - KST R l 1920 o - Mar 28 0 1 S R l 1920 o - O 25 0 0 - R l 1921 o - Ap 3 0 1 S @@ -923,18 +595,8 @@ R l 1992 o - O 4 0 0 - R l 1993 ma - Mar lastSu 0 1 S R l 1993 1998 - S lastSu 0 0 - R l 1999 ma - O lastSu 0 0 - -Z Asia/Beirut 2:22 - LMT 1880 -2 l EE%sT R NB 1935 1941 - S 14 0 0:20 - R NB 1935 1941 - D 14 0 0 - -Z Asia/Kuching 7:21:20 - LMT 1926 Mar -7:30 - +0730 1933 -8 NB +08/+0820 1942 F 16 -9 - +09 1945 S 12 -8 - +08 -Z Indian/Maldives 4:54 - LMT 1880 -4:54 - MMT 1960 -5 - +05 R X 1983 1984 - Ap 1 0 1 - R X 1983 o - O 1 0 0 - R X 1985 1998 - Mar lastSu 0 1 - @@ -944,31 +606,11 @@ R X 2001 2006 - S lastSa 2 0 - R X 2002 2006 - Mar lastSa 2 1 - R X 2015 2016 - Mar lastSa 2 1 - R X 2015 2016 - S lastSa 0 0 - -Z Asia/Hovd 6:6:36 - LMT 1905 Au -6 - +06 1978 -7 X +07/+08 -Z Asia/Ulaanbaatar 7:7:32 - LMT 1905 Au -7 - +07 1978 -8 X +08/+09 -Z Asia/Choibalsan 7:38 - LMT 1905 Au -7 - +07 1978 -8 - +08 1983 Ap -9 X +09/+10 2008 Mar 31 -8 X +08/+09 -Z Asia/Kathmandu 5:41:16 - LMT 1920 -5:30 - +0530 1986 -5:45 - +0545 R PK 2002 o - Ap Su>=2 0 1 S R PK 2002 o - O Su>=2 0 0 - R PK 2008 o - Jun 1 0 1 S R PK 2008 2009 - N 1 0 0 - R PK 2009 o - Ap 15 0 1 S -Z Asia/Karachi 4:28:12 - LMT 1907 -5:30 - +0530 1942 S -5:30 1 +0630 1945 O 15 -5:30 - +0530 1951 S 30 -5 - +05 1971 Mar 26 -5 PK PK%sT R P 1999 2005 - Ap F>=15 0 1 S R P 1999 2003 - O F>=15 0 0 - R P 2004 o - O 1 1 0 - @@ -1001,136 +643,90 @@ R P 2021 o - O 29 1 0 - R P 2022 o - Mar 27 0 1 S R P 2022 2035 - O Sa<=30 2 0 - R P 2023 o - Ap 29 2 1 S -R P 2024 o - Ap 13 2 1 S -R P 2025 o - Ap 5 2 1 S +R P 2024 o - Ap 20 2 1 S +R P 2025 o - Ap 12 2 1 S R P 2026 2054 - Mar Sa<=30 2 1 S R P 2036 o - O 18 2 0 - R P 2037 o - O 10 2 0 - R P 2038 o - S 25 2 0 - R P 2039 o - S 17 2 0 - -R P 2039 o - O 22 2 1 S -R P 2039 2067 - O Sa<=30 2 0 - R P 2040 o - S 1 2 0 - -R P 2040 o - O 13 2 1 S +R P 2040 o - O 20 2 1 S +R P 2040 2067 - O Sa<=30 2 0 - R P 2041 o - Au 24 2 0 - -R P 2041 o - S 28 2 1 S +R P 2041 o - O 5 2 1 S R P 2042 o - Au 16 2 0 - -R P 2042 o - S 20 2 1 S +R P 2042 o - S 27 2 1 S R P 2043 o - Au 1 2 0 - -R P 2043 o - S 12 2 1 S +R P 2043 o - S 19 2 1 S R P 2044 o - Jul 23 2 0 - -R P 2044 o - Au 27 2 1 S +R P 2044 o - S 3 2 1 S R P 2045 o - Jul 15 2 0 - -R P 2045 o - Au 19 2 1 S +R P 2045 o - Au 26 2 1 S R P 2046 o - Jun 30 2 0 - -R P 2046 o - Au 11 2 1 S +R P 2046 o - Au 18 2 1 S R P 2047 o - Jun 22 2 0 - -R P 2047 o - Jul 27 2 1 S +R P 2047 o - Au 3 2 1 S R P 2048 o - Jun 6 2 0 - -R P 2048 o - Jul 18 2 1 S +R P 2048 o - Jul 25 2 1 S R P 2049 o - May 29 2 0 - -R P 2049 o - Jul 3 2 1 S +R P 2049 o - Jul 10 2 1 S R P 2050 o - May 21 2 0 - -R P 2050 o - Jun 25 2 1 S +R P 2050 o - Jul 2 2 1 S R P 2051 o - May 6 2 0 - -R P 2051 o - Jun 17 2 1 S +R P 2051 o - Jun 24 2 1 S R P 2052 o - Ap 27 2 0 - -R P 2052 o - Jun 1 2 1 S +R P 2052 o - Jun 8 2 1 S R P 2053 o - Ap 12 2 0 - -R P 2053 o - May 24 2 1 S +R P 2053 o - May 31 2 1 S R P 2054 o - Ap 4 2 0 - -R P 2054 o - May 16 2 1 S -R P 2055 o - May 1 2 1 S -R P 2056 o - Ap 22 2 1 S -R P 2057 o - Ap 7 2 1 S -R P 2058 ma - Mar Sa<=30 2 1 S +R P 2054 o - May 23 2 1 S +R P 2055 o - May 8 2 1 S +R P 2056 o - Ap 29 2 1 S +R P 2057 o - Ap 14 2 1 S +R P 2058 o - Ap 6 2 1 S +R P 2059 ma - Mar Sa<=30 2 1 S R P 2068 o - O 20 2 0 - R P 2069 o - O 12 2 0 - R P 2070 o - O 4 2 0 - R P 2071 o - S 19 2 0 - R P 2072 o - S 10 2 0 - -R P 2072 o - O 15 2 1 S +R P 2072 o - O 22 2 1 S +R P 2072 ma - O Sa<=30 2 0 - R P 2073 o - S 2 2 0 - -R P 2073 o - O 7 2 1 S +R P 2073 o - O 14 2 1 S R P 2074 o - Au 18 2 0 - -R P 2074 o - S 29 2 1 S +R P 2074 o - O 6 2 1 S R P 2075 o - Au 10 2 0 - -R P 2075 o - S 14 2 1 S -R P 2075 ma - O Sa<=30 2 0 - +R P 2075 o - S 21 2 1 S R P 2076 o - Jul 25 2 0 - -R P 2076 o - S 5 2 1 S +R P 2076 o - S 12 2 1 S R P 2077 o - Jul 17 2 0 - -R P 2077 o - Au 28 2 1 S +R P 2077 o - S 4 2 1 S R P 2078 o - Jul 9 2 0 - -R P 2078 o - Au 13 2 1 S +R P 2078 o - Au 20 2 1 S R P 2079 o - Jun 24 2 0 - -R P 2079 o - Au 5 2 1 S +R P 2079 o - Au 12 2 1 S R P 2080 o - Jun 15 2 0 - -R P 2080 o - Jul 20 2 1 S +R P 2080 o - Jul 27 2 1 S R P 2081 o - Jun 7 2 0 - -R P 2081 o - Jul 12 2 1 S +R P 2081 o - Jul 19 2 1 S R P 2082 o - May 23 2 0 - -R P 2082 o - Jul 4 2 1 S +R P 2082 o - Jul 11 2 1 S R P 2083 o - May 15 2 0 - -R P 2083 o - Jun 19 2 1 S +R P 2083 o - Jun 26 2 1 S R P 2084 o - Ap 29 2 0 - -R P 2084 o - Jun 10 2 1 S +R P 2084 o - Jun 17 2 1 S R P 2085 o - Ap 21 2 0 - -R P 2085 o - Jun 2 2 1 S +R P 2085 o - Jun 9 2 1 S R P 2086 o - Ap 13 2 0 - -R P 2086 o - May 18 2 1 S -Z Asia/Gaza 2:17:52 - LMT 1900 O -2 Z EET/EEST 1948 May 15 -2 K EE%sT 1967 Jun 5 -2 Z I%sT 1996 -2 J EE%sT 1999 -2 P EE%sT 2008 Au 29 -2 - EET 2008 S -2 P EE%sT 2010 -2 - EET 2010 Mar 27 0:1 -2 P EE%sT 2011 Au -2 - EET 2012 -2 P EE%sT -Z Asia/Hebron 2:20:23 - LMT 1900 O -2 Z EET/EEST 1948 May 15 -2 K EE%sT 1967 Jun 5 -2 Z I%sT 1996 -2 J EE%sT 1999 -2 P EE%sT +R P 2086 o - May 25 2 1 S R PH 1936 o - N 1 0 1 D R PH 1937 o - F 1 0 0 S R PH 1954 o - Ap 12 0 1 D R PH 1954 o - Jul 1 0 0 S R PH 1978 o - Mar 22 0 1 D R PH 1978 o - S 21 0 0 S -Z Asia/Manila -15:56 - LMT 1844 D 31 -8:4 - LMT 1899 May 11 -8 PH P%sT 1942 May -9 - JST 1944 N -8 PH P%sT -Z Asia/Qatar 3:26:8 - LMT 1920 -4 - +04 1972 Jun -3 - +03 -Z Asia/Riyadh 3:6:52 - LMT 1947 Mar 14 -3 - +03 -Z Asia/Singapore 6:55:25 - LMT 1901 -6:55:25 - SMT 1905 Jun -7 - +07 1933 -7 0:20 +0720 1936 -7:20 - +0720 1941 S -7:30 - +0730 1942 F 16 -9 - +09 1945 S 12 -7:30 - +0730 1981 D 31 16u -8 - +08 -Z Asia/Colombo 5:19:24 - LMT 1880 -5:19:32 - MMT 1906 -5:30 - +0530 1942 Ja 5 -5:30 0:30 +06 1942 S -5:30 1 +0630 1945 O 16 2 -5:30 - +0530 1996 May 25 -6:30 - +0630 1996 O 26 0:30 -6 - +06 2006 Ap 15 0:30 -5:30 - +0530 R S 1920 1923 - Ap Su>=15 2 1 S R S 1920 1923 - O Su>=1 2 0 - R S 1962 o - Ap 29 2 1 S @@ -1172,46 +768,6 @@ R S 2009 o - Mar lastF 0 1 S R S 2010 2011 - Ap F>=1 0 1 S R S 2012 2022 - Mar lastF 0 1 S R S 2009 2022 - O lastF 0 0 - -Z Asia/Damascus 2:25:12 - LMT 1920 -2 S EE%sT 2022 O 28 -3 - +03 -Z Asia/Dushanbe 4:35:12 - LMT 1924 May 2 -5 - +05 1930 Jun 21 -6 R +06/+07 1991 Mar 31 2s -5 1 +06 1991 S 9 2s -5 - +05 -Z Asia/Bangkok 6:42:4 - LMT 1880 -6:42:4 - BMT 1920 Ap -7 - +07 -Z Asia/Ashgabat 3:53:32 - LMT 1924 May 2 -4 - +04 1930 Jun 21 -5 R +05/+06 1991 Mar 31 2 -4 R +04/+05 1992 Ja 19 2 -5 - +05 -Z Asia/Dubai 3:41:12 - LMT 1920 -4 - +04 -Z Asia/Samarkand 4:27:53 - LMT 1924 May 2 -4 - +04 1930 Jun 21 -5 - +05 1981 Ap -5 1 +06 1981 O -6 - +06 1982 Ap -5 R +05/+06 1992 -5 - +05 -Z Asia/Tashkent 4:37:11 - LMT 1924 May 2 -5 - +05 1930 Jun 21 -6 R +06/+07 1991 Mar 31 2 -5 R +05/+06 1992 -5 - +05 -Z Asia/Ho_Chi_Minh 7:6:30 - LMT 1906 Jul -7:6:30 - PLMT 1911 May -7 - +07 1942 D 31 23 -8 - +08 1945 Mar 14 23 -9 - +09 1945 S 2 -7 - +07 1947 Ap -8 - +08 1955 Jul -7 - +07 1959 D 31 23 -8 - +08 1975 Jun 13 -7 - +07 R AU 1917 o - Ja 1 2s 1 D R AU 1917 o - Mar lastSu 2s 0 S R AU 1942 o - Ja 1 2s 1 D @@ -1219,9 +775,6 @@ R AU 1942 o - Mar lastSu 2s 0 S R AU 1942 o - S 27 2s 1 D R AU 1943 1944 - Mar lastSu 2s 0 S R AU 1943 o - O 3 2s 1 D -Z Australia/Darwin 8:43:20 - LMT 1895 F -9 - ACST 1899 May -9:30 AU AC%sT R AW 1974 o - O lastSu 2s 1 D R AW 1975 o - Mar Su>=1 2s 0 S R AW 1983 o - O lastSu 2s 1 D @@ -1231,25 +784,12 @@ R AW 1992 o - Mar Su>=1 2s 0 S R AW 2006 o - D 3 2s 1 D R AW 2007 2009 - Mar lastSu 2s 0 S R AW 2007 2008 - O lastSu 2s 1 D -Z Australia/Perth 7:43:24 - LMT 1895 D -8 AU AW%sT 1943 Jul -8 AW AW%sT -Z Australia/Eucla 8:35:28 - LMT 1895 D -8:45 AU +0845/+0945 1943 Jul -8:45 AW +0845/+0945 R AQ 1971 o - O lastSu 2s 1 D R AQ 1972 o - F lastSu 2s 0 S R AQ 1989 1991 - O lastSu 2s 1 D R AQ 1990 1992 - Mar Su>=1 2s 0 S R Ho 1992 1993 - O lastSu 2s 1 D R Ho 1993 1994 - Mar Su>=1 2s 0 S -Z Australia/Brisbane 10:12:8 - LMT 1895 -10 AU AE%sT 1971 -10 AQ AE%sT -Z Australia/Lindeman 9:55:56 - LMT 1895 -10 AU AE%sT 1971 -10 AQ AE%sT 1992 Jul -10 Ho AE%sT R AS 1971 1985 - O lastSu 2s 1 D R AS 1986 o - O 19 2s 1 D R AS 1987 2007 - O lastSu 2s 1 D @@ -1265,10 +805,6 @@ R AS 2006 o - Ap 2 2s 0 S R AS 2007 o - Mar lastSu 2s 0 S R AS 2008 ma - Ap Su>=1 2s 0 S R AS 2008 ma - O Su>=1 2s 1 D -Z Australia/Adelaide 9:14:20 - LMT 1895 F -9 - ACST 1899 May -9:30 AU AC%sT 1971 -9:30 AS AC%sT R AT 1916 o - O Su>=1 2s 1 D R AT 1917 o - Mar lastSu 2s 0 S R AT 1917 1918 - O Su>=22 2s 1 D @@ -1292,10 +828,6 @@ R AT 2001 ma - O Su>=1 2s 1 D R AT 2006 o - Ap Su>=1 2s 0 S R AT 2007 o - Mar lastSu 2s 0 S R AT 2008 ma - Ap Su>=1 2s 0 S -Z Australia/Hobart 9:49:16 - LMT 1895 S -10 AT AE%sT 1919 O 24 -10 AU AE%sT 1967 -10 AT AE%sT R AV 1971 1985 - O lastSu 2s 1 D R AV 1972 o - F lastSu 2s 0 S R AV 1973 1985 - Mar Su>=1 2s 0 S @@ -1310,9 +842,6 @@ R AV 2006 o - Ap Su>=1 2s 0 S R AV 2007 o - Mar lastSu 2s 0 S R AV 2008 ma - Ap Su>=1 2s 0 S R AV 2008 ma - O Su>=1 2s 1 D -Z Australia/Melbourne 9:39:52 - LMT 1895 F -10 AU AE%sT 1971 -10 AV AE%sT R AN 1971 1985 - O lastSu 2s 1 D R AN 1972 o - F 27 2s 0 S R AN 1973 1981 - Mar Su>=1 2s 0 S @@ -1329,15 +858,6 @@ R AN 2006 o - Ap Su>=1 2s 0 S R AN 2007 o - Mar lastSu 2s 0 S R AN 2008 ma - Ap Su>=1 2s 0 S R AN 2008 ma - O Su>=1 2s 1 D -Z Australia/Sydney 10:4:52 - LMT 1895 F -10 AU AE%sT 1971 -10 AN AE%sT -Z Australia/Broken_Hill 9:25:48 - LMT 1895 F -10 - AEST 1896 Au 23 -9 - ACST 1899 May -9:30 AU AC%sT 1971 -9:30 AN AC%sT 2000 -9:30 AS AC%sT R LH 1981 1984 - O lastSu 2 1 - R LH 1982 1985 - Mar Su>=1 2 0 - R LH 1985 o - O lastSu 2 0:30 - @@ -1352,19 +872,6 @@ R LH 2006 o - Ap Su>=1 2 0 - R LH 2007 o - Mar lastSu 2 0 - R LH 2008 ma - Ap Su>=1 2 0 - R LH 2008 ma - O Su>=1 2 0:30 - -Z Australia/Lord_Howe 10:36:20 - LMT 1895 F -10 - AEST 1981 Mar -10:30 LH +1030/+1130 1985 Jul -10:30 LH +1030/+11 -Z Antarctica/Macquarie 0 - -00 1899 N -10 - AEST 1916 O 1 2 -10 1 AEDT 1917 F -10 AU AE%sT 1919 Ap 1 0s -0 - -00 1948 Mar 25 -10 AU AE%sT 1967 -10 AT AE%sT 2010 -10 1 AEDT 2011 -10 AT AE%sT R FJ 1998 1999 - N Su>=1 2 1 - R FJ 1999 2000 - F lastSu 3 0 - R FJ 2009 o - N 29 2 1 - @@ -1377,14 +884,6 @@ R FJ 2014 2018 - N Su>=1 2 1 - R FJ 2015 2021 - Ja Su>=12 3 0 - R FJ 2019 o - N Su>=8 2 1 - R FJ 2020 o - D 20 2 1 - -Z Pacific/Fiji 11:55:44 - LMT 1915 O 26 -12 FJ +12/+13 -Z Pacific/Gambier -8:59:48 - LMT 1912 O --9 - -09 -Z Pacific/Marquesas -9:18 - LMT 1912 O --9:30 - -0930 -Z Pacific/Tahiti -9:58:16 - LMT 1912 O --10 - -10 R Gu 1959 o - Jun 27 2 1 D R Gu 1961 o - Ja 29 2 0 S R Gu 1967 o - S 1 2 1 D @@ -1399,50 +898,10 @@ R Gu 1976 o - May 26 2 1 D R Gu 1976 o - Au 22 2:1 0 S R Gu 1977 o - Ap 24 2 1 D R Gu 1977 o - Au 28 2 0 S -Z Pacific/Guam -14:21 - LMT 1844 D 31 -9:39 - LMT 1901 -10 - GST 1941 D 10 -9 - +09 1944 Jul 31 -10 Gu G%sT 2000 D 23 -10 - ChST -Z Pacific/Tarawa 11:32:4 - LMT 1901 -12 - +12 -Z Pacific/Kanton 0 - -00 1937 Au 31 --12 - -12 1979 O --11 - -11 1994 D 31 -13 - +13 -Z Pacific/Kiritimati -10:29:20 - LMT 1901 --10:40 - -1040 1979 O --10 - -10 1994 D 31 -14 - +14 -Z Pacific/Kwajalein 11:9:20 - LMT 1901 -11 - +11 1937 -10 - +10 1941 Ap -9 - +09 1944 F 6 -11 - +11 1969 O --12 - -12 1993 Au 20 24 -12 - +12 -Z Pacific/Kosrae -13:8:4 - LMT 1844 D 31 -10:51:56 - LMT 1901 -11 - +11 1914 O -9 - +09 1919 F -11 - +11 1937 -10 - +10 1941 Ap -9 - +09 1945 Au -11 - +11 1969 O -12 - +12 1999 -11 - +11 -Z Pacific/Nauru 11:7:40 - LMT 1921 Ja 15 -11:30 - +1130 1942 Au 29 -9 - +09 1945 S 8 -11:30 - +1130 1979 F 10 2 -12 - +12 R NC 1977 1978 - D Su>=1 0 1 - R NC 1978 1979 - F 27 0 0 - R NC 1996 o - D 1 2s 1 - R NC 1997 o - Mar 2 2s 0 - -Z Pacific/Noumea 11:5:48 - LMT 1912 Ja 13 -11 NC +11/+12 R NZ 1927 o - N 6 2 1 S R NZ 1928 o - Mar 4 2 0 M R NZ 1928 1933 - O Su>=8 2 0:30 S @@ -1468,80 +927,26 @@ R NZ 2007 ma - S lastSu 2s 1 D R k 2007 ma - S lastSu 2:45s 1 - R NZ 2008 ma - Ap Su>=1 2s 0 S R k 2008 ma - Ap Su>=1 2:45s 0 - -Z Pacific/Auckland 11:39:4 - LMT 1868 N 2 -11:30 NZ NZ%sT 1946 -12 NZ NZ%sT -Z Pacific/Chatham 12:13:48 - LMT 1868 N 2 -12:15 - +1215 1946 -12:45 k +1245/+1345 R CK 1978 o - N 12 0 0:30 - R CK 1979 1991 - Mar Su>=1 0 0 - R CK 1979 1990 - O lastSu 0 0:30 - -Z Pacific/Rarotonga 13:20:56 - LMT 1899 D 26 --10:39:4 - LMT 1952 O 16 --10:30 - -1030 1978 N 12 --10 CK -10/-0930 -Z Pacific/Niue -11:19:40 - LMT 1952 O 16 --11:20 - -1120 1964 Jul --11 - -11 -Z Pacific/Norfolk 11:11:52 - LMT 1901 -11:12 - +1112 1951 -11:30 - +1130 1974 O 27 2s -11:30 1 +1230 1975 Mar 2 2s -11:30 - +1130 2015 O 4 2s -11 - +11 2019 Jul -11 AN +11/+12 -Z Pacific/Palau -15:2:4 - LMT 1844 D 31 -8:57:56 - LMT 1901 -9 - +09 -Z Pacific/Port_Moresby 9:48:40 - LMT 1880 -9:48:32 - PMMT 1895 -10 - +10 -Z Pacific/Bougainville 10:22:16 - LMT 1880 -9:48:32 - PMMT 1895 -10 - +10 1942 Jul -9 - +09 1945 Au 21 -10 - +10 2014 D 28 2 -11 - +11 -Z Pacific/Pitcairn -8:40:20 - LMT 1901 --8:30 - -0830 1998 Ap 27 --8 - -08 -Z Pacific/Pago_Pago 12:37:12 - LMT 1892 Jul 5 --11:22:48 - LMT 1911 --11 - SST R WS 2010 o - S lastSu 0 1 - R WS 2011 o - Ap Sa>=1 4 0 - R WS 2011 o - S lastSa 3 1 - R WS 2012 2021 - Ap Su>=1 4 0 - R WS 2012 2020 - S lastSu 3 1 - -Z Pacific/Apia 12:33:4 - LMT 1892 Jul 5 --11:26:56 - LMT 1911 --11:30 - -1130 1950 --11 WS -11/-10 2011 D 29 24 -13 WS +13/+14 -Z Pacific/Guadalcanal 10:39:48 - LMT 1912 O -11 - +11 -Z Pacific/Fakaofo -11:24:56 - LMT 1901 --11 - -11 2011 D 30 -13 - +13 R TO 1999 o - O 7 2s 1 - R TO 2000 o - Mar 19 2s 0 - R TO 2000 2001 - N Su>=1 2 1 - R TO 2001 2002 - Ja lastSu 2 0 - R TO 2016 o - N Su>=1 2 1 - R TO 2017 o - Ja Su>=15 3 0 - -Z Pacific/Tongatapu 12:19:12 - LMT 1945 S 10 -12:20 - +1220 1961 -13 - +13 1999 -13 TO +13/+14 R VU 1973 o - D 22 12u 1 - R VU 1974 o - Mar 30 12u 0 - R VU 1983 1991 - S Sa>=22 24 1 - R VU 1984 1991 - Mar Sa>=22 24 0 - R VU 1992 1993 - Ja Sa>=22 24 0 - R VU 1992 o - O Sa>=22 24 1 - -Z Pacific/Efate 11:13:16 - LMT 1912 Ja 13 -11 VU +11/+12 R G 1916 o - May 21 2s 1 BST R G 1916 o - O 1 2s 0 GMT R G 1917 o - Ap 8 2s 1 BST @@ -1607,11 +1012,6 @@ R G 1972 1980 - O Su>=23 2s 0 GMT R G 1981 1995 - Mar lastSu 1u 1 BST R G 1981 1989 - O Su>=23 1u 0 GMT R G 1990 1995 - O Su>=22 1u 0 GMT -Z Europe/London -0:1:15 - LMT 1847 D -0 G %s 1968 O 27 -1 - BST 1971 O 31 2u -0 G %s 1996 -0 E GMT/BST R IE 1971 o - O 31 2u -1 - R IE 1972 1980 - Mar Su>=16 2u 0 - R IE 1972 1980 - O Su>=23 2u -1 - @@ -1619,17 +1019,6 @@ R IE 1981 ma - Mar lastSu 1u 0 - R IE 1981 1989 - O Su>=23 1u -1 - R IE 1990 1995 - O Su>=22 1u -1 - R IE 1996 ma - O lastSu 1u -1 - -Z Europe/Dublin -0:25:21 - LMT 1880 Au 2 --0:25:21 - DMT 1916 May 21 2s --0:25:21 1 IST 1916 O 1 2s -0 G %s 1921 D 6 -0 G GMT/IST 1940 F 25 2s -0 1 IST 1946 O 6 2s -0 - GMT 1947 Mar 16 2s -0 1 IST 1947 N 2 2s -0 - GMT 1948 Ap 18 2s -0 G GMT/IST 1968 O 27 -1 IE IST/GMT R E 1977 1980 - Ap Su>=1 1u 1 S R E 1977 o - S lastSu 1u 0 - R E 1978 o - O 1 1u 0 - @@ -1681,10 +1070,6 @@ R R 1981 1983 - O 1 0 0 - R R 1984 1995 - S lastSu 2s 0 - R R 1985 2010 - Mar lastSu 2s 1 S R R 1996 2010 - O lastSu 2s 0 - -Z WET 0 E WE%sT -Z CET 1 c CE%sT -Z MET 1 c ME%sT -Z EET 2 E EE%sT R q 1940 o - Jun 16 0 1 S R q 1942 o - N 2 3 0 - R q 1943 o - Mar 29 2 1 S @@ -1710,14 +1095,6 @@ R q 1982 o - O 3 0 0 - R q 1983 o - Ap 18 0 1 S R q 1983 o - O 1 0 0 - R q 1984 o - Ap 1 0 1 S -Z Europe/Tirane 1:19:20 - LMT 1914 -1 - CET 1940 Jun 16 -1 q CE%sT 1984 Jul -1 E CE%sT -Z Europe/Andorra 0:6:4 - LMT 1901 -0 - WET 1946 S 30 -1 - CET 1985 Mar 31 2 -1 E CE%sT R a 1920 o - Ap 5 2s 1 S R a 1920 o - S 13 2s 0 - R a 1946 o - Ap 14 2s 1 S @@ -1727,23 +1104,6 @@ R a 1947 o - Ap 6 2s 1 S R a 1948 o - Ap 18 2s 1 S R a 1980 o - Ap 6 0 1 S R a 1980 o - S 28 0 0 - -Z Europe/Vienna 1:5:21 - LMT 1893 Ap -1 c CE%sT 1920 -1 a CE%sT 1940 Ap 1 2s -1 c CE%sT 1945 Ap 2 2s -1 1 CEST 1945 Ap 12 2s -1 - CET 1946 -1 a CE%sT 1981 -1 E CE%sT -Z Europe/Minsk 1:50:16 - LMT 1880 -1:50 - MMT 1924 May 2 -2 - EET 1930 Jun 21 -3 - MSK 1941 Jun 28 -1 c CE%sT 1944 Jul 3 -3 R MSK/MSD 1990 -3 - MSK 1991 Mar 31 2s -2 R EE%sT 2011 Mar 27 2s -3 - +03 R b 1918 o - Mar 9 0s 1 S R b 1918 1919 - O Sa>=1 23s 0 - R b 1919 o - Mar 1 23s 1 S @@ -1778,87 +1138,27 @@ R b 1945 o - Ap 2 2s 1 S R b 1945 o - S 16 2s 0 - R b 1946 o - May 19 2s 1 S R b 1946 o - O 7 2s 0 - -Z Europe/Brussels 0:17:30 - LMT 1880 -0:17:30 - BMT 1892 May 1 0:17:30 -0 - WET 1914 N 8 -1 - CET 1916 May -1 c CE%sT 1918 N 11 11u -0 b WE%sT 1940 May 20 2s -1 c CE%sT 1944 S 3 -1 b CE%sT 1977 -1 E CE%sT R BG 1979 o - Mar 31 23 1 S R BG 1979 o - O 1 1 0 - R BG 1980 1982 - Ap Sa>=1 23 1 S R BG 1980 o - S 29 1 0 - R BG 1981 o - S 27 2 0 - -Z Europe/Sofia 1:33:16 - LMT 1880 -1:56:56 - IMT 1894 N 30 -2 - EET 1942 N 2 3 -1 c CE%sT 1945 -1 - CET 1945 Ap 2 3 -2 - EET 1979 Mar 31 23 -2 BG EE%sT 1982 S 26 3 -2 c EE%sT 1991 -2 e EE%sT 1997 -2 E EE%sT R CZ 1945 o - Ap M>=1 2s 1 S R CZ 1945 o - O 1 2s 0 - R CZ 1946 o - May 6 2s 1 S R CZ 1946 1949 - O Su>=1 2s 0 - R CZ 1947 1948 - Ap Su>=15 2s 1 S R CZ 1949 o - Ap 9 2s 1 S -Z Europe/Prague 0:57:44 - LMT 1850 -0:57:44 - PMT 1891 O -1 c CE%sT 1945 May 9 -1 CZ CE%sT 1946 D 1 3 -1 -1 GMT 1947 F 23 2 -1 CZ CE%sT 1979 -1 E CE%sT -Z Atlantic/Faroe -0:27:4 - LMT 1908 Ja 11 -0 - WET 1981 -0 E WE%sT R Th 1991 1992 - Mar lastSu 2 1 D R Th 1991 1992 - S lastSu 2 0 S R Th 1993 2006 - Ap Su>=1 2 1 D R Th 1993 2006 - O lastSu 2 0 S R Th 2007 ma - Mar Su>=8 2 1 D R Th 2007 ma - N Su>=1 2 0 S -Z America/Danmarkshavn -1:14:40 - LMT 1916 Jul 28 --3 - -03 1980 Ap 6 2 --3 E -03/-02 1996 -0 - GMT -Z America/Scoresbysund -1:27:52 - LMT 1916 Jul 28 --2 - -02 1980 Ap 6 2 --2 c -02/-01 1981 Mar 29 --1 E -01/+00 -Z America/Nuuk -3:26:56 - LMT 1916 Jul 28 --3 - -03 1980 Ap 6 2 --3 E -03/-02 2023 O 29 1u --2 E -02/-01 -Z America/Thule -4:35:8 - LMT 1916 Jul 28 --4 Th A%sT -Z Europe/Tallinn 1:39 - LMT 1880 -1:39 - TMT 1918 F -1 c CE%sT 1919 Jul -1:39 - TMT 1921 May -2 - EET 1940 Au 6 -3 - MSK 1941 S 15 -1 c CE%sT 1944 S 22 -3 R MSK/MSD 1989 Mar 26 2s -2 1 EEST 1989 S 24 2s -2 c EE%sT 1998 S 22 -2 E EE%sT 1999 O 31 4 -2 - EET 2002 F 21 -2 E EE%sT R FI 1942 o - Ap 2 24 1 S R FI 1942 o - O 4 1 0 - R FI 1981 1982 - Mar lastSu 2 1 S R FI 1981 1982 - S lastSu 3 0 - -Z Europe/Helsinki 1:39:49 - LMT 1878 May 31 -1:39:49 - HMT 1921 May -2 FI EE%sT 1983 -2 E EE%sT R F 1916 o - Jun 14 23s 1 S R F 1916 1919 - O Su>=1 23s 0 - R F 1917 o - Mar 24 23s 1 S @@ -1901,13 +1201,6 @@ R F 1945 o - Ap 2 2 2 M R F 1945 o - S 16 3 0 - R F 1976 o - Mar 28 1 1 S R F 1976 o - S 26 1 0 - -Z Europe/Paris 0:9:21 - LMT 1891 Mar 16 -0:9:21 - PMT 1911 Mar 11 -0 F WE%sT 1940 Jun 14 23 -1 c CE%sT 1944 Au 25 -0 F WE%sT 1945 S 16 3 -1 F CE%sT 1977 -1 E CE%sT R DE 1946 o - Ap 14 2s 1 S R DE 1946 o - O 7 2s 0 - R DE 1947 1949 - O Su>=1 2s 0 - @@ -1919,15 +1212,6 @@ R DE 1949 o - Ap 10 2s 1 S R So 1945 o - May 24 2 2 M R So 1945 o - S 24 3 1 S R So 1945 o - N 18 2s 0 - -Z Europe/Berlin 0:53:28 - LMT 1893 Ap -1 c CE%sT 1945 May 24 2 -1 So CE%sT 1946 -1 DE CE%sT 1980 -1 E CE%sT -Z Europe/Gibraltar -0:21:24 - LMT 1880 Au 2 -0 G %s 1957 Ap 14 2 -1 - CET 1982 -1 E CE%sT R g 1932 o - Jul 7 0 1 S R g 1932 o - S 1 0 0 - R g 1941 o - Ap 7 0 1 S @@ -1947,12 +1231,6 @@ R g 1979 o - Ap 1 9 1 S R g 1979 o - S 29 2 0 - R g 1980 o - Ap 1 0 1 S R g 1980 o - S 28 0 0 - -Z Europe/Athens 1:34:52 - LMT 1895 S 14 -1:34:52 - AMT 1916 Jul 28 0:1 -2 g EE%sT 1941 Ap 30 -1 g CE%sT 1944 Ap 4 -2 g EE%sT 1981 -2 E EE%sT R h 1918 1919 - Ap 15 2 1 S R h 1918 1920 - S M>=15 3 0 - R h 1920 o - Ap 5 2 1 S @@ -1972,12 +1250,6 @@ R h 1980 o - Ap 6 0 1 S R h 1980 o - S 28 1 0 - R h 1981 1983 - Mar lastSu 0 1 S R h 1981 1983 - S lastSu 1 0 - -Z Europe/Budapest 1:16:20 - LMT 1890 N -1 c CE%sT 1918 -1 h CE%sT 1941 Ap 7 23 -1 c CE%sT 1945 -1 h CE%sT 1984 -1 E CE%sT R I 1916 o - Jun 3 24 1 S R I 1916 1917 - S 30 24 0 - R I 1917 o - Mar 31 24 1 S @@ -2019,44 +1291,8 @@ R I 1976 o - May 30 0s 1 S R I 1977 1979 - May Su>=22 0s 1 S R I 1978 o - O 1 0s 0 - R I 1979 o - S 30 0s 0 - -Z Europe/Rome 0:49:56 - LMT 1866 D 12 -0:49:56 - RMT 1893 O 31 23u -1 I CE%sT 1943 S 10 -1 c CE%sT 1944 Jun 4 -1 I CE%sT 1980 -1 E CE%sT R LV 1989 1996 - Mar lastSu 2s 1 S R LV 1989 1996 - S lastSu 2s 0 - -Z Europe/Riga 1:36:34 - LMT 1880 -1:36:34 - RMT 1918 Ap 15 2 -1:36:34 1 LST 1918 S 16 3 -1:36:34 - RMT 1919 Ap 1 2 -1:36:34 1 LST 1919 May 22 3 -1:36:34 - RMT 1926 May 11 -2 - EET 1940 Au 5 -3 - MSK 1941 Jul -1 c CE%sT 1944 O 13 -3 R MSK/MSD 1989 Mar lastSu 2s -2 1 EEST 1989 S lastSu 2s -2 LV EE%sT 1997 Ja 21 -2 E EE%sT 2000 F 29 -2 - EET 2001 Ja 2 -2 E EE%sT -Z Europe/Vilnius 1:41:16 - LMT 1880 -1:24 - WMT 1917 -1:35:36 - KMT 1919 O 10 -1 - CET 1920 Jul 12 -2 - EET 1920 O 9 -1 - CET 1940 Au 3 -3 - MSK 1941 Jun 24 -1 c CE%sT 1944 Au -3 R MSK/MSD 1989 Mar 26 2s -2 R EE%sT 1991 S 29 2s -2 c EE%sT 1998 -2 - EET 1998 Mar 29 1u -1 E CE%sT 1999 O 31 1u -2 - EET 2003 -2 E EE%sT R MT 1973 o - Mar 31 0s 1 S R MT 1973 o - S 29 0s 0 - R MT 1974 o - Ap 21 0s 1 S @@ -2064,22 +1300,8 @@ R MT 1974 o - S 16 0s 0 - R MT 1975 1979 - Ap Su>=15 2 1 S R MT 1975 1980 - S Su>=15 2 0 - R MT 1980 o - Mar 31 2 1 S -Z Europe/Malta 0:58:4 - LMT 1893 N 2 -1 I CE%sT 1973 Mar 31 -1 MT CE%sT 1981 -1 E CE%sT R MD 1997 ma - Mar lastSu 2 1 S R MD 1997 ma - O lastSu 3 0 - -Z Europe/Chisinau 1:55:20 - LMT 1880 -1:55 - CMT 1918 F 15 -1:44:24 - BMT 1931 Jul 24 -2 z EE%sT 1940 Au 15 -2 1 EEST 1941 Jul 17 -1 c CE%sT 1944 Au 24 -3 R MSK/MSD 1990 May 6 2 -2 R EE%sT 1992 -2 e EE%sT 1997 -2 MD EE%sT R O 1918 1919 - S 16 2s 0 - R O 1919 o - Ap 15 2s 1 S R O 1944 o - Ap 3 2s 1 S @@ -2100,15 +1322,6 @@ R O 1959 1961 - O Su>=1 1s 0 - R O 1960 o - Ap 3 1s 1 S R O 1961 1964 - May lastSu 1s 1 S R O 1962 1964 - S lastSu 1s 0 - -Z Europe/Warsaw 1:24 - LMT 1880 -1:24 - WMT 1915 Au 5 -1 c CE%sT 1918 S 16 3 -2 O EE%sT 1922 Jun -1 O CE%sT 1940 Jun 23 2 -1 c CE%sT 1944 O -1 O CE%sT 1977 -1 W- CE%sT 1988 -1 E CE%sT R p 1916 o - Jun 17 23 1 S R p 1916 o - N 1 1 0 - R p 1917 o - F 28 23s 1 S @@ -2157,42 +1370,6 @@ R p 1979 1982 - S lastSu 1s 0 - R p 1980 o - Mar lastSu 0s 1 S R p 1981 1982 - Mar lastSu 1s 1 S R p 1983 o - Mar lastSu 2s 1 S -Z Europe/Lisbon -0:36:45 - LMT 1884 --0:36:45 - LMT 1912 Ja 1 0u -0 p WE%sT 1966 Ap 3 2 -1 - CET 1976 S 26 1 -0 p WE%sT 1983 S 25 1s -0 W- WE%sT 1992 S 27 1s -1 E CE%sT 1996 Mar 31 1u -0 E WE%sT -Z Atlantic/Azores -1:42:40 - LMT 1884 --1:54:32 - HMT 1912 Ja 1 2u --2 p -02/-01 1942 Ap 25 22s --2 p +00 1942 Au 15 22s --2 p -02/-01 1943 Ap 17 22s --2 p +00 1943 Au 28 22s --2 p -02/-01 1944 Ap 22 22s --2 p +00 1944 Au 26 22s --2 p -02/-01 1945 Ap 21 22s --2 p +00 1945 Au 25 22s --2 p -02/-01 1966 Ap 3 2 --1 p -01/+00 1983 S 25 1s --1 W- -01/+00 1992 S 27 1s -0 E WE%sT 1993 Mar 28 1u --1 E -01/+00 -Z Atlantic/Madeira -1:7:36 - LMT 1884 --1:7:36 - FMT 1912 Ja 1 1u --1 p -01/+00 1942 Ap 25 22s --1 p +01 1942 Au 15 22s --1 p -01/+00 1943 Ap 17 22s --1 p +01 1943 Au 28 22s --1 p -01/+00 1944 Ap 22 22s --1 p +01 1944 Au 26 22s --1 p -01/+00 1945 Ap 21 22s --1 p +01 1945 Au 25 22s --1 p -01/+00 1966 Ap 3 2 -0 p WE%sT 1983 S 25 1s -0 E WE%sT R z 1932 o - May 21 0s 1 S R z 1932 1939 - O Su>=1 0s 0 - R z 1933 1939 - Ap Su>=2 0s 1 S @@ -2202,252 +1379,6 @@ R z 1980 o - Ap 5 23 1 S R z 1980 o - S lastSu 1 0 - R z 1991 1993 - Mar lastSu 0s 1 S R z 1991 1993 - S lastSu 0s 0 - -Z Europe/Bucharest 1:44:24 - LMT 1891 O -1:44:24 - BMT 1931 Jul 24 -2 z EE%sT 1981 Mar 29 2s -2 c EE%sT 1991 -2 z EE%sT 1994 -2 e EE%sT 1997 -2 E EE%sT -Z Europe/Kaliningrad 1:22 - LMT 1893 Ap -1 c CE%sT 1945 Ap 10 -2 O EE%sT 1946 Ap 7 -3 R MSK/MSD 1989 Mar 26 2s -2 R EE%sT 2011 Mar 27 2s -3 - +03 2014 O 26 2s -2 - EET -Z Europe/Moscow 2:30:17 - LMT 1880 -2:30:17 - MMT 1916 Jul 3 -2:31:19 R %s 1919 Jul 1 0u -3 R %s 1921 O -3 R MSK/MSD 1922 O -2 - EET 1930 Jun 21 -3 R MSK/MSD 1991 Mar 31 2s -2 R EE%sT 1992 Ja 19 2s -3 R MSK/MSD 2011 Mar 27 2s -4 - MSK 2014 O 26 2s -3 - MSK -Z Europe/Simferopol 2:16:24 - LMT 1880 -2:16 - SMT 1924 May 2 -2 - EET 1930 Jun 21 -3 - MSK 1941 N -1 c CE%sT 1944 Ap 13 -3 R MSK/MSD 1990 -3 - MSK 1990 Jul 1 2 -2 - EET 1992 Mar 20 -2 c EE%sT 1994 May -3 c MSK/MSD 1996 Mar 31 0s -3 1 MSD 1996 O 27 3s -3 - MSK 1997 Mar lastSu 1u -2 E EE%sT 2014 Mar 30 2 -4 - MSK 2014 O 26 2s -3 - MSK -Z Europe/Astrakhan 3:12:12 - LMT 1924 May -3 - +03 1930 Jun 21 -4 R +04/+05 1989 Mar 26 2s -3 R +03/+04 1991 Mar 31 2s -4 - +04 1992 Mar 29 2s -3 R +03/+04 2011 Mar 27 2s -4 - +04 2014 O 26 2s -3 - +03 2016 Mar 27 2s -4 - +04 -Z Europe/Volgograd 2:57:40 - LMT 1920 Ja 3 -3 - +03 1930 Jun 21 -4 - +04 1961 N 11 -4 R +04/+05 1988 Mar 27 2s -3 R MSK/MSD 1991 Mar 31 2s -4 - +04 1992 Mar 29 2s -3 R MSK/MSD 2011 Mar 27 2s -4 - MSK 2014 O 26 2s -3 - MSK 2018 O 28 2s -4 - +04 2020 D 27 2s -3 - MSK -Z Europe/Saratov 3:4:18 - LMT 1919 Jul 1 0u -3 - +03 1930 Jun 21 -4 R +04/+05 1988 Mar 27 2s -3 R +03/+04 1991 Mar 31 2s -4 - +04 1992 Mar 29 2s -3 R +03/+04 2011 Mar 27 2s -4 - +04 2014 O 26 2s -3 - +03 2016 D 4 2s -4 - +04 -Z Europe/Kirov 3:18:48 - LMT 1919 Jul 1 0u -3 - +03 1930 Jun 21 -4 R +04/+05 1989 Mar 26 2s -3 R MSK/MSD 1991 Mar 31 2s -4 - +04 1992 Mar 29 2s -3 R MSK/MSD 2011 Mar 27 2s -4 - MSK 2014 O 26 2s -3 - MSK -Z Europe/Samara 3:20:20 - LMT 1919 Jul 1 0u -3 - +03 1930 Jun 21 -4 - +04 1935 Ja 27 -4 R +04/+05 1989 Mar 26 2s -3 R +03/+04 1991 Mar 31 2s -2 R +02/+03 1991 S 29 2s -3 - +03 1991 O 20 3 -4 R +04/+05 2010 Mar 28 2s -3 R +03/+04 2011 Mar 27 2s -4 - +04 -Z Europe/Ulyanovsk 3:13:36 - LMT 1919 Jul 1 0u -3 - +03 1930 Jun 21 -4 R +04/+05 1989 Mar 26 2s -3 R +03/+04 1991 Mar 31 2s -2 R +02/+03 1992 Ja 19 2s -3 R +03/+04 2011 Mar 27 2s -4 - +04 2014 O 26 2s -3 - +03 2016 Mar 27 2s -4 - +04 -Z Asia/Yekaterinburg 4:2:33 - LMT 1916 Jul 3 -3:45:5 - PMT 1919 Jul 15 4 -4 - +04 1930 Jun 21 -5 R +05/+06 1991 Mar 31 2s -4 R +04/+05 1992 Ja 19 2s -5 R +05/+06 2011 Mar 27 2s -6 - +06 2014 O 26 2s -5 - +05 -Z Asia/Omsk 4:53:30 - LMT 1919 N 14 -5 - +05 1930 Jun 21 -6 R +06/+07 1991 Mar 31 2s -5 R +05/+06 1992 Ja 19 2s -6 R +06/+07 2011 Mar 27 2s -7 - +07 2014 O 26 2s -6 - +06 -Z Asia/Barnaul 5:35 - LMT 1919 D 10 -6 - +06 1930 Jun 21 -7 R +07/+08 1991 Mar 31 2s -6 R +06/+07 1992 Ja 19 2s -7 R +07/+08 1995 May 28 -6 R +06/+07 2011 Mar 27 2s -7 - +07 2014 O 26 2s -6 - +06 2016 Mar 27 2s -7 - +07 -Z Asia/Novosibirsk 5:31:40 - LMT 1919 D 14 6 -6 - +06 1930 Jun 21 -7 R +07/+08 1991 Mar 31 2s -6 R +06/+07 1992 Ja 19 2s -7 R +07/+08 1993 May 23 -6 R +06/+07 2011 Mar 27 2s -7 - +07 2014 O 26 2s -6 - +06 2016 Jul 24 2s -7 - +07 -Z Asia/Tomsk 5:39:51 - LMT 1919 D 22 -6 - +06 1930 Jun 21 -7 R +07/+08 1991 Mar 31 2s -6 R +06/+07 1992 Ja 19 2s -7 R +07/+08 2002 May 1 3 -6 R +06/+07 2011 Mar 27 2s -7 - +07 2014 O 26 2s -6 - +06 2016 May 29 2s -7 - +07 -Z Asia/Novokuznetsk 5:48:48 - LMT 1924 May -6 - +06 1930 Jun 21 -7 R +07/+08 1991 Mar 31 2s -6 R +06/+07 1992 Ja 19 2s -7 R +07/+08 2010 Mar 28 2s -6 R +06/+07 2011 Mar 27 2s -7 - +07 -Z Asia/Krasnoyarsk 6:11:26 - LMT 1920 Ja 6 -6 - +06 1930 Jun 21 -7 R +07/+08 1991 Mar 31 2s -6 R +06/+07 1992 Ja 19 2s -7 R +07/+08 2011 Mar 27 2s -8 - +08 2014 O 26 2s -7 - +07 -Z Asia/Irkutsk 6:57:5 - LMT 1880 -6:57:5 - IMT 1920 Ja 25 -7 - +07 1930 Jun 21 -8 R +08/+09 1991 Mar 31 2s -7 R +07/+08 1992 Ja 19 2s -8 R +08/+09 2011 Mar 27 2s -9 - +09 2014 O 26 2s -8 - +08 -Z Asia/Chita 7:33:52 - LMT 1919 D 15 -8 - +08 1930 Jun 21 -9 R +09/+10 1991 Mar 31 2s -8 R +08/+09 1992 Ja 19 2s -9 R +09/+10 2011 Mar 27 2s -10 - +10 2014 O 26 2s -8 - +08 2016 Mar 27 2 -9 - +09 -Z Asia/Yakutsk 8:38:58 - LMT 1919 D 15 -8 - +08 1930 Jun 21 -9 R +09/+10 1991 Mar 31 2s -8 R +08/+09 1992 Ja 19 2s -9 R +09/+10 2011 Mar 27 2s -10 - +10 2014 O 26 2s -9 - +09 -Z Asia/Vladivostok 8:47:31 - LMT 1922 N 15 -9 - +09 1930 Jun 21 -10 R +10/+11 1991 Mar 31 2s -9 R +09/+10 1992 Ja 19 2s -10 R +10/+11 2011 Mar 27 2s -11 - +11 2014 O 26 2s -10 - +10 -Z Asia/Khandyga 9:2:13 - LMT 1919 D 15 -8 - +08 1930 Jun 21 -9 R +09/+10 1991 Mar 31 2s -8 R +08/+09 1992 Ja 19 2s -9 R +09/+10 2004 -10 R +10/+11 2011 Mar 27 2s -11 - +11 2011 S 13 0s -10 - +10 2014 O 26 2s -9 - +09 -Z Asia/Sakhalin 9:30:48 - LMT 1905 Au 23 -9 - +09 1945 Au 25 -11 R +11/+12 1991 Mar 31 2s -10 R +10/+11 1992 Ja 19 2s -11 R +11/+12 1997 Mar lastSu 2s -10 R +10/+11 2011 Mar 27 2s -11 - +11 2014 O 26 2s -10 - +10 2016 Mar 27 2s -11 - +11 -Z Asia/Magadan 10:3:12 - LMT 1924 May 2 -10 - +10 1930 Jun 21 -11 R +11/+12 1991 Mar 31 2s -10 R +10/+11 1992 Ja 19 2s -11 R +11/+12 2011 Mar 27 2s -12 - +12 2014 O 26 2s -10 - +10 2016 Ap 24 2s -11 - +11 -Z Asia/Srednekolymsk 10:14:52 - LMT 1924 May 2 -10 - +10 1930 Jun 21 -11 R +11/+12 1991 Mar 31 2s -10 R +10/+11 1992 Ja 19 2s -11 R +11/+12 2011 Mar 27 2s -12 - +12 2014 O 26 2s -11 - +11 -Z Asia/Ust-Nera 9:32:54 - LMT 1919 D 15 -8 - +08 1930 Jun 21 -9 R +09/+10 1981 Ap -11 R +11/+12 1991 Mar 31 2s -10 R +10/+11 1992 Ja 19 2s -11 R +11/+12 2011 Mar 27 2s -12 - +12 2011 S 13 0s -11 - +11 2014 O 26 2s -10 - +10 -Z Asia/Kamchatka 10:34:36 - LMT 1922 N 10 -11 - +11 1930 Jun 21 -12 R +12/+13 1991 Mar 31 2s -11 R +11/+12 1992 Ja 19 2s -12 R +12/+13 2010 Mar 28 2s -11 R +11/+12 2011 Mar 27 2s -12 - +12 -Z Asia/Anadyr 11:49:56 - LMT 1924 May 2 -12 - +12 1930 Jun 21 -13 R +13/+14 1982 Ap 1 0s -12 R +12/+13 1991 Mar 31 2s -11 R +11/+12 1992 Ja 19 2s -12 R +12/+13 2010 Mar 28 2s -11 R +11/+12 2011 Mar 27 2s -12 - +12 -Z Europe/Belgrade 1:22 - LMT 1884 -1 - CET 1941 Ap 18 23 -1 c CE%sT 1945 -1 - CET 1945 May 8 2s -1 1 CEST 1945 S 16 2s -1 - CET 1982 N 27 -1 E CE%sT R s 1918 o - Ap 15 23 1 S R s 1918 1919 - O 6 24s 0 - R s 1919 o - Ap 6 23 1 S @@ -2487,30 +1418,8 @@ R Sp 1976 o - Au 1 0 0 - R Sp 1977 o - S 28 0 0 - R Sp 1978 o - Jun 1 0 1 S R Sp 1978 o - Au 4 0 0 - -Z Europe/Madrid -0:14:44 - LMT 1901 Ja 1 0u -0 s WE%sT 1940 Mar 16 23 -1 s CE%sT 1979 -1 E CE%sT -Z Africa/Ceuta -0:21:16 - LMT 1901 Ja 1 0u -0 - WET 1918 May 6 23 -0 1 WEST 1918 O 7 23 -0 - WET 1924 -0 s WE%sT 1929 -0 - WET 1967 -0 Sp WE%sT 1984 Mar 16 -1 - CET 1986 -1 E CE%sT -Z Atlantic/Canary -1:1:36 - LMT 1922 Mar --1 - -01 1946 S 30 1 -0 - WET 1980 Ap 6 0s -0 1 WEST 1980 S 28 1u -0 E WE%sT R CH 1941 1942 - May M>=1 1 1 S R CH 1941 1942 - O M>=1 2 0 - -Z Europe/Zurich 0:34:8 - LMT 1853 Jul 16 -0:29:46 - BMT 1894 Jun -1 CH CE%sT 1981 -1 E CE%sT R T 1916 o - May 1 0 1 S R T 1916 o - O 1 0 0 - R T 1920 o - Mar 28 0 1 S @@ -2556,28 +1465,6 @@ R T 1986 1995 - S lastSu 1s 0 - R T 1994 o - Mar 20 1s 1 S R T 1995 2006 - Mar lastSu 1s 1 S R T 1996 2006 - O lastSu 1s 0 - -Z Europe/Istanbul 1:55:52 - LMT 1880 -1:56:56 - IMT 1910 O -2 T EE%sT 1978 Jun 29 -3 T +03/+04 1984 N 1 2 -2 T EE%sT 2007 -2 E EE%sT 2011 Mar 27 1u -2 - EET 2011 Mar 28 1u -2 E EE%sT 2014 Mar 30 1u -2 - EET 2014 Mar 31 1u -2 E EE%sT 2015 O 25 1u -2 1 EEST 2015 N 8 1u -2 E EE%sT 2016 S 7 -3 - +03 -Z Europe/Kyiv 2:2:4 - LMT 1880 -2:2:4 - KMT 1924 May 2 -2 - EET 1930 Jun 21 -3 - MSK 1941 S 20 -1 c CE%sT 1943 N 6 -3 R MSK/MSD 1990 Jul 1 2 -2 1 EEST 1991 S 29 3 -2 c EE%sT 1996 May 13 -2 E EE%sT R u 1918 1919 - Mar lastSu 2 1 D R u 1918 1919 - O lastSu 2 0 S R u 1942 o - F 9 2 1 W @@ -2591,172 +1478,34 @@ R u 1976 1986 - Ap lastSu 2 1 D R u 1987 2006 - Ap Su>=1 2 1 D R u 2007 ma - Mar Su>=8 2 1 D R u 2007 ma - N Su>=1 2 0 S -Z EST -5 - EST -Z MST -7 - MST -Z HST -10 - HST -Z EST5EDT -5 u E%sT -Z CST6CDT -6 u C%sT -Z MST7MDT -7 u M%sT -Z PST8PDT -8 u P%sT R NY 1920 o - Mar lastSu 2 1 D R NY 1920 o - O lastSu 2 0 S R NY 1921 1966 - Ap lastSu 2 1 D R NY 1921 1954 - S lastSu 2 0 S R NY 1955 1966 - O lastSu 2 0 S -Z America/New_York -4:56:2 - LMT 1883 N 18 17u --5 u E%sT 1920 --5 NY E%sT 1942 --5 u E%sT 1946 --5 NY E%sT 1967 --5 u E%sT R Ch 1920 o - Jun 13 2 1 D R Ch 1920 1921 - O lastSu 2 0 S R Ch 1921 o - Mar lastSu 2 1 D R Ch 1922 1966 - Ap lastSu 2 1 D R Ch 1922 1954 - S lastSu 2 0 S R Ch 1955 1966 - O lastSu 2 0 S -Z America/Chicago -5:50:36 - LMT 1883 N 18 18u --6 u C%sT 1920 --6 Ch C%sT 1936 Mar 1 2 --5 - EST 1936 N 15 2 --6 Ch C%sT 1942 --6 u C%sT 1946 --6 Ch C%sT 1967 --6 u C%sT -Z America/North_Dakota/Center -6:45:12 - LMT 1883 N 18 19u --7 u M%sT 1992 O 25 2 --6 u C%sT -Z America/North_Dakota/New_Salem -6:45:39 - LMT 1883 N 18 19u --7 u M%sT 2003 O 26 2 --6 u C%sT -Z America/North_Dakota/Beulah -6:47:7 - LMT 1883 N 18 19u --7 u M%sT 2010 N 7 2 --6 u C%sT R De 1920 1921 - Mar lastSu 2 1 D R De 1920 o - O lastSu 2 0 S R De 1921 o - May 22 2 0 S R De 1965 1966 - Ap lastSu 2 1 D R De 1965 1966 - O lastSu 2 0 S -Z America/Denver -6:59:56 - LMT 1883 N 18 19u --7 u M%sT 1920 --7 De M%sT 1942 --7 u M%sT 1946 --7 De M%sT 1967 --7 u M%sT R CA 1948 o - Mar 14 2:1 1 D R CA 1949 o - Ja 1 2 0 S R CA 1950 1966 - Ap lastSu 1 1 D R CA 1950 1961 - S lastSu 2 0 S R CA 1962 1966 - O lastSu 2 0 S -Z America/Los_Angeles -7:52:58 - LMT 1883 N 18 20u --8 u P%sT 1946 --8 CA P%sT 1967 --8 u P%sT -Z America/Juneau 15:2:19 - LMT 1867 O 19 15:33:32 --8:57:41 - LMT 1900 Au 20 12 --8 - PST 1942 --8 u P%sT 1946 --8 - PST 1969 --8 u P%sT 1980 Ap 27 2 --9 u Y%sT 1980 O 26 2 --8 u P%sT 1983 O 30 2 --9 u Y%sT 1983 N 30 --9 u AK%sT -Z America/Sitka 14:58:47 - LMT 1867 O 19 15:30 --9:1:13 - LMT 1900 Au 20 12 --8 - PST 1942 --8 u P%sT 1946 --8 - PST 1969 --8 u P%sT 1983 O 30 2 --9 u Y%sT 1983 N 30 --9 u AK%sT -Z America/Metlakatla 15:13:42 - LMT 1867 O 19 15:44:55 --8:46:18 - LMT 1900 Au 20 12 --8 - PST 1942 --8 u P%sT 1946 --8 - PST 1969 --8 u P%sT 1983 O 30 2 --8 - PST 2015 N 1 2 --9 u AK%sT 2018 N 4 2 --8 - PST 2019 Ja 20 2 --9 u AK%sT -Z America/Yakutat 14:41:5 - LMT 1867 O 19 15:12:18 --9:18:55 - LMT 1900 Au 20 12 --9 - YST 1942 --9 u Y%sT 1946 --9 - YST 1969 --9 u Y%sT 1983 N 30 --9 u AK%sT -Z America/Anchorage 14:0:24 - LMT 1867 O 19 14:31:37 --9:59:36 - LMT 1900 Au 20 12 --10 - AST 1942 --10 u A%sT 1967 Ap --10 - AHST 1969 --10 u AH%sT 1983 O 30 2 --9 u Y%sT 1983 N 30 --9 u AK%sT -Z America/Nome 12:58:22 - LMT 1867 O 19 13:29:35 --11:1:38 - LMT 1900 Au 20 12 --11 - NST 1942 --11 u N%sT 1946 --11 - NST 1967 Ap --11 - BST 1969 --11 u B%sT 1983 O 30 2 --9 u Y%sT 1983 N 30 --9 u AK%sT -Z America/Adak 12:13:22 - LMT 1867 O 19 12:44:35 --11:46:38 - LMT 1900 Au 20 12 --11 - NST 1942 --11 u N%sT 1946 --11 - NST 1967 Ap --11 - BST 1969 --11 u B%sT 1983 O 30 2 --10 u AH%sT 1983 N 30 --10 u H%sT -Z Pacific/Honolulu -10:31:26 - LMT 1896 Ja 13 12 --10:30 - HST 1933 Ap 30 2 --10:30 1 HDT 1933 May 21 12 --10:30 u H%sT 1947 Jun 8 2 --10 - HST -Z America/Phoenix -7:28:18 - LMT 1883 N 18 19u --7 u M%sT 1944 Ja 1 0:1 --7 - MST 1944 Ap 1 0:1 --7 u M%sT 1944 O 1 0:1 --7 - MST 1967 --7 u M%sT 1968 Mar 21 --7 - MST -Z America/Boise -7:44:49 - LMT 1883 N 18 20u --8 u P%sT 1923 May 13 2 --7 u M%sT 1974 --7 - MST 1974 F 3 2 --7 u M%sT R In 1941 o - Jun 22 2 1 D R In 1941 1954 - S lastSu 2 0 S R In 1946 1954 - Ap lastSu 2 1 D -Z America/Indiana/Indianapolis -5:44:38 - LMT 1883 N 18 18u --6 u C%sT 1920 --6 In C%sT 1942 --6 u C%sT 1946 --6 In C%sT 1955 Ap 24 2 --5 - EST 1957 S 29 2 --6 - CST 1958 Ap 27 2 --5 - EST 1969 --5 u E%sT 1971 --5 - EST 2006 --5 u E%sT R Ma 1951 o - Ap lastSu 2 1 D R Ma 1951 o - S lastSu 2 0 S R Ma 1954 1960 - Ap lastSu 2 1 D R Ma 1954 1960 - S lastSu 2 0 S -Z America/Indiana/Marengo -5:45:23 - LMT 1883 N 18 18u --6 u C%sT 1951 --6 Ma C%sT 1961 Ap 30 2 --5 - EST 1969 --5 u E%sT 1974 Ja 6 2 --6 1 CDT 1974 O 27 2 --5 u E%sT 1976 --5 - EST 2006 --5 u E%sT R V 1946 o - Ap lastSu 2 1 D R V 1946 o - S lastSu 2 0 S R V 1953 1954 - Ap lastSu 2 1 D @@ -2766,68 +1515,23 @@ R V 1956 1963 - Ap lastSu 2 1 D R V 1960 o - O lastSu 2 0 S R V 1961 o - S lastSu 2 0 S R V 1962 1963 - O lastSu 2 0 S -Z America/Indiana/Vincennes -5:50:7 - LMT 1883 N 18 18u --6 u C%sT 1946 --6 V C%sT 1964 Ap 26 2 --5 - EST 1969 --5 u E%sT 1971 --5 - EST 2006 Ap 2 2 --6 u C%sT 2007 N 4 2 --5 u E%sT R Pe 1955 o - May 1 0 1 D R Pe 1955 1960 - S lastSu 2 0 S R Pe 1956 1963 - Ap lastSu 2 1 D R Pe 1961 1963 - O lastSu 2 0 S -Z America/Indiana/Tell_City -5:47:3 - LMT 1883 N 18 18u --6 u C%sT 1946 --6 Pe C%sT 1964 Ap 26 2 --5 - EST 1967 O 29 2 --6 u C%sT 1969 Ap 27 2 --5 u E%sT 1971 --5 - EST 2006 Ap 2 2 --6 u C%sT R Pi 1955 o - May 1 0 1 D R Pi 1955 1960 - S lastSu 2 0 S R Pi 1956 1964 - Ap lastSu 2 1 D R Pi 1961 1964 - O lastSu 2 0 S -Z America/Indiana/Petersburg -5:49:7 - LMT 1883 N 18 18u --6 u C%sT 1955 --6 Pi C%sT 1965 Ap 25 2 --5 - EST 1966 O 30 2 --6 u C%sT 1977 O 30 2 --5 - EST 2006 Ap 2 2 --6 u C%sT 2007 N 4 2 --5 u E%sT R St 1947 1961 - Ap lastSu 2 1 D R St 1947 1954 - S lastSu 2 0 S R St 1955 1956 - O lastSu 2 0 S R St 1957 1958 - S lastSu 2 0 S R St 1959 1961 - O lastSu 2 0 S -Z America/Indiana/Knox -5:46:30 - LMT 1883 N 18 18u --6 u C%sT 1947 --6 St C%sT 1962 Ap 29 2 --5 - EST 1963 O 27 2 --6 u C%sT 1991 O 27 2 --5 - EST 2006 Ap 2 2 --6 u C%sT R Pu 1946 1960 - Ap lastSu 2 1 D R Pu 1946 1954 - S lastSu 2 0 S R Pu 1955 1956 - O lastSu 2 0 S R Pu 1957 1960 - S lastSu 2 0 S -Z America/Indiana/Winamac -5:46:25 - LMT 1883 N 18 18u --6 u C%sT 1946 --6 Pu C%sT 1961 Ap 30 2 --5 - EST 1969 --5 u E%sT 1971 --5 - EST 2006 Ap 2 2 --6 u C%sT 2007 Mar 11 2 --5 u E%sT -Z America/Indiana/Vevay -5:40:16 - LMT 1883 N 18 18u --6 u C%sT 1954 Ap 25 2 --5 - EST 1969 --5 u E%sT 1973 --5 - EST 2006 --5 u E%sT R v 1921 o - May 1 2 1 D R v 1921 o - S 1 2 0 S R v 1941 o - Ap lastSu 2 1 D @@ -2837,41 +1541,12 @@ R v 1946 o - Jun 2 2 0 S R v 1950 1961 - Ap lastSu 2 1 D R v 1950 1955 - S lastSu 2 0 S R v 1956 1961 - O lastSu 2 0 S -Z America/Kentucky/Louisville -5:43:2 - LMT 1883 N 18 18u --6 u C%sT 1921 --6 v C%sT 1942 --6 u C%sT 1946 --6 v C%sT 1961 Jul 23 2 --5 - EST 1968 --5 u E%sT 1974 Ja 6 2 --6 1 CDT 1974 O 27 2 --5 u E%sT -Z America/Kentucky/Monticello -5:39:24 - LMT 1883 N 18 18u --6 u C%sT 1946 --6 - CST 1968 --6 u C%sT 2000 O 29 2 --5 u E%sT R Dt 1948 o - Ap lastSu 2 1 D R Dt 1948 o - S lastSu 2 0 S -Z America/Detroit -5:32:11 - LMT 1905 --6 - CST 1915 May 15 2 --5 - EST 1942 --5 u E%sT 1946 --5 Dt E%sT 1967 Jun 14 0:1 --5 u E%sT 1969 --5 - EST 1973 --5 u E%sT 1975 --5 - EST 1975 Ap 27 2 --5 u E%sT R Me 1946 o - Ap lastSu 2 1 D R Me 1946 o - S lastSu 2 0 S R Me 1966 o - Ap lastSu 2 1 D R Me 1966 o - O lastSu 2 0 S -Z America/Menominee -5:50:27 - LMT 1885 S 18 12 --6 u C%sT 1946 --6 Me C%sT 1969 Ap 27 2 --5 - EST 1973 Ap 29 2 --6 u C%sT R C 1918 o - Ap 14 2 1 D R C 1918 o - O 27 2 0 S R C 1942 o - F 9 2 1 W @@ -2901,24 +1576,6 @@ R j 1988 o - Ap Su>=1 0:1 2 DD R j 1989 2006 - Ap Su>=1 0:1 1 D R j 2007 2011 - Mar Su>=8 0:1 1 D R j 2007 2010 - N Su>=1 0:1 0 S -Z America/St_Johns -3:30:52 - LMT 1884 --3:30:52 j N%sT 1918 --3:30:52 C N%sT 1919 --3:30:52 j N%sT 1935 Mar 30 --3:30 j N%sT 1942 May 11 --3:30 C N%sT 1946 --3:30 j N%sT 2011 N --3:30 C N%sT -Z America/Goose_Bay -4:1:40 - LMT 1884 --3:30:52 - NST 1918 --3:30:52 C N%sT 1919 --3:30:52 - NST 1935 Mar 30 --3:30 - NST 1936 --3:30 j N%sT 1942 May 11 --3:30 C N%sT 1946 --3:30 j N%sT 1966 Mar 15 2 --4 j A%sT 2011 N --4 C A%sT R H 1916 o - Ap 1 0 1 D R H 1916 o - O 1 0 0 S R H 1920 o - May 9 0 1 D @@ -2960,19 +1617,6 @@ R H 1956 1959 - Ap lastSu 2 1 D R H 1956 1959 - S lastSu 2 0 S R H 1962 1973 - Ap lastSu 2 1 D R H 1962 1973 - O lastSu 2 0 S -Z America/Halifax -4:14:24 - LMT 1902 Jun 15 --4 H A%sT 1918 --4 C A%sT 1919 --4 H A%sT 1942 F 9 2s --4 C A%sT 1946 --4 H A%sT 1974 --4 C A%sT -Z America/Glace_Bay -3:59:48 - LMT 1902 Jun 15 --4 C A%sT 1953 --4 H A%sT 1954 --4 - AST 1972 --4 H A%sT 1974 --4 C A%sT R o 1933 1935 - Jun Su>=8 1 1 D R o 1933 1935 - S Su>=8 1 0 S R o 1936 1938 - Jun Su>=1 1 1 D @@ -2986,15 +1630,6 @@ R o 1946 1956 - S lastSu 2 0 S R o 1957 1972 - O lastSu 2 0 S R o 1993 2006 - Ap Su>=1 0:1 1 D R o 1993 2006 - O lastSu 0:1 0 S -Z America/Moncton -4:19:8 - LMT 1883 D 9 --5 - EST 1902 Jun 15 --4 C A%sT 1933 --4 o A%sT 1942 --4 C A%sT 1946 --4 o A%sT 1973 --4 C A%sT 1993 --4 o A%sT 2007 --4 C A%sT R t 1919 o - Mar 30 23:30 1 D R t 1919 o - O 26 0 0 S R t 1920 o - May 2 2 1 D @@ -3008,21 +1643,11 @@ R t 1927 1937 - S Su>=25 2 0 S R t 1928 1937 - Ap Su>=25 2 1 D R t 1938 1940 - Ap lastSu 2 1 D R t 1938 1939 - S lastSu 2 0 S -R t 1945 1946 - S lastSu 2 0 S -R t 1946 o - Ap lastSu 2 1 D -R t 1947 1949 - Ap lastSu 0 1 D -R t 1947 1948 - S lastSu 0 0 S -R t 1949 o - N lastSu 0 0 S -R t 1950 1973 - Ap lastSu 2 1 D -R t 1950 o - N lastSu 2 0 S +R t 1945 1948 - S lastSu 2 0 S +R t 1946 1973 - Ap lastSu 2 1 D +R t 1949 1950 - N lastSu 2 0 S R t 1951 1956 - S lastSu 2 0 S R t 1957 1973 - O lastSu 2 0 S -Z America/Toronto -5:17:32 - LMT 1895 --5 C E%sT 1919 --5 t E%sT 1942 F 9 2s --5 C E%sT 1946 --5 t E%sT 1974 --5 C E%sT R W 1916 o - Ap 23 0 1 D R W 1916 o - S 17 0 0 S R W 1918 o - Ap 14 2 1 D @@ -3047,9 +1672,6 @@ R W 1963 o - S 22 2 0 S R W 1966 1986 - Ap lastSu 2s 1 D R W 1966 2005 - O lastSu 2s 0 S R W 1987 2005 - Ap Su>=1 2s 1 D -Z America/Winnipeg -6:28:36 - LMT 1887 Jul 16 --6 W C%sT 2006 --6 C C%sT R r 1918 o - Ap 14 2 1 D R r 1918 o - O 27 2 0 S R r 1930 1934 - May Su>=1 0 1 D @@ -3072,14 +1694,6 @@ R Sw 1957 o - O lastSu 2 0 S R Sw 1959 1961 - Ap lastSu 2 1 D R Sw 1959 o - O lastSu 2 0 S R Sw 1960 1961 - S lastSu 2 0 S -Z America/Regina -6:58:36 - LMT 1905 S --7 r M%sT 1960 Ap lastSu 2 --6 - CST -Z America/Swift_Current -7:11:20 - LMT 1905 S --7 C M%sT 1946 Ap lastSu 2 --7 r M%sT 1950 --7 Sw M%sT 1972 Ap lastSu 2 --6 - CST R Ed 1918 1919 - Ap Su>=8 2 1 D R Ed 1918 o - O 27 2 0 S R Ed 1919 o - May 27 2 0 S @@ -3093,9 +1707,6 @@ R Ed 1947 o - Ap lastSu 2 1 D R Ed 1947 o - S lastSu 2 0 S R Ed 1972 1986 - Ap lastSu 2 1 D R Ed 1972 2006 - O lastSu 2 0 S -Z America/Edmonton -7:33:52 - LMT 1906 S --7 Ed M%sT 1987 --7 C M%sT R Va 1918 o - Ap 14 2 1 D R Va 1918 o - O 27 2 0 S R Va 1942 o - F 9 2 1 W @@ -3105,19 +1716,6 @@ R Va 1946 1986 - Ap lastSu 2 1 D R Va 1946 o - S 29 2 0 S R Va 1947 1961 - S lastSu 2 0 S R Va 1962 2006 - O lastSu 2 0 S -Z America/Vancouver -8:12:28 - LMT 1884 --8 Va P%sT 1987 --8 C P%sT -Z America/Dawson_Creek -8:0:56 - LMT 1884 --8 C P%sT 1947 --8 Va P%sT 1972 Au 30 2 --7 - MST -Z America/Fort_Nelson -8:10:47 - LMT 1884 --8 Va P%sT 1946 --8 - PST 1947 --8 Va P%sT 1987 --8 C P%sT 2015 Mar 8 2 --7 - MST R Y 1918 o - Ap 14 2 1 D R Y 1918 o - O 27 2 0 S R Y 1919 o - May 25 2 1 D @@ -3130,42 +1728,6 @@ R Y 1972 2006 - O lastSu 2 0 S R Y 1987 2006 - Ap Su>=1 2 1 D R Yu 1965 o - Ap lastSu 0 2 DD R Yu 1965 o - O lastSu 2 0 S -Z America/Iqaluit 0 - -00 1942 Au --5 Y E%sT 1999 O 31 2 --6 C C%sT 2000 O 29 2 --5 C E%sT -Z America/Resolute 0 - -00 1947 Au 31 --6 Y C%sT 2000 O 29 2 --5 - EST 2001 Ap 1 3 --6 C C%sT 2006 O 29 2 --5 - EST 2007 Mar 11 3 --6 C C%sT -Z America/Rankin_Inlet 0 - -00 1957 --6 Y C%sT 2000 O 29 2 --5 - EST 2001 Ap 1 3 --6 C C%sT -Z America/Cambridge_Bay 0 - -00 1920 --7 Y M%sT 1999 O 31 2 --6 C C%sT 2000 O 29 2 --5 - EST 2000 N 5 --6 - CST 2001 Ap 1 3 --7 C M%sT -Z America/Inuvik 0 - -00 1953 --8 Y P%sT 1979 Ap lastSu 2 --7 Y M%sT 1980 --7 C M%sT -Z America/Whitehorse -9:0:12 - LMT 1900 Au 20 --9 Y Y%sT 1965 --9 Yu Y%sT 1966 F 27 --8 - PST 1980 --8 C P%sT 2020 N --7 - MST -Z America/Dawson -9:17:40 - LMT 1900 Au 20 --9 Y Y%sT 1965 --9 Yu Y%sT 1973 O 28 --8 - PST 1980 --8 C P%sT 2020 N --7 - MST R m 1931 o - May 1 23 1 D R m 1931 o - O 1 0 0 S R m 1939 o - F 5 0 1 D @@ -3182,107 +1744,6 @@ R m 2001 o - May Su>=1 2 1 D R m 2001 o - S lastSu 2 0 S R m 2002 2022 - Ap Su>=1 2 1 D R m 2002 2022 - O lastSu 2 0 S -Z America/Cancun -5:47:4 - LMT 1922 Ja 1 6u --6 - CST 1981 D 23 --5 m E%sT 1998 Au 2 2 --6 m C%sT 2015 F 1 2 --5 - EST -Z America/Merida -5:58:28 - LMT 1922 Ja 1 6u --6 - CST 1981 D 23 --5 - EST 1982 D 2 --6 m C%sT -Z America/Matamoros -6:30 - LMT 1922 Ja 1 6u --6 - CST 1988 --6 u C%sT 1989 --6 m C%sT 2010 --6 u C%sT -Z America/Monterrey -6:41:16 - LMT 1922 Ja 1 6u --6 - CST 1988 --6 u C%sT 1989 --6 m C%sT -Z America/Mexico_City -6:36:36 - LMT 1922 Ja 1 7u --7 - MST 1927 Jun 10 23 --6 - CST 1930 N 15 --7 m M%sT 1932 Ap --6 m C%sT 2001 S 30 2 --6 - CST 2002 F 20 --6 m C%sT -Z America/Ciudad_Juarez -7:5:56 - LMT 1922 Ja 1 7u --7 - MST 1927 Jun 10 23 --6 - CST 1930 N 15 --7 m M%sT 1932 Ap --6 - CST 1996 --6 m C%sT 1998 --6 - CST 1998 Ap Su>=1 3 --7 m M%sT 2010 --7 u M%sT 2022 O 30 2 --6 - CST 2022 N 30 --7 u M%sT -Z America/Ojinaga -6:57:40 - LMT 1922 Ja 1 7u --7 - MST 1927 Jun 10 23 --6 - CST 1930 N 15 --7 m M%sT 1932 Ap --6 - CST 1996 --6 m C%sT 1998 --6 - CST 1998 Ap Su>=1 3 --7 m M%sT 2010 --7 u M%sT 2022 O 30 2 --6 - CST 2022 N 30 --6 u C%sT -Z America/Chihuahua -7:4:20 - LMT 1922 Ja 1 7u --7 - MST 1927 Jun 10 23 --6 - CST 1930 N 15 --7 m M%sT 1932 Ap --6 - CST 1996 --6 m C%sT 1998 --6 - CST 1998 Ap Su>=1 3 --7 m M%sT 2022 O 30 2 --6 - CST -Z America/Hermosillo -7:23:52 - LMT 1922 Ja 1 7u --7 - MST 1927 Jun 10 23 --6 - CST 1930 N 15 --7 m M%sT 1932 Ap --6 - CST 1942 Ap 24 --7 - MST 1949 Ja 14 --8 - PST 1970 --7 m M%sT 1999 --7 - MST -Z America/Mazatlan -7:5:40 - LMT 1922 Ja 1 7u --7 - MST 1927 Jun 10 23 --6 - CST 1930 N 15 --7 m M%sT 1932 Ap --6 - CST 1942 Ap 24 --7 - MST 1949 Ja 14 --8 - PST 1970 --7 m M%sT -Z America/Bahia_Banderas -7:1 - LMT 1922 Ja 1 7u --7 - MST 1927 Jun 10 23 --6 - CST 1930 N 15 --7 m M%sT 1932 Ap --6 - CST 1942 Ap 24 --7 - MST 1949 Ja 14 --8 - PST 1970 --7 m M%sT 2010 Ap 4 2 --6 m C%sT -Z America/Tijuana -7:48:4 - LMT 1922 Ja 1 7u --7 - MST 1924 --8 - PST 1927 Jun 10 23 --7 - MST 1930 N 15 --8 - PST 1931 Ap --8 1 PDT 1931 S 30 --8 - PST 1942 Ap 24 --8 1 PWT 1945 Au 14 23u --8 1 PPT 1945 N 12 --8 - PST 1948 Ap 5 --8 1 PDT 1949 Ja 14 --8 - PST 1954 --8 CA P%sT 1961 --8 - PST 1976 --8 u P%sT 1996 --8 m P%sT 2001 --8 u P%sT 2002 F 20 --8 m P%sT 2010 --8 u P%sT R BB 1942 o - Ap 19 5u 1 D R BB 1942 o - Au 31 6u 0 S R BB 1943 o - May 2 5u 1 D @@ -3294,10 +1755,6 @@ R BB 1977 1978 - O Su>=1 2 0 S R BB 1978 1980 - Ap Su>=15 2 1 D R BB 1979 o - S 30 2 0 S R BB 1980 o - S 25 2 0 S -Z America/Barbados -3:58:29 - LMT 1911 Au 28 --4 BB A%sT 1944 --4 BB AST/-0330 1945 --4 BB A%sT R BZ 1918 1941 - O Sa>=1 24 0:30 -0530 R BZ 1919 1942 - F Sa>=8 24 0 CST R BZ 1942 o - Jun 27 24 1 CWT @@ -3309,8 +1766,6 @@ R BZ 1973 o - D 5 0 1 CDT R BZ 1974 o - F 9 0 0 CST R BZ 1982 o - D 18 0 1 CDT R BZ 1983 o - F 12 0 0 CST -Z America/Belize -5:52:48 - LMT 1912 Ap --6 BZ %s R Be 1917 o - Ap 5 24 1 - R Be 1917 o - S 30 24 0 - R Be 1918 o - Ap 13 24 1 - @@ -3327,19 +1782,11 @@ R Be 1948 1952 - May Su>=22 2 1 D R Be 1948 1952 - S Su>=1 2 0 S R Be 1956 o - May Su>=22 2 1 D R Be 1956 o - O lastSu 2 0 S -Z Atlantic/Bermuda -4:19:18 - LMT 1890 --4:19:18 Be BMT/BST 1930 Ja 1 2 --4 Be A%sT 1974 Ap 28 2 --4 C A%sT 1976 --4 u A%sT R CR 1979 1980 - F lastSu 0 1 D R CR 1979 1980 - Jun Su>=1 0 0 S R CR 1991 1992 - Ja Sa>=15 0 1 D R CR 1991 o - Jul 1 0 0 S R CR 1992 o - Mar 15 0 0 S -Z America/Costa_Rica -5:36:13 - LMT 1890 --5:36:13 - SJMT 1921 Ja 15 --6 CR C%sT R Q 1928 o - Jun 10 0 1 D R Q 1928 o - O 10 0 0 S R Q 1940 1942 - Jun Su>=1 0 1 D @@ -3379,25 +1826,14 @@ R Q 2011 o - N 13 0s 0 S R Q 2012 o - Ap 1 0s 1 D R Q 2012 ma - N Su>=1 0s 0 S R Q 2013 ma - Mar Su>=8 0s 1 D -Z America/Havana -5:29:28 - LMT 1890 --5:29:36 - HMT 1925 Jul 19 12 --5 Q C%sT R DO 1966 o - O 30 0 1 EDT R DO 1967 o - F 28 0 0 EST R DO 1969 1973 - O lastSu 0 0:30 -0430 R DO 1970 o - F 21 0 0 EST R DO 1971 o - Ja 20 0 0 EST R DO 1972 1974 - Ja 21 0 0 EST -Z America/Santo_Domingo -4:39:36 - LMT 1890 --4:40 - SDMT 1933 Ap 1 12 --5 DO %s 1974 O 27 --4 - AST 2000 O 29 2 --5 u E%sT 2000 D 3 1 --4 - AST R SV 1987 1988 - May Su>=1 0 1 D R SV 1987 1988 - S lastSu 0 0 S -Z America/El_Salvador -5:56:48 - LMT 1921 --6 SV C%sT R GT 1973 o - N 25 0 1 D R GT 1974 o - F 24 0 0 S R GT 1983 o - May 21 0 1 D @@ -3406,8 +1842,6 @@ R GT 1991 o - Mar 23 0 1 D R GT 1991 o - S 7 0 0 S R GT 2006 o - Ap 30 0 1 D R GT 2006 o - O 1 0 0 S -Z America/Guatemala -6:2:4 - LMT 1918 O 5 --6 GT C%sT R HT 1983 o - May 8 0 1 D R HT 1984 1987 - Ap lastSu 0 1 D R HT 1983 1987 - O lastSu 0 0 S @@ -3419,57 +1853,16 @@ R HT 2012 2015 - Mar Su>=8 2 1 D R HT 2012 2015 - N Su>=1 2 0 S R HT 2017 ma - Mar Su>=8 2 1 D R HT 2017 ma - N Su>=1 2 0 S -Z America/Port-au-Prince -4:49:20 - LMT 1890 --4:49 - PPMT 1917 Ja 24 12 --5 HT E%sT R HN 1987 1988 - May Su>=1 0 1 D R HN 1987 1988 - S lastSu 0 0 S R HN 2006 o - May Su>=1 0 1 D R HN 2006 o - Au M>=1 0 0 S -Z America/Tegucigalpa -5:48:52 - LMT 1921 Ap --6 HN C%sT -Z America/Jamaica -5:7:10 - LMT 1890 --5:7:10 - KMT 1912 F --5 - EST 1974 --5 u E%sT 1984 --5 - EST -Z America/Martinique -4:4:20 - LMT 1890 --4:4:20 - FFMT 1911 May --4 - AST 1980 Ap 6 --4 1 ADT 1980 S 28 --4 - AST R NI 1979 1980 - Mar Su>=16 0 1 D R NI 1979 1980 - Jun M>=23 0 0 S R NI 2005 o - Ap 10 0 1 D R NI 2005 o - O Su>=1 0 0 S R NI 2006 o - Ap 30 2 1 D R NI 2006 o - O Su>=1 1 0 S -Z America/Managua -5:45:8 - LMT 1890 --5:45:12 - MMT 1934 Jun 23 --6 - CST 1973 May --5 - EST 1975 F 16 --6 NI C%sT 1992 Ja 1 4 --5 - EST 1992 S 24 --6 - CST 1993 --5 - EST 1997 --6 NI C%sT -Z America/Panama -5:18:8 - LMT 1890 --5:19:36 - CMT 1908 Ap 22 --5 - EST -Z America/Puerto_Rico -4:24:25 - LMT 1899 Mar 28 12 --4 - AST 1942 May 3 --4 u A%sT 1946 --4 - AST -Z America/Miquelon -3:44:40 - LMT 1911 May 15 --4 - AST 1980 May --3 - -03 1987 --3 C -03/-02 -Z America/Grand_Turk -4:44:32 - LMT 1890 --5:7:10 - KMT 1912 F --5 - EST 1979 --5 u E%sT 2015 Mar 8 2 --4 - AST 2018 Mar 11 3 --5 u E%sT R A 1930 o - D 1 0 1 - R A 1931 o - Ap 1 0 0 - R A 1931 o - O 15 0 1 - @@ -3499,150 +1892,8 @@ R A 2000 o - Mar 3 0 0 - R A 2007 o - D 30 0 1 - R A 2008 2009 - Mar Su>=15 0 0 - R A 2008 o - O Su>=15 0 1 - -Z America/Argentina/Buenos_Aires -3:53:48 - LMT 1894 O 31 --4:16:48 - CMT 1920 May --4 - -04 1930 D --4 A -04/-03 1969 O 5 --3 A -03/-02 1999 O 3 --4 A -04/-03 2000 Mar 3 --3 A -03/-02 -Z America/Argentina/Cordoba -4:16:48 - LMT 1894 O 31 --4:16:48 - CMT 1920 May --4 - -04 1930 D --4 A -04/-03 1969 O 5 --3 A -03/-02 1991 Mar 3 --4 - -04 1991 O 20 --3 A -03/-02 1999 O 3 --4 A -04/-03 2000 Mar 3 --3 A -03/-02 -Z America/Argentina/Salta -4:21:40 - LMT 1894 O 31 --4:16:48 - CMT 1920 May --4 - -04 1930 D --4 A -04/-03 1969 O 5 --3 A -03/-02 1991 Mar 3 --4 - -04 1991 O 20 --3 A -03/-02 1999 O 3 --4 A -04/-03 2000 Mar 3 --3 A -03/-02 2008 O 18 --3 - -03 -Z America/Argentina/Tucuman -4:20:52 - LMT 1894 O 31 --4:16:48 - CMT 1920 May --4 - -04 1930 D --4 A -04/-03 1969 O 5 --3 A -03/-02 1991 Mar 3 --4 - -04 1991 O 20 --3 A -03/-02 1999 O 3 --4 A -04/-03 2000 Mar 3 --3 - -03 2004 Jun --4 - -04 2004 Jun 13 --3 A -03/-02 -Z America/Argentina/La_Rioja -4:27:24 - LMT 1894 O 31 --4:16:48 - CMT 1920 May --4 - -04 1930 D --4 A -04/-03 1969 O 5 --3 A -03/-02 1991 Mar --4 - -04 1991 May 7 --3 A -03/-02 1999 O 3 --4 A -04/-03 2000 Mar 3 --3 - -03 2004 Jun --4 - -04 2004 Jun 20 --3 A -03/-02 2008 O 18 --3 - -03 -Z America/Argentina/San_Juan -4:34:4 - LMT 1894 O 31 --4:16:48 - CMT 1920 May --4 - -04 1930 D --4 A -04/-03 1969 O 5 --3 A -03/-02 1991 Mar --4 - -04 1991 May 7 --3 A -03/-02 1999 O 3 --4 A -04/-03 2000 Mar 3 --3 - -03 2004 May 31 --4 - -04 2004 Jul 25 --3 A -03/-02 2008 O 18 --3 - -03 -Z America/Argentina/Jujuy -4:21:12 - LMT 1894 O 31 --4:16:48 - CMT 1920 May --4 - -04 1930 D --4 A -04/-03 1969 O 5 --3 A -03/-02 1990 Mar 4 --4 - -04 1990 O 28 --4 1 -03 1991 Mar 17 --4 - -04 1991 O 6 --3 1 -02 1992 --3 A -03/-02 1999 O 3 --4 A -04/-03 2000 Mar 3 --3 A -03/-02 2008 O 18 --3 - -03 -Z America/Argentina/Catamarca -4:23:8 - LMT 1894 O 31 --4:16:48 - CMT 1920 May --4 - -04 1930 D --4 A -04/-03 1969 O 5 --3 A -03/-02 1991 Mar 3 --4 - -04 1991 O 20 --3 A -03/-02 1999 O 3 --4 A -04/-03 2000 Mar 3 --3 - -03 2004 Jun --4 - -04 2004 Jun 20 --3 A -03/-02 2008 O 18 --3 - -03 -Z America/Argentina/Mendoza -4:35:16 - LMT 1894 O 31 --4:16:48 - CMT 1920 May --4 - -04 1930 D --4 A -04/-03 1969 O 5 --3 A -03/-02 1990 Mar 4 --4 - -04 1990 O 15 --4 1 -03 1991 Mar --4 - -04 1991 O 15 --4 1 -03 1992 Mar --4 - -04 1992 O 18 --3 A -03/-02 1999 O 3 --4 A -04/-03 2000 Mar 3 --3 - -03 2004 May 23 --4 - -04 2004 S 26 --3 A -03/-02 2008 O 18 --3 - -03 R Sa 2008 2009 - Mar Su>=8 0 0 - R Sa 2007 2008 - O Su>=8 0 1 - -Z America/Argentina/San_Luis -4:25:24 - LMT 1894 O 31 --4:16:48 - CMT 1920 May --4 - -04 1930 D --4 A -04/-03 1969 O 5 --3 A -03/-02 1990 --3 1 -02 1990 Mar 14 --4 - -04 1990 O 15 --4 1 -03 1991 Mar --4 - -04 1991 Jun --3 - -03 1999 O 3 --4 1 -03 2000 Mar 3 --3 - -03 2004 May 31 --4 - -04 2004 Jul 25 --3 A -03/-02 2008 Ja 21 --4 Sa -04/-03 2009 O 11 --3 - -03 -Z America/Argentina/Rio_Gallegos -4:36:52 - LMT 1894 O 31 --4:16:48 - CMT 1920 May --4 - -04 1930 D --4 A -04/-03 1969 O 5 --3 A -03/-02 1999 O 3 --4 A -04/-03 2000 Mar 3 --3 - -03 2004 Jun --4 - -04 2004 Jun 20 --3 A -03/-02 2008 O 18 --3 - -03 -Z America/Argentina/Ushuaia -4:33:12 - LMT 1894 O 31 --4:16:48 - CMT 1920 May --4 - -04 1930 D --4 A -04/-03 1969 O 5 --3 A -03/-02 1999 O 3 --4 A -04/-03 2000 Mar 3 --3 - -03 2004 May 30 --4 - -04 2004 Jun 20 --3 A -03/-02 2008 O 18 --3 - -03 -Z America/La_Paz -4:32:36 - LMT 1890 --4:32:36 - CMT 1931 O 15 --4:32:36 1 BST 1932 Mar 21 --4 - -04 R B 1931 o - O 3 11 1 - R B 1932 1933 - Ap 1 0 0 - R B 1932 o - O 3 0 1 - @@ -3700,90 +1951,6 @@ R B 2013 2014 - F Su>=15 0 0 - R B 2015 o - F Su>=22 0 0 - R B 2016 2019 - F Su>=15 0 0 - R B 2018 o - N Su>=1 0 1 - -Z America/Noronha -2:9:40 - LMT 1914 --2 B -02/-01 1990 S 17 --2 - -02 1999 S 30 --2 B -02/-01 2000 O 15 --2 - -02 2001 S 13 --2 B -02/-01 2002 O --2 - -02 -Z America/Belem -3:13:56 - LMT 1914 --3 B -03/-02 1988 S 12 --3 - -03 -Z America/Santarem -3:38:48 - LMT 1914 --4 B -04/-03 1988 S 12 --4 - -04 2008 Jun 24 --3 - -03 -Z America/Fortaleza -2:34 - LMT 1914 --3 B -03/-02 1990 S 17 --3 - -03 1999 S 30 --3 B -03/-02 2000 O 22 --3 - -03 2001 S 13 --3 B -03/-02 2002 O --3 - -03 -Z America/Recife -2:19:36 - LMT 1914 --3 B -03/-02 1990 S 17 --3 - -03 1999 S 30 --3 B -03/-02 2000 O 15 --3 - -03 2001 S 13 --3 B -03/-02 2002 O --3 - -03 -Z America/Araguaina -3:12:48 - LMT 1914 --3 B -03/-02 1990 S 17 --3 - -03 1995 S 14 --3 B -03/-02 2003 S 24 --3 - -03 2012 O 21 --3 B -03/-02 2013 S --3 - -03 -Z America/Maceio -2:22:52 - LMT 1914 --3 B -03/-02 1990 S 17 --3 - -03 1995 O 13 --3 B -03/-02 1996 S 4 --3 - -03 1999 S 30 --3 B -03/-02 2000 O 22 --3 - -03 2001 S 13 --3 B -03/-02 2002 O --3 - -03 -Z America/Bahia -2:34:4 - LMT 1914 --3 B -03/-02 2003 S 24 --3 - -03 2011 O 16 --3 B -03/-02 2012 O 21 --3 - -03 -Z America/Sao_Paulo -3:6:28 - LMT 1914 --3 B -03/-02 1963 O 23 --3 1 -02 1964 --3 B -03/-02 -Z America/Campo_Grande -3:38:28 - LMT 1914 --4 B -04/-03 -Z America/Cuiaba -3:44:20 - LMT 1914 --4 B -04/-03 2003 S 24 --4 - -04 2004 O --4 B -04/-03 -Z America/Porto_Velho -4:15:36 - LMT 1914 --4 B -04/-03 1988 S 12 --4 - -04 -Z America/Boa_Vista -4:2:40 - LMT 1914 --4 B -04/-03 1988 S 12 --4 - -04 1999 S 30 --4 B -04/-03 2000 O 15 --4 - -04 -Z America/Manaus -4:0:4 - LMT 1914 --4 B -04/-03 1988 S 12 --4 - -04 1993 S 28 --4 B -04/-03 1994 S 22 --4 - -04 -Z America/Eirunepe -4:39:28 - LMT 1914 --5 B -05/-04 1988 S 12 --5 - -05 1993 S 28 --5 B -05/-04 1994 S 22 --5 - -05 2008 Jun 24 --4 - -04 2013 N 10 --5 - -05 -Z America/Rio_Branco -4:31:12 - LMT 1914 --5 B -05/-04 1988 S 12 --5 - -05 2008 Jun 24 --4 - -04 2013 N 10 --5 - -05 R x 1927 1931 - S 1 0 1 - R x 1928 1932 - Ap 1 0 0 - R x 1968 o - N 3 4u 1 - @@ -3820,56 +1987,10 @@ R x 2019 ma - Ap Su>=2 3u 0 - R x 2019 2021 - S Su>=2 4u 1 - R x 2022 o - S Su>=9 4u 1 - R x 2023 ma - S Su>=2 4u 1 - -Z America/Santiago -4:42:45 - LMT 1890 --4:42:45 - SMT 1910 Ja 10 --5 - -05 1916 Jul --4:42:45 - SMT 1918 S 10 --4 - -04 1919 Jul --4:42:45 - SMT 1927 S --5 x -05/-04 1932 S --4 - -04 1942 Jun --5 - -05 1942 Au --4 - -04 1946 Jul 14 24 --4 1 -03 1946 Au 28 24 --5 1 -04 1947 Mar 31 24 --5 - -05 1947 May 21 23 --4 x -04/-03 -Z America/Punta_Arenas -4:43:40 - LMT 1890 --4:42:45 - SMT 1910 Ja 10 --5 - -05 1916 Jul --4:42:45 - SMT 1918 S 10 --4 - -04 1919 Jul --4:42:45 - SMT 1927 S --5 x -05/-04 1932 S --4 - -04 1942 Jun --5 - -05 1942 Au --4 - -04 1946 Au 28 24 --5 1 -04 1947 Mar 31 24 --5 - -05 1947 May 21 23 --4 x -04/-03 2016 D 4 --3 - -03 -Z Pacific/Easter -7:17:28 - LMT 1890 --7:17:28 - EMT 1932 S --7 x -07/-06 1982 Mar 14 3u --6 x -06/-05 -Z Antarctica/Palmer 0 - -00 1965 --4 A -04/-03 1969 O 5 --3 A -03/-02 1982 May --4 x -04/-03 2016 D 4 --3 - -03 R CO 1992 o - May 3 0 1 - R CO 1993 o - F 6 24 0 - -Z America/Bogota -4:56:16 - LMT 1884 Mar 13 --4:56:16 - BMT 1914 N 23 --5 CO -05/-04 R EC 1992 o - N 28 0 1 - R EC 1993 o - F 5 0 0 - -Z America/Guayaquil -5:19:20 - LMT 1890 --5:14 - QMT 1931 --5 EC -05/-04 -Z Pacific/Galapagos -5:58:24 - LMT 1931 --5 - -05 1986 --6 EC -06/-05 R FK 1937 1938 - S lastSu 0 1 - R FK 1938 1942 - Mar Su>=19 0 0 - R FK 1939 o - O 1 0 1 - @@ -3882,20 +2003,6 @@ R FK 1985 2000 - S Su>=9 0 1 - R FK 1986 2000 - Ap Su>=16 0 0 - R FK 2001 2010 - Ap Su>=15 2 0 - R FK 2001 2010 - S Su>=1 2 1 - -Z Atlantic/Stanley -3:51:24 - LMT 1890 --3:51:24 - SMT 1912 Mar 12 --4 FK -04/-03 1983 May --3 FK -03/-02 1985 S 15 --4 FK -04/-03 2010 S 5 2 --3 - -03 -Z America/Cayenne -3:29:20 - LMT 1911 Jul --4 - -04 1967 O --3 - -03 -Z America/Guyana -3:52:39 - LMT 1911 Au --4 - -04 1915 Mar --3:45 - -0345 1975 Au --3 - -03 1992 Mar 29 1 --4 - -04 R y 1975 1988 - O 1 0 1 - R y 1975 1978 - Mar 1 0 0 - R y 1979 1991 - Ap 1 0 0 - @@ -3918,11 +2025,6 @@ R y 2005 2009 - Mar Su>=8 0 0 - R y 2010 ma - O Su>=1 0 1 - R y 2010 2012 - Ap Su>=8 0 0 - R y 2013 ma - Mar Su>=22 0 0 - -Z America/Asuncion -3:50:40 - LMT 1890 --3:50:40 - AMT 1931 O 10 --4 - -04 1972 O --3 - -03 1974 Ap --4 y -04/-03 R PE 1938 o - Ja 1 0 1 - R PE 1938 o - Ap 1 0 0 - R PE 1938 1939 - S lastSu 0 1 - @@ -3933,16 +2035,6 @@ R PE 1990 o - Ja 1 0 1 - R PE 1990 o - Ap 1 0 0 - R PE 1994 o - Ja 1 0 1 - R PE 1994 o - Ap 1 0 0 - -Z America/Lima -5:8:12 - LMT 1890 --5:8:36 - LMT 1908 Jul 28 --5 PE -05/-04 -Z Atlantic/South_Georgia -2:26:8 - LMT 1890 --2 - -02 -Z America/Paramaribo -3:40:40 - LMT 1911 --3:40:52 - PMT 1935 --3:40:36 - PMT 1945 O --3:30 - -0330 1984 O --3 - -03 R U 1923 1925 - O 1 0 0:30 - R U 1924 1926 - Ap 1 0 0 - R U 1933 1938 - O lastSu 0 0:30 - @@ -3991,6 +2083,659 @@ R U 2005 o - Mar 27 2 0 - R U 2005 o - O 9 2 1 - R U 2006 2015 - Mar Su>=8 2 0 - R U 2006 2014 - O Su>=1 2 1 - +Z Africa/Abidjan -0:16:8 - LMT 1912 +0 - GMT +Z Africa/Algiers 0:12:12 - LMT 1891 Mar 16 +0:9:21 - PMT 1911 Mar 11 +0 d WE%sT 1940 F 25 2 +1 d CE%sT 1946 O 7 +0 - WET 1956 Ja 29 +1 - CET 1963 Ap 14 +0 d WE%sT 1977 O 21 +1 d CE%sT 1979 O 26 +0 d WE%sT 1981 May +1 - CET +Z Africa/Bissau -1:2:20 - LMT 1912 Ja 1 1u +-1 - -01 1975 +0 - GMT +Z Africa/Cairo 2:5:9 - LMT 1900 O +2 K EE%sT +Z Africa/Casablanca -0:30:20 - LMT 1913 O 26 +0 M +00/+01 1984 Mar 16 +1 - +01 1986 +0 M +00/+01 2018 O 28 3 +1 M +01/+00 +Z Africa/Ceuta -0:21:16 - LMT 1901 Ja 1 0u +0 - WET 1918 May 6 23 +0 1 WEST 1918 O 7 23 +0 - WET 1924 +0 s WE%sT 1929 +0 - WET 1967 +0 Sp WE%sT 1984 Mar 16 +1 - CET 1986 +1 E CE%sT +Z Africa/El_Aaiun -0:52:48 - LMT 1934 +-1 - -01 1976 Ap 14 +0 M +00/+01 2018 O 28 3 +1 M +01/+00 +Z Africa/Johannesburg 1:52 - LMT 1892 F 8 +1:30 - SAST 1903 Mar +2 SA SAST +Z Africa/Juba 2:6:28 - LMT 1931 +2 SD CA%sT 2000 Ja 15 12 +3 - EAT 2021 F +2 - CAT +Z Africa/Khartoum 2:10:8 - LMT 1931 +2 SD CA%sT 2000 Ja 15 12 +3 - EAT 2017 N +2 - CAT +Z Africa/Lagos 0:13:35 - LMT 1905 Jul +0 - GMT 1908 Jul +0:13:35 - LMT 1914 +0:30 - +0030 1919 S +1 - WAT +Z Africa/Maputo 2:10:20 - LMT 1903 Mar +2 - CAT +Z Africa/Monrovia -0:43:8 - LMT 1882 +-0:43:8 - MMT 1919 Mar +-0:44:30 - MMT 1972 Ja 7 +0 - GMT +Z Africa/Nairobi 2:27:16 - LMT 1908 May +2:30 - +0230 1928 Jun 30 24 +3 - EAT 1930 Ja 4 24 +2:30 - +0230 1936 D 31 24 +2:45 - +0245 1942 Jul 31 24 +3 - EAT +Z Africa/Ndjamena 1:0:12 - LMT 1912 +1 - WAT 1979 O 14 +1 1 WAST 1980 Mar 8 +1 - WAT +Z Africa/Sao_Tome 0:26:56 - LMT 1884 +-0:36:45 - LMT 1912 Ja 1 0u +0 - GMT 2018 Ja 1 1 +1 - WAT 2019 Ja 1 2 +0 - GMT +Z Africa/Tripoli 0:52:44 - LMT 1920 +1 L CE%sT 1959 +2 - EET 1982 +1 L CE%sT 1990 May 4 +2 - EET 1996 S 30 +1 L CE%sT 1997 O 4 +2 - EET 2012 N 10 2 +1 L CE%sT 2013 O 25 2 +2 - EET +Z Africa/Tunis 0:40:44 - LMT 1881 May 12 +0:9:21 - PMT 1911 Mar 11 +1 n CE%sT +Z Africa/Windhoek 1:8:24 - LMT 1892 F 8 +1:30 - +0130 1903 Mar +2 - SAST 1942 S 20 2 +2 1 SAST 1943 Mar 21 2 +2 - SAST 1990 Mar 21 +2 NA %s +Z America/Adak 12:13:22 - LMT 1867 O 19 12:44:35 +-11:46:38 - LMT 1900 Au 20 12 +-11 - NST 1942 +-11 u N%sT 1946 +-11 - NST 1967 Ap +-11 - BST 1969 +-11 u B%sT 1983 O 30 2 +-10 u AH%sT 1983 N 30 +-10 u H%sT +Z America/Anchorage 14:0:24 - LMT 1867 O 19 14:31:37 +-9:59:36 - LMT 1900 Au 20 12 +-10 - AST 1942 +-10 u A%sT 1967 Ap +-10 - AHST 1969 +-10 u AH%sT 1983 O 30 2 +-9 u Y%sT 1983 N 30 +-9 u AK%sT +Z America/Araguaina -3:12:48 - LMT 1914 +-3 B -03/-02 1990 S 17 +-3 - -03 1995 S 14 +-3 B -03/-02 2003 S 24 +-3 - -03 2012 O 21 +-3 B -03/-02 2013 S +-3 - -03 +Z America/Argentina/Buenos_Aires -3:53:48 - LMT 1894 O 31 +-4:16:48 - CMT 1920 May +-4 - -04 1930 D +-4 A -04/-03 1969 O 5 +-3 A -03/-02 1999 O 3 +-4 A -04/-03 2000 Mar 3 +-3 A -03/-02 +Z America/Argentina/Catamarca -4:23:8 - LMT 1894 O 31 +-4:16:48 - CMT 1920 May +-4 - -04 1930 D +-4 A -04/-03 1969 O 5 +-3 A -03/-02 1991 Mar 3 +-4 - -04 1991 O 20 +-3 A -03/-02 1999 O 3 +-4 A -04/-03 2000 Mar 3 +-3 - -03 2004 Jun +-4 - -04 2004 Jun 20 +-3 A -03/-02 2008 O 18 +-3 - -03 +Z America/Argentina/Cordoba -4:16:48 - LMT 1894 O 31 +-4:16:48 - CMT 1920 May +-4 - -04 1930 D +-4 A -04/-03 1969 O 5 +-3 A -03/-02 1991 Mar 3 +-4 - -04 1991 O 20 +-3 A -03/-02 1999 O 3 +-4 A -04/-03 2000 Mar 3 +-3 A -03/-02 +Z America/Argentina/Jujuy -4:21:12 - LMT 1894 O 31 +-4:16:48 - CMT 1920 May +-4 - -04 1930 D +-4 A -04/-03 1969 O 5 +-3 A -03/-02 1990 Mar 4 +-4 - -04 1990 O 28 +-4 1 -03 1991 Mar 17 +-4 - -04 1991 O 6 +-3 1 -02 1992 +-3 A -03/-02 1999 O 3 +-4 A -04/-03 2000 Mar 3 +-3 A -03/-02 2008 O 18 +-3 - -03 +Z America/Argentina/La_Rioja -4:27:24 - LMT 1894 O 31 +-4:16:48 - CMT 1920 May +-4 - -04 1930 D +-4 A -04/-03 1969 O 5 +-3 A -03/-02 1991 Mar +-4 - -04 1991 May 7 +-3 A -03/-02 1999 O 3 +-4 A -04/-03 2000 Mar 3 +-3 - -03 2004 Jun +-4 - -04 2004 Jun 20 +-3 A -03/-02 2008 O 18 +-3 - -03 +Z America/Argentina/Mendoza -4:35:16 - LMT 1894 O 31 +-4:16:48 - CMT 1920 May +-4 - -04 1930 D +-4 A -04/-03 1969 O 5 +-3 A -03/-02 1990 Mar 4 +-4 - -04 1990 O 15 +-4 1 -03 1991 Mar +-4 - -04 1991 O 15 +-4 1 -03 1992 Mar +-4 - -04 1992 O 18 +-3 A -03/-02 1999 O 3 +-4 A -04/-03 2000 Mar 3 +-3 - -03 2004 May 23 +-4 - -04 2004 S 26 +-3 A -03/-02 2008 O 18 +-3 - -03 +Z America/Argentina/Rio_Gallegos -4:36:52 - LMT 1894 O 31 +-4:16:48 - CMT 1920 May +-4 - -04 1930 D +-4 A -04/-03 1969 O 5 +-3 A -03/-02 1999 O 3 +-4 A -04/-03 2000 Mar 3 +-3 - -03 2004 Jun +-4 - -04 2004 Jun 20 +-3 A -03/-02 2008 O 18 +-3 - -03 +Z America/Argentina/Salta -4:21:40 - LMT 1894 O 31 +-4:16:48 - CMT 1920 May +-4 - -04 1930 D +-4 A -04/-03 1969 O 5 +-3 A -03/-02 1991 Mar 3 +-4 - -04 1991 O 20 +-3 A -03/-02 1999 O 3 +-4 A -04/-03 2000 Mar 3 +-3 A -03/-02 2008 O 18 +-3 - -03 +Z America/Argentina/San_Juan -4:34:4 - LMT 1894 O 31 +-4:16:48 - CMT 1920 May +-4 - -04 1930 D +-4 A -04/-03 1969 O 5 +-3 A -03/-02 1991 Mar +-4 - -04 1991 May 7 +-3 A -03/-02 1999 O 3 +-4 A -04/-03 2000 Mar 3 +-3 - -03 2004 May 31 +-4 - -04 2004 Jul 25 +-3 A -03/-02 2008 O 18 +-3 - -03 +Z America/Argentina/San_Luis -4:25:24 - LMT 1894 O 31 +-4:16:48 - CMT 1920 May +-4 - -04 1930 D +-4 A -04/-03 1969 O 5 +-3 A -03/-02 1990 +-3 1 -02 1990 Mar 14 +-4 - -04 1990 O 15 +-4 1 -03 1991 Mar +-4 - -04 1991 Jun +-3 - -03 1999 O 3 +-4 1 -03 2000 Mar 3 +-3 - -03 2004 May 31 +-4 - -04 2004 Jul 25 +-3 A -03/-02 2008 Ja 21 +-4 Sa -04/-03 2009 O 11 +-3 - -03 +Z America/Argentina/Tucuman -4:20:52 - LMT 1894 O 31 +-4:16:48 - CMT 1920 May +-4 - -04 1930 D +-4 A -04/-03 1969 O 5 +-3 A -03/-02 1991 Mar 3 +-4 - -04 1991 O 20 +-3 A -03/-02 1999 O 3 +-4 A -04/-03 2000 Mar 3 +-3 - -03 2004 Jun +-4 - -04 2004 Jun 13 +-3 A -03/-02 +Z America/Argentina/Ushuaia -4:33:12 - LMT 1894 O 31 +-4:16:48 - CMT 1920 May +-4 - -04 1930 D +-4 A -04/-03 1969 O 5 +-3 A -03/-02 1999 O 3 +-4 A -04/-03 2000 Mar 3 +-3 - -03 2004 May 30 +-4 - -04 2004 Jun 20 +-3 A -03/-02 2008 O 18 +-3 - -03 +Z America/Asuncion -3:50:40 - LMT 1890 +-3:50:40 - AMT 1931 O 10 +-4 - -04 1972 O +-3 - -03 1974 Ap +-4 y -04/-03 +Z America/Bahia -2:34:4 - LMT 1914 +-3 B -03/-02 2003 S 24 +-3 - -03 2011 O 16 +-3 B -03/-02 2012 O 21 +-3 - -03 +Z America/Bahia_Banderas -7:1 - LMT 1922 Ja 1 7u +-7 - MST 1927 Jun 10 23 +-6 - CST 1930 N 15 +-7 m M%sT 1932 Ap +-6 - CST 1942 Ap 24 +-7 - MST 1949 Ja 14 +-8 - PST 1970 +-7 m M%sT 2010 Ap 4 2 +-6 m C%sT +Z America/Barbados -3:58:29 - LMT 1911 Au 28 +-4 BB A%sT 1944 +-4 BB AST/-0330 1945 +-4 BB A%sT +Z America/Belem -3:13:56 - LMT 1914 +-3 B -03/-02 1988 S 12 +-3 - -03 +Z America/Belize -5:52:48 - LMT 1912 Ap +-6 BZ %s +Z America/Boa_Vista -4:2:40 - LMT 1914 +-4 B -04/-03 1988 S 12 +-4 - -04 1999 S 30 +-4 B -04/-03 2000 O 15 +-4 - -04 +Z America/Bogota -4:56:16 - LMT 1884 Mar 13 +-4:56:16 - BMT 1914 N 23 +-5 CO -05/-04 +Z America/Boise -7:44:49 - LMT 1883 N 18 20u +-8 u P%sT 1923 May 13 2 +-7 u M%sT 1974 +-7 - MST 1974 F 3 2 +-7 u M%sT +Z America/Cambridge_Bay 0 - -00 1920 +-7 Y M%sT 1999 O 31 2 +-6 C C%sT 2000 O 29 2 +-5 - EST 2000 N 5 +-6 - CST 2001 Ap 1 3 +-7 C M%sT +Z America/Campo_Grande -3:38:28 - LMT 1914 +-4 B -04/-03 +Z America/Cancun -5:47:4 - LMT 1922 Ja 1 6u +-6 - CST 1981 D 23 +-5 m E%sT 1998 Au 2 2 +-6 m C%sT 2015 F 1 2 +-5 - EST +Z America/Caracas -4:27:44 - LMT 1890 +-4:27:40 - CMT 1912 F 12 +-4:30 - -0430 1965 +-4 - -04 2007 D 9 3 +-4:30 - -0430 2016 May 1 2:30 +-4 - -04 +Z America/Cayenne -3:29:20 - LMT 1911 Jul +-4 - -04 1967 O +-3 - -03 +Z America/Chicago -5:50:36 - LMT 1883 N 18 18u +-6 u C%sT 1920 +-6 Ch C%sT 1936 Mar 1 2 +-5 - EST 1936 N 15 2 +-6 Ch C%sT 1942 +-6 u C%sT 1946 +-6 Ch C%sT 1967 +-6 u C%sT +Z America/Chihuahua -7:4:20 - LMT 1922 Ja 1 7u +-7 - MST 1927 Jun 10 23 +-6 - CST 1930 N 15 +-7 m M%sT 1932 Ap +-6 - CST 1996 +-6 m C%sT 1998 +-6 - CST 1998 Ap Su>=1 3 +-7 m M%sT 2022 O 30 2 +-6 - CST +Z America/Ciudad_Juarez -7:5:56 - LMT 1922 Ja 1 7u +-7 - MST 1927 Jun 10 23 +-6 - CST 1930 N 15 +-7 m M%sT 1932 Ap +-6 - CST 1996 +-6 m C%sT 1998 +-6 - CST 1998 Ap Su>=1 3 +-7 m M%sT 2010 +-7 u M%sT 2022 O 30 2 +-6 - CST 2022 N 30 +-7 u M%sT +Z America/Costa_Rica -5:36:13 - LMT 1890 +-5:36:13 - SJMT 1921 Ja 15 +-6 CR C%sT +Z America/Cuiaba -3:44:20 - LMT 1914 +-4 B -04/-03 2003 S 24 +-4 - -04 2004 O +-4 B -04/-03 +Z America/Danmarkshavn -1:14:40 - LMT 1916 Jul 28 +-3 - -03 1980 Ap 6 2 +-3 E -03/-02 1996 +0 - GMT +Z America/Dawson -9:17:40 - LMT 1900 Au 20 +-9 Y Y%sT 1965 +-9 Yu Y%sT 1973 O 28 +-8 - PST 1980 +-8 C P%sT 2020 N +-7 - MST +Z America/Dawson_Creek -8:0:56 - LMT 1884 +-8 C P%sT 1947 +-8 Va P%sT 1972 Au 30 2 +-7 - MST +Z America/Denver -6:59:56 - LMT 1883 N 18 19u +-7 u M%sT 1920 +-7 De M%sT 1942 +-7 u M%sT 1946 +-7 De M%sT 1967 +-7 u M%sT +Z America/Detroit -5:32:11 - LMT 1905 +-6 - CST 1915 May 15 2 +-5 - EST 1942 +-5 u E%sT 1946 +-5 Dt E%sT 1967 Jun 14 0:1 +-5 u E%sT 1969 +-5 - EST 1973 +-5 u E%sT 1975 +-5 - EST 1975 Ap 27 2 +-5 u E%sT +Z America/Edmonton -7:33:52 - LMT 1906 S +-7 Ed M%sT 1987 +-7 C M%sT +Z America/Eirunepe -4:39:28 - LMT 1914 +-5 B -05/-04 1988 S 12 +-5 - -05 1993 S 28 +-5 B -05/-04 1994 S 22 +-5 - -05 2008 Jun 24 +-4 - -04 2013 N 10 +-5 - -05 +Z America/El_Salvador -5:56:48 - LMT 1921 +-6 SV C%sT +Z America/Fort_Nelson -8:10:47 - LMT 1884 +-8 Va P%sT 1946 +-8 - PST 1947 +-8 Va P%sT 1987 +-8 C P%sT 2015 Mar 8 2 +-7 - MST +Z America/Fortaleza -2:34 - LMT 1914 +-3 B -03/-02 1990 S 17 +-3 - -03 1999 S 30 +-3 B -03/-02 2000 O 22 +-3 - -03 2001 S 13 +-3 B -03/-02 2002 O +-3 - -03 +Z America/Glace_Bay -3:59:48 - LMT 1902 Jun 15 +-4 C A%sT 1953 +-4 H A%sT 1954 +-4 - AST 1972 +-4 H A%sT 1974 +-4 C A%sT +Z America/Goose_Bay -4:1:40 - LMT 1884 +-3:30:52 - NST 1918 +-3:30:52 C N%sT 1919 +-3:30:52 - NST 1935 Mar 30 +-3:30 - NST 1936 +-3:30 j N%sT 1942 May 11 +-3:30 C N%sT 1946 +-3:30 j N%sT 1966 Mar 15 2 +-4 j A%sT 2011 N +-4 C A%sT +Z America/Grand_Turk -4:44:32 - LMT 1890 +-5:7:10 - KMT 1912 F +-5 - EST 1979 +-5 u E%sT 2015 Mar 8 2 +-4 - AST 2018 Mar 11 3 +-5 u E%sT +Z America/Guatemala -6:2:4 - LMT 1918 O 5 +-6 GT C%sT +Z America/Guayaquil -5:19:20 - LMT 1890 +-5:14 - QMT 1931 +-5 EC -05/-04 +Z America/Guyana -3:52:39 - LMT 1911 Au +-4 - -04 1915 Mar +-3:45 - -0345 1975 Au +-3 - -03 1992 Mar 29 1 +-4 - -04 +Z America/Halifax -4:14:24 - LMT 1902 Jun 15 +-4 H A%sT 1918 +-4 C A%sT 1919 +-4 H A%sT 1942 F 9 2s +-4 C A%sT 1946 +-4 H A%sT 1974 +-4 C A%sT +Z America/Havana -5:29:28 - LMT 1890 +-5:29:36 - HMT 1925 Jul 19 12 +-5 Q C%sT +Z America/Hermosillo -7:23:52 - LMT 1922 Ja 1 7u +-7 - MST 1927 Jun 10 23 +-6 - CST 1930 N 15 +-7 m M%sT 1932 Ap +-6 - CST 1942 Ap 24 +-7 - MST 1949 Ja 14 +-8 - PST 1970 +-7 m M%sT 1999 +-7 - MST +Z America/Indiana/Indianapolis -5:44:38 - LMT 1883 N 18 18u +-6 u C%sT 1920 +-6 In C%sT 1942 +-6 u C%sT 1946 +-6 In C%sT 1955 Ap 24 2 +-5 - EST 1957 S 29 2 +-6 - CST 1958 Ap 27 2 +-5 - EST 1969 +-5 u E%sT 1971 +-5 - EST 2006 +-5 u E%sT +Z America/Indiana/Knox -5:46:30 - LMT 1883 N 18 18u +-6 u C%sT 1947 +-6 St C%sT 1962 Ap 29 2 +-5 - EST 1963 O 27 2 +-6 u C%sT 1991 O 27 2 +-5 - EST 2006 Ap 2 2 +-6 u C%sT +Z America/Indiana/Marengo -5:45:23 - LMT 1883 N 18 18u +-6 u C%sT 1951 +-6 Ma C%sT 1961 Ap 30 2 +-5 - EST 1969 +-5 u E%sT 1974 Ja 6 2 +-6 1 CDT 1974 O 27 2 +-5 u E%sT 1976 +-5 - EST 2006 +-5 u E%sT +Z America/Indiana/Petersburg -5:49:7 - LMT 1883 N 18 18u +-6 u C%sT 1955 +-6 Pi C%sT 1965 Ap 25 2 +-5 - EST 1966 O 30 2 +-6 u C%sT 1977 O 30 2 +-5 - EST 2006 Ap 2 2 +-6 u C%sT 2007 N 4 2 +-5 u E%sT +Z America/Indiana/Tell_City -5:47:3 - LMT 1883 N 18 18u +-6 u C%sT 1946 +-6 Pe C%sT 1964 Ap 26 2 +-5 - EST 1967 O 29 2 +-6 u C%sT 1969 Ap 27 2 +-5 u E%sT 1971 +-5 - EST 2006 Ap 2 2 +-6 u C%sT +Z America/Indiana/Vevay -5:40:16 - LMT 1883 N 18 18u +-6 u C%sT 1954 Ap 25 2 +-5 - EST 1969 +-5 u E%sT 1973 +-5 - EST 2006 +-5 u E%sT +Z America/Indiana/Vincennes -5:50:7 - LMT 1883 N 18 18u +-6 u C%sT 1946 +-6 V C%sT 1964 Ap 26 2 +-5 - EST 1969 +-5 u E%sT 1971 +-5 - EST 2006 Ap 2 2 +-6 u C%sT 2007 N 4 2 +-5 u E%sT +Z America/Indiana/Winamac -5:46:25 - LMT 1883 N 18 18u +-6 u C%sT 1946 +-6 Pu C%sT 1961 Ap 30 2 +-5 - EST 1969 +-5 u E%sT 1971 +-5 - EST 2006 Ap 2 2 +-6 u C%sT 2007 Mar 11 2 +-5 u E%sT +Z America/Inuvik 0 - -00 1953 +-8 Y P%sT 1979 Ap lastSu 2 +-7 Y M%sT 1980 +-7 C M%sT +Z America/Iqaluit 0 - -00 1942 Au +-5 Y E%sT 1999 O 31 2 +-6 C C%sT 2000 O 29 2 +-5 C E%sT +Z America/Jamaica -5:7:10 - LMT 1890 +-5:7:10 - KMT 1912 F +-5 - EST 1974 +-5 u E%sT 1984 +-5 - EST +Z America/Juneau 15:2:19 - LMT 1867 O 19 15:33:32 +-8:57:41 - LMT 1900 Au 20 12 +-8 - PST 1942 +-8 u P%sT 1946 +-8 - PST 1969 +-8 u P%sT 1980 Ap 27 2 +-9 u Y%sT 1980 O 26 2 +-8 u P%sT 1983 O 30 2 +-9 u Y%sT 1983 N 30 +-9 u AK%sT +Z America/Kentucky/Louisville -5:43:2 - LMT 1883 N 18 18u +-6 u C%sT 1921 +-6 v C%sT 1942 +-6 u C%sT 1946 +-6 v C%sT 1961 Jul 23 2 +-5 - EST 1968 +-5 u E%sT 1974 Ja 6 2 +-6 1 CDT 1974 O 27 2 +-5 u E%sT +Z America/Kentucky/Monticello -5:39:24 - LMT 1883 N 18 18u +-6 u C%sT 1946 +-6 - CST 1968 +-6 u C%sT 2000 O 29 2 +-5 u E%sT +Z America/La_Paz -4:32:36 - LMT 1890 +-4:32:36 - CMT 1931 O 15 +-4:32:36 1 BST 1932 Mar 21 +-4 - -04 +Z America/Lima -5:8:12 - LMT 1890 +-5:8:36 - LMT 1908 Jul 28 +-5 PE -05/-04 +Z America/Los_Angeles -7:52:58 - LMT 1883 N 18 20u +-8 u P%sT 1946 +-8 CA P%sT 1967 +-8 u P%sT +Z America/Maceio -2:22:52 - LMT 1914 +-3 B -03/-02 1990 S 17 +-3 - -03 1995 O 13 +-3 B -03/-02 1996 S 4 +-3 - -03 1999 S 30 +-3 B -03/-02 2000 O 22 +-3 - -03 2001 S 13 +-3 B -03/-02 2002 O +-3 - -03 +Z America/Managua -5:45:8 - LMT 1890 +-5:45:12 - MMT 1934 Jun 23 +-6 - CST 1973 May +-5 - EST 1975 F 16 +-6 NI C%sT 1992 Ja 1 4 +-5 - EST 1992 S 24 +-6 - CST 1993 +-5 - EST 1997 +-6 NI C%sT +Z America/Manaus -4:0:4 - LMT 1914 +-4 B -04/-03 1988 S 12 +-4 - -04 1993 S 28 +-4 B -04/-03 1994 S 22 +-4 - -04 +Z America/Martinique -4:4:20 - LMT 1890 +-4:4:20 - FFMT 1911 May +-4 - AST 1980 Ap 6 +-4 1 ADT 1980 S 28 +-4 - AST +Z America/Matamoros -6:30 - LMT 1922 Ja 1 6u +-6 - CST 1988 +-6 u C%sT 1989 +-6 m C%sT 2010 +-6 u C%sT +Z America/Mazatlan -7:5:40 - LMT 1922 Ja 1 7u +-7 - MST 1927 Jun 10 23 +-6 - CST 1930 N 15 +-7 m M%sT 1932 Ap +-6 - CST 1942 Ap 24 +-7 - MST 1949 Ja 14 +-8 - PST 1970 +-7 m M%sT +Z America/Menominee -5:50:27 - LMT 1885 S 18 12 +-6 u C%sT 1946 +-6 Me C%sT 1969 Ap 27 2 +-5 - EST 1973 Ap 29 2 +-6 u C%sT +Z America/Merida -5:58:28 - LMT 1922 Ja 1 6u +-6 - CST 1981 D 23 +-5 - EST 1982 D 2 +-6 m C%sT +Z America/Metlakatla 15:13:42 - LMT 1867 O 19 15:44:55 +-8:46:18 - LMT 1900 Au 20 12 +-8 - PST 1942 +-8 u P%sT 1946 +-8 - PST 1969 +-8 u P%sT 1983 O 30 2 +-8 - PST 2015 N 1 2 +-9 u AK%sT 2018 N 4 2 +-8 - PST 2019 Ja 20 2 +-9 u AK%sT +Z America/Mexico_City -6:36:36 - LMT 1922 Ja 1 7u +-7 - MST 1927 Jun 10 23 +-6 - CST 1930 N 15 +-7 m M%sT 1932 Ap +-6 m C%sT 2001 S 30 2 +-6 - CST 2002 F 20 +-6 m C%sT +Z America/Miquelon -3:44:40 - LMT 1911 Jun 15 +-4 - AST 1980 May +-3 - -03 1987 +-3 C -03/-02 +Z America/Moncton -4:19:8 - LMT 1883 D 9 +-5 - EST 1902 Jun 15 +-4 C A%sT 1933 +-4 o A%sT 1942 +-4 C A%sT 1946 +-4 o A%sT 1973 +-4 C A%sT 1993 +-4 o A%sT 2007 +-4 C A%sT +Z America/Monterrey -6:41:16 - LMT 1922 Ja 1 6u +-6 - CST 1988 +-6 u C%sT 1989 +-6 m C%sT Z America/Montevideo -3:44:51 - LMT 1908 Jun 10 -3:44:51 - MMT 1920 May -4 - -04 1923 O @@ -4002,30 +2747,842 @@ Z America/Montevideo -3:44:51 - LMT 1908 Jun 10 -3 U -03/-0130 1974 Mar 10 -3 U -03/-0230 1974 D 22 -3 U -03/-02 -Z America/Caracas -4:27:44 - LMT 1890 --4:27:40 - CMT 1912 F 12 --4:30 - -0430 1965 --4 - -04 2007 D 9 3 --4:30 - -0430 2016 May 1 2:30 +Z America/New_York -4:56:2 - LMT 1883 N 18 17u +-5 u E%sT 1920 +-5 NY E%sT 1942 +-5 u E%sT 1946 +-5 NY E%sT 1967 +-5 u E%sT +Z America/Nome 12:58:22 - LMT 1867 O 19 13:29:35 +-11:1:38 - LMT 1900 Au 20 12 +-11 - NST 1942 +-11 u N%sT 1946 +-11 - NST 1967 Ap +-11 - BST 1969 +-11 u B%sT 1983 O 30 2 +-9 u Y%sT 1983 N 30 +-9 u AK%sT +Z America/Noronha -2:9:40 - LMT 1914 +-2 B -02/-01 1990 S 17 +-2 - -02 1999 S 30 +-2 B -02/-01 2000 O 15 +-2 - -02 2001 S 13 +-2 B -02/-01 2002 O +-2 - -02 +Z America/North_Dakota/Beulah -6:47:7 - LMT 1883 N 18 19u +-7 u M%sT 2010 N 7 2 +-6 u C%sT +Z America/North_Dakota/Center -6:45:12 - LMT 1883 N 18 19u +-7 u M%sT 1992 O 25 2 +-6 u C%sT +Z America/North_Dakota/New_Salem -6:45:39 - LMT 1883 N 18 19u +-7 u M%sT 2003 O 26 2 +-6 u C%sT +Z America/Nuuk -3:26:56 - LMT 1916 Jul 28 +-3 - -03 1980 Ap 6 2 +-3 E -03/-02 2023 Mar 26 1u +-2 - -02 2023 O 29 1u +-2 E -02/-01 +Z America/Ojinaga -6:57:40 - LMT 1922 Ja 1 7u +-7 - MST 1927 Jun 10 23 +-6 - CST 1930 N 15 +-7 m M%sT 1932 Ap +-6 - CST 1996 +-6 m C%sT 1998 +-6 - CST 1998 Ap Su>=1 3 +-7 m M%sT 2010 +-7 u M%sT 2022 O 30 2 +-6 - CST 2022 N 30 +-6 u C%sT +Z America/Panama -5:18:8 - LMT 1890 +-5:19:36 - CMT 1908 Ap 22 +-5 - EST +Z America/Paramaribo -3:40:40 - LMT 1911 +-3:40:52 - PMT 1935 +-3:40:36 - PMT 1945 O +-3:30 - -0330 1984 O +-3 - -03 +Z America/Phoenix -7:28:18 - LMT 1883 N 18 19u +-7 u M%sT 1944 Ja 1 0:1 +-7 - MST 1944 Ap 1 0:1 +-7 u M%sT 1944 O 1 0:1 +-7 - MST 1967 +-7 u M%sT 1968 Mar 21 +-7 - MST +Z America/Port-au-Prince -4:49:20 - LMT 1890 +-4:49 - PPMT 1917 Ja 24 12 +-5 HT E%sT +Z America/Porto_Velho -4:15:36 - LMT 1914 +-4 B -04/-03 1988 S 12 -4 - -04 -Z Etc/UTC 0 - UTC +Z America/Puerto_Rico -4:24:25 - LMT 1899 Mar 28 12 +-4 - AST 1942 May 3 +-4 u A%sT 1946 +-4 - AST +Z America/Punta_Arenas -4:43:40 - LMT 1890 +-4:42:45 - SMT 1910 Ja 10 +-5 - -05 1916 Jul +-4:42:45 - SMT 1918 S 10 +-4 - -04 1919 Jul +-4:42:45 - SMT 1927 S +-5 x -05/-04 1932 S +-4 - -04 1942 Jun +-5 - -05 1942 Au +-4 - -04 1946 Au 28 24 +-5 1 -04 1947 Mar 31 24 +-5 - -05 1947 May 21 23 +-4 x -04/-03 2016 D 4 +-3 - -03 +Z America/Rankin_Inlet 0 - -00 1957 +-6 Y C%sT 2000 O 29 2 +-5 - EST 2001 Ap 1 3 +-6 C C%sT +Z America/Recife -2:19:36 - LMT 1914 +-3 B -03/-02 1990 S 17 +-3 - -03 1999 S 30 +-3 B -03/-02 2000 O 15 +-3 - -03 2001 S 13 +-3 B -03/-02 2002 O +-3 - -03 +Z America/Regina -6:58:36 - LMT 1905 S +-7 r M%sT 1960 Ap lastSu 2 +-6 - CST +Z America/Resolute 0 - -00 1947 Au 31 +-6 Y C%sT 2000 O 29 2 +-5 - EST 2001 Ap 1 3 +-6 C C%sT 2006 O 29 2 +-5 - EST 2007 Mar 11 3 +-6 C C%sT +Z America/Rio_Branco -4:31:12 - LMT 1914 +-5 B -05/-04 1988 S 12 +-5 - -05 2008 Jun 24 +-4 - -04 2013 N 10 +-5 - -05 +Z America/Santarem -3:38:48 - LMT 1914 +-4 B -04/-03 1988 S 12 +-4 - -04 2008 Jun 24 +-3 - -03 +Z America/Santiago -4:42:45 - LMT 1890 +-4:42:45 - SMT 1910 Ja 10 +-5 - -05 1916 Jul +-4:42:45 - SMT 1918 S 10 +-4 - -04 1919 Jul +-4:42:45 - SMT 1927 S +-5 x -05/-04 1932 S +-4 - -04 1942 Jun +-5 - -05 1942 Au +-4 - -04 1946 Jul 14 24 +-4 1 -03 1946 Au 28 24 +-5 1 -04 1947 Mar 31 24 +-5 - -05 1947 May 21 23 +-4 x -04/-03 +Z America/Santo_Domingo -4:39:36 - LMT 1890 +-4:40 - SDMT 1933 Ap 1 12 +-5 DO %s 1974 O 27 +-4 - AST 2000 O 29 2 +-5 u E%sT 2000 D 3 1 +-4 - AST +Z America/Sao_Paulo -3:6:28 - LMT 1914 +-3 B -03/-02 1963 O 23 +-3 1 -02 1964 +-3 B -03/-02 +Z America/Scoresbysund -1:27:52 - LMT 1916 Jul 28 +-2 - -02 1980 Ap 6 2 +-2 c -02/-01 1981 Mar 29 +-1 E -01/+00 2024 Mar 31 +-2 E -02/-01 +Z America/Sitka 14:58:47 - LMT 1867 O 19 15:30 +-9:1:13 - LMT 1900 Au 20 12 +-8 - PST 1942 +-8 u P%sT 1946 +-8 - PST 1969 +-8 u P%sT 1983 O 30 2 +-9 u Y%sT 1983 N 30 +-9 u AK%sT +Z America/St_Johns -3:30:52 - LMT 1884 +-3:30:52 j N%sT 1918 +-3:30:52 C N%sT 1919 +-3:30:52 j N%sT 1935 Mar 30 +-3:30 j N%sT 1942 May 11 +-3:30 C N%sT 1946 +-3:30 j N%sT 2011 N +-3:30 C N%sT +Z America/Swift_Current -7:11:20 - LMT 1905 S +-7 C M%sT 1946 Ap lastSu 2 +-7 r M%sT 1950 +-7 Sw M%sT 1972 Ap lastSu 2 +-6 - CST +Z America/Tegucigalpa -5:48:52 - LMT 1921 Ap +-6 HN C%sT +Z America/Thule -4:35:8 - LMT 1916 Jul 28 +-4 Th A%sT +Z America/Tijuana -7:48:4 - LMT 1922 Ja 1 7u +-7 - MST 1924 +-8 - PST 1927 Jun 10 23 +-7 - MST 1930 N 15 +-8 - PST 1931 Ap +-8 1 PDT 1931 S 30 +-8 - PST 1942 Ap 24 +-8 1 PWT 1945 Au 14 23u +-8 1 PPT 1945 N 12 +-8 - PST 1948 Ap 5 +-8 1 PDT 1949 Ja 14 +-8 - PST 1954 +-8 CA P%sT 1961 +-8 - PST 1976 +-8 u P%sT 1996 +-8 m P%sT 2001 +-8 u P%sT 2002 F 20 +-8 m P%sT 2010 +-8 u P%sT +Z America/Toronto -5:17:32 - LMT 1895 +-5 C E%sT 1919 +-5 t E%sT 1942 F 9 2s +-5 C E%sT 1946 +-5 t E%sT 1974 +-5 C E%sT +Z America/Vancouver -8:12:28 - LMT 1884 +-8 Va P%sT 1987 +-8 C P%sT +Z America/Whitehorse -9:0:12 - LMT 1900 Au 20 +-9 Y Y%sT 1965 +-9 Yu Y%sT 1966 F 27 +-8 - PST 1980 +-8 C P%sT 2020 N +-7 - MST +Z America/Winnipeg -6:28:36 - LMT 1887 Jul 16 +-6 W C%sT 2006 +-6 C C%sT +Z America/Yakutat 14:41:5 - LMT 1867 O 19 15:12:18 +-9:18:55 - LMT 1900 Au 20 12 +-9 - YST 1942 +-9 u Y%sT 1946 +-9 - YST 1969 +-9 u Y%sT 1983 N 30 +-9 u AK%sT +Z Antarctica/Casey 0 - -00 1969 +8 - +08 2009 O 18 2 +11 - +11 2010 Mar 5 2 +8 - +08 2011 O 28 2 +11 - +11 2012 F 21 17u +8 - +08 2016 O 22 +11 - +11 2018 Mar 11 4 +8 - +08 2018 O 7 4 +11 - +11 2019 Mar 17 3 +8 - +08 2019 O 4 3 +11 - +11 2020 Mar 8 3 +8 - +08 2020 O 4 0:1 +11 - +11 2021 Mar 14 +8 - +08 2021 O 3 0:1 +11 - +11 2022 Mar 13 +8 - +08 2022 O 2 0:1 +11 - +11 2023 Mar 9 3 +8 - +08 +Z Antarctica/Davis 0 - -00 1957 Ja 13 +7 - +07 1964 N +0 - -00 1969 F +7 - +07 2009 O 18 2 +5 - +05 2010 Mar 10 20u +7 - +07 2011 O 28 2 +5 - +05 2012 F 21 20u +7 - +07 +Z Antarctica/Macquarie 0 - -00 1899 N +10 - AEST 1916 O 1 2 +10 1 AEDT 1917 F +10 AU AE%sT 1919 Ap 1 0s +0 - -00 1948 Mar 25 +10 AU AE%sT 1967 +10 AT AE%sT 2010 +10 1 AEDT 2011 +10 AT AE%sT +Z Antarctica/Mawson 0 - -00 1954 F 13 +6 - +06 2009 O 18 2 +5 - +05 +Z Antarctica/Palmer 0 - -00 1965 +-4 A -04/-03 1969 O 5 +-3 A -03/-02 1982 May +-4 x -04/-03 2016 D 4 +-3 - -03 +Z Antarctica/Rothera 0 - -00 1976 D +-3 - -03 +Z Antarctica/Troll 0 - -00 2005 F 12 +0 Tr %s +Z Antarctica/Vostok 0 - -00 1957 D 16 +7 - +07 1994 F +0 - -00 1994 N +7 - +07 2023 D 18 2 +5 - +05 +Z Asia/Almaty 5:7:48 - LMT 1924 May 2 +5 - +05 1930 Jun 21 +6 R +06/+07 1991 Mar 31 2s +5 R +05/+06 1992 Ja 19 2s +6 R +06/+07 2004 O 31 2s +6 - +06 2024 Mar +5 - +05 +Z Asia/Amman 2:23:44 - LMT 1931 +2 J EE%sT 2022 O 28 0s +3 - +03 +Z Asia/Anadyr 11:49:56 - LMT 1924 May 2 +12 - +12 1930 Jun 21 +13 R +13/+14 1982 Ap 1 0s +12 R +12/+13 1991 Mar 31 2s +11 R +11/+12 1992 Ja 19 2s +12 R +12/+13 2010 Mar 28 2s +11 R +11/+12 2011 Mar 27 2s +12 - +12 +Z Asia/Aqtau 3:21:4 - LMT 1924 May 2 +4 - +04 1930 Jun 21 +5 - +05 1981 O +6 - +06 1982 Ap +5 R +05/+06 1991 Mar 31 2s +4 R +04/+05 1992 Ja 19 2s +5 R +05/+06 1994 S 25 2s +4 R +04/+05 2004 O 31 2s +5 - +05 +Z Asia/Aqtobe 3:48:40 - LMT 1924 May 2 +4 - +04 1930 Jun 21 +5 - +05 1981 Ap +5 1 +06 1981 O +6 - +06 1982 Ap +5 R +05/+06 1991 Mar 31 2s +4 R +04/+05 1992 Ja 19 2s +5 R +05/+06 2004 O 31 2s +5 - +05 +Z Asia/Ashgabat 3:53:32 - LMT 1924 May 2 +4 - +04 1930 Jun 21 +5 R +05/+06 1991 Mar 31 2 +4 R +04/+05 1992 Ja 19 2 +5 - +05 +Z Asia/Atyrau 3:27:44 - LMT 1924 May 2 +3 - +03 1930 Jun 21 +5 - +05 1981 O +6 - +06 1982 Ap +5 R +05/+06 1991 Mar 31 2s +4 R +04/+05 1992 Ja 19 2s +5 R +05/+06 1999 Mar 28 2s +4 R +04/+05 2004 O 31 2s +5 - +05 +Z Asia/Baghdad 2:57:40 - LMT 1890 +2:57:36 - BMT 1918 +3 - +03 1982 May +3 IQ +03/+04 +Z Asia/Baku 3:19:24 - LMT 1924 May 2 +3 - +03 1957 Mar +4 R +04/+05 1991 Mar 31 2s +3 R +03/+04 1992 S lastSu 2s +4 - +04 1996 +4 E +04/+05 1997 +4 AZ +04/+05 +Z Asia/Bangkok 6:42:4 - LMT 1880 +6:42:4 - BMT 1920 Ap +7 - +07 +Z Asia/Barnaul 5:35 - LMT 1919 D 10 +6 - +06 1930 Jun 21 +7 R +07/+08 1991 Mar 31 2s +6 R +06/+07 1992 Ja 19 2s +7 R +07/+08 1995 May 28 +6 R +06/+07 2011 Mar 27 2s +7 - +07 2014 O 26 2s +6 - +06 2016 Mar 27 2s +7 - +07 +Z Asia/Beirut 2:22 - LMT 1880 +2 l EE%sT +Z Asia/Bishkek 4:58:24 - LMT 1924 May 2 +5 - +05 1930 Jun 21 +6 R +06/+07 1991 Mar 31 2s +5 R +05/+06 1991 Au 31 2 +5 KG +05/+06 2005 Au 12 +6 - +06 +Z Asia/Chita 7:33:52 - LMT 1919 D 15 +8 - +08 1930 Jun 21 +9 R +09/+10 1991 Mar 31 2s +8 R +08/+09 1992 Ja 19 2s +9 R +09/+10 2011 Mar 27 2s +10 - +10 2014 O 26 2s +8 - +08 2016 Mar 27 2 +9 - +09 +Z Asia/Choibalsan 7:38 - LMT 1905 Au +7 - +07 1978 +8 - +08 1983 Ap +9 X +09/+10 2008 Mar 31 +8 X +08/+09 +Z Asia/Colombo 5:19:24 - LMT 1880 +5:19:32 - MMT 1906 +5:30 - +0530 1942 Ja 5 +5:30 0:30 +06 1942 S +5:30 1 +0630 1945 O 16 2 +5:30 - +0530 1996 May 25 +6:30 - +0630 1996 O 26 0:30 +6 - +06 2006 Ap 15 0:30 +5:30 - +0530 +Z Asia/Damascus 2:25:12 - LMT 1920 +2 S EE%sT 2022 O 28 +3 - +03 +Z Asia/Dhaka 6:1:40 - LMT 1890 +5:53:20 - HMT 1941 O +6:30 - +0630 1942 May 15 +5:30 - +0530 1942 S +6:30 - +0630 1951 S 30 +6 - +06 2009 +6 BD +06/+07 +Z Asia/Dili 8:22:20 - LMT 1912 +8 - +08 1942 F 21 23 +9 - +09 1976 May 3 +8 - +08 2000 S 17 +9 - +09 +Z Asia/Dubai 3:41:12 - LMT 1920 +4 - +04 +Z Asia/Dushanbe 4:35:12 - LMT 1924 May 2 +5 - +05 1930 Jun 21 +6 R +06/+07 1991 Mar 31 2s +5 1 +06 1991 S 9 2s +5 - +05 +Z Asia/Famagusta 2:15:48 - LMT 1921 N 14 +2 CY EE%sT 1998 S +2 E EE%sT 2016 S 8 +3 - +03 2017 O 29 1u +2 E EE%sT +Z Asia/Gaza 2:17:52 - LMT 1900 O +2 Z EET/EEST 1948 May 15 +2 K EE%sT 1967 Jun 5 +2 Z I%sT 1996 +2 J EE%sT 1999 +2 P EE%sT 2008 Au 29 +2 - EET 2008 S +2 P EE%sT 2010 +2 - EET 2010 Mar 27 0:1 +2 P EE%sT 2011 Au +2 - EET 2012 +2 P EE%sT +Z Asia/Hebron 2:20:23 - LMT 1900 O +2 Z EET/EEST 1948 May 15 +2 K EE%sT 1967 Jun 5 +2 Z I%sT 1996 +2 J EE%sT 1999 +2 P EE%sT +Z Asia/Ho_Chi_Minh 7:6:30 - LMT 1906 Jul +7:6:30 - PLMT 1911 May +7 - +07 1942 D 31 23 +8 - +08 1945 Mar 14 23 +9 - +09 1945 S 1 24 +7 - +07 1947 Ap +8 - +08 1955 Jul 1 1 +7 - +07 1959 D 31 23 +8 - +08 1975 Jun 13 +7 - +07 +Z Asia/Hong_Kong 7:36:42 - LMT 1904 O 29 17u +8 - HKT 1941 Jun 15 3 +8 1 HKST 1941 O 1 4 +8 0:30 HKWT 1941 D 25 +9 - JST 1945 N 18 2 +8 HK HK%sT +Z Asia/Hovd 6:6:36 - LMT 1905 Au +6 - +06 1978 +7 X +07/+08 +Z Asia/Irkutsk 6:57:5 - LMT 1880 +6:57:5 - IMT 1920 Ja 25 +7 - +07 1930 Jun 21 +8 R +08/+09 1991 Mar 31 2s +7 R +07/+08 1992 Ja 19 2s +8 R +08/+09 2011 Mar 27 2s +9 - +09 2014 O 26 2s +8 - +08 +Z Asia/Jakarta 7:7:12 - LMT 1867 Au 10 +7:7:12 - BMT 1923 D 31 16:40u +7:20 - +0720 1932 N +7:30 - +0730 1942 Mar 23 +9 - +09 1945 S 23 +7:30 - +0730 1948 May +8 - +08 1950 May +7:30 - +0730 1964 +7 - WIB +Z Asia/Jayapura 9:22:48 - LMT 1932 N +9 - +09 1944 S +9:30 - +0930 1964 +9 - WIT +Z Asia/Jerusalem 2:20:54 - LMT 1880 +2:20:40 - JMT 1918 +2 Z I%sT +Z Asia/Kabul 4:36:48 - LMT 1890 +4 - +04 1945 +4:30 - +0430 +Z Asia/Kamchatka 10:34:36 - LMT 1922 N 10 +11 - +11 1930 Jun 21 +12 R +12/+13 1991 Mar 31 2s +11 R +11/+12 1992 Ja 19 2s +12 R +12/+13 2010 Mar 28 2s +11 R +11/+12 2011 Mar 27 2s +12 - +12 +Z Asia/Karachi 4:28:12 - LMT 1907 +5:30 - +0530 1942 S +5:30 1 +0630 1945 O 15 +5:30 - +0530 1951 S 30 +5 - +05 1971 Mar 26 +5 PK PK%sT +Z Asia/Kathmandu 5:41:16 - LMT 1920 +5:30 - +0530 1986 +5:45 - +0545 +Z Asia/Khandyga 9:2:13 - LMT 1919 D 15 +8 - +08 1930 Jun 21 +9 R +09/+10 1991 Mar 31 2s +8 R +08/+09 1992 Ja 19 2s +9 R +09/+10 2004 +10 R +10/+11 2011 Mar 27 2s +11 - +11 2011 S 13 0s +10 - +10 2014 O 26 2s +9 - +09 +Z Asia/Kolkata 5:53:28 - LMT 1854 Jun 28 +5:53:20 - HMT 1870 +5:21:10 - MMT 1906 +5:30 - IST 1941 O +5:30 1 +0630 1942 May 15 +5:30 - IST 1942 S +5:30 1 +0630 1945 O 15 +5:30 - IST +Z Asia/Krasnoyarsk 6:11:26 - LMT 1920 Ja 6 +6 - +06 1930 Jun 21 +7 R +07/+08 1991 Mar 31 2s +6 R +06/+07 1992 Ja 19 2s +7 R +07/+08 2011 Mar 27 2s +8 - +08 2014 O 26 2s +7 - +07 +Z Asia/Kuching 7:21:20 - LMT 1926 Mar +7:30 - +0730 1933 +8 NB +08/+0820 1942 F 16 +9 - +09 1945 S 12 +8 - +08 +Z Asia/Macau 7:34:10 - LMT 1904 O 30 +8 - CST 1941 D 21 23 +9 _ +09/+10 1945 S 30 24 +8 _ C%sT +Z Asia/Magadan 10:3:12 - LMT 1924 May 2 +10 - +10 1930 Jun 21 +11 R +11/+12 1991 Mar 31 2s +10 R +10/+11 1992 Ja 19 2s +11 R +11/+12 2011 Mar 27 2s +12 - +12 2014 O 26 2s +10 - +10 2016 Ap 24 2s +11 - +11 +Z Asia/Makassar 7:57:36 - LMT 1920 +7:57:36 - MMT 1932 N +8 - +08 1942 F 9 +9 - +09 1945 S 23 +8 - WITA +Z Asia/Manila -15:56 - LMT 1844 D 31 +8:4 - LMT 1899 May 11 +8 PH P%sT 1942 May +9 - JST 1944 N +8 PH P%sT +Z Asia/Nicosia 2:13:28 - LMT 1921 N 14 +2 CY EE%sT 1998 S +2 E EE%sT +Z Asia/Novokuznetsk 5:48:48 - LMT 1924 May +6 - +06 1930 Jun 21 +7 R +07/+08 1991 Mar 31 2s +6 R +06/+07 1992 Ja 19 2s +7 R +07/+08 2010 Mar 28 2s +6 R +06/+07 2011 Mar 27 2s +7 - +07 +Z Asia/Novosibirsk 5:31:40 - LMT 1919 D 14 6 +6 - +06 1930 Jun 21 +7 R +07/+08 1991 Mar 31 2s +6 R +06/+07 1992 Ja 19 2s +7 R +07/+08 1993 May 23 +6 R +06/+07 2011 Mar 27 2s +7 - +07 2014 O 26 2s +6 - +06 2016 Jul 24 2s +7 - +07 +Z Asia/Omsk 4:53:30 - LMT 1919 N 14 +5 - +05 1930 Jun 21 +6 R +06/+07 1991 Mar 31 2s +5 R +05/+06 1992 Ja 19 2s +6 R +06/+07 2011 Mar 27 2s +7 - +07 2014 O 26 2s +6 - +06 +Z Asia/Oral 3:25:24 - LMT 1924 May 2 +3 - +03 1930 Jun 21 +5 - +05 1981 Ap +5 1 +06 1981 O +6 - +06 1982 Ap +5 R +05/+06 1989 Mar 26 2s +4 R +04/+05 1992 Ja 19 2s +5 R +05/+06 1992 Mar 29 2s +4 R +04/+05 2004 O 31 2s +5 - +05 +Z Asia/Pontianak 7:17:20 - LMT 1908 May +7:17:20 - PMT 1932 N +7:30 - +0730 1942 Ja 29 +9 - +09 1945 S 23 +7:30 - +0730 1948 May +8 - +08 1950 May +7:30 - +0730 1964 +8 - WITA 1988 +7 - WIB +Z Asia/Pyongyang 8:23 - LMT 1908 Ap +8:30 - KST 1912 +9 - JST 1945 Au 24 +9 - KST 2015 Au 15 +8:30 - KST 2018 May 4 23:30 +9 - KST +Z Asia/Qatar 3:26:8 - LMT 1920 +4 - +04 1972 Jun +3 - +03 +Z Asia/Qostanay 4:14:28 - LMT 1924 May 2 +4 - +04 1930 Jun 21 +5 - +05 1981 Ap +5 1 +06 1981 O +6 - +06 1982 Ap +5 R +05/+06 1991 Mar 31 2s +4 R +04/+05 1992 Ja 19 2s +5 R +05/+06 2004 O 31 2s +6 - +06 2024 Mar +5 - +05 +Z Asia/Qyzylorda 4:21:52 - LMT 1924 May 2 +4 - +04 1930 Jun 21 +5 - +05 1981 Ap +5 1 +06 1981 O +6 - +06 1982 Ap +5 R +05/+06 1991 Mar 31 2s +4 R +04/+05 1991 S 29 2s +5 R +05/+06 1992 Ja 19 2s +6 R +06/+07 1992 Mar 29 2s +5 R +05/+06 2004 O 31 2s +6 - +06 2018 D 21 +5 - +05 +Z Asia/Riyadh 3:6:52 - LMT 1947 Mar 14 +3 - +03 +Z Asia/Sakhalin 9:30:48 - LMT 1905 Au 23 +9 - +09 1945 Au 25 +11 R +11/+12 1991 Mar 31 2s +10 R +10/+11 1992 Ja 19 2s +11 R +11/+12 1997 Mar lastSu 2s +10 R +10/+11 2011 Mar 27 2s +11 - +11 2014 O 26 2s +10 - +10 2016 Mar 27 2s +11 - +11 +Z Asia/Samarkand 4:27:53 - LMT 1924 May 2 +4 - +04 1930 Jun 21 +5 - +05 1981 Ap +5 1 +06 1981 O +6 - +06 1982 Ap +5 R +05/+06 1992 +5 - +05 +Z Asia/Seoul 8:27:52 - LMT 1908 Ap +8:30 - KST 1912 +9 - JST 1945 S 8 +9 KR K%sT 1954 Mar 21 +8:30 KR K%sT 1961 Au 10 +9 KR K%sT +Z Asia/Shanghai 8:5:43 - LMT 1901 +8 Sh C%sT 1949 May 28 +8 CN C%sT +Z Asia/Singapore 6:55:25 - LMT 1901 +6:55:25 - SMT 1905 Jun +7 - +07 1933 +7 0:20 +0720 1936 +7:20 - +0720 1941 S +7:30 - +0730 1942 F 16 +9 - +09 1945 S 12 +7:30 - +0730 1981 D 31 16u +8 - +08 +Z Asia/Srednekolymsk 10:14:52 - LMT 1924 May 2 +10 - +10 1930 Jun 21 +11 R +11/+12 1991 Mar 31 2s +10 R +10/+11 1992 Ja 19 2s +11 R +11/+12 2011 Mar 27 2s +12 - +12 2014 O 26 2s +11 - +11 +Z Asia/Taipei 8:6 - LMT 1896 +8 - CST 1937 O +9 - JST 1945 S 21 1 +8 f C%sT +Z Asia/Tashkent 4:37:11 - LMT 1924 May 2 +5 - +05 1930 Jun 21 +6 R +06/+07 1991 Mar 31 2 +5 R +05/+06 1992 +5 - +05 +Z Asia/Tbilisi 2:59:11 - LMT 1880 +2:59:11 - TBMT 1924 May 2 +3 - +03 1957 Mar +4 R +04/+05 1991 Mar 31 2s +3 R +03/+04 1992 +3 e +03/+04 1994 S lastSu +4 e +04/+05 1996 O lastSu +4 1 +05 1997 Mar lastSu +4 e +04/+05 2004 Jun 27 +3 R +03/+04 2005 Mar lastSu 2 +4 - +04 +Z Asia/Tehran 3:25:44 - LMT 1916 +3:25:44 - TMT 1935 Jun 13 +3:30 i +0330/+0430 1977 O 20 24 +4 i +04/+05 1979 +3:30 i +0330/+0430 +Z Asia/Thimphu 5:58:36 - LMT 1947 Au 15 +5:30 - +0530 1987 O +6 - +06 +Z Asia/Tokyo 9:18:59 - LMT 1887 D 31 15u +9 JP J%sT +Z Asia/Tomsk 5:39:51 - LMT 1919 D 22 +6 - +06 1930 Jun 21 +7 R +07/+08 1991 Mar 31 2s +6 R +06/+07 1992 Ja 19 2s +7 R +07/+08 2002 May 1 3 +6 R +06/+07 2011 Mar 27 2s +7 - +07 2014 O 26 2s +6 - +06 2016 May 29 2s +7 - +07 +Z Asia/Ulaanbaatar 7:7:32 - LMT 1905 Au +7 - +07 1978 +8 X +08/+09 +Z Asia/Urumqi 5:50:20 - LMT 1928 +6 - +06 +Z Asia/Ust-Nera 9:32:54 - LMT 1919 D 15 +8 - +08 1930 Jun 21 +9 R +09/+10 1981 Ap +11 R +11/+12 1991 Mar 31 2s +10 R +10/+11 1992 Ja 19 2s +11 R +11/+12 2011 Mar 27 2s +12 - +12 2011 S 13 0s +11 - +11 2014 O 26 2s +10 - +10 +Z Asia/Vladivostok 8:47:31 - LMT 1922 N 15 +9 - +09 1930 Jun 21 +10 R +10/+11 1991 Mar 31 2s +9 R +09/+10 1992 Ja 19 2s +10 R +10/+11 2011 Mar 27 2s +11 - +11 2014 O 26 2s +10 - +10 +Z Asia/Yakutsk 8:38:58 - LMT 1919 D 15 +8 - +08 1930 Jun 21 +9 R +09/+10 1991 Mar 31 2s +8 R +08/+09 1992 Ja 19 2s +9 R +09/+10 2011 Mar 27 2s +10 - +10 2014 O 26 2s +9 - +09 +Z Asia/Yangon 6:24:47 - LMT 1880 +6:24:47 - RMT 1920 +6:30 - +0630 1942 May +9 - +09 1945 May 3 +6:30 - +0630 +Z Asia/Yekaterinburg 4:2:33 - LMT 1916 Jul 3 +3:45:5 - PMT 1919 Jul 15 4 +4 - +04 1930 Jun 21 +5 R +05/+06 1991 Mar 31 2s +4 R +04/+05 1992 Ja 19 2s +5 R +05/+06 2011 Mar 27 2s +6 - +06 2014 O 26 2s +5 - +05 +Z Asia/Yerevan 2:58 - LMT 1924 May 2 +3 - +03 1957 Mar +4 R +04/+05 1991 Mar 31 2s +3 R +03/+04 1995 S 24 2s +4 - +04 1997 +4 R +04/+05 2011 +4 AM +04/+05 +Z Atlantic/Azores -1:42:40 - LMT 1884 +-1:54:32 - HMT 1912 Ja 1 2u +-2 p -02/-01 1942 Ap 25 22s +-2 p +00 1942 Au 15 22s +-2 p -02/-01 1943 Ap 17 22s +-2 p +00 1943 Au 28 22s +-2 p -02/-01 1944 Ap 22 22s +-2 p +00 1944 Au 26 22s +-2 p -02/-01 1945 Ap 21 22s +-2 p +00 1945 Au 25 22s +-2 p -02/-01 1966 Ap 3 2 +-1 p -01/+00 1983 S 25 1s +-1 W- -01/+00 1992 S 27 1s +0 E WE%sT 1993 Mar 28 1u +-1 E -01/+00 +Z Atlantic/Bermuda -4:19:18 - LMT 1890 +-4:19:18 Be BMT/BST 1930 Ja 1 2 +-4 Be A%sT 1974 Ap 28 2 +-4 C A%sT 1976 +-4 u A%sT +Z Atlantic/Canary -1:1:36 - LMT 1922 Mar +-1 - -01 1946 S 30 1 +0 - WET 1980 Ap 6 0s +0 1 WEST 1980 S 28 1u +0 E WE%sT +Z Atlantic/Cape_Verde -1:34:4 - LMT 1912 Ja 1 2u +-2 - -02 1942 S +-2 1 -01 1945 O 15 +-2 - -02 1975 N 25 2 +-1 - -01 +Z Atlantic/Faroe -0:27:4 - LMT 1908 Ja 11 +0 - WET 1981 +0 E WE%sT +Z Atlantic/Madeira -1:7:36 - LMT 1884 +-1:7:36 - FMT 1912 Ja 1 1u +-1 p -01/+00 1942 Ap 25 22s +-1 p +01 1942 Au 15 22s +-1 p -01/+00 1943 Ap 17 22s +-1 p +01 1943 Au 28 22s +-1 p -01/+00 1944 Ap 22 22s +-1 p +01 1944 Au 26 22s +-1 p -01/+00 1945 Ap 21 22s +-1 p +01 1945 Au 25 22s +-1 p -01/+00 1966 Ap 3 2 +0 p WE%sT 1983 S 25 1s +0 E WE%sT +Z Atlantic/South_Georgia -2:26:8 - LMT 1890 +-2 - -02 +Z Atlantic/Stanley -3:51:24 - LMT 1890 +-3:51:24 - SMT 1912 Mar 12 +-4 FK -04/-03 1983 May +-3 FK -03/-02 1985 S 15 +-4 FK -04/-03 2010 S 5 2 +-3 - -03 +Z Australia/Adelaide 9:14:20 - LMT 1895 F +9 - ACST 1899 May +9:30 AU AC%sT 1971 +9:30 AS AC%sT +Z Australia/Brisbane 10:12:8 - LMT 1895 +10 AU AE%sT 1971 +10 AQ AE%sT +Z Australia/Broken_Hill 9:25:48 - LMT 1895 F +10 - AEST 1896 Au 23 +9 - ACST 1899 May +9:30 AU AC%sT 1971 +9:30 AN AC%sT 2000 +9:30 AS AC%sT +Z Australia/Darwin 8:43:20 - LMT 1895 F +9 - ACST 1899 May +9:30 AU AC%sT +Z Australia/Eucla 8:35:28 - LMT 1895 D +8:45 AU +0845/+0945 1943 Jul +8:45 AW +0845/+0945 +Z Australia/Hobart 9:49:16 - LMT 1895 S +10 AT AE%sT 1919 O 24 +10 AU AE%sT 1967 +10 AT AE%sT +Z Australia/Lindeman 9:55:56 - LMT 1895 +10 AU AE%sT 1971 +10 AQ AE%sT 1992 Jul +10 Ho AE%sT +Z Australia/Lord_Howe 10:36:20 - LMT 1895 F +10 - AEST 1981 Mar +10:30 LH +1030/+1130 1985 Jul +10:30 LH +1030/+11 +Z Australia/Melbourne 9:39:52 - LMT 1895 F +10 AU AE%sT 1971 +10 AV AE%sT +Z Australia/Perth 7:43:24 - LMT 1895 D +8 AU AW%sT 1943 Jul +8 AW AW%sT +Z Australia/Sydney 10:4:52 - LMT 1895 F +10 AU AE%sT 1971 +10 AN AE%sT +Z CET 1 c CE%sT +Z CST6CDT -6 u C%sT +Z EET 2 E EE%sT +Z EST -5 - EST +Z EST5EDT -5 u E%sT Z Etc/GMT 0 - GMT -L Etc/GMT GMT -Z Etc/GMT-14 14 - +14 -Z Etc/GMT-13 13 - +13 -Z Etc/GMT-12 12 - +12 -Z Etc/GMT-11 11 - +11 -Z Etc/GMT-10 10 - +10 -Z Etc/GMT-9 9 - +09 -Z Etc/GMT-8 8 - +08 -Z Etc/GMT-7 7 - +07 -Z Etc/GMT-6 6 - +06 -Z Etc/GMT-5 5 - +05 -Z Etc/GMT-4 4 - +04 -Z Etc/GMT-3 3 - +03 -Z Etc/GMT-2 2 - +02 -Z Etc/GMT-1 1 - +01 Z Etc/GMT+1 -1 - -01 +Z Etc/GMT+10 -10 - -10 +Z Etc/GMT+11 -11 - -11 +Z Etc/GMT+12 -12 - -12 Z Etc/GMT+2 -2 - -02 Z Etc/GMT+3 -3 - -03 Z Etc/GMT+4 -4 - -04 @@ -4034,10 +3591,463 @@ Z Etc/GMT+6 -6 - -06 Z Etc/GMT+7 -7 - -07 Z Etc/GMT+8 -8 - -08 Z Etc/GMT+9 -9 - -09 -Z Etc/GMT+10 -10 - -10 -Z Etc/GMT+11 -11 - -11 -Z Etc/GMT+12 -12 - -12 +Z Etc/GMT-1 1 - +01 +Z Etc/GMT-10 10 - +10 +Z Etc/GMT-11 11 - +11 +Z Etc/GMT-12 12 - +12 +Z Etc/GMT-13 13 - +13 +Z Etc/GMT-14 14 - +14 +Z Etc/GMT-2 2 - +02 +Z Etc/GMT-3 3 - +03 +Z Etc/GMT-4 4 - +04 +Z Etc/GMT-5 5 - +05 +Z Etc/GMT-6 6 - +06 +Z Etc/GMT-7 7 - +07 +Z Etc/GMT-8 8 - +08 +Z Etc/GMT-9 9 - +09 +Z Etc/UTC 0 - UTC +Z Europe/Andorra 0:6:4 - LMT 1901 +0 - WET 1946 S 30 +1 - CET 1985 Mar 31 2 +1 E CE%sT +Z Europe/Astrakhan 3:12:12 - LMT 1924 May +3 - +03 1930 Jun 21 +4 R +04/+05 1989 Mar 26 2s +3 R +03/+04 1991 Mar 31 2s +4 - +04 1992 Mar 29 2s +3 R +03/+04 2011 Mar 27 2s +4 - +04 2014 O 26 2s +3 - +03 2016 Mar 27 2s +4 - +04 +Z Europe/Athens 1:34:52 - LMT 1895 S 14 +1:34:52 - AMT 1916 Jul 28 0:1 +2 g EE%sT 1941 Ap 30 +1 g CE%sT 1944 Ap 4 +2 g EE%sT 1981 +2 E EE%sT +Z Europe/Belgrade 1:22 - LMT 1884 +1 - CET 1941 Ap 18 23 +1 c CE%sT 1945 +1 - CET 1945 May 8 2s +1 1 CEST 1945 S 16 2s +1 - CET 1982 N 27 +1 E CE%sT +Z Europe/Berlin 0:53:28 - LMT 1893 Ap +1 c CE%sT 1945 May 24 2 +1 So CE%sT 1946 +1 DE CE%sT 1980 +1 E CE%sT +Z Europe/Brussels 0:17:30 - LMT 1880 +0:17:30 - BMT 1892 May 1 0:17:30 +0 - WET 1914 N 8 +1 - CET 1916 May +1 c CE%sT 1918 N 11 11u +0 b WE%sT 1940 May 20 2s +1 c CE%sT 1944 S 3 +1 b CE%sT 1977 +1 E CE%sT +Z Europe/Bucharest 1:44:24 - LMT 1891 O +1:44:24 - BMT 1931 Jul 24 +2 z EE%sT 1981 Mar 29 2s +2 c EE%sT 1991 +2 z EE%sT 1994 +2 e EE%sT 1997 +2 E EE%sT +Z Europe/Budapest 1:16:20 - LMT 1890 N +1 c CE%sT 1918 +1 h CE%sT 1941 Ap 7 23 +1 c CE%sT 1945 +1 h CE%sT 1984 +1 E CE%sT +Z Europe/Chisinau 1:55:20 - LMT 1880 +1:55 - CMT 1918 F 15 +1:44:24 - BMT 1931 Jul 24 +2 z EE%sT 1940 Au 15 +2 1 EEST 1941 Jul 17 +1 c CE%sT 1944 Au 24 +3 R MSK/MSD 1990 May 6 2 +2 R EE%sT 1992 +2 e EE%sT 1997 +2 MD EE%sT +Z Europe/Dublin -0:25:21 - LMT 1880 Au 2 +-0:25:21 - DMT 1916 May 21 2s +-0:25:21 1 IST 1916 O 1 2s +0 G %s 1921 D 6 +0 G GMT/IST 1940 F 25 2s +0 1 IST 1946 O 6 2s +0 - GMT 1947 Mar 16 2s +0 1 IST 1947 N 2 2s +0 - GMT 1948 Ap 18 2s +0 G GMT/IST 1968 O 27 +1 IE IST/GMT +Z Europe/Gibraltar -0:21:24 - LMT 1880 Au 2 +0 G %s 1957 Ap 14 2 +1 - CET 1982 +1 E CE%sT +Z Europe/Helsinki 1:39:49 - LMT 1878 May 31 +1:39:49 - HMT 1921 May +2 FI EE%sT 1983 +2 E EE%sT +Z Europe/Istanbul 1:55:52 - LMT 1880 +1:56:56 - IMT 1910 O +2 T EE%sT 1978 Jun 29 +3 T +03/+04 1984 N 1 2 +2 T EE%sT 2007 +2 E EE%sT 2011 Mar 27 1u +2 - EET 2011 Mar 28 1u +2 E EE%sT 2014 Mar 30 1u +2 - EET 2014 Mar 31 1u +2 E EE%sT 2015 O 25 1u +2 1 EEST 2015 N 8 1u +2 E EE%sT 2016 S 7 +3 - +03 +Z Europe/Kaliningrad 1:22 - LMT 1893 Ap +1 c CE%sT 1945 Ap 10 +2 O EE%sT 1946 Ap 7 +3 R MSK/MSD 1989 Mar 26 2s +2 R EE%sT 2011 Mar 27 2s +3 - +03 2014 O 26 2s +2 - EET +Z Europe/Kirov 3:18:48 - LMT 1919 Jul 1 0u +3 - +03 1930 Jun 21 +4 R +04/+05 1989 Mar 26 2s +3 R MSK/MSD 1991 Mar 31 2s +4 - +04 1992 Mar 29 2s +3 R MSK/MSD 2011 Mar 27 2s +4 - MSK 2014 O 26 2s +3 - MSK +Z Europe/Kyiv 2:2:4 - LMT 1880 +2:2:4 - KMT 1924 May 2 +2 - EET 1930 Jun 21 +3 - MSK 1941 S 20 +1 c CE%sT 1943 N 6 +3 R MSK/MSD 1990 Jul 1 2 +2 1 EEST 1991 S 29 3 +2 c EE%sT 1996 May 13 +2 E EE%sT +Z Europe/Lisbon -0:36:45 - LMT 1884 +-0:36:45 - LMT 1912 Ja 1 0u +0 p WE%sT 1966 Ap 3 2 +1 - CET 1976 S 26 1 +0 p WE%sT 1983 S 25 1s +0 W- WE%sT 1992 S 27 1s +1 E CE%sT 1996 Mar 31 1u +0 E WE%sT +Z Europe/London -0:1:15 - LMT 1847 D +0 G %s 1968 O 27 +1 - BST 1971 O 31 2u +0 G %s 1996 +0 E GMT/BST +Z Europe/Madrid -0:14:44 - LMT 1901 Ja 1 0u +0 s WE%sT 1940 Mar 16 23 +1 s CE%sT 1979 +1 E CE%sT +Z Europe/Malta 0:58:4 - LMT 1893 N 2 +1 I CE%sT 1973 Mar 31 +1 MT CE%sT 1981 +1 E CE%sT +Z Europe/Minsk 1:50:16 - LMT 1880 +1:50 - MMT 1924 May 2 +2 - EET 1930 Jun 21 +3 - MSK 1941 Jun 28 +1 c CE%sT 1944 Jul 3 +3 R MSK/MSD 1990 +3 - MSK 1991 Mar 31 2s +2 R EE%sT 2011 Mar 27 2s +3 - +03 +Z Europe/Moscow 2:30:17 - LMT 1880 +2:30:17 - MMT 1916 Jul 3 +2:31:19 R %s 1919 Jul 1 0u +3 R %s 1921 O +3 R MSK/MSD 1922 O +2 - EET 1930 Jun 21 +3 R MSK/MSD 1991 Mar 31 2s +2 R EE%sT 1992 Ja 19 2s +3 R MSK/MSD 2011 Mar 27 2s +4 - MSK 2014 O 26 2s +3 - MSK +Z Europe/Paris 0:9:21 - LMT 1891 Mar 16 +0:9:21 - PMT 1911 Mar 11 +0 F WE%sT 1940 Jun 14 23 +1 c CE%sT 1944 Au 25 +0 F WE%sT 1945 S 16 3 +1 F CE%sT 1977 +1 E CE%sT +Z Europe/Prague 0:57:44 - LMT 1850 +0:57:44 - PMT 1891 O +1 c CE%sT 1945 May 9 +1 CZ CE%sT 1946 D 1 3 +1 -1 GMT 1947 F 23 2 +1 CZ CE%sT 1979 +1 E CE%sT +Z Europe/Riga 1:36:34 - LMT 1880 +1:36:34 - RMT 1918 Ap 15 2 +1:36:34 1 LST 1918 S 16 3 +1:36:34 - RMT 1919 Ap 1 2 +1:36:34 1 LST 1919 May 22 3 +1:36:34 - RMT 1926 May 11 +2 - EET 1940 Au 5 +3 - MSK 1941 Jul +1 c CE%sT 1944 O 13 +3 R MSK/MSD 1989 Mar lastSu 2s +2 1 EEST 1989 S lastSu 2s +2 LV EE%sT 1997 Ja 21 +2 E EE%sT 2000 F 29 +2 - EET 2001 Ja 2 +2 E EE%sT +Z Europe/Rome 0:49:56 - LMT 1866 D 12 +0:49:56 - RMT 1893 O 31 23u +1 I CE%sT 1943 S 10 +1 c CE%sT 1944 Jun 4 +1 I CE%sT 1980 +1 E CE%sT +Z Europe/Samara 3:20:20 - LMT 1919 Jul 1 0u +3 - +03 1930 Jun 21 +4 - +04 1935 Ja 27 +4 R +04/+05 1989 Mar 26 2s +3 R +03/+04 1991 Mar 31 2s +2 R +02/+03 1991 S 29 2s +3 - +03 1991 O 20 3 +4 R +04/+05 2010 Mar 28 2s +3 R +03/+04 2011 Mar 27 2s +4 - +04 +Z Europe/Saratov 3:4:18 - LMT 1919 Jul 1 0u +3 - +03 1930 Jun 21 +4 R +04/+05 1988 Mar 27 2s +3 R +03/+04 1991 Mar 31 2s +4 - +04 1992 Mar 29 2s +3 R +03/+04 2011 Mar 27 2s +4 - +04 2014 O 26 2s +3 - +03 2016 D 4 2s +4 - +04 +Z Europe/Simferopol 2:16:24 - LMT 1880 +2:16 - SMT 1924 May 2 +2 - EET 1930 Jun 21 +3 - MSK 1941 N +1 c CE%sT 1944 Ap 13 +3 R MSK/MSD 1990 +3 - MSK 1990 Jul 1 2 +2 - EET 1992 Mar 20 +2 c EE%sT 1994 May +3 c MSK/MSD 1996 Mar 31 0s +3 1 MSD 1996 O 27 3s +3 - MSK 1997 Mar lastSu 1u +2 E EE%sT 2014 Mar 30 2 +4 - MSK 2014 O 26 2s +3 - MSK +Z Europe/Sofia 1:33:16 - LMT 1880 +1:56:56 - IMT 1894 N 30 +2 - EET 1942 N 2 3 +1 c CE%sT 1945 +1 - CET 1945 Ap 2 3 +2 - EET 1979 Mar 31 23 +2 BG EE%sT 1982 S 26 3 +2 c EE%sT 1991 +2 e EE%sT 1997 +2 E EE%sT +Z Europe/Tallinn 1:39 - LMT 1880 +1:39 - TMT 1918 F +1 c CE%sT 1919 Jul +1:39 - TMT 1921 May +2 - EET 1940 Au 6 +3 - MSK 1941 S 15 +1 c CE%sT 1944 S 22 +3 R MSK/MSD 1989 Mar 26 2s +2 1 EEST 1989 S 24 2s +2 c EE%sT 1998 S 22 +2 E EE%sT 1999 O 31 4 +2 - EET 2002 F 21 +2 E EE%sT +Z Europe/Tirane 1:19:20 - LMT 1914 +1 - CET 1940 Jun 16 +1 q CE%sT 1984 Jul +1 E CE%sT +Z Europe/Ulyanovsk 3:13:36 - LMT 1919 Jul 1 0u +3 - +03 1930 Jun 21 +4 R +04/+05 1989 Mar 26 2s +3 R +03/+04 1991 Mar 31 2s +2 R +02/+03 1992 Ja 19 2s +3 R +03/+04 2011 Mar 27 2s +4 - +04 2014 O 26 2s +3 - +03 2016 Mar 27 2s +4 - +04 +Z Europe/Vienna 1:5:21 - LMT 1893 Ap +1 c CE%sT 1920 +1 a CE%sT 1940 Ap 1 2s +1 c CE%sT 1945 Ap 2 2s +1 1 CEST 1945 Ap 12 2s +1 - CET 1946 +1 a CE%sT 1981 +1 E CE%sT +Z Europe/Vilnius 1:41:16 - LMT 1880 +1:24 - WMT 1917 +1:35:36 - KMT 1919 O 10 +1 - CET 1920 Jul 12 +2 - EET 1920 O 9 +1 - CET 1940 Au 3 +3 - MSK 1941 Jun 24 +1 c CE%sT 1944 Au +3 R MSK/MSD 1989 Mar 26 2s +2 R EE%sT 1991 S 29 2s +2 c EE%sT 1998 +2 - EET 1998 Mar 29 1u +1 E CE%sT 1999 O 31 1u +2 - EET 2003 +2 E EE%sT +Z Europe/Volgograd 2:57:40 - LMT 1920 Ja 3 +3 - +03 1930 Jun 21 +4 - +04 1961 N 11 +4 R +04/+05 1988 Mar 27 2s +3 R MSK/MSD 1991 Mar 31 2s +4 - +04 1992 Mar 29 2s +3 R MSK/MSD 2011 Mar 27 2s +4 - MSK 2014 O 26 2s +3 - MSK 2018 O 28 2s +4 - +04 2020 D 27 2s +3 - MSK +Z Europe/Warsaw 1:24 - LMT 1880 +1:24 - WMT 1915 Au 5 +1 c CE%sT 1918 S 16 3 +2 O EE%sT 1922 Jun +1 O CE%sT 1940 Jun 23 2 +1 c CE%sT 1944 O +1 O CE%sT 1977 +1 W- CE%sT 1988 +1 E CE%sT +Z Europe/Zurich 0:34:8 - LMT 1853 Jul 16 +0:29:46 - BMT 1894 Jun +1 CH CE%sT 1981 +1 E CE%sT Z Factory 0 - -00 +Z HST -10 - HST +Z Indian/Chagos 4:49:40 - LMT 1907 +5 - +05 1996 +6 - +06 +Z Indian/Maldives 4:54 - LMT 1880 +4:54 - MMT 1960 +5 - +05 +Z Indian/Mauritius 3:50 - LMT 1907 +4 MU +04/+05 +Z MET 1 c ME%sT +Z MST -7 - MST +Z MST7MDT -7 u M%sT +Z PST8PDT -8 u P%sT +Z Pacific/Apia 12:33:4 - LMT 1892 Jul 5 +-11:26:56 - LMT 1911 +-11:30 - -1130 1950 +-11 WS -11/-10 2011 D 29 24 +13 WS +13/+14 +Z Pacific/Auckland 11:39:4 - LMT 1868 N 2 +11:30 NZ NZ%sT 1946 +12 NZ NZ%sT +Z Pacific/Bougainville 10:22:16 - LMT 1880 +9:48:32 - PMMT 1895 +10 - +10 1942 Jul +9 - +09 1945 Au 21 +10 - +10 2014 D 28 2 +11 - +11 +Z Pacific/Chatham 12:13:48 - LMT 1868 N 2 +12:15 - +1215 1946 +12:45 k +1245/+1345 +Z Pacific/Easter -7:17:28 - LMT 1890 +-7:17:28 - EMT 1932 S +-7 x -07/-06 1982 Mar 14 3u +-6 x -06/-05 +Z Pacific/Efate 11:13:16 - LMT 1912 Ja 13 +11 VU +11/+12 +Z Pacific/Fakaofo -11:24:56 - LMT 1901 +-11 - -11 2011 D 30 +13 - +13 +Z Pacific/Fiji 11:55:44 - LMT 1915 O 26 +12 FJ +12/+13 +Z Pacific/Galapagos -5:58:24 - LMT 1931 +-5 - -05 1986 +-6 EC -06/-05 +Z Pacific/Gambier -8:59:48 - LMT 1912 O +-9 - -09 +Z Pacific/Guadalcanal 10:39:48 - LMT 1912 O +11 - +11 +Z Pacific/Guam -14:21 - LMT 1844 D 31 +9:39 - LMT 1901 +10 - GST 1941 D 10 +9 - +09 1944 Jul 31 +10 Gu G%sT 2000 D 23 +10 - ChST +Z Pacific/Honolulu -10:31:26 - LMT 1896 Ja 13 12 +-10:30 - HST 1933 Ap 30 2 +-10:30 1 HDT 1933 May 21 12 +-10:30 u H%sT 1947 Jun 8 2 +-10 - HST +Z Pacific/Kanton 0 - -00 1937 Au 31 +-12 - -12 1979 O +-11 - -11 1994 D 31 +13 - +13 +Z Pacific/Kiritimati -10:29:20 - LMT 1901 +-10:40 - -1040 1979 O +-10 - -10 1994 D 31 +14 - +14 +Z Pacific/Kosrae -13:8:4 - LMT 1844 D 31 +10:51:56 - LMT 1901 +11 - +11 1914 O +9 - +09 1919 F +11 - +11 1937 +10 - +10 1941 Ap +9 - +09 1945 Au +11 - +11 1969 O +12 - +12 1999 +11 - +11 +Z Pacific/Kwajalein 11:9:20 - LMT 1901 +11 - +11 1937 +10 - +10 1941 Ap +9 - +09 1944 F 6 +11 - +11 1969 O +-12 - -12 1993 Au 20 24 +12 - +12 +Z Pacific/Marquesas -9:18 - LMT 1912 O +-9:30 - -0930 +Z Pacific/Nauru 11:7:40 - LMT 1921 Ja 15 +11:30 - +1130 1942 Au 29 +9 - +09 1945 S 8 +11:30 - +1130 1979 F 10 2 +12 - +12 +Z Pacific/Niue -11:19:40 - LMT 1952 O 16 +-11:20 - -1120 1964 Jul +-11 - -11 +Z Pacific/Norfolk 11:11:52 - LMT 1901 +11:12 - +1112 1951 +11:30 - +1130 1974 O 27 2s +11:30 1 +1230 1975 Mar 2 2s +11:30 - +1130 2015 O 4 2s +11 - +11 2019 Jul +11 AN +11/+12 +Z Pacific/Noumea 11:5:48 - LMT 1912 Ja 13 +11 NC +11/+12 +Z Pacific/Pago_Pago 12:37:12 - LMT 1892 Jul 5 +-11:22:48 - LMT 1911 +-11 - SST +Z Pacific/Palau -15:2:4 - LMT 1844 D 31 +8:57:56 - LMT 1901 +9 - +09 +Z Pacific/Pitcairn -8:40:20 - LMT 1901 +-8:30 - -0830 1998 Ap 27 +-8 - -08 +Z Pacific/Port_Moresby 9:48:40 - LMT 1880 +9:48:32 - PMMT 1895 +10 - +10 +Z Pacific/Rarotonga 13:20:56 - LMT 1899 D 26 +-10:39:4 - LMT 1952 O 16 +-10:30 - -1030 1978 N 12 +-10 CK -10/-0930 +Z Pacific/Tahiti -9:58:16 - LMT 1912 O +-10 - -10 +Z Pacific/Tarawa 11:32:4 - LMT 1901 +12 - +12 +Z Pacific/Tongatapu 12:19:12 - LMT 1945 S 10 +12:20 - +1220 1961 +13 - +13 1999 +13 TO +13/+14 +Z WET 0 E WE%sT +L Etc/GMT GMT L Australia/Sydney Australia/ACT L Australia/Lord_Howe Australia/LHI L Australia/Sydney Australia/NSW @@ -4185,7 +4195,6 @@ L America/Puerto_Rico America/Tortola L Pacific/Port_Moresby Antarctica/DumontDUrville L Pacific/Auckland Antarctica/McMurdo L Asia/Riyadh Antarctica/Syowa -L Asia/Urumqi Antarctica/Vostok L Europe/Berlin Arctic/Longyearbyen L Asia/Riyadh Asia/Aden L Asia/Qatar Asia/Bahrain diff --git a/lib/pytz/zoneinfo/zone.tab b/lib/pytz/zoneinfo/zone.tab index dbcb6179..3fa9306a 100644 --- a/lib/pytz/zoneinfo/zone.tab +++ b/lib/pytz/zoneinfo/zone.tab @@ -48,7 +48,7 @@ AR -3124-06411 America/Argentina/Cordoba Argentina (most areas: CB, CC, CN, ER, AR -2447-06525 America/Argentina/Salta Salta (SA, LP, NQ, RN) AR -2411-06518 America/Argentina/Jujuy Jujuy (JY) AR -2649-06513 America/Argentina/Tucuman Tucuman (TM) -AR -2828-06547 America/Argentina/Catamarca Catamarca (CT); Chubut (CH) +AR -2828-06547 America/Argentina/Catamarca Catamarca (CT), Chubut (CH) AR -2926-06651 America/Argentina/La_Rioja La Rioja (LR) AR -3132-06831 America/Argentina/San_Juan San Juan (SJ) AR -3253-06849 America/Argentina/Mendoza Mendoza (MZ) @@ -87,7 +87,7 @@ BN +0456+11455 Asia/Brunei BO -1630-06809 America/La_Paz BQ +120903-0681636 America/Kralendijk BR -0351-03225 America/Noronha Atlantic islands -BR -0127-04829 America/Belem Para (east); Amapa +BR -0127-04829 America/Belem Para (east), Amapa BR -0343-03830 America/Fortaleza Brazil (northeast: MA, PI, CE, RN, PB) BR -0803-03454 America/Recife Pernambuco BR -0712-04812 America/Araguaina Tocantins @@ -107,21 +107,21 @@ BT +2728+08939 Asia/Thimphu BW -2439+02555 Africa/Gaborone BY +5354+02734 Europe/Minsk BZ +1730-08812 America/Belize -CA +4734-05243 America/St_Johns Newfoundland; Labrador (southeast) -CA +4439-06336 America/Halifax Atlantic - NS (most areas); PE +CA +4734-05243 America/St_Johns Newfoundland, Labrador (SE) +CA +4439-06336 America/Halifax Atlantic - NS (most areas), PE CA +4612-05957 America/Glace_Bay Atlantic - NS (Cape Breton) CA +4606-06447 America/Moncton Atlantic - New Brunswick CA +5320-06025 America/Goose_Bay Atlantic - Labrador (most areas) CA +5125-05707 America/Blanc-Sablon AST - QC (Lower North Shore) -CA +4339-07923 America/Toronto Eastern - ON, QC (most areas) +CA +4339-07923 America/Toronto Eastern - ON & QC (most areas) CA +6344-06828 America/Iqaluit Eastern - NU (most areas) -CA +484531-0913718 America/Atikokan EST - ON (Atikokan); NU (Coral H) -CA +4953-09709 America/Winnipeg Central - ON (west); Manitoba +CA +484531-0913718 America/Atikokan EST - ON (Atikokan), NU (Coral H) +CA +4953-09709 America/Winnipeg Central - ON (west), Manitoba CA +744144-0944945 America/Resolute Central - NU (Resolute) CA +624900-0920459 America/Rankin_Inlet Central - NU (central) CA +5024-10439 America/Regina CST - SK (most areas) CA +5017-10750 America/Swift_Current CST - SK (midwest) -CA +5333-11328 America/Edmonton Mountain - AB; BC (E); NT (E); SK (W) +CA +5333-11328 America/Edmonton Mountain - AB, BC(E), NT(E), SK(W) CA +690650-1050310 America/Cambridge_Bay Mountain - NU (west) CA +682059-1334300 America/Inuvik Mountain - NT (west) CA +4906-11631 America/Creston MST - BC (Creston) @@ -207,8 +207,8 @@ HT +1832-07220 America/Port-au-Prince HU +4730+01905 Europe/Budapest ID -0610+10648 Asia/Jakarta Java, Sumatra ID -0002+10920 Asia/Pontianak Borneo (west, central) -ID -0507+11924 Asia/Makassar Borneo (east, south); Sulawesi/Celebes, Bali, Nusa Tengarra; Timor (west) -ID -0232+14042 Asia/Jayapura New Guinea (West Papua / Irian Jaya); Malukus/Moluccas +ID -0507+11924 Asia/Makassar Borneo (east, south), Sulawesi/Celebes, Bali, Nusa Tengarra, Timor (west) +ID -0232+14042 Asia/Jayapura New Guinea (West Papua / Irian Jaya), Malukus/Moluccas IE +5320-00615 Europe/Dublin IL +314650+0351326 Asia/Jerusalem IM +5409-00428 Europe/Isle_of_Man @@ -355,7 +355,7 @@ RU +4310+13156 Asia/Vladivostok MSK+07 - Amur River RU +643337+1431336 Asia/Ust-Nera MSK+07 - Oymyakonsky RU +5934+15048 Asia/Magadan MSK+08 - Magadan RU +4658+14242 Asia/Sakhalin MSK+08 - Sakhalin Island -RU +6728+15343 Asia/Srednekolymsk MSK+08 - Sakha (E); N Kuril Is +RU +6728+15343 Asia/Srednekolymsk MSK+08 - Sakha (E), N Kuril Is RU +5301+15839 Asia/Kamchatka MSK+09 - Kamchatka RU +6445+17729 Asia/Anadyr MSK+09 - Bering Sea RW -0157+03004 Africa/Kigali @@ -418,7 +418,7 @@ US +470659-1011757 America/North_Dakota/Center Central - ND (Oliver) US +465042-1012439 America/North_Dakota/New_Salem Central - ND (Morton rural) US +471551-1014640 America/North_Dakota/Beulah Central - ND (Mercer) US +394421-1045903 America/Denver Mountain (most areas) -US +433649-1161209 America/Boise Mountain - ID (south); OR (east) +US +433649-1161209 America/Boise Mountain - ID (south), OR (east) US +332654-1120424 America/Phoenix MST - AZ (except Navajo) US +340308-1181434 America/Los_Angeles Pacific US +611305-1495401 America/Anchorage Alaska (most areas) diff --git a/lib/pytz/zoneinfo/zone1970.tab b/lib/pytz/zoneinfo/zone1970.tab index 1f1cecb8..abd94897 100644 --- a/lib/pytz/zoneinfo/zone1970.tab +++ b/lib/pytz/zoneinfo/zone1970.tab @@ -37,7 +37,7 @@ #country- #codes coordinates TZ comments AD +4230+00131 Europe/Andorra -AE,OM,RE,SC,TF +2518+05518 Asia/Dubai Crozet, Scattered Is +AE,OM,RE,SC,TF +2518+05518 Asia/Dubai Crozet AF +3431+06912 Asia/Kabul AL +4120+01950 Europe/Tirane AM +4011+04430 Asia/Yerevan @@ -47,12 +47,13 @@ AQ -6736+06253 Antarctica/Mawson Mawson AQ -6448-06406 Antarctica/Palmer Palmer AQ -6734-06808 Antarctica/Rothera Rothera AQ -720041+0023206 Antarctica/Troll Troll +AQ -7824+10654 Antarctica/Vostok Vostok AR -3436-05827 America/Argentina/Buenos_Aires Buenos Aires (BA, CF) AR -3124-06411 America/Argentina/Cordoba most areas: CB, CC, CN, ER, FM, MN, SE, SF AR -2447-06525 America/Argentina/Salta Salta (SA, LP, NQ, RN) AR -2411-06518 America/Argentina/Jujuy Jujuy (JY) AR -2649-06513 America/Argentina/Tucuman Tucumán (TM) -AR -2828-06547 America/Argentina/Catamarca Catamarca (CT); Chubut (CH) +AR -2828-06547 America/Argentina/Catamarca Catamarca (CT), Chubut (CH) AR -2926-06651 America/Argentina/La_Rioja La Rioja (LR) AR -3132-06831 America/Argentina/San_Juan San Juan (SJ) AR -3253-06849 America/Argentina/Mendoza Mendoza (MZ) @@ -81,7 +82,7 @@ BG +4241+02319 Europe/Sofia BM +3217-06446 Atlantic/Bermuda BO -1630-06809 America/La_Paz BR -0351-03225 America/Noronha Atlantic islands -BR -0127-04829 America/Belem Pará (east); Amapá +BR -0127-04829 America/Belem Pará (east), Amapá BR -0343-03830 America/Fortaleza Brazil (northeast: MA, PI, CE, RN, PB) BR -0803-03454 America/Recife Pernambuco BR -0712-04812 America/Araguaina Tocantins @@ -99,19 +100,19 @@ BR -0958-06748 America/Rio_Branco Acre BT +2728+08939 Asia/Thimphu BY +5354+02734 Europe/Minsk BZ +1730-08812 America/Belize -CA +4734-05243 America/St_Johns Newfoundland; Labrador (southeast) -CA +4439-06336 America/Halifax Atlantic - NS (most areas); PE +CA +4734-05243 America/St_Johns Newfoundland, Labrador (SE) +CA +4439-06336 America/Halifax Atlantic - NS (most areas), PE CA +4612-05957 America/Glace_Bay Atlantic - NS (Cape Breton) CA +4606-06447 America/Moncton Atlantic - New Brunswick CA +5320-06025 America/Goose_Bay Atlantic - Labrador (most areas) -CA,BS +4339-07923 America/Toronto Eastern - ON, QC (most areas) +CA,BS +4339-07923 America/Toronto Eastern - ON & QC (most areas) CA +6344-06828 America/Iqaluit Eastern - NU (most areas) -CA +4953-09709 America/Winnipeg Central - ON (west); Manitoba +CA +4953-09709 America/Winnipeg Central - ON (west), Manitoba CA +744144-0944945 America/Resolute Central - NU (Resolute) CA +624900-0920459 America/Rankin_Inlet Central - NU (central) CA +5024-10439 America/Regina CST - SK (most areas) CA +5017-10750 America/Swift_Current CST - SK (midwest) -CA +5333-11328 America/Edmonton Mountain - AB; BC (E); NT (E); SK (W) +CA +5333-11328 America/Edmonton Mountain - AB, BC(E), NT(E), SK(W) CA +690650-1050310 America/Cambridge_Bay Mountain - NU (west) CA +682059-1334300 America/Inuvik Mountain - NT (west) CA +5546-12014 America/Dawson_Creek MST - BC (Dawson Cr, Ft St John) @@ -126,7 +127,7 @@ CL -3327-07040 America/Santiago most of Chile CL -5309-07055 America/Punta_Arenas Region of Magallanes CL -2709-10926 Pacific/Easter Easter Island CN +3114+12128 Asia/Shanghai Beijing Time -CN,AQ +4348+08735 Asia/Urumqi Xinjiang Time, Vostok +CN +4348+08735 Asia/Urumqi Xinjiang Time CO +0436-07405 America/Bogota CR +0956-08405 America/Costa_Rica CU +2308-08222 America/Havana @@ -171,8 +172,8 @@ HT +1832-07220 America/Port-au-Prince HU +4730+01905 Europe/Budapest ID -0610+10648 Asia/Jakarta Java, Sumatra ID -0002+10920 Asia/Pontianak Borneo (west, central) -ID -0507+11924 Asia/Makassar Borneo (east, south); Sulawesi/Celebes, Bali, Nusa Tengarra; Timor (west) -ID -0232+14042 Asia/Jayapura New Guinea (West Papua / Irian Jaya); Malukus/Moluccas +ID -0507+11924 Asia/Makassar Borneo (east, south), Sulawesi/Celebes, Bali, Nusa Tengarra, Timor (west) +ID -0232+14042 Asia/Jayapura New Guinea (West Papua / Irian Jaya), Malukus/Moluccas IE +5320-00615 Europe/Dublin IL +314650+0351326 Asia/Jerusalem IN +2232+08822 Asia/Kolkata @@ -251,7 +252,7 @@ PK +2452+06703 Asia/Karachi PL +5215+02100 Europe/Warsaw PM +4703-05620 America/Miquelon PN -2504-13005 Pacific/Pitcairn -PR,AG,CA,AI,AW,BL,BQ,CW,DM,GD,GP,KN,LC,MF,MS,SX,TT,VC,VG,VI +182806-0660622 America/Puerto_Rico AST +PR,AG,CA,AI,AW,BL,BQ,CW,DM,GD,GP,KN,LC,MF,MS,SX,TT,VC,VG,VI +182806-0660622 America/Puerto_Rico AST - QC (Lower North Shore) PS +3130+03428 Asia/Gaza Gaza Strip PS +313200+0350542 Asia/Hebron West Bank PT +3843-00908 Europe/Lisbon Portugal (mainland) @@ -287,7 +288,7 @@ RU +4310+13156 Asia/Vladivostok MSK+07 - Amur River RU +643337+1431336 Asia/Ust-Nera MSK+07 - Oymyakonsky RU +5934+15048 Asia/Magadan MSK+08 - Magadan RU +4658+14242 Asia/Sakhalin MSK+08 - Sakhalin Island -RU +6728+15343 Asia/Srednekolymsk MSK+08 - Sakha (E); N Kuril Is +RU +6728+15343 Asia/Srednekolymsk MSK+08 - Sakha (E), N Kuril Is RU +5301+15839 Asia/Kamchatka MSK+09 - Kamchatka RU +6445+17729 Asia/Anadyr MSK+09 - Bering Sea SA,AQ,KW,YE +2438+04643 Asia/Riyadh Syowa @@ -329,7 +330,7 @@ US +470659-1011757 America/North_Dakota/Center Central - ND (Oliver) US +465042-1012439 America/North_Dakota/New_Salem Central - ND (Morton rural) US +471551-1014640 America/North_Dakota/Beulah Central - ND (Mercer) US +394421-1045903 America/Denver Mountain (most areas) -US +433649-1161209 America/Boise Mountain - ID (south); OR (east) +US +433649-1161209 America/Boise Mountain - ID (south), OR (east) US,CA +332654-1120424 America/Phoenix MST - AZ (most areas), Creston BC US +340308-1181434 America/Los_Angeles Pacific US +611305-1495401 America/Anchorage Alaska (most areas) From c8c6ac83e1cdeea92fc368b16b237afd4f6b47f0 Mon Sep 17 00:00:00 2001 From: JackDandy Date: Fri, 7 Jun 2024 16:43:38 +0100 Subject: [PATCH 16/29] =?UTF-8?q?Update=20Rarfile=204.1a1=20(8a72967)=20?= =?UTF-8?q?=E2=86=92=204.2=20(db1df33).?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGES.md | 1 + lib/rarfile/rarfile.py | 59 ++++++++++++++++++++++++++++++++---------- 2 files changed, 47 insertions(+), 13 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 06091de0..ae36ee8d 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -12,6 +12,7 @@ * Update imdbpie 5.6.4 (f695e87) to 5.6.5 (f8ed7a0) * Update profilehooks module 1.12.1 (c3fc078) to 1.13.0.dev0 (99f8a31) * Update pytz 2023.3/2023c (488d3eb) to 2024.1/2024a (3680953) +* Update Rarfile 4.1a1 (8a72967) to 4.2 (db1df33) * Update Requests library 2.31.0 (8812812) to 2.32.3 (0e322af) * Update Tornado Web Server 6.4 (b3f2a4b) to 6.4.1 (2a0e1d1) * Update urllib3 2.0.7 (56f01e0) to 2.2.1 (54d6edf) diff --git a/lib/rarfile/rarfile.py b/lib/rarfile/rarfile.py index 37c64992..fc95f7fd 100644 --- a/lib/rarfile/rarfile.py +++ b/lib/rarfile/rarfile.py @@ -1,6 +1,6 @@ # rarfile.py # -# Copyright (c) 2005-2020 Marko Kreen +# Copyright (c) 2005-2024 Marko Kreen # # Permission to use, copy, modify, and/or distribute this software for any # purpose with or without fee is hereby granted, provided that the above @@ -92,7 +92,7 @@ class AES_CBC_Decrypt: self.decrypt = ciph.decryptor().update -__version__ = "4.1" +__version__ = "4.2" # export only interesting items __all__ = ["get_rar_version", "is_rarfile", "is_rarfile_sfx", "RarInfo", "RarFile", "RarExtFile"] @@ -210,6 +210,9 @@ RAR_M3 = 0x33 #: Compression level `-m3`. RAR_M4 = 0x34 #: Compression level `-m4`. RAR_M5 = 0x35 #: Compression level `-m5` - Maximum compression. +RAR_MAX_PASSWORD = 127 #: Max number of utf-16 chars in passwords. +RAR_MAX_KDF_SHIFT = 24 #: Max power-of-2 for KDF count + # # RAR5 constants # @@ -739,6 +742,13 @@ class RarFile: """ return self._file_parser.needs_password() + def is_solid(self): + """Returns True if archive uses solid compression. + + .. versionadded:: 4.2 + """ + return self._file_parser.is_solid() + def namelist(self): """Return list of filenames in archive. """ @@ -1030,6 +1040,14 @@ class CommonParser: self._sfx_offset = sfx_offset self._part_only = part_only + def is_solid(self): + """Returns True if archive uses solid compression. + """ + if self._main: + if self._main.flags & RAR_MAIN_SOLID: + return True + return False + def has_header_encryption(self): """Returns True if headers are encrypted """ @@ -1167,7 +1185,9 @@ class CommonParser: if not self._password: break elif h.type == RAR_BLOCK_ENDARC: - more_vols = (h.flags & RAR_ENDARC_NEXT_VOLUME) > 0 + # use flag, but also allow RAR 2.x logic below to trigger + if h.flags & RAR_ENDARC_NEXT_VOLUME: + more_vols = True endarc = True if raise_need_first_vol and (h.flags & RAR_ENDARC_VOLNR) > 0: raise NeedFirstVolume( @@ -1810,10 +1830,10 @@ class RAR5Parser(CommonParser): def _gen_key(self, kdf_count, salt): if self._last_aes256_key[:2] == (kdf_count, salt): return self._last_aes256_key[2] - if kdf_count > 24: + if kdf_count > RAR_MAX_KDF_SHIFT: raise BadRarFile("Too large kdf_count") pwd = self._get_utf8_password() - key = pbkdf2_hmac("sha256", pwd, salt, 1 << kdf_count) + key = rar5_s2k(pwd, salt, 1 << kdf_count) self._last_aes256_key = (kdf_count, salt, key) return key @@ -1978,6 +1998,8 @@ class RAR5Parser(CommonParser): def _check_password(self, check_value, kdf_count_shift, salt): if len(check_value) != RAR5_PW_CHECK_SIZE + RAR5_PW_SUM_SIZE: return + if kdf_count_shift > RAR_MAX_KDF_SHIFT: + raise BadRarFile("Too large kdf_count") hdr_check = check_value[:RAR5_PW_CHECK_SIZE] hdr_sum = check_value[RAR5_PW_CHECK_SIZE:] @@ -1987,7 +2009,7 @@ class RAR5Parser(CommonParser): kdf_count = (1 << kdf_count_shift) + 32 pwd = self._get_utf8_password() - pwd_hash = pbkdf2_hmac("sha256", pwd, salt, kdf_count) + pwd_hash = rar5_s2k(pwd, salt, kdf_count) pwd_check = bytearray(RAR5_PW_CHECK_SIZE) len_mask = RAR5_PW_CHECK_SIZE - 1 @@ -2341,8 +2363,8 @@ class RarExtFile(io.RawIOBase): """Seek in data. On uncompressed files, the seeking works by actual - seeks so it's fast. On compresses files its slow - - forward seeking happends by reading ahead, + seeks so it's fast. On compressed files its slow + - forward seeking happens by reading ahead, backwards by re-opening and decompressing from the start. """ @@ -3044,12 +3066,23 @@ def is_filelike(obj): return True +def rar5_s2k(pwd, salt, kdf_count): + """String-to-key hash for RAR5. + """ + if not isinstance(pwd, str): + pwd = pwd.decode("utf8") + wstr = pwd.encode("utf-16le")[:RAR_MAX_PASSWORD*2] + ustr = wstr.decode("utf-16le").encode("utf8") + return pbkdf2_hmac("sha256", ustr, salt, kdf_count) + + def rar3_s2k(pwd, salt): """String-to-key hash for RAR3. """ if not isinstance(pwd, str): pwd = pwd.decode("utf8") - seed = bytearray(pwd.encode("utf-16le") + salt) + wstr = pwd.encode("utf-16le")[:RAR_MAX_PASSWORD*2] + seed = bytearray(wstr + salt) h = Rar3Sha1(rarbug=True) iv = b"" for i in range(16): @@ -3114,7 +3147,7 @@ def rar3_decompress(vers, meth, data, declen=0, flags=0, crc=0, pwd=None, salt=N def sanitize_filename(fname, pathsep, is_win32): - """Simulate unrar sanitization. + """Make filename safe for write access. """ if is_win32: if len(fname) > 1 and fname[1] == ":": @@ -3186,12 +3219,12 @@ def parse_dos_time(stamp): class nsdatetime(datetime): """Datetime that carries nanoseconds. - Arithmetic not supported, will lose nanoseconds. + Arithmetic operations will lose nanoseconds. .. versionadded:: 4.0 """ __slots__ = ("nanosecond",) - nanosecond: int #: Number of nanoseconds, 0 <= nanosecond < 999999999 + nanosecond: int #: Number of nanoseconds, 0 <= nanosecond <= 999999999 def __new__(cls, year, month=None, day=None, hour=0, minute=0, second=0, microsecond=0, tzinfo=None, *, fold=0, nanosecond=0): @@ -3393,7 +3426,7 @@ class ToolSetup: UNRAR_CONFIG = { "open_cmd": ("UNRAR_TOOL", "p", "-inul"), - "check_cmd": ("UNRAR_TOOL", "-inul"), + "check_cmd": ("UNRAR_TOOL", "-inul", "-?"), "password": "-p", "no_password": ("-p-",), # map return code to exception class, codes from rar.txt From 5ec9926bd4a5af71a50fb833db3386b26cc5dc25 Mon Sep 17 00:00:00 2001 From: JackDandy Date: Fri, 7 Jun 2024 16:56:21 +0100 Subject: [PATCH 17/29] =?UTF-8?q?Update=20Send2Trash=201.5.0=20(66afce7)?= =?UTF-8?q?=20=E2=86=92=201.8.3=20(91d0698).?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGES.md | 1 + lib/send2trash/compat.py | 5 +++++ lib/send2trash/plat_other.py | 2 +- lib/send2trash/util.py | 6 +++++- 4 files changed, 12 insertions(+), 2 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index ae36ee8d..bbbdc9c9 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -14,6 +14,7 @@ * Update pytz 2023.3/2023c (488d3eb) to 2024.1/2024a (3680953) * Update Rarfile 4.1a1 (8a72967) to 4.2 (db1df33) * Update Requests library 2.31.0 (8812812) to 2.32.3 (0e322af) +* Update Send2Trash 1.5.0 (66afce7) to 1.8.3 (91d0698) * Update Tornado Web Server 6.4 (b3f2a4b) to 6.4.1 (2a0e1d1) * Update urllib3 2.0.7 (56f01e0) to 2.2.1 (54d6edf) diff --git a/lib/send2trash/compat.py b/lib/send2trash/compat.py index 9e9b5fb2..a3043a4e 100644 --- a/lib/send2trash/compat.py +++ b/lib/send2trash/compat.py @@ -18,3 +18,8 @@ else: text_type = unicode # noqa: F821 binary_type = str environb = os.environ + +try: + from collections.abc import Iterable as iterable_type +except ImportError: + from collections import Iterable as iterable_type # noqa: F401 diff --git a/lib/send2trash/plat_other.py b/lib/send2trash/plat_other.py index 517e2a05..ace7b130 100644 --- a/lib/send2trash/plat_other.py +++ b/lib/send2trash/plat_other.py @@ -115,7 +115,7 @@ def trash_move(src, dst, topdir=None, cross_dev=False): f.write(info_for(src, topdir)) destpath = op.join(filespath, destname) if cross_dev: - shutil.move(src, destpath) + shutil.move(fsdecode(src), fsdecode(destpath)) else: os.rename(src, destpath) diff --git a/lib/send2trash/util.py b/lib/send2trash/util.py index 2c73d443..108537e1 100644 --- a/lib/send2trash/util.py +++ b/lib/send2trash/util.py @@ -5,9 +5,13 @@ # which should be included with this package. The terms are also available at # http://www.hardcoded.net/licenses/bsd_license +from send2trash.compat import text_type, binary_type, iterable_type + def preprocess_paths(paths): - if not isinstance(paths, list): + if isinstance(paths, iterable_type) and not isinstance(paths, (text_type, binary_type)): + paths = list(paths) + elif not isinstance(paths, list): paths = [paths] # Convert items such as pathlib paths to strings paths = [path.__fspath__() if hasattr(path, "__fspath__") else path for path in paths] From 4572ed367c4e5b721091eff3b804dbc4b33244a7 Mon Sep 17 00:00:00 2001 From: JackDandy Date: Fri, 7 Jun 2024 17:18:00 +0100 Subject: [PATCH 18/29] =?UTF-8?q?Update=20package=20resource=20API=2068.1.?= =?UTF-8?q?2=20(1ef36f2)=20=E2=86=92=2068.2.2=20(8ad627d).?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGES.md | 1 + lib/pkg_resources/__init__.py | 836 +++-- .../_vendor/backports/__init__.py | 0 .../_vendor/backports/tarfile.py | 2900 +++++++++++++++++ lib/pkg_resources/_vendor/jaraco/context.py | 137 +- .../{functools.py => functools/__init__.py} | 205 +- .../_vendor/jaraco/functools/__init__.pyi | 128 + .../_vendor/jaraco/functools/py.typed | 0 .../_vendor/more_itertools/__init__.py | 2 +- .../_vendor/more_itertools/more.py | 400 ++- .../_vendor/more_itertools/more.pyi | 41 +- .../_vendor/more_itertools/recipes.py | 230 +- .../_vendor/more_itertools/recipes.pyi | 29 +- .../_vendor/packaging/__init__.py | 4 +- .../_vendor/packaging/_manylinux.py | 74 +- .../_vendor/packaging/_musllinux.py | 19 +- .../_vendor/packaging/_parser.py | 13 +- .../_vendor/packaging/metadata.py | 439 ++- .../_vendor/packaging/requirements.py | 45 +- .../_vendor/packaging/specifiers.py | 63 +- lib/pkg_resources/_vendor/packaging/tags.py | 63 +- lib/pkg_resources/_vendor/packaging/utils.py | 39 +- .../_vendor/packaging/version.py | 63 +- lib/pkg_resources/_vendor/ruff.toml | 1 + .../_vendor/typing_extensions.py | 2209 ------------- lib/pkg_resources/_vendor/vendored.txt | 8 +- lib/pkg_resources/extern/__init__.py | 41 +- 27 files changed, 5014 insertions(+), 2976 deletions(-) create mode 100644 lib/pkg_resources/_vendor/backports/__init__.py create mode 100644 lib/pkg_resources/_vendor/backports/tarfile.py rename lib/pkg_resources/_vendor/jaraco/{functools.py => functools/__init__.py} (79%) create mode 100644 lib/pkg_resources/_vendor/jaraco/functools/__init__.pyi create mode 100644 lib/pkg_resources/_vendor/jaraco/functools/py.typed create mode 100644 lib/pkg_resources/_vendor/ruff.toml delete mode 100644 lib/pkg_resources/_vendor/typing_extensions.py diff --git a/CHANGES.md b/CHANGES.md index bbbdc9c9..1201794a 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -10,6 +10,7 @@ * Update filelock 3.12.4 (c1163ae) to 3.14.0 (8556141) * Update idna library 3.4 (cab054c) to 3.7 (1d365e1) * Update imdbpie 5.6.4 (f695e87) to 5.6.5 (f8ed7a0) +* Update package resource API 68.1.2 (1ef36f2) to 68.2.2 (8ad627d) * Update profilehooks module 1.12.1 (c3fc078) to 1.13.0.dev0 (99f8a31) * Update pytz 2023.3/2023c (488d3eb) to 2024.1/2024a (3680953) * Update Rarfile 4.1a1 (8a72967) to 4.2 (db1df33) diff --git a/lib/pkg_resources/__init__.py b/lib/pkg_resources/__init__.py index 3baa1f3c..c10a88e5 100644 --- a/lib/pkg_resources/__init__.py +++ b/lib/pkg_resources/__init__.py @@ -1,3 +1,6 @@ +# TODO: Add Generic type annotations to initialized collections. +# For now we'd simply use implicit Any/Unknown which would add redundant annotations +# mypy: disable-error-code="var-annotated" """ Package resource API -------------------- @@ -18,11 +21,35 @@ This module is deprecated. Users are directed to :mod:`importlib.resources`, """ import sys + +if sys.version_info < (3, 8): + raise RuntimeError("Python 3.8 or later is required") + import os import io import time import re import types +from typing import ( + Any, + Mapping, + MutableSequence, + NamedTuple, + NoReturn, + Sequence, + Set, + Tuple, + Type, + Union, + TYPE_CHECKING, + List, + Protocol, + Callable, + Dict, + Iterable, + Optional, + TypeVar, +) import zipfile import zipimport import warnings @@ -41,21 +68,16 @@ import inspect import ntpath import posixpath import importlib +import importlib.abc +import importlib.machinery from pkgutil import get_importer -try: - import _imp -except ImportError: - # Python 3.2 compatibility - import imp as _imp - -try: - FileExistsError -except NameError: - FileExistsError = OSError +import _imp # capture these to bypass sandboxing from os import utime +from os import open as os_open +from os.path import isdir, split try: from os import mkdir, rename, unlink @@ -65,55 +87,19 @@ except ImportError: # no write support, probably under GAE WRITE_SUPPORT = False -from os import open as os_open -from os.path import isdir, split - -try: - import importlib.machinery as importlib_machinery - - # access attribute to force import under delayed import mechanisms. - importlib_machinery.__name__ -except ImportError: - importlib_machinery = None - from pkg_resources.extern.jaraco.text import ( yield_lines, drop_comment, join_continuation, ) +from pkg_resources.extern.packaging import markers as _packaging_markers +from pkg_resources.extern.packaging import requirements as _packaging_requirements +from pkg_resources.extern.packaging import utils as _packaging_utils +from pkg_resources.extern.packaging import version as _packaging_version +from pkg_resources.extern.platformdirs import user_cache_dir as _user_cache_dir -from pkg_resources.extern import platformdirs -from pkg_resources.extern import packaging - -__import__('pkg_resources.extern.packaging.version') -__import__('pkg_resources.extern.packaging.specifiers') -__import__('pkg_resources.extern.packaging.requirements') -__import__('pkg_resources.extern.packaging.markers') -__import__('pkg_resources.extern.packaging.utils') - -if sys.version_info < (3, 5): - raise RuntimeError("Python 3.5 or later is required") - -# declare some globals that will be defined later to -# satisfy the linters. -require = None -working_set = None -add_activation_listener = None -resources_stream = None -cleanup_resources = None -resource_dir = None -resource_stream = None -set_extraction_path = None -resource_isdir = None -resource_string = None -iter_entry_points = None -resource_listdir = None -resource_filename = None -resource_exists = None -_distribution_finders = None -_namespace_handlers = None -_namespace_packages = None - +if TYPE_CHECKING: + from _typeshed import StrPath warnings.warn( "pkg_resources is deprecated as an API. " @@ -123,6 +109,28 @@ warnings.warn( ) +_T = TypeVar("_T") +# Type aliases +_NestedStr = Union[str, Iterable[Union[str, Iterable["_NestedStr"]]]] +_InstallerType = Callable[["Requirement"], Optional["Distribution"]] +_PkgReqType = Union[str, "Requirement"] +_EPDistType = Union["Distribution", _PkgReqType] +_MetadataType = Optional["IResourceProvider"] +# Any object works, but let's indicate we expect something like a module (optionally has __loader__ or __file__) +_ModuleLike = Union[object, types.ModuleType] +_ProviderFactoryType = Callable[[_ModuleLike], "IResourceProvider"] +_DistFinderType = Callable[[_T, str, bool], Iterable["Distribution"]] +_NSHandlerType = Callable[[_T, str, str, types.ModuleType], Optional[str]] +_AdapterT = TypeVar( + "_AdapterT", _DistFinderType[Any], _ProviderFactoryType, _NSHandlerType[Any] +) + + +# Use _typeshed.importlib.LoaderProtocol once available https://github.com/python/typeshed/pull/11890 +class _LoaderProtocol(Protocol): + def load_module(self, fullname: str, /) -> types.ModuleType: ... + + _PEP440_FALLBACK = re.compile(r"^v?(?P(?:[0-9]+!)?[0-9]+(?:\.[0-9]+)*)", re.I) @@ -133,15 +141,15 @@ class PEP440Warning(RuntimeWarning): """ -parse_version = packaging.version.Version +parse_version = _packaging_version.Version -_state_vars = {} +_state_vars: Dict[str, str] = {} -def _declare_state(vartype, **kw): - globals().update(kw) - _state_vars.update(dict.fromkeys(kw, vartype)) +def _declare_state(vartype: str, varname: str, initial_value: _T) -> _T: + _state_vars[varname] = vartype + return initial_value def __getstate__(): @@ -317,7 +325,7 @@ class VersionConflict(ResolutionError): def report(self): return self._template.format(**locals()) - def with_context(self, required_by): + def with_context(self, required_by: Set[Union["Distribution", str]]): """ If required_by is non-empty, return a version of self that is a ContextualVersionConflict. @@ -374,7 +382,7 @@ class UnknownExtra(ResolutionError): """Distribution doesn't have an "extra feature" of the given name""" -_provider_factories = {} +_provider_factories: Dict[Type[_ModuleLike], _ProviderFactoryType] = {} PY_MAJOR = '{}.{}'.format(*sys.version_info) EGG_DIST = 3 @@ -384,7 +392,9 @@ CHECKOUT_DIST = 0 DEVELOP_DIST = -1 -def register_loader_type(loader_type, provider_factory): +def register_loader_type( + loader_type: Type[_ModuleLike], provider_factory: _ProviderFactoryType +): """Register `provider_factory` to make providers for `loader_type` `loader_type` is the type or class of a PEP 302 ``module.__loader__``, @@ -394,7 +404,7 @@ def register_loader_type(loader_type, provider_factory): _provider_factories[loader_type] = provider_factory -def get_provider(moduleOrReq): +def get_provider(moduleOrReq: Union[str, "Requirement"]): """Return an IResourceProvider for the named module or requirement""" if isinstance(moduleOrReq, Requirement): return working_set.find(moduleOrReq) or require(str(moduleOrReq))[0] @@ -407,20 +417,18 @@ def get_provider(moduleOrReq): return _find_adapter(_provider_factories, loader)(module) -def _macos_vers(_cache=[]): - if not _cache: - version = platform.mac_ver()[0] - # fallback for MacPorts - if version == '': - plist = '/System/Library/CoreServices/SystemVersion.plist' - if os.path.exists(plist): - if hasattr(plistlib, 'readPlist'): - plist_content = plistlib.readPlist(plist) - if 'ProductVersion' in plist_content: - version = plist_content['ProductVersion'] - - _cache.append(version.split('.')) - return _cache[0] +@functools.lru_cache(maxsize=None) +def _macos_vers(): + version = platform.mac_ver()[0] + # fallback for MacPorts + if version == '': + plist = '/System/Library/CoreServices/SystemVersion.plist' + if os.path.exists(plist): + with open(plist, 'rb') as fh: + plist_content = plistlib.load(fh) + if 'ProductVersion' in plist_content: + version = plist_content['ProductVersion'] + return version.split('.') def _macos_arch(machine): @@ -458,7 +466,7 @@ darwinVersionString = re.compile(r"darwin-(\d+)\.(\d+)\.(\d+)-(.*)") get_platform = get_build_platform -def compatible_platforms(provided, required): +def compatible_platforms(provided: Optional[str], required: Optional[str]): """Can code for the `provided` platform run on the `required` platform? Returns true if either platform is ``None``, or the platforms are equal. @@ -507,20 +515,7 @@ def compatible_platforms(provided, required): return False -def run_script(dist_spec, script_name): - """Locate distribution `dist_spec` and run its `script_name` script""" - ns = sys._getframe(1).f_globals - name = ns['__name__'] - ns.clear() - ns['__name__'] = name - require(dist_spec)[0].run_script(script_name, ns) - - -# backward compatibility -run_main = run_script - - -def get_distribution(dist): +def get_distribution(dist: _EPDistType): """Return a current distribution object for a Requirement or string""" if isinstance(dist, str): dist = Requirement.parse(dist) @@ -531,78 +526,80 @@ def get_distribution(dist): return dist -def load_entry_point(dist, group, name): +def load_entry_point(dist: _EPDistType, group: str, name: str): """Return `name` entry point of `group` for `dist` or raise ImportError""" return get_distribution(dist).load_entry_point(group, name) -def get_entry_map(dist, group=None): +def get_entry_map(dist: _EPDistType, group: Optional[str] = None): """Return the entry point map for `group`, or the full entry map""" return get_distribution(dist).get_entry_map(group) -def get_entry_info(dist, group, name): +def get_entry_info(dist: _EPDistType, group: str, name: str): """Return the EntryPoint object for `group`+`name`, or ``None``""" return get_distribution(dist).get_entry_info(group, name) -class IMetadataProvider: - def has_metadata(name): +class IMetadataProvider(Protocol): + def has_metadata(self, name: str) -> bool: """Does the package's distribution contain the named metadata?""" - def get_metadata(name): + def get_metadata(self, name: str): """The named metadata resource as a string""" - def get_metadata_lines(name): + def get_metadata_lines(self, name: str): """Yield named metadata resource as list of non-blank non-comment lines Leading and trailing whitespace is stripped from each line, and lines with ``#`` as the first non-blank character are omitted.""" - def metadata_isdir(name): + def metadata_isdir(self, name: str) -> bool: """Is the named metadata a directory? (like ``os.path.isdir()``)""" - def metadata_listdir(name): + def metadata_listdir(self, name: str): """List of metadata names in the directory (like ``os.listdir()``)""" - def run_script(script_name, namespace): + def run_script(self, script_name: str, namespace: Dict[str, Any]): """Execute the named script in the supplied namespace dictionary""" -class IResourceProvider(IMetadataProvider): +class IResourceProvider(IMetadataProvider, Protocol): """An object that provides access to package resources""" - def get_resource_filename(manager, resource_name): + def get_resource_filename(self, manager: "ResourceManager", resource_name: str): """Return a true filesystem path for `resource_name` - `manager` must be an ``IResourceManager``""" + `manager` must be a ``ResourceManager``""" - def get_resource_stream(manager, resource_name): + def get_resource_stream(self, manager: "ResourceManager", resource_name: str): """Return a readable file-like object for `resource_name` - `manager` must be an ``IResourceManager``""" + `manager` must be a ``ResourceManager``""" - def get_resource_string(manager, resource_name): - """Return a string containing the contents of `resource_name` + def get_resource_string( + self, manager: "ResourceManager", resource_name: str + ) -> bytes: + """Return the contents of `resource_name` as :obj:`bytes` - `manager` must be an ``IResourceManager``""" + `manager` must be a ``ResourceManager``""" - def has_resource(resource_name): + def has_resource(self, resource_name: str): """Does the package contain the named resource?""" - def resource_isdir(resource_name): + def resource_isdir(self, resource_name: str): """Is the named resource a directory? (like ``os.path.isdir()``)""" - def resource_listdir(resource_name): + def resource_listdir(self, resource_name: str): """List of resource names in the directory (like ``os.listdir()``)""" class WorkingSet: """A collection of active distributions on sys.path (or a similar list)""" - def __init__(self, entries=None): + def __init__(self, entries: Optional[Iterable[str]] = None): """Create working set from list of path entries (default=sys.path)""" - self.entries = [] + self.entries: List[str] = [] self.entry_keys = {} self.by_key = {} self.normalized_to_canonical_keys = {} @@ -656,7 +653,7 @@ class WorkingSet: sys.path[:] = ws.entries return ws - def add_entry(self, entry): + def add_entry(self, entry: str): """Add a path item to ``.entries``, finding any distributions on it ``find_distributions(entry, True)`` is used to find distributions @@ -671,11 +668,11 @@ class WorkingSet: for dist in find_distributions(entry, True): self.add(dist, entry, False) - def __contains__(self, dist): + def __contains__(self, dist: "Distribution"): """True if `dist` is the active distribution for its project""" return self.by_key.get(dist.key) == dist - def find(self, req): + def find(self, req: "Requirement"): """Find a distribution matching requirement `req` If there is an active distribution for the requested project, this @@ -699,7 +696,7 @@ class WorkingSet: raise VersionConflict(dist, req) return dist - def iter_entry_points(self, group, name=None): + def iter_entry_points(self, group: str, name: Optional[str] = None): """Yield entry point objects from `group` matching `name` If `name` is None, yields all entry points in `group` from all @@ -713,7 +710,7 @@ class WorkingSet: if name is None or name == entry.name ) - def run_script(self, requires, script_name): + def run_script(self, requires: str, script_name: str): """Locate distribution for `requires` and run `script_name` script""" ns = sys._getframe(1).f_globals name = ns['__name__'] @@ -738,7 +735,13 @@ class WorkingSet: seen[key] = 1 yield self.by_key[key] - def add(self, dist, entry=None, insert=True, replace=False): + def add( + self, + dist: "Distribution", + entry: Optional[str] = None, + insert: bool = True, + replace: bool = False, + ): """Add `dist` to working set, associated with `entry` If `entry` is unspecified, it defaults to the ``.location`` of `dist`. @@ -762,7 +765,7 @@ class WorkingSet: return self.by_key[dist.key] = dist - normalized_name = packaging.utils.canonicalize_name(dist.key) + normalized_name = _packaging_utils.canonicalize_name(dist.key) self.normalized_to_canonical_keys[normalized_name] = dist.key if dist.key not in keys: keys.append(dist.key) @@ -772,11 +775,11 @@ class WorkingSet: def resolve( self, - requirements, - env=None, - installer=None, - replace_conflicting=False, - extras=None, + requirements: Iterable["Requirement"], + env: Optional["Environment"] = None, + installer: Optional[_InstallerType] = None, + replace_conflicting: bool = False, + extras: Optional[Tuple[str, ...]] = None, ): """List all distributions needed to (recursively) meet `requirements` @@ -846,7 +849,7 @@ class WorkingSet: def _resolve_dist( self, req, best, replace_conflicting, env, installer, required_by, to_activate - ): + ) -> "Distribution": dist = best.get(req.key) if dist is None: # Find the best distribution and add it to the map @@ -875,7 +878,13 @@ class WorkingSet: raise VersionConflict(dist, req).with_context(dependent_req) return dist - def find_plugins(self, plugin_env, full_env=None, installer=None, fallback=True): + def find_plugins( + self, + plugin_env: "Environment", + full_env: Optional["Environment"] = None, + installer: Optional[_InstallerType] = None, + fallback: bool = True, + ): """Find all activatable distributions in `plugin_env` Example usage:: @@ -951,12 +960,12 @@ class WorkingSet: # success, no need to try any more versions of this project break - distributions = list(distributions) - distributions.sort() + sorted_distributions = list(distributions) + sorted_distributions.sort() - return distributions, error_info + return sorted_distributions, error_info - def require(self, *requirements): + def require(self, *requirements: _NestedStr): """Ensure that distributions matching `requirements` are activated `requirements` must be a string or a (possibly-nested) sequence @@ -972,7 +981,9 @@ class WorkingSet: return needed - def subscribe(self, callback, existing=True): + def subscribe( + self, callback: Callable[["Distribution"], object], existing: bool = True + ): """Invoke `callback` for all distributions If `existing=True` (default), @@ -1008,12 +1019,14 @@ class WorkingSet: self.callbacks = callbacks[:] -class _ReqExtras(dict): +class _ReqExtras(Dict["Requirement", Tuple[str, ...]]): """ Map each requirement to the extras that demanded it. """ - def markers_pass(self, req, extras=None): + def markers_pass( + self, req: "Requirement", extras: Optional[Tuple[str, ...]] = None + ): """ Evaluate markers for req against each extra that demanded it. @@ -1032,7 +1045,10 @@ class Environment: """Searchable snapshot of distributions on a search path""" def __init__( - self, search_path=None, platform=get_supported_platform(), python=PY_MAJOR + self, + search_path: Optional[Sequence[str]] = None, + platform: Optional[str] = get_supported_platform(), + python: Optional[str] = PY_MAJOR, ): """Snapshot distributions available on a search path @@ -1055,7 +1071,7 @@ class Environment: self.python = python self.scan(search_path) - def can_add(self, dist): + def can_add(self, dist: "Distribution"): """Is distribution `dist` acceptable for this environment? The distribution must match the platform and python version @@ -1069,11 +1085,11 @@ class Environment: ) return py_compat and compatible_platforms(dist.platform, self.platform) - def remove(self, dist): + def remove(self, dist: "Distribution"): """Remove `dist` from the environment""" self._distmap[dist.key].remove(dist) - def scan(self, search_path=None): + def scan(self, search_path: Optional[Sequence[str]] = None): """Scan `search_path` for distributions usable in this environment Any distributions found are added to the environment. @@ -1088,7 +1104,7 @@ class Environment: for dist in find_distributions(item): self.add(dist) - def __getitem__(self, project_name): + def __getitem__(self, project_name: str): """Return a newest-to-oldest list of distributions for `project_name` Uses case-insensitive `project_name` comparison, assuming all the @@ -1099,7 +1115,7 @@ class Environment: distribution_key = project_name.lower() return self._distmap.get(distribution_key, []) - def add(self, dist): + def add(self, dist: "Distribution"): """Add `dist` if we ``can_add()`` it and it has not already been added""" if self.can_add(dist) and dist.has_version(): dists = self._distmap.setdefault(dist.key, []) @@ -1107,7 +1123,13 @@ class Environment: dists.append(dist) dists.sort(key=operator.attrgetter('hashcmp'), reverse=True) - def best_match(self, req, working_set, installer=None, replace_conflicting=False): + def best_match( + self, + req: "Requirement", + working_set: WorkingSet, + installer: Optional[Callable[["Requirement"], Any]] = None, + replace_conflicting: bool = False, + ): """Find distribution best matching `req` and usable on `working_set` This calls the ``find(req)`` method of the `working_set` to see if a @@ -1134,7 +1156,11 @@ class Environment: # try to download/install return self.obtain(req, installer) - def obtain(self, requirement, installer=None): + def obtain( + self, + requirement: "Requirement", + installer: Optional[Callable[["Requirement"], Any]] = None, + ): """Obtain a distribution matching `requirement` (e.g. via download) Obtain a distro that matches requirement (e.g. via download). In the @@ -1143,8 +1169,7 @@ class Environment: None is returned instead. This method is a hook that allows subclasses to attempt other ways of obtaining a distribution before falling back to the `installer` argument.""" - if installer is not None: - return installer(requirement) + return installer(requirement) if installer else None def __iter__(self): """Yield the unique project names of the available distributions""" @@ -1152,7 +1177,7 @@ class Environment: if self[key]: yield key - def __iadd__(self, other): + def __iadd__(self, other: Union["Distribution", "Environment"]): """In-place addition of a distribution or environment""" if isinstance(other, Distribution): self.add(other) @@ -1164,7 +1189,7 @@ class Environment: raise TypeError("Can't add %r to environment" % (other,)) return self - def __add__(self, other): + def __add__(self, other: Union["Distribution", "Environment"]): """Add an environment or distribution to an environment""" new = self.__class__([], platform=None, python=None) for env in self, other: @@ -1191,46 +1216,54 @@ class ExtractionError(RuntimeError): The exception instance that caused extraction to fail """ + manager: "ResourceManager" + cache_path: str + original_error: Optional[BaseException] + class ResourceManager: """Manage resource extraction and packages""" - extraction_path = None + extraction_path: Optional[str] = None def __init__(self): self.cached_files = {} - def resource_exists(self, package_or_requirement, resource_name): + def resource_exists(self, package_or_requirement: _PkgReqType, resource_name: str): """Does the named resource exist?""" return get_provider(package_or_requirement).has_resource(resource_name) - def resource_isdir(self, package_or_requirement, resource_name): + def resource_isdir(self, package_or_requirement: _PkgReqType, resource_name: str): """Is the named resource an existing directory?""" return get_provider(package_or_requirement).resource_isdir(resource_name) - def resource_filename(self, package_or_requirement, resource_name): + def resource_filename( + self, package_or_requirement: _PkgReqType, resource_name: str + ): """Return a true filesystem path for specified resource""" return get_provider(package_or_requirement).get_resource_filename( self, resource_name ) - def resource_stream(self, package_or_requirement, resource_name): + def resource_stream(self, package_or_requirement: _PkgReqType, resource_name: str): """Return a readable file-like object for specified resource""" return get_provider(package_or_requirement).get_resource_stream( self, resource_name ) - def resource_string(self, package_or_requirement, resource_name): - """Return specified resource as a string""" + def resource_string( + self, package_or_requirement: _PkgReqType, resource_name: str + ) -> bytes: + """Return specified resource as :obj:`bytes`""" return get_provider(package_or_requirement).get_resource_string( self, resource_name ) - def resource_listdir(self, package_or_requirement, resource_name): + def resource_listdir(self, package_or_requirement: _PkgReqType, resource_name: str): """List the contents of the named resource directory""" return get_provider(package_or_requirement).resource_listdir(resource_name) - def extraction_error(self): + def extraction_error(self) -> NoReturn: """Give an error message for problems extracting file(s)""" old_exc = sys.exc_info()[1] @@ -1260,7 +1293,7 @@ class ResourceManager: err.original_error = old_exc raise err - def get_cache_path(self, archive_name, names=()): + def get_cache_path(self, archive_name: str, names: Iterable[str] = ()): """Return absolute location in cache for `archive_name` and `names` The parent directory of the resulting path will be created if it does @@ -1312,7 +1345,7 @@ class ResourceManager: ).format(**locals()) warnings.warn(msg, UserWarning) - def postprocess(self, tempname, filename): + def postprocess(self, tempname: str, filename: str): """Perform any platform-specific postprocessing of `tempname` This is where Mac header rewrites should be done; other platforms don't @@ -1332,7 +1365,7 @@ class ResourceManager: mode = ((os.stat(tempname).st_mode) | 0o555) & 0o7777 os.chmod(tempname, mode) - def set_extraction_path(self, path): + def set_extraction_path(self, path: str): """Set the base path where resources will be extracted to, if needed. If you do not call this routine before any extractions take place, the @@ -1356,7 +1389,7 @@ class ResourceManager: self.extraction_path = path - def cleanup_resources(self, force=False): + def cleanup_resources(self, force: bool = False) -> List[str]: """ Delete all extracted resource files and directories, returning a list of the file and directory names that could not be successfully removed. @@ -1368,6 +1401,7 @@ class ResourceManager: directory used for extractions. """ # XXX + return [] def get_default_cache(): @@ -1376,12 +1410,10 @@ def get_default_cache(): or a platform-relevant user cache dir for an app named "Python-Eggs". """ - return os.environ.get('PYTHON_EGG_CACHE') or platformdirs.user_cache_dir( - appname='Python-Eggs' - ) + return os.environ.get('PYTHON_EGG_CACHE') or _user_cache_dir(appname='Python-Eggs') -def safe_name(name): +def safe_name(name: str): """Convert an arbitrary string to a standard distribution name Any runs of non-alphanumeric/. characters are replaced with a single '-'. @@ -1389,14 +1421,14 @@ def safe_name(name): return re.sub('[^A-Za-z0-9.]+', '-', name) -def safe_version(version): +def safe_version(version: str): """ Convert an arbitrary string to a standard version string """ try: # normalize the version - return str(packaging.version.Version(version)) - except packaging.version.InvalidVersion: + return str(_packaging_version.Version(version)) + except _packaging_version.InvalidVersion: version = version.replace(' ', '.') return re.sub('[^A-Za-z0-9.]+', '-', version) @@ -1433,7 +1465,7 @@ def _safe_segment(segment): return re.sub(r'\.[^A-Za-z0-9]+', '.', segment).strip(".-") -def safe_extra(extra): +def safe_extra(extra: str): """Convert an arbitrary string to a standard 'extra' name Any runs of non-alphanumeric characters are replaced with a single '_', @@ -1442,7 +1474,7 @@ def safe_extra(extra): return re.sub('[^A-Za-z0-9.-]+', '_', extra).lower() -def to_filename(name): +def to_filename(name: str): """Convert a project or version name to its filename-escaped form Any '-' characters are currently replaced with '_'. @@ -1450,7 +1482,7 @@ def to_filename(name): return name.replace('-', '_') -def invalid_marker(text): +def invalid_marker(text: str): """ Validate text as a PEP 508 environment marker; return an exception if invalid or False otherwise. @@ -1464,7 +1496,7 @@ def invalid_marker(text): return False -def evaluate_marker(text, extra=None): +def evaluate_marker(text: str, extra: Optional[str] = None): """ Evaluate a PEP 508 environment marker. Return a boolean indicating the marker result in this environment. @@ -1473,46 +1505,49 @@ def evaluate_marker(text, extra=None): This implementation uses the 'pyparsing' module. """ try: - marker = packaging.markers.Marker(text) + marker = _packaging_markers.Marker(text) return marker.evaluate() - except packaging.markers.InvalidMarker as e: + except _packaging_markers.InvalidMarker as e: raise SyntaxError(e) from e class NullProvider: """Try to implement resources and metadata for arbitrary PEP 302 loaders""" - egg_name = None - egg_info = None - loader = None + egg_name: Optional[str] = None + egg_info: Optional[str] = None + loader: Optional[_LoaderProtocol] = None + module_path: Optional[str] # Some subclasses can have a None module_path - def __init__(self, module): + def __init__(self, module: _ModuleLike): self.loader = getattr(module, '__loader__', None) self.module_path = os.path.dirname(getattr(module, '__file__', '')) - def get_resource_filename(self, manager, resource_name): + def get_resource_filename(self, manager: ResourceManager, resource_name: str): return self._fn(self.module_path, resource_name) - def get_resource_stream(self, manager, resource_name): + def get_resource_stream(self, manager: ResourceManager, resource_name: str): return io.BytesIO(self.get_resource_string(manager, resource_name)) - def get_resource_string(self, manager, resource_name): + def get_resource_string( + self, manager: ResourceManager, resource_name: str + ) -> bytes: return self._get(self._fn(self.module_path, resource_name)) - def has_resource(self, resource_name): + def has_resource(self, resource_name: str): return self._has(self._fn(self.module_path, resource_name)) def _get_metadata_path(self, name): return self._fn(self.egg_info, name) - def has_metadata(self, name): + def has_metadata(self, name: str) -> bool: if not self.egg_info: - return self.egg_info + return False path = self._get_metadata_path(name) return self._has(path) - def get_metadata(self, name): + def get_metadata(self, name: str): if not self.egg_info: return "" path = self._get_metadata_path(name) @@ -1525,24 +1560,24 @@ class NullProvider: exc.reason += ' in {} file at path: {}'.format(name, path) raise - def get_metadata_lines(self, name): + def get_metadata_lines(self, name: str): return yield_lines(self.get_metadata(name)) - def resource_isdir(self, resource_name): + def resource_isdir(self, resource_name: str): return self._isdir(self._fn(self.module_path, resource_name)) - def metadata_isdir(self, name): - return self.egg_info and self._isdir(self._fn(self.egg_info, name)) + def metadata_isdir(self, name: str) -> bool: + return bool(self.egg_info and self._isdir(self._fn(self.egg_info, name))) - def resource_listdir(self, resource_name): + def resource_listdir(self, resource_name: str): return self._listdir(self._fn(self.module_path, resource_name)) - def metadata_listdir(self, name): + def metadata_listdir(self, name: str): if self.egg_info: return self._listdir(self._fn(self.egg_info, name)) return [] - def run_script(self, script_name, namespace): + def run_script(self, script_name: str, namespace: Dict[str, Any]): script = 'scripts/' + script_name if not self.has_metadata(script): raise ResolutionError( @@ -1555,8 +1590,7 @@ class NullProvider: script_filename = self._fn(self.egg_info, script) namespace['__file__'] = script_filename if os.path.exists(script_filename): - with open(script_filename) as fid: - source = fid.read() + source = _read_utf8_with_fallback(script_filename) code = compile(source, script_filename, 'exec') exec(code, namespace, namespace) else: @@ -1571,12 +1605,12 @@ class NullProvider: script_code = compile(script_text, script_filename, 'exec') exec(script_code, namespace, namespace) - def _has(self, path): + def _has(self, path) -> bool: raise NotImplementedError( "Can't perform this operation for unregistered loader type" ) - def _isdir(self, path): + def _isdir(self, path) -> bool: raise NotImplementedError( "Can't perform this operation for unregistered loader type" ) @@ -1586,7 +1620,7 @@ class NullProvider: "Can't perform this operation for unregistered loader type" ) - def _fn(self, base, resource_name): + def _fn(self, base, resource_name: str): self._validate_resource_path(resource_name) if resource_name: return os.path.join(base, *resource_name.split('/')) @@ -1649,6 +1683,7 @@ is not allowed. os.path.pardir in path.split(posixpath.sep) or posixpath.isabs(path) or ntpath.isabs(path) + or path.startswith("\\") ) if not invalid: return @@ -1656,7 +1691,7 @@ is not allowed. msg = "Use of .. or absolute path in a resource path is not allowed." # Aggressively disallow Windows absolute paths - if ntpath.isabs(path) and not posixpath.isabs(path): + if (path.startswith("\\") or ntpath.isabs(path)) and not posixpath.isabs(path): raise ValueError(msg) # for compatibility, warn; in future @@ -1666,9 +1701,10 @@ is not allowed. DeprecationWarning, ) - def _get(self, path): - if hasattr(self.loader, 'get_data'): - return self.loader.get_data(path) + def _get(self, path) -> bytes: + if hasattr(self.loader, 'get_data') and self.loader: + # Already checked get_data exists + return self.loader.get_data(path) # type: ignore[attr-defined] raise NotImplementedError( "Can't perform this operation for loaders without 'get_data()'" ) @@ -1691,7 +1727,7 @@ def _parents(path): class EggProvider(NullProvider): """Provider based on a virtual filesystem""" - def __init__(self, module): + def __init__(self, module: _ModuleLike): super().__init__(module) self._setup_prefix() @@ -1702,7 +1738,7 @@ class EggProvider(NullProvider): egg = next(eggs, None) egg and self._set_egg(egg) - def _set_egg(self, path): + def _set_egg(self, path: str): self.egg_name = os.path.basename(path) self.egg_info = os.path.join(path, 'EGG-INFO') self.egg_root = path @@ -1711,19 +1747,19 @@ class EggProvider(NullProvider): class DefaultProvider(EggProvider): """Provides access to package resources in the filesystem""" - def _has(self, path): + def _has(self, path) -> bool: return os.path.exists(path) - def _isdir(self, path): + def _isdir(self, path) -> bool: return os.path.isdir(path) def _listdir(self, path): return os.listdir(path) - def get_resource_stream(self, manager, resource_name): + def get_resource_stream(self, manager: object, resource_name: str): return open(self._fn(self.module_path, resource_name), 'rb') - def _get(self, path): + def _get(self, path) -> bytes: with open(path, 'rb') as stream: return stream.read() @@ -1734,7 +1770,7 @@ class DefaultProvider(EggProvider): 'SourcelessFileLoader', ) for name in loader_names: - loader_cls = getattr(importlib_machinery, name, type(None)) + loader_cls = getattr(importlib.machinery, name, type(None)) register_loader_type(loader_cls, cls) @@ -1748,8 +1784,8 @@ class EmptyProvider(NullProvider): _isdir = _has = lambda self, path: False - def _get(self, path): - return '' + def _get(self, path) -> bytes: + return b'' def _listdir(self, path): return [] @@ -1761,13 +1797,14 @@ class EmptyProvider(NullProvider): empty_provider = EmptyProvider() -class ZipManifests(dict): +class ZipManifests(Dict[str, "MemoizedZipManifests.manifest_mod"]): """ zip manifest builder """ + # `path` could be `Union["StrPath", IO[bytes]]` but that violates the LSP for `MemoizedZipManifests.load` @classmethod - def build(cls, path): + def build(cls, path: str): """ Build a dictionary similar to the zipimport directory caches, except instead of tuples, store ZipInfo objects. @@ -1793,9 +1830,11 @@ class MemoizedZipManifests(ZipManifests): Memoized zipfile manifests. """ - manifest_mod = collections.namedtuple('manifest_mod', 'manifest mtime') + class manifest_mod(NamedTuple): + manifest: Dict[str, zipfile.ZipInfo] + mtime: float - def load(self, path): + def load(self, path: str): # type: ignore[override] # ZipManifests.load is a classmethod """ Load a manifest at path or return a suitable manifest already loaded. """ @@ -1812,10 +1851,12 @@ class MemoizedZipManifests(ZipManifests): class ZipProvider(EggProvider): """Resource support for zips and eggs""" - eagers = None + eagers: Optional[List[str]] = None _zip_manifests = MemoizedZipManifests() + # ZipProvider's loader should always be a zipimporter or equivalent + loader: zipimport.zipimporter - def __init__(self, module): + def __init__(self, module: _ModuleLike): super().__init__(module) self.zip_pre = self.loader.archive + os.sep @@ -1841,7 +1882,7 @@ class ZipProvider(EggProvider): def zipinfo(self): return self._zip_manifests.load(self.loader.archive) - def get_resource_filename(self, manager, resource_name): + def get_resource_filename(self, manager: ResourceManager, resource_name: str): if not self.egg_name: raise NotImplementedError( "resource_filename() only supported for .egg, not .zip" @@ -1864,7 +1905,7 @@ class ZipProvider(EggProvider): return timestamp, size # FIXME: 'ZipProvider._extract_resource' is too complex (12) - def _extract_resource(self, manager, zip_path): # noqa: C901 + def _extract_resource(self, manager: ResourceManager, zip_path): # noqa: C901 if zip_path in self._index(): for name in self._index()[zip_path]: last = self._extract_resource(manager, os.path.join(zip_path, name)) @@ -1874,10 +1915,14 @@ class ZipProvider(EggProvider): timestamp, size = self._get_date_and_size(self.zipinfo[zip_path]) if not WRITE_SUPPORT: - raise IOError( + raise OSError( '"os.rename" and "os.unlink" are not supported ' 'on this platform' ) try: + if not self.egg_name: + raise OSError( + '"egg_name" is empty. This likely means no egg could be found from the "module_path".' + ) real_path = manager.get_cache_path(self.egg_name, self._parts(zip_path)) if self._is_current(real_path, zip_path): @@ -1895,7 +1940,7 @@ class ZipProvider(EggProvider): try: rename(tmpnam, real_path) - except os.error: + except OSError: if os.path.isfile(real_path): if self._is_current(real_path, zip_path): # the file became current since it was checked above, @@ -1908,7 +1953,7 @@ class ZipProvider(EggProvider): return real_path raise - except os.error: + except OSError: # report a user-friendly error manager.extraction_error() @@ -1956,20 +2001,20 @@ class ZipProvider(EggProvider): self._dirindex = ind return ind - def _has(self, fspath): + def _has(self, fspath) -> bool: zip_path = self._zipinfo_name(fspath) return zip_path in self.zipinfo or zip_path in self._index() - def _isdir(self, fspath): + def _isdir(self, fspath) -> bool: return self._zipinfo_name(fspath) in self._index() def _listdir(self, fspath): return list(self._index().get(self._zipinfo_name(fspath), ())) - def _eager_to_zip(self, resource_name): + def _eager_to_zip(self, resource_name: str): return self._zipinfo_name(self._fn(self.egg_root, resource_name)) - def _resource_to_zip(self, resource_name): + def _resource_to_zip(self, resource_name: str): return self._zipinfo_name(self._fn(self.module_path, resource_name)) @@ -1988,20 +2033,20 @@ class FileMetadata(EmptyProvider): the provided location. """ - def __init__(self, path): + def __init__(self, path: "StrPath"): self.path = path def _get_metadata_path(self, name): return self.path - def has_metadata(self, name): + def has_metadata(self, name: str) -> bool: return name == 'PKG-INFO' and os.path.isfile(self.path) def get_metadata(self, name): if name != 'PKG-INFO': raise KeyError("No metadata except PKG-INFO is available") - with io.open(self.path, encoding='utf-8', errors="replace") as f: + with open(self.path, encoding='utf-8', errors="replace") as f: metadata = f.read() self._warn_on_replacement(metadata) return metadata @@ -2037,7 +2082,7 @@ class PathMetadata(DefaultProvider): dist = Distribution.from_filename(egg_path, metadata=metadata) """ - def __init__(self, path, egg_info): + def __init__(self, path: str, egg_info: str): self.module_path = path self.egg_info = egg_info @@ -2045,7 +2090,7 @@ class PathMetadata(DefaultProvider): class EggMetadata(ZipProvider): """Metadata provider for .egg files""" - def __init__(self, importer): + def __init__(self, importer: zipimport.zipimporter): """Create a metadata provider from a zipimporter""" self.zip_pre = importer.archive + os.sep @@ -2057,10 +2102,12 @@ class EggMetadata(ZipProvider): self._setup_prefix() -_declare_state('dict', _distribution_finders={}) +_distribution_finders: Dict[type, _DistFinderType[Any]] = _declare_state( + 'dict', '_distribution_finders', {} +) -def register_finder(importer_type, distribution_finder): +def register_finder(importer_type: Type[_T], distribution_finder: _DistFinderType[_T]): """Register `distribution_finder` to find distributions in sys.path items `importer_type` is the type or class of a PEP 302 "Importer" (sys.path item @@ -2070,14 +2117,16 @@ def register_finder(importer_type, distribution_finder): _distribution_finders[importer_type] = distribution_finder -def find_distributions(path_item, only=False): +def find_distributions(path_item: str, only: bool = False): """Yield distributions accessible via `path_item`""" importer = get_importer(path_item) finder = _find_adapter(_distribution_finders, importer) return finder(importer, path_item, only) -def find_eggs_in_zip(importer, path_item, only=False): +def find_eggs_in_zip( + importer: zipimport.zipimporter, path_item: str, only: bool = False +): """ Find eggs in zip files; possibly multiple nested eggs. """ @@ -2095,8 +2144,7 @@ def find_eggs_in_zip(importer, path_item, only=False): if _is_egg_path(subitem): subpath = os.path.join(path_item, subitem) dists = find_eggs_in_zip(zipimport.zipimporter(subpath), subpath) - for dist in dists: - yield dist + yield from dists elif subitem.lower().endswith(('.dist-info', '.egg-info')): subpath = os.path.join(path_item, subitem) submeta = EggMetadata(zipimport.zipimporter(subpath)) @@ -2107,14 +2155,16 @@ def find_eggs_in_zip(importer, path_item, only=False): register_finder(zipimport.zipimporter, find_eggs_in_zip) -def find_nothing(importer, path_item, only=False): +def find_nothing( + importer: Optional[object], path_item: Optional[str], only: Optional[bool] = False +): return () register_finder(object, find_nothing) -def find_on_path(importer, path_item, only=False): +def find_on_path(importer: Optional[object], path_item, only=False): """Yield distributions accessible on a sys.path directory""" path_item = _normalize_cached(path_item) @@ -2131,8 +2181,7 @@ def find_on_path(importer, path_item, only=False): for entry in sorted(entries): fullpath = os.path.join(path_item, entry) factory = dist_factory(path_item, entry, only) - for dist in factory(fullpath): - yield dist + yield from factory(fullpath) def dist_factory(path_item, entry, only): @@ -2208,11 +2257,10 @@ def non_empty_lines(path): """ Yield non-empty lines from file at path """ - with open(path) as f: - for line in f: - line = line.strip() - if line: - yield line + for line in _read_utf8_with_fallback(path).splitlines(): + line = line.strip() + if line: + yield line def resolve_egg_link(path): @@ -2231,13 +2279,19 @@ def resolve_egg_link(path): if hasattr(pkgutil, 'ImpImporter'): register_finder(pkgutil.ImpImporter, find_on_path) -register_finder(importlib_machinery.FileFinder, find_on_path) +register_finder(importlib.machinery.FileFinder, find_on_path) -_declare_state('dict', _namespace_handlers={}) -_declare_state('dict', _namespace_packages={}) +_namespace_handlers: Dict[type, _NSHandlerType[Any]] = _declare_state( + 'dict', '_namespace_handlers', {} +) +_namespace_packages: Dict[Optional[str], List[str]] = _declare_state( + 'dict', '_namespace_packages', {} +) -def register_namespace_handler(importer_type, namespace_handler): +def register_namespace_handler( + importer_type: Type[_T], namespace_handler: _NSHandlerType[_T] +): """Register `namespace_handler` to declare namespace packages `importer_type` is the type or class of a PEP 302 "Importer" (sys.path item @@ -2292,7 +2346,7 @@ def _handle_ns(packageName, path_item): return subpath -def _rebuild_mod_path(orig_path, package_name, module): +def _rebuild_mod_path(orig_path, package_name, module: types.ModuleType): """ Rebuild module.__path__ ensuring that all entries are ordered corresponding to their sys.path order @@ -2326,7 +2380,7 @@ def _rebuild_mod_path(orig_path, package_name, module): module.__path__ = new_path -def declare_namespace(packageName): +def declare_namespace(packageName: str): """Declare that package 'packageName' is a namespace package""" msg = ( @@ -2343,7 +2397,7 @@ def declare_namespace(packageName): if packageName in _namespace_packages: return - path = sys.path + path: MutableSequence[str] = sys.path parent, _, _ = packageName.rpartition('.') if parent: @@ -2369,7 +2423,7 @@ def declare_namespace(packageName): _imp.release_lock() -def fixup_namespace_packages(path_item, parent=None): +def fixup_namespace_packages(path_item: str, parent: Optional[str] = None): """Ensure that previously-declared namespace packages include path_item""" _imp.acquire_lock() try: @@ -2381,7 +2435,12 @@ def fixup_namespace_packages(path_item, parent=None): _imp.release_lock() -def file_ns_handler(importer, path_item, packageName, module): +def file_ns_handler( + importer: object, + path_item: "StrPath", + packageName: str, + module: types.ModuleType, +): """Compute an ns-package subpath for a filesystem or zipfile importer""" subpath = os.path.join(path_item, packageName.split('.')[-1]) @@ -2398,22 +2457,27 @@ if hasattr(pkgutil, 'ImpImporter'): register_namespace_handler(pkgutil.ImpImporter, file_ns_handler) register_namespace_handler(zipimport.zipimporter, file_ns_handler) -register_namespace_handler(importlib_machinery.FileFinder, file_ns_handler) +register_namespace_handler(importlib.machinery.FileFinder, file_ns_handler) -def null_ns_handler(importer, path_item, packageName, module): +def null_ns_handler( + importer: object, + path_item: Optional[str], + packageName: Optional[str], + module: Optional[_ModuleLike], +): return None register_namespace_handler(object, null_ns_handler) -def normalize_path(filename): +def normalize_path(filename: "StrPath"): """Normalize a file/dir name for comparison purposes""" return os.path.normcase(os.path.realpath(os.path.normpath(_cygwin_patch(filename)))) -def _cygwin_patch(filename): # pragma: nocover +def _cygwin_patch(filename: "StrPath"): # pragma: nocover """ Contrary to POSIX 2008, on Cygwin, getcwd (3) contains symlink components. Using @@ -2424,12 +2488,9 @@ def _cygwin_patch(filename): # pragma: nocover return os.path.abspath(filename) if sys.platform == 'cygwin' else filename -def _normalize_cached(filename, _cache={}): - try: - return _cache[filename] - except KeyError: - _cache[filename] = result = normalize_path(filename) - return result +@functools.lru_cache(maxsize=None) +def _normalize_cached(filename): + return normalize_path(filename) def _is_egg_path(path): @@ -2482,7 +2543,14 @@ EGG_NAME = re.compile( class EntryPoint: """Object representing an advertised importable object""" - def __init__(self, name, module_name, attrs=(), extras=(), dist=None): + def __init__( + self, + name: str, + module_name: str, + attrs: Iterable[str] = (), + extras: Iterable[str] = (), + dist: Optional["Distribution"] = None, + ): if not MODULE(module_name): raise ValueError("Invalid module name", module_name) self.name = name @@ -2502,7 +2570,12 @@ class EntryPoint: def __repr__(self): return "EntryPoint.parse(%r)" % str(self) - def load(self, require=True, *args, **kwargs): + def load( + self, + require: bool = True, + *args: Optional[Union[Environment, _InstallerType]], + **kwargs: Optional[Union[Environment, _InstallerType]], + ): """ Require packages for this EntryPoint, then resolve it. """ @@ -2514,7 +2587,9 @@ class EntryPoint: stacklevel=2, ) if require: - self.require(*args, **kwargs) + # We could pass `env` and `installer` directly, + # but keeping `*args` and `**kwargs` for backwards compatibility + self.require(*args, **kwargs) # type: ignore return self.resolve() def resolve(self): @@ -2527,9 +2602,14 @@ class EntryPoint: except AttributeError as exc: raise ImportError(str(exc)) from exc - def require(self, env=None, installer=None): - if self.extras and not self.dist: - raise UnknownExtra("Can't require() without a distribution", self) + def require( + self, + env: Optional[Environment] = None, + installer: Optional[_InstallerType] = None, + ): + if not self.dist: + error_cls = UnknownExtra if self.extras else AttributeError + raise error_cls("Can't require() without a distribution", self) # Get the requirements for this entry point with all its extras and # then resolve them. We have to pass `extras` along when resolving so @@ -2550,7 +2630,7 @@ class EntryPoint: ) @classmethod - def parse(cls, src, dist=None): + def parse(cls, src: str, dist: Optional["Distribution"] = None): """Parse a single entry point from string `src` Entry point syntax follows the form:: @@ -2579,7 +2659,12 @@ class EntryPoint: return req.extras @classmethod - def parse_group(cls, group, lines, dist=None): + def parse_group( + cls, + group: str, + lines: _NestedStr, + dist: Optional["Distribution"] = None, + ): """Parse an entry point group""" if not MODULE(group): raise ValueError("Invalid group name", group) @@ -2592,14 +2677,18 @@ class EntryPoint: return this @classmethod - def parse_map(cls, data, dist=None): + def parse_map( + cls, + data: Union[str, Iterable[str], Dict[str, Union[str, Iterable[str]]]], + dist: Optional["Distribution"] = None, + ): """Parse a map of entry point groups""" if isinstance(data, dict): - data = data.items() + _data = data.items() else: - data = split_sections(data) - maps = {} - for group, lines in data: + _data = split_sections(data) + maps: Dict[str, Dict[str, EntryPoint]] = {} + for group, lines in _data: if group is None: if not lines: continue @@ -2633,13 +2722,13 @@ class Distribution: def __init__( self, - location=None, - metadata=None, - project_name=None, - version=None, - py_version=PY_MAJOR, - platform=None, - precedence=EGG_DIST, + location: Optional[str] = None, + metadata: _MetadataType = None, + project_name: Optional[str] = None, + version: Optional[str] = None, + py_version: Optional[str] = PY_MAJOR, + platform: Optional[str] = None, + precedence: int = EGG_DIST, ): self.project_name = safe_name(project_name or 'Unknown') if version is not None: @@ -2651,7 +2740,13 @@ class Distribution: self._provider = metadata or empty_provider @classmethod - def from_location(cls, location, basename, metadata=None, **kw): + def from_location( + cls, + location: str, + basename: str, + metadata: _MetadataType = None, + **kw: int, # We could set `precedence` explicitly, but keeping this as `**kw` for full backwards and subclassing compatibility + ): project_name, version, py_version, platform = [None] * 4 basename, ext = os.path.splitext(basename) if ext.lower() in _distributionImpl: @@ -2689,25 +2784,25 @@ class Distribution: def __hash__(self): return hash(self.hashcmp) - def __lt__(self, other): + def __lt__(self, other: "Distribution"): return self.hashcmp < other.hashcmp - def __le__(self, other): + def __le__(self, other: "Distribution"): return self.hashcmp <= other.hashcmp - def __gt__(self, other): + def __gt__(self, other: "Distribution"): return self.hashcmp > other.hashcmp - def __ge__(self, other): + def __ge__(self, other: "Distribution"): return self.hashcmp >= other.hashcmp - def __eq__(self, other): + def __eq__(self, other: object): if not isinstance(other, self.__class__): # It's not a Distribution, so they are not equal return False return self.hashcmp == other.hashcmp - def __ne__(self, other): + def __ne__(self, other: object): return not self == other # These properties have to be lazy so that we don't have to load any @@ -2727,12 +2822,12 @@ class Distribution: if not hasattr(self, "_parsed_version"): try: self._parsed_version = parse_version(self.version) - except packaging.version.InvalidVersion as ex: + except _packaging_version.InvalidVersion as ex: info = f"(package: {self.project_name})" if hasattr(ex, "add_note"): ex.add_note(info) # PEP 678 raise - raise packaging.version.InvalidVersion(f"{str(ex)} {info}") from None + raise _packaging_version.InvalidVersion(f"{str(ex)} {info}") from None return self._parsed_version @@ -2740,7 +2835,7 @@ class Distribution: def _forgiving_parsed_version(self): try: return self.parsed_version - except packaging.version.InvalidVersion as ex: + except _packaging_version.InvalidVersion as ex: self._parsed_version = parse_version(_forgiving_version(self.version)) notes = "\n".join(getattr(ex, "__notes__", [])) # PEP 678 @@ -2817,7 +2912,7 @@ class Distribution: dm.setdefault(extra, []).extend(parse_requirements(reqs)) return dm - def requires(self, extras=()): + def requires(self, extras: Iterable[str] = ()): """List of Requirements needed for this distro if `extras` are used""" dm = self._dep_map deps = [] @@ -2850,21 +2945,18 @@ class Distribution: def _get_metadata(self, name): if self.has_metadata(name): - for line in self.get_metadata_lines(name): - yield line + yield from self.get_metadata_lines(name) def _get_version(self): lines = self._get_metadata(self.PKG_INFO) - version = _version_from_file(lines) + return _version_from_file(lines) - return version - - def activate(self, path=None, replace=False): + def activate(self, path: Optional[List[str]] = None, replace: bool = False): """Ensure distribution is importable on `path` (default=sys.path)""" if path is None: path = sys.path self.insert_on(path, replace=replace) - if path is sys.path: + if path is sys.path and self.location is not None: fixup_namespace_packages(self.location) for pkg in self._get_metadata('namespace_packages.txt'): if pkg in sys.modules: @@ -2904,50 +2996,58 @@ class Distribution: def __dir__(self): return list( - set(super(Distribution, self).__dir__()) + set(super().__dir__()) | set(attr for attr in self._provider.__dir__() if not attr.startswith('_')) ) @classmethod - def from_filename(cls, filename, metadata=None, **kw): + def from_filename( + cls, + filename: str, + metadata: _MetadataType = None, + **kw: int, # We could set `precedence` explicitly, but keeping this as `**kw` for full backwards and subclassing compatibility + ): return cls.from_location( _normalize_cached(filename), os.path.basename(filename), metadata, **kw ) def as_requirement(self): """Return a ``Requirement`` that matches this distribution exactly""" - if isinstance(self.parsed_version, packaging.version.Version): + if isinstance(self.parsed_version, _packaging_version.Version): spec = "%s==%s" % (self.project_name, self.parsed_version) else: spec = "%s===%s" % (self.project_name, self.parsed_version) return Requirement.parse(spec) - def load_entry_point(self, group, name): + def load_entry_point(self, group: str, name: str): """Return the `name` entry point of `group` or raise ImportError""" ep = self.get_entry_info(group, name) if ep is None: raise ImportError("Entry point %r not found" % ((group, name),)) return ep.load() - def get_entry_map(self, group=None): + def get_entry_map(self, group: Optional[str] = None): """Return the entry point map for `group`, or the full entry map""" - try: - ep_map = self._ep_map - except AttributeError: - ep_map = self._ep_map = EntryPoint.parse_map( + if not hasattr(self, "_ep_map"): + self._ep_map = EntryPoint.parse_map( self._get_metadata('entry_points.txt'), self ) if group is not None: - return ep_map.get(group, {}) - return ep_map + return self._ep_map.get(group, {}) + return self._ep_map - def get_entry_info(self, group, name): + def get_entry_info(self, group: str, name: str): """Return the EntryPoint object for `group`+`name`, or ``None``""" return self.get_entry_map(group).get(name) # FIXME: 'Distribution.insert_on' is too complex (13) - def insert_on(self, path, loc=None, replace=False): # noqa: C901 + def insert_on( # noqa: C901 + self, + path: List[str], + loc=None, + replace: bool = False, + ): """Ensure self.location is on path If replace=False (default): @@ -3052,13 +3152,14 @@ class Distribution: return False return True - def clone(self, **kw): + def clone(self, **kw: Optional[Union[str, int, IResourceProvider]]): """Copy this distribution, substituting in any changed keyword args""" names = 'project_name version py_version platform location precedence' for attr in names.split(): kw.setdefault(attr, getattr(self, attr, None)) kw.setdefault('metadata', self._provider) - return self.__class__(**kw) + # Unsafely unpacking. But keeping **kw for backwards and subclassing compatibility + return self.__class__(**kw) # type:ignore[arg-type] @property def extras(self): @@ -3155,7 +3256,7 @@ def issue_warning(*args, **kw): warnings.warn(stacklevel=level + 1, *args, **kw) -def parse_requirements(strs): +def parse_requirements(strs: _NestedStr): """ Yield ``Requirement`` objects for each specification in `strs`. @@ -3164,19 +3265,20 @@ def parse_requirements(strs): return map(Requirement, join_continuation(map(drop_comment, yield_lines(strs)))) -class RequirementParseError(packaging.requirements.InvalidRequirement): +class RequirementParseError(_packaging_requirements.InvalidRequirement): "Compatibility wrapper for InvalidRequirement" -class Requirement(packaging.requirements.Requirement): - def __init__(self, requirement_string): +class Requirement(_packaging_requirements.Requirement): + def __init__(self, requirement_string: str): """DO NOT CALL THIS UNDOCUMENTED METHOD; use Requirement.parse()!""" - super(Requirement, self).__init__(requirement_string) + super().__init__(requirement_string) self.unsafe_name = self.name project_name = safe_name(self.name) self.project_name, self.key = project_name, project_name.lower() self.specs = [(spec.operator, spec.version) for spec in self.specifier] - self.extras = tuple(map(safe_extra, self.extras)) + # packaging.requirements.Requirement uses a set for its extras. We use a variable-length tuple + self.extras: Tuple[str] = tuple(map(safe_extra, self.extras)) self.hashCmp = ( self.key, self.url, @@ -3186,13 +3288,13 @@ class Requirement(packaging.requirements.Requirement): ) self.__hash = hash(self.hashCmp) - def __eq__(self, other): + def __eq__(self, other: object): return isinstance(other, Requirement) and self.hashCmp == other.hashCmp def __ne__(self, other): return not self == other - def __contains__(self, item): + def __contains__(self, item: Union[Distribution, str, Tuple[str, ...]]): if isinstance(item, Distribution): if item.key != self.key: return False @@ -3211,7 +3313,7 @@ class Requirement(packaging.requirements.Requirement): return "Requirement.parse(%r)" % str(self) @staticmethod - def parse(s): + def parse(s: Union[str, Iterable[str]]): (req,) = parse_requirements(s) return req @@ -3226,15 +3328,18 @@ def _always_object(classes): return classes -def _find_adapter(registry, ob): +def _find_adapter(registry: Mapping[type, _AdapterT], ob: object) -> _AdapterT: """Return an adapter factory for `ob` from `registry`""" types = _always_object(inspect.getmro(getattr(ob, '__class__', type(ob)))) for t in types: if t in registry: return registry[t] + # _find_adapter would previously return None, and immediately be called. + # So we're raising a TypeError to keep backward compatibility if anyone depended on that behaviour. + raise TypeError(f"Could not find adapter for {registry} and {ob}") -def ensure_directory(path): +def ensure_directory(path: str): """Ensure that the parent directory of `path` exists""" dirname = os.path.dirname(path) os.makedirs(dirname, exist_ok=True) @@ -3243,7 +3348,7 @@ def ensure_directory(path): def _bypass_ensure_directory(path): """Sandbox-bypassing version of ensure_directory()""" if not WRITE_SUPPORT: - raise IOError('"os.mkdir" not supported on this platform.') + raise OSError('"os.mkdir" not supported on this platform.') dirname, filename = split(path) if dirname and filename and not isdir(dirname): _bypass_ensure_directory(dirname) @@ -3253,7 +3358,7 @@ def _bypass_ensure_directory(path): pass -def split_sections(s): +def split_sections(s: _NestedStr): """Split a string or iterable thereof into (section, content) pairs Each ``section`` is a stripped version of the section header ("[section]") @@ -3297,6 +3402,15 @@ def _mkstemp(*args, **kw): warnings.filterwarnings("ignore", category=PEP440Warning, append=True) +class PkgResourcesDeprecationWarning(Warning): + """ + Base class for warning about deprecations in ``pkg_resources`` + + This class is not derived from ``DeprecationWarning``, and as such is + visible by default. + """ + + # from jaraco.functools 1.3 def _call_aside(f, *args, **kwargs): f(*args, **kwargs) @@ -3315,15 +3429,6 @@ def _initialize(g=globals()): ) -class PkgResourcesDeprecationWarning(Warning): - """ - Base class for warning about deprecations in ``pkg_resources`` - - This class is not derived from ``DeprecationWarning``, and as such is - visible by default. - """ - - @_call_aside def _initialize_master_working_set(): """ @@ -3337,8 +3442,7 @@ def _initialize_master_working_set(): Invocation by other packages is unsupported and done at their own risk. """ - working_set = WorkingSet._build_master() - _declare_state('object', working_set=working_set) + working_set = _declare_state('object', 'working_set', WorkingSet._build_master()) require = working_set.require iter_entry_points = working_set.iter_entry_points @@ -3359,3 +3463,55 @@ def _initialize_master_working_set(): # match order list(map(working_set.add_entry, sys.path)) globals().update(locals()) + + +if TYPE_CHECKING: + # All of these are set by the @_call_aside methods above + __resource_manager = ResourceManager() # Won't exist at runtime + resource_exists = __resource_manager.resource_exists + resource_isdir = __resource_manager.resource_isdir + resource_filename = __resource_manager.resource_filename + resource_stream = __resource_manager.resource_stream + resource_string = __resource_manager.resource_string + resource_listdir = __resource_manager.resource_listdir + set_extraction_path = __resource_manager.set_extraction_path + cleanup_resources = __resource_manager.cleanup_resources + + working_set = WorkingSet() + require = working_set.require + iter_entry_points = working_set.iter_entry_points + add_activation_listener = working_set.subscribe + run_script = working_set.run_script + run_main = run_script + + +# ---- Ported from ``setuptools`` to avoid introducing an import inter-dependency ---- +LOCALE_ENCODING = "locale" if sys.version_info >= (3, 10) else None + + +def _read_utf8_with_fallback(file: str, fallback_encoding=LOCALE_ENCODING) -> str: + """See setuptools.unicode_utils._read_utf8_with_fallback""" + try: + with open(file, "r", encoding="utf-8") as f: + return f.read() + except UnicodeDecodeError: # pragma: no cover + msg = f"""\ + ******************************************************************************** + `encoding="utf-8"` fails with {file!r}, trying `encoding={fallback_encoding!r}`. + + This fallback behaviour is considered **deprecated** and future versions of + `setuptools/pkg_resources` may not implement it. + + Please encode {file!r} with "utf-8" to ensure future builds will succeed. + + If this file was produced by `setuptools` itself, cleaning up the cached files + and re-building/re-installing the package with a newer version of `setuptools` + (e.g. by updating `build-system.requires` in its `pyproject.toml`) + might solve the problem. + ******************************************************************************** + """ + # TODO: Add a deadline? + # See comment in setuptools.unicode_utils._Utf8EncodingNeeded + warnings.warn(msg, PkgResourcesDeprecationWarning, stacklevel=2) + with open(file, "r", encoding=fallback_encoding) as f: + return f.read() diff --git a/lib/pkg_resources/_vendor/backports/__init__.py b/lib/pkg_resources/_vendor/backports/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/lib/pkg_resources/_vendor/backports/tarfile.py b/lib/pkg_resources/_vendor/backports/tarfile.py new file mode 100644 index 00000000..a7a9a6e7 --- /dev/null +++ b/lib/pkg_resources/_vendor/backports/tarfile.py @@ -0,0 +1,2900 @@ +#!/usr/bin/env python3 +#------------------------------------------------------------------- +# tarfile.py +#------------------------------------------------------------------- +# Copyright (C) 2002 Lars Gustaebel +# All rights reserved. +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation +# files (the "Software"), to deal in the Software without +# restriction, including without limitation the rights to use, +# copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following +# conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +# OTHER DEALINGS IN THE SOFTWARE. +# +"""Read from and write to tar format archives. +""" + +version = "0.9.0" +__author__ = "Lars Gust\u00e4bel (lars@gustaebel.de)" +__credits__ = "Gustavo Niemeyer, Niels Gust\u00e4bel, Richard Townsend." + +#--------- +# Imports +#--------- +from builtins import open as bltn_open +import sys +import os +import io +import shutil +import stat +import time +import struct +import copy +import re +import warnings + +try: + import pwd +except ImportError: + pwd = None +try: + import grp +except ImportError: + grp = None + +# os.symlink on Windows prior to 6.0 raises NotImplementedError +# OSError (winerror=1314) will be raised if the caller does not hold the +# SeCreateSymbolicLinkPrivilege privilege +symlink_exception = (AttributeError, NotImplementedError, OSError) + +# from tarfile import * +__all__ = ["TarFile", "TarInfo", "is_tarfile", "TarError", "ReadError", + "CompressionError", "StreamError", "ExtractError", "HeaderError", + "ENCODING", "USTAR_FORMAT", "GNU_FORMAT", "PAX_FORMAT", + "DEFAULT_FORMAT", "open","fully_trusted_filter", "data_filter", + "tar_filter", "FilterError", "AbsoluteLinkError", + "OutsideDestinationError", "SpecialFileError", "AbsolutePathError", + "LinkOutsideDestinationError"] + + +#--------------------------------------------------------- +# tar constants +#--------------------------------------------------------- +NUL = b"\0" # the null character +BLOCKSIZE = 512 # length of processing blocks +RECORDSIZE = BLOCKSIZE * 20 # length of records +GNU_MAGIC = b"ustar \0" # magic gnu tar string +POSIX_MAGIC = b"ustar\x0000" # magic posix tar string + +LENGTH_NAME = 100 # maximum length of a filename +LENGTH_LINK = 100 # maximum length of a linkname +LENGTH_PREFIX = 155 # maximum length of the prefix field + +REGTYPE = b"0" # regular file +AREGTYPE = b"\0" # regular file +LNKTYPE = b"1" # link (inside tarfile) +SYMTYPE = b"2" # symbolic link +CHRTYPE = b"3" # character special device +BLKTYPE = b"4" # block special device +DIRTYPE = b"5" # directory +FIFOTYPE = b"6" # fifo special device +CONTTYPE = b"7" # contiguous file + +GNUTYPE_LONGNAME = b"L" # GNU tar longname +GNUTYPE_LONGLINK = b"K" # GNU tar longlink +GNUTYPE_SPARSE = b"S" # GNU tar sparse file + +XHDTYPE = b"x" # POSIX.1-2001 extended header +XGLTYPE = b"g" # POSIX.1-2001 global header +SOLARIS_XHDTYPE = b"X" # Solaris extended header + +USTAR_FORMAT = 0 # POSIX.1-1988 (ustar) format +GNU_FORMAT = 1 # GNU tar format +PAX_FORMAT = 2 # POSIX.1-2001 (pax) format +DEFAULT_FORMAT = PAX_FORMAT + +#--------------------------------------------------------- +# tarfile constants +#--------------------------------------------------------- +# File types that tarfile supports: +SUPPORTED_TYPES = (REGTYPE, AREGTYPE, LNKTYPE, + SYMTYPE, DIRTYPE, FIFOTYPE, + CONTTYPE, CHRTYPE, BLKTYPE, + GNUTYPE_LONGNAME, GNUTYPE_LONGLINK, + GNUTYPE_SPARSE) + +# File types that will be treated as a regular file. +REGULAR_TYPES = (REGTYPE, AREGTYPE, + CONTTYPE, GNUTYPE_SPARSE) + +# File types that are part of the GNU tar format. +GNU_TYPES = (GNUTYPE_LONGNAME, GNUTYPE_LONGLINK, + GNUTYPE_SPARSE) + +# Fields from a pax header that override a TarInfo attribute. +PAX_FIELDS = ("path", "linkpath", "size", "mtime", + "uid", "gid", "uname", "gname") + +# Fields from a pax header that are affected by hdrcharset. +PAX_NAME_FIELDS = {"path", "linkpath", "uname", "gname"} + +# Fields in a pax header that are numbers, all other fields +# are treated as strings. +PAX_NUMBER_FIELDS = { + "atime": float, + "ctime": float, + "mtime": float, + "uid": int, + "gid": int, + "size": int +} + +#--------------------------------------------------------- +# initialization +#--------------------------------------------------------- +if os.name == "nt": + ENCODING = "utf-8" +else: + ENCODING = sys.getfilesystemencoding() + +#--------------------------------------------------------- +# Some useful functions +#--------------------------------------------------------- + +def stn(s, length, encoding, errors): + """Convert a string to a null-terminated bytes object. + """ + if s is None: + raise ValueError("metadata cannot contain None") + s = s.encode(encoding, errors) + return s[:length] + (length - len(s)) * NUL + +def nts(s, encoding, errors): + """Convert a null-terminated bytes object to a string. + """ + p = s.find(b"\0") + if p != -1: + s = s[:p] + return s.decode(encoding, errors) + +def nti(s): + """Convert a number field to a python number. + """ + # There are two possible encodings for a number field, see + # itn() below. + if s[0] in (0o200, 0o377): + n = 0 + for i in range(len(s) - 1): + n <<= 8 + n += s[i + 1] + if s[0] == 0o377: + n = -(256 ** (len(s) - 1) - n) + else: + try: + s = nts(s, "ascii", "strict") + n = int(s.strip() or "0", 8) + except ValueError: + raise InvalidHeaderError("invalid header") + return n + +def itn(n, digits=8, format=DEFAULT_FORMAT): + """Convert a python number to a number field. + """ + # POSIX 1003.1-1988 requires numbers to be encoded as a string of + # octal digits followed by a null-byte, this allows values up to + # (8**(digits-1))-1. GNU tar allows storing numbers greater than + # that if necessary. A leading 0o200 or 0o377 byte indicate this + # particular encoding, the following digits-1 bytes are a big-endian + # base-256 representation. This allows values up to (256**(digits-1))-1. + # A 0o200 byte indicates a positive number, a 0o377 byte a negative + # number. + original_n = n + n = int(n) + if 0 <= n < 8 ** (digits - 1): + s = bytes("%0*o" % (digits - 1, n), "ascii") + NUL + elif format == GNU_FORMAT and -256 ** (digits - 1) <= n < 256 ** (digits - 1): + if n >= 0: + s = bytearray([0o200]) + else: + s = bytearray([0o377]) + n = 256 ** digits + n + + for i in range(digits - 1): + s.insert(1, n & 0o377) + n >>= 8 + else: + raise ValueError("overflow in number field") + + return s + +def calc_chksums(buf): + """Calculate the checksum for a member's header by summing up all + characters except for the chksum field which is treated as if + it was filled with spaces. According to the GNU tar sources, + some tars (Sun and NeXT) calculate chksum with signed char, + which will be different if there are chars in the buffer with + the high bit set. So we calculate two checksums, unsigned and + signed. + """ + unsigned_chksum = 256 + sum(struct.unpack_from("148B8x356B", buf)) + signed_chksum = 256 + sum(struct.unpack_from("148b8x356b", buf)) + return unsigned_chksum, signed_chksum + +def copyfileobj(src, dst, length=None, exception=OSError, bufsize=None): + """Copy length bytes from fileobj src to fileobj dst. + If length is None, copy the entire content. + """ + bufsize = bufsize or 16 * 1024 + if length == 0: + return + if length is None: + shutil.copyfileobj(src, dst, bufsize) + return + + blocks, remainder = divmod(length, bufsize) + for b in range(blocks): + buf = src.read(bufsize) + if len(buf) < bufsize: + raise exception("unexpected end of data") + dst.write(buf) + + if remainder != 0: + buf = src.read(remainder) + if len(buf) < remainder: + raise exception("unexpected end of data") + dst.write(buf) + return + +def _safe_print(s): + encoding = getattr(sys.stdout, 'encoding', None) + if encoding is not None: + s = s.encode(encoding, 'backslashreplace').decode(encoding) + print(s, end=' ') + + +class TarError(Exception): + """Base exception.""" + pass +class ExtractError(TarError): + """General exception for extract errors.""" + pass +class ReadError(TarError): + """Exception for unreadable tar archives.""" + pass +class CompressionError(TarError): + """Exception for unavailable compression methods.""" + pass +class StreamError(TarError): + """Exception for unsupported operations on stream-like TarFiles.""" + pass +class HeaderError(TarError): + """Base exception for header errors.""" + pass +class EmptyHeaderError(HeaderError): + """Exception for empty headers.""" + pass +class TruncatedHeaderError(HeaderError): + """Exception for truncated headers.""" + pass +class EOFHeaderError(HeaderError): + """Exception for end of file headers.""" + pass +class InvalidHeaderError(HeaderError): + """Exception for invalid headers.""" + pass +class SubsequentHeaderError(HeaderError): + """Exception for missing and invalid extended headers.""" + pass + +#--------------------------- +# internal stream interface +#--------------------------- +class _LowLevelFile: + """Low-level file object. Supports reading and writing. + It is used instead of a regular file object for streaming + access. + """ + + def __init__(self, name, mode): + mode = { + "r": os.O_RDONLY, + "w": os.O_WRONLY | os.O_CREAT | os.O_TRUNC, + }[mode] + if hasattr(os, "O_BINARY"): + mode |= os.O_BINARY + self.fd = os.open(name, mode, 0o666) + + def close(self): + os.close(self.fd) + + def read(self, size): + return os.read(self.fd, size) + + def write(self, s): + os.write(self.fd, s) + +class _Stream: + """Class that serves as an adapter between TarFile and + a stream-like object. The stream-like object only + needs to have a read() or write() method that works with bytes, + and the method is accessed blockwise. + Use of gzip or bzip2 compression is possible. + A stream-like object could be for example: sys.stdin.buffer, + sys.stdout.buffer, a socket, a tape device etc. + + _Stream is intended to be used only internally. + """ + + def __init__(self, name, mode, comptype, fileobj, bufsize, + compresslevel): + """Construct a _Stream object. + """ + self._extfileobj = True + if fileobj is None: + fileobj = _LowLevelFile(name, mode) + self._extfileobj = False + + if comptype == '*': + # Enable transparent compression detection for the + # stream interface + fileobj = _StreamProxy(fileobj) + comptype = fileobj.getcomptype() + + self.name = name or "" + self.mode = mode + self.comptype = comptype + self.fileobj = fileobj + self.bufsize = bufsize + self.buf = b"" + self.pos = 0 + self.closed = False + + try: + if comptype == "gz": + try: + import zlib + except ImportError: + raise CompressionError("zlib module is not available") from None + self.zlib = zlib + self.crc = zlib.crc32(b"") + if mode == "r": + self.exception = zlib.error + self._init_read_gz() + else: + self._init_write_gz(compresslevel) + + elif comptype == "bz2": + try: + import bz2 + except ImportError: + raise CompressionError("bz2 module is not available") from None + if mode == "r": + self.dbuf = b"" + self.cmp = bz2.BZ2Decompressor() + self.exception = OSError + else: + self.cmp = bz2.BZ2Compressor(compresslevel) + + elif comptype == "xz": + try: + import lzma + except ImportError: + raise CompressionError("lzma module is not available") from None + if mode == "r": + self.dbuf = b"" + self.cmp = lzma.LZMADecompressor() + self.exception = lzma.LZMAError + else: + self.cmp = lzma.LZMACompressor() + + elif comptype != "tar": + raise CompressionError("unknown compression type %r" % comptype) + + except: + if not self._extfileobj: + self.fileobj.close() + self.closed = True + raise + + def __del__(self): + if hasattr(self, "closed") and not self.closed: + self.close() + + def _init_write_gz(self, compresslevel): + """Initialize for writing with gzip compression. + """ + self.cmp = self.zlib.compressobj(compresslevel, + self.zlib.DEFLATED, + -self.zlib.MAX_WBITS, + self.zlib.DEF_MEM_LEVEL, + 0) + timestamp = struct.pack(" self.bufsize: + self.fileobj.write(self.buf[:self.bufsize]) + self.buf = self.buf[self.bufsize:] + + def close(self): + """Close the _Stream object. No operation should be + done on it afterwards. + """ + if self.closed: + return + + self.closed = True + try: + if self.mode == "w" and self.comptype != "tar": + self.buf += self.cmp.flush() + + if self.mode == "w" and self.buf: + self.fileobj.write(self.buf) + self.buf = b"" + if self.comptype == "gz": + self.fileobj.write(struct.pack("= 0: + blocks, remainder = divmod(pos - self.pos, self.bufsize) + for i in range(blocks): + self.read(self.bufsize) + self.read(remainder) + else: + raise StreamError("seeking backwards is not allowed") + return self.pos + + def read(self, size): + """Return the next size number of bytes from the stream.""" + assert size is not None + buf = self._read(size) + self.pos += len(buf) + return buf + + def _read(self, size): + """Return size bytes from the stream. + """ + if self.comptype == "tar": + return self.__read(size) + + c = len(self.dbuf) + t = [self.dbuf] + while c < size: + # Skip underlying buffer to avoid unaligned double buffering. + if self.buf: + buf = self.buf + self.buf = b"" + else: + buf = self.fileobj.read(self.bufsize) + if not buf: + break + try: + buf = self.cmp.decompress(buf) + except self.exception as e: + raise ReadError("invalid compressed data") from e + t.append(buf) + c += len(buf) + t = b"".join(t) + self.dbuf = t[size:] + return t[:size] + + def __read(self, size): + """Return size bytes from stream. If internal buffer is empty, + read another block from the stream. + """ + c = len(self.buf) + t = [self.buf] + while c < size: + buf = self.fileobj.read(self.bufsize) + if not buf: + break + t.append(buf) + c += len(buf) + t = b"".join(t) + self.buf = t[size:] + return t[:size] +# class _Stream + +class _StreamProxy(object): + """Small proxy class that enables transparent compression + detection for the Stream interface (mode 'r|*'). + """ + + def __init__(self, fileobj): + self.fileobj = fileobj + self.buf = self.fileobj.read(BLOCKSIZE) + + def read(self, size): + self.read = self.fileobj.read + return self.buf + + def getcomptype(self): + if self.buf.startswith(b"\x1f\x8b\x08"): + return "gz" + elif self.buf[0:3] == b"BZh" and self.buf[4:10] == b"1AY&SY": + return "bz2" + elif self.buf.startswith((b"\x5d\x00\x00\x80", b"\xfd7zXZ")): + return "xz" + else: + return "tar" + + def close(self): + self.fileobj.close() +# class StreamProxy + +#------------------------ +# Extraction file object +#------------------------ +class _FileInFile(object): + """A thin wrapper around an existing file object that + provides a part of its data as an individual file + object. + """ + + def __init__(self, fileobj, offset, size, name, blockinfo=None): + self.fileobj = fileobj + self.offset = offset + self.size = size + self.position = 0 + self.name = name + self.closed = False + + if blockinfo is None: + blockinfo = [(0, size)] + + # Construct a map with data and zero blocks. + self.map_index = 0 + self.map = [] + lastpos = 0 + realpos = self.offset + for offset, size in blockinfo: + if offset > lastpos: + self.map.append((False, lastpos, offset, None)) + self.map.append((True, offset, offset + size, realpos)) + realpos += size + lastpos = offset + size + if lastpos < self.size: + self.map.append((False, lastpos, self.size, None)) + + def flush(self): + pass + + def readable(self): + return True + + def writable(self): + return False + + def seekable(self): + return self.fileobj.seekable() + + def tell(self): + """Return the current file position. + """ + return self.position + + def seek(self, position, whence=io.SEEK_SET): + """Seek to a position in the file. + """ + if whence == io.SEEK_SET: + self.position = min(max(position, 0), self.size) + elif whence == io.SEEK_CUR: + if position < 0: + self.position = max(self.position + position, 0) + else: + self.position = min(self.position + position, self.size) + elif whence == io.SEEK_END: + self.position = max(min(self.size + position, self.size), 0) + else: + raise ValueError("Invalid argument") + return self.position + + def read(self, size=None): + """Read data from the file. + """ + if size is None: + size = self.size - self.position + else: + size = min(size, self.size - self.position) + + buf = b"" + while size > 0: + while True: + data, start, stop, offset = self.map[self.map_index] + if start <= self.position < stop: + break + else: + self.map_index += 1 + if self.map_index == len(self.map): + self.map_index = 0 + length = min(size, stop - self.position) + if data: + self.fileobj.seek(offset + (self.position - start)) + b = self.fileobj.read(length) + if len(b) != length: + raise ReadError("unexpected end of data") + buf += b + else: + buf += NUL * length + size -= length + self.position += length + return buf + + def readinto(self, b): + buf = self.read(len(b)) + b[:len(buf)] = buf + return len(buf) + + def close(self): + self.closed = True +#class _FileInFile + +class ExFileObject(io.BufferedReader): + + def __init__(self, tarfile, tarinfo): + fileobj = _FileInFile(tarfile.fileobj, tarinfo.offset_data, + tarinfo.size, tarinfo.name, tarinfo.sparse) + super().__init__(fileobj) +#class ExFileObject + + +#----------------------------- +# extraction filters (PEP 706) +#----------------------------- + +class FilterError(TarError): + pass + +class AbsolutePathError(FilterError): + def __init__(self, tarinfo): + self.tarinfo = tarinfo + super().__init__(f'member {tarinfo.name!r} has an absolute path') + +class OutsideDestinationError(FilterError): + def __init__(self, tarinfo, path): + self.tarinfo = tarinfo + self._path = path + super().__init__(f'{tarinfo.name!r} would be extracted to {path!r}, ' + + 'which is outside the destination') + +class SpecialFileError(FilterError): + def __init__(self, tarinfo): + self.tarinfo = tarinfo + super().__init__(f'{tarinfo.name!r} is a special file') + +class AbsoluteLinkError(FilterError): + def __init__(self, tarinfo): + self.tarinfo = tarinfo + super().__init__(f'{tarinfo.name!r} is a link to an absolute path') + +class LinkOutsideDestinationError(FilterError): + def __init__(self, tarinfo, path): + self.tarinfo = tarinfo + self._path = path + super().__init__(f'{tarinfo.name!r} would link to {path!r}, ' + + 'which is outside the destination') + +def _get_filtered_attrs(member, dest_path, for_data=True): + new_attrs = {} + name = member.name + dest_path = os.path.realpath(dest_path) + # Strip leading / (tar's directory separator) from filenames. + # Include os.sep (target OS directory separator) as well. + if name.startswith(('/', os.sep)): + name = new_attrs['name'] = member.path.lstrip('/' + os.sep) + if os.path.isabs(name): + # Path is absolute even after stripping. + # For example, 'C:/foo' on Windows. + raise AbsolutePathError(member) + # Ensure we stay in the destination + target_path = os.path.realpath(os.path.join(dest_path, name)) + if os.path.commonpath([target_path, dest_path]) != dest_path: + raise OutsideDestinationError(member, target_path) + # Limit permissions (no high bits, and go-w) + mode = member.mode + if mode is not None: + # Strip high bits & group/other write bits + mode = mode & 0o755 + if for_data: + # For data, handle permissions & file types + if member.isreg() or member.islnk(): + if not mode & 0o100: + # Clear executable bits if not executable by user + mode &= ~0o111 + # Ensure owner can read & write + mode |= 0o600 + elif member.isdir() or member.issym(): + # Ignore mode for directories & symlinks + mode = None + else: + # Reject special files + raise SpecialFileError(member) + if mode != member.mode: + new_attrs['mode'] = mode + if for_data: + # Ignore ownership for 'data' + if member.uid is not None: + new_attrs['uid'] = None + if member.gid is not None: + new_attrs['gid'] = None + if member.uname is not None: + new_attrs['uname'] = None + if member.gname is not None: + new_attrs['gname'] = None + # Check link destination for 'data' + if member.islnk() or member.issym(): + if os.path.isabs(member.linkname): + raise AbsoluteLinkError(member) + if member.issym(): + target_path = os.path.join(dest_path, + os.path.dirname(name), + member.linkname) + else: + target_path = os.path.join(dest_path, + member.linkname) + target_path = os.path.realpath(target_path) + if os.path.commonpath([target_path, dest_path]) != dest_path: + raise LinkOutsideDestinationError(member, target_path) + return new_attrs + +def fully_trusted_filter(member, dest_path): + return member + +def tar_filter(member, dest_path): + new_attrs = _get_filtered_attrs(member, dest_path, False) + if new_attrs: + return member.replace(**new_attrs, deep=False) + return member + +def data_filter(member, dest_path): + new_attrs = _get_filtered_attrs(member, dest_path, True) + if new_attrs: + return member.replace(**new_attrs, deep=False) + return member + +_NAMED_FILTERS = { + "fully_trusted": fully_trusted_filter, + "tar": tar_filter, + "data": data_filter, +} + +#------------------ +# Exported Classes +#------------------ + +# Sentinel for replace() defaults, meaning "don't change the attribute" +_KEEP = object() + +class TarInfo(object): + """Informational class which holds the details about an + archive member given by a tar header block. + TarInfo objects are returned by TarFile.getmember(), + TarFile.getmembers() and TarFile.gettarinfo() and are + usually created internally. + """ + + __slots__ = dict( + name = 'Name of the archive member.', + mode = 'Permission bits.', + uid = 'User ID of the user who originally stored this member.', + gid = 'Group ID of the user who originally stored this member.', + size = 'Size in bytes.', + mtime = 'Time of last modification.', + chksum = 'Header checksum.', + type = ('File type. type is usually one of these constants: ' + 'REGTYPE, AREGTYPE, LNKTYPE, SYMTYPE, DIRTYPE, FIFOTYPE, ' + 'CONTTYPE, CHRTYPE, BLKTYPE, GNUTYPE_SPARSE.'), + linkname = ('Name of the target file name, which is only present ' + 'in TarInfo objects of type LNKTYPE and SYMTYPE.'), + uname = 'User name.', + gname = 'Group name.', + devmajor = 'Device major number.', + devminor = 'Device minor number.', + offset = 'The tar header starts here.', + offset_data = "The file's data starts here.", + pax_headers = ('A dictionary containing key-value pairs of an ' + 'associated pax extended header.'), + sparse = 'Sparse member information.', + tarfile = None, + _sparse_structs = None, + _link_target = None, + ) + + def __init__(self, name=""): + """Construct a TarInfo object. name is the optional name + of the member. + """ + self.name = name # member name + self.mode = 0o644 # file permissions + self.uid = 0 # user id + self.gid = 0 # group id + self.size = 0 # file size + self.mtime = 0 # modification time + self.chksum = 0 # header checksum + self.type = REGTYPE # member type + self.linkname = "" # link name + self.uname = "" # user name + self.gname = "" # group name + self.devmajor = 0 # device major number + self.devminor = 0 # device minor number + + self.offset = 0 # the tar header starts here + self.offset_data = 0 # the file's data starts here + + self.sparse = None # sparse member information + self.pax_headers = {} # pax header information + + @property + def path(self): + 'In pax headers, "name" is called "path".' + return self.name + + @path.setter + def path(self, name): + self.name = name + + @property + def linkpath(self): + 'In pax headers, "linkname" is called "linkpath".' + return self.linkname + + @linkpath.setter + def linkpath(self, linkname): + self.linkname = linkname + + def __repr__(self): + return "<%s %r at %#x>" % (self.__class__.__name__,self.name,id(self)) + + def replace(self, *, + name=_KEEP, mtime=_KEEP, mode=_KEEP, linkname=_KEEP, + uid=_KEEP, gid=_KEEP, uname=_KEEP, gname=_KEEP, + deep=True, _KEEP=_KEEP): + """Return a deep copy of self with the given attributes replaced. + """ + if deep: + result = copy.deepcopy(self) + else: + result = copy.copy(self) + if name is not _KEEP: + result.name = name + if mtime is not _KEEP: + result.mtime = mtime + if mode is not _KEEP: + result.mode = mode + if linkname is not _KEEP: + result.linkname = linkname + if uid is not _KEEP: + result.uid = uid + if gid is not _KEEP: + result.gid = gid + if uname is not _KEEP: + result.uname = uname + if gname is not _KEEP: + result.gname = gname + return result + + def get_info(self): + """Return the TarInfo's attributes as a dictionary. + """ + if self.mode is None: + mode = None + else: + mode = self.mode & 0o7777 + info = { + "name": self.name, + "mode": mode, + "uid": self.uid, + "gid": self.gid, + "size": self.size, + "mtime": self.mtime, + "chksum": self.chksum, + "type": self.type, + "linkname": self.linkname, + "uname": self.uname, + "gname": self.gname, + "devmajor": self.devmajor, + "devminor": self.devminor + } + + if info["type"] == DIRTYPE and not info["name"].endswith("/"): + info["name"] += "/" + + return info + + def tobuf(self, format=DEFAULT_FORMAT, encoding=ENCODING, errors="surrogateescape"): + """Return a tar header as a string of 512 byte blocks. + """ + info = self.get_info() + for name, value in info.items(): + if value is None: + raise ValueError("%s may not be None" % name) + + if format == USTAR_FORMAT: + return self.create_ustar_header(info, encoding, errors) + elif format == GNU_FORMAT: + return self.create_gnu_header(info, encoding, errors) + elif format == PAX_FORMAT: + return self.create_pax_header(info, encoding) + else: + raise ValueError("invalid format") + + def create_ustar_header(self, info, encoding, errors): + """Return the object as a ustar header block. + """ + info["magic"] = POSIX_MAGIC + + if len(info["linkname"].encode(encoding, errors)) > LENGTH_LINK: + raise ValueError("linkname is too long") + + if len(info["name"].encode(encoding, errors)) > LENGTH_NAME: + info["prefix"], info["name"] = self._posix_split_name(info["name"], encoding, errors) + + return self._create_header(info, USTAR_FORMAT, encoding, errors) + + def create_gnu_header(self, info, encoding, errors): + """Return the object as a GNU header block sequence. + """ + info["magic"] = GNU_MAGIC + + buf = b"" + if len(info["linkname"].encode(encoding, errors)) > LENGTH_LINK: + buf += self._create_gnu_long_header(info["linkname"], GNUTYPE_LONGLINK, encoding, errors) + + if len(info["name"].encode(encoding, errors)) > LENGTH_NAME: + buf += self._create_gnu_long_header(info["name"], GNUTYPE_LONGNAME, encoding, errors) + + return buf + self._create_header(info, GNU_FORMAT, encoding, errors) + + def create_pax_header(self, info, encoding): + """Return the object as a ustar header block. If it cannot be + represented this way, prepend a pax extended header sequence + with supplement information. + """ + info["magic"] = POSIX_MAGIC + pax_headers = self.pax_headers.copy() + + # Test string fields for values that exceed the field length or cannot + # be represented in ASCII encoding. + for name, hname, length in ( + ("name", "path", LENGTH_NAME), ("linkname", "linkpath", LENGTH_LINK), + ("uname", "uname", 32), ("gname", "gname", 32)): + + if hname in pax_headers: + # The pax header has priority. + continue + + # Try to encode the string as ASCII. + try: + info[name].encode("ascii", "strict") + except UnicodeEncodeError: + pax_headers[hname] = info[name] + continue + + if len(info[name]) > length: + pax_headers[hname] = info[name] + + # Test number fields for values that exceed the field limit or values + # that like to be stored as float. + for name, digits in (("uid", 8), ("gid", 8), ("size", 12), ("mtime", 12)): + needs_pax = False + + val = info[name] + val_is_float = isinstance(val, float) + val_int = round(val) if val_is_float else val + if not 0 <= val_int < 8 ** (digits - 1): + # Avoid overflow. + info[name] = 0 + needs_pax = True + elif val_is_float: + # Put rounded value in ustar header, and full + # precision value in pax header. + info[name] = val_int + needs_pax = True + + # The existing pax header has priority. + if needs_pax and name not in pax_headers: + pax_headers[name] = str(val) + + # Create a pax extended header if necessary. + if pax_headers: + buf = self._create_pax_generic_header(pax_headers, XHDTYPE, encoding) + else: + buf = b"" + + return buf + self._create_header(info, USTAR_FORMAT, "ascii", "replace") + + @classmethod + def create_pax_global_header(cls, pax_headers): + """Return the object as a pax global header block sequence. + """ + return cls._create_pax_generic_header(pax_headers, XGLTYPE, "utf-8") + + def _posix_split_name(self, name, encoding, errors): + """Split a name longer than 100 chars into a prefix + and a name part. + """ + components = name.split("/") + for i in range(1, len(components)): + prefix = "/".join(components[:i]) + name = "/".join(components[i:]) + if len(prefix.encode(encoding, errors)) <= LENGTH_PREFIX and \ + len(name.encode(encoding, errors)) <= LENGTH_NAME: + break + else: + raise ValueError("name is too long") + + return prefix, name + + @staticmethod + def _create_header(info, format, encoding, errors): + """Return a header block. info is a dictionary with file + information, format must be one of the *_FORMAT constants. + """ + has_device_fields = info.get("type") in (CHRTYPE, BLKTYPE) + if has_device_fields: + devmajor = itn(info.get("devmajor", 0), 8, format) + devminor = itn(info.get("devminor", 0), 8, format) + else: + devmajor = stn("", 8, encoding, errors) + devminor = stn("", 8, encoding, errors) + + # None values in metadata should cause ValueError. + # itn()/stn() do this for all fields except type. + filetype = info.get("type", REGTYPE) + if filetype is None: + raise ValueError("TarInfo.type must not be None") + + parts = [ + stn(info.get("name", ""), 100, encoding, errors), + itn(info.get("mode", 0) & 0o7777, 8, format), + itn(info.get("uid", 0), 8, format), + itn(info.get("gid", 0), 8, format), + itn(info.get("size", 0), 12, format), + itn(info.get("mtime", 0), 12, format), + b" ", # checksum field + filetype, + stn(info.get("linkname", ""), 100, encoding, errors), + info.get("magic", POSIX_MAGIC), + stn(info.get("uname", ""), 32, encoding, errors), + stn(info.get("gname", ""), 32, encoding, errors), + devmajor, + devminor, + stn(info.get("prefix", ""), 155, encoding, errors) + ] + + buf = struct.pack("%ds" % BLOCKSIZE, b"".join(parts)) + chksum = calc_chksums(buf[-BLOCKSIZE:])[0] + buf = buf[:-364] + bytes("%06o\0" % chksum, "ascii") + buf[-357:] + return buf + + @staticmethod + def _create_payload(payload): + """Return the string payload filled with zero bytes + up to the next 512 byte border. + """ + blocks, remainder = divmod(len(payload), BLOCKSIZE) + if remainder > 0: + payload += (BLOCKSIZE - remainder) * NUL + return payload + + @classmethod + def _create_gnu_long_header(cls, name, type, encoding, errors): + """Return a GNUTYPE_LONGNAME or GNUTYPE_LONGLINK sequence + for name. + """ + name = name.encode(encoding, errors) + NUL + + info = {} + info["name"] = "././@LongLink" + info["type"] = type + info["size"] = len(name) + info["magic"] = GNU_MAGIC + + # create extended header + name blocks. + return cls._create_header(info, USTAR_FORMAT, encoding, errors) + \ + cls._create_payload(name) + + @classmethod + def _create_pax_generic_header(cls, pax_headers, type, encoding): + """Return a POSIX.1-2008 extended or global header sequence + that contains a list of keyword, value pairs. The values + must be strings. + """ + # Check if one of the fields contains surrogate characters and thereby + # forces hdrcharset=BINARY, see _proc_pax() for more information. + binary = False + for keyword, value in pax_headers.items(): + try: + value.encode("utf-8", "strict") + except UnicodeEncodeError: + binary = True + break + + records = b"" + if binary: + # Put the hdrcharset field at the beginning of the header. + records += b"21 hdrcharset=BINARY\n" + + for keyword, value in pax_headers.items(): + keyword = keyword.encode("utf-8") + if binary: + # Try to restore the original byte representation of `value'. + # Needless to say, that the encoding must match the string. + value = value.encode(encoding, "surrogateescape") + else: + value = value.encode("utf-8") + + l = len(keyword) + len(value) + 3 # ' ' + '=' + '\n' + n = p = 0 + while True: + n = l + len(str(p)) + if n == p: + break + p = n + records += bytes(str(p), "ascii") + b" " + keyword + b"=" + value + b"\n" + + # We use a hardcoded "././@PaxHeader" name like star does + # instead of the one that POSIX recommends. + info = {} + info["name"] = "././@PaxHeader" + info["type"] = type + info["size"] = len(records) + info["magic"] = POSIX_MAGIC + + # Create pax header + record blocks. + return cls._create_header(info, USTAR_FORMAT, "ascii", "replace") + \ + cls._create_payload(records) + + @classmethod + def frombuf(cls, buf, encoding, errors): + """Construct a TarInfo object from a 512 byte bytes object. + """ + if len(buf) == 0: + raise EmptyHeaderError("empty header") + if len(buf) != BLOCKSIZE: + raise TruncatedHeaderError("truncated header") + if buf.count(NUL) == BLOCKSIZE: + raise EOFHeaderError("end of file header") + + chksum = nti(buf[148:156]) + if chksum not in calc_chksums(buf): + raise InvalidHeaderError("bad checksum") + + obj = cls() + obj.name = nts(buf[0:100], encoding, errors) + obj.mode = nti(buf[100:108]) + obj.uid = nti(buf[108:116]) + obj.gid = nti(buf[116:124]) + obj.size = nti(buf[124:136]) + obj.mtime = nti(buf[136:148]) + obj.chksum = chksum + obj.type = buf[156:157] + obj.linkname = nts(buf[157:257], encoding, errors) + obj.uname = nts(buf[265:297], encoding, errors) + obj.gname = nts(buf[297:329], encoding, errors) + obj.devmajor = nti(buf[329:337]) + obj.devminor = nti(buf[337:345]) + prefix = nts(buf[345:500], encoding, errors) + + # Old V7 tar format represents a directory as a regular + # file with a trailing slash. + if obj.type == AREGTYPE and obj.name.endswith("/"): + obj.type = DIRTYPE + + # The old GNU sparse format occupies some of the unused + # space in the buffer for up to 4 sparse structures. + # Save them for later processing in _proc_sparse(). + if obj.type == GNUTYPE_SPARSE: + pos = 386 + structs = [] + for i in range(4): + try: + offset = nti(buf[pos:pos + 12]) + numbytes = nti(buf[pos + 12:pos + 24]) + except ValueError: + break + structs.append((offset, numbytes)) + pos += 24 + isextended = bool(buf[482]) + origsize = nti(buf[483:495]) + obj._sparse_structs = (structs, isextended, origsize) + + # Remove redundant slashes from directories. + if obj.isdir(): + obj.name = obj.name.rstrip("/") + + # Reconstruct a ustar longname. + if prefix and obj.type not in GNU_TYPES: + obj.name = prefix + "/" + obj.name + return obj + + @classmethod + def fromtarfile(cls, tarfile): + """Return the next TarInfo object from TarFile object + tarfile. + """ + buf = tarfile.fileobj.read(BLOCKSIZE) + obj = cls.frombuf(buf, tarfile.encoding, tarfile.errors) + obj.offset = tarfile.fileobj.tell() - BLOCKSIZE + return obj._proc_member(tarfile) + + #-------------------------------------------------------------------------- + # The following are methods that are called depending on the type of a + # member. The entry point is _proc_member() which can be overridden in a + # subclass to add custom _proc_*() methods. A _proc_*() method MUST + # implement the following + # operations: + # 1. Set self.offset_data to the position where the data blocks begin, + # if there is data that follows. + # 2. Set tarfile.offset to the position where the next member's header will + # begin. + # 3. Return self or another valid TarInfo object. + def _proc_member(self, tarfile): + """Choose the right processing method depending on + the type and call it. + """ + if self.type in (GNUTYPE_LONGNAME, GNUTYPE_LONGLINK): + return self._proc_gnulong(tarfile) + elif self.type == GNUTYPE_SPARSE: + return self._proc_sparse(tarfile) + elif self.type in (XHDTYPE, XGLTYPE, SOLARIS_XHDTYPE): + return self._proc_pax(tarfile) + else: + return self._proc_builtin(tarfile) + + def _proc_builtin(self, tarfile): + """Process a builtin type or an unknown type which + will be treated as a regular file. + """ + self.offset_data = tarfile.fileobj.tell() + offset = self.offset_data + if self.isreg() or self.type not in SUPPORTED_TYPES: + # Skip the following data blocks. + offset += self._block(self.size) + tarfile.offset = offset + + # Patch the TarInfo object with saved global + # header information. + self._apply_pax_info(tarfile.pax_headers, tarfile.encoding, tarfile.errors) + + # Remove redundant slashes from directories. This is to be consistent + # with frombuf(). + if self.isdir(): + self.name = self.name.rstrip("/") + + return self + + def _proc_gnulong(self, tarfile): + """Process the blocks that hold a GNU longname + or longlink member. + """ + buf = tarfile.fileobj.read(self._block(self.size)) + + # Fetch the next header and process it. + try: + next = self.fromtarfile(tarfile) + except HeaderError as e: + raise SubsequentHeaderError(str(e)) from None + + # Patch the TarInfo object from the next header with + # the longname information. + next.offset = self.offset + if self.type == GNUTYPE_LONGNAME: + next.name = nts(buf, tarfile.encoding, tarfile.errors) + elif self.type == GNUTYPE_LONGLINK: + next.linkname = nts(buf, tarfile.encoding, tarfile.errors) + + # Remove redundant slashes from directories. This is to be consistent + # with frombuf(). + if next.isdir(): + next.name = next.name.removesuffix("/") + + return next + + def _proc_sparse(self, tarfile): + """Process a GNU sparse header plus extra headers. + """ + # We already collected some sparse structures in frombuf(). + structs, isextended, origsize = self._sparse_structs + del self._sparse_structs + + # Collect sparse structures from extended header blocks. + while isextended: + buf = tarfile.fileobj.read(BLOCKSIZE) + pos = 0 + for i in range(21): + try: + offset = nti(buf[pos:pos + 12]) + numbytes = nti(buf[pos + 12:pos + 24]) + except ValueError: + break + if offset and numbytes: + structs.append((offset, numbytes)) + pos += 24 + isextended = bool(buf[504]) + self.sparse = structs + + self.offset_data = tarfile.fileobj.tell() + tarfile.offset = self.offset_data + self._block(self.size) + self.size = origsize + return self + + def _proc_pax(self, tarfile): + """Process an extended or global header as described in + POSIX.1-2008. + """ + # Read the header information. + buf = tarfile.fileobj.read(self._block(self.size)) + + # A pax header stores supplemental information for either + # the following file (extended) or all following files + # (global). + if self.type == XGLTYPE: + pax_headers = tarfile.pax_headers + else: + pax_headers = tarfile.pax_headers.copy() + + # Check if the pax header contains a hdrcharset field. This tells us + # the encoding of the path, linkpath, uname and gname fields. Normally, + # these fields are UTF-8 encoded but since POSIX.1-2008 tar + # implementations are allowed to store them as raw binary strings if + # the translation to UTF-8 fails. + match = re.search(br"\d+ hdrcharset=([^\n]+)\n", buf) + if match is not None: + pax_headers["hdrcharset"] = match.group(1).decode("utf-8") + + # For the time being, we don't care about anything other than "BINARY". + # The only other value that is currently allowed by the standard is + # "ISO-IR 10646 2000 UTF-8" in other words UTF-8. + hdrcharset = pax_headers.get("hdrcharset") + if hdrcharset == "BINARY": + encoding = tarfile.encoding + else: + encoding = "utf-8" + + # Parse pax header information. A record looks like that: + # "%d %s=%s\n" % (length, keyword, value). length is the size + # of the complete record including the length field itself and + # the newline. keyword and value are both UTF-8 encoded strings. + regex = re.compile(br"(\d+) ([^=]+)=") + pos = 0 + while match := regex.match(buf, pos): + length, keyword = match.groups() + length = int(length) + if length == 0: + raise InvalidHeaderError("invalid header") + value = buf[match.end(2) + 1:match.start(1) + length - 1] + + # Normally, we could just use "utf-8" as the encoding and "strict" + # as the error handler, but we better not take the risk. For + # example, GNU tar <= 1.23 is known to store filenames it cannot + # translate to UTF-8 as raw strings (unfortunately without a + # hdrcharset=BINARY header). + # We first try the strict standard encoding, and if that fails we + # fall back on the user's encoding and error handler. + keyword = self._decode_pax_field(keyword, "utf-8", "utf-8", + tarfile.errors) + if keyword in PAX_NAME_FIELDS: + value = self._decode_pax_field(value, encoding, tarfile.encoding, + tarfile.errors) + else: + value = self._decode_pax_field(value, "utf-8", "utf-8", + tarfile.errors) + + pax_headers[keyword] = value + pos += length + + # Fetch the next header. + try: + next = self.fromtarfile(tarfile) + except HeaderError as e: + raise SubsequentHeaderError(str(e)) from None + + # Process GNU sparse information. + if "GNU.sparse.map" in pax_headers: + # GNU extended sparse format version 0.1. + self._proc_gnusparse_01(next, pax_headers) + + elif "GNU.sparse.size" in pax_headers: + # GNU extended sparse format version 0.0. + self._proc_gnusparse_00(next, pax_headers, buf) + + elif pax_headers.get("GNU.sparse.major") == "1" and pax_headers.get("GNU.sparse.minor") == "0": + # GNU extended sparse format version 1.0. + self._proc_gnusparse_10(next, pax_headers, tarfile) + + if self.type in (XHDTYPE, SOLARIS_XHDTYPE): + # Patch the TarInfo object with the extended header info. + next._apply_pax_info(pax_headers, tarfile.encoding, tarfile.errors) + next.offset = self.offset + + if "size" in pax_headers: + # If the extended header replaces the size field, + # we need to recalculate the offset where the next + # header starts. + offset = next.offset_data + if next.isreg() or next.type not in SUPPORTED_TYPES: + offset += next._block(next.size) + tarfile.offset = offset + + return next + + def _proc_gnusparse_00(self, next, pax_headers, buf): + """Process a GNU tar extended sparse header, version 0.0. + """ + offsets = [] + for match in re.finditer(br"\d+ GNU.sparse.offset=(\d+)\n", buf): + offsets.append(int(match.group(1))) + numbytes = [] + for match in re.finditer(br"\d+ GNU.sparse.numbytes=(\d+)\n", buf): + numbytes.append(int(match.group(1))) + next.sparse = list(zip(offsets, numbytes)) + + def _proc_gnusparse_01(self, next, pax_headers): + """Process a GNU tar extended sparse header, version 0.1. + """ + sparse = [int(x) for x in pax_headers["GNU.sparse.map"].split(",")] + next.sparse = list(zip(sparse[::2], sparse[1::2])) + + def _proc_gnusparse_10(self, next, pax_headers, tarfile): + """Process a GNU tar extended sparse header, version 1.0. + """ + fields = None + sparse = [] + buf = tarfile.fileobj.read(BLOCKSIZE) + fields, buf = buf.split(b"\n", 1) + fields = int(fields) + while len(sparse) < fields * 2: + if b"\n" not in buf: + buf += tarfile.fileobj.read(BLOCKSIZE) + number, buf = buf.split(b"\n", 1) + sparse.append(int(number)) + next.offset_data = tarfile.fileobj.tell() + next.sparse = list(zip(sparse[::2], sparse[1::2])) + + def _apply_pax_info(self, pax_headers, encoding, errors): + """Replace fields with supplemental information from a previous + pax extended or global header. + """ + for keyword, value in pax_headers.items(): + if keyword == "GNU.sparse.name": + setattr(self, "path", value) + elif keyword == "GNU.sparse.size": + setattr(self, "size", int(value)) + elif keyword == "GNU.sparse.realsize": + setattr(self, "size", int(value)) + elif keyword in PAX_FIELDS: + if keyword in PAX_NUMBER_FIELDS: + try: + value = PAX_NUMBER_FIELDS[keyword](value) + except ValueError: + value = 0 + if keyword == "path": + value = value.rstrip("/") + setattr(self, keyword, value) + + self.pax_headers = pax_headers.copy() + + def _decode_pax_field(self, value, encoding, fallback_encoding, fallback_errors): + """Decode a single field from a pax record. + """ + try: + return value.decode(encoding, "strict") + except UnicodeDecodeError: + return value.decode(fallback_encoding, fallback_errors) + + def _block(self, count): + """Round up a byte count by BLOCKSIZE and return it, + e.g. _block(834) => 1024. + """ + blocks, remainder = divmod(count, BLOCKSIZE) + if remainder: + blocks += 1 + return blocks * BLOCKSIZE + + def isreg(self): + 'Return True if the Tarinfo object is a regular file.' + return self.type in REGULAR_TYPES + + def isfile(self): + 'Return True if the Tarinfo object is a regular file.' + return self.isreg() + + def isdir(self): + 'Return True if it is a directory.' + return self.type == DIRTYPE + + def issym(self): + 'Return True if it is a symbolic link.' + return self.type == SYMTYPE + + def islnk(self): + 'Return True if it is a hard link.' + return self.type == LNKTYPE + + def ischr(self): + 'Return True if it is a character device.' + return self.type == CHRTYPE + + def isblk(self): + 'Return True if it is a block device.' + return self.type == BLKTYPE + + def isfifo(self): + 'Return True if it is a FIFO.' + return self.type == FIFOTYPE + + def issparse(self): + return self.sparse is not None + + def isdev(self): + 'Return True if it is one of character device, block device or FIFO.' + return self.type in (CHRTYPE, BLKTYPE, FIFOTYPE) +# class TarInfo + +class TarFile(object): + """The TarFile Class provides an interface to tar archives. + """ + + debug = 0 # May be set from 0 (no msgs) to 3 (all msgs) + + dereference = False # If true, add content of linked file to the + # tar file, else the link. + + ignore_zeros = False # If true, skips empty or invalid blocks and + # continues processing. + + errorlevel = 1 # If 0, fatal errors only appear in debug + # messages (if debug >= 0). If > 0, errors + # are passed to the caller as exceptions. + + format = DEFAULT_FORMAT # The format to use when creating an archive. + + encoding = ENCODING # Encoding for 8-bit character strings. + + errors = None # Error handler for unicode conversion. + + tarinfo = TarInfo # The default TarInfo class to use. + + fileobject = ExFileObject # The file-object for extractfile(). + + extraction_filter = None # The default filter for extraction. + + def __init__(self, name=None, mode="r", fileobj=None, format=None, + tarinfo=None, dereference=None, ignore_zeros=None, encoding=None, + errors="surrogateescape", pax_headers=None, debug=None, + errorlevel=None, copybufsize=None): + """Open an (uncompressed) tar archive `name'. `mode' is either 'r' to + read from an existing archive, 'a' to append data to an existing + file or 'w' to create a new file overwriting an existing one. `mode' + defaults to 'r'. + If `fileobj' is given, it is used for reading or writing data. If it + can be determined, `mode' is overridden by `fileobj's mode. + `fileobj' is not closed, when TarFile is closed. + """ + modes = {"r": "rb", "a": "r+b", "w": "wb", "x": "xb"} + if mode not in modes: + raise ValueError("mode must be 'r', 'a', 'w' or 'x'") + self.mode = mode + self._mode = modes[mode] + + if not fileobj: + if self.mode == "a" and not os.path.exists(name): + # Create nonexistent files in append mode. + self.mode = "w" + self._mode = "wb" + fileobj = bltn_open(name, self._mode) + self._extfileobj = False + else: + if (name is None and hasattr(fileobj, "name") and + isinstance(fileobj.name, (str, bytes))): + name = fileobj.name + if hasattr(fileobj, "mode"): + self._mode = fileobj.mode + self._extfileobj = True + self.name = os.path.abspath(name) if name else None + self.fileobj = fileobj + + # Init attributes. + if format is not None: + self.format = format + if tarinfo is not None: + self.tarinfo = tarinfo + if dereference is not None: + self.dereference = dereference + if ignore_zeros is not None: + self.ignore_zeros = ignore_zeros + if encoding is not None: + self.encoding = encoding + self.errors = errors + + if pax_headers is not None and self.format == PAX_FORMAT: + self.pax_headers = pax_headers + else: + self.pax_headers = {} + + if debug is not None: + self.debug = debug + if errorlevel is not None: + self.errorlevel = errorlevel + + # Init datastructures. + self.copybufsize = copybufsize + self.closed = False + self.members = [] # list of members as TarInfo objects + self._loaded = False # flag if all members have been read + self.offset = self.fileobj.tell() + # current position in the archive file + self.inodes = {} # dictionary caching the inodes of + # archive members already added + + try: + if self.mode == "r": + self.firstmember = None + self.firstmember = self.next() + + if self.mode == "a": + # Move to the end of the archive, + # before the first empty block. + while True: + self.fileobj.seek(self.offset) + try: + tarinfo = self.tarinfo.fromtarfile(self) + self.members.append(tarinfo) + except EOFHeaderError: + self.fileobj.seek(self.offset) + break + except HeaderError as e: + raise ReadError(str(e)) from None + + if self.mode in ("a", "w", "x"): + self._loaded = True + + if self.pax_headers: + buf = self.tarinfo.create_pax_global_header(self.pax_headers.copy()) + self.fileobj.write(buf) + self.offset += len(buf) + except: + if not self._extfileobj: + self.fileobj.close() + self.closed = True + raise + + #-------------------------------------------------------------------------- + # Below are the classmethods which act as alternate constructors to the + # TarFile class. The open() method is the only one that is needed for + # public use; it is the "super"-constructor and is able to select an + # adequate "sub"-constructor for a particular compression using the mapping + # from OPEN_METH. + # + # This concept allows one to subclass TarFile without losing the comfort of + # the super-constructor. A sub-constructor is registered and made available + # by adding it to the mapping in OPEN_METH. + + @classmethod + def open(cls, name=None, mode="r", fileobj=None, bufsize=RECORDSIZE, **kwargs): + r"""Open a tar archive for reading, writing or appending. Return + an appropriate TarFile class. + + mode: + 'r' or 'r:\*' open for reading with transparent compression + 'r:' open for reading exclusively uncompressed + 'r:gz' open for reading with gzip compression + 'r:bz2' open for reading with bzip2 compression + 'r:xz' open for reading with lzma compression + 'a' or 'a:' open for appending, creating the file if necessary + 'w' or 'w:' open for writing without compression + 'w:gz' open for writing with gzip compression + 'w:bz2' open for writing with bzip2 compression + 'w:xz' open for writing with lzma compression + + 'x' or 'x:' create a tarfile exclusively without compression, raise + an exception if the file is already created + 'x:gz' create a gzip compressed tarfile, raise an exception + if the file is already created + 'x:bz2' create a bzip2 compressed tarfile, raise an exception + if the file is already created + 'x:xz' create an lzma compressed tarfile, raise an exception + if the file is already created + + 'r|\*' open a stream of tar blocks with transparent compression + 'r|' open an uncompressed stream of tar blocks for reading + 'r|gz' open a gzip compressed stream of tar blocks + 'r|bz2' open a bzip2 compressed stream of tar blocks + 'r|xz' open an lzma compressed stream of tar blocks + 'w|' open an uncompressed stream for writing + 'w|gz' open a gzip compressed stream for writing + 'w|bz2' open a bzip2 compressed stream for writing + 'w|xz' open an lzma compressed stream for writing + """ + + if not name and not fileobj: + raise ValueError("nothing to open") + + if mode in ("r", "r:*"): + # Find out which *open() is appropriate for opening the file. + def not_compressed(comptype): + return cls.OPEN_METH[comptype] == 'taropen' + error_msgs = [] + for comptype in sorted(cls.OPEN_METH, key=not_compressed): + func = getattr(cls, cls.OPEN_METH[comptype]) + if fileobj is not None: + saved_pos = fileobj.tell() + try: + return func(name, "r", fileobj, **kwargs) + except (ReadError, CompressionError) as e: + error_msgs.append(f'- method {comptype}: {e!r}') + if fileobj is not None: + fileobj.seek(saved_pos) + continue + error_msgs_summary = '\n'.join(error_msgs) + raise ReadError(f"file could not be opened successfully:\n{error_msgs_summary}") + + elif ":" in mode: + filemode, comptype = mode.split(":", 1) + filemode = filemode or "r" + comptype = comptype or "tar" + + # Select the *open() function according to + # given compression. + if comptype in cls.OPEN_METH: + func = getattr(cls, cls.OPEN_METH[comptype]) + else: + raise CompressionError("unknown compression type %r" % comptype) + return func(name, filemode, fileobj, **kwargs) + + elif "|" in mode: + filemode, comptype = mode.split("|", 1) + filemode = filemode or "r" + comptype = comptype or "tar" + + if filemode not in ("r", "w"): + raise ValueError("mode must be 'r' or 'w'") + + compresslevel = kwargs.pop("compresslevel", 9) + stream = _Stream(name, filemode, comptype, fileobj, bufsize, + compresslevel) + try: + t = cls(name, filemode, stream, **kwargs) + except: + stream.close() + raise + t._extfileobj = False + return t + + elif mode in ("a", "w", "x"): + return cls.taropen(name, mode, fileobj, **kwargs) + + raise ValueError("undiscernible mode") + + @classmethod + def taropen(cls, name, mode="r", fileobj=None, **kwargs): + """Open uncompressed tar archive name for reading or writing. + """ + if mode not in ("r", "a", "w", "x"): + raise ValueError("mode must be 'r', 'a', 'w' or 'x'") + return cls(name, mode, fileobj, **kwargs) + + @classmethod + def gzopen(cls, name, mode="r", fileobj=None, compresslevel=9, **kwargs): + """Open gzip compressed tar archive name for reading or writing. + Appending is not allowed. + """ + if mode not in ("r", "w", "x"): + raise ValueError("mode must be 'r', 'w' or 'x'") + + try: + from gzip import GzipFile + except ImportError: + raise CompressionError("gzip module is not available") from None + + try: + fileobj = GzipFile(name, mode + "b", compresslevel, fileobj) + except OSError as e: + if fileobj is not None and mode == 'r': + raise ReadError("not a gzip file") from e + raise + + try: + t = cls.taropen(name, mode, fileobj, **kwargs) + except OSError as e: + fileobj.close() + if mode == 'r': + raise ReadError("not a gzip file") from e + raise + except: + fileobj.close() + raise + t._extfileobj = False + return t + + @classmethod + def bz2open(cls, name, mode="r", fileobj=None, compresslevel=9, **kwargs): + """Open bzip2 compressed tar archive name for reading or writing. + Appending is not allowed. + """ + if mode not in ("r", "w", "x"): + raise ValueError("mode must be 'r', 'w' or 'x'") + + try: + from bz2 import BZ2File + except ImportError: + raise CompressionError("bz2 module is not available") from None + + fileobj = BZ2File(fileobj or name, mode, compresslevel=compresslevel) + + try: + t = cls.taropen(name, mode, fileobj, **kwargs) + except (OSError, EOFError) as e: + fileobj.close() + if mode == 'r': + raise ReadError("not a bzip2 file") from e + raise + except: + fileobj.close() + raise + t._extfileobj = False + return t + + @classmethod + def xzopen(cls, name, mode="r", fileobj=None, preset=None, **kwargs): + """Open lzma compressed tar archive name for reading or writing. + Appending is not allowed. + """ + if mode not in ("r", "w", "x"): + raise ValueError("mode must be 'r', 'w' or 'x'") + + try: + from lzma import LZMAFile, LZMAError + except ImportError: + raise CompressionError("lzma module is not available") from None + + fileobj = LZMAFile(fileobj or name, mode, preset=preset) + + try: + t = cls.taropen(name, mode, fileobj, **kwargs) + except (LZMAError, EOFError) as e: + fileobj.close() + if mode == 'r': + raise ReadError("not an lzma file") from e + raise + except: + fileobj.close() + raise + t._extfileobj = False + return t + + # All *open() methods are registered here. + OPEN_METH = { + "tar": "taropen", # uncompressed tar + "gz": "gzopen", # gzip compressed tar + "bz2": "bz2open", # bzip2 compressed tar + "xz": "xzopen" # lzma compressed tar + } + + #-------------------------------------------------------------------------- + # The public methods which TarFile provides: + + def close(self): + """Close the TarFile. In write-mode, two finishing zero blocks are + appended to the archive. + """ + if self.closed: + return + + self.closed = True + try: + if self.mode in ("a", "w", "x"): + self.fileobj.write(NUL * (BLOCKSIZE * 2)) + self.offset += (BLOCKSIZE * 2) + # fill up the end with zero-blocks + # (like option -b20 for tar does) + blocks, remainder = divmod(self.offset, RECORDSIZE) + if remainder > 0: + self.fileobj.write(NUL * (RECORDSIZE - remainder)) + finally: + if not self._extfileobj: + self.fileobj.close() + + def getmember(self, name): + """Return a TarInfo object for member ``name``. If ``name`` can not be + found in the archive, KeyError is raised. If a member occurs more + than once in the archive, its last occurrence is assumed to be the + most up-to-date version. + """ + tarinfo = self._getmember(name.rstrip('/')) + if tarinfo is None: + raise KeyError("filename %r not found" % name) + return tarinfo + + def getmembers(self): + """Return the members of the archive as a list of TarInfo objects. The + list has the same order as the members in the archive. + """ + self._check() + if not self._loaded: # if we want to obtain a list of + self._load() # all members, we first have to + # scan the whole archive. + return self.members + + def getnames(self): + """Return the members of the archive as a list of their names. It has + the same order as the list returned by getmembers(). + """ + return [tarinfo.name for tarinfo in self.getmembers()] + + def gettarinfo(self, name=None, arcname=None, fileobj=None): + """Create a TarInfo object from the result of os.stat or equivalent + on an existing file. The file is either named by ``name``, or + specified as a file object ``fileobj`` with a file descriptor. If + given, ``arcname`` specifies an alternative name for the file in the + archive, otherwise, the name is taken from the 'name' attribute of + 'fileobj', or the 'name' argument. The name should be a text + string. + """ + self._check("awx") + + # When fileobj is given, replace name by + # fileobj's real name. + if fileobj is not None: + name = fileobj.name + + # Building the name of the member in the archive. + # Backward slashes are converted to forward slashes, + # Absolute paths are turned to relative paths. + if arcname is None: + arcname = name + drv, arcname = os.path.splitdrive(arcname) + arcname = arcname.replace(os.sep, "/") + arcname = arcname.lstrip("/") + + # Now, fill the TarInfo object with + # information specific for the file. + tarinfo = self.tarinfo() + tarinfo.tarfile = self # Not needed + + # Use os.stat or os.lstat, depending on if symlinks shall be resolved. + if fileobj is None: + if not self.dereference: + statres = os.lstat(name) + else: + statres = os.stat(name) + else: + statres = os.fstat(fileobj.fileno()) + linkname = "" + + stmd = statres.st_mode + if stat.S_ISREG(stmd): + inode = (statres.st_ino, statres.st_dev) + if not self.dereference and statres.st_nlink > 1 and \ + inode in self.inodes and arcname != self.inodes[inode]: + # Is it a hardlink to an already + # archived file? + type = LNKTYPE + linkname = self.inodes[inode] + else: + # The inode is added only if its valid. + # For win32 it is always 0. + type = REGTYPE + if inode[0]: + self.inodes[inode] = arcname + elif stat.S_ISDIR(stmd): + type = DIRTYPE + elif stat.S_ISFIFO(stmd): + type = FIFOTYPE + elif stat.S_ISLNK(stmd): + type = SYMTYPE + linkname = os.readlink(name) + elif stat.S_ISCHR(stmd): + type = CHRTYPE + elif stat.S_ISBLK(stmd): + type = BLKTYPE + else: + return None + + # Fill the TarInfo object with all + # information we can get. + tarinfo.name = arcname + tarinfo.mode = stmd + tarinfo.uid = statres.st_uid + tarinfo.gid = statres.st_gid + if type == REGTYPE: + tarinfo.size = statres.st_size + else: + tarinfo.size = 0 + tarinfo.mtime = statres.st_mtime + tarinfo.type = type + tarinfo.linkname = linkname + if pwd: + try: + tarinfo.uname = pwd.getpwuid(tarinfo.uid)[0] + except KeyError: + pass + if grp: + try: + tarinfo.gname = grp.getgrgid(tarinfo.gid)[0] + except KeyError: + pass + + if type in (CHRTYPE, BLKTYPE): + if hasattr(os, "major") and hasattr(os, "minor"): + tarinfo.devmajor = os.major(statres.st_rdev) + tarinfo.devminor = os.minor(statres.st_rdev) + return tarinfo + + def list(self, verbose=True, *, members=None): + """Print a table of contents to sys.stdout. If ``verbose`` is False, only + the names of the members are printed. If it is True, an `ls -l'-like + output is produced. ``members`` is optional and must be a subset of the + list returned by getmembers(). + """ + self._check() + + if members is None: + members = self + for tarinfo in members: + if verbose: + if tarinfo.mode is None: + _safe_print("??????????") + else: + _safe_print(stat.filemode(tarinfo.mode)) + _safe_print("%s/%s" % (tarinfo.uname or tarinfo.uid, + tarinfo.gname or tarinfo.gid)) + if tarinfo.ischr() or tarinfo.isblk(): + _safe_print("%10s" % + ("%d,%d" % (tarinfo.devmajor, tarinfo.devminor))) + else: + _safe_print("%10d" % tarinfo.size) + if tarinfo.mtime is None: + _safe_print("????-??-?? ??:??:??") + else: + _safe_print("%d-%02d-%02d %02d:%02d:%02d" \ + % time.localtime(tarinfo.mtime)[:6]) + + _safe_print(tarinfo.name + ("/" if tarinfo.isdir() else "")) + + if verbose: + if tarinfo.issym(): + _safe_print("-> " + tarinfo.linkname) + if tarinfo.islnk(): + _safe_print("link to " + tarinfo.linkname) + print() + + def add(self, name, arcname=None, recursive=True, *, filter=None): + """Add the file ``name`` to the archive. ``name`` may be any type of file + (directory, fifo, symbolic link, etc.). If given, ``arcname`` + specifies an alternative name for the file in the archive. + Directories are added recursively by default. This can be avoided by + setting ``recursive`` to False. ``filter`` is a function + that expects a TarInfo object argument and returns the changed + TarInfo object, if it returns None the TarInfo object will be + excluded from the archive. + """ + self._check("awx") + + if arcname is None: + arcname = name + + # Skip if somebody tries to archive the archive... + if self.name is not None and os.path.abspath(name) == self.name: + self._dbg(2, "tarfile: Skipped %r" % name) + return + + self._dbg(1, name) + + # Create a TarInfo object from the file. + tarinfo = self.gettarinfo(name, arcname) + + if tarinfo is None: + self._dbg(1, "tarfile: Unsupported type %r" % name) + return + + # Change or exclude the TarInfo object. + if filter is not None: + tarinfo = filter(tarinfo) + if tarinfo is None: + self._dbg(2, "tarfile: Excluded %r" % name) + return + + # Append the tar header and data to the archive. + if tarinfo.isreg(): + with bltn_open(name, "rb") as f: + self.addfile(tarinfo, f) + + elif tarinfo.isdir(): + self.addfile(tarinfo) + if recursive: + for f in sorted(os.listdir(name)): + self.add(os.path.join(name, f), os.path.join(arcname, f), + recursive, filter=filter) + + else: + self.addfile(tarinfo) + + def addfile(self, tarinfo, fileobj=None): + """Add the TarInfo object ``tarinfo`` to the archive. If ``fileobj`` is + given, it should be a binary file, and tarinfo.size bytes are read + from it and added to the archive. You can create TarInfo objects + directly, or by using gettarinfo(). + """ + self._check("awx") + + tarinfo = copy.copy(tarinfo) + + buf = tarinfo.tobuf(self.format, self.encoding, self.errors) + self.fileobj.write(buf) + self.offset += len(buf) + bufsize=self.copybufsize + # If there's data to follow, append it. + if fileobj is not None: + copyfileobj(fileobj, self.fileobj, tarinfo.size, bufsize=bufsize) + blocks, remainder = divmod(tarinfo.size, BLOCKSIZE) + if remainder > 0: + self.fileobj.write(NUL * (BLOCKSIZE - remainder)) + blocks += 1 + self.offset += blocks * BLOCKSIZE + + self.members.append(tarinfo) + + def _get_filter_function(self, filter): + if filter is None: + filter = self.extraction_filter + if filter is None: + warnings.warn( + 'Python 3.14 will, by default, filter extracted tar ' + + 'archives and reject files or modify their metadata. ' + + 'Use the filter argument to control this behavior.', + DeprecationWarning) + return fully_trusted_filter + if isinstance(filter, str): + raise TypeError( + 'String names are not supported for ' + + 'TarFile.extraction_filter. Use a function such as ' + + 'tarfile.data_filter directly.') + return filter + if callable(filter): + return filter + try: + return _NAMED_FILTERS[filter] + except KeyError: + raise ValueError(f"filter {filter!r} not found") from None + + def extractall(self, path=".", members=None, *, numeric_owner=False, + filter=None): + """Extract all members from the archive to the current working + directory and set owner, modification time and permissions on + directories afterwards. `path' specifies a different directory + to extract to. `members' is optional and must be a subset of the + list returned by getmembers(). If `numeric_owner` is True, only + the numbers for user/group names are used and not the names. + + The `filter` function will be called on each member just + before extraction. + It can return a changed TarInfo or None to skip the member. + String names of common filters are accepted. + """ + directories = [] + + filter_function = self._get_filter_function(filter) + if members is None: + members = self + + for member in members: + tarinfo = self._get_extract_tarinfo(member, filter_function, path) + if tarinfo is None: + continue + if tarinfo.isdir(): + # For directories, delay setting attributes until later, + # since permissions can interfere with extraction and + # extracting contents can reset mtime. + directories.append(tarinfo) + self._extract_one(tarinfo, path, set_attrs=not tarinfo.isdir(), + numeric_owner=numeric_owner) + + # Reverse sort directories. + directories.sort(key=lambda a: a.name, reverse=True) + + # Set correct owner, mtime and filemode on directories. + for tarinfo in directories: + dirpath = os.path.join(path, tarinfo.name) + try: + self.chown(tarinfo, dirpath, numeric_owner=numeric_owner) + self.utime(tarinfo, dirpath) + self.chmod(tarinfo, dirpath) + except ExtractError as e: + self._handle_nonfatal_error(e) + + def extract(self, member, path="", set_attrs=True, *, numeric_owner=False, + filter=None): + """Extract a member from the archive to the current working directory, + using its full name. Its file information is extracted as accurately + as possible. `member' may be a filename or a TarInfo object. You can + specify a different directory using `path'. File attributes (owner, + mtime, mode) are set unless `set_attrs' is False. If `numeric_owner` + is True, only the numbers for user/group names are used and not + the names. + + The `filter` function will be called before extraction. + It can return a changed TarInfo or None to skip the member. + String names of common filters are accepted. + """ + filter_function = self._get_filter_function(filter) + tarinfo = self._get_extract_tarinfo(member, filter_function, path) + if tarinfo is not None: + self._extract_one(tarinfo, path, set_attrs, numeric_owner) + + def _get_extract_tarinfo(self, member, filter_function, path): + """Get filtered TarInfo (or None) from member, which might be a str""" + if isinstance(member, str): + tarinfo = self.getmember(member) + else: + tarinfo = member + + unfiltered = tarinfo + try: + tarinfo = filter_function(tarinfo, path) + except (OSError, FilterError) as e: + self._handle_fatal_error(e) + except ExtractError as e: + self._handle_nonfatal_error(e) + if tarinfo is None: + self._dbg(2, "tarfile: Excluded %r" % unfiltered.name) + return None + # Prepare the link target for makelink(). + if tarinfo.islnk(): + tarinfo = copy.copy(tarinfo) + tarinfo._link_target = os.path.join(path, tarinfo.linkname) + return tarinfo + + def _extract_one(self, tarinfo, path, set_attrs, numeric_owner): + """Extract from filtered tarinfo to disk""" + self._check("r") + + try: + self._extract_member(tarinfo, os.path.join(path, tarinfo.name), + set_attrs=set_attrs, + numeric_owner=numeric_owner) + except OSError as e: + self._handle_fatal_error(e) + except ExtractError as e: + self._handle_nonfatal_error(e) + + def _handle_nonfatal_error(self, e): + """Handle non-fatal error (ExtractError) according to errorlevel""" + if self.errorlevel > 1: + raise + else: + self._dbg(1, "tarfile: %s" % e) + + def _handle_fatal_error(self, e): + """Handle "fatal" error according to self.errorlevel""" + if self.errorlevel > 0: + raise + elif isinstance(e, OSError): + if e.filename is None: + self._dbg(1, "tarfile: %s" % e.strerror) + else: + self._dbg(1, "tarfile: %s %r" % (e.strerror, e.filename)) + else: + self._dbg(1, "tarfile: %s %s" % (type(e).__name__, e)) + + def extractfile(self, member): + """Extract a member from the archive as a file object. ``member`` may be + a filename or a TarInfo object. If ``member`` is a regular file or + a link, an io.BufferedReader object is returned. For all other + existing members, None is returned. If ``member`` does not appear + in the archive, KeyError is raised. + """ + self._check("r") + + if isinstance(member, str): + tarinfo = self.getmember(member) + else: + tarinfo = member + + if tarinfo.isreg() or tarinfo.type not in SUPPORTED_TYPES: + # Members with unknown types are treated as regular files. + return self.fileobject(self, tarinfo) + + elif tarinfo.islnk() or tarinfo.issym(): + if isinstance(self.fileobj, _Stream): + # A small but ugly workaround for the case that someone tries + # to extract a (sym)link as a file-object from a non-seekable + # stream of tar blocks. + raise StreamError("cannot extract (sym)link as file object") + else: + # A (sym)link's file object is its target's file object. + return self.extractfile(self._find_link_target(tarinfo)) + else: + # If there's no data associated with the member (directory, chrdev, + # blkdev, etc.), return None instead of a file object. + return None + + def _extract_member(self, tarinfo, targetpath, set_attrs=True, + numeric_owner=False): + """Extract the TarInfo object tarinfo to a physical + file called targetpath. + """ + # Fetch the TarInfo object for the given name + # and build the destination pathname, replacing + # forward slashes to platform specific separators. + targetpath = targetpath.rstrip("/") + targetpath = targetpath.replace("/", os.sep) + + # Create all upper directories. + upperdirs = os.path.dirname(targetpath) + if upperdirs and not os.path.exists(upperdirs): + # Create directories that are not part of the archive with + # default permissions. + os.makedirs(upperdirs) + + if tarinfo.islnk() or tarinfo.issym(): + self._dbg(1, "%s -> %s" % (tarinfo.name, tarinfo.linkname)) + else: + self._dbg(1, tarinfo.name) + + if tarinfo.isreg(): + self.makefile(tarinfo, targetpath) + elif tarinfo.isdir(): + self.makedir(tarinfo, targetpath) + elif tarinfo.isfifo(): + self.makefifo(tarinfo, targetpath) + elif tarinfo.ischr() or tarinfo.isblk(): + self.makedev(tarinfo, targetpath) + elif tarinfo.islnk() or tarinfo.issym(): + self.makelink(tarinfo, targetpath) + elif tarinfo.type not in SUPPORTED_TYPES: + self.makeunknown(tarinfo, targetpath) + else: + self.makefile(tarinfo, targetpath) + + if set_attrs: + self.chown(tarinfo, targetpath, numeric_owner) + if not tarinfo.issym(): + self.chmod(tarinfo, targetpath) + self.utime(tarinfo, targetpath) + + #-------------------------------------------------------------------------- + # Below are the different file methods. They are called via + # _extract_member() when extract() is called. They can be replaced in a + # subclass to implement other functionality. + + def makedir(self, tarinfo, targetpath): + """Make a directory called targetpath. + """ + try: + if tarinfo.mode is None: + # Use the system's default mode + os.mkdir(targetpath) + else: + # Use a safe mode for the directory, the real mode is set + # later in _extract_member(). + os.mkdir(targetpath, 0o700) + except FileExistsError: + if not os.path.isdir(targetpath): + raise + + def makefile(self, tarinfo, targetpath): + """Make a file called targetpath. + """ + source = self.fileobj + source.seek(tarinfo.offset_data) + bufsize = self.copybufsize + with bltn_open(targetpath, "wb") as target: + if tarinfo.sparse is not None: + for offset, size in tarinfo.sparse: + target.seek(offset) + copyfileobj(source, target, size, ReadError, bufsize) + target.seek(tarinfo.size) + target.truncate() + else: + copyfileobj(source, target, tarinfo.size, ReadError, bufsize) + + def makeunknown(self, tarinfo, targetpath): + """Make a file from a TarInfo object with an unknown type + at targetpath. + """ + self.makefile(tarinfo, targetpath) + self._dbg(1, "tarfile: Unknown file type %r, " \ + "extracted as regular file." % tarinfo.type) + + def makefifo(self, tarinfo, targetpath): + """Make a fifo called targetpath. + """ + if hasattr(os, "mkfifo"): + os.mkfifo(targetpath) + else: + raise ExtractError("fifo not supported by system") + + def makedev(self, tarinfo, targetpath): + """Make a character or block device called targetpath. + """ + if not hasattr(os, "mknod") or not hasattr(os, "makedev"): + raise ExtractError("special devices not supported by system") + + mode = tarinfo.mode + if mode is None: + # Use mknod's default + mode = 0o600 + if tarinfo.isblk(): + mode |= stat.S_IFBLK + else: + mode |= stat.S_IFCHR + + os.mknod(targetpath, mode, + os.makedev(tarinfo.devmajor, tarinfo.devminor)) + + def makelink(self, tarinfo, targetpath): + """Make a (symbolic) link called targetpath. If it cannot be created + (platform limitation), we try to make a copy of the referenced file + instead of a link. + """ + try: + # For systems that support symbolic and hard links. + if tarinfo.issym(): + if os.path.lexists(targetpath): + # Avoid FileExistsError on following os.symlink. + os.unlink(targetpath) + os.symlink(tarinfo.linkname, targetpath) + else: + if os.path.exists(tarinfo._link_target): + os.link(tarinfo._link_target, targetpath) + else: + self._extract_member(self._find_link_target(tarinfo), + targetpath) + except symlink_exception: + try: + self._extract_member(self._find_link_target(tarinfo), + targetpath) + except KeyError: + raise ExtractError("unable to resolve link inside archive") from None + + def chown(self, tarinfo, targetpath, numeric_owner): + """Set owner of targetpath according to tarinfo. If numeric_owner + is True, use .gid/.uid instead of .gname/.uname. If numeric_owner + is False, fall back to .gid/.uid when the search based on name + fails. + """ + if hasattr(os, "geteuid") and os.geteuid() == 0: + # We have to be root to do so. + g = tarinfo.gid + u = tarinfo.uid + if not numeric_owner: + try: + if grp and tarinfo.gname: + g = grp.getgrnam(tarinfo.gname)[2] + except KeyError: + pass + try: + if pwd and tarinfo.uname: + u = pwd.getpwnam(tarinfo.uname)[2] + except KeyError: + pass + if g is None: + g = -1 + if u is None: + u = -1 + try: + if tarinfo.issym() and hasattr(os, "lchown"): + os.lchown(targetpath, u, g) + else: + os.chown(targetpath, u, g) + except OSError as e: + raise ExtractError("could not change owner") from e + + def chmod(self, tarinfo, targetpath): + """Set file permissions of targetpath according to tarinfo. + """ + if tarinfo.mode is None: + return + try: + os.chmod(targetpath, tarinfo.mode) + except OSError as e: + raise ExtractError("could not change mode") from e + + def utime(self, tarinfo, targetpath): + """Set modification time of targetpath according to tarinfo. + """ + mtime = tarinfo.mtime + if mtime is None: + return + if not hasattr(os, 'utime'): + return + try: + os.utime(targetpath, (mtime, mtime)) + except OSError as e: + raise ExtractError("could not change modification time") from e + + #-------------------------------------------------------------------------- + def next(self): + """Return the next member of the archive as a TarInfo object, when + TarFile is opened for reading. Return None if there is no more + available. + """ + self._check("ra") + if self.firstmember is not None: + m = self.firstmember + self.firstmember = None + return m + + # Advance the file pointer. + if self.offset != self.fileobj.tell(): + if self.offset == 0: + return None + self.fileobj.seek(self.offset - 1) + if not self.fileobj.read(1): + raise ReadError("unexpected end of data") + + # Read the next block. + tarinfo = None + while True: + try: + tarinfo = self.tarinfo.fromtarfile(self) + except EOFHeaderError as e: + if self.ignore_zeros: + self._dbg(2, "0x%X: %s" % (self.offset, e)) + self.offset += BLOCKSIZE + continue + except InvalidHeaderError as e: + if self.ignore_zeros: + self._dbg(2, "0x%X: %s" % (self.offset, e)) + self.offset += BLOCKSIZE + continue + elif self.offset == 0: + raise ReadError(str(e)) from None + except EmptyHeaderError: + if self.offset == 0: + raise ReadError("empty file") from None + except TruncatedHeaderError as e: + if self.offset == 0: + raise ReadError(str(e)) from None + except SubsequentHeaderError as e: + raise ReadError(str(e)) from None + except Exception as e: + try: + import zlib + if isinstance(e, zlib.error): + raise ReadError(f'zlib error: {e}') from None + else: + raise e + except ImportError: + raise e + break + + if tarinfo is not None: + self.members.append(tarinfo) + else: + self._loaded = True + + return tarinfo + + #-------------------------------------------------------------------------- + # Little helper methods: + + def _getmember(self, name, tarinfo=None, normalize=False): + """Find an archive member by name from bottom to top. + If tarinfo is given, it is used as the starting point. + """ + # Ensure that all members have been loaded. + members = self.getmembers() + + # Limit the member search list up to tarinfo. + skipping = False + if tarinfo is not None: + try: + index = members.index(tarinfo) + except ValueError: + # The given starting point might be a (modified) copy. + # We'll later skip members until we find an equivalent. + skipping = True + else: + # Happy fast path + members = members[:index] + + if normalize: + name = os.path.normpath(name) + + for member in reversed(members): + if skipping: + if tarinfo.offset == member.offset: + skipping = False + continue + if normalize: + member_name = os.path.normpath(member.name) + else: + member_name = member.name + + if name == member_name: + return member + + if skipping: + # Starting point was not found + raise ValueError(tarinfo) + + def _load(self): + """Read through the entire archive file and look for readable + members. + """ + while self.next() is not None: + pass + self._loaded = True + + def _check(self, mode=None): + """Check if TarFile is still open, and if the operation's mode + corresponds to TarFile's mode. + """ + if self.closed: + raise OSError("%s is closed" % self.__class__.__name__) + if mode is not None and self.mode not in mode: + raise OSError("bad operation for mode %r" % self.mode) + + def _find_link_target(self, tarinfo): + """Find the target member of a symlink or hardlink member in the + archive. + """ + if tarinfo.issym(): + # Always search the entire archive. + linkname = "/".join(filter(None, (os.path.dirname(tarinfo.name), tarinfo.linkname))) + limit = None + else: + # Search the archive before the link, because a hard link is + # just a reference to an already archived file. + linkname = tarinfo.linkname + limit = tarinfo + + member = self._getmember(linkname, tarinfo=limit, normalize=True) + if member is None: + raise KeyError("linkname %r not found" % linkname) + return member + + def __iter__(self): + """Provide an iterator object. + """ + if self._loaded: + yield from self.members + return + + # Yield items using TarFile's next() method. + # When all members have been read, set TarFile as _loaded. + index = 0 + # Fix for SF #1100429: Under rare circumstances it can + # happen that getmembers() is called during iteration, + # which will have already exhausted the next() method. + if self.firstmember is not None: + tarinfo = self.next() + index += 1 + yield tarinfo + + while True: + if index < len(self.members): + tarinfo = self.members[index] + elif not self._loaded: + tarinfo = self.next() + if not tarinfo: + self._loaded = True + return + else: + return + index += 1 + yield tarinfo + + def _dbg(self, level, msg): + """Write debugging output to sys.stderr. + """ + if level <= self.debug: + print(msg, file=sys.stderr) + + def __enter__(self): + self._check() + return self + + def __exit__(self, type, value, traceback): + if type is None: + self.close() + else: + # An exception occurred. We must not call close() because + # it would try to write end-of-archive blocks and padding. + if not self._extfileobj: + self.fileobj.close() + self.closed = True + +#-------------------- +# exported functions +#-------------------- + +def is_tarfile(name): + """Return True if name points to a tar archive that we + are able to handle, else return False. + + 'name' should be a string, file, or file-like object. + """ + try: + if hasattr(name, "read"): + pos = name.tell() + t = open(fileobj=name) + name.seek(pos) + else: + t = open(name) + t.close() + return True + except TarError: + return False + +open = TarFile.open + + +def main(): + import argparse + + description = 'A simple command-line interface for tarfile module.' + parser = argparse.ArgumentParser(description=description) + parser.add_argument('-v', '--verbose', action='store_true', default=False, + help='Verbose output') + parser.add_argument('--filter', metavar='', + choices=_NAMED_FILTERS, + help='Filter for extraction') + + group = parser.add_mutually_exclusive_group(required=True) + group.add_argument('-l', '--list', metavar='', + help='Show listing of a tarfile') + group.add_argument('-e', '--extract', nargs='+', + metavar=('', ''), + help='Extract tarfile into target dir') + group.add_argument('-c', '--create', nargs='+', + metavar=('', ''), + help='Create tarfile from sources') + group.add_argument('-t', '--test', metavar='', + help='Test if a tarfile is valid') + + args = parser.parse_args() + + if args.filter and args.extract is None: + parser.exit(1, '--filter is only valid for extraction\n') + + if args.test is not None: + src = args.test + if is_tarfile(src): + with open(src, 'r') as tar: + tar.getmembers() + print(tar.getmembers(), file=sys.stderr) + if args.verbose: + print('{!r} is a tar archive.'.format(src)) + else: + parser.exit(1, '{!r} is not a tar archive.\n'.format(src)) + + elif args.list is not None: + src = args.list + if is_tarfile(src): + with TarFile.open(src, 'r:*') as tf: + tf.list(verbose=args.verbose) + else: + parser.exit(1, '{!r} is not a tar archive.\n'.format(src)) + + elif args.extract is not None: + if len(args.extract) == 1: + src = args.extract[0] + curdir = os.curdir + elif len(args.extract) == 2: + src, curdir = args.extract + else: + parser.exit(1, parser.format_help()) + + if is_tarfile(src): + with TarFile.open(src, 'r:*') as tf: + tf.extractall(path=curdir, filter=args.filter) + if args.verbose: + if curdir == '.': + msg = '{!r} file is extracted.'.format(src) + else: + msg = ('{!r} file is extracted ' + 'into {!r} directory.').format(src, curdir) + print(msg) + else: + parser.exit(1, '{!r} is not a tar archive.\n'.format(src)) + + elif args.create is not None: + tar_name = args.create.pop(0) + _, ext = os.path.splitext(tar_name) + compressions = { + # gz + '.gz': 'gz', + '.tgz': 'gz', + # xz + '.xz': 'xz', + '.txz': 'xz', + # bz2 + '.bz2': 'bz2', + '.tbz': 'bz2', + '.tbz2': 'bz2', + '.tb2': 'bz2', + } + tar_mode = 'w:' + compressions[ext] if ext in compressions else 'w' + tar_files = args.create + + with TarFile.open(tar_name, tar_mode) as tf: + for file_name in tar_files: + tf.add(file_name) + + if args.verbose: + print('{!r} file created.'.format(tar_name)) + +if __name__ == '__main__': + main() diff --git a/lib/pkg_resources/_vendor/jaraco/context.py b/lib/pkg_resources/_vendor/jaraco/context.py index b0d1ef37..c42f6135 100644 --- a/lib/pkg_resources/_vendor/jaraco/context.py +++ b/lib/pkg_resources/_vendor/jaraco/context.py @@ -1,15 +1,26 @@ -import os -import subprocess +from __future__ import annotations + import contextlib import functools -import tempfile -import shutil import operator +import os +import shutil +import subprocess +import sys +import tempfile +import urllib.request import warnings +from typing import Iterator + + +if sys.version_info < (3, 12): + from pkg_resources.extern.backports import tarfile +else: + import tarfile @contextlib.contextmanager -def pushd(dir): +def pushd(dir: str | os.PathLike) -> Iterator[str | os.PathLike]: """ >>> tmp_path = getfixture('tmp_path') >>> with pushd(tmp_path): @@ -26,33 +37,88 @@ def pushd(dir): @contextlib.contextmanager -def tarball_context(url, target_dir=None, runner=None, pushd=pushd): +def tarball( + url, target_dir: str | os.PathLike | None = None +) -> Iterator[str | os.PathLike]: """ - Get a tarball, extract it, change to that directory, yield, then - clean up. - `runner` is the function to invoke commands. - `pushd` is a context manager for changing the directory. + Get a tarball, extract it, yield, then clean up. + + >>> import urllib.request + >>> url = getfixture('tarfile_served') + >>> target = getfixture('tmp_path') / 'out' + >>> tb = tarball(url, target_dir=target) + >>> import pathlib + >>> with tb as extracted: + ... contents = pathlib.Path(extracted, 'contents.txt').read_text(encoding='utf-8') + >>> assert not os.path.exists(extracted) """ if target_dir is None: target_dir = os.path.basename(url).replace('.tar.gz', '').replace('.tgz', '') - if runner is None: - runner = functools.partial(subprocess.check_call, shell=True) - else: - warnings.warn("runner parameter is deprecated", DeprecationWarning) # In the tar command, use --strip-components=1 to strip the first path and # then # use -C to cause the files to be extracted to {target_dir}. This ensures # that we always know where the files were extracted. - runner('mkdir {target_dir}'.format(**vars())) + os.mkdir(target_dir) try: - getter = 'wget {url} -O -' - extract = 'tar x{compression} --strip-components=1 -C {target_dir}' - cmd = ' | '.join((getter, extract)) - runner(cmd.format(compression=infer_compression(url), **vars())) - with pushd(target_dir): - yield target_dir + req = urllib.request.urlopen(url) + with tarfile.open(fileobj=req, mode='r|*') as tf: + tf.extractall(path=target_dir, filter=strip_first_component) + yield target_dir finally: - runner('rm -Rf {target_dir}'.format(**vars())) + shutil.rmtree(target_dir) + + +def strip_first_component( + member: tarfile.TarInfo, + path, +) -> tarfile.TarInfo: + _, member.name = member.name.split('/', 1) + return member + + +def _compose(*cmgrs): + """ + Compose any number of dependent context managers into a single one. + + The last, innermost context manager may take arbitrary arguments, but + each successive context manager should accept the result from the + previous as a single parameter. + + Like :func:`jaraco.functools.compose`, behavior works from right to + left, so the context manager should be indicated from outermost to + innermost. + + Example, to create a context manager to change to a temporary + directory: + + >>> temp_dir_as_cwd = _compose(pushd, temp_dir) + >>> with temp_dir_as_cwd() as dir: + ... assert os.path.samefile(os.getcwd(), dir) + """ + + def compose_two(inner, outer): + def composed(*args, **kwargs): + with inner(*args, **kwargs) as saved, outer(saved) as res: + yield res + + return contextlib.contextmanager(composed) + + return functools.reduce(compose_two, reversed(cmgrs)) + + +tarball_cwd = _compose(pushd, tarball) + + +@contextlib.contextmanager +def tarball_context(*args, **kwargs): + warnings.warn( + "tarball_context is deprecated. Use tarball or tarball_cwd instead.", + DeprecationWarning, + stacklevel=2, + ) + pushd_ctx = kwargs.pop('pushd', pushd) + with tarball(*args, **kwargs) as tball, pushd_ctx(tball) as dir: + yield dir def infer_compression(url): @@ -68,6 +134,11 @@ def infer_compression(url): >>> infer_compression('file.xz') 'J' """ + warnings.warn( + "infer_compression is deprecated with no replacement", + DeprecationWarning, + stacklevel=2, + ) # cheat and just assume it's the last two characters compression_indicator = url[-2:] mapping = dict(gz='z', bz='j', xz='J') @@ -84,7 +155,7 @@ def temp_dir(remover=shutil.rmtree): >>> import pathlib >>> with temp_dir() as the_dir: ... assert os.path.isdir(the_dir) - ... _ = pathlib.Path(the_dir).joinpath('somefile').write_text('contents') + ... _ = pathlib.Path(the_dir).joinpath('somefile').write_text('contents', encoding='utf-8') >>> assert not os.path.exists(the_dir) """ temp_dir = tempfile.mkdtemp() @@ -113,15 +184,23 @@ def repo_context(url, branch=None, quiet=True, dest_ctx=temp_dir): yield repo_dir -@contextlib.contextmanager def null(): """ A null context suitable to stand in for a meaningful context. >>> with null() as value: ... assert value is None + + This context is most useful when dealing with two or more code + branches but only some need a context. Wrap the others in a null + context to provide symmetry across all options. """ - yield + warnings.warn( + "null is deprecated. Use contextlib.nullcontext", + DeprecationWarning, + stacklevel=2, + ) + return contextlib.nullcontext() class ExceptionTrap: @@ -267,13 +346,7 @@ class on_interrupt(contextlib.ContextDecorator): ... on_interrupt('ignore')(do_interrupt)() """ - def __init__( - self, - action='error', - # py3.7 compat - # /, - code=1, - ): + def __init__(self, action='error', /, code=1): self.action = action self.code = code diff --git a/lib/pkg_resources/_vendor/jaraco/functools.py b/lib/pkg_resources/_vendor/jaraco/functools/__init__.py similarity index 79% rename from lib/pkg_resources/_vendor/jaraco/functools.py rename to lib/pkg_resources/_vendor/jaraco/functools/__init__.py index 67aeadc3..f523099c 100644 --- a/lib/pkg_resources/_vendor/jaraco/functools.py +++ b/lib/pkg_resources/_vendor/jaraco/functools/__init__.py @@ -1,18 +1,14 @@ +import collections.abc import functools -import time import inspect -import collections -import types import itertools +import operator +import time +import types import warnings import pkg_resources.extern.more_itertools -from typing import Callable, TypeVar - - -CallableT = TypeVar("CallableT", bound=Callable[..., object]) - def compose(*funcs): """ @@ -38,24 +34,6 @@ def compose(*funcs): return functools.reduce(compose_two, funcs) -def method_caller(method_name, *args, **kwargs): - """ - Return a function that will call a named method on the - target object with optional positional and keyword - arguments. - - >>> lower = method_caller('lower') - >>> lower('MyString') - 'mystring' - """ - - def call_method(target): - func = getattr(target, method_name) - return func(*args, **kwargs) - - return call_method - - def once(func): """ Decorate func so it's only ever called the first time. @@ -98,12 +76,7 @@ def once(func): return wrapper -def method_cache( - method: CallableT, - cache_wrapper: Callable[ - [CallableT], CallableT - ] = functools.lru_cache(), # type: ignore[assignment] -) -> CallableT: +def method_cache(method, cache_wrapper=functools.lru_cache()): """ Wrap lru_cache to support storing the cache data in the object instances. @@ -171,21 +144,17 @@ def method_cache( for another implementation and additional justification. """ - def wrapper(self: object, *args: object, **kwargs: object) -> object: + def wrapper(self, *args, **kwargs): # it's the first call, replace the method with a cached, bound method - bound_method: CallableT = types.MethodType( # type: ignore[assignment] - method, self - ) + bound_method = types.MethodType(method, self) cached_method = cache_wrapper(bound_method) setattr(self, method.__name__, cached_method) return cached_method(*args, **kwargs) # Support cache clear even before cache has been created. - wrapper.cache_clear = lambda: None # type: ignore[attr-defined] + wrapper.cache_clear = lambda: None - return ( # type: ignore[return-value] - _special_method_cache(method, cache_wrapper) or wrapper - ) + return _special_method_cache(method, cache_wrapper) or wrapper def _special_method_cache(method, cache_wrapper): @@ -201,12 +170,13 @@ def _special_method_cache(method, cache_wrapper): """ name = method.__name__ special_names = '__getattr__', '__getitem__' + if name not in special_names: - return + return None wrapper_name = '__cached' + name - def proxy(self, *args, **kwargs): + def proxy(self, /, *args, **kwargs): if wrapper_name not in vars(self): bound = types.MethodType(method, self) cache = cache_wrapper(bound) @@ -243,7 +213,7 @@ def result_invoke(action): r""" Decorate a function with an action function that is invoked on the results returned from the decorated - function (for its side-effect), then return the original + function (for its side effect), then return the original result. >>> @result_invoke(print) @@ -267,7 +237,7 @@ def result_invoke(action): return wrap -def invoke(f, *args, **kwargs): +def invoke(f, /, *args, **kwargs): """ Call a function for its side effect after initialization. @@ -302,25 +272,15 @@ def invoke(f, *args, **kwargs): Use functools.partial to pass parameters to the initial call >>> @functools.partial(invoke, name='bingo') - ... def func(name): print("called with", name) + ... def func(name): print('called with', name) called with bingo """ f(*args, **kwargs) return f -def call_aside(*args, **kwargs): - """ - Deprecated name for invoke. - """ - warnings.warn("call_aside is deprecated, use invoke", DeprecationWarning) - return invoke(*args, **kwargs) - - class Throttler: - """ - Rate-limit a function (or other callable) - """ + """Rate-limit a function (or other callable).""" def __init__(self, func, max_rate=float('Inf')): if isinstance(func, Throttler): @@ -337,20 +297,20 @@ class Throttler: return self.func(*args, **kwargs) def _wait(self): - "ensure at least 1/max_rate seconds from last call" + """Ensure at least 1/max_rate seconds from last call.""" elapsed = time.time() - self.last_called must_wait = 1 / self.max_rate - elapsed time.sleep(max(0, must_wait)) self.last_called = time.time() - def __get__(self, obj, type=None): + def __get__(self, obj, owner=None): return first_invoke(self._wait, functools.partial(self.func, obj)) def first_invoke(func1, func2): """ Return a function that when invoked will invoke func1 without - any parameters (for its side-effect) and then invoke func2 + any parameters (for its side effect) and then invoke func2 with whatever parameters were passed, returning its result. """ @@ -361,6 +321,17 @@ def first_invoke(func1, func2): return wrapper +method_caller = first_invoke( + lambda: warnings.warn( + '`jaraco.functools.method_caller` is deprecated, ' + 'use `operator.methodcaller` instead', + DeprecationWarning, + stacklevel=3, + ), + operator.methodcaller, +) + + def retry_call(func, cleanup=lambda: None, retries=0, trap=()): """ Given a callable func, trap the indicated exceptions @@ -369,7 +340,7 @@ def retry_call(func, cleanup=lambda: None, retries=0, trap=()): to propagate. """ attempts = itertools.count() if retries == float('inf') else range(retries) - for attempt in attempts: + for _ in attempts: try: return func() except trap: @@ -406,7 +377,7 @@ def retry(*r_args, **r_kwargs): def print_yielded(func): """ - Convert a generator into a function that prints all yielded elements + Convert a generator into a function that prints all yielded elements. >>> @print_yielded ... def x(): @@ -422,7 +393,7 @@ def print_yielded(func): def pass_none(func): """ - Wrap func so it's not called if its first param is None + Wrap func so it's not called if its first param is None. >>> print_text = pass_none(print) >>> print_text('text') @@ -431,9 +402,10 @@ def pass_none(func): """ @functools.wraps(func) - def wrapper(param, *args, **kwargs): + def wrapper(param, /, *args, **kwargs): if param is not None: return func(param, *args, **kwargs) + return None return wrapper @@ -507,7 +479,7 @@ def save_method_args(method): args_and_kwargs = collections.namedtuple('args_and_kwargs', 'args kwargs') @functools.wraps(method) - def wrapper(self, *args, **kwargs): + def wrapper(self, /, *args, **kwargs): attr_name = '_saved_' + method.__name__ attr = args_and_kwargs(args, kwargs) setattr(self, attr_name, attr) @@ -554,3 +526,108 @@ def except_(*exceptions, replace=None, use=None): return wrapper return decorate + + +def identity(x): + """ + Return the argument. + + >>> o = object() + >>> identity(o) is o + True + """ + return x + + +def bypass_when(check, *, _op=identity): + """ + Decorate a function to return its parameter when ``check``. + + >>> bypassed = [] # False + + >>> @bypass_when(bypassed) + ... def double(x): + ... return x * 2 + >>> double(2) + 4 + >>> bypassed[:] = [object()] # True + >>> double(2) + 2 + """ + + def decorate(func): + @functools.wraps(func) + def wrapper(param, /): + return param if _op(check) else func(param) + + return wrapper + + return decorate + + +def bypass_unless(check): + """ + Decorate a function to return its parameter unless ``check``. + + >>> enabled = [object()] # True + + >>> @bypass_unless(enabled) + ... def double(x): + ... return x * 2 + >>> double(2) + 4 + >>> del enabled[:] # False + >>> double(2) + 2 + """ + return bypass_when(check, _op=operator.not_) + + +@functools.singledispatch +def _splat_inner(args, func): + """Splat args to func.""" + return func(*args) + + +@_splat_inner.register +def _(args: collections.abc.Mapping, func): + """Splat kargs to func as kwargs.""" + return func(**args) + + +def splat(func): + """ + Wrap func to expect its parameters to be passed positionally in a tuple. + + Has a similar effect to that of ``itertools.starmap`` over + simple ``map``. + + >>> pairs = [(-1, 1), (0, 2)] + >>> pkg_resources.extern.more_itertools.consume(itertools.starmap(print, pairs)) + -1 1 + 0 2 + >>> pkg_resources.extern.more_itertools.consume(map(splat(print), pairs)) + -1 1 + 0 2 + + The approach generalizes to other iterators that don't have a "star" + equivalent, such as a "starfilter". + + >>> list(filter(splat(operator.add), pairs)) + [(0, 2)] + + Splat also accepts a mapping argument. + + >>> def is_nice(msg, code): + ... return "smile" in msg or code == 0 + >>> msgs = [ + ... dict(msg='smile!', code=20), + ... dict(msg='error :(', code=1), + ... dict(msg='unknown', code=0), + ... ] + >>> for msg in filter(splat(is_nice), msgs): + ... print(msg) + {'msg': 'smile!', 'code': 20} + {'msg': 'unknown', 'code': 0} + """ + return functools.wraps(func)(functools.partial(_splat_inner, func=func)) diff --git a/lib/pkg_resources/_vendor/jaraco/functools/__init__.pyi b/lib/pkg_resources/_vendor/jaraco/functools/__init__.pyi new file mode 100644 index 00000000..c2b9ab17 --- /dev/null +++ b/lib/pkg_resources/_vendor/jaraco/functools/__init__.pyi @@ -0,0 +1,128 @@ +from collections.abc import Callable, Hashable, Iterator +from functools import partial +from operator import methodcaller +import sys +from typing import ( + Any, + Generic, + Protocol, + TypeVar, + overload, +) + +if sys.version_info >= (3, 10): + from typing import Concatenate, ParamSpec +else: + from typing_extensions import Concatenate, ParamSpec + +_P = ParamSpec('_P') +_R = TypeVar('_R') +_T = TypeVar('_T') +_R1 = TypeVar('_R1') +_R2 = TypeVar('_R2') +_V = TypeVar('_V') +_S = TypeVar('_S') +_R_co = TypeVar('_R_co', covariant=True) + +class _OnceCallable(Protocol[_P, _R]): + saved_result: _R + reset: Callable[[], None] + def __call__(self, *args: _P.args, **kwargs: _P.kwargs) -> _R: ... + +class _ProxyMethodCacheWrapper(Protocol[_R_co]): + cache_clear: Callable[[], None] + def __call__(self, *args: Hashable, **kwargs: Hashable) -> _R_co: ... + +class _MethodCacheWrapper(Protocol[_R_co]): + def cache_clear(self) -> None: ... + def __call__(self, *args: Hashable, **kwargs: Hashable) -> _R_co: ... + +# `compose()` overloads below will cover most use cases. + +@overload +def compose( + __func1: Callable[[_R], _T], + __func2: Callable[_P, _R], + /, +) -> Callable[_P, _T]: ... +@overload +def compose( + __func1: Callable[[_R], _T], + __func2: Callable[[_R1], _R], + __func3: Callable[_P, _R1], + /, +) -> Callable[_P, _T]: ... +@overload +def compose( + __func1: Callable[[_R], _T], + __func2: Callable[[_R2], _R], + __func3: Callable[[_R1], _R2], + __func4: Callable[_P, _R1], + /, +) -> Callable[_P, _T]: ... +def once(func: Callable[_P, _R]) -> _OnceCallable[_P, _R]: ... +def method_cache( + method: Callable[..., _R], + cache_wrapper: Callable[[Callable[..., _R]], _MethodCacheWrapper[_R]] = ..., +) -> _MethodCacheWrapper[_R] | _ProxyMethodCacheWrapper[_R]: ... +def apply( + transform: Callable[[_R], _T] +) -> Callable[[Callable[_P, _R]], Callable[_P, _T]]: ... +def result_invoke( + action: Callable[[_R], Any] +) -> Callable[[Callable[_P, _R]], Callable[_P, _R]]: ... +def invoke( + f: Callable[_P, _R], /, *args: _P.args, **kwargs: _P.kwargs +) -> Callable[_P, _R]: ... +def call_aside( + f: Callable[_P, _R], *args: _P.args, **kwargs: _P.kwargs +) -> Callable[_P, _R]: ... + +class Throttler(Generic[_R]): + last_called: float + func: Callable[..., _R] + max_rate: float + def __init__( + self, func: Callable[..., _R] | Throttler[_R], max_rate: float = ... + ) -> None: ... + def reset(self) -> None: ... + def __call__(self, *args: Any, **kwargs: Any) -> _R: ... + def __get__(self, obj: Any, owner: type[Any] | None = ...) -> Callable[..., _R]: ... + +def first_invoke( + func1: Callable[..., Any], func2: Callable[_P, _R] +) -> Callable[_P, _R]: ... + +method_caller: Callable[..., methodcaller] + +def retry_call( + func: Callable[..., _R], + cleanup: Callable[..., None] = ..., + retries: int | float = ..., + trap: type[BaseException] | tuple[type[BaseException], ...] = ..., +) -> _R: ... +def retry( + cleanup: Callable[..., None] = ..., + retries: int | float = ..., + trap: type[BaseException] | tuple[type[BaseException], ...] = ..., +) -> Callable[[Callable[..., _R]], Callable[..., _R]]: ... +def print_yielded(func: Callable[_P, Iterator[Any]]) -> Callable[_P, None]: ... +def pass_none( + func: Callable[Concatenate[_T, _P], _R] +) -> Callable[Concatenate[_T, _P], _R]: ... +def assign_params( + func: Callable[..., _R], namespace: dict[str, Any] +) -> partial[_R]: ... +def save_method_args( + method: Callable[Concatenate[_S, _P], _R] +) -> Callable[Concatenate[_S, _P], _R]: ... +def except_( + *exceptions: type[BaseException], replace: Any = ..., use: Any = ... +) -> Callable[[Callable[_P, Any]], Callable[_P, Any]]: ... +def identity(x: _T) -> _T: ... +def bypass_when( + check: _V, *, _op: Callable[[_V], Any] = ... +) -> Callable[[Callable[[_T], _R]], Callable[[_T], _T | _R]]: ... +def bypass_unless( + check: Any, +) -> Callable[[Callable[[_T], _R]], Callable[[_T], _T | _R]]: ... diff --git a/lib/pkg_resources/_vendor/jaraco/functools/py.typed b/lib/pkg_resources/_vendor/jaraco/functools/py.typed new file mode 100644 index 00000000..e69de29b diff --git a/lib/pkg_resources/_vendor/more_itertools/__init__.py b/lib/pkg_resources/_vendor/more_itertools/__init__.py index 66443971..aff94a9a 100644 --- a/lib/pkg_resources/_vendor/more_itertools/__init__.py +++ b/lib/pkg_resources/_vendor/more_itertools/__init__.py @@ -3,4 +3,4 @@ from .more import * # noqa from .recipes import * # noqa -__version__ = '9.1.0' +__version__ = '10.2.0' diff --git a/lib/pkg_resources/_vendor/more_itertools/more.py b/lib/pkg_resources/_vendor/more_itertools/more.py index e0e2d3de..d0957681 100644 --- a/lib/pkg_resources/_vendor/more_itertools/more.py +++ b/lib/pkg_resources/_vendor/more_itertools/more.py @@ -2,7 +2,7 @@ import warnings from collections import Counter, defaultdict, deque, abc from collections.abc import Sequence -from functools import partial, reduce, wraps +from functools import cached_property, partial, reduce, wraps from heapq import heapify, heapreplace, heappop from itertools import ( chain, @@ -17,8 +17,9 @@ from itertools import ( takewhile, tee, zip_longest, + product, ) -from math import exp, factorial, floor, log +from math import exp, factorial, floor, log, perm, comb from queue import Empty, Queue from random import random, randrange, uniform from operator import itemgetter, mul, sub, gt, lt, ge, le @@ -36,6 +37,7 @@ from .recipes import ( take, unique_everseen, all_equal, + batched, ) __all__ = [ @@ -53,6 +55,7 @@ __all__ = [ 'circular_shifts', 'collapse', 'combination_index', + 'combination_with_replacement_index', 'consecutive_groups', 'constrained_batches', 'consumer', @@ -65,8 +68,10 @@ __all__ = [ 'divide', 'duplicates_everseen', 'duplicates_justseen', + 'classify_unique', 'exactly_n', 'filter_except', + 'filter_map', 'first', 'gray_product', 'groupby_transform', @@ -80,6 +85,7 @@ __all__ = [ 'is_sorted', 'islice_extended', 'iterate', + 'iter_suppress', 'last', 'locate', 'longest_common_prefix', @@ -93,10 +99,13 @@ __all__ = [ 'nth_or_last', 'nth_permutation', 'nth_product', + 'nth_combination_with_replacement', 'numeric_range', 'one', 'only', + 'outer_product', 'padded', + 'partial_product', 'partitions', 'peekable', 'permutation_index', @@ -125,6 +134,7 @@ __all__ = [ 'strictly_n', 'substrings', 'substrings_indexes', + 'takewhile_inclusive', 'time_limited', 'unique_in_window', 'unique_to_each', @@ -191,15 +201,14 @@ def first(iterable, default=_marker): ``next(iter(iterable), default)``. """ - try: - return next(iter(iterable)) - except StopIteration as e: - if default is _marker: - raise ValueError( - 'first() was called on an empty iterable, and no ' - 'default value was provided.' - ) from e - return default + for item in iterable: + return item + if default is _marker: + raise ValueError( + 'first() was called on an empty iterable, and no ' + 'default value was provided.' + ) + return default def last(iterable, default=_marker): @@ -472,7 +481,10 @@ def iterate(func, start): """ while True: yield start - start = func(start) + try: + start = func(start) + except StopIteration: + break def with_iter(context_manager): @@ -572,6 +584,9 @@ def strictly_n(iterable, n, too_short=None, too_long=None): >>> list(strictly_n(iterable, n)) ['a', 'b', 'c', 'd'] + Note that the returned iterable must be consumed in order for the check to + be made. + By default, *too_short* and *too_long* are functions that raise ``ValueError``. @@ -909,7 +924,7 @@ def substrings_indexes(seq, reverse=False): class bucket: - """Wrap *iterable* and return an object that buckets it iterable into + """Wrap *iterable* and return an object that buckets the iterable into child iterables based on a *key* function. >>> iterable = ['a1', 'b1', 'c1', 'a2', 'b2', 'c2', 'b3'] @@ -2069,7 +2084,6 @@ class numeric_range(abc.Sequence, abc.Hashable): if self._step == self._zero: raise ValueError('numeric_range() arg 3 must not be zero') self._growing = self._step > self._zero - self._init_len() def __bool__(self): if self._growing: @@ -2145,7 +2159,8 @@ class numeric_range(abc.Sequence, abc.Hashable): def __len__(self): return self._len - def _init_len(self): + @cached_property + def _len(self): if self._growing: start = self._start stop = self._stop @@ -2156,10 +2171,10 @@ class numeric_range(abc.Sequence, abc.Hashable): step = -self._step distance = stop - start if distance <= self._zero: - self._len = 0 + return 0 else: # distance > 0 and step > 0: regular euclidean division q, r = divmod(distance, step) - self._len = int(q) + int(r != self._zero) + return int(q) + int(r != self._zero) def __reduce__(self): return numeric_range, (self._start, self._stop, self._step) @@ -2699,6 +2714,9 @@ class seekable: >>> it.seek(10) >>> next(it) '10' + >>> it.relative_seek(-2) # Seeking relative to the current position + >>> next(it) + '9' >>> it.seek(20) # Seeking past the end of the source isn't a problem >>> list(it) [] @@ -2812,6 +2830,10 @@ class seekable: if remainder > 0: consume(self, remainder) + def relative_seek(self, count): + index = len(self._cache) + self.seek(max(index + count, 0)) + class run_length: """ @@ -3205,6 +3227,8 @@ class time_limited: stops if the time elapsed is greater than *limit_seconds*. If your time limit is 1 second, but it takes 2 seconds to generate the first item from the iterable, the function will run for 2 seconds and not yield anything. + As a special case, when *limit_seconds* is zero, the iterator never + returns anything. """ @@ -3220,6 +3244,9 @@ class time_limited: return self def __next__(self): + if self.limit_seconds == 0: + self.timed_out = True + raise StopIteration item = next(self._iterable) if monotonic() - self._start_time > self.limit_seconds: self.timed_out = True @@ -3339,7 +3366,7 @@ def iequals(*iterables): >>> iequals("abc", "acb") False - Not to be confused with :func:`all_equals`, which checks whether all + Not to be confused with :func:`all_equal`, which checks whether all elements of iterable are equal to each other. """ @@ -3835,7 +3862,7 @@ def nth_permutation(iterable, r, index): elif not 0 <= r < n: raise ValueError else: - c = factorial(n) // factorial(n - r) + c = perm(n, r) if index < 0: index += c @@ -3858,6 +3885,52 @@ def nth_permutation(iterable, r, index): return tuple(map(pool.pop, result)) +def nth_combination_with_replacement(iterable, r, index): + """Equivalent to + ``list(combinations_with_replacement(iterable, r))[index]``. + + + The subsequences with repetition of *iterable* that are of length *r* can + be ordered lexicographically. :func:`nth_combination_with_replacement` + computes the subsequence at sort position *index* directly, without + computing the previous subsequences with replacement. + + >>> nth_combination_with_replacement(range(5), 3, 5) + (0, 1, 1) + + ``ValueError`` will be raised If *r* is negative or greater than the length + of *iterable*. + ``IndexError`` will be raised if the given *index* is invalid. + """ + pool = tuple(iterable) + n = len(pool) + if (r < 0) or (r > n): + raise ValueError + + c = comb(n + r - 1, r) + + if index < 0: + index += c + + if (index < 0) or (index >= c): + raise IndexError + + result = [] + i = 0 + while r: + r -= 1 + while n >= 0: + num_combs = comb(n + r - 1, r) + if index < num_combs: + break + n -= 1 + i += 1 + index -= num_combs + result.append(pool[i]) + + return tuple(result) + + def value_chain(*args): """Yield all arguments passed to the function in the same order in which they were passed. If an argument itself is iterable then iterate over its @@ -3949,9 +4022,66 @@ def combination_index(element, iterable): for i, j in enumerate(reversed(indexes), start=1): j = n - j if i <= j: - index += factorial(j) // (factorial(i) * factorial(j - i)) + index += comb(j, i) - return factorial(n + 1) // (factorial(k + 1) * factorial(n - k)) - index + return comb(n + 1, k + 1) - index + + +def combination_with_replacement_index(element, iterable): + """Equivalent to + ``list(combinations_with_replacement(iterable, r)).index(element)`` + + The subsequences with repetition of *iterable* that are of length *r* can + be ordered lexicographically. :func:`combination_with_replacement_index` + computes the index of the first *element*, without computing the previous + combinations with replacement. + + >>> combination_with_replacement_index('adf', 'abcdefg') + 20 + + ``ValueError`` will be raised if the given *element* isn't one of the + combinations with replacement of *iterable*. + """ + element = tuple(element) + l = len(element) + element = enumerate(element) + + k, y = next(element, (None, None)) + if k is None: + return 0 + + indexes = [] + pool = tuple(iterable) + for n, x in enumerate(pool): + while x == y: + indexes.append(n) + tmp, y = next(element, (None, None)) + if tmp is None: + break + else: + k = tmp + if y is None: + break + else: + raise ValueError( + 'element is not a combination with replacement of iterable' + ) + + n = len(pool) + occupations = [0] * n + for p in indexes: + occupations[p] += 1 + + index = 0 + cumulative_sum = 0 + for k in range(1, n): + cumulative_sum += occupations[k - 1] + j = l + n - 1 - k - cumulative_sum + i = n - k + if i <= j: + index += comb(j, i) + + return index def permutation_index(element, iterable): @@ -4056,26 +4186,20 @@ def _chunked_even_finite(iterable, N, n): num_full = N - partial_size * num_lists num_partial = num_lists - num_full - buffer = [] - iterator = iter(iterable) - # Yield num_full lists of full_size - for x in iterator: - buffer.append(x) - if len(buffer) == full_size: - yield buffer - buffer = [] - num_full -= 1 - if num_full <= 0: - break + partial_start_idx = num_full * full_size + if full_size > 0: + for i in range(0, partial_start_idx, full_size): + yield list(islice(iterable, i, i + full_size)) # Yield num_partial lists of partial_size - for x in iterator: - buffer.append(x) - if len(buffer) == partial_size: - yield buffer - buffer = [] - num_partial -= 1 + if partial_size > 0: + for i in range( + partial_start_idx, + partial_start_idx + (num_partial * partial_size), + partial_size, + ): + yield list(islice(iterable, i, i + partial_size)) def zip_broadcast(*objects, scalar_types=(str, bytes), strict=False): @@ -4114,30 +4238,23 @@ def zip_broadcast(*objects, scalar_types=(str, bytes), strict=False): if not size: return + new_item = [None] * size iterables, iterable_positions = [], [] - scalars, scalar_positions = [], [] for i, obj in enumerate(objects): if is_scalar(obj): - scalars.append(obj) - scalar_positions.append(i) + new_item[i] = obj else: iterables.append(iter(obj)) iterable_positions.append(i) - if len(scalars) == size: + if not iterables: yield tuple(objects) return zipper = _zip_equal if strict else zip for item in zipper(*iterables): - new_item = [None] * size - - for i, elem in zip(iterable_positions, item): - new_item[i] = elem - - for i, elem in zip(scalar_positions, scalars): - new_item[i] = elem - + for i, new_item[i] in zip(iterable_positions, item): + pass yield tuple(new_item) @@ -4162,22 +4279,23 @@ def unique_in_window(iterable, n, key=None): raise ValueError('n must be greater than 0') window = deque(maxlen=n) - uniques = set() + counts = defaultdict(int) use_key = key is not None for item in iterable: + if len(window) == n: + to_discard = window[0] + if counts[to_discard] == 1: + del counts[to_discard] + else: + counts[to_discard] -= 1 + k = key(item) if use_key else item - if k in uniques: - continue - - if len(uniques) == n: - uniques.discard(window[0]) - - uniques.add(k) + if k not in counts: + yield item + counts[k] += 1 window.append(k) - yield item - def duplicates_everseen(iterable, key=None): """Yield duplicate elements after their first appearance. @@ -4187,7 +4305,7 @@ def duplicates_everseen(iterable, key=None): >>> list(duplicates_everseen('AaaBbbCccAaa', str.lower)) ['a', 'a', 'b', 'b', 'c', 'c', 'A', 'a', 'a'] - This function is analagous to :func:`unique_everseen` and is subject to + This function is analogous to :func:`unique_everseen` and is subject to the same performance considerations. """ @@ -4217,15 +4335,52 @@ def duplicates_justseen(iterable, key=None): >>> list(duplicates_justseen('AaaBbbCccAaa', str.lower)) ['a', 'a', 'b', 'b', 'c', 'c', 'a', 'a'] - This function is analagous to :func:`unique_justseen`. + This function is analogous to :func:`unique_justseen`. """ - return flatten( - map( - lambda group_tuple: islice_extended(group_tuple[1])[1:], - groupby(iterable, key), - ) - ) + return flatten(g for _, g in groupby(iterable, key) for _ in g) + + +def classify_unique(iterable, key=None): + """Classify each element in terms of its uniqueness. + + For each element in the input iterable, return a 3-tuple consisting of: + + 1. The element itself + 2. ``False`` if the element is equal to the one preceding it in the input, + ``True`` otherwise (i.e. the equivalent of :func:`unique_justseen`) + 3. ``False`` if this element has been seen anywhere in the input before, + ``True`` otherwise (i.e. the equivalent of :func:`unique_everseen`) + + >>> list(classify_unique('otto')) # doctest: +NORMALIZE_WHITESPACE + [('o', True, True), + ('t', True, True), + ('t', False, False), + ('o', True, False)] + + This function is analogous to :func:`unique_everseen` and is subject to + the same performance considerations. + + """ + seen_set = set() + seen_list = [] + use_key = key is not None + previous = None + + for i, element in enumerate(iterable): + k = key(element) if use_key else element + is_unique_justseen = not i or previous != k + previous = k + is_unique_everseen = False + try: + if k not in seen_set: + seen_set.add(k) + is_unique_everseen = True + except TypeError: + if k not in seen_list: + seen_list.append(k) + is_unique_everseen = True + yield element, is_unique_justseen, is_unique_everseen def minmax(iterable_or_value, *others, key=None, default=_marker): @@ -4389,3 +4544,112 @@ def gray_product(*iterables): o[j] = -o[j] f[j] = f[j + 1] f[j + 1] = j + 1 + + +def partial_product(*iterables): + """Yields tuples containing one item from each iterator, with subsequent + tuples changing a single item at a time by advancing each iterator until it + is exhausted. This sequence guarantees every value in each iterable is + output at least once without generating all possible combinations. + + This may be useful, for example, when testing an expensive function. + + >>> list(partial_product('AB', 'C', 'DEF')) + [('A', 'C', 'D'), ('B', 'C', 'D'), ('B', 'C', 'E'), ('B', 'C', 'F')] + """ + + iterators = list(map(iter, iterables)) + + try: + prod = [next(it) for it in iterators] + except StopIteration: + return + yield tuple(prod) + + for i, it in enumerate(iterators): + for prod[i] in it: + yield tuple(prod) + + +def takewhile_inclusive(predicate, iterable): + """A variant of :func:`takewhile` that yields one additional element. + + >>> list(takewhile_inclusive(lambda x: x < 5, [1, 4, 6, 4, 1])) + [1, 4, 6] + + :func:`takewhile` would return ``[1, 4]``. + """ + for x in iterable: + yield x + if not predicate(x): + break + + +def outer_product(func, xs, ys, *args, **kwargs): + """A generalized outer product that applies a binary function to all + pairs of items. Returns a 2D matrix with ``len(xs)`` rows and ``len(ys)`` + columns. + Also accepts ``*args`` and ``**kwargs`` that are passed to ``func``. + + Multiplication table: + + >>> list(outer_product(mul, range(1, 4), range(1, 6))) + [(1, 2, 3, 4, 5), (2, 4, 6, 8, 10), (3, 6, 9, 12, 15)] + + Cross tabulation: + + >>> xs = ['A', 'B', 'A', 'A', 'B', 'B', 'A', 'A', 'B', 'B'] + >>> ys = ['X', 'X', 'X', 'Y', 'Z', 'Z', 'Y', 'Y', 'Z', 'Z'] + >>> rows = list(zip(xs, ys)) + >>> count_rows = lambda x, y: rows.count((x, y)) + >>> list(outer_product(count_rows, sorted(set(xs)), sorted(set(ys)))) + [(2, 3, 0), (1, 0, 4)] + + Usage with ``*args`` and ``**kwargs``: + + >>> animals = ['cat', 'wolf', 'mouse'] + >>> list(outer_product(min, animals, animals, key=len)) + [('cat', 'cat', 'cat'), ('cat', 'wolf', 'wolf'), ('cat', 'wolf', 'mouse')] + """ + ys = tuple(ys) + return batched( + starmap(lambda x, y: func(x, y, *args, **kwargs), product(xs, ys)), + n=len(ys), + ) + + +def iter_suppress(iterable, *exceptions): + """Yield each of the items from *iterable*. If the iteration raises one of + the specified *exceptions*, that exception will be suppressed and iteration + will stop. + + >>> from itertools import chain + >>> def breaks_at_five(x): + ... while True: + ... if x >= 5: + ... raise RuntimeError + ... yield x + ... x += 1 + >>> it_1 = iter_suppress(breaks_at_five(1), RuntimeError) + >>> it_2 = iter_suppress(breaks_at_five(2), RuntimeError) + >>> list(chain(it_1, it_2)) + [1, 2, 3, 4, 2, 3, 4] + """ + try: + yield from iterable + except exceptions: + return + + +def filter_map(func, iterable): + """Apply *func* to every element of *iterable*, yielding only those which + are not ``None``. + + >>> elems = ['1', 'a', '2', 'b', '3'] + >>> list(filter_map(lambda s: int(s) if s.isnumeric() else None, elems)) + [1, 2, 3] + """ + for x in iterable: + y = func(x) + if y is not None: + yield y diff --git a/lib/pkg_resources/_vendor/more_itertools/more.pyi b/lib/pkg_resources/_vendor/more_itertools/more.pyi index 75c5232c..9a5fc911 100644 --- a/lib/pkg_resources/_vendor/more_itertools/more.pyi +++ b/lib/pkg_resources/_vendor/more_itertools/more.pyi @@ -29,7 +29,7 @@ _U = TypeVar('_U') _V = TypeVar('_V') _W = TypeVar('_W') _T_co = TypeVar('_T_co', covariant=True) -_GenFn = TypeVar('_GenFn', bound=Callable[..., Iterator[object]]) +_GenFn = TypeVar('_GenFn', bound=Callable[..., Iterator[Any]]) _Raisable = BaseException | Type[BaseException] @type_check_only @@ -74,7 +74,7 @@ class peekable(Generic[_T], Iterator[_T]): def __getitem__(self, index: slice) -> list[_T]: ... def consumer(func: _GenFn) -> _GenFn: ... -def ilen(iterable: Iterable[object]) -> int: ... +def ilen(iterable: Iterable[_T]) -> int: ... def iterate(func: Callable[[_T], _T], start: _T) -> Iterator[_T]: ... def with_iter( context_manager: ContextManager[Iterable[_T]], @@ -116,7 +116,7 @@ class bucket(Generic[_T, _U], Container[_U]): self, iterable: Iterable[_T], key: Callable[[_T], _U], - validator: Callable[[object], object] | None = ..., + validator: Callable[[_U], object] | None = ..., ) -> None: ... def __contains__(self, value: object) -> bool: ... def __iter__(self) -> Iterator[_U]: ... @@ -383,7 +383,7 @@ def mark_ends( iterable: Iterable[_T], ) -> Iterable[tuple[bool, bool, _T]]: ... def locate( - iterable: Iterable[object], + iterable: Iterable[_T], pred: Callable[..., Any] = ..., window_size: int | None = ..., ) -> Iterator[int]: ... @@ -440,6 +440,7 @@ class seekable(Generic[_T], Iterator[_T]): def peek(self, default: _U) -> _T | _U: ... def elements(self) -> SequenceView[_T]: ... def seek(self, index: int) -> None: ... + def relative_seek(self, count: int) -> None: ... class run_length: @staticmethod @@ -578,6 +579,9 @@ def all_unique( iterable: Iterable[_T], key: Callable[[_T], _U] | None = ... ) -> bool: ... def nth_product(index: int, *args: Iterable[_T]) -> tuple[_T, ...]: ... +def nth_combination_with_replacement( + iterable: Iterable[_T], r: int, index: int +) -> tuple[_T, ...]: ... def nth_permutation( iterable: Iterable[_T], r: int, index: int ) -> tuple[_T, ...]: ... @@ -586,6 +590,9 @@ def product_index(element: Iterable[_T], *args: Iterable[_T]) -> int: ... def combination_index( element: Iterable[_T], iterable: Iterable[_T] ) -> int: ... +def combination_with_replacement_index( + element: Iterable[_T], iterable: Iterable[_T] +) -> int: ... def permutation_index( element: Iterable[_T], iterable: Iterable[_T] ) -> int: ... @@ -611,6 +618,9 @@ def duplicates_everseen( def duplicates_justseen( iterable: Iterable[_T], key: Callable[[_T], _U] | None = ... ) -> Iterator[_T]: ... +def classify_unique( + iterable: Iterable[_T], key: Callable[[_T], _U] | None = ... +) -> Iterator[tuple[_T, bool, bool]]: ... class _SupportsLessThan(Protocol): def __lt__(self, __other: Any) -> bool: ... @@ -655,12 +665,31 @@ def minmax( def longest_common_prefix( iterables: Iterable[Iterable[_T]], ) -> Iterator[_T]: ... -def iequals(*iterables: Iterable[object]) -> bool: ... +def iequals(*iterables: Iterable[Any]) -> bool: ... def constrained_batches( - iterable: Iterable[object], + iterable: Iterable[_T], max_size: int, max_count: int | None = ..., get_len: Callable[[_T], object] = ..., strict: bool = ..., ) -> Iterator[tuple[_T]]: ... def gray_product(*iterables: Iterable[_T]) -> Iterator[tuple[_T, ...]]: ... +def partial_product(*iterables: Iterable[_T]) -> Iterator[tuple[_T, ...]]: ... +def takewhile_inclusive( + predicate: Callable[[_T], bool], iterable: Iterable[_T] +) -> Iterator[_T]: ... +def outer_product( + func: Callable[[_T, _U], _V], + xs: Iterable[_T], + ys: Iterable[_U], + *args: Any, + **kwargs: Any, +) -> Iterator[tuple[_V, ...]]: ... +def iter_suppress( + iterable: Iterable[_T], + *exceptions: Type[BaseException], +) -> Iterator[_T]: ... +def filter_map( + func: Callable[[_T], _V | None], + iterable: Iterable[_T], +) -> Iterator[_V]: ... diff --git a/lib/pkg_resources/_vendor/more_itertools/recipes.py b/lib/pkg_resources/_vendor/more_itertools/recipes.py index 3facc2e3..145e3cb5 100644 --- a/lib/pkg_resources/_vendor/more_itertools/recipes.py +++ b/lib/pkg_resources/_vendor/more_itertools/recipes.py @@ -9,11 +9,10 @@ Some backward-compatible usability improvements have been made. """ import math import operator -import warnings from collections import deque from collections.abc import Sized -from functools import reduce +from functools import partial, reduce from itertools import ( chain, combinations, @@ -52,10 +51,13 @@ __all__ = [ 'pad_none', 'pairwise', 'partition', + 'polynomial_eval', 'polynomial_from_roots', + 'polynomial_derivative', 'powerset', 'prepend', 'quantify', + 'reshape', 'random_combination_with_replacement', 'random_combination', 'random_permutation', @@ -65,9 +67,11 @@ __all__ = [ 'sieve', 'sliding_window', 'subslices', + 'sum_of_squares', 'tabulate', 'tail', 'take', + 'totient', 'transpose', 'triplewise', 'unique_everseen', @@ -77,6 +81,18 @@ __all__ = [ _marker = object() +# zip with strict is available for Python 3.10+ +try: + zip(strict=True) +except TypeError: + _zip_strict = zip +else: + _zip_strict = partial(zip, strict=True) + +# math.sumprod is available for Python 3.12+ +_sumprod = getattr(math, 'sumprod', lambda x, y: dotproduct(x, y)) + + def take(n, iterable): """Return first *n* items of the iterable as a list. @@ -293,7 +309,7 @@ def _pairwise(iterable): """ a, b = tee(iterable) next(b, None) - yield from zip(a, b) + return zip(a, b) try: @@ -303,7 +319,7 @@ except ImportError: else: def pairwise(iterable): - yield from itertools_pairwise(iterable) + return itertools_pairwise(iterable) pairwise.__doc__ = _pairwise.__doc__ @@ -334,13 +350,9 @@ def _zip_equal(*iterables): for i, it in enumerate(iterables[1:], 1): size = len(it) if size != first_size: - break - else: - # If we didn't break out, we can use the built-in zip. - return zip(*iterables) - - # If we did break out, there was a mismatch. - raise UnequalIterablesError(details=(first_size, i, size)) + raise UnequalIterablesError(details=(first_size, i, size)) + # All sizes are equal, we can use the built-in zip. + return zip(*iterables) # If any one of the iterables didn't have a length, start reading # them until one runs out. except TypeError: @@ -433,12 +445,9 @@ def partition(pred, iterable): if pred is None: pred = bool - evaluations = ((pred(x), x) for x in iterable) - t1, t2 = tee(evaluations) - return ( - (x for (cond, x) in t1 if not cond), - (x for (cond, x) in t2 if cond), - ) + t1, t2, p = tee(iterable, 3) + p1, p2 = tee(map(pred, p)) + return (compress(t1, map(operator.not_, p1)), compress(t2, p2)) def powerset(iterable): @@ -486,7 +495,7 @@ def unique_everseen(iterable, key=None): >>> list(unique_everseen(iterable, key=tuple)) # Faster [[1, 2], [2, 3]] - Similary, you may want to convert unhashable ``set`` objects with + Similarly, you may want to convert unhashable ``set`` objects with ``key=frozenset``. For ``dict`` objects, ``key=lambda x: frozenset(x.items())`` can be used. @@ -518,6 +527,9 @@ def unique_justseen(iterable, key=None): ['A', 'B', 'C', 'A', 'D'] """ + if key is None: + return map(operator.itemgetter(0), groupby(iterable)) + return map(next, map(operator.itemgetter(1), groupby(iterable, key))) @@ -712,12 +724,14 @@ def convolve(signal, kernel): is immediately consumed and stored. """ + # This implementation intentionally doesn't match the one in the itertools + # documentation. kernel = tuple(kernel)[::-1] n = len(kernel) window = deque([0], maxlen=n) * n for x in chain(signal, repeat(0, n - 1)): window.append(x) - yield sum(map(operator.mul, kernel, window)) + yield _sumprod(kernel, window) def before_and_after(predicate, it): @@ -778,9 +792,7 @@ def sliding_window(iterable, n): For a variant with more features, see :func:`windowed`. """ it = iter(iterable) - window = deque(islice(it, n), maxlen=n) - if len(window) == n: - yield tuple(window) + window = deque(islice(it, n - 1), maxlen=n) for x in it: window.append(x) yield tuple(window) @@ -807,39 +819,38 @@ def polynomial_from_roots(roots): >>> polynomial_from_roots(roots) # x^3 - 4 * x^2 - 17 * x + 60 [1, -4, -17, 60] """ - # Use math.prod for Python 3.8+, - prod = getattr(math, 'prod', lambda x: reduce(operator.mul, x, 1)) - roots = list(map(operator.neg, roots)) - return [ - sum(map(prod, combinations(roots, k))) for k in range(len(roots) + 1) - ] + factors = zip(repeat(1), map(operator.neg, roots)) + return list(reduce(convolve, factors, [1])) -def iter_index(iterable, value, start=0): +def iter_index(iterable, value, start=0, stop=None): """Yield the index of each place in *iterable* that *value* occurs, - beginning with index *start*. + beginning with index *start* and ending before index *stop*. See :func:`locate` for a more general means of finding the indexes associated with particular values. >>> list(iter_index('AABCADEAF', 'A')) [0, 1, 4, 7] + >>> list(iter_index('AABCADEAF', 'A', 1)) # start index is inclusive + [1, 4, 7] + >>> list(iter_index('AABCADEAF', 'A', 1, 7)) # stop index is not inclusive + [1, 4] """ - try: - seq_index = iterable.index - except AttributeError: + seq_index = getattr(iterable, 'index', None) + if seq_index is None: # Slow path for general iterables - it = islice(iterable, start, None) + it = islice(iterable, start, stop) for i, element in enumerate(it, start): if element is value or element == value: yield i else: # Fast path for sequences + stop = len(iterable) if stop is None else stop i = start - 1 try: while True: - i = seq_index(value, i + 1) - yield i + yield (i := seq_index(value, i + 1, stop)) except ValueError: pass @@ -850,81 +861,152 @@ def sieve(n): >>> list(sieve(30)) [2, 3, 5, 7, 11, 13, 17, 19, 23, 29] """ - isqrt = getattr(math, 'isqrt', lambda x: int(math.sqrt(x))) + if n > 2: + yield 2 + start = 3 data = bytearray((0, 1)) * (n // 2) - data[:3] = 0, 0, 0 - limit = isqrt(n) + 1 - for p in compress(range(limit), data): + limit = math.isqrt(n) + 1 + for p in iter_index(data, 1, start, limit): + yield from iter_index(data, 1, start, p * p) data[p * p : n : p + p] = bytes(len(range(p * p, n, p + p))) - data[2] = 1 - return iter_index(data, 1) if n > 2 else iter([]) + start = p * p + yield from iter_index(data, 1, start) -def batched(iterable, n): - """Batch data into lists of length *n*. The last batch may be shorter. +def _batched(iterable, n, *, strict=False): + """Batch data into tuples of length *n*. If the number of items in + *iterable* is not divisible by *n*: + * The last batch will be shorter if *strict* is ``False``. + * :exc:`ValueError` will be raised if *strict* is ``True``. >>> list(batched('ABCDEFG', 3)) - [['A', 'B', 'C'], ['D', 'E', 'F'], ['G']] + [('A', 'B', 'C'), ('D', 'E', 'F'), ('G',)] - This recipe is from the ``itertools`` docs. This library also provides - :func:`chunked`, which has a different implementation. + On Python 3.13 and above, this is an alias for :func:`itertools.batched`. """ - if hexversion >= 0x30C00A0: # Python 3.12.0a0 - warnings.warn( - ( - 'batched will be removed in a future version of ' - 'more-itertools. Use the standard library ' - 'itertools.batched function instead' - ), - DeprecationWarning, - ) - + if n < 1: + raise ValueError('n must be at least one') it = iter(iterable) - while True: - batch = list(islice(it, n)) - if not batch: - break + while batch := tuple(islice(it, n)): + if strict and len(batch) != n: + raise ValueError('batched(): incomplete batch') yield batch +if hexversion >= 0x30D00A2: + from itertools import batched as itertools_batched + + def batched(iterable, n, *, strict=False): + return itertools_batched(iterable, n, strict=strict) + +else: + batched = _batched + + batched.__doc__ = _batched.__doc__ + + def transpose(it): - """Swap the rows and columns of the input. + """Swap the rows and columns of the input matrix. >>> list(transpose([(1, 2, 3), (11, 22, 33)])) [(1, 11), (2, 22), (3, 33)] The caller should ensure that the dimensions of the input are compatible. + If the input is empty, no output will be produced. """ - # TODO: when 3.9 goes end-of-life, add stric=True to this. - return zip(*it) + return _zip_strict(*it) + + +def reshape(matrix, cols): + """Reshape the 2-D input *matrix* to have a column count given by *cols*. + + >>> matrix = [(0, 1), (2, 3), (4, 5)] + >>> cols = 3 + >>> list(reshape(matrix, cols)) + [(0, 1, 2), (3, 4, 5)] + """ + return batched(chain.from_iterable(matrix), cols) def matmul(m1, m2): """Multiply two matrices. + >>> list(matmul([(7, 5), (3, 5)], [(2, 5), (7, 9)])) - [[49, 80], [41, 60]] + [(49, 80), (41, 60)] The caller should ensure that the dimensions of the input matrices are compatible with each other. """ n = len(m2[0]) - return batched(starmap(dotproduct, product(m1, transpose(m2))), n) + return batched(starmap(_sumprod, product(m1, transpose(m2))), n) def factor(n): """Yield the prime factors of n. + >>> list(factor(360)) [2, 2, 2, 3, 3, 5] """ - isqrt = getattr(math, 'isqrt', lambda x: int(math.sqrt(x))) - for prime in sieve(isqrt(n) + 1): - while True: - quotient, remainder = divmod(n, prime) - if remainder: - break + for prime in sieve(math.isqrt(n) + 1): + while not n % prime: yield prime - n = quotient + n //= prime if n == 1: return - if n >= 2: + if n > 1: yield n + + +def polynomial_eval(coefficients, x): + """Evaluate a polynomial at a specific value. + + Example: evaluating x^3 - 4 * x^2 - 17 * x + 60 at x = 2.5: + + >>> coefficients = [1, -4, -17, 60] + >>> x = 2.5 + >>> polynomial_eval(coefficients, x) + 8.125 + """ + n = len(coefficients) + if n == 0: + return x * 0 # coerce zero to the type of x + powers = map(pow, repeat(x), reversed(range(n))) + return _sumprod(coefficients, powers) + + +def sum_of_squares(it): + """Return the sum of the squares of the input values. + + >>> sum_of_squares([10, 20, 30]) + 1400 + """ + return _sumprod(*tee(it)) + + +def polynomial_derivative(coefficients): + """Compute the first derivative of a polynomial. + + Example: evaluating the derivative of x^3 - 4 * x^2 - 17 * x + 60 + + >>> coefficients = [1, -4, -17, 60] + >>> derivative_coefficients = polynomial_derivative(coefficients) + >>> derivative_coefficients + [3, -8, -17] + """ + n = len(coefficients) + powers = reversed(range(1, n)) + return list(map(operator.mul, coefficients, powers)) + + +def totient(n): + """Return the count of natural numbers up to *n* that are coprime with *n*. + + >>> totient(9) + 6 + >>> totient(12) + 4 + """ + for p in unique_justseen(factor(n)): + n = n // p * (p - 1) + + return n diff --git a/lib/pkg_resources/_vendor/more_itertools/recipes.pyi b/lib/pkg_resources/_vendor/more_itertools/recipes.pyi index 0267ed56..ed4c19db 100644 --- a/lib/pkg_resources/_vendor/more_itertools/recipes.pyi +++ b/lib/pkg_resources/_vendor/more_itertools/recipes.pyi @@ -14,6 +14,8 @@ from typing import ( # Type and type variable definitions _T = TypeVar('_T') +_T1 = TypeVar('_T1') +_T2 = TypeVar('_T2') _U = TypeVar('_U') def take(n: int, iterable: Iterable[_T]) -> list[_T]: ... @@ -21,19 +23,19 @@ def tabulate( function: Callable[[int], _T], start: int = ... ) -> Iterator[_T]: ... def tail(n: int, iterable: Iterable[_T]) -> Iterator[_T]: ... -def consume(iterator: Iterable[object], n: int | None = ...) -> None: ... +def consume(iterator: Iterable[_T], n: int | None = ...) -> None: ... @overload def nth(iterable: Iterable[_T], n: int) -> _T | None: ... @overload def nth(iterable: Iterable[_T], n: int, default: _U) -> _T | _U: ... -def all_equal(iterable: Iterable[object]) -> bool: ... +def all_equal(iterable: Iterable[_T]) -> bool: ... def quantify( iterable: Iterable[_T], pred: Callable[[_T], bool] = ... ) -> int: ... def pad_none(iterable: Iterable[_T]) -> Iterator[_T | None]: ... def padnone(iterable: Iterable[_T]) -> Iterator[_T | None]: ... def ncycles(iterable: Iterable[_T], n: int) -> Iterator[_T]: ... -def dotproduct(vec1: Iterable[object], vec2: Iterable[object]) -> object: ... +def dotproduct(vec1: Iterable[_T1], vec2: Iterable[_T2]) -> Any: ... def flatten(listOfLists: Iterable[Iterable[_T]]) -> Iterator[_T]: ... def repeatfunc( func: Callable[..., _U], times: int | None = ..., *args: Any @@ -101,19 +103,26 @@ def sliding_window( iterable: Iterable[_T], n: int ) -> Iterator[tuple[_T, ...]]: ... def subslices(iterable: Iterable[_T]) -> Iterator[list[_T]]: ... -def polynomial_from_roots(roots: Sequence[int]) -> list[int]: ... +def polynomial_from_roots(roots: Sequence[_T]) -> list[_T]: ... def iter_index( - iterable: Iterable[object], + iterable: Iterable[_T], value: Any, start: int | None = ..., + stop: int | None = ..., ) -> Iterator[int]: ... def sieve(n: int) -> Iterator[int]: ... def batched( - iterable: Iterable[_T], - n: int, -) -> Iterator[list[_T]]: ... + iterable: Iterable[_T], n: int, *, strict: bool = False +) -> Iterator[tuple[_T]]: ... def transpose( it: Iterable[Iterable[_T]], -) -> tuple[Iterator[_T], ...]: ... -def matmul(m1: Sequence[_T], m2: Sequence[_T]) -> Iterator[list[_T]]: ... +) -> Iterator[tuple[_T, ...]]: ... +def reshape( + matrix: Iterable[Iterable[_T]], cols: int +) -> Iterator[tuple[_T, ...]]: ... +def matmul(m1: Sequence[_T], m2: Sequence[_T]) -> Iterator[tuple[_T]]: ... def factor(n: int) -> Iterator[int]: ... +def polynomial_eval(coefficients: Sequence[_T], x: _U) -> _U: ... +def sum_of_squares(it: Iterable[_T]) -> _T: ... +def polynomial_derivative(coefficients: Sequence[_T]) -> list[_T]: ... +def totient(n: int) -> int: ... diff --git a/lib/pkg_resources/_vendor/packaging/__init__.py b/lib/pkg_resources/_vendor/packaging/__init__.py index 13cadc7f..e7c0aa12 100644 --- a/lib/pkg_resources/_vendor/packaging/__init__.py +++ b/lib/pkg_resources/_vendor/packaging/__init__.py @@ -6,10 +6,10 @@ __title__ = "packaging" __summary__ = "Core utilities for Python packages" __uri__ = "https://github.com/pypa/packaging" -__version__ = "23.1" +__version__ = "24.0" __author__ = "Donald Stufft and individual contributors" __email__ = "donald@stufft.io" __license__ = "BSD-2-Clause or Apache-2.0" -__copyright__ = "2014-2019 %s" % __author__ +__copyright__ = "2014 %s" % __author__ diff --git a/lib/pkg_resources/_vendor/packaging/_manylinux.py b/lib/pkg_resources/_vendor/packaging/_manylinux.py index 449c655b..ad62505f 100644 --- a/lib/pkg_resources/_vendor/packaging/_manylinux.py +++ b/lib/pkg_resources/_vendor/packaging/_manylinux.py @@ -5,7 +5,7 @@ import os import re import sys import warnings -from typing import Dict, Generator, Iterator, NamedTuple, Optional, Tuple +from typing import Dict, Generator, Iterator, NamedTuple, Optional, Sequence, Tuple from ._elffile import EIClass, EIData, ELFFile, EMachine @@ -50,12 +50,21 @@ def _is_linux_i686(executable: str) -> bool: ) -def _have_compatible_abi(executable: str, arch: str) -> bool: - if arch == "armv7l": +def _have_compatible_abi(executable: str, archs: Sequence[str]) -> bool: + if "armv7l" in archs: return _is_linux_armhf(executable) - if arch == "i686": + if "i686" in archs: return _is_linux_i686(executable) - return arch in {"x86_64", "aarch64", "ppc64", "ppc64le", "s390x"} + allowed_archs = { + "x86_64", + "aarch64", + "ppc64", + "ppc64le", + "s390x", + "loongarch64", + "riscv64", + } + return any(arch in allowed_archs for arch in archs) # If glibc ever changes its major version, we need to know what the last @@ -81,7 +90,7 @@ def _glibc_version_string_confstr() -> Optional[str]: # https://github.com/python/cpython/blob/fcf1d003bf4f0100c/Lib/platform.py#L175-L183 try: # Should be a string like "glibc 2.17". - version_string: str = getattr(os, "confstr")("CS_GNU_LIBC_VERSION") + version_string: Optional[str] = os.confstr("CS_GNU_LIBC_VERSION") assert version_string is not None _, version = version_string.rsplit() except (AssertionError, AttributeError, OSError, ValueError): @@ -167,13 +176,13 @@ def _get_glibc_version() -> Tuple[int, int]: # From PEP 513, PEP 600 -def _is_compatible(name: str, arch: str, version: _GLibCVersion) -> bool: +def _is_compatible(arch: str, version: _GLibCVersion) -> bool: sys_glibc = _get_glibc_version() if sys_glibc < version: return False # Check for presence of _manylinux module. try: - import _manylinux # noqa + import _manylinux except ImportError: return True if hasattr(_manylinux, "manylinux_compatible"): @@ -203,12 +212,22 @@ _LEGACY_MANYLINUX_MAP = { } -def platform_tags(linux: str, arch: str) -> Iterator[str]: - if not _have_compatible_abi(sys.executable, arch): +def platform_tags(archs: Sequence[str]) -> Iterator[str]: + """Generate manylinux tags compatible to the current platform. + + :param archs: Sequence of compatible architectures. + The first one shall be the closest to the actual architecture and be the part of + platform tag after the ``linux_`` prefix, e.g. ``x86_64``. + The ``linux_`` prefix is assumed as a prerequisite for the current platform to + be manylinux-compatible. + + :returns: An iterator of compatible manylinux tags. + """ + if not _have_compatible_abi(sys.executable, archs): return # Oldest glibc to be supported regardless of architecture is (2, 17). too_old_glibc2 = _GLibCVersion(2, 16) - if arch in {"x86_64", "i686"}: + if set(archs) & {"x86_64", "i686"}: # On x86/i686 also oldest glibc to be supported is (2, 5). too_old_glibc2 = _GLibCVersion(2, 4) current_glibc = _GLibCVersion(*_get_glibc_version()) @@ -222,19 +241,20 @@ def platform_tags(linux: str, arch: str) -> Iterator[str]: for glibc_major in range(current_glibc.major - 1, 1, -1): glibc_minor = _LAST_GLIBC_MINOR[glibc_major] glibc_max_list.append(_GLibCVersion(glibc_major, glibc_minor)) - for glibc_max in glibc_max_list: - if glibc_max.major == too_old_glibc2.major: - min_minor = too_old_glibc2.minor - else: - # For other glibc major versions oldest supported is (x, 0). - min_minor = -1 - for glibc_minor in range(glibc_max.minor, min_minor, -1): - glibc_version = _GLibCVersion(glibc_max.major, glibc_minor) - tag = "manylinux_{}_{}".format(*glibc_version) - if _is_compatible(tag, arch, glibc_version): - yield linux.replace("linux", tag) - # Handle the legacy manylinux1, manylinux2010, manylinux2014 tags. - if glibc_version in _LEGACY_MANYLINUX_MAP: - legacy_tag = _LEGACY_MANYLINUX_MAP[glibc_version] - if _is_compatible(legacy_tag, arch, glibc_version): - yield linux.replace("linux", legacy_tag) + for arch in archs: + for glibc_max in glibc_max_list: + if glibc_max.major == too_old_glibc2.major: + min_minor = too_old_glibc2.minor + else: + # For other glibc major versions oldest supported is (x, 0). + min_minor = -1 + for glibc_minor in range(glibc_max.minor, min_minor, -1): + glibc_version = _GLibCVersion(glibc_max.major, glibc_minor) + tag = "manylinux_{}_{}".format(*glibc_version) + if _is_compatible(arch, glibc_version): + yield f"{tag}_{arch}" + # Handle the legacy manylinux1, manylinux2010, manylinux2014 tags. + if glibc_version in _LEGACY_MANYLINUX_MAP: + legacy_tag = _LEGACY_MANYLINUX_MAP[glibc_version] + if _is_compatible(arch, glibc_version): + yield f"{legacy_tag}_{arch}" diff --git a/lib/pkg_resources/_vendor/packaging/_musllinux.py b/lib/pkg_resources/_vendor/packaging/_musllinux.py index 706ba600..86419df9 100644 --- a/lib/pkg_resources/_vendor/packaging/_musllinux.py +++ b/lib/pkg_resources/_vendor/packaging/_musllinux.py @@ -8,7 +8,7 @@ import functools import re import subprocess import sys -from typing import Iterator, NamedTuple, Optional +from typing import Iterator, NamedTuple, Optional, Sequence from ._elffile import ELFFile @@ -47,24 +47,27 @@ def _get_musl_version(executable: str) -> Optional[_MuslVersion]: return None if ld is None or "musl" not in ld: return None - proc = subprocess.run([ld], stderr=subprocess.PIPE, universal_newlines=True) + proc = subprocess.run([ld], stderr=subprocess.PIPE, text=True) return _parse_musl_version(proc.stderr) -def platform_tags(arch: str) -> Iterator[str]: +def platform_tags(archs: Sequence[str]) -> Iterator[str]: """Generate musllinux tags compatible to the current platform. - :param arch: Should be the part of platform tag after the ``linux_`` - prefix, e.g. ``x86_64``. The ``linux_`` prefix is assumed as a - prerequisite for the current platform to be musllinux-compatible. + :param archs: Sequence of compatible architectures. + The first one shall be the closest to the actual architecture and be the part of + platform tag after the ``linux_`` prefix, e.g. ``x86_64``. + The ``linux_`` prefix is assumed as a prerequisite for the current platform to + be musllinux-compatible. :returns: An iterator of compatible musllinux tags. """ sys_musl = _get_musl_version(sys.executable) if sys_musl is None: # Python not dynamically linked against musl. return - for minor in range(sys_musl.minor, -1, -1): - yield f"musllinux_{sys_musl.major}_{minor}_{arch}" + for arch in archs: + for minor in range(sys_musl.minor, -1, -1): + yield f"musllinux_{sys_musl.major}_{minor}_{arch}" if __name__ == "__main__": # pragma: no cover diff --git a/lib/pkg_resources/_vendor/packaging/_parser.py b/lib/pkg_resources/_vendor/packaging/_parser.py index 5a18b758..684df754 100644 --- a/lib/pkg_resources/_vendor/packaging/_parser.py +++ b/lib/pkg_resources/_vendor/packaging/_parser.py @@ -252,7 +252,13 @@ def _parse_version_many(tokenizer: Tokenizer) -> str: # Recursive descent parser for marker expression # -------------------------------------------------------------------------------------- def parse_marker(source: str) -> MarkerList: - return _parse_marker(Tokenizer(source, rules=DEFAULT_RULES)) + return _parse_full_marker(Tokenizer(source, rules=DEFAULT_RULES)) + + +def _parse_full_marker(tokenizer: Tokenizer) -> MarkerList: + retval = _parse_marker(tokenizer) + tokenizer.expect("END", expected="end of marker expression") + return retval def _parse_marker(tokenizer: Tokenizer) -> MarkerList: @@ -318,10 +324,7 @@ def _parse_marker_var(tokenizer: Tokenizer) -> MarkerVar: def process_env_var(env_var: str) -> Variable: - if ( - env_var == "platform_python_implementation" - or env_var == "python_implementation" - ): + if env_var in ("platform_python_implementation", "python_implementation"): return Variable("platform_python_implementation") else: return Variable(env_var) diff --git a/lib/pkg_resources/_vendor/packaging/metadata.py b/lib/pkg_resources/_vendor/packaging/metadata.py index e76a60c3..fb274930 100644 --- a/lib/pkg_resources/_vendor/packaging/metadata.py +++ b/lib/pkg_resources/_vendor/packaging/metadata.py @@ -5,23 +5,77 @@ import email.parser import email.policy import sys import typing -from typing import Dict, List, Optional, Tuple, Union, cast +from typing import ( + Any, + Callable, + Dict, + Generic, + List, + Optional, + Tuple, + Type, + Union, + cast, +) -if sys.version_info >= (3, 8): # pragma: no cover - from typing import TypedDict +from . import requirements, specifiers, utils, version as version_module + +T = typing.TypeVar("T") +if sys.version_info[:2] >= (3, 8): # pragma: no cover + from typing import Literal, TypedDict else: # pragma: no cover if typing.TYPE_CHECKING: - from typing_extensions import TypedDict + from typing_extensions import Literal, TypedDict else: try: - from typing_extensions import TypedDict + from typing_extensions import Literal, TypedDict except ImportError: + class Literal: + def __init_subclass__(*_args, **_kwargs): + pass + class TypedDict: def __init_subclass__(*_args, **_kwargs): pass +try: + ExceptionGroup +except NameError: # pragma: no cover + + class ExceptionGroup(Exception): # noqa: N818 + """A minimal implementation of :external:exc:`ExceptionGroup` from Python 3.11. + + If :external:exc:`ExceptionGroup` is already defined by Python itself, + that version is used instead. + """ + + message: str + exceptions: List[Exception] + + def __init__(self, message: str, exceptions: List[Exception]) -> None: + self.message = message + self.exceptions = exceptions + + def __repr__(self) -> str: + return f"{self.__class__.__name__}({self.message!r}, {self.exceptions!r})" + +else: # pragma: no cover + ExceptionGroup = ExceptionGroup + + +class InvalidMetadata(ValueError): + """A metadata field contains invalid data.""" + + field: str + """The name of the field that contains invalid data.""" + + def __init__(self, field: str, message: str) -> None: + self.field = field + super().__init__(message) + + # The RawMetadata class attempts to make as few assumptions about the underlying # serialization formats as possible. The idea is that as long as a serialization # formats offer some very basic primitives in *some* way then we can support @@ -33,7 +87,8 @@ class RawMetadata(TypedDict, total=False): provided). The key is lower-case and underscores are used instead of dashes compared to the equivalent core metadata field. Any core metadata field that can be specified multiple times or can hold multiple values in a single - field have a key with a plural name. + field have a key with a plural name. See :class:`Metadata` whose attributes + match the keys of this dictionary. Core metadata fields that can be specified multiple times are stored as a list or dict depending on which is appropriate for the field. Any fields @@ -77,7 +132,7 @@ class RawMetadata(TypedDict, total=False): # but got stuck without ever being able to build consensus on # it and ultimately ended up withdrawn. # - # However, a number of tools had started emiting METADATA with + # However, a number of tools had started emitting METADATA with # `2.0` Metadata-Version, so for historical reasons, this version # was skipped. @@ -110,7 +165,7 @@ _STRING_FIELDS = { "version", } -_LIST_STRING_FIELDS = { +_LIST_FIELDS = { "classifiers", "dynamic", "obsoletes", @@ -125,6 +180,10 @@ _LIST_STRING_FIELDS = { "supported_platforms", } +_DICT_FIELDS = { + "project_urls", +} + def _parse_keywords(data: str) -> List[str]: """Split a string of comma-separate keyboards into a list of keywords.""" @@ -230,10 +289,11 @@ _EMAIL_TO_RAW_MAPPING = { "supported-platform": "supported_platforms", "version": "version", } +_RAW_TO_EMAIL_MAPPING = {raw: email for email, raw in _EMAIL_TO_RAW_MAPPING.items()} def parse_email(data: Union[bytes, str]) -> Tuple[RawMetadata, Dict[str, List[str]]]: - """Parse a distribution's metadata. + """Parse a distribution's metadata stored as email headers (e.g. from ``METADATA``). This function returns a two-item tuple of dicts. The first dict is of recognized fields from the core metadata specification. Fields that can be @@ -267,7 +327,7 @@ def parse_email(data: Union[bytes, str]) -> Tuple[RawMetadata, Dict[str, List[st # We use get_all() here, even for fields that aren't multiple use, # because otherwise someone could have e.g. two Name fields, and we # would just silently ignore it rather than doing something about it. - headers = parsed.get_all(name) + headers = parsed.get_all(name) or [] # The way the email module works when parsing bytes is that it # unconditionally decodes the bytes as ascii using the surrogateescape @@ -349,7 +409,7 @@ def parse_email(data: Union[bytes, str]) -> Tuple[RawMetadata, Dict[str, List[st # If this is one of our list of string fields, then we can just assign # the value, since email *only* has strings, and our get_all() call # above ensures that this is a list. - elif raw_name in _LIST_STRING_FIELDS: + elif raw_name in _LIST_FIELDS: raw[raw_name] = value # Special Case: Keywords # The keywords field is implemented in the metadata spec as a str, @@ -406,3 +466,360 @@ def parse_email(data: Union[bytes, str]) -> Tuple[RawMetadata, Dict[str, List[st # way this function is implemented, our `TypedDict` can only have valid key # names. return cast(RawMetadata, raw), unparsed + + +_NOT_FOUND = object() + + +# Keep the two values in sync. +_VALID_METADATA_VERSIONS = ["1.0", "1.1", "1.2", "2.1", "2.2", "2.3"] +_MetadataVersion = Literal["1.0", "1.1", "1.2", "2.1", "2.2", "2.3"] + +_REQUIRED_ATTRS = frozenset(["metadata_version", "name", "version"]) + + +class _Validator(Generic[T]): + """Validate a metadata field. + + All _process_*() methods correspond to a core metadata field. The method is + called with the field's raw value. If the raw value is valid it is returned + in its "enriched" form (e.g. ``version.Version`` for the ``Version`` field). + If the raw value is invalid, :exc:`InvalidMetadata` is raised (with a cause + as appropriate). + """ + + name: str + raw_name: str + added: _MetadataVersion + + def __init__( + self, + *, + added: _MetadataVersion = "1.0", + ) -> None: + self.added = added + + def __set_name__(self, _owner: "Metadata", name: str) -> None: + self.name = name + self.raw_name = _RAW_TO_EMAIL_MAPPING[name] + + def __get__(self, instance: "Metadata", _owner: Type["Metadata"]) -> T: + # With Python 3.8, the caching can be replaced with functools.cached_property(). + # No need to check the cache as attribute lookup will resolve into the + # instance's __dict__ before __get__ is called. + cache = instance.__dict__ + value = instance._raw.get(self.name) + + # To make the _process_* methods easier, we'll check if the value is None + # and if this field is NOT a required attribute, and if both of those + # things are true, we'll skip the the converter. This will mean that the + # converters never have to deal with the None union. + if self.name in _REQUIRED_ATTRS or value is not None: + try: + converter: Callable[[Any], T] = getattr(self, f"_process_{self.name}") + except AttributeError: + pass + else: + value = converter(value) + + cache[self.name] = value + try: + del instance._raw[self.name] # type: ignore[misc] + except KeyError: + pass + + return cast(T, value) + + def _invalid_metadata( + self, msg: str, cause: Optional[Exception] = None + ) -> InvalidMetadata: + exc = InvalidMetadata( + self.raw_name, msg.format_map({"field": repr(self.raw_name)}) + ) + exc.__cause__ = cause + return exc + + def _process_metadata_version(self, value: str) -> _MetadataVersion: + # Implicitly makes Metadata-Version required. + if value not in _VALID_METADATA_VERSIONS: + raise self._invalid_metadata(f"{value!r} is not a valid metadata version") + return cast(_MetadataVersion, value) + + def _process_name(self, value: str) -> str: + if not value: + raise self._invalid_metadata("{field} is a required field") + # Validate the name as a side-effect. + try: + utils.canonicalize_name(value, validate=True) + except utils.InvalidName as exc: + raise self._invalid_metadata( + f"{value!r} is invalid for {{field}}", cause=exc + ) + else: + return value + + def _process_version(self, value: str) -> version_module.Version: + if not value: + raise self._invalid_metadata("{field} is a required field") + try: + return version_module.parse(value) + except version_module.InvalidVersion as exc: + raise self._invalid_metadata( + f"{value!r} is invalid for {{field}}", cause=exc + ) + + def _process_summary(self, value: str) -> str: + """Check the field contains no newlines.""" + if "\n" in value: + raise self._invalid_metadata("{field} must be a single line") + return value + + def _process_description_content_type(self, value: str) -> str: + content_types = {"text/plain", "text/x-rst", "text/markdown"} + message = email.message.EmailMessage() + message["content-type"] = value + + content_type, parameters = ( + # Defaults to `text/plain` if parsing failed. + message.get_content_type().lower(), + message["content-type"].params, + ) + # Check if content-type is valid or defaulted to `text/plain` and thus was + # not parseable. + if content_type not in content_types or content_type not in value.lower(): + raise self._invalid_metadata( + f"{{field}} must be one of {list(content_types)}, not {value!r}" + ) + + charset = parameters.get("charset", "UTF-8") + if charset != "UTF-8": + raise self._invalid_metadata( + f"{{field}} can only specify the UTF-8 charset, not {list(charset)}" + ) + + markdown_variants = {"GFM", "CommonMark"} + variant = parameters.get("variant", "GFM") # Use an acceptable default. + if content_type == "text/markdown" and variant not in markdown_variants: + raise self._invalid_metadata( + f"valid Markdown variants for {{field}} are {list(markdown_variants)}, " + f"not {variant!r}", + ) + return value + + def _process_dynamic(self, value: List[str]) -> List[str]: + for dynamic_field in map(str.lower, value): + if dynamic_field in {"name", "version", "metadata-version"}: + raise self._invalid_metadata( + f"{value!r} is not allowed as a dynamic field" + ) + elif dynamic_field not in _EMAIL_TO_RAW_MAPPING: + raise self._invalid_metadata(f"{value!r} is not a valid dynamic field") + return list(map(str.lower, value)) + + def _process_provides_extra( + self, + value: List[str], + ) -> List[utils.NormalizedName]: + normalized_names = [] + try: + for name in value: + normalized_names.append(utils.canonicalize_name(name, validate=True)) + except utils.InvalidName as exc: + raise self._invalid_metadata( + f"{name!r} is invalid for {{field}}", cause=exc + ) + else: + return normalized_names + + def _process_requires_python(self, value: str) -> specifiers.SpecifierSet: + try: + return specifiers.SpecifierSet(value) + except specifiers.InvalidSpecifier as exc: + raise self._invalid_metadata( + f"{value!r} is invalid for {{field}}", cause=exc + ) + + def _process_requires_dist( + self, + value: List[str], + ) -> List[requirements.Requirement]: + reqs = [] + try: + for req in value: + reqs.append(requirements.Requirement(req)) + except requirements.InvalidRequirement as exc: + raise self._invalid_metadata(f"{req!r} is invalid for {{field}}", cause=exc) + else: + return reqs + + +class Metadata: + """Representation of distribution metadata. + + Compared to :class:`RawMetadata`, this class provides objects representing + metadata fields instead of only using built-in types. Any invalid metadata + will cause :exc:`InvalidMetadata` to be raised (with a + :py:attr:`~BaseException.__cause__` attribute as appropriate). + """ + + _raw: RawMetadata + + @classmethod + def from_raw(cls, data: RawMetadata, *, validate: bool = True) -> "Metadata": + """Create an instance from :class:`RawMetadata`. + + If *validate* is true, all metadata will be validated. All exceptions + related to validation will be gathered and raised as an :class:`ExceptionGroup`. + """ + ins = cls() + ins._raw = data.copy() # Mutations occur due to caching enriched values. + + if validate: + exceptions: List[Exception] = [] + try: + metadata_version = ins.metadata_version + metadata_age = _VALID_METADATA_VERSIONS.index(metadata_version) + except InvalidMetadata as metadata_version_exc: + exceptions.append(metadata_version_exc) + metadata_version = None + + # Make sure to check for the fields that are present, the required + # fields (so their absence can be reported). + fields_to_check = frozenset(ins._raw) | _REQUIRED_ATTRS + # Remove fields that have already been checked. + fields_to_check -= {"metadata_version"} + + for key in fields_to_check: + try: + if metadata_version: + # Can't use getattr() as that triggers descriptor protocol which + # will fail due to no value for the instance argument. + try: + field_metadata_version = cls.__dict__[key].added + except KeyError: + exc = InvalidMetadata(key, f"unrecognized field: {key!r}") + exceptions.append(exc) + continue + field_age = _VALID_METADATA_VERSIONS.index( + field_metadata_version + ) + if field_age > metadata_age: + field = _RAW_TO_EMAIL_MAPPING[key] + exc = InvalidMetadata( + field, + "{field} introduced in metadata version " + "{field_metadata_version}, not {metadata_version}", + ) + exceptions.append(exc) + continue + getattr(ins, key) + except InvalidMetadata as exc: + exceptions.append(exc) + + if exceptions: + raise ExceptionGroup("invalid metadata", exceptions) + + return ins + + @classmethod + def from_email( + cls, data: Union[bytes, str], *, validate: bool = True + ) -> "Metadata": + """Parse metadata from email headers. + + If *validate* is true, the metadata will be validated. All exceptions + related to validation will be gathered and raised as an :class:`ExceptionGroup`. + """ + raw, unparsed = parse_email(data) + + if validate: + exceptions: list[Exception] = [] + for unparsed_key in unparsed: + if unparsed_key in _EMAIL_TO_RAW_MAPPING: + message = f"{unparsed_key!r} has invalid data" + else: + message = f"unrecognized field: {unparsed_key!r}" + exceptions.append(InvalidMetadata(unparsed_key, message)) + + if exceptions: + raise ExceptionGroup("unparsed", exceptions) + + try: + return cls.from_raw(raw, validate=validate) + except ExceptionGroup as exc_group: + raise ExceptionGroup( + "invalid or unparsed metadata", exc_group.exceptions + ) from None + + metadata_version: _Validator[_MetadataVersion] = _Validator() + """:external:ref:`core-metadata-metadata-version` + (required; validated to be a valid metadata version)""" + name: _Validator[str] = _Validator() + """:external:ref:`core-metadata-name` + (required; validated using :func:`~packaging.utils.canonicalize_name` and its + *validate* parameter)""" + version: _Validator[version_module.Version] = _Validator() + """:external:ref:`core-metadata-version` (required)""" + dynamic: _Validator[Optional[List[str]]] = _Validator( + added="2.2", + ) + """:external:ref:`core-metadata-dynamic` + (validated against core metadata field names and lowercased)""" + platforms: _Validator[Optional[List[str]]] = _Validator() + """:external:ref:`core-metadata-platform`""" + supported_platforms: _Validator[Optional[List[str]]] = _Validator(added="1.1") + """:external:ref:`core-metadata-supported-platform`""" + summary: _Validator[Optional[str]] = _Validator() + """:external:ref:`core-metadata-summary` (validated to contain no newlines)""" + description: _Validator[Optional[str]] = _Validator() # TODO 2.1: can be in body + """:external:ref:`core-metadata-description`""" + description_content_type: _Validator[Optional[str]] = _Validator(added="2.1") + """:external:ref:`core-metadata-description-content-type` (validated)""" + keywords: _Validator[Optional[List[str]]] = _Validator() + """:external:ref:`core-metadata-keywords`""" + home_page: _Validator[Optional[str]] = _Validator() + """:external:ref:`core-metadata-home-page`""" + download_url: _Validator[Optional[str]] = _Validator(added="1.1") + """:external:ref:`core-metadata-download-url`""" + author: _Validator[Optional[str]] = _Validator() + """:external:ref:`core-metadata-author`""" + author_email: _Validator[Optional[str]] = _Validator() + """:external:ref:`core-metadata-author-email`""" + maintainer: _Validator[Optional[str]] = _Validator(added="1.2") + """:external:ref:`core-metadata-maintainer`""" + maintainer_email: _Validator[Optional[str]] = _Validator(added="1.2") + """:external:ref:`core-metadata-maintainer-email`""" + license: _Validator[Optional[str]] = _Validator() + """:external:ref:`core-metadata-license`""" + classifiers: _Validator[Optional[List[str]]] = _Validator(added="1.1") + """:external:ref:`core-metadata-classifier`""" + requires_dist: _Validator[Optional[List[requirements.Requirement]]] = _Validator( + added="1.2" + ) + """:external:ref:`core-metadata-requires-dist`""" + requires_python: _Validator[Optional[specifiers.SpecifierSet]] = _Validator( + added="1.2" + ) + """:external:ref:`core-metadata-requires-python`""" + # Because `Requires-External` allows for non-PEP 440 version specifiers, we + # don't do any processing on the values. + requires_external: _Validator[Optional[List[str]]] = _Validator(added="1.2") + """:external:ref:`core-metadata-requires-external`""" + project_urls: _Validator[Optional[Dict[str, str]]] = _Validator(added="1.2") + """:external:ref:`core-metadata-project-url`""" + # PEP 685 lets us raise an error if an extra doesn't pass `Name` validation + # regardless of metadata version. + provides_extra: _Validator[Optional[List[utils.NormalizedName]]] = _Validator( + added="2.1", + ) + """:external:ref:`core-metadata-provides-extra`""" + provides_dist: _Validator[Optional[List[str]]] = _Validator(added="1.2") + """:external:ref:`core-metadata-provides-dist`""" + obsoletes_dist: _Validator[Optional[List[str]]] = _Validator(added="1.2") + """:external:ref:`core-metadata-obsoletes-dist`""" + requires: _Validator[Optional[List[str]]] = _Validator(added="1.1") + """``Requires`` (deprecated)""" + provides: _Validator[Optional[List[str]]] = _Validator(added="1.1") + """``Provides`` (deprecated)""" + obsoletes: _Validator[Optional[List[str]]] = _Validator(added="1.1") + """``Obsoletes`` (deprecated)""" diff --git a/lib/pkg_resources/_vendor/packaging/requirements.py b/lib/pkg_resources/_vendor/packaging/requirements.py index f34bfa85..bdc43a7e 100644 --- a/lib/pkg_resources/_vendor/packaging/requirements.py +++ b/lib/pkg_resources/_vendor/packaging/requirements.py @@ -2,13 +2,13 @@ # 2.0, and the BSD License. See the LICENSE file in the root of this repository # for complete details. -import urllib.parse -from typing import Any, List, Optional, Set +from typing import Any, Iterator, Optional, Set from ._parser import parse_requirement as _parse_requirement from ._tokenizer import ParserSyntaxError from .markers import Marker, _normalize_extra_values from .specifiers import SpecifierSet +from .utils import canonicalize_name class InvalidRequirement(ValueError): @@ -37,57 +37,52 @@ class Requirement: raise InvalidRequirement(str(e)) from e self.name: str = parsed.name - if parsed.url: - parsed_url = urllib.parse.urlparse(parsed.url) - if parsed_url.scheme == "file": - if urllib.parse.urlunparse(parsed_url) != parsed.url: - raise InvalidRequirement("Invalid URL given") - elif not (parsed_url.scheme and parsed_url.netloc) or ( - not parsed_url.scheme and not parsed_url.netloc - ): - raise InvalidRequirement(f"Invalid URL: {parsed.url}") - self.url: Optional[str] = parsed.url - else: - self.url = None - self.extras: Set[str] = set(parsed.extras if parsed.extras else []) + self.url: Optional[str] = parsed.url or None + self.extras: Set[str] = set(parsed.extras or []) self.specifier: SpecifierSet = SpecifierSet(parsed.specifier) self.marker: Optional[Marker] = None if parsed.marker is not None: self.marker = Marker.__new__(Marker) self.marker._markers = _normalize_extra_values(parsed.marker) - def __str__(self) -> str: - parts: List[str] = [self.name] + def _iter_parts(self, name: str) -> Iterator[str]: + yield name if self.extras: formatted_extras = ",".join(sorted(self.extras)) - parts.append(f"[{formatted_extras}]") + yield f"[{formatted_extras}]" if self.specifier: - parts.append(str(self.specifier)) + yield str(self.specifier) if self.url: - parts.append(f"@ {self.url}") + yield f"@ {self.url}" if self.marker: - parts.append(" ") + yield " " if self.marker: - parts.append(f"; {self.marker}") + yield f"; {self.marker}" - return "".join(parts) + def __str__(self) -> str: + return "".join(self._iter_parts(self.name)) def __repr__(self) -> str: return f"" def __hash__(self) -> int: - return hash((self.__class__.__name__, str(self))) + return hash( + ( + self.__class__.__name__, + *self._iter_parts(canonicalize_name(self.name)), + ) + ) def __eq__(self, other: Any) -> bool: if not isinstance(other, Requirement): return NotImplemented return ( - self.name == other.name + canonicalize_name(self.name) == canonicalize_name(other.name) and self.extras == other.extras and self.specifier == other.specifier and self.url == other.url diff --git a/lib/pkg_resources/_vendor/packaging/specifiers.py b/lib/pkg_resources/_vendor/packaging/specifiers.py index ba8fe37b..2d015bab 100644 --- a/lib/pkg_resources/_vendor/packaging/specifiers.py +++ b/lib/pkg_resources/_vendor/packaging/specifiers.py @@ -11,17 +11,7 @@ import abc import itertools import re -from typing import ( - Callable, - Iterable, - Iterator, - List, - Optional, - Set, - Tuple, - TypeVar, - Union, -) +from typing import Callable, Iterable, Iterator, List, Optional, Tuple, TypeVar, Union from .utils import canonicalize_version from .version import Version @@ -383,7 +373,7 @@ class Specifier(BaseSpecifier): # We want everything but the last item in the version, but we want to # ignore suffix segments. - prefix = ".".join( + prefix = _version_join( list(itertools.takewhile(_is_not_suffix, _version_split(spec)))[:-1] ) @@ -404,13 +394,13 @@ class Specifier(BaseSpecifier): ) # Get the normalized version string ignoring the trailing .* normalized_spec = canonicalize_version(spec[:-2], strip_trailing_zero=False) - # Split the spec out by dots, and pretend that there is an implicit - # dot in between a release segment and a pre-release segment. + # Split the spec out by bangs and dots, and pretend that there is + # an implicit dot in between a release segment and a pre-release segment. split_spec = _version_split(normalized_spec) - # Split the prospective version out by dots, and pretend that there - # is an implicit dot in between a release segment and a pre-release - # segment. + # Split the prospective version out by bangs and dots, and pretend + # that there is an implicit dot in between a release segment and + # a pre-release segment. split_prospective = _version_split(normalized_prospective) # 0-pad the prospective version before shortening it to get the correct @@ -644,8 +634,19 @@ _prefix_regex = re.compile(r"^([0-9]+)((?:a|b|c|rc)[0-9]+)$") def _version_split(version: str) -> List[str]: + """Split version into components. + + The split components are intended for version comparison. The logic does + not attempt to retain the original version string, so joining the + components back with :func:`_version_join` may not produce the original + version string. + """ result: List[str] = [] - for item in version.split("."): + + epoch, _, rest = version.rpartition("!") + result.append(epoch or "0") + + for item in rest.split("."): match = _prefix_regex.search(item) if match: result.extend(match.groups()) @@ -654,6 +655,17 @@ def _version_split(version: str) -> List[str]: return result +def _version_join(components: List[str]) -> str: + """Join split version components into a version string. + + This function assumes the input came from :func:`_version_split`, where the + first component must be the epoch (either empty or numeric), and all other + components numeric. + """ + epoch, *rest = components + return f"{epoch}!{'.'.join(rest)}" + + def _is_not_suffix(segment: str) -> bool: return not any( segment.startswith(prefix) for prefix in ("dev", "a", "b", "rc", "post") @@ -675,7 +687,10 @@ def _pad_version(left: List[str], right: List[str]) -> Tuple[List[str], List[str left_split.insert(1, ["0"] * max(0, len(right_split[0]) - len(left_split[0]))) right_split.insert(1, ["0"] * max(0, len(left_split[0]) - len(right_split[0]))) - return (list(itertools.chain(*left_split)), list(itertools.chain(*right_split))) + return ( + list(itertools.chain.from_iterable(left_split)), + list(itertools.chain.from_iterable(right_split)), + ) class SpecifierSet(BaseSpecifier): @@ -707,14 +722,8 @@ class SpecifierSet(BaseSpecifier): # strip each item to remove leading/trailing whitespace. split_specifiers = [s.strip() for s in specifiers.split(",") if s.strip()] - # Parsed each individual specifier, attempting first to make it a - # Specifier. - parsed: Set[Specifier] = set() - for specifier in split_specifiers: - parsed.add(Specifier(specifier)) - - # Turn our parsed specifiers into a frozen set and save them for later. - self._specs = frozenset(parsed) + # Make each individual specifier a Specifier and save in a frozen set for later. + self._specs = frozenset(map(Specifier, split_specifiers)) # Store our prereleases value so we can use it later to determine if # we accept prereleases or not. diff --git a/lib/pkg_resources/_vendor/packaging/tags.py b/lib/pkg_resources/_vendor/packaging/tags.py index 76d24341..89f19261 100644 --- a/lib/pkg_resources/_vendor/packaging/tags.py +++ b/lib/pkg_resources/_vendor/packaging/tags.py @@ -4,6 +4,8 @@ import logging import platform +import re +import struct import subprocess import sys import sysconfig @@ -37,7 +39,7 @@ INTERPRETER_SHORT_NAMES: Dict[str, str] = { } -_32_BIT_INTERPRETER = sys.maxsize <= 2**32 +_32_BIT_INTERPRETER = struct.calcsize("P") == 4 class Tag: @@ -123,20 +125,37 @@ def _normalize_string(string: str) -> str: return string.replace(".", "_").replace("-", "_").replace(" ", "_") -def _abi3_applies(python_version: PythonVersion) -> bool: +def _is_threaded_cpython(abis: List[str]) -> bool: + """ + Determine if the ABI corresponds to a threaded (`--disable-gil`) build. + + The threaded builds are indicated by a "t" in the abiflags. + """ + if len(abis) == 0: + return False + # expect e.g., cp313 + m = re.match(r"cp\d+(.*)", abis[0]) + if not m: + return False + abiflags = m.group(1) + return "t" in abiflags + + +def _abi3_applies(python_version: PythonVersion, threading: bool) -> bool: """ Determine if the Python version supports abi3. - PEP 384 was first implemented in Python 3.2. + PEP 384 was first implemented in Python 3.2. The threaded (`--disable-gil`) + builds do not support abi3. """ - return len(python_version) > 1 and tuple(python_version) >= (3, 2) + return len(python_version) > 1 and tuple(python_version) >= (3, 2) and not threading def _cpython_abis(py_version: PythonVersion, warn: bool = False) -> List[str]: py_version = tuple(py_version) # To allow for version comparison. abis = [] version = _version_nodot(py_version[:2]) - debug = pymalloc = ucs4 = "" + threading = debug = pymalloc = ucs4 = "" with_debug = _get_config_var("Py_DEBUG", warn) has_refcount = hasattr(sys, "gettotalrefcount") # Windows doesn't set Py_DEBUG, so checking for support of debug-compiled @@ -145,6 +164,8 @@ def _cpython_abis(py_version: PythonVersion, warn: bool = False) -> List[str]: has_ext = "_d.pyd" in EXTENSION_SUFFIXES if with_debug or (with_debug is None and (has_refcount or has_ext)): debug = "d" + if py_version >= (3, 13) and _get_config_var("Py_GIL_DISABLED", warn): + threading = "t" if py_version < (3, 8): with_pymalloc = _get_config_var("WITH_PYMALLOC", warn) if with_pymalloc or with_pymalloc is None: @@ -158,13 +179,8 @@ def _cpython_abis(py_version: PythonVersion, warn: bool = False) -> List[str]: elif debug: # Debug builds can also load "normal" extension modules. # We can also assume no UCS-4 or pymalloc requirement. - abis.append(f"cp{version}") - abis.insert( - 0, - "cp{version}{debug}{pymalloc}{ucs4}".format( - version=version, debug=debug, pymalloc=pymalloc, ucs4=ucs4 - ), - ) + abis.append(f"cp{version}{threading}") + abis.insert(0, f"cp{version}{threading}{debug}{pymalloc}{ucs4}") return abis @@ -212,11 +228,14 @@ def cpython_tags( for abi in abis: for platform_ in platforms: yield Tag(interpreter, abi, platform_) - if _abi3_applies(python_version): + + threading = _is_threaded_cpython(abis) + use_abi3 = _abi3_applies(python_version, threading) + if use_abi3: yield from (Tag(interpreter, "abi3", platform_) for platform_ in platforms) yield from (Tag(interpreter, "none", platform_) for platform_ in platforms) - if _abi3_applies(python_version): + if use_abi3: for minor_version in range(python_version[1] - 1, 1, -1): for platform_ in platforms: interpreter = "cp{version}".format( @@ -406,7 +425,7 @@ def mac_platforms( check=True, env={"SYSTEM_VERSION_COMPAT": "0"}, stdout=subprocess.PIPE, - universal_newlines=True, + text=True, ).stdout version = cast("MacVersion", tuple(map(int, version_str.split(".")[:2]))) else: @@ -469,15 +488,21 @@ def mac_platforms( def _linux_platforms(is_32bit: bool = _32_BIT_INTERPRETER) -> Iterator[str]: linux = _normalize_string(sysconfig.get_platform()) + if not linux.startswith("linux_"): + # we should never be here, just yield the sysconfig one and return + yield linux + return if is_32bit: if linux == "linux_x86_64": linux = "linux_i686" elif linux == "linux_aarch64": - linux = "linux_armv7l" + linux = "linux_armv8l" _, arch = linux.split("_", 1) - yield from _manylinux.platform_tags(linux, arch) - yield from _musllinux.platform_tags(arch) - yield linux + archs = {"armv8l": ["armv8l", "armv7l"]}.get(arch, [arch]) + yield from _manylinux.platform_tags(archs) + yield from _musllinux.platform_tags(archs) + for arch in archs: + yield f"linux_{arch}" def _generic_platforms() -> Iterator[str]: diff --git a/lib/pkg_resources/_vendor/packaging/utils.py b/lib/pkg_resources/_vendor/packaging/utils.py index 33c613b7..c2c2f75a 100644 --- a/lib/pkg_resources/_vendor/packaging/utils.py +++ b/lib/pkg_resources/_vendor/packaging/utils.py @@ -12,6 +12,12 @@ BuildTag = Union[Tuple[()], Tuple[int, str]] NormalizedName = NewType("NormalizedName", str) +class InvalidName(ValueError): + """ + An invalid distribution name; users should refer to the packaging user guide. + """ + + class InvalidWheelFilename(ValueError): """ An invalid wheel filename was found, users should refer to PEP 427. @@ -24,17 +30,28 @@ class InvalidSdistFilename(ValueError): """ +# Core metadata spec for `Name` +_validate_regex = re.compile( + r"^([A-Z0-9]|[A-Z0-9][A-Z0-9._-]*[A-Z0-9])$", re.IGNORECASE +) _canonicalize_regex = re.compile(r"[-_.]+") +_normalized_regex = re.compile(r"^([a-z0-9]|[a-z0-9]([a-z0-9-](?!--))*[a-z0-9])$") # PEP 427: The build number must start with a digit. _build_tag_regex = re.compile(r"(\d+)(.*)") -def canonicalize_name(name: str) -> NormalizedName: +def canonicalize_name(name: str, *, validate: bool = False) -> NormalizedName: + if validate and not _validate_regex.match(name): + raise InvalidName(f"name is invalid: {name!r}") # This is taken from PEP 503. value = _canonicalize_regex.sub("-", name).lower() return cast(NormalizedName, value) +def is_normalized_name(name: str) -> bool: + return _normalized_regex.match(name) is not None + + def canonicalize_version( version: Union[Version, str], *, strip_trailing_zero: bool = True ) -> str: @@ -100,11 +117,18 @@ def parse_wheel_filename( parts = filename.split("-", dashes - 2) name_part = parts[0] - # See PEP 427 for the rules on escaping the project name + # See PEP 427 for the rules on escaping the project name. if "__" in name_part or re.match(r"^[\w\d._]*$", name_part, re.UNICODE) is None: raise InvalidWheelFilename(f"Invalid project name: {filename}") name = canonicalize_name(name_part) - version = Version(parts[1]) + + try: + version = Version(parts[1]) + except InvalidVersion as e: + raise InvalidWheelFilename( + f"Invalid wheel filename (invalid version): {filename}" + ) from e + if dashes == 5: build_part = parts[2] build_match = _build_tag_regex.match(build_part) @@ -137,5 +161,12 @@ def parse_sdist_filename(filename: str) -> Tuple[NormalizedName, Version]: raise InvalidSdistFilename(f"Invalid sdist filename: {filename}") name = canonicalize_name(name_part) - version = Version(version_part) + + try: + version = Version(version_part) + except InvalidVersion as e: + raise InvalidSdistFilename( + f"Invalid sdist filename (invalid version): {filename}" + ) from e + return (name, version) diff --git a/lib/pkg_resources/_vendor/packaging/version.py b/lib/pkg_resources/_vendor/packaging/version.py index b30e8cbf..5faab9bd 100644 --- a/lib/pkg_resources/_vendor/packaging/version.py +++ b/lib/pkg_resources/_vendor/packaging/version.py @@ -7,37 +7,39 @@ from packaging.version import parse, Version """ -import collections import itertools import re -from typing import Any, Callable, Optional, SupportsInt, Tuple, Union +from typing import Any, Callable, NamedTuple, Optional, SupportsInt, Tuple, Union from ._structures import Infinity, InfinityType, NegativeInfinity, NegativeInfinityType __all__ = ["VERSION_PATTERN", "parse", "Version", "InvalidVersion"] -InfiniteTypes = Union[InfinityType, NegativeInfinityType] -PrePostDevType = Union[InfiniteTypes, Tuple[str, int]] -SubLocalType = Union[InfiniteTypes, int, str] -LocalType = Union[ +LocalType = Tuple[Union[int, str], ...] + +CmpPrePostDevType = Union[InfinityType, NegativeInfinityType, Tuple[str, int]] +CmpLocalType = Union[ NegativeInfinityType, - Tuple[ - Union[ - SubLocalType, - Tuple[SubLocalType, str], - Tuple[NegativeInfinityType, SubLocalType], - ], - ..., - ], + Tuple[Union[Tuple[int, str], Tuple[NegativeInfinityType, Union[int, str]]], ...], ] CmpKey = Tuple[ - int, Tuple[int, ...], PrePostDevType, PrePostDevType, PrePostDevType, LocalType + int, + Tuple[int, ...], + CmpPrePostDevType, + CmpPrePostDevType, + CmpPrePostDevType, + CmpLocalType, ] VersionComparisonMethod = Callable[[CmpKey, CmpKey], bool] -_Version = collections.namedtuple( - "_Version", ["epoch", "release", "dev", "pre", "post", "local"] -) + +class _Version(NamedTuple): + epoch: int + release: Tuple[int, ...] + dev: Optional[Tuple[str, int]] + pre: Optional[Tuple[str, int]] + post: Optional[Tuple[str, int]] + local: Optional[LocalType] def parse(version: str) -> "Version": @@ -117,7 +119,7 @@ _VERSION_PATTERN = r""" (?P[0-9]+(?:\.[0-9]+)*) # release segment (?P
                                          # pre-release
             [-_\.]?
-            (?P(a|b|c|rc|alpha|beta|pre|preview))
+            (?Palpha|a|beta|b|preview|pre|c|rc)
             [-_\.]?
             (?P[0-9]+)?
         )?
@@ -269,8 +271,7 @@ class Version(_BaseVersion):
         >>> Version("1!2.0.0").epoch
         1
         """
-        _epoch: int = self._version.epoch
-        return _epoch
+        return self._version.epoch
 
     @property
     def release(self) -> Tuple[int, ...]:
@@ -286,8 +287,7 @@ class Version(_BaseVersion):
         Includes trailing zeroes but not the epoch or any pre-release / development /
         post-release suffixes.
         """
-        _release: Tuple[int, ...] = self._version.release
-        return _release
+        return self._version.release
 
     @property
     def pre(self) -> Optional[Tuple[str, int]]:
@@ -302,8 +302,7 @@ class Version(_BaseVersion):
         >>> Version("1.2.3rc1").pre
         ('rc', 1)
         """
-        _pre: Optional[Tuple[str, int]] = self._version.pre
-        return _pre
+        return self._version.pre
 
     @property
     def post(self) -> Optional[int]:
@@ -451,7 +450,7 @@ class Version(_BaseVersion):
 
 
 def _parse_letter_version(
-    letter: str, number: Union[str, bytes, SupportsInt]
+    letter: Optional[str], number: Union[str, bytes, SupportsInt, None]
 ) -> Optional[Tuple[str, int]]:
 
     if letter:
@@ -489,7 +488,7 @@ def _parse_letter_version(
 _local_version_separators = re.compile(r"[\._-]")
 
 
-def _parse_local_version(local: str) -> Optional[LocalType]:
+def _parse_local_version(local: Optional[str]) -> Optional[LocalType]:
     """
     Takes a string like abc.1.twelve and turns it into ("abc", 1, "twelve").
     """
@@ -507,7 +506,7 @@ def _cmpkey(
     pre: Optional[Tuple[str, int]],
     post: Optional[Tuple[str, int]],
     dev: Optional[Tuple[str, int]],
-    local: Optional[Tuple[SubLocalType]],
+    local: Optional[LocalType],
 ) -> CmpKey:
 
     # When we compare a release version, we want to compare it with all of the
@@ -524,7 +523,7 @@ def _cmpkey(
     # if there is not a pre or a post segment. If we have one of those then
     # the normal sorting rules will handle this case correctly.
     if pre is None and post is None and dev is not None:
-        _pre: PrePostDevType = NegativeInfinity
+        _pre: CmpPrePostDevType = NegativeInfinity
     # Versions without a pre-release (except as noted above) should sort after
     # those with one.
     elif pre is None:
@@ -534,21 +533,21 @@ def _cmpkey(
 
     # Versions without a post segment should sort before those with one.
     if post is None:
-        _post: PrePostDevType = NegativeInfinity
+        _post: CmpPrePostDevType = NegativeInfinity
 
     else:
         _post = post
 
     # Versions without a development segment should sort after those with one.
     if dev is None:
-        _dev: PrePostDevType = Infinity
+        _dev: CmpPrePostDevType = Infinity
 
     else:
         _dev = dev
 
     if local is None:
         # Versions without a local segment should sort before those with one.
-        _local: LocalType = NegativeInfinity
+        _local: CmpLocalType = NegativeInfinity
     else:
         # Versions with a local segment need that segment parsed to implement
         # the sorting rules in PEP440.
diff --git a/lib/pkg_resources/_vendor/ruff.toml b/lib/pkg_resources/_vendor/ruff.toml
new file mode 100644
index 00000000..00fee625
--- /dev/null
+++ b/lib/pkg_resources/_vendor/ruff.toml
@@ -0,0 +1 @@
+exclude = ["*"]
diff --git a/lib/pkg_resources/_vendor/typing_extensions.py b/lib/pkg_resources/_vendor/typing_extensions.py
deleted file mode 100644
index ef42417c..00000000
--- a/lib/pkg_resources/_vendor/typing_extensions.py
+++ /dev/null
@@ -1,2209 +0,0 @@
-import abc
-import collections
-import collections.abc
-import functools
-import operator
-import sys
-import types as _types
-import typing
-
-
-__all__ = [
-    # Super-special typing primitives.
-    'Any',
-    'ClassVar',
-    'Concatenate',
-    'Final',
-    'LiteralString',
-    'ParamSpec',
-    'ParamSpecArgs',
-    'ParamSpecKwargs',
-    'Self',
-    'Type',
-    'TypeVar',
-    'TypeVarTuple',
-    'Unpack',
-
-    # ABCs (from collections.abc).
-    'Awaitable',
-    'AsyncIterator',
-    'AsyncIterable',
-    'Coroutine',
-    'AsyncGenerator',
-    'AsyncContextManager',
-    'ChainMap',
-
-    # Concrete collection types.
-    'ContextManager',
-    'Counter',
-    'Deque',
-    'DefaultDict',
-    'NamedTuple',
-    'OrderedDict',
-    'TypedDict',
-
-    # Structural checks, a.k.a. protocols.
-    'SupportsIndex',
-
-    # One-off things.
-    'Annotated',
-    'assert_never',
-    'assert_type',
-    'clear_overloads',
-    'dataclass_transform',
-    'get_overloads',
-    'final',
-    'get_args',
-    'get_origin',
-    'get_type_hints',
-    'IntVar',
-    'is_typeddict',
-    'Literal',
-    'NewType',
-    'overload',
-    'override',
-    'Protocol',
-    'reveal_type',
-    'runtime',
-    'runtime_checkable',
-    'Text',
-    'TypeAlias',
-    'TypeGuard',
-    'TYPE_CHECKING',
-    'Never',
-    'NoReturn',
-    'Required',
-    'NotRequired',
-]
-
-# for backward compatibility
-PEP_560 = True
-GenericMeta = type
-
-# The functions below are modified copies of typing internal helpers.
-# They are needed by _ProtocolMeta and they provide support for PEP 646.
-
-_marker = object()
-
-
-def _check_generic(cls, parameters, elen=_marker):
-    """Check correct count for parameters of a generic cls (internal helper).
-    This gives a nice error message in case of count mismatch.
-    """
-    if not elen:
-        raise TypeError(f"{cls} is not a generic class")
-    if elen is _marker:
-        if not hasattr(cls, "__parameters__") or not cls.__parameters__:
-            raise TypeError(f"{cls} is not a generic class")
-        elen = len(cls.__parameters__)
-    alen = len(parameters)
-    if alen != elen:
-        if hasattr(cls, "__parameters__"):
-            parameters = [p for p in cls.__parameters__ if not _is_unpack(p)]
-            num_tv_tuples = sum(isinstance(p, TypeVarTuple) for p in parameters)
-            if (num_tv_tuples > 0) and (alen >= elen - num_tv_tuples):
-                return
-        raise TypeError(f"Too {'many' if alen > elen else 'few'} parameters for {cls};"
-                        f" actual {alen}, expected {elen}")
-
-
-if sys.version_info >= (3, 10):
-    def _should_collect_from_parameters(t):
-        return isinstance(
-            t, (typing._GenericAlias, _types.GenericAlias, _types.UnionType)
-        )
-elif sys.version_info >= (3, 9):
-    def _should_collect_from_parameters(t):
-        return isinstance(t, (typing._GenericAlias, _types.GenericAlias))
-else:
-    def _should_collect_from_parameters(t):
-        return isinstance(t, typing._GenericAlias) and not t._special
-
-
-def _collect_type_vars(types, typevar_types=None):
-    """Collect all type variable contained in types in order of
-    first appearance (lexicographic order). For example::
-
-        _collect_type_vars((T, List[S, T])) == (T, S)
-    """
-    if typevar_types is None:
-        typevar_types = typing.TypeVar
-    tvars = []
-    for t in types:
-        if (
-            isinstance(t, typevar_types) and
-            t not in tvars and
-            not _is_unpack(t)
-        ):
-            tvars.append(t)
-        if _should_collect_from_parameters(t):
-            tvars.extend([t for t in t.__parameters__ if t not in tvars])
-    return tuple(tvars)
-
-
-NoReturn = typing.NoReturn
-
-# Some unconstrained type variables.  These are used by the container types.
-# (These are not for export.)
-T = typing.TypeVar('T')  # Any type.
-KT = typing.TypeVar('KT')  # Key type.
-VT = typing.TypeVar('VT')  # Value type.
-T_co = typing.TypeVar('T_co', covariant=True)  # Any type covariant containers.
-T_contra = typing.TypeVar('T_contra', contravariant=True)  # Ditto contravariant.
-
-
-if sys.version_info >= (3, 11):
-    from typing import Any
-else:
-
-    class _AnyMeta(type):
-        def __instancecheck__(self, obj):
-            if self is Any:
-                raise TypeError("typing_extensions.Any cannot be used with isinstance()")
-            return super().__instancecheck__(obj)
-
-        def __repr__(self):
-            if self is Any:
-                return "typing_extensions.Any"
-            return super().__repr__()
-
-    class Any(metaclass=_AnyMeta):
-        """Special type indicating an unconstrained type.
-        - Any is compatible with every type.
-        - Any assumed to have all methods.
-        - All values assumed to be instances of Any.
-        Note that all the above statements are true from the point of view of
-        static type checkers. At runtime, Any should not be used with instance
-        checks.
-        """
-        def __new__(cls, *args, **kwargs):
-            if cls is Any:
-                raise TypeError("Any cannot be instantiated")
-            return super().__new__(cls, *args, **kwargs)
-
-
-ClassVar = typing.ClassVar
-
-# On older versions of typing there is an internal class named "Final".
-# 3.8+
-if hasattr(typing, 'Final') and sys.version_info[:2] >= (3, 7):
-    Final = typing.Final
-# 3.7
-else:
-    class _FinalForm(typing._SpecialForm, _root=True):
-
-        def __repr__(self):
-            return 'typing_extensions.' + self._name
-
-        def __getitem__(self, parameters):
-            item = typing._type_check(parameters,
-                                      f'{self._name} accepts only a single type.')
-            return typing._GenericAlias(self, (item,))
-
-    Final = _FinalForm('Final',
-                       doc="""A special typing construct to indicate that a name
-                       cannot be re-assigned or overridden in a subclass.
-                       For example:
-
-                           MAX_SIZE: Final = 9000
-                           MAX_SIZE += 1  # Error reported by type checker
-
-                           class Connection:
-                               TIMEOUT: Final[int] = 10
-                           class FastConnector(Connection):
-                               TIMEOUT = 1  # Error reported by type checker
-
-                       There is no runtime checking of these properties.""")
-
-if sys.version_info >= (3, 11):
-    final = typing.final
-else:
-    # @final exists in 3.8+, but we backport it for all versions
-    # before 3.11 to keep support for the __final__ attribute.
-    # See https://bugs.python.org/issue46342
-    def final(f):
-        """This decorator can be used to indicate to type checkers that
-        the decorated method cannot be overridden, and decorated class
-        cannot be subclassed. For example:
-
-            class Base:
-                @final
-                def done(self) -> None:
-                    ...
-            class Sub(Base):
-                def done(self) -> None:  # Error reported by type checker
-                    ...
-            @final
-            class Leaf:
-                ...
-            class Other(Leaf):  # Error reported by type checker
-                ...
-
-        There is no runtime checking of these properties. The decorator
-        sets the ``__final__`` attribute to ``True`` on the decorated object
-        to allow runtime introspection.
-        """
-        try:
-            f.__final__ = True
-        except (AttributeError, TypeError):
-            # Skip the attribute silently if it is not writable.
-            # AttributeError happens if the object has __slots__ or a
-            # read-only property, TypeError if it's a builtin class.
-            pass
-        return f
-
-
-def IntVar(name):
-    return typing.TypeVar(name)
-
-
-# 3.8+:
-if hasattr(typing, 'Literal'):
-    Literal = typing.Literal
-# 3.7:
-else:
-    class _LiteralForm(typing._SpecialForm, _root=True):
-
-        def __repr__(self):
-            return 'typing_extensions.' + self._name
-
-        def __getitem__(self, parameters):
-            return typing._GenericAlias(self, parameters)
-
-    Literal = _LiteralForm('Literal',
-                           doc="""A type that can be used to indicate to type checkers
-                           that the corresponding value has a value literally equivalent
-                           to the provided parameter. For example:
-
-                               var: Literal[4] = 4
-
-                           The type checker understands that 'var' is literally equal to
-                           the value 4 and no other value.
-
-                           Literal[...] cannot be subclassed. There is no runtime
-                           checking verifying that the parameter is actually a value
-                           instead of a type.""")
-
-
-_overload_dummy = typing._overload_dummy  # noqa
-
-
-if hasattr(typing, "get_overloads"):  # 3.11+
-    overload = typing.overload
-    get_overloads = typing.get_overloads
-    clear_overloads = typing.clear_overloads
-else:
-    # {module: {qualname: {firstlineno: func}}}
-    _overload_registry = collections.defaultdict(
-        functools.partial(collections.defaultdict, dict)
-    )
-
-    def overload(func):
-        """Decorator for overloaded functions/methods.
-
-        In a stub file, place two or more stub definitions for the same
-        function in a row, each decorated with @overload.  For example:
-
-        @overload
-        def utf8(value: None) -> None: ...
-        @overload
-        def utf8(value: bytes) -> bytes: ...
-        @overload
-        def utf8(value: str) -> bytes: ...
-
-        In a non-stub file (i.e. a regular .py file), do the same but
-        follow it with an implementation.  The implementation should *not*
-        be decorated with @overload.  For example:
-
-        @overload
-        def utf8(value: None) -> None: ...
-        @overload
-        def utf8(value: bytes) -> bytes: ...
-        @overload
-        def utf8(value: str) -> bytes: ...
-        def utf8(value):
-            # implementation goes here
-
-        The overloads for a function can be retrieved at runtime using the
-        get_overloads() function.
-        """
-        # classmethod and staticmethod
-        f = getattr(func, "__func__", func)
-        try:
-            _overload_registry[f.__module__][f.__qualname__][
-                f.__code__.co_firstlineno
-            ] = func
-        except AttributeError:
-            # Not a normal function; ignore.
-            pass
-        return _overload_dummy
-
-    def get_overloads(func):
-        """Return all defined overloads for *func* as a sequence."""
-        # classmethod and staticmethod
-        f = getattr(func, "__func__", func)
-        if f.__module__ not in _overload_registry:
-            return []
-        mod_dict = _overload_registry[f.__module__]
-        if f.__qualname__ not in mod_dict:
-            return []
-        return list(mod_dict[f.__qualname__].values())
-
-    def clear_overloads():
-        """Clear all overloads in the registry."""
-        _overload_registry.clear()
-
-
-# This is not a real generic class.  Don't use outside annotations.
-Type = typing.Type
-
-# Various ABCs mimicking those in collections.abc.
-# A few are simply re-exported for completeness.
-
-
-Awaitable = typing.Awaitable
-Coroutine = typing.Coroutine
-AsyncIterable = typing.AsyncIterable
-AsyncIterator = typing.AsyncIterator
-Deque = typing.Deque
-ContextManager = typing.ContextManager
-AsyncContextManager = typing.AsyncContextManager
-DefaultDict = typing.DefaultDict
-
-# 3.7.2+
-if hasattr(typing, 'OrderedDict'):
-    OrderedDict = typing.OrderedDict
-# 3.7.0-3.7.2
-else:
-    OrderedDict = typing._alias(collections.OrderedDict, (KT, VT))
-
-Counter = typing.Counter
-ChainMap = typing.ChainMap
-AsyncGenerator = typing.AsyncGenerator
-NewType = typing.NewType
-Text = typing.Text
-TYPE_CHECKING = typing.TYPE_CHECKING
-
-
-_PROTO_WHITELIST = ['Callable', 'Awaitable',
-                    'Iterable', 'Iterator', 'AsyncIterable', 'AsyncIterator',
-                    'Hashable', 'Sized', 'Container', 'Collection', 'Reversible',
-                    'ContextManager', 'AsyncContextManager']
-
-
-def _get_protocol_attrs(cls):
-    attrs = set()
-    for base in cls.__mro__[:-1]:  # without object
-        if base.__name__ in ('Protocol', 'Generic'):
-            continue
-        annotations = getattr(base, '__annotations__', {})
-        for attr in list(base.__dict__.keys()) + list(annotations.keys()):
-            if (not attr.startswith('_abc_') and attr not in (
-                    '__abstractmethods__', '__annotations__', '__weakref__',
-                    '_is_protocol', '_is_runtime_protocol', '__dict__',
-                    '__args__', '__slots__',
-                    '__next_in_mro__', '__parameters__', '__origin__',
-                    '__orig_bases__', '__extra__', '__tree_hash__',
-                    '__doc__', '__subclasshook__', '__init__', '__new__',
-                    '__module__', '_MutableMapping__marker', '_gorg')):
-                attrs.add(attr)
-    return attrs
-
-
-def _is_callable_members_only(cls):
-    return all(callable(getattr(cls, attr, None)) for attr in _get_protocol_attrs(cls))
-
-
-def _maybe_adjust_parameters(cls):
-    """Helper function used in Protocol.__init_subclass__ and _TypedDictMeta.__new__.
-
-    The contents of this function are very similar
-    to logic found in typing.Generic.__init_subclass__
-    on the CPython main branch.
-    """
-    tvars = []
-    if '__orig_bases__' in cls.__dict__:
-        tvars = typing._collect_type_vars(cls.__orig_bases__)
-        # Look for Generic[T1, ..., Tn] or Protocol[T1, ..., Tn].
-        # If found, tvars must be a subset of it.
-        # If not found, tvars is it.
-        # Also check for and reject plain Generic,
-        # and reject multiple Generic[...] and/or Protocol[...].
-        gvars = None
-        for base in cls.__orig_bases__:
-            if (isinstance(base, typing._GenericAlias) and
-                    base.__origin__ in (typing.Generic, Protocol)):
-                # for error messages
-                the_base = base.__origin__.__name__
-                if gvars is not None:
-                    raise TypeError(
-                        "Cannot inherit from Generic[...]"
-                        " and/or Protocol[...] multiple types.")
-                gvars = base.__parameters__
-        if gvars is None:
-            gvars = tvars
-        else:
-            tvarset = set(tvars)
-            gvarset = set(gvars)
-            if not tvarset <= gvarset:
-                s_vars = ', '.join(str(t) for t in tvars if t not in gvarset)
-                s_args = ', '.join(str(g) for g in gvars)
-                raise TypeError(f"Some type variables ({s_vars}) are"
-                                f" not listed in {the_base}[{s_args}]")
-            tvars = gvars
-    cls.__parameters__ = tuple(tvars)
-
-
-# 3.8+
-if hasattr(typing, 'Protocol'):
-    Protocol = typing.Protocol
-# 3.7
-else:
-
-    def _no_init(self, *args, **kwargs):
-        if type(self)._is_protocol:
-            raise TypeError('Protocols cannot be instantiated')
-
-    class _ProtocolMeta(abc.ABCMeta):  # noqa: B024
-        # This metaclass is a bit unfortunate and exists only because of the lack
-        # of __instancehook__.
-        def __instancecheck__(cls, instance):
-            # We need this method for situations where attributes are
-            # assigned in __init__.
-            if ((not getattr(cls, '_is_protocol', False) or
-                 _is_callable_members_only(cls)) and
-                    issubclass(instance.__class__, cls)):
-                return True
-            if cls._is_protocol:
-                if all(hasattr(instance, attr) and
-                       (not callable(getattr(cls, attr, None)) or
-                        getattr(instance, attr) is not None)
-                       for attr in _get_protocol_attrs(cls)):
-                    return True
-            return super().__instancecheck__(instance)
-
-    class Protocol(metaclass=_ProtocolMeta):
-        # There is quite a lot of overlapping code with typing.Generic.
-        # Unfortunately it is hard to avoid this while these live in two different
-        # modules. The duplicated code will be removed when Protocol is moved to typing.
-        """Base class for protocol classes. Protocol classes are defined as::
-
-            class Proto(Protocol):
-                def meth(self) -> int:
-                    ...
-
-        Such classes are primarily used with static type checkers that recognize
-        structural subtyping (static duck-typing), for example::
-
-            class C:
-                def meth(self) -> int:
-                    return 0
-
-            def func(x: Proto) -> int:
-                return x.meth()
-
-            func(C())  # Passes static type check
-
-        See PEP 544 for details. Protocol classes decorated with
-        @typing_extensions.runtime act as simple-minded runtime protocol that checks
-        only the presence of given attributes, ignoring their type signatures.
-
-        Protocol classes can be generic, they are defined as::
-
-            class GenProto(Protocol[T]):
-                def meth(self) -> T:
-                    ...
-        """
-        __slots__ = ()
-        _is_protocol = True
-
-        def __new__(cls, *args, **kwds):
-            if cls is Protocol:
-                raise TypeError("Type Protocol cannot be instantiated; "
-                                "it can only be used as a base class")
-            return super().__new__(cls)
-
-        @typing._tp_cache
-        def __class_getitem__(cls, params):
-            if not isinstance(params, tuple):
-                params = (params,)
-            if not params and cls is not typing.Tuple:
-                raise TypeError(
-                    f"Parameter list to {cls.__qualname__}[...] cannot be empty")
-            msg = "Parameters to generic types must be types."
-            params = tuple(typing._type_check(p, msg) for p in params)  # noqa
-            if cls is Protocol:
-                # Generic can only be subscripted with unique type variables.
-                if not all(isinstance(p, typing.TypeVar) for p in params):
-                    i = 0
-                    while isinstance(params[i], typing.TypeVar):
-                        i += 1
-                    raise TypeError(
-                        "Parameters to Protocol[...] must all be type variables."
-                        f" Parameter {i + 1} is {params[i]}")
-                if len(set(params)) != len(params):
-                    raise TypeError(
-                        "Parameters to Protocol[...] must all be unique")
-            else:
-                # Subscripting a regular Generic subclass.
-                _check_generic(cls, params, len(cls.__parameters__))
-            return typing._GenericAlias(cls, params)
-
-        def __init_subclass__(cls, *args, **kwargs):
-            if '__orig_bases__' in cls.__dict__:
-                error = typing.Generic in cls.__orig_bases__
-            else:
-                error = typing.Generic in cls.__bases__
-            if error:
-                raise TypeError("Cannot inherit from plain Generic")
-            _maybe_adjust_parameters(cls)
-
-            # Determine if this is a protocol or a concrete subclass.
-            if not cls.__dict__.get('_is_protocol', None):
-                cls._is_protocol = any(b is Protocol for b in cls.__bases__)
-
-            # Set (or override) the protocol subclass hook.
-            def _proto_hook(other):
-                if not cls.__dict__.get('_is_protocol', None):
-                    return NotImplemented
-                if not getattr(cls, '_is_runtime_protocol', False):
-                    if sys._getframe(2).f_globals['__name__'] in ['abc', 'functools']:
-                        return NotImplemented
-                    raise TypeError("Instance and class checks can only be used with"
-                                    " @runtime protocols")
-                if not _is_callable_members_only(cls):
-                    if sys._getframe(2).f_globals['__name__'] in ['abc', 'functools']:
-                        return NotImplemented
-                    raise TypeError("Protocols with non-method members"
-                                    " don't support issubclass()")
-                if not isinstance(other, type):
-                    # Same error as for issubclass(1, int)
-                    raise TypeError('issubclass() arg 1 must be a class')
-                for attr in _get_protocol_attrs(cls):
-                    for base in other.__mro__:
-                        if attr in base.__dict__:
-                            if base.__dict__[attr] is None:
-                                return NotImplemented
-                            break
-                        annotations = getattr(base, '__annotations__', {})
-                        if (isinstance(annotations, typing.Mapping) and
-                                attr in annotations and
-                                isinstance(other, _ProtocolMeta) and
-                                other._is_protocol):
-                            break
-                    else:
-                        return NotImplemented
-                return True
-            if '__subclasshook__' not in cls.__dict__:
-                cls.__subclasshook__ = _proto_hook
-
-            # We have nothing more to do for non-protocols.
-            if not cls._is_protocol:
-                return
-
-            # Check consistency of bases.
-            for base in cls.__bases__:
-                if not (base in (object, typing.Generic) or
-                        base.__module__ == 'collections.abc' and
-                        base.__name__ in _PROTO_WHITELIST or
-                        isinstance(base, _ProtocolMeta) and base._is_protocol):
-                    raise TypeError('Protocols can only inherit from other'
-                                    f' protocols, got {repr(base)}')
-            cls.__init__ = _no_init
-
-
-# 3.8+
-if hasattr(typing, 'runtime_checkable'):
-    runtime_checkable = typing.runtime_checkable
-# 3.7
-else:
-    def runtime_checkable(cls):
-        """Mark a protocol class as a runtime protocol, so that it
-        can be used with isinstance() and issubclass(). Raise TypeError
-        if applied to a non-protocol class.
-
-        This allows a simple-minded structural check very similar to the
-        one-offs in collections.abc such as Hashable.
-        """
-        if not isinstance(cls, _ProtocolMeta) or not cls._is_protocol:
-            raise TypeError('@runtime_checkable can be only applied to protocol classes,'
-                            f' got {cls!r}')
-        cls._is_runtime_protocol = True
-        return cls
-
-
-# Exists for backwards compatibility.
-runtime = runtime_checkable
-
-
-# 3.8+
-if hasattr(typing, 'SupportsIndex'):
-    SupportsIndex = typing.SupportsIndex
-# 3.7
-else:
-    @runtime_checkable
-    class SupportsIndex(Protocol):
-        __slots__ = ()
-
-        @abc.abstractmethod
-        def __index__(self) -> int:
-            pass
-
-
-if hasattr(typing, "Required"):
-    # The standard library TypedDict in Python 3.8 does not store runtime information
-    # about which (if any) keys are optional.  See https://bugs.python.org/issue38834
-    # The standard library TypedDict in Python 3.9.0/1 does not honour the "total"
-    # keyword with old-style TypedDict().  See https://bugs.python.org/issue42059
-    # The standard library TypedDict below Python 3.11 does not store runtime
-    # information about optional and required keys when using Required or NotRequired.
-    # Generic TypedDicts are also impossible using typing.TypedDict on Python <3.11.
-    TypedDict = typing.TypedDict
-    _TypedDictMeta = typing._TypedDictMeta
-    is_typeddict = typing.is_typeddict
-else:
-    def _check_fails(cls, other):
-        try:
-            if sys._getframe(1).f_globals['__name__'] not in ['abc',
-                                                              'functools',
-                                                              'typing']:
-                # Typed dicts are only for static structural subtyping.
-                raise TypeError('TypedDict does not support instance and class checks')
-        except (AttributeError, ValueError):
-            pass
-        return False
-
-    def _dict_new(*args, **kwargs):
-        if not args:
-            raise TypeError('TypedDict.__new__(): not enough arguments')
-        _, args = args[0], args[1:]  # allow the "cls" keyword be passed
-        return dict(*args, **kwargs)
-
-    _dict_new.__text_signature__ = '($cls, _typename, _fields=None, /, **kwargs)'
-
-    def _typeddict_new(*args, total=True, **kwargs):
-        if not args:
-            raise TypeError('TypedDict.__new__(): not enough arguments')
-        _, args = args[0], args[1:]  # allow the "cls" keyword be passed
-        if args:
-            typename, args = args[0], args[1:]  # allow the "_typename" keyword be passed
-        elif '_typename' in kwargs:
-            typename = kwargs.pop('_typename')
-            import warnings
-            warnings.warn("Passing '_typename' as keyword argument is deprecated",
-                          DeprecationWarning, stacklevel=2)
-        else:
-            raise TypeError("TypedDict.__new__() missing 1 required positional "
-                            "argument: '_typename'")
-        if args:
-            try:
-                fields, = args  # allow the "_fields" keyword be passed
-            except ValueError:
-                raise TypeError('TypedDict.__new__() takes from 2 to 3 '
-                                f'positional arguments but {len(args) + 2} '
-                                'were given')
-        elif '_fields' in kwargs and len(kwargs) == 1:
-            fields = kwargs.pop('_fields')
-            import warnings
-            warnings.warn("Passing '_fields' as keyword argument is deprecated",
-                          DeprecationWarning, stacklevel=2)
-        else:
-            fields = None
-
-        if fields is None:
-            fields = kwargs
-        elif kwargs:
-            raise TypeError("TypedDict takes either a dict or keyword arguments,"
-                            " but not both")
-
-        ns = {'__annotations__': dict(fields)}
-        try:
-            # Setting correct module is necessary to make typed dict classes pickleable.
-            ns['__module__'] = sys._getframe(1).f_globals.get('__name__', '__main__')
-        except (AttributeError, ValueError):
-            pass
-
-        return _TypedDictMeta(typename, (), ns, total=total)
-
-    _typeddict_new.__text_signature__ = ('($cls, _typename, _fields=None,'
-                                         ' /, *, total=True, **kwargs)')
-
-    class _TypedDictMeta(type):
-        def __init__(cls, name, bases, ns, total=True):
-            super().__init__(name, bases, ns)
-
-        def __new__(cls, name, bases, ns, total=True):
-            # Create new typed dict class object.
-            # This method is called directly when TypedDict is subclassed,
-            # or via _typeddict_new when TypedDict is instantiated. This way
-            # TypedDict supports all three syntaxes described in its docstring.
-            # Subclasses and instances of TypedDict return actual dictionaries
-            # via _dict_new.
-            ns['__new__'] = _typeddict_new if name == 'TypedDict' else _dict_new
-            # Don't insert typing.Generic into __bases__ here,
-            # or Generic.__init_subclass__ will raise TypeError
-            # in the super().__new__() call.
-            # Instead, monkey-patch __bases__ onto the class after it's been created.
-            tp_dict = super().__new__(cls, name, (dict,), ns)
-
-            if any(issubclass(base, typing.Generic) for base in bases):
-                tp_dict.__bases__ = (typing.Generic, dict)
-                _maybe_adjust_parameters(tp_dict)
-
-            annotations = {}
-            own_annotations = ns.get('__annotations__', {})
-            msg = "TypedDict('Name', {f0: t0, f1: t1, ...}); each t must be a type"
-            own_annotations = {
-                n: typing._type_check(tp, msg) for n, tp in own_annotations.items()
-            }
-            required_keys = set()
-            optional_keys = set()
-
-            for base in bases:
-                annotations.update(base.__dict__.get('__annotations__', {}))
-                required_keys.update(base.__dict__.get('__required_keys__', ()))
-                optional_keys.update(base.__dict__.get('__optional_keys__', ()))
-
-            annotations.update(own_annotations)
-            for annotation_key, annotation_type in own_annotations.items():
-                annotation_origin = get_origin(annotation_type)
-                if annotation_origin is Annotated:
-                    annotation_args = get_args(annotation_type)
-                    if annotation_args:
-                        annotation_type = annotation_args[0]
-                        annotation_origin = get_origin(annotation_type)
-
-                if annotation_origin is Required:
-                    required_keys.add(annotation_key)
-                elif annotation_origin is NotRequired:
-                    optional_keys.add(annotation_key)
-                elif total:
-                    required_keys.add(annotation_key)
-                else:
-                    optional_keys.add(annotation_key)
-
-            tp_dict.__annotations__ = annotations
-            tp_dict.__required_keys__ = frozenset(required_keys)
-            tp_dict.__optional_keys__ = frozenset(optional_keys)
-            if not hasattr(tp_dict, '__total__'):
-                tp_dict.__total__ = total
-            return tp_dict
-
-        __instancecheck__ = __subclasscheck__ = _check_fails
-
-    TypedDict = _TypedDictMeta('TypedDict', (dict,), {})
-    TypedDict.__module__ = __name__
-    TypedDict.__doc__ = \
-        """A simple typed name space. At runtime it is equivalent to a plain dict.
-
-        TypedDict creates a dictionary type that expects all of its
-        instances to have a certain set of keys, with each key
-        associated with a value of a consistent type. This expectation
-        is not checked at runtime but is only enforced by type checkers.
-        Usage::
-
-            class Point2D(TypedDict):
-                x: int
-                y: int
-                label: str
-
-            a: Point2D = {'x': 1, 'y': 2, 'label': 'good'}  # OK
-            b: Point2D = {'z': 3, 'label': 'bad'}           # Fails type check
-
-            assert Point2D(x=1, y=2, label='first') == dict(x=1, y=2, label='first')
-
-        The type info can be accessed via the Point2D.__annotations__ dict, and
-        the Point2D.__required_keys__ and Point2D.__optional_keys__ frozensets.
-        TypedDict supports two additional equivalent forms::
-
-            Point2D = TypedDict('Point2D', x=int, y=int, label=str)
-            Point2D = TypedDict('Point2D', {'x': int, 'y': int, 'label': str})
-
-        The class syntax is only supported in Python 3.6+, while two other
-        syntax forms work for Python 2.7 and 3.2+
-        """
-
-    if hasattr(typing, "_TypedDictMeta"):
-        _TYPEDDICT_TYPES = (typing._TypedDictMeta, _TypedDictMeta)
-    else:
-        _TYPEDDICT_TYPES = (_TypedDictMeta,)
-
-    def is_typeddict(tp):
-        """Check if an annotation is a TypedDict class
-
-        For example::
-            class Film(TypedDict):
-                title: str
-                year: int
-
-            is_typeddict(Film)  # => True
-            is_typeddict(Union[list, str])  # => False
-        """
-        return isinstance(tp, tuple(_TYPEDDICT_TYPES))
-
-
-if hasattr(typing, "assert_type"):
-    assert_type = typing.assert_type
-
-else:
-    def assert_type(__val, __typ):
-        """Assert (to the type checker) that the value is of the given type.
-
-        When the type checker encounters a call to assert_type(), it
-        emits an error if the value is not of the specified type::
-
-            def greet(name: str) -> None:
-                assert_type(name, str)  # ok
-                assert_type(name, int)  # type checker error
-
-        At runtime this returns the first argument unchanged and otherwise
-        does nothing.
-        """
-        return __val
-
-
-if hasattr(typing, "Required"):
-    get_type_hints = typing.get_type_hints
-else:
-    import functools
-    import types
-
-    # replaces _strip_annotations()
-    def _strip_extras(t):
-        """Strips Annotated, Required and NotRequired from a given type."""
-        if isinstance(t, _AnnotatedAlias):
-            return _strip_extras(t.__origin__)
-        if hasattr(t, "__origin__") and t.__origin__ in (Required, NotRequired):
-            return _strip_extras(t.__args__[0])
-        if isinstance(t, typing._GenericAlias):
-            stripped_args = tuple(_strip_extras(a) for a in t.__args__)
-            if stripped_args == t.__args__:
-                return t
-            return t.copy_with(stripped_args)
-        if hasattr(types, "GenericAlias") and isinstance(t, types.GenericAlias):
-            stripped_args = tuple(_strip_extras(a) for a in t.__args__)
-            if stripped_args == t.__args__:
-                return t
-            return types.GenericAlias(t.__origin__, stripped_args)
-        if hasattr(types, "UnionType") and isinstance(t, types.UnionType):
-            stripped_args = tuple(_strip_extras(a) for a in t.__args__)
-            if stripped_args == t.__args__:
-                return t
-            return functools.reduce(operator.or_, stripped_args)
-
-        return t
-
-    def get_type_hints(obj, globalns=None, localns=None, include_extras=False):
-        """Return type hints for an object.
-
-        This is often the same as obj.__annotations__, but it handles
-        forward references encoded as string literals, adds Optional[t] if a
-        default value equal to None is set and recursively replaces all
-        'Annotated[T, ...]', 'Required[T]' or 'NotRequired[T]' with 'T'
-        (unless 'include_extras=True').
-
-        The argument may be a module, class, method, or function. The annotations
-        are returned as a dictionary. For classes, annotations include also
-        inherited members.
-
-        TypeError is raised if the argument is not of a type that can contain
-        annotations, and an empty dictionary is returned if no annotations are
-        present.
-
-        BEWARE -- the behavior of globalns and localns is counterintuitive
-        (unless you are familiar with how eval() and exec() work).  The
-        search order is locals first, then globals.
-
-        - If no dict arguments are passed, an attempt is made to use the
-          globals from obj (or the respective module's globals for classes),
-          and these are also used as the locals.  If the object does not appear
-          to have globals, an empty dictionary is used.
-
-        - If one dict argument is passed, it is used for both globals and
-          locals.
-
-        - If two dict arguments are passed, they specify globals and
-          locals, respectively.
-        """
-        if hasattr(typing, "Annotated"):
-            hint = typing.get_type_hints(
-                obj, globalns=globalns, localns=localns, include_extras=True
-            )
-        else:
-            hint = typing.get_type_hints(obj, globalns=globalns, localns=localns)
-        if include_extras:
-            return hint
-        return {k: _strip_extras(t) for k, t in hint.items()}
-
-
-# Python 3.9+ has PEP 593 (Annotated)
-if hasattr(typing, 'Annotated'):
-    Annotated = typing.Annotated
-    # Not exported and not a public API, but needed for get_origin() and get_args()
-    # to work.
-    _AnnotatedAlias = typing._AnnotatedAlias
-# 3.7-3.8
-else:
-    class _AnnotatedAlias(typing._GenericAlias, _root=True):
-        """Runtime representation of an annotated type.
-
-        At its core 'Annotated[t, dec1, dec2, ...]' is an alias for the type 't'
-        with extra annotations. The alias behaves like a normal typing alias,
-        instantiating is the same as instantiating the underlying type, binding
-        it to types is also the same.
-        """
-        def __init__(self, origin, metadata):
-            if isinstance(origin, _AnnotatedAlias):
-                metadata = origin.__metadata__ + metadata
-                origin = origin.__origin__
-            super().__init__(origin, origin)
-            self.__metadata__ = metadata
-
-        def copy_with(self, params):
-            assert len(params) == 1
-            new_type = params[0]
-            return _AnnotatedAlias(new_type, self.__metadata__)
-
-        def __repr__(self):
-            return (f"typing_extensions.Annotated[{typing._type_repr(self.__origin__)}, "
-                    f"{', '.join(repr(a) for a in self.__metadata__)}]")
-
-        def __reduce__(self):
-            return operator.getitem, (
-                Annotated, (self.__origin__,) + self.__metadata__
-            )
-
-        def __eq__(self, other):
-            if not isinstance(other, _AnnotatedAlias):
-                return NotImplemented
-            if self.__origin__ != other.__origin__:
-                return False
-            return self.__metadata__ == other.__metadata__
-
-        def __hash__(self):
-            return hash((self.__origin__, self.__metadata__))
-
-    class Annotated:
-        """Add context specific metadata to a type.
-
-        Example: Annotated[int, runtime_check.Unsigned] indicates to the
-        hypothetical runtime_check module that this type is an unsigned int.
-        Every other consumer of this type can ignore this metadata and treat
-        this type as int.
-
-        The first argument to Annotated must be a valid type (and will be in
-        the __origin__ field), the remaining arguments are kept as a tuple in
-        the __extra__ field.
-
-        Details:
-
-        - It's an error to call `Annotated` with less than two arguments.
-        - Nested Annotated are flattened::
-
-            Annotated[Annotated[T, Ann1, Ann2], Ann3] == Annotated[T, Ann1, Ann2, Ann3]
-
-        - Instantiating an annotated type is equivalent to instantiating the
-        underlying type::
-
-            Annotated[C, Ann1](5) == C(5)
-
-        - Annotated can be used as a generic type alias::
-
-            Optimized = Annotated[T, runtime.Optimize()]
-            Optimized[int] == Annotated[int, runtime.Optimize()]
-
-            OptimizedList = Annotated[List[T], runtime.Optimize()]
-            OptimizedList[int] == Annotated[List[int], runtime.Optimize()]
-        """
-
-        __slots__ = ()
-
-        def __new__(cls, *args, **kwargs):
-            raise TypeError("Type Annotated cannot be instantiated.")
-
-        @typing._tp_cache
-        def __class_getitem__(cls, params):
-            if not isinstance(params, tuple) or len(params) < 2:
-                raise TypeError("Annotated[...] should be used "
-                                "with at least two arguments (a type and an "
-                                "annotation).")
-            allowed_special_forms = (ClassVar, Final)
-            if get_origin(params[0]) in allowed_special_forms:
-                origin = params[0]
-            else:
-                msg = "Annotated[t, ...]: t must be a type."
-                origin = typing._type_check(params[0], msg)
-            metadata = tuple(params[1:])
-            return _AnnotatedAlias(origin, metadata)
-
-        def __init_subclass__(cls, *args, **kwargs):
-            raise TypeError(
-                f"Cannot subclass {cls.__module__}.Annotated"
-            )
-
-# Python 3.8 has get_origin() and get_args() but those implementations aren't
-# Annotated-aware, so we can't use those. Python 3.9's versions don't support
-# ParamSpecArgs and ParamSpecKwargs, so only Python 3.10's versions will do.
-if sys.version_info[:2] >= (3, 10):
-    get_origin = typing.get_origin
-    get_args = typing.get_args
-# 3.7-3.9
-else:
-    try:
-        # 3.9+
-        from typing import _BaseGenericAlias
-    except ImportError:
-        _BaseGenericAlias = typing._GenericAlias
-    try:
-        # 3.9+
-        from typing import GenericAlias as _typing_GenericAlias
-    except ImportError:
-        _typing_GenericAlias = typing._GenericAlias
-
-    def get_origin(tp):
-        """Get the unsubscripted version of a type.
-
-        This supports generic types, Callable, Tuple, Union, Literal, Final, ClassVar
-        and Annotated. Return None for unsupported types. Examples::
-
-            get_origin(Literal[42]) is Literal
-            get_origin(int) is None
-            get_origin(ClassVar[int]) is ClassVar
-            get_origin(Generic) is Generic
-            get_origin(Generic[T]) is Generic
-            get_origin(Union[T, int]) is Union
-            get_origin(List[Tuple[T, T]][int]) == list
-            get_origin(P.args) is P
-        """
-        if isinstance(tp, _AnnotatedAlias):
-            return Annotated
-        if isinstance(tp, (typing._GenericAlias, _typing_GenericAlias, _BaseGenericAlias,
-                           ParamSpecArgs, ParamSpecKwargs)):
-            return tp.__origin__
-        if tp is typing.Generic:
-            return typing.Generic
-        return None
-
-    def get_args(tp):
-        """Get type arguments with all substitutions performed.
-
-        For unions, basic simplifications used by Union constructor are performed.
-        Examples::
-            get_args(Dict[str, int]) == (str, int)
-            get_args(int) == ()
-            get_args(Union[int, Union[T, int], str][int]) == (int, str)
-            get_args(Union[int, Tuple[T, int]][str]) == (int, Tuple[str, int])
-            get_args(Callable[[], T][int]) == ([], int)
-        """
-        if isinstance(tp, _AnnotatedAlias):
-            return (tp.__origin__,) + tp.__metadata__
-        if isinstance(tp, (typing._GenericAlias, _typing_GenericAlias)):
-            if getattr(tp, "_special", False):
-                return ()
-            res = tp.__args__
-            if get_origin(tp) is collections.abc.Callable and res[0] is not Ellipsis:
-                res = (list(res[:-1]), res[-1])
-            return res
-        return ()
-
-
-# 3.10+
-if hasattr(typing, 'TypeAlias'):
-    TypeAlias = typing.TypeAlias
-# 3.9
-elif sys.version_info[:2] >= (3, 9):
-    class _TypeAliasForm(typing._SpecialForm, _root=True):
-        def __repr__(self):
-            return 'typing_extensions.' + self._name
-
-    @_TypeAliasForm
-    def TypeAlias(self, parameters):
-        """Special marker indicating that an assignment should
-        be recognized as a proper type alias definition by type
-        checkers.
-
-        For example::
-
-            Predicate: TypeAlias = Callable[..., bool]
-
-        It's invalid when used anywhere except as in the example above.
-        """
-        raise TypeError(f"{self} is not subscriptable")
-# 3.7-3.8
-else:
-    class _TypeAliasForm(typing._SpecialForm, _root=True):
-        def __repr__(self):
-            return 'typing_extensions.' + self._name
-
-    TypeAlias = _TypeAliasForm('TypeAlias',
-                               doc="""Special marker indicating that an assignment should
-                               be recognized as a proper type alias definition by type
-                               checkers.
-
-                               For example::
-
-                                   Predicate: TypeAlias = Callable[..., bool]
-
-                               It's invalid when used anywhere except as in the example
-                               above.""")
-
-
-class _DefaultMixin:
-    """Mixin for TypeVarLike defaults."""
-
-    __slots__ = ()
-
-    def __init__(self, default):
-        if isinstance(default, (tuple, list)):
-            self.__default__ = tuple((typing._type_check(d, "Default must be a type")
-                                      for d in default))
-        elif default:
-            self.__default__ = typing._type_check(default, "Default must be a type")
-        else:
-            self.__default__ = None
-
-
-# Add default and infer_variance parameters from PEP 696 and 695
-class TypeVar(typing.TypeVar, _DefaultMixin, _root=True):
-    """Type variable."""
-
-    __module__ = 'typing'
-
-    def __init__(self, name, *constraints, bound=None,
-                 covariant=False, contravariant=False,
-                 default=None, infer_variance=False):
-        super().__init__(name, *constraints, bound=bound, covariant=covariant,
-                         contravariant=contravariant)
-        _DefaultMixin.__init__(self, default)
-        self.__infer_variance__ = infer_variance
-
-        # for pickling:
-        try:
-            def_mod = sys._getframe(1).f_globals.get('__name__', '__main__')
-        except (AttributeError, ValueError):
-            def_mod = None
-        if def_mod != 'typing_extensions':
-            self.__module__ = def_mod
-
-
-# Python 3.10+ has PEP 612
-if hasattr(typing, 'ParamSpecArgs'):
-    ParamSpecArgs = typing.ParamSpecArgs
-    ParamSpecKwargs = typing.ParamSpecKwargs
-# 3.7-3.9
-else:
-    class _Immutable:
-        """Mixin to indicate that object should not be copied."""
-        __slots__ = ()
-
-        def __copy__(self):
-            return self
-
-        def __deepcopy__(self, memo):
-            return self
-
-    class ParamSpecArgs(_Immutable):
-        """The args for a ParamSpec object.
-
-        Given a ParamSpec object P, P.args is an instance of ParamSpecArgs.
-
-        ParamSpecArgs objects have a reference back to their ParamSpec:
-
-        P.args.__origin__ is P
-
-        This type is meant for runtime introspection and has no special meaning to
-        static type checkers.
-        """
-        def __init__(self, origin):
-            self.__origin__ = origin
-
-        def __repr__(self):
-            return f"{self.__origin__.__name__}.args"
-
-        def __eq__(self, other):
-            if not isinstance(other, ParamSpecArgs):
-                return NotImplemented
-            return self.__origin__ == other.__origin__
-
-    class ParamSpecKwargs(_Immutable):
-        """The kwargs for a ParamSpec object.
-
-        Given a ParamSpec object P, P.kwargs is an instance of ParamSpecKwargs.
-
-        ParamSpecKwargs objects have a reference back to their ParamSpec:
-
-        P.kwargs.__origin__ is P
-
-        This type is meant for runtime introspection and has no special meaning to
-        static type checkers.
-        """
-        def __init__(self, origin):
-            self.__origin__ = origin
-
-        def __repr__(self):
-            return f"{self.__origin__.__name__}.kwargs"
-
-        def __eq__(self, other):
-            if not isinstance(other, ParamSpecKwargs):
-                return NotImplemented
-            return self.__origin__ == other.__origin__
-
-# 3.10+
-if hasattr(typing, 'ParamSpec'):
-
-    # Add default Parameter - PEP 696
-    class ParamSpec(typing.ParamSpec, _DefaultMixin, _root=True):
-        """Parameter specification variable."""
-
-        __module__ = 'typing'
-
-        def __init__(self, name, *, bound=None, covariant=False, contravariant=False,
-                     default=None):
-            super().__init__(name, bound=bound, covariant=covariant,
-                             contravariant=contravariant)
-            _DefaultMixin.__init__(self, default)
-
-            # for pickling:
-            try:
-                def_mod = sys._getframe(1).f_globals.get('__name__', '__main__')
-            except (AttributeError, ValueError):
-                def_mod = None
-            if def_mod != 'typing_extensions':
-                self.__module__ = def_mod
-
-# 3.7-3.9
-else:
-
-    # Inherits from list as a workaround for Callable checks in Python < 3.9.2.
-    class ParamSpec(list, _DefaultMixin):
-        """Parameter specification variable.
-
-        Usage::
-
-           P = ParamSpec('P')
-
-        Parameter specification variables exist primarily for the benefit of static
-        type checkers.  They are used to forward the parameter types of one
-        callable to another callable, a pattern commonly found in higher order
-        functions and decorators.  They are only valid when used in ``Concatenate``,
-        or s the first argument to ``Callable``. In Python 3.10 and higher,
-        they are also supported in user-defined Generics at runtime.
-        See class Generic for more information on generic types.  An
-        example for annotating a decorator::
-
-           T = TypeVar('T')
-           P = ParamSpec('P')
-
-           def add_logging(f: Callable[P, T]) -> Callable[P, T]:
-               '''A type-safe decorator to add logging to a function.'''
-               def inner(*args: P.args, **kwargs: P.kwargs) -> T:
-                   logging.info(f'{f.__name__} was called')
-                   return f(*args, **kwargs)
-               return inner
-
-           @add_logging
-           def add_two(x: float, y: float) -> float:
-               '''Add two numbers together.'''
-               return x + y
-
-        Parameter specification variables defined with covariant=True or
-        contravariant=True can be used to declare covariant or contravariant
-        generic types.  These keyword arguments are valid, but their actual semantics
-        are yet to be decided.  See PEP 612 for details.
-
-        Parameter specification variables can be introspected. e.g.:
-
-           P.__name__ == 'T'
-           P.__bound__ == None
-           P.__covariant__ == False
-           P.__contravariant__ == False
-
-        Note that only parameter specification variables defined in global scope can
-        be pickled.
-        """
-
-        # Trick Generic __parameters__.
-        __class__ = typing.TypeVar
-
-        @property
-        def args(self):
-            return ParamSpecArgs(self)
-
-        @property
-        def kwargs(self):
-            return ParamSpecKwargs(self)
-
-        def __init__(self, name, *, bound=None, covariant=False, contravariant=False,
-                     default=None):
-            super().__init__([self])
-            self.__name__ = name
-            self.__covariant__ = bool(covariant)
-            self.__contravariant__ = bool(contravariant)
-            if bound:
-                self.__bound__ = typing._type_check(bound, 'Bound must be a type.')
-            else:
-                self.__bound__ = None
-            _DefaultMixin.__init__(self, default)
-
-            # for pickling:
-            try:
-                def_mod = sys._getframe(1).f_globals.get('__name__', '__main__')
-            except (AttributeError, ValueError):
-                def_mod = None
-            if def_mod != 'typing_extensions':
-                self.__module__ = def_mod
-
-        def __repr__(self):
-            if self.__covariant__:
-                prefix = '+'
-            elif self.__contravariant__:
-                prefix = '-'
-            else:
-                prefix = '~'
-            return prefix + self.__name__
-
-        def __hash__(self):
-            return object.__hash__(self)
-
-        def __eq__(self, other):
-            return self is other
-
-        def __reduce__(self):
-            return self.__name__
-
-        # Hack to get typing._type_check to pass.
-        def __call__(self, *args, **kwargs):
-            pass
-
-
-# 3.7-3.9
-if not hasattr(typing, 'Concatenate'):
-    # Inherits from list as a workaround for Callable checks in Python < 3.9.2.
-    class _ConcatenateGenericAlias(list):
-
-        # Trick Generic into looking into this for __parameters__.
-        __class__ = typing._GenericAlias
-
-        # Flag in 3.8.
-        _special = False
-
-        def __init__(self, origin, args):
-            super().__init__(args)
-            self.__origin__ = origin
-            self.__args__ = args
-
-        def __repr__(self):
-            _type_repr = typing._type_repr
-            return (f'{_type_repr(self.__origin__)}'
-                    f'[{", ".join(_type_repr(arg) for arg in self.__args__)}]')
-
-        def __hash__(self):
-            return hash((self.__origin__, self.__args__))
-
-        # Hack to get typing._type_check to pass in Generic.
-        def __call__(self, *args, **kwargs):
-            pass
-
-        @property
-        def __parameters__(self):
-            return tuple(
-                tp for tp in self.__args__ if isinstance(tp, (typing.TypeVar, ParamSpec))
-            )
-
-
-# 3.7-3.9
-@typing._tp_cache
-def _concatenate_getitem(self, parameters):
-    if parameters == ():
-        raise TypeError("Cannot take a Concatenate of no types.")
-    if not isinstance(parameters, tuple):
-        parameters = (parameters,)
-    if not isinstance(parameters[-1], ParamSpec):
-        raise TypeError("The last parameter to Concatenate should be a "
-                        "ParamSpec variable.")
-    msg = "Concatenate[arg, ...]: each arg must be a type."
-    parameters = tuple(typing._type_check(p, msg) for p in parameters)
-    return _ConcatenateGenericAlias(self, parameters)
-
-
-# 3.10+
-if hasattr(typing, 'Concatenate'):
-    Concatenate = typing.Concatenate
-    _ConcatenateGenericAlias = typing._ConcatenateGenericAlias # noqa
-# 3.9
-elif sys.version_info[:2] >= (3, 9):
-    @_TypeAliasForm
-    def Concatenate(self, parameters):
-        """Used in conjunction with ``ParamSpec`` and ``Callable`` to represent a
-        higher order function which adds, removes or transforms parameters of a
-        callable.
-
-        For example::
-
-           Callable[Concatenate[int, P], int]
-
-        See PEP 612 for detailed information.
-        """
-        return _concatenate_getitem(self, parameters)
-# 3.7-8
-else:
-    class _ConcatenateForm(typing._SpecialForm, _root=True):
-        def __repr__(self):
-            return 'typing_extensions.' + self._name
-
-        def __getitem__(self, parameters):
-            return _concatenate_getitem(self, parameters)
-
-    Concatenate = _ConcatenateForm(
-        'Concatenate',
-        doc="""Used in conjunction with ``ParamSpec`` and ``Callable`` to represent a
-        higher order function which adds, removes or transforms parameters of a
-        callable.
-
-        For example::
-
-           Callable[Concatenate[int, P], int]
-
-        See PEP 612 for detailed information.
-        """)
-
-# 3.10+
-if hasattr(typing, 'TypeGuard'):
-    TypeGuard = typing.TypeGuard
-# 3.9
-elif sys.version_info[:2] >= (3, 9):
-    class _TypeGuardForm(typing._SpecialForm, _root=True):
-        def __repr__(self):
-            return 'typing_extensions.' + self._name
-
-    @_TypeGuardForm
-    def TypeGuard(self, parameters):
-        """Special typing form used to annotate the return type of a user-defined
-        type guard function.  ``TypeGuard`` only accepts a single type argument.
-        At runtime, functions marked this way should return a boolean.
-
-        ``TypeGuard`` aims to benefit *type narrowing* -- a technique used by static
-        type checkers to determine a more precise type of an expression within a
-        program's code flow.  Usually type narrowing is done by analyzing
-        conditional code flow and applying the narrowing to a block of code.  The
-        conditional expression here is sometimes referred to as a "type guard".
-
-        Sometimes it would be convenient to use a user-defined boolean function
-        as a type guard.  Such a function should use ``TypeGuard[...]`` as its
-        return type to alert static type checkers to this intention.
-
-        Using  ``-> TypeGuard`` tells the static type checker that for a given
-        function:
-
-        1. The return value is a boolean.
-        2. If the return value is ``True``, the type of its argument
-        is the type inside ``TypeGuard``.
-
-        For example::
-
-            def is_str(val: Union[str, float]):
-                # "isinstance" type guard
-                if isinstance(val, str):
-                    # Type of ``val`` is narrowed to ``str``
-                    ...
-                else:
-                    # Else, type of ``val`` is narrowed to ``float``.
-                    ...
-
-        Strict type narrowing is not enforced -- ``TypeB`` need not be a narrower
-        form of ``TypeA`` (it can even be a wider form) and this may lead to
-        type-unsafe results.  The main reason is to allow for things like
-        narrowing ``List[object]`` to ``List[str]`` even though the latter is not
-        a subtype of the former, since ``List`` is invariant.  The responsibility of
-        writing type-safe type guards is left to the user.
-
-        ``TypeGuard`` also works with type variables.  For more information, see
-        PEP 647 (User-Defined Type Guards).
-        """
-        item = typing._type_check(parameters, f'{self} accepts only a single type.')
-        return typing._GenericAlias(self, (item,))
-# 3.7-3.8
-else:
-    class _TypeGuardForm(typing._SpecialForm, _root=True):
-
-        def __repr__(self):
-            return 'typing_extensions.' + self._name
-
-        def __getitem__(self, parameters):
-            item = typing._type_check(parameters,
-                                      f'{self._name} accepts only a single type')
-            return typing._GenericAlias(self, (item,))
-
-    TypeGuard = _TypeGuardForm(
-        'TypeGuard',
-        doc="""Special typing form used to annotate the return type of a user-defined
-        type guard function.  ``TypeGuard`` only accepts a single type argument.
-        At runtime, functions marked this way should return a boolean.
-
-        ``TypeGuard`` aims to benefit *type narrowing* -- a technique used by static
-        type checkers to determine a more precise type of an expression within a
-        program's code flow.  Usually type narrowing is done by analyzing
-        conditional code flow and applying the narrowing to a block of code.  The
-        conditional expression here is sometimes referred to as a "type guard".
-
-        Sometimes it would be convenient to use a user-defined boolean function
-        as a type guard.  Such a function should use ``TypeGuard[...]`` as its
-        return type to alert static type checkers to this intention.
-
-        Using  ``-> TypeGuard`` tells the static type checker that for a given
-        function:
-
-        1. The return value is a boolean.
-        2. If the return value is ``True``, the type of its argument
-        is the type inside ``TypeGuard``.
-
-        For example::
-
-            def is_str(val: Union[str, float]):
-                # "isinstance" type guard
-                if isinstance(val, str):
-                    # Type of ``val`` is narrowed to ``str``
-                    ...
-                else:
-                    # Else, type of ``val`` is narrowed to ``float``.
-                    ...
-
-        Strict type narrowing is not enforced -- ``TypeB`` need not be a narrower
-        form of ``TypeA`` (it can even be a wider form) and this may lead to
-        type-unsafe results.  The main reason is to allow for things like
-        narrowing ``List[object]`` to ``List[str]`` even though the latter is not
-        a subtype of the former, since ``List`` is invariant.  The responsibility of
-        writing type-safe type guards is left to the user.
-
-        ``TypeGuard`` also works with type variables.  For more information, see
-        PEP 647 (User-Defined Type Guards).
-        """)
-
-
-# Vendored from cpython typing._SpecialFrom
-class _SpecialForm(typing._Final, _root=True):
-    __slots__ = ('_name', '__doc__', '_getitem')
-
-    def __init__(self, getitem):
-        self._getitem = getitem
-        self._name = getitem.__name__
-        self.__doc__ = getitem.__doc__
-
-    def __getattr__(self, item):
-        if item in {'__name__', '__qualname__'}:
-            return self._name
-
-        raise AttributeError(item)
-
-    def __mro_entries__(self, bases):
-        raise TypeError(f"Cannot subclass {self!r}")
-
-    def __repr__(self):
-        return f'typing_extensions.{self._name}'
-
-    def __reduce__(self):
-        return self._name
-
-    def __call__(self, *args, **kwds):
-        raise TypeError(f"Cannot instantiate {self!r}")
-
-    def __or__(self, other):
-        return typing.Union[self, other]
-
-    def __ror__(self, other):
-        return typing.Union[other, self]
-
-    def __instancecheck__(self, obj):
-        raise TypeError(f"{self} cannot be used with isinstance()")
-
-    def __subclasscheck__(self, cls):
-        raise TypeError(f"{self} cannot be used with issubclass()")
-
-    @typing._tp_cache
-    def __getitem__(self, parameters):
-        return self._getitem(self, parameters)
-
-
-if hasattr(typing, "LiteralString"):
-    LiteralString = typing.LiteralString
-else:
-    @_SpecialForm
-    def LiteralString(self, params):
-        """Represents an arbitrary literal string.
-
-        Example::
-
-          from typing_extensions import LiteralString
-
-          def query(sql: LiteralString) -> ...:
-              ...
-
-          query("SELECT * FROM table")  # ok
-          query(f"SELECT * FROM {input()}")  # not ok
-
-        See PEP 675 for details.
-
-        """
-        raise TypeError(f"{self} is not subscriptable")
-
-
-if hasattr(typing, "Self"):
-    Self = typing.Self
-else:
-    @_SpecialForm
-    def Self(self, params):
-        """Used to spell the type of "self" in classes.
-
-        Example::
-
-          from typing import Self
-
-          class ReturnsSelf:
-              def parse(self, data: bytes) -> Self:
-                  ...
-                  return self
-
-        """
-
-        raise TypeError(f"{self} is not subscriptable")
-
-
-if hasattr(typing, "Never"):
-    Never = typing.Never
-else:
-    @_SpecialForm
-    def Never(self, params):
-        """The bottom type, a type that has no members.
-
-        This can be used to define a function that should never be
-        called, or a function that never returns::
-
-            from typing_extensions import Never
-
-            def never_call_me(arg: Never) -> None:
-                pass
-
-            def int_or_str(arg: int | str) -> None:
-                never_call_me(arg)  # type checker error
-                match arg:
-                    case int():
-                        print("It's an int")
-                    case str():
-                        print("It's a str")
-                    case _:
-                        never_call_me(arg)  # ok, arg is of type Never
-
-        """
-
-        raise TypeError(f"{self} is not subscriptable")
-
-
-if hasattr(typing, 'Required'):
-    Required = typing.Required
-    NotRequired = typing.NotRequired
-elif sys.version_info[:2] >= (3, 9):
-    class _ExtensionsSpecialForm(typing._SpecialForm, _root=True):
-        def __repr__(self):
-            return 'typing_extensions.' + self._name
-
-    @_ExtensionsSpecialForm
-    def Required(self, parameters):
-        """A special typing construct to mark a key of a total=False TypedDict
-        as required. For example:
-
-            class Movie(TypedDict, total=False):
-                title: Required[str]
-                year: int
-
-            m = Movie(
-                title='The Matrix',  # typechecker error if key is omitted
-                year=1999,
-            )
-
-        There is no runtime checking that a required key is actually provided
-        when instantiating a related TypedDict.
-        """
-        item = typing._type_check(parameters, f'{self._name} accepts only a single type.')
-        return typing._GenericAlias(self, (item,))
-
-    @_ExtensionsSpecialForm
-    def NotRequired(self, parameters):
-        """A special typing construct to mark a key of a TypedDict as
-        potentially missing. For example:
-
-            class Movie(TypedDict):
-                title: str
-                year: NotRequired[int]
-
-            m = Movie(
-                title='The Matrix',  # typechecker error if key is omitted
-                year=1999,
-            )
-        """
-        item = typing._type_check(parameters, f'{self._name} accepts only a single type.')
-        return typing._GenericAlias(self, (item,))
-
-else:
-    class _RequiredForm(typing._SpecialForm, _root=True):
-        def __repr__(self):
-            return 'typing_extensions.' + self._name
-
-        def __getitem__(self, parameters):
-            item = typing._type_check(parameters,
-                                      f'{self._name} accepts only a single type.')
-            return typing._GenericAlias(self, (item,))
-
-    Required = _RequiredForm(
-        'Required',
-        doc="""A special typing construct to mark a key of a total=False TypedDict
-        as required. For example:
-
-            class Movie(TypedDict, total=False):
-                title: Required[str]
-                year: int
-
-            m = Movie(
-                title='The Matrix',  # typechecker error if key is omitted
-                year=1999,
-            )
-
-        There is no runtime checking that a required key is actually provided
-        when instantiating a related TypedDict.
-        """)
-    NotRequired = _RequiredForm(
-        'NotRequired',
-        doc="""A special typing construct to mark a key of a TypedDict as
-        potentially missing. For example:
-
-            class Movie(TypedDict):
-                title: str
-                year: NotRequired[int]
-
-            m = Movie(
-                title='The Matrix',  # typechecker error if key is omitted
-                year=1999,
-            )
-        """)
-
-
-if hasattr(typing, "Unpack"):  # 3.11+
-    Unpack = typing.Unpack
-elif sys.version_info[:2] >= (3, 9):
-    class _UnpackSpecialForm(typing._SpecialForm, _root=True):
-        def __repr__(self):
-            return 'typing_extensions.' + self._name
-
-    class _UnpackAlias(typing._GenericAlias, _root=True):
-        __class__ = typing.TypeVar
-
-    @_UnpackSpecialForm
-    def Unpack(self, parameters):
-        """A special typing construct to unpack a variadic type. For example:
-
-            Shape = TypeVarTuple('Shape')
-            Batch = NewType('Batch', int)
-
-            def add_batch_axis(
-                x: Array[Unpack[Shape]]
-            ) -> Array[Batch, Unpack[Shape]]: ...
-
-        """
-        item = typing._type_check(parameters, f'{self._name} accepts only a single type.')
-        return _UnpackAlias(self, (item,))
-
-    def _is_unpack(obj):
-        return isinstance(obj, _UnpackAlias)
-
-else:
-    class _UnpackAlias(typing._GenericAlias, _root=True):
-        __class__ = typing.TypeVar
-
-    class _UnpackForm(typing._SpecialForm, _root=True):
-        def __repr__(self):
-            return 'typing_extensions.' + self._name
-
-        def __getitem__(self, parameters):
-            item = typing._type_check(parameters,
-                                      f'{self._name} accepts only a single type.')
-            return _UnpackAlias(self, (item,))
-
-    Unpack = _UnpackForm(
-        'Unpack',
-        doc="""A special typing construct to unpack a variadic type. For example:
-
-            Shape = TypeVarTuple('Shape')
-            Batch = NewType('Batch', int)
-
-            def add_batch_axis(
-                x: Array[Unpack[Shape]]
-            ) -> Array[Batch, Unpack[Shape]]: ...
-
-        """)
-
-    def _is_unpack(obj):
-        return isinstance(obj, _UnpackAlias)
-
-
-if hasattr(typing, "TypeVarTuple"):  # 3.11+
-
-    # Add default Parameter - PEP 696
-    class TypeVarTuple(typing.TypeVarTuple, _DefaultMixin, _root=True):
-        """Type variable tuple."""
-
-        def __init__(self, name, *, default=None):
-            super().__init__(name)
-            _DefaultMixin.__init__(self, default)
-
-            # for pickling:
-            try:
-                def_mod = sys._getframe(1).f_globals.get('__name__', '__main__')
-            except (AttributeError, ValueError):
-                def_mod = None
-            if def_mod != 'typing_extensions':
-                self.__module__ = def_mod
-
-else:
-    class TypeVarTuple(_DefaultMixin):
-        """Type variable tuple.
-
-        Usage::
-
-            Ts = TypeVarTuple('Ts')
-
-        In the same way that a normal type variable is a stand-in for a single
-        type such as ``int``, a type variable *tuple* is a stand-in for a *tuple*
-        type such as ``Tuple[int, str]``.
-
-        Type variable tuples can be used in ``Generic`` declarations.
-        Consider the following example::
-
-            class Array(Generic[*Ts]): ...
-
-        The ``Ts`` type variable tuple here behaves like ``tuple[T1, T2]``,
-        where ``T1`` and ``T2`` are type variables. To use these type variables
-        as type parameters of ``Array``, we must *unpack* the type variable tuple using
-        the star operator: ``*Ts``. The signature of ``Array`` then behaves
-        as if we had simply written ``class Array(Generic[T1, T2]): ...``.
-        In contrast to ``Generic[T1, T2]``, however, ``Generic[*Shape]`` allows
-        us to parameterise the class with an *arbitrary* number of type parameters.
-
-        Type variable tuples can be used anywhere a normal ``TypeVar`` can.
-        This includes class definitions, as shown above, as well as function
-        signatures and variable annotations::
-
-            class Array(Generic[*Ts]):
-
-                def __init__(self, shape: Tuple[*Ts]):
-                    self._shape: Tuple[*Ts] = shape
-
-                def get_shape(self) -> Tuple[*Ts]:
-                    return self._shape
-
-            shape = (Height(480), Width(640))
-            x: Array[Height, Width] = Array(shape)
-            y = abs(x)  # Inferred type is Array[Height, Width]
-            z = x + x   #        ...    is Array[Height, Width]
-            x.get_shape()  #     ...    is tuple[Height, Width]
-
-        """
-
-        # Trick Generic __parameters__.
-        __class__ = typing.TypeVar
-
-        def __iter__(self):
-            yield self.__unpacked__
-
-        def __init__(self, name, *, default=None):
-            self.__name__ = name
-            _DefaultMixin.__init__(self, default)
-
-            # for pickling:
-            try:
-                def_mod = sys._getframe(1).f_globals.get('__name__', '__main__')
-            except (AttributeError, ValueError):
-                def_mod = None
-            if def_mod != 'typing_extensions':
-                self.__module__ = def_mod
-
-            self.__unpacked__ = Unpack[self]
-
-        def __repr__(self):
-            return self.__name__
-
-        def __hash__(self):
-            return object.__hash__(self)
-
-        def __eq__(self, other):
-            return self is other
-
-        def __reduce__(self):
-            return self.__name__
-
-        def __init_subclass__(self, *args, **kwds):
-            if '_root' not in kwds:
-                raise TypeError("Cannot subclass special typing classes")
-
-
-if hasattr(typing, "reveal_type"):
-    reveal_type = typing.reveal_type
-else:
-    def reveal_type(__obj: T) -> T:
-        """Reveal the inferred type of a variable.
-
-        When a static type checker encounters a call to ``reveal_type()``,
-        it will emit the inferred type of the argument::
-
-            x: int = 1
-            reveal_type(x)
-
-        Running a static type checker (e.g., ``mypy``) on this example
-        will produce output similar to 'Revealed type is "builtins.int"'.
-
-        At runtime, the function prints the runtime type of the
-        argument and returns it unchanged.
-
-        """
-        print(f"Runtime type is {type(__obj).__name__!r}", file=sys.stderr)
-        return __obj
-
-
-if hasattr(typing, "assert_never"):
-    assert_never = typing.assert_never
-else:
-    def assert_never(__arg: Never) -> Never:
-        """Assert to the type checker that a line of code is unreachable.
-
-        Example::
-
-            def int_or_str(arg: int | str) -> None:
-                match arg:
-                    case int():
-                        print("It's an int")
-                    case str():
-                        print("It's a str")
-                    case _:
-                        assert_never(arg)
-
-        If a type checker finds that a call to assert_never() is
-        reachable, it will emit an error.
-
-        At runtime, this throws an exception when called.
-
-        """
-        raise AssertionError("Expected code to be unreachable")
-
-
-if hasattr(typing, 'dataclass_transform'):
-    dataclass_transform = typing.dataclass_transform
-else:
-    def dataclass_transform(
-        *,
-        eq_default: bool = True,
-        order_default: bool = False,
-        kw_only_default: bool = False,
-        field_specifiers: typing.Tuple[
-            typing.Union[typing.Type[typing.Any], typing.Callable[..., typing.Any]],
-            ...
-        ] = (),
-        **kwargs: typing.Any,
-    ) -> typing.Callable[[T], T]:
-        """Decorator that marks a function, class, or metaclass as providing
-        dataclass-like behavior.
-
-        Example:
-
-            from typing_extensions import dataclass_transform
-
-            _T = TypeVar("_T")
-
-            # Used on a decorator function
-            @dataclass_transform()
-            def create_model(cls: type[_T]) -> type[_T]:
-                ...
-                return cls
-
-            @create_model
-            class CustomerModel:
-                id: int
-                name: str
-
-            # Used on a base class
-            @dataclass_transform()
-            class ModelBase: ...
-
-            class CustomerModel(ModelBase):
-                id: int
-                name: str
-
-            # Used on a metaclass
-            @dataclass_transform()
-            class ModelMeta(type): ...
-
-            class ModelBase(metaclass=ModelMeta): ...
-
-            class CustomerModel(ModelBase):
-                id: int
-                name: str
-
-        Each of the ``CustomerModel`` classes defined in this example will now
-        behave similarly to a dataclass created with the ``@dataclasses.dataclass``
-        decorator. For example, the type checker will synthesize an ``__init__``
-        method.
-
-        The arguments to this decorator can be used to customize this behavior:
-        - ``eq_default`` indicates whether the ``eq`` parameter is assumed to be
-          True or False if it is omitted by the caller.
-        - ``order_default`` indicates whether the ``order`` parameter is
-          assumed to be True or False if it is omitted by the caller.
-        - ``kw_only_default`` indicates whether the ``kw_only`` parameter is
-          assumed to be True or False if it is omitted by the caller.
-        - ``field_specifiers`` specifies a static list of supported classes
-          or functions that describe fields, similar to ``dataclasses.field()``.
-
-        At runtime, this decorator records its arguments in the
-        ``__dataclass_transform__`` attribute on the decorated object.
-
-        See PEP 681 for details.
-
-        """
-        def decorator(cls_or_fn):
-            cls_or_fn.__dataclass_transform__ = {
-                "eq_default": eq_default,
-                "order_default": order_default,
-                "kw_only_default": kw_only_default,
-                "field_specifiers": field_specifiers,
-                "kwargs": kwargs,
-            }
-            return cls_or_fn
-        return decorator
-
-
-if hasattr(typing, "override"):
-    override = typing.override
-else:
-    _F = typing.TypeVar("_F", bound=typing.Callable[..., typing.Any])
-
-    def override(__arg: _F) -> _F:
-        """Indicate that a method is intended to override a method in a base class.
-
-        Usage:
-
-            class Base:
-                def method(self) -> None: ...
-                    pass
-
-            class Child(Base):
-                @override
-                def method(self) -> None:
-                    super().method()
-
-        When this decorator is applied to a method, the type checker will
-        validate that it overrides a method with the same name on a base class.
-        This helps prevent bugs that may occur when a base class is changed
-        without an equivalent change to a child class.
-
-        See PEP 698 for details.
-
-        """
-        return __arg
-
-
-# We have to do some monkey patching to deal with the dual nature of
-# Unpack/TypeVarTuple:
-# - We want Unpack to be a kind of TypeVar so it gets accepted in
-#   Generic[Unpack[Ts]]
-# - We want it to *not* be treated as a TypeVar for the purposes of
-#   counting generic parameters, so that when we subscript a generic,
-#   the runtime doesn't try to substitute the Unpack with the subscripted type.
-if not hasattr(typing, "TypeVarTuple"):
-    typing._collect_type_vars = _collect_type_vars
-    typing._check_generic = _check_generic
-
-
-# Backport typing.NamedTuple as it exists in Python 3.11.
-# In 3.11, the ability to define generic `NamedTuple`s was supported.
-# This was explicitly disallowed in 3.9-3.10, and only half-worked in <=3.8.
-if sys.version_info >= (3, 11):
-    NamedTuple = typing.NamedTuple
-else:
-    def _caller():
-        try:
-            return sys._getframe(2).f_globals.get('__name__', '__main__')
-        except (AttributeError, ValueError):  # For platforms without _getframe()
-            return None
-
-    def _make_nmtuple(name, types, module, defaults=()):
-        fields = [n for n, t in types]
-        annotations = {n: typing._type_check(t, f"field {n} annotation must be a type")
-                       for n, t in types}
-        nm_tpl = collections.namedtuple(name, fields,
-                                        defaults=defaults, module=module)
-        nm_tpl.__annotations__ = nm_tpl.__new__.__annotations__ = annotations
-        # The `_field_types` attribute was removed in 3.9;
-        # in earlier versions, it is the same as the `__annotations__` attribute
-        if sys.version_info < (3, 9):
-            nm_tpl._field_types = annotations
-        return nm_tpl
-
-    _prohibited_namedtuple_fields = typing._prohibited
-    _special_namedtuple_fields = frozenset({'__module__', '__name__', '__annotations__'})
-
-    class _NamedTupleMeta(type):
-        def __new__(cls, typename, bases, ns):
-            assert _NamedTuple in bases
-            for base in bases:
-                if base is not _NamedTuple and base is not typing.Generic:
-                    raise TypeError(
-                        'can only inherit from a NamedTuple type and Generic')
-            bases = tuple(tuple if base is _NamedTuple else base for base in bases)
-            types = ns.get('__annotations__', {})
-            default_names = []
-            for field_name in types:
-                if field_name in ns:
-                    default_names.append(field_name)
-                elif default_names:
-                    raise TypeError(f"Non-default namedtuple field {field_name} "
-                                    f"cannot follow default field"
-                                    f"{'s' if len(default_names) > 1 else ''} "
-                                    f"{', '.join(default_names)}")
-            nm_tpl = _make_nmtuple(
-                typename, types.items(),
-                defaults=[ns[n] for n in default_names],
-                module=ns['__module__']
-            )
-            nm_tpl.__bases__ = bases
-            if typing.Generic in bases:
-                class_getitem = typing.Generic.__class_getitem__.__func__
-                nm_tpl.__class_getitem__ = classmethod(class_getitem)
-            # update from user namespace without overriding special namedtuple attributes
-            for key in ns:
-                if key in _prohibited_namedtuple_fields:
-                    raise AttributeError("Cannot overwrite NamedTuple attribute " + key)
-                elif key not in _special_namedtuple_fields and key not in nm_tpl._fields:
-                    setattr(nm_tpl, key, ns[key])
-            if typing.Generic in bases:
-                nm_tpl.__init_subclass__()
-            return nm_tpl
-
-    def NamedTuple(__typename, __fields=None, **kwargs):
-        if __fields is None:
-            __fields = kwargs.items()
-        elif kwargs:
-            raise TypeError("Either list of fields or keywords"
-                            " can be provided to NamedTuple, not both")
-        return _make_nmtuple(__typename, __fields, module=_caller())
-
-    NamedTuple.__doc__ = typing.NamedTuple.__doc__
-    _NamedTuple = type.__new__(_NamedTupleMeta, 'NamedTuple', (), {})
-
-    # On 3.8+, alter the signature so that it matches typing.NamedTuple.
-    # The signature of typing.NamedTuple on >=3.8 is invalid syntax in Python 3.7,
-    # so just leave the signature as it is on 3.7.
-    if sys.version_info >= (3, 8):
-        NamedTuple.__text_signature__ = '(typename, fields=None, /, **kwargs)'
-
-    def _namedtuple_mro_entries(bases):
-        assert NamedTuple in bases
-        return (_NamedTuple,)
-
-    NamedTuple.__mro_entries__ = _namedtuple_mro_entries
diff --git a/lib/pkg_resources/_vendor/vendored.txt b/lib/pkg_resources/_vendor/vendored.txt
index 4cd4ab8c..0c8fdc38 100644
--- a/lib/pkg_resources/_vendor/vendored.txt
+++ b/lib/pkg_resources/_vendor/vendored.txt
@@ -1,11 +1,13 @@
-packaging==23.1
+packaging==24
 
 platformdirs==2.6.2
-# required for platformdirs on Python < 3.8
-typing_extensions==4.4.0
 
 jaraco.text==3.7.0
 # required for jaraco.text on older Pythons
 importlib_resources==5.10.2
 # required for importlib_resources on older Pythons
 zipp==3.7.0
+# required for jaraco.functools
+more_itertools==10.2.0
+# required for jaraco.context on older Pythons
+backports.tarfile
diff --git a/lib/pkg_resources/extern/__init__.py b/lib/pkg_resources/extern/__init__.py
index 948bcc60..a1b7490d 100644
--- a/lib/pkg_resources/extern/__init__.py
+++ b/lib/pkg_resources/extern/__init__.py
@@ -1,5 +1,8 @@
+from importlib.machinery import ModuleSpec
 import importlib.util
 import sys
+from types import ModuleType
+from typing import Iterable, Optional, Sequence
 
 
 class VendorImporter:
@@ -8,7 +11,12 @@ class VendorImporter:
     or otherwise naturally-installed packages from root_name.
     """
 
-    def __init__(self, root_name, vendored_names=(), vendor_pkg=None):
+    def __init__(
+        self,
+        root_name: str,
+        vendored_names: Iterable[str] = (),
+        vendor_pkg: Optional[str] = None,
+    ):
         self.root_name = root_name
         self.vendored_names = set(vendored_names)
         self.vendor_pkg = vendor_pkg or root_name.replace('extern', '_vendor')
@@ -26,7 +34,7 @@ class VendorImporter:
         root, base, target = fullname.partition(self.root_name + '.')
         return not root and any(map(target.startswith, self.vendored_names))
 
-    def load_module(self, fullname):
+    def load_module(self, fullname: str):
         """
         Iterate over the search path to locate and load fullname.
         """
@@ -48,16 +56,22 @@ class VendorImporter:
                 "distribution.".format(**locals())
             )
 
-    def create_module(self, spec):
+    def create_module(self, spec: ModuleSpec):
         return self.load_module(spec.name)
 
-    def exec_module(self, module):
+    def exec_module(self, module: ModuleType):
         pass
 
-    def find_spec(self, fullname, path=None, target=None):
+    def find_spec(
+        self,
+        fullname: str,
+        path: Optional[Sequence[str]] = None,
+        target: Optional[ModuleType] = None,
+    ):
         """Return a module spec for vendored names."""
         return (
-            importlib.util.spec_from_loader(fullname, self)
+            # This should fix itself next mypy release https://github.com/python/typeshed/pull/11890
+            importlib.util.spec_from_loader(fullname, self)  # type: ignore[arg-type]
             if self._module_matches_namespace(fullname)
             else None
         )
@@ -70,11 +84,20 @@ class VendorImporter:
             sys.meta_path.append(self)
 
 
+# [[[cog
+# import cog
+# from tools.vendored import yield_top_level
+# names = "\n".join(f"    {x!r}," for x in yield_top_level('pkg_resources'))
+# cog.outl(f"names = (\n{names}\n)")
+# ]]]
 names = (
+    'backports',
+    'importlib_resources',
+    'jaraco',
+    'more_itertools',
     'packaging',
     'platformdirs',
-    'jaraco',
-    'importlib_resources',
-    'more_itertools',
+    'zipp',
 )
+# [[[end]]]
 VendorImporter(__name__, names).install()

From 16bb30029b42d5a17fcf4bd077f4a1c8d09eabcb Mon Sep 17 00:00:00 2001
From: JackDandy 
Date: Fri, 7 Jun 2024 17:42:01 +0100
Subject: [PATCH 19/29] =?UTF-8?q?Update=20unidecode=20module=201.3.6=20(41?=
 =?UTF-8?q?41992)=20=E2=86=92=201.3.8=20(dfe397d).?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 CHANGES.md                |  1 +
 lib/unidecode/__init__.py |  2 +-
 lib/unidecode/py.typed    |  0
 lib/unidecode/x01e.py     |  2 +-
 lib/unidecode/x021.py     |  6 ++---
 lib/unidecode/x026.py     | 46 ++++++++++++++++++++++-----------------
 lib/unidecode/x030.py     |  6 ++---
 lib/unidecode/x1f1.py     | 10 ++++-----
 8 files changed, 40 insertions(+), 33 deletions(-)
 create mode 100644 lib/unidecode/py.typed

diff --git a/CHANGES.md b/CHANGES.md
index 1201794a..f3e10232 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -17,6 +17,7 @@
 * Update Requests library 2.31.0 (8812812) to 2.32.3 (0e322af)
 * Update Send2Trash 1.5.0 (66afce7) to 1.8.3 (91d0698)
 * Update Tornado Web Server 6.4 (b3f2a4b) to 6.4.1 (2a0e1d1)
+* Update unidecode module 1.3.6 (4141992) to 1.3.8 (dfe397d)
 * Update urllib3 2.0.7 (56f01e0) to 2.2.1 (54d6edf)
 
 
diff --git a/lib/unidecode/__init__.py b/lib/unidecode/__init__.py
index 5633c458..7ac36754 100644
--- a/lib/unidecode/__init__.py
+++ b/lib/unidecode/__init__.py
@@ -19,7 +19,7 @@ b'Knosos'
 import warnings
 from typing import Dict, Optional, Sequence
 
-Cache = {} # type: Dict[int, Optional[Sequence[Optional[str]]]]
+Cache: Dict[int, Optional[Sequence[Optional[str]]]] = {}
 
 class UnidecodeError(ValueError):
     def __init__(self, message: str, index: Optional[int] = None) -> None:
diff --git a/lib/unidecode/py.typed b/lib/unidecode/py.typed
new file mode 100644
index 00000000..e69de29b
diff --git a/lib/unidecode/x01e.py b/lib/unidecode/x01e.py
index 540786a8..1c28ab2d 100644
--- a/lib/unidecode/x01e.py
+++ b/lib/unidecode/x01e.py
@@ -157,7 +157,7 @@ data = (
 'S',    # 0x9b
 None,    # 0x9c
 None,    # 0x9d
-'Ss',    # 0x9e
+'SS',    # 0x9e
 None,    # 0x9f
 'A',    # 0xa0
 'a',    # 0xa1
diff --git a/lib/unidecode/x021.py b/lib/unidecode/x021.py
index 6bd37f7e..2479c710 100644
--- a/lib/unidecode/x021.py
+++ b/lib/unidecode/x021.py
@@ -30,11 +30,11 @@ data = (
 'R',    # 0x1c
 'R',    # 0x1d
 '',    # 0x1e
-'',    # 0x1f
+'R/',    # 0x1f
 '(sm)',    # 0x20
 'TEL',    # 0x21
 '(tm)',    # 0x22
-'',    # 0x23
+'V/',    # 0x23
 'Z',    # 0x24
 '',    # 0x25
 'ohm',    # 0x26
@@ -76,7 +76,7 @@ None,    # 0x44
 None,    # 0x4a
 None,    # 0x4b
 None,    # 0x4c
-None,    # 0x4d
+'A/S',    # 0x4d
 'F',    # 0x4e
 None,    # 0x4f
 ' 1/7 ',    # 0x50
diff --git a/lib/unidecode/x026.py b/lib/unidecode/x026.py
index 380e84b9..49b5f3eb 100644
--- a/lib/unidecode/x026.py
+++ b/lib/unidecode/x026.py
@@ -83,26 +83,32 @@ None,    # 0x18
 '',    # 0x51
 '',    # 0x52
 '',    # 0x53
-'',    # 0x54
-'',    # 0x55
-'',    # 0x56
-'',    # 0x57
-'',    # 0x58
-'',    # 0x59
-'',    # 0x5a
-'',    # 0x5b
-'',    # 0x5c
-'',    # 0x5d
-'',    # 0x5e
-'',    # 0x5f
-'',    # 0x60
-'',    # 0x61
-'',    # 0x62
-'',    # 0x63
-'',    # 0x64
-'',    # 0x65
-'',    # 0x66
-'',    # 0x67
+'white king',    # 0x54
+'white queen',    # 0x55
+'white rook',    # 0x56
+'white bishop',    # 0x57
+'white knight',    # 0x58
+'white pawn',    # 0x59
+'black king',    # 0x5a
+'black queen',    # 0x5b
+'black rook',    # 0x5c
+'black bishop',    # 0x5d
+'black knight',    # 0x5e
+'black pawn',    # 0x5f
+
+# Unicode standard doesn't give an explanation why card suits exist in black
+# and white variants. Adding "white ..." and "black ..." here isn't useful
+# since both variants can be used for either red or black suit color.
+#
+# See discussion at https://github.com/avian2/unidecode/pull/93
+'spades',    # 0x60
+'hearts',    # 0x61
+'diamonds',    # 0x62
+'clubs',    # 0x63
+'spades',    # 0x64
+'hearts',    # 0x65
+'diamonds',    # 0x66
+'clubs',    # 0x67
 '',    # 0x68
 '',    # 0x69
 '',    # 0x6a
diff --git a/lib/unidecode/x030.py b/lib/unidecode/x030.py
index 549ee824..958c9b1f 100644
--- a/lib/unidecode/x030.py
+++ b/lib/unidecode/x030.py
@@ -159,7 +159,7 @@ None,    # 0x98
 '"',    # 0x9d
 '"',    # 0x9e
 None,    # 0x9f
-None,    # 0xa0
+'=',    # 0xa0
 'a',    # 0xa1
 'a',    # 0xa2
 'i',    # 0xa3
@@ -250,8 +250,8 @@ None,    # 0xa0
 'vi',    # 0xf8
 've',    # 0xf9
 'vo',    # 0xfa
-'',    # 0xfb
-'',    # 0xfc
+'*',    # 0xfb
+'-',    # 0xfc
 '"',    # 0xfd
 '"',    # 0xfe
 )
diff --git a/lib/unidecode/x1f1.py b/lib/unidecode/x1f1.py
index 2f78cf01..fffc296e 100644
--- a/lib/unidecode/x1f1.py
+++ b/lib/unidecode/x1f1.py
@@ -105,9 +105,9 @@ None,    # 0x4f
 '(X)',    # 0x67
 '(Y)',    # 0x68
 '(Z)',    # 0x69
-None,    # 0x6a
-None,    # 0x6b
-None,    # 0x6c
+'(mc)',    # 0x6a
+'(md)',    # 0x6b
+'(mr)',    # 0x6c
 None,    # 0x6d
 None,    # 0x6e
 None,    # 0x6f
@@ -155,7 +155,7 @@ None,    # 0x8f
 '[UP!]',    # 0x99
 '[VS]',    # 0x9a
 '[3D]',    # 0x9b
-'[2nc-Scr]',    # 0x9c
+'[2nd-Scr]',    # 0x9c
 '[2K]',    # 0x9d
 '[4K]',    # 0x9e
 '[8K]',    # 0x9f
@@ -168,7 +168,7 @@ None,    # 0x8f
 '[HC]',    # 0xa6
 '[HDR]',    # 0xa7
 '[Hi-res]',    # 0xa8
-'[Loss-less]',    # 0xa9
+'[Lossless]',    # 0xa9
 '[SHV]',    # 0xaa
 '[UHD]',    # 0xab
 '[VOD]',    # 0xac

From cd7804df1e0980fa4fa32a4947fe68a928dda08b Mon Sep 17 00:00:00 2001
From: JackDandy 
Date: Fri, 7 Jun 2024 19:48:09 +0100
Subject: [PATCH 20/29] Update apprise 1.6.0 (0c0d5da) to 1.8.0 (81caf92).

---
 CHANGES.md                                    |    1 +
 lib/apprise/__init__.py                       |   34 +-
 lib/apprise/{Apprise.py => apprise.py}        |   53 +-
 lib/apprise/{Apprise.pyi => apprise.pyi}      |    0
 ...iseAttachment.py => apprise_attachment.py} |   35 +-
 ...eAttachment.pyi => apprise_attachment.pyi} |    0
 .../{AppriseConfig.py => apprise_config.py}   |   22 +-
 .../{AppriseConfig.pyi => apprise_config.pyi} |    0
 lib/apprise/{AppriseAsset.py => asset.py}     |   26 +-
 lib/apprise/{AppriseAsset.pyi => asset.pyi}   |    0
 lib/apprise/attachment/AttachHTTP.py          |  337 ---
 lib/apprise/attachment/__init__.py            |  100 +-
 .../attachment/{AttachBase.py => base.py}     |   59 +-
 .../attachment/{AttachBase.pyi => base.pyi}   |    0
 .../attachment/{AttachFile.py => file.py}     |    9 +-
 lib/apprise/attachment/http.py                |  375 +++
 lib/apprise/attachment/memory.py              |  212 ++
 lib/apprise/common.py                         |   46 +-
 lib/apprise/config/__init__.py                |   91 +-
 lib/apprise/config/{ConfigBase.py => base.py} |   41 +-
 .../config/{ConfigBase.pyi => base.pyi}       |    0
 lib/apprise/config/{ConfigFile.py => file.py} |    6 +-
 lib/apprise/config/{ConfigHTTP.py => http.py} |    8 +-
 .../config/{ConfigMemory.py => memory.py}     |    6 +-
 lib/apprise/conversion.py                     |    8 +-
 lib/apprise/decorators/__init__.py            |    2 +-
 .../{CustomNotifyPlugin.py => base.py}        |   74 +-
 lib/apprise/decorators/notify.py              |    4 +-
 lib/apprise/emojis.py                         | 2273 +++++++++++++++++
 lib/apprise/i18n/apprise.pot                  |   64 +-
 lib/apprise/{AppriseLocale.py => locale.py}   |    5 +-
 lib/apprise/logger.py                         |    2 +-
 lib/apprise/manager.py                        |  756 ++++++
 lib/apprise/manager_attachment.py             |   59 +
 lib/apprise/manager_config.py                 |   59 +
 lib/apprise/manager_plugins.py                |   60 +
 .../plugins/NotifyGrowl/gntp/__init__.py      |    0
 lib/apprise/plugins/NotifySpontit.py          |  386 ---
 lib/apprise/plugins/__init__.py               |  135 +-
 .../{NotifyAppriseAPI.py => apprise_api.py}   |   10 +-
 lib/apprise/plugins/aprs.py                   |  778 ++++++
 .../plugins/{NotifyBark.py => bark.py}        |    8 +-
 .../plugins/{NotifyBase.py => base.py}        |  261 +-
 .../plugins/{NotifyBase.pyi => base.pyi}      |    0
 .../plugins/{NotifyBoxcar.py => boxcar.py}    |   14 +-
 .../plugins/{NotifyBulkSMS.py => bulksms.py}  |   10 +-
 lib/apprise/plugins/bulkvs.py                 |  394 +++
 .../{NotifyBurstSMS.py => burstsms.py}        |    8 +-
 .../plugins/{NotifyFaast.py => chantify.py}   |  141 +-
 .../{NotifyClickSend.py => clicksend.py}      |   29 +-
 .../plugins/{NotifyForm.py => custom_form.py} |    8 +-
 .../plugins/{NotifyJSON.py => custom_json.py} |    8 +-
 .../plugins/{NotifyXML.py => custom_xml.py}   |    8 +-
 .../{NotifyD7Networks.py => d7networks.py}    |    6 +-
 .../plugins/{NotifyDapnet.py => dapnet.py}    |    8 +-
 .../plugins/{NotifyDBus.py => dbus.py}        |    9 +-
 .../{NotifyDingTalk.py => dingtalk.py}        |    8 +-
 .../plugins/{NotifyDiscord.py => discord.py}  |   53 +-
 .../plugins/{NotifyEmail.py => email.py}      |  158 +-
 .../plugins/{NotifyEmby.py => emby.py}        |    8 +-
 .../plugins/{NotifyEnigma2.py => enigma2.py}  |    8 +-
 .../plugins/{NotifyFCM => fcm}/__init__.py    |    8 +-
 .../plugins/{NotifyFCM => fcm}/color.py       |    4 +-
 .../plugins/{NotifyFCM => fcm}/common.py      |    2 +-
 .../plugins/{NotifyFCM => fcm}/oauth.py       |    2 +-
 .../plugins/{NotifyFCM => fcm}/priority.py    |    2 +-
 lib/apprise/plugins/feishu.py                 |  231 ++
 .../plugins/{NotifyFlock.py => flock.py}      |    6 +-
 lib/apprise/plugins/freemobile.py             |  205 ++
 .../plugins/{NotifyGnome.py => gnome.py}      |    9 +-
 .../plugins/{NotifyGrowl => gntp}/__init__.py |    0
 .../plugins/{NotifyGrowl => }/gntp/cli.py     |    0
 .../plugins/{NotifyGrowl => }/gntp/config.py  |    0
 .../plugins/{NotifyGrowl => }/gntp/core.py    |    0
 .../plugins/{NotifyGrowl => }/gntp/errors.py  |    0
 .../{NotifyGrowl => }/gntp/notifier.py        |    0
 .../plugins/{NotifyGrowl => }/gntp/shim.py    |    0
 .../plugins/{NotifyGrowl => }/gntp/version.py |    0
 .../{NotifyGoogleChat.py => google_chat.py}   |    6 +-
 .../plugins/{NotifyGotify.py => gotify.py}    |    6 +-
 .../plugins/{NotifyGrowl.py => growl.py}      |    8 +-
 .../plugins/{NotifyGuilded.py => guilded.py}  |    7 +-
 ...tifyHomeAssistant.py => home_assistant.py} |    8 +-
 lib/apprise/plugins/httpsms.py                |  330 +++
 .../plugins/{NotifyIFTTT.py => ifttt.py}      |    6 +-
 .../plugins/{NotifyJoin.py => join.py}        |    6 +-
 .../{NotifyKavenegar.py => kavenegar.py}      |    6 +-
 .../plugins/{NotifyKumulos.py => kumulos.py}  |    6 +-
 .../{NotifyLametric.py => lametric.py}        |    6 +-
 .../plugins/{NotifyLine.py => line.py}        |    8 +-
 lib/apprise/plugins/lunasea.py                |  440 ++++
 .../plugins/{NotifyMacOSX.py => macosx.py}    |   11 +-
 .../plugins/{NotifyMailgun.py => mailgun.py}  |    6 +-
 .../{NotifyMastodon.py => mastodon.py}        |   10 +-
 .../plugins/{NotifyMatrix.py => matrix.py}    |   84 +-
 .../{NotifyMatterMost.py => mattermost.py}    |    6 +-
 .../{NotifyMessageBird.py => messagebird.py}  |    6 +-
 .../plugins/{NotifyMisskey.py => misskey.py}  |    6 +-
 .../plugins/{NotifyMQTT.py => mqtt.py}        |   10 +-
 .../plugins/{NotifyMSG91.py => msg91.py}      |    6 +-
 .../plugins/{NotifyMSTeams.py => msteams.py}  |    8 +-
 .../{NotifyNextcloud.py => nextcloud.py}      |    8 +-
 ...otifyNextcloudTalk.py => nextcloudtalk.py} |    8 +-
 .../plugins/{NotifyNotica.py => notica.py}    |    8 +-
 .../{NotifyNotifiarr.py => notifiarr.py}      |    6 +-
 .../{NotifyNotifico.py => notifico.py}        |    6 +-
 .../plugins/{NotifyNtfy.py => ntfy.py}        |   65 +-
 .../{NotifyOffice365.py => office365.py}      |    8 +-
 .../{NotifyOneSignal.py => one_signal.py}     |    6 +-
 .../{NotifyOpsgenie.py => opsgenie.py}        |    6 +-
 .../{NotifyPagerDuty.py => pagerduty.py}      |    8 +-
 .../{NotifyPagerTree.py => pagertree.py}      |    6 +-
 ...otifyParsePlatform.py => parseplatform.py} |    6 +-
 ...tifyPopcornNotify.py => popcorn_notify.py} |    6 +-
 .../plugins/{NotifyProwl.py => prowl.py}      |    6 +-
 .../{NotifyPushBullet.py => pushbullet.py}    |    8 +-
 .../{NotifyPushDeer.py => pushdeer.py}        |    6 +-
 .../plugins/{NotifyPushed.py => pushed.py}    |    8 +-
 .../plugins/{NotifyPushjet.py => pushjet.py}  |    8 +-
 .../plugins/{NotifyPushMe.py => pushme.py}    |    6 +-
 .../{NotifyPushover.py => pushover.py}        |    8 +-
 .../{NotifyPushSafer.py => pushsafer.py}      |    6 +-
 .../plugins/{NotifyPushy.py => pushy.py}      |    6 +-
 .../plugins/{NotifyReddit.py => reddit.py}    |    8 +-
 lib/apprise/plugins/revolt.py                 |  437 ++++
 .../{NotifyRocketChat.py => rocketchat.py}    |   67 +-
 .../plugins/{NotifyRSyslog.py => rsyslog.py}  |    6 +-
 .../plugins/{NotifyRyver.py => ryver.py}      |    6 +-
 .../{NotifySendGrid.py => sendgrid.py}        |    6 +-
 .../{NotifyServerChan.py => serverchan.py}    |    6 +-
 lib/apprise/plugins/{NotifySES.py => ses.py}  |    8 +-
 .../{NotifySignalAPI.py => signal_api.py}     |    8 +-
 .../{NotifySimplePush.py => simplepush.py}    |    8 +-
 .../plugins/{NotifySinch.py => sinch.py}      |    8 +-
 .../plugins/{NotifySlack.py => slack.py}      |  308 ++-
 .../{NotifySMSEagle.py => smseagle.py}        |    8 +-
 lib/apprise/plugins/smsmanager.py             |  413 +++
 .../plugins/{NotifySMTP2Go.py => smtp2go.py}  |    6 +-
 lib/apprise/plugins/{NotifySNS.py => sns.py}  |    8 +-
 .../{NotifySparkPost.py => sparkpost.py}      |    6 +-
 lib/apprise/plugins/splunk.py                 |  488 ++++
 .../{NotifyStreamlabs.py => streamlabs.py}    |    6 +-
 lib/apprise/plugins/synology.py               |  338 +++
 .../plugins/{NotifySyslog.py => syslog.py}    |    6 +-
 ...{NotifyTechulusPush.py => techuluspush.py} |    6 +-
 .../{NotifyTelegram.py => telegram.py}        |  184 +-
 lib/apprise/plugins/threema.py                |  370 +++
 .../plugins/{NotifyTwilio.py => twilio.py}    |    8 +-
 .../plugins/{NotifyTwist.py => twist.py}      |    8 +-
 .../plugins/{NotifyTwitter.py => twitter.py}  |   10 +-
 .../plugins/{NotifyVoipms.py => voipms.py}    |    6 +-
 .../plugins/{NotifyVonage.py => vonage.py}    |    8 +-
 .../{NotifyWebexTeams.py => webexteams.py}    |    6 +-
 lib/apprise/plugins/wecombot.py               |  258 ++
 .../{NotifyWhatsApp.py => whatsapp.py}        |    6 +-
 .../plugins/{NotifyWindows.py => windows.py}  |    9 +-
 .../plugins/{NotifyXBMC.py => xbmc.py}        |    8 +-
 .../plugins/{NotifyZulip.py => zulip.py}      |   30 +-
 lib/apprise/{URLBase.py => url.py}            |  145 +-
 lib/apprise/{URLBase.pyi => url.pyi}          |    0
 lib/apprise/utils.py                          |  190 +-
 161 files changed, 10193 insertions(+), 2171 deletions(-)
 rename lib/apprise/{Apprise.py => apprise.py} (95%)
 rename lib/apprise/{Apprise.pyi => apprise.pyi} (100%)
 rename lib/apprise/{AppriseAttachment.py => apprise_attachment.py} (92%)
 rename lib/apprise/{AppriseAttachment.pyi => apprise_attachment.pyi} (100%)
 rename lib/apprise/{AppriseConfig.py => apprise_config.py} (96%)
 rename lib/apprise/{AppriseConfig.pyi => apprise_config.pyi} (100%)
 rename lib/apprise/{AppriseAsset.py => asset.py} (94%)
 rename lib/apprise/{AppriseAsset.pyi => asset.pyi} (100%)
 delete mode 100644 lib/apprise/attachment/AttachHTTP.py
 rename lib/apprise/attachment/{AttachBase.py => base.py} (90%)
 rename lib/apprise/attachment/{AttachBase.pyi => base.pyi} (100%)
 rename lib/apprise/attachment/{AttachFile.py => file.py} (95%)
 create mode 100644 lib/apprise/attachment/http.py
 create mode 100644 lib/apprise/attachment/memory.py
 rename lib/apprise/config/{ConfigBase.py => base.py} (96%)
 rename lib/apprise/config/{ConfigBase.pyi => base.pyi} (100%)
 rename lib/apprise/config/{ConfigFile.py => file.py} (97%)
 rename lib/apprise/config/{ConfigHTTP.py => http.py} (98%)
 rename lib/apprise/config/{ConfigMemory.py => memory.py} (95%)
 rename lib/apprise/decorators/{CustomNotifyPlugin.py => base.py} (76%)
 create mode 100644 lib/apprise/emojis.py
 rename lib/apprise/{AppriseLocale.py => locale.py} (97%)
 create mode 100644 lib/apprise/manager.py
 create mode 100644 lib/apprise/manager_attachment.py
 create mode 100644 lib/apprise/manager_config.py
 create mode 100644 lib/apprise/manager_plugins.py
 delete mode 100644 lib/apprise/plugins/NotifyGrowl/gntp/__init__.py
 delete mode 100644 lib/apprise/plugins/NotifySpontit.py
 rename lib/apprise/plugins/{NotifyAppriseAPI.py => apprise_api.py} (98%)
 create mode 100644 lib/apprise/plugins/aprs.py
 rename lib/apprise/plugins/{NotifyBark.py => bark.py} (98%)
 rename lib/apprise/plugins/{NotifyBase.py => base.py} (68%)
 rename lib/apprise/plugins/{NotifyBase.pyi => base.pyi} (100%)
 rename lib/apprise/plugins/{NotifyBoxcar.py => boxcar.py} (97%)
 rename lib/apprise/plugins/{NotifyBulkSMS.py => bulksms.py} (98%)
 create mode 100644 lib/apprise/plugins/bulkvs.py
 rename lib/apprise/plugins/{NotifyBurstSMS.py => burstsms.py} (98%)
 rename lib/apprise/plugins/{NotifyFaast.py => chantify.py} (55%)
 rename lib/apprise/plugins/{NotifyClickSend.py => clicksend.py} (94%)
 rename lib/apprise/plugins/{NotifyForm.py => custom_form.py} (99%)
 rename lib/apprise/plugins/{NotifyJSON.py => custom_json.py} (98%)
 rename lib/apprise/plugins/{NotifyXML.py => custom_xml.py} (98%)
 rename lib/apprise/plugins/{NotifyD7Networks.py => d7networks.py} (99%)
 rename lib/apprise/plugins/{NotifyDapnet.py => dapnet.py} (98%)
 rename lib/apprise/plugins/{NotifyDBus.py => dbus.py} (98%)
 rename lib/apprise/plugins/{NotifyDingTalk.py => dingtalk.py} (98%)
 rename lib/apprise/plugins/{NotifyDiscord.py => discord.py} (93%)
 rename lib/apprise/plugins/{NotifyEmail.py => email.py} (94%)
 rename lib/apprise/plugins/{NotifyEmby.py => emby.py} (99%)
 rename lib/apprise/plugins/{NotifyEnigma2.py => enigma2.py} (98%)
 rename lib/apprise/plugins/{NotifyFCM => fcm}/__init__.py (99%)
 rename lib/apprise/plugins/{NotifyFCM => fcm}/color.py (97%)
 rename lib/apprise/plugins/{NotifyFCM => fcm}/common.py (96%)
 rename lib/apprise/plugins/{NotifyFCM => fcm}/oauth.py (99%)
 rename lib/apprise/plugins/{NotifyFCM => fcm}/priority.py (99%)
 create mode 100644 lib/apprise/plugins/feishu.py
 rename lib/apprise/plugins/{NotifyFlock.py => flock.py} (98%)
 create mode 100644 lib/apprise/plugins/freemobile.py
 rename lib/apprise/plugins/{NotifyGnome.py => gnome.py} (97%)
 rename lib/apprise/plugins/{NotifyGrowl => gntp}/__init__.py (100%)
 rename lib/apprise/plugins/{NotifyGrowl => }/gntp/cli.py (100%)
 rename lib/apprise/plugins/{NotifyGrowl => }/gntp/config.py (100%)
 rename lib/apprise/plugins/{NotifyGrowl => }/gntp/core.py (100%)
 rename lib/apprise/plugins/{NotifyGrowl => }/gntp/errors.py (100%)
 rename lib/apprise/plugins/{NotifyGrowl => }/gntp/notifier.py (100%)
 rename lib/apprise/plugins/{NotifyGrowl => }/gntp/shim.py (100%)
 rename lib/apprise/plugins/{NotifyGrowl => }/gntp/version.py (100%)
 rename lib/apprise/plugins/{NotifyGoogleChat.py => google_chat.py} (98%)
 rename lib/apprise/plugins/{NotifyGotify.py => gotify.py} (98%)
 rename lib/apprise/plugins/{NotifyGrowl.py => growl.py} (98%)
 rename lib/apprise/plugins/{NotifyGuilded.py => guilded.py} (94%)
 rename lib/apprise/plugins/{NotifyHomeAssistant.py => home_assistant.py} (98%)
 create mode 100644 lib/apprise/plugins/httpsms.py
 rename lib/apprise/plugins/{NotifyIFTTT.py => ifttt.py} (98%)
 rename lib/apprise/plugins/{NotifyJoin.py => join.py} (98%)
 rename lib/apprise/plugins/{NotifyKavenegar.py => kavenegar.py} (98%)
 rename lib/apprise/plugins/{NotifyKumulos.py => kumulos.py} (98%)
 rename lib/apprise/plugins/{NotifyLametric.py => lametric.py} (99%)
 rename lib/apprise/plugins/{NotifyLine.py => line.py} (98%)
 create mode 100644 lib/apprise/plugins/lunasea.py
 rename lib/apprise/plugins/{NotifyMacOSX.py => macosx.py} (96%)
 rename lib/apprise/plugins/{NotifyMailgun.py => mailgun.py} (99%)
 rename lib/apprise/plugins/{NotifyMastodon.py => mastodon.py} (99%)
 rename lib/apprise/plugins/{NotifyMatrix.py => matrix.py} (95%)
 rename lib/apprise/plugins/{NotifyMatterMost.py => mattermost.py} (98%)
 rename lib/apprise/plugins/{NotifyMessageBird.py => messagebird.py} (98%)
 rename lib/apprise/plugins/{NotifyMisskey.py => misskey.py} (98%)
 rename lib/apprise/plugins/{NotifyMQTT.py => mqtt.py} (98%)
 rename lib/apprise/plugins/{NotifyMSG91.py => msg91.py} (98%)
 rename lib/apprise/plugins/{NotifyMSTeams.py => msteams.py} (99%)
 rename lib/apprise/plugins/{NotifyNextcloud.py => nextcloud.py} (98%)
 rename lib/apprise/plugins/{NotifyNextcloudTalk.py => nextcloudtalk.py} (98%)
 rename lib/apprise/plugins/{NotifyNotica.py => notica.py} (98%)
 rename lib/apprise/plugins/{NotifyNotifiarr.py => notifiarr.py} (99%)
 rename lib/apprise/plugins/{NotifyNotifico.py => notifico.py} (98%)
 rename lib/apprise/plugins/{NotifyNtfy.py => ntfy.py} (93%)
 rename lib/apprise/plugins/{NotifyOffice365.py => office365.py} (99%)
 rename lib/apprise/plugins/{NotifyOneSignal.py => one_signal.py} (99%)
 rename lib/apprise/plugins/{NotifyOpsgenie.py => opsgenie.py} (99%)
 rename lib/apprise/plugins/{NotifyPagerDuty.py => pagerduty.py} (99%)
 rename lib/apprise/plugins/{NotifyPagerTree.py => pagertree.py} (99%)
 rename lib/apprise/plugins/{NotifyParsePlatform.py => parseplatform.py} (98%)
 rename lib/apprise/plugins/{NotifyPopcornNotify.py => popcorn_notify.py} (98%)
 rename lib/apprise/plugins/{NotifyProwl.py => prowl.py} (98%)
 rename lib/apprise/plugins/{NotifyPushBullet.py => pushbullet.py} (98%)
 rename lib/apprise/plugins/{NotifyPushDeer.py => pushdeer.py} (98%)
 rename lib/apprise/plugins/{NotifyPushed.py => pushed.py} (98%)
 rename lib/apprise/plugins/{NotifyPushjet.py => pushjet.py} (98%)
 rename lib/apprise/plugins/{NotifyPushMe.py => pushme.py} (98%)
 rename lib/apprise/plugins/{NotifyPushover.py => pushover.py} (99%)
 rename lib/apprise/plugins/{NotifyPushSafer.py => pushsafer.py} (99%)
 rename lib/apprise/plugins/{NotifyPushy.py => pushy.py} (98%)
 rename lib/apprise/plugins/{NotifyReddit.py => reddit.py} (99%)
 create mode 100644 lib/apprise/plugins/revolt.py
 rename lib/apprise/plugins/{NotifyRocketChat.py => rocketchat.py} (92%)
 rename lib/apprise/plugins/{NotifyRSyslog.py => rsyslog.py} (98%)
 rename lib/apprise/plugins/{NotifyRyver.py => ryver.py} (98%)
 rename lib/apprise/plugins/{NotifySendGrid.py => sendgrid.py} (99%)
 rename lib/apprise/plugins/{NotifyServerChan.py => serverchan.py} (97%)
 rename lib/apprise/plugins/{NotifySES.py => ses.py} (99%)
 rename lib/apprise/plugins/{NotifySignalAPI.py => signal_api.py} (98%)
 rename lib/apprise/plugins/{NotifySimplePush.py => simplepush.py} (98%)
 rename lib/apprise/plugins/{NotifySinch.py => sinch.py} (98%)
 rename lib/apprise/plugins/{NotifySlack.py => slack.py} (82%)
 rename lib/apprise/plugins/{NotifySMSEagle.py => smseagle.py} (99%)
 create mode 100644 lib/apprise/plugins/smsmanager.py
 rename lib/apprise/plugins/{NotifySMTP2Go.py => smtp2go.py} (99%)
 rename lib/apprise/plugins/{NotifySNS.py => sns.py} (99%)
 rename lib/apprise/plugins/{NotifySparkPost.py => sparkpost.py} (99%)
 create mode 100644 lib/apprise/plugins/splunk.py
 rename lib/apprise/plugins/{NotifyStreamlabs.py => streamlabs.py} (99%)
 create mode 100644 lib/apprise/plugins/synology.py
 rename lib/apprise/plugins/{NotifySyslog.py => syslog.py} (98%)
 rename lib/apprise/plugins/{NotifyTechulusPush.py => techuluspush.py} (98%)
 rename lib/apprise/plugins/{NotifyTelegram.py => telegram.py} (87%)
 create mode 100644 lib/apprise/plugins/threema.py
 rename lib/apprise/plugins/{NotifyTwilio.py => twilio.py} (98%)
 rename lib/apprise/plugins/{NotifyTwist.py => twist.py} (99%)
 rename lib/apprise/plugins/{NotifyTwitter.py => twitter.py} (99%)
 rename lib/apprise/plugins/{NotifyVoipms.py => voipms.py} (98%)
 rename lib/apprise/plugins/{NotifyVonage.py => vonage.py} (98%)
 rename lib/apprise/plugins/{NotifyWebexTeams.py => webexteams.py} (98%)
 create mode 100644 lib/apprise/plugins/wecombot.py
 rename lib/apprise/plugins/{NotifyWhatsApp.py => whatsapp.py} (99%)
 rename lib/apprise/plugins/{NotifyWindows.py => windows.py} (97%)
 rename lib/apprise/plugins/{NotifyXBMC.py => xbmc.py} (98%)
 rename lib/apprise/plugins/{NotifyZulip.py => zulip.py} (95%)
 rename lib/apprise/{URLBase.py => url.py} (89%)
 rename lib/apprise/{URLBase.pyi => url.pyi} (100%)

diff --git a/CHANGES.md b/CHANGES.md
index f3e10232..93aad29c 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -1,5 +1,6 @@
 ### 3.32.0 (2024-xx-xx xx:xx:00 UTC)
 
+* Update apprise 1.6.0 (0c0d5da) to 1.8.0 (81caf92)
 * Update attr 23.1.0 (67e4ff2) to 23.2.0 (b393d79)
 * Update Beautiful Soup 4.12.2 (30c58a1) to 4.12.3 (7fb5175)
 * Update CacheControl 0.13.1 (783a338) to 0.14.0 (e2be0c2)
diff --git a/lib/apprise/__init__.py b/lib/apprise/__init__.py
index f8bb5c75..c57e7701 100644
--- a/lib/apprise/__init__.py
+++ b/lib/apprise/__init__.py
@@ -2,7 +2,7 @@
 # BSD 2-Clause License
 #
 # Apprise - Push Notification Library.
-# Copyright (c) 2023, Chris Caron 
+# Copyright (c) 2024, Chris Caron 
 #
 # Redistribution and use in source and binary forms, with or without
 # modification, are permitted provided that the following conditions are met:
@@ -27,10 +27,10 @@
 # POSSIBILITY OF SUCH DAMAGE.
 
 __title__ = 'Apprise'
-__version__ = '1.6.0'
+__version__ = '1.8.0'
 __author__ = 'Chris Caron'
 __license__ = 'BSD'
-__copywrite__ = 'Copyright (C) 2023 Chris Caron '
+__copywrite__ = 'Copyright (C) 2024 Chris Caron '
 __email__ = 'lead2gold@gmail.com'
 __status__ = 'Production'
 
@@ -49,17 +49,20 @@ from .common import CONTENT_INCLUDE_MODES
 from .common import ContentLocation
 from .common import CONTENT_LOCATIONS
 
-from .URLBase import URLBase
-from .URLBase import PrivacyMode
-from .plugins.NotifyBase import NotifyBase
-from .config.ConfigBase import ConfigBase
-from .attachment.AttachBase import AttachBase
-
-from .Apprise import Apprise
-from .AppriseAsset import AppriseAsset
-from .AppriseConfig import AppriseConfig
-from .AppriseAttachment import AppriseAttachment
+from .url import URLBase
+from .url import PrivacyMode
+from .plugins.base import NotifyBase
+from .config.base import ConfigBase
+from .attachment.base import AttachBase
 
+from .apprise import Apprise
+from .locale import AppriseLocale
+from .asset import AppriseAsset
+from .apprise_config import AppriseConfig
+from .apprise_attachment import AppriseAttachment
+from .manager_attachment import AttachmentManager
+from .manager_config import ConfigurationManager
+from .manager_plugins import NotificationManager
 from . import decorators
 
 # Inherit our logging with our additional entries added to it
@@ -73,7 +76,7 @@ logging.getLogger(__name__).addHandler(logging.NullHandler())
 __all__ = [
     # Core
     'Apprise', 'AppriseAsset', 'AppriseConfig', 'AppriseAttachment', 'URLBase',
-    'NotifyBase', 'ConfigBase', 'AttachBase',
+    'NotifyBase', 'ConfigBase', 'AttachBase', 'AppriseLocale',
 
     # Reference
     'NotifyType', 'NotifyImageSize', 'NotifyFormat', 'OverflowMode',
@@ -83,6 +86,9 @@ __all__ = [
     'ContentLocation', 'CONTENT_LOCATIONS',
     'PrivacyMode',
 
+    # Managers
+    'NotificationManager', 'ConfigurationManager', 'AttachmentManager',
+
     # Decorator
     'decorators',
 
diff --git a/lib/apprise/Apprise.py b/lib/apprise/apprise.py
similarity index 95%
rename from lib/apprise/Apprise.py
rename to lib/apprise/apprise.py
index 4c83c481..05a2ee3c 100644
--- a/lib/apprise/Apprise.py
+++ b/lib/apprise/apprise.py
@@ -2,7 +2,7 @@
 # BSD 2-Clause License
 #
 # Apprise - Push Notification Library.
-# Copyright (c) 2023, Chris Caron 
+# Copyright (c) 2024, Chris Caron 
 #
 # Redistribution and use in source and binary forms, with or without
 # modification, are permitted provided that the following conditions are met:
@@ -33,21 +33,25 @@ from itertools import chain
 from . import common
 from .conversion import convert_between
 from .utils import is_exclusive_match
+from .manager_plugins import NotificationManager
 from .utils import parse_list
 from .utils import parse_urls
 from .utils import cwe312_url
+from .emojis import apply_emojis
 from .logger import logger
-from .AppriseAsset import AppriseAsset
-from .AppriseConfig import AppriseConfig
-from .AppriseAttachment import AppriseAttachment
-from .AppriseLocale import AppriseLocale
-from .config.ConfigBase import ConfigBase
-from .plugins.NotifyBase import NotifyBase
-
+from .asset import AppriseAsset
+from .apprise_config import AppriseConfig
+from .apprise_attachment import AppriseAttachment
+from .locale import AppriseLocale
+from .config.base import ConfigBase
+from .plugins.base import NotifyBase
 
 from . import plugins
 from . import __version__
 
+# Grant access to our Notification Manager Singleton
+N_MGR = NotificationManager()
+
 
 class Apprise:
     """
@@ -137,7 +141,7 @@ class Apprise:
             # We already have our result set
             results = url
 
-            if results.get('schema') not in common.NOTIFY_SCHEMA_MAP:
+            if results.get('schema') not in N_MGR:
                 # schema is a mandatory dictionary item as it is the only way
                 # we can index into our loaded plugins
                 logger.error('Dictionary does not include a "schema" entry.')
@@ -160,7 +164,7 @@ class Apprise:
                 type(url))
             return None
 
-        if not common.NOTIFY_SCHEMA_MAP[results['schema']].enabled:
+        if not N_MGR[results['schema']].enabled:
             #
             # First Plugin Enable Check (Pre Initialization)
             #
@@ -180,13 +184,12 @@ class Apprise:
             try:
                 # Attempt to create an instance of our plugin using the parsed
                 # URL information
-                plugin = common.NOTIFY_SCHEMA_MAP[results['schema']](**results)
+                plugin = N_MGR[results['schema']](**results)
 
                 # Create log entry of loaded URL
                 logger.debug(
                     'Loaded {} URL: {}'.format(
-                        common.
-                        NOTIFY_SCHEMA_MAP[results['schema']].service_name,
+                        N_MGR[results['schema']].service_name,
                         plugin.url(privacy=asset.secure_logging)))
 
             except Exception:
@@ -197,15 +200,14 @@ class Apprise:
                 # the arguments are invalid or can not be used.
                 logger.error(
                     'Could not load {} URL: {}'.format(
-                        common.
-                        NOTIFY_SCHEMA_MAP[results['schema']].service_name,
+                        N_MGR[results['schema']].service_name,
                         loggable_url))
                 return None
 
         else:
             # Attempt to create an instance of our plugin using the parsed
             # URL information but don't wrap it in a try catch
-            plugin = common.NOTIFY_SCHEMA_MAP[results['schema']](**results)
+            plugin = N_MGR[results['schema']](**results)
 
         if not plugin.enabled:
             #
@@ -376,7 +378,7 @@ class Apprise:
                 body, title,
                 notify_type=notify_type, body_format=body_format,
                 tag=tag, match_always=match_always, attach=attach,
-                interpret_escapes=interpret_escapes
+                interpret_escapes=interpret_escapes,
             )
 
         except TypeError:
@@ -501,6 +503,11 @@ class Apprise:
             key = server.notify_format if server.title_maxlen > 0\
                 else f'_{server.notify_format}'
 
+            if server.interpret_emojis:
+                # alter our key slightly to handle emojis since their value is
+                # pulled out of the notification
+                key += "-emojis"
+
             if key not in conversion_title_map:
 
                 # Prepare our title
@@ -542,6 +549,16 @@ class Apprise:
                         logger.error(msg)
                         raise TypeError(msg)
 
+                if server.interpret_emojis:
+                    #
+                    # Convert our :emoji: definitions
+                    #
+
+                    conversion_body_map[key] = \
+                        apply_emojis(conversion_body_map[key])
+                    conversion_title_map[key] = \
+                        apply_emojis(conversion_title_map[key])
+
             kwargs = dict(
                 body=conversion_body_map[key],
                 title=conversion_title_map[key],
@@ -674,7 +691,7 @@ class Apprise:
             'asset': self.asset.details(),
         }
 
-        for plugin in set(common.NOTIFY_SCHEMA_MAP.values()):
+        for plugin in N_MGR.plugins():
             # Iterate over our hashed plugins and dynamically build details on
             # their status:
 
diff --git a/lib/apprise/Apprise.pyi b/lib/apprise/apprise.pyi
similarity index 100%
rename from lib/apprise/Apprise.pyi
rename to lib/apprise/apprise.pyi
diff --git a/lib/apprise/AppriseAttachment.py b/lib/apprise/apprise_attachment.py
similarity index 92%
rename from lib/apprise/AppriseAttachment.py
rename to lib/apprise/apprise_attachment.py
index e00645d2..ecf415ec 100644
--- a/lib/apprise/AppriseAttachment.py
+++ b/lib/apprise/apprise_attachment.py
@@ -2,7 +2,7 @@
 # BSD 2-Clause License
 #
 # Apprise - Push Notification Library.
-# Copyright (c) 2023, Chris Caron 
+# Copyright (c) 2024, Chris Caron 
 #
 # Redistribution and use in source and binary forms, with or without
 # modification, are permitted provided that the following conditions are met:
@@ -26,15 +26,18 @@
 # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 # POSSIBILITY OF SUCH DAMAGE.
 
-from . import attachment
 from . import URLBase
-from .AppriseAsset import AppriseAsset
+from .attachment.base import AttachBase
+from .asset import AppriseAsset
+from .manager_attachment import AttachmentManager
 from .logger import logger
 from .common import ContentLocation
 from .common import CONTENT_LOCATIONS
-from .common import ATTACHMENT_SCHEMA_MAP
 from .utils import GET_SCHEMA_RE
 
+# Grant access to our Notification Manager Singleton
+A_MGR = AttachmentManager()
+
 
 class AppriseAttachment:
     """
@@ -139,13 +142,8 @@ class AppriseAttachment:
             # prepare default asset
             asset = self.asset
 
-        if isinstance(attachments, attachment.AttachBase):
-            # Go ahead and just add our attachments into our list
-            self.attachments.append(attachments)
-            return True
-
-        elif isinstance(attachments, str):
-            # Save our path
+        if isinstance(attachments, (AttachBase, str)):
+            # store our instance
             attachments = (attachments, )
 
         elif not isinstance(attachments, (tuple, set, list)):
@@ -169,7 +167,7 @@ class AppriseAttachment:
                 # returns None if it fails
                 instance = AppriseAttachment.instantiate(
                     _attachment, asset=asset, cache=cache)
-                if not isinstance(instance, attachment.AttachBase):
+                if not isinstance(instance, AttachBase):
                     return_status = False
                     continue
 
@@ -178,7 +176,7 @@ class AppriseAttachment:
                 # append our content together
                 instance = _attachment.attachments
 
-            elif not isinstance(_attachment, attachment.AttachBase):
+            elif not isinstance(_attachment, AttachBase):
                 logger.warning(
                     "An invalid attachment (type={}) was specified.".format(
                         type(_attachment)))
@@ -228,7 +226,7 @@ class AppriseAttachment:
         schema = GET_SCHEMA_RE.match(url)
         if schema is None:
             # Plan B is to assume we're dealing with a file
-            schema = attachment.AttachFile.protocol
+            schema = 'file'
             url = '{}://{}'.format(schema, URLBase.quote(url))
 
         else:
@@ -236,13 +234,13 @@ class AppriseAttachment:
             schema = schema.group('schema').lower()
 
             # Some basic validation
-            if schema not in ATTACHMENT_SCHEMA_MAP:
+            if schema not in A_MGR:
                 logger.warning('Unsupported schema {}.'.format(schema))
                 return None
 
         # Parse our url details of the server object as dictionary containing
         # all of the information parsed from our URL
-        results = ATTACHMENT_SCHEMA_MAP[schema].parse_url(url)
+        results = A_MGR[schema].parse_url(url)
 
         if not results:
             # Failed to parse the server URL
@@ -261,8 +259,7 @@ class AppriseAttachment:
             try:
                 # Attempt to create an instance of our plugin using the parsed
                 # URL information
-                attach_plugin = \
-                    ATTACHMENT_SCHEMA_MAP[results['schema']](**results)
+                attach_plugin = A_MGR[results['schema']](**results)
 
             except Exception:
                 # the arguments are invalid or can not be used.
@@ -272,7 +269,7 @@ class AppriseAttachment:
         else:
             # Attempt to create an instance of our plugin using the parsed
             # URL information but don't wrap it in a try catch
-            attach_plugin = ATTACHMENT_SCHEMA_MAP[results['schema']](**results)
+            attach_plugin = A_MGR[results['schema']](**results)
 
         return attach_plugin
 
diff --git a/lib/apprise/AppriseAttachment.pyi b/lib/apprise/apprise_attachment.pyi
similarity index 100%
rename from lib/apprise/AppriseAttachment.pyi
rename to lib/apprise/apprise_attachment.pyi
diff --git a/lib/apprise/AppriseConfig.py b/lib/apprise/apprise_config.py
similarity index 96%
rename from lib/apprise/AppriseConfig.py
rename to lib/apprise/apprise_config.py
index 07e7b48e..080f70d3 100644
--- a/lib/apprise/AppriseConfig.py
+++ b/lib/apprise/apprise_config.py
@@ -2,7 +2,7 @@
 # BSD 2-Clause License
 #
 # Apprise - Push Notification Library.
-# Copyright (c) 2023, Chris Caron 
+# Copyright (c) 2024, Chris Caron 
 #
 # Redistribution and use in source and binary forms, with or without
 # modification, are permitted provided that the following conditions are met:
@@ -26,17 +26,20 @@
 # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 # POSSIBILITY OF SUCH DAMAGE.
 
-from . import config
 from . import ConfigBase
 from . import CONFIG_FORMATS
+from .manager_config import ConfigurationManager
 from . import URLBase
-from .AppriseAsset import AppriseAsset
+from .asset import AppriseAsset
 from . import common
 from .utils import GET_SCHEMA_RE
 from .utils import parse_list
 from .utils import is_exclusive_match
 from .logger import logger
 
+# Grant access to our Configuration Manager Singleton
+C_MGR = ConfigurationManager()
+
 
 class AppriseConfig:
     """
@@ -251,7 +254,7 @@ class AppriseConfig:
         logger.debug("Loading raw configuration: {}".format(content))
 
         # Create ourselves a ConfigMemory Object to store our configuration
-        instance = config.ConfigMemory(
+        instance = C_MGR['memory'](
             content=content, format=format, asset=asset, tag=tag,
             recursion=recursion, insecure_includes=insecure_includes)
 
@@ -326,7 +329,7 @@ class AppriseConfig:
         schema = GET_SCHEMA_RE.match(url)
         if schema is None:
             # Plan B is to assume we're dealing with a file
-            schema = config.ConfigFile.protocol
+            schema = 'file'
             url = '{}://{}'.format(schema, URLBase.quote(url))
 
         else:
@@ -334,13 +337,13 @@ class AppriseConfig:
             schema = schema.group('schema').lower()
 
             # Some basic validation
-            if schema not in common.CONFIG_SCHEMA_MAP:
+            if schema not in C_MGR:
                 logger.warning('Unsupported schema {}.'.format(schema))
                 return None
 
         # Parse our url details of the server object as dictionary containing
         # all of the information parsed from our URL
-        results = common.CONFIG_SCHEMA_MAP[schema].parse_url(url)
+        results = C_MGR[schema].parse_url(url)
 
         if not results:
             # Failed to parse the server URL
@@ -368,8 +371,7 @@ class AppriseConfig:
             try:
                 # Attempt to create an instance of our plugin using the parsed
                 # URL information
-                cfg_plugin = \
-                    common.CONFIG_SCHEMA_MAP[results['schema']](**results)
+                cfg_plugin = C_MGR[results['schema']](**results)
 
             except Exception:
                 # the arguments are invalid or can not be used.
@@ -379,7 +381,7 @@ class AppriseConfig:
         else:
             # Attempt to create an instance of our plugin using the parsed
             # URL information but don't wrap it in a try catch
-            cfg_plugin = common.CONFIG_SCHEMA_MAP[results['schema']](**results)
+            cfg_plugin = C_MGR[results['schema']](**results)
 
         return cfg_plugin
 
diff --git a/lib/apprise/AppriseConfig.pyi b/lib/apprise/apprise_config.pyi
similarity index 100%
rename from lib/apprise/AppriseConfig.pyi
rename to lib/apprise/apprise_config.pyi
diff --git a/lib/apprise/AppriseAsset.py b/lib/apprise/asset.py
similarity index 94%
rename from lib/apprise/AppriseAsset.py
rename to lib/apprise/asset.py
index 835c3b6a..c0fab9c0 100644
--- a/lib/apprise/AppriseAsset.py
+++ b/lib/apprise/asset.py
@@ -2,7 +2,7 @@
 # BSD 2-Clause License
 #
 # Apprise - Push Notification Library.
-# Copyright (c) 2023, Chris Caron 
+# Copyright (c) 2024, Chris Caron 
 #
 # Redistribution and use in source and binary forms, with or without
 # modification, are permitted provided that the following conditions are met:
@@ -33,7 +33,11 @@ from os.path import dirname
 from os.path import isfile
 from os.path import abspath
 from .common import NotifyType
-from .utils import module_detection
+from .manager_plugins import NotificationManager
+
+
+# Grant access to our Notification Manager Singleton
+N_MGR = NotificationManager()
 
 
 class AppriseAsset:
@@ -66,6 +70,9 @@ class AppriseAsset:
         NotifyType.WARNING: '#CACF29',
     }
 
+    # The default color to return if a mapping isn't found in our table above
+    default_html_color = '#888888'
+
     # Ascii Notification
     ascii_notify_map = {
         NotifyType.INFO: '[i]',
@@ -74,8 +81,8 @@ class AppriseAsset:
         NotifyType.WARNING: '[~]',
     }
 
-    # The default color to return if a mapping isn't found in our table above
-    default_html_color = '#888888'
+    # The default ascii to return if a mapping isn't found in our table above
+    default_ascii_chars = '[?]'
 
     # The default image extension to use
     default_extension = '.png'
@@ -121,6 +128,12 @@ class AppriseAsset:
     # notifications are sent sequentially (one after another)
     async_mode = True
 
+    # Support :smile:, and other alike keywords swapping them for their
+    # unicode value. A value of None leaves the interpretation up to the
+    # end user to control (allowing them to specify emojis=yes on the
+    # URL)
+    interpret_emojis = None
+
     # Whether or not to interpret escapes found within the input text prior
     # to passing it upstream. Such as converting \t to an actual tab and \n
     # to a new line.
@@ -174,7 +187,7 @@ class AppriseAsset:
 
         if plugin_paths:
             # Load any decorated modules if defined
-            module_detection(plugin_paths)
+            N_MGR.module_detection(plugin_paths)
 
     def color(self, notify_type, color_type=None):
         """
@@ -213,9 +226,8 @@ class AppriseAsset:
         Returns an ascii representation based on passed in notify type
 
         """
-
         # look our response up
-        return self.ascii_notify_map.get(notify_type, self.default_html_color)
+        return self.ascii_notify_map.get(notify_type, self.default_ascii_chars)
 
     def image_url(self, notify_type, image_size, logo=False, extension=None):
         """
diff --git a/lib/apprise/AppriseAsset.pyi b/lib/apprise/asset.pyi
similarity index 100%
rename from lib/apprise/AppriseAsset.pyi
rename to lib/apprise/asset.pyi
diff --git a/lib/apprise/attachment/AttachHTTP.py b/lib/apprise/attachment/AttachHTTP.py
deleted file mode 100644
index 0c859477..00000000
--- a/lib/apprise/attachment/AttachHTTP.py
+++ /dev/null
@@ -1,337 +0,0 @@
-# -*- coding: utf-8 -*-
-# BSD 2-Clause License
-#
-# Apprise - Push Notification Library.
-# Copyright (c) 2023, Chris Caron 
-#
-# Redistribution and use in source and binary forms, with or without
-# modification, are permitted provided that the following conditions are met:
-#
-# 1. Redistributions of source code must retain the above copyright notice,
-#    this list of conditions and the following disclaimer.
-#
-# 2. Redistributions in binary form must reproduce the above copyright notice,
-#    this list of conditions and the following disclaimer in the documentation
-#    and/or other materials provided with the distribution.
-#
-# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
-# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
-# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
-# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
-# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
-# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
-# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
-# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
-# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
-# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
-# POSSIBILITY OF SUCH DAMAGE.
-
-import re
-import os
-import requests
-from tempfile import NamedTemporaryFile
-from .AttachBase import AttachBase
-from ..common import ContentLocation
-from ..URLBase import PrivacyMode
-from ..AppriseLocale import gettext_lazy as _
-
-
-class AttachHTTP(AttachBase):
-    """
-    A wrapper for HTTP based attachment sources
-    """
-
-    # The default descriptive name associated with the service
-    service_name = _('Web Based')
-
-    # The default protocol
-    protocol = 'http'
-
-    # The default secure protocol
-    secure_protocol = 'https'
-
-    # The number of bytes in memory to read from the remote source at a time
-    chunk_size = 8192
-
-    # Web based requests are remote/external to our current location
-    location = ContentLocation.HOSTED
-
-    def __init__(self, headers=None, **kwargs):
-        """
-        Initialize HTTP Object
-
-        headers can be a dictionary of key/value pairs that you want to
-        additionally include as part of the server headers to post with
-
-        """
-        super().__init__(**kwargs)
-
-        self.schema = 'https' if self.secure else 'http'
-
-        self.fullpath = kwargs.get('fullpath')
-        if not isinstance(self.fullpath, str):
-            self.fullpath = '/'
-
-        self.headers = {}
-        if headers:
-            # Store our extra headers
-            self.headers.update(headers)
-
-        # Where our content is written to upon a call to download.
-        self._temp_file = None
-
-        # Our Query String Dictionary; we use this to track arguments
-        # specified that aren't otherwise part of this class
-        self.qsd = {k: v for k, v in kwargs.get('qsd', {}).items()
-                    if k not in self.template_args}
-
-        return
-
-    def download(self, **kwargs):
-        """
-        Perform retrieval of the configuration based on the specified request
-        """
-
-        if self.location == ContentLocation.INACCESSIBLE:
-            # our content is inaccessible
-            return False
-
-        # Ensure any existing content set has been invalidated
-        self.invalidate()
-
-        # prepare header
-        headers = {
-            'User-Agent': self.app_id,
-        }
-
-        # Apply any/all header over-rides defined
-        headers.update(self.headers)
-
-        auth = None
-        if self.user:
-            auth = (self.user, self.password)
-
-        url = '%s://%s' % (self.schema, self.host)
-        if isinstance(self.port, int):
-            url += ':%d' % self.port
-
-        url += self.fullpath
-
-        self.logger.debug('HTTP POST URL: %s (cert_verify=%r)' % (
-            url, self.verify_certificate,
-        ))
-
-        # Where our request object will temporarily live.
-        r = None
-
-        # Always call throttle before any remote server i/o is made
-        self.throttle()
-
-        try:
-            # Make our request
-            with requests.get(
-                    url,
-                    headers=headers,
-                    auth=auth,
-                    params=self.qsd,
-                    verify=self.verify_certificate,
-                    timeout=self.request_timeout,
-                    stream=True) as r:
-
-                # Handle Errors
-                r.raise_for_status()
-
-                # Get our file-size (if known)
-                try:
-                    file_size = int(r.headers.get('Content-Length', '0'))
-                except (TypeError, ValueError):
-                    # Handle edge case where Content-Length is a bad value
-                    file_size = 0
-
-                # Perform a little Q/A on file limitations and restrictions
-                if self.max_file_size > 0 and file_size > self.max_file_size:
-
-                    # The content retrieved is to large
-                    self.logger.error(
-                        'HTTP response exceeds allowable maximum file length '
-                        '({}KB): {}'.format(
-                            int(self.max_file_size / 1024),
-                            self.url(privacy=True)))
-
-                    # Return False (signifying a failure)
-                    return False
-
-                # Detect config format based on mime if the format isn't
-                # already enforced
-                self.detected_mimetype = r.headers.get('Content-Type')
-
-                d = r.headers.get('Content-Disposition', '')
-                result = re.search(
-                    "filename=['\"]?(?P[^'\"]+)['\"]?", d, re.I)
-                if result:
-                    self.detected_name = result.group('name').strip()
-
-                # Create a temporary file to work with
-                self._temp_file = NamedTemporaryFile()
-
-                # Get our chunk size
-                chunk_size = self.chunk_size
-
-                # Track all bytes written to disk
-                bytes_written = 0
-
-                # If we get here, we can now safely write our content to disk
-                for chunk in r.iter_content(chunk_size=chunk_size):
-                    # filter out keep-alive chunks
-                    if chunk:
-                        self._temp_file.write(chunk)
-                        bytes_written = self._temp_file.tell()
-
-                        # Prevent a case where Content-Length isn't provided
-                        # we don't want to fetch beyond our limits
-                        if self.max_file_size > 0:
-                            if bytes_written > self.max_file_size:
-                                # The content retrieved is to large
-                                self.logger.error(
-                                    'HTTP response exceeds allowable maximum '
-                                    'file length ({}KB): {}'.format(
-                                        int(self.max_file_size / 1024),
-                                        self.url(privacy=True)))
-
-                                # Invalidate any variables previously set
-                                self.invalidate()
-
-                                # Return False (signifying a failure)
-                                return False
-
-                            elif bytes_written + chunk_size \
-                                    > self.max_file_size:
-                                # Adjust out next read to accomodate up to our
-                                # limit +1. This will prevent us from readig
-                                # to much into our memory buffer
-                                self.max_file_size - bytes_written + 1
-
-                # Ensure our content is flushed to disk for post-processing
-                self._temp_file.flush()
-
-            # Set our minimum requirements for a successful download() call
-            self.download_path = self._temp_file.name
-            if not self.detected_name:
-                self.detected_name = os.path.basename(self.fullpath)
-
-        except requests.RequestException as e:
-            self.logger.error(
-                'A Connection error occurred retrieving HTTP '
-                'configuration from %s.' % self.host)
-            self.logger.debug('Socket Exception: %s' % str(e))
-
-            # Invalidate any variables previously set
-            self.invalidate()
-
-            # Return False (signifying a failure)
-            return False
-
-        except (IOError, OSError):
-            # IOError is present for backwards compatibility with Python
-            # versions older then 3.3.  >= 3.3 throw OSError now.
-
-            # Could not open and/or write the temporary file
-            self.logger.error(
-                'Could not write attachment to disk: {}'.format(
-                    self.url(privacy=True)))
-
-            # Invalidate any variables previously set
-            self.invalidate()
-
-            # Return False (signifying a failure)
-            return False
-
-        # Return our success
-        return True
-
-    def invalidate(self):
-        """
-        Close our temporary file
-        """
-        if self._temp_file:
-            self._temp_file.close()
-            self._temp_file = None
-
-        super().invalidate()
-
-    def url(self, privacy=False, *args, **kwargs):
-        """
-        Returns the URL built dynamically based on specified arguments.
-        """
-
-        # Our URL parameters
-        params = self.url_parameters(privacy=privacy, *args, **kwargs)
-
-        # Prepare our cache value
-        if self.cache is not None:
-            if isinstance(self.cache, bool) or not self.cache:
-                cache = 'yes' if self.cache else 'no'
-            else:
-                cache = int(self.cache)
-
-            # Set our cache value
-            params['cache'] = cache
-
-        if self._mimetype:
-            # A format was enforced
-            params['mime'] = self._mimetype
-
-        if self._name:
-            # A name was enforced
-            params['name'] = self._name
-
-        # Append our headers into our parameters
-        params.update({'+{}'.format(k): v for k, v in self.headers.items()})
-
-        # Apply any remaining entries to our URL
-        params.update(self.qsd)
-
-        # Determine Authentication
-        auth = ''
-        if self.user and self.password:
-            auth = '{user}:{password}@'.format(
-                user=self.quote(self.user, safe=''),
-                password=self.pprint(
-                    self.password, privacy, mode=PrivacyMode.Secret, safe=''),
-            )
-        elif self.user:
-            auth = '{user}@'.format(
-                user=self.quote(self.user, safe=''),
-            )
-
-        default_port = 443 if self.secure else 80
-
-        return '{schema}://{auth}{hostname}{port}{fullpath}?{params}'.format(
-            schema=self.secure_protocol if self.secure else self.protocol,
-            auth=auth,
-            hostname=self.quote(self.host, safe=''),
-            port='' if self.port is None or self.port == default_port
-                 else ':{}'.format(self.port),
-            fullpath=self.quote(self.fullpath, safe='/'),
-            params=self.urlencode(params),
-        )
-
-    @staticmethod
-    def parse_url(url):
-        """
-        Parses the URL and returns enough arguments that can allow
-        us to re-instantiate this object.
-
-        """
-        results = AttachBase.parse_url(url)
-
-        if not results:
-            # We're done early as we couldn't load the results
-            return results
-
-        # Add our headers that the user can potentially over-ride if they wish
-        # to to our returned result set
-        results['headers'] = results['qsd-']
-        results['headers'].update(results['qsd+'])
-
-        return results
diff --git a/lib/apprise/attachment/__init__.py b/lib/apprise/attachment/__init__.py
index ba7620a4..c2aef1ee 100644
--- a/lib/apprise/attachment/__init__.py
+++ b/lib/apprise/attachment/__init__.py
@@ -2,7 +2,7 @@
 # BSD 2-Clause License
 #
 # Apprise - Push Notification Library.
-# Copyright (c) 2023, Chris Caron 
+# Copyright (c) 2024, Chris Caron 
 #
 # Redistribution and use in source and binary forms, with or without
 # modification, are permitted provided that the following conditions are met:
@@ -26,93 +26,15 @@
 # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 # POSSIBILITY OF SUCH DAMAGE.
 
-import re
-from os import listdir
-from os.path import dirname
-from os.path import abspath
-from ..common import ATTACHMENT_SCHEMA_MAP
+# Used for testing
+from .base import AttachBase
+from ..manager_attachment import AttachmentManager
 
-__all__ = []
+# Initalize our Attachment Manager Singleton
+A_MGR = AttachmentManager()
 
-
-# Load our Lookup Matrix
-def __load_matrix(path=abspath(dirname(__file__)), name='apprise.attachment'):
-    """
-    Dynamically load our schema map; this allows us to gracefully
-    skip over modules we simply don't have the dependencies for.
-
-    """
-    # Used for the detection of additional Attachment Services objects
-    # The .py extension is optional as we support loading directories too
-    module_re = re.compile(r'^(?PAttach[a-z0-9]+)(\.py)?$', re.I)
-
-    for f in listdir(path):
-        match = module_re.match(f)
-        if not match:
-            # keep going
-            continue
-
-        # Store our notification/plugin name:
-        plugin_name = match.group('name')
-        try:
-            module = __import__(
-                '{}.{}'.format(name, plugin_name),
-                globals(), locals(),
-                fromlist=[plugin_name])
-
-        except ImportError:
-            # No problem, we can't use this object
-            continue
-
-        if not hasattr(module, plugin_name):
-            # Not a library we can load as it doesn't follow the simple rule
-            # that the class must bear the same name as the notification
-            # file itself.
-            continue
-
-        # Get our plugin
-        plugin = getattr(module, plugin_name)
-        if not hasattr(plugin, 'app_id'):
-            # Filter out non-notification modules
-            continue
-
-        elif plugin_name in __all__:
-            # we're already handling this object
-            continue
-
-        # Add our module name to our __all__
-        __all__.append(plugin_name)
-
-        # Ensure we provide the class as the reference to this directory and
-        # not the module:
-        globals()[plugin_name] = plugin
-
-        # Load protocol(s) if defined
-        proto = getattr(plugin, 'protocol', None)
-        if isinstance(proto, str):
-            if proto not in ATTACHMENT_SCHEMA_MAP:
-                ATTACHMENT_SCHEMA_MAP[proto] = plugin
-
-        elif isinstance(proto, (set, list, tuple)):
-            # Support iterables list types
-            for p in proto:
-                if p not in ATTACHMENT_SCHEMA_MAP:
-                    ATTACHMENT_SCHEMA_MAP[p] = plugin
-
-        # Load secure protocol(s) if defined
-        protos = getattr(plugin, 'secure_protocol', None)
-        if isinstance(protos, str):
-            if protos not in ATTACHMENT_SCHEMA_MAP:
-                ATTACHMENT_SCHEMA_MAP[protos] = plugin
-
-        if isinstance(protos, (set, list, tuple)):
-            # Support iterables list types
-            for p in protos:
-                if p not in ATTACHMENT_SCHEMA_MAP:
-                    ATTACHMENT_SCHEMA_MAP[p] = plugin
-
-    return ATTACHMENT_SCHEMA_MAP
-
-
-# Dynamically build our schema base
-__load_matrix()
+__all__ = [
+    # Reference
+    'AttachBase',
+    'AttachmentManager',
+]
diff --git a/lib/apprise/attachment/AttachBase.py b/lib/apprise/attachment/base.py
similarity index 90%
rename from lib/apprise/attachment/AttachBase.py
rename to lib/apprise/attachment/base.py
index c1cadbf9..6ae9d3aa 100644
--- a/lib/apprise/attachment/AttachBase.py
+++ b/lib/apprise/attachment/base.py
@@ -2,7 +2,7 @@
 # BSD 2-Clause License
 #
 # Apprise - Push Notification Library.
-# Copyright (c) 2023, Chris Caron 
+# Copyright (c) 2024, Chris Caron 
 #
 # Redistribution and use in source and binary forms, with or without
 # modification, are permitted provided that the following conditions are met:
@@ -29,10 +29,10 @@
 import os
 import time
 import mimetypes
-from ..URLBase import URLBase
+from ..url import URLBase
 from ..utils import parse_bool
 from ..common import ContentLocation
-from ..AppriseLocale import gettext_lazy as _
+from ..locale import gettext_lazy as _
 
 
 class AttachBase(URLBase):
@@ -148,6 +148,9 @@ class AttachBase(URLBase):
         # Absolute path to attachment
         self.download_path = None
 
+        # Track open file pointers
+        self.__pointers = set()
+
         # Set our cache flag; it can be True, False, None, or a (positive)
         # integer... nothing else
         if cache is not None:
@@ -226,15 +229,14 @@ class AttachBase(URLBase):
         Content is cached once determied to prevent overhead of future
         calls.
         """
+        if not self.exists():
+            # we could not obtain our attachment
+            return None
 
         if self._mimetype:
             # return our pre-calculated cached content
             return self._mimetype
 
-        if not self.exists():
-            # we could not obtain our attachment
-            return None
-
         if not self.detected_mimetype:
             # guess_type() returns: (type, encoding) and sets type to None
             # if it can't otherwise determine it.
@@ -253,11 +255,14 @@ class AttachBase(URLBase):
         return self.detected_mimetype \
             if self.detected_mimetype else self.unknown_mimetype
 
-    def exists(self):
+    def exists(self, retrieve_if_missing=True):
         """
         Simply returns true if the object has downloaded and stored the
         attachment AND the attachment has not expired.
         """
+        if self.location == ContentLocation.INACCESSIBLE:
+            # our content is inaccessible
+            return False
 
         cache = self.template_args['cache']['default'] \
             if self.cache is None else self.cache
@@ -282,7 +287,7 @@ class AttachBase(URLBase):
                 # The file is not present
                 pass
 
-        return self.download()
+        return False if not retrieve_if_missing else self.download()
 
     def invalidate(self):
         """
@@ -295,6 +300,11 @@ class AttachBase(URLBase):
           - download_path: Must contain a absolute path to content
           - detected_mimetype: Should identify mimetype of content
         """
+
+        # Remove all open pointers
+        while self.__pointers:
+            self.__pointers.pop().close()
+
         self.detected_name = None
         self.download_path = None
         self.detected_mimetype = None
@@ -314,8 +324,28 @@ class AttachBase(URLBase):
         raise NotImplementedError(
             "download() is implimented by the child class.")
 
+    def open(self, mode='rb'):
+        """
+        return our file pointer and track it (we'll auto close later
+        """
+        pointer = open(self.path, mode=mode)
+        self.__pointers.add(pointer)
+        return pointer
+
+    def __enter__(self):
+        """
+        support with keyword
+        """
+        return self.open()
+
+    def __exit__(self, value_type, value, traceback):
+        """
+        stub to do nothing; but support exit of with statement gracefully
+        """
+        return
+
     @staticmethod
-    def parse_url(url, verify_host=True, mimetype_db=None):
+    def parse_url(url, verify_host=True, mimetype_db=None, sanitize=True):
         """Parses the URL and returns it broken apart into a dictionary.
 
         This is very specific and customized for Apprise.
@@ -333,7 +363,8 @@ class AttachBase(URLBase):
             successful, otherwise None is returned.
         """
 
-        results = URLBase.parse_url(url, verify_host=verify_host)
+        results = URLBase.parse_url(
+            url, verify_host=verify_host, sanitize=sanitize)
 
         if not results:
             # We're done; we failed to parse our url
@@ -375,3 +406,9 @@ class AttachBase(URLBase):
         True is returned if our content was downloaded correctly.
         """
         return True if self.path else False
+
+    def __del__(self):
+        """
+        Perform any house cleaning
+        """
+        self.invalidate()
diff --git a/lib/apprise/attachment/AttachBase.pyi b/lib/apprise/attachment/base.pyi
similarity index 100%
rename from lib/apprise/attachment/AttachBase.pyi
rename to lib/apprise/attachment/base.pyi
diff --git a/lib/apprise/attachment/AttachFile.py b/lib/apprise/attachment/file.py
similarity index 95%
rename from lib/apprise/attachment/AttachFile.py
rename to lib/apprise/attachment/file.py
index d3085555..88d8f6e1 100644
--- a/lib/apprise/attachment/AttachFile.py
+++ b/lib/apprise/attachment/file.py
@@ -2,7 +2,7 @@
 # BSD 2-Clause License
 #
 # Apprise - Push Notification Library.
-# Copyright (c) 2023, Chris Caron 
+# Copyright (c) 2024, Chris Caron 
 #
 # Redistribution and use in source and binary forms, with or without
 # modification, are permitted provided that the following conditions are met:
@@ -28,9 +28,9 @@
 
 import re
 import os
-from .AttachBase import AttachBase
+from .base import AttachBase
 from ..common import ContentLocation
-from ..AppriseLocale import gettext_lazy as _
+from ..locale import gettext_lazy as _
 
 
 class AttachFile(AttachBase):
@@ -78,7 +78,8 @@ class AttachFile(AttachBase):
 
         return 'file://{path}{params}'.format(
             path=self.quote(self.dirty_path),
-            params='?{}'.format(self.urlencode(params)) if params else '',
+            params='?{}'.format(self.urlencode(params, safe='/'))
+            if params else '',
         )
 
     def download(self, **kwargs):
diff --git a/lib/apprise/attachment/http.py b/lib/apprise/attachment/http.py
new file mode 100644
index 00000000..870f7cc2
--- /dev/null
+++ b/lib/apprise/attachment/http.py
@@ -0,0 +1,375 @@
+# -*- coding: utf-8 -*-
+# BSD 2-Clause License
+#
+# Apprise - Push Notification Library.
+# Copyright (c) 2024, Chris Caron 
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met:
+#
+# 1. Redistributions of source code must retain the above copyright notice,
+#    this list of conditions and the following disclaimer.
+#
+# 2. Redistributions in binary form must reproduce the above copyright notice,
+#    this list of conditions and the following disclaimer in the documentation
+#    and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+
+import re
+import os
+import requests
+import threading
+from tempfile import NamedTemporaryFile
+from .base import AttachBase
+from ..common import ContentLocation
+from ..url import PrivacyMode
+from ..locale import gettext_lazy as _
+
+
+class AttachHTTP(AttachBase):
+    """
+    A wrapper for HTTP based attachment sources
+    """
+
+    # The default descriptive name associated with the service
+    service_name = _('Web Based')
+
+    # The default protocol
+    protocol = 'http'
+
+    # The default secure protocol
+    secure_protocol = 'https'
+
+    # The number of bytes in memory to read from the remote source at a time
+    chunk_size = 8192
+
+    # Web based requests are remote/external to our current location
+    location = ContentLocation.HOSTED
+
+    # thread safe loading
+    _lock = threading.Lock()
+
+    def __init__(self, headers=None, **kwargs):
+        """
+        Initialize HTTP Object
+
+        headers can be a dictionary of key/value pairs that you want to
+        additionally include as part of the server headers to post with
+
+        """
+        super().__init__(**kwargs)
+
+        self.schema = 'https' if self.secure else 'http'
+
+        self.fullpath = kwargs.get('fullpath')
+        if not isinstance(self.fullpath, str):
+            self.fullpath = '/'
+
+        self.headers = {}
+        if headers:
+            # Store our extra headers
+            self.headers.update(headers)
+
+        # Where our content is written to upon a call to download.
+        self._temp_file = None
+
+        # Our Query String Dictionary; we use this to track arguments
+        # specified that aren't otherwise part of this class
+        self.qsd = {k: v for k, v in kwargs.get('qsd', {}).items()
+                    if k not in self.template_args}
+
+        return
+
+    def download(self, **kwargs):
+        """
+        Perform retrieval of the configuration based on the specified request
+        """
+
+        if self.location == ContentLocation.INACCESSIBLE:
+            # our content is inaccessible
+            return False
+
+        # prepare header
+        headers = {
+            'User-Agent': self.app_id,
+        }
+
+        # Apply any/all header over-rides defined
+        headers.update(self.headers)
+
+        auth = None
+        if self.user:
+            auth = (self.user, self.password)
+
+        url = '%s://%s' % (self.schema, self.host)
+        if isinstance(self.port, int):
+            url += ':%d' % self.port
+
+        url += self.fullpath
+
+        # Where our request object will temporarily live.
+        r = None
+
+        # Always call throttle before any remote server i/o is made
+        self.throttle()
+
+        with self._lock:
+            if self.exists(retrieve_if_missing=False):
+                # Due to locking; it's possible a concurrent thread already
+                # handled the retrieval in which case we can safely move on
+                self.logger.trace(
+                    'HTTP Attachment %s already retrieved',
+                    self._temp_file.name)
+                return True
+
+            # Ensure any existing content set has been invalidated
+            self.invalidate()
+
+            self.logger.debug(
+                'HTTP Attachment Fetch URL: %s (cert_verify=%r)' % (
+                    url, self.verify_certificate))
+
+            try:
+                # Make our request
+                with requests.get(
+                        url,
+                        headers=headers,
+                        auth=auth,
+                        params=self.qsd,
+                        verify=self.verify_certificate,
+                        timeout=self.request_timeout,
+                        stream=True) as r:
+
+                    # Handle Errors
+                    r.raise_for_status()
+
+                    # Get our file-size (if known)
+                    try:
+                        file_size = int(r.headers.get('Content-Length', '0'))
+                    except (TypeError, ValueError):
+                        # Handle edge case where Content-Length is a bad value
+                        file_size = 0
+
+                    # Perform a little Q/A on file limitations and restrictions
+                    if self.max_file_size > 0 and \
+                            file_size > self.max_file_size:
+
+                        # The content retrieved is to large
+                        self.logger.error(
+                            'HTTP response exceeds allowable maximum file '
+                            'length ({}KB): {}'.format(
+                                int(self.max_file_size / 1024),
+                                self.url(privacy=True)))
+
+                        # Return False (signifying a failure)
+                        return False
+
+                    # Detect config format based on mime if the format isn't
+                    # already enforced
+                    self.detected_mimetype = r.headers.get('Content-Type')
+
+                    d = r.headers.get('Content-Disposition', '')
+                    result = re.search(
+                        "filename=['\"]?(?P[^'\"]+)['\"]?", d, re.I)
+                    if result:
+                        self.detected_name = result.group('name').strip()
+
+                    # Create a temporary file to work with; delete must be set
+                    # to False or it isn't compatible with Microsoft Windows
+                    # instances. In lieu of this, __del__ will clean up the
+                    # file for us.
+                    self._temp_file = NamedTemporaryFile(delete=False)
+
+                    # Get our chunk size
+                    chunk_size = self.chunk_size
+
+                    # Track all bytes written to disk
+                    bytes_written = 0
+
+                    # If we get here, we can now safely write our content to
+                    # disk
+                    for chunk in r.iter_content(chunk_size=chunk_size):
+                        # filter out keep-alive chunks
+                        if chunk:
+                            self._temp_file.write(chunk)
+                            bytes_written = self._temp_file.tell()
+
+                            # Prevent a case where Content-Length isn't
+                            # provided. In this case we don't want to fetch
+                            # beyond our limits
+                            if self.max_file_size > 0:
+                                if bytes_written > self.max_file_size:
+                                    # The content retrieved is to large
+                                    self.logger.error(
+                                        'HTTP response exceeds allowable '
+                                        'maximum file length '
+                                        '({}KB): {}'.format(
+                                            int(self.max_file_size / 1024),
+                                            self.url(privacy=True)))
+
+                                    # Invalidate any variables previously set
+                                    self.invalidate()
+
+                                    # Return False (signifying a failure)
+                                    return False
+
+                                elif bytes_written + chunk_size \
+                                        > self.max_file_size:
+                                    # Adjust out next read to accomodate up to
+                                    # our limit +1. This will prevent us from
+                                    # reading to much into our memory buffer
+                                    self.max_file_size - bytes_written + 1
+
+                    # Ensure our content is flushed to disk for post-processing
+                    self._temp_file.flush()
+
+                    # Set our minimum requirements for a successful download()
+                    # call
+                    self.download_path = self._temp_file.name
+                    if not self.detected_name:
+                        self.detected_name = os.path.basename(self.fullpath)
+
+            except requests.RequestException as e:
+                self.logger.error(
+                    'A Connection error occurred retrieving HTTP '
+                    'configuration from %s.' % self.host)
+                self.logger.debug('Socket Exception: %s' % str(e))
+
+                # Invalidate any variables previously set
+                self.invalidate()
+
+                # Return False (signifying a failure)
+                return False
+
+            except (IOError, OSError):
+                # IOError is present for backwards compatibility with Python
+                # versions older then 3.3.  >= 3.3 throw OSError now.
+
+                # Could not open and/or write the temporary file
+                self.logger.error(
+                    'Could not write attachment to disk: {}'.format(
+                        self.url(privacy=True)))
+
+                # Invalidate any variables previously set
+                self.invalidate()
+
+                # Return False (signifying a failure)
+                return False
+
+        # Return our success
+        return True
+
+    def invalidate(self):
+        """
+        Close our temporary file
+        """
+        if self._temp_file:
+            self.logger.trace(
+                'Attachment cleanup of %s', self._temp_file.name)
+            self._temp_file.close()
+
+            try:
+                # Ensure our file is removed (if it exists)
+                os.unlink(self._temp_file.name)
+
+            except OSError:
+                pass
+
+            # Reset our temporary file to prevent from entering
+            # this block again
+            self._temp_file = None
+
+        super().invalidate()
+
+    def __del__(self):
+        """
+        Tidy memory if open
+        """
+        self.invalidate()
+
+    def url(self, privacy=False, *args, **kwargs):
+        """
+        Returns the URL built dynamically based on specified arguments.
+        """
+
+        # Our URL parameters
+        params = self.url_parameters(privacy=privacy, *args, **kwargs)
+
+        # Prepare our cache value
+        if self.cache is not None:
+            if isinstance(self.cache, bool) or not self.cache:
+                cache = 'yes' if self.cache else 'no'
+            else:
+                cache = int(self.cache)
+
+            # Set our cache value
+            params['cache'] = cache
+
+        if self._mimetype:
+            # A format was enforced
+            params['mime'] = self._mimetype
+
+        if self._name:
+            # A name was enforced
+            params['name'] = self._name
+
+        # Append our headers into our parameters
+        params.update({'+{}'.format(k): v for k, v in self.headers.items()})
+
+        # Apply any remaining entries to our URL
+        params.update(self.qsd)
+
+        # Determine Authentication
+        auth = ''
+        if self.user and self.password:
+            auth = '{user}:{password}@'.format(
+                user=self.quote(self.user, safe=''),
+                password=self.pprint(
+                    self.password, privacy, mode=PrivacyMode.Secret, safe=''),
+            )
+        elif self.user:
+            auth = '{user}@'.format(
+                user=self.quote(self.user, safe=''),
+            )
+
+        default_port = 443 if self.secure else 80
+
+        return '{schema}://{auth}{hostname}{port}{fullpath}?{params}'.format(
+            schema=self.secure_protocol if self.secure else self.protocol,
+            auth=auth,
+            hostname=self.quote(self.host, safe=''),
+            port='' if self.port is None or self.port == default_port
+                 else ':{}'.format(self.port),
+            fullpath=self.quote(self.fullpath, safe='/'),
+            params=self.urlencode(params, safe='/'),
+        )
+
+    @staticmethod
+    def parse_url(url):
+        """
+        Parses the URL and returns enough arguments that can allow
+        us to re-instantiate this object.
+
+        """
+        results = AttachBase.parse_url(url, sanitize=False)
+        if not results:
+            # We're done early as we couldn't load the results
+            return results
+
+        # Add our headers that the user can potentially over-ride if they wish
+        # to to our returned result set
+        results['headers'] = results['qsd-']
+        results['headers'].update(results['qsd+'])
+
+        return results
diff --git a/lib/apprise/attachment/memory.py b/lib/apprise/attachment/memory.py
new file mode 100644
index 00000000..94645f26
--- /dev/null
+++ b/lib/apprise/attachment/memory.py
@@ -0,0 +1,212 @@
+# -*- coding: utf-8 -*-
+# BSD 2-Clause License
+#
+# Apprise - Push Notification Library.
+# Copyright (c) 2024, Chris Caron 
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met:
+#
+# 1. Redistributions of source code must retain the above copyright notice,
+#    this list of conditions and the following disclaimer.
+#
+# 2. Redistributions in binary form must reproduce the above copyright notice,
+#    this list of conditions and the following disclaimer in the documentation
+#    and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+
+import re
+import os
+import io
+from .base import AttachBase
+from ..common import ContentLocation
+from ..locale import gettext_lazy as _
+import uuid
+
+
+class AttachMemory(AttachBase):
+    """
+    A wrapper for Memory based attachment sources
+    """
+
+    # The default descriptive name associated with the service
+    service_name = _('Memory')
+
+    # The default protocol
+    protocol = 'memory'
+
+    # Content is local to the same location as the apprise instance
+    # being called (server-side)
+    location = ContentLocation.LOCAL
+
+    def __init__(self, content=None, name=None, mimetype=None,
+                 encoding='utf-8', **kwargs):
+        """
+        Initialize Memory Based Attachment Object
+
+        """
+        # Create our BytesIO object
+        self._data = io.BytesIO()
+
+        if content is None:
+            # Empty; do nothing
+            pass
+
+        elif isinstance(content, str):
+            content = content.encode(encoding)
+            if mimetype is None:
+                mimetype = 'text/plain'
+
+            if not name:
+                # Generate a unique filename
+                name = str(uuid.uuid4()) + '.txt'
+
+        elif not isinstance(content, bytes):
+            raise TypeError(
+                'Provided content for memory attachment is invalid')
+
+        # Store our content
+        if content:
+            self._data.write(content)
+
+        if mimetype is None:
+            # Default mimetype
+            mimetype = 'application/octet-stream'
+
+        if not name:
+            # Generate a unique filename
+            name = str(uuid.uuid4()) + '.dat'
+
+        # Initialize our base object
+        super().__init__(name=name, mimetype=mimetype, **kwargs)
+
+        return
+
+    def url(self, privacy=False, *args, **kwargs):
+        """
+        Returns the URL built dynamically based on specified arguments.
+        """
+
+        # Define any URL parameters
+        params = {
+            'mime': self._mimetype,
+        }
+
+        return 'memory://{name}?{params}'.format(
+            name=self.quote(self._name),
+            params=self.urlencode(params, safe='/')
+        )
+
+    def open(self, *args, **kwargs):
+        """
+        return our memory object
+        """
+        # Return our object
+        self._data.seek(0, 0)
+        return self._data
+
+    def __enter__(self):
+        """
+        support with clause
+        """
+        # Return our object
+        self._data.seek(0, 0)
+        return self._data
+
+    def download(self, **kwargs):
+        """
+        Handle memory download() call
+        """
+
+        if self.location == ContentLocation.INACCESSIBLE:
+            # our content is inaccessible
+            return False
+
+        if self.max_file_size > 0 and len(self) > self.max_file_size:
+            # The content to attach is to large
+            self.logger.error(
+                'Content exceeds allowable maximum memory size '
+                '({}KB): {}'.format(
+                    int(self.max_file_size / 1024), self.url(privacy=True)))
+
+            # Return False (signifying a failure)
+            return False
+
+        return True
+
+    def invalidate(self):
+        """
+        Removes data
+        """
+        self._data.truncate(0)
+        return
+
+    def exists(self):
+        """
+        over-ride exists() call
+        """
+        size = len(self)
+        return True if self.location != ContentLocation.INACCESSIBLE \
+            and size > 0 and (
+                self.max_file_size <= 0 or
+                (self.max_file_size > 0 and size <= self.max_file_size)) \
+            else False
+
+    @staticmethod
+    def parse_url(url):
+        """
+        Parses the URL so that we can handle all different file paths
+        and return it as our path object
+
+        """
+
+        results = AttachBase.parse_url(url, verify_host=False)
+        if not results:
+            # We're done early; it's not a good URL
+            return results
+
+        if 'name' not in results:
+            # Allow fall-back to be from URL
+            match = re.match(r'memory://(?P[^?]+)(\?.*)?', url, re.I)
+            if match:
+                # Store our filename only (ignore any defined paths)
+                results['name'] = \
+                    os.path.basename(AttachMemory.unquote(match.group('path')))
+        return results
+
+    @property
+    def path(self):
+        """
+        return the filename
+        """
+        if not self.exists():
+            # we could not obtain our path
+            return None
+
+        return self._name
+
+    def __len__(self):
+        """
+        Returns the size of he memory attachment
+
+        """
+        return self._data.getbuffer().nbytes
+
+    def __bool__(self):
+        """
+        Allows the Apprise object to be wrapped in an based 'if statement'.
+        True is returned if our content was downloaded correctly.
+        """
+
+        return self.exists()
diff --git a/lib/apprise/common.py b/lib/apprise/common.py
index 5e3a3567..b90a8537 100644
--- a/lib/apprise/common.py
+++ b/lib/apprise/common.py
@@ -2,7 +2,7 @@
 # BSD 2-Clause License
 #
 # Apprise - Push Notification Library.
-# Copyright (c) 2023, Chris Caron 
+# Copyright (c) 2024, Chris Caron 
 #
 # Redistribution and use in source and binary forms, with or without
 # modification, are permitted provided that the following conditions are met:
@@ -26,50 +26,6 @@
 # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 # POSSIBILITY OF SUCH DAMAGE.
 
-# we mirror our base purely for the ability to reset everything; this
-# is generally only used in testing and should not be used by developers
-# It is also used as a means of preventing a module from being reloaded
-# in the event it already exists
-NOTIFY_MODULE_MAP = {}
-
-# Maintains a mapping of all of the Notification services
-NOTIFY_SCHEMA_MAP = {}
-
-# This contains a mapping of all plugins dynamicaly loaded at runtime from
-# external modules such as the @notify decorator
-#
-# The elements here will be additionally added to the NOTIFY_SCHEMA_MAP if
-# there is no conflict otherwise.
-# The structure looks like the following:
-# Module path, e.g. /usr/share/apprise/plugins/my_notify_hook.py
-# {
-#   'path': path,
-#
-#   'notify': {
-#     'schema': {
-#       'name': 'Custom schema name',
-#       'fn_name': 'name_of_function_decorator_was_found_on',
-#       'url': 'schema://any/additional/info/found/on/url'
-#       'plugin': 
-#    },
-#     'schema2': {
-#       'name': 'Custom schema name',
-#       'fn_name': 'name_of_function_decorator_was_found_on',
-#       'url': 'schema://any/additional/info/found/on/url'
-#       'plugin': 
-#    }
-#  }
-#
-# Note: that the  inherits from
-#       NotifyBase
-NOTIFY_CUSTOM_MODULE_MAP = {}
-
-# Maintains a mapping of all configuration schema's supported
-CONFIG_SCHEMA_MAP = {}
-
-# Maintains a mapping of all attachment schema's supported
-ATTACHMENT_SCHEMA_MAP = {}
-
 
 class NotifyType:
     """
diff --git a/lib/apprise/config/__init__.py b/lib/apprise/config/__init__.py
index 4b7e3fd7..24957e88 100644
--- a/lib/apprise/config/__init__.py
+++ b/lib/apprise/config/__init__.py
@@ -2,7 +2,7 @@
 # BSD 2-Clause License
 #
 # Apprise - Push Notification Library.
-# Copyright (c) 2023, Chris Caron 
+# Copyright (c) 2024, Chris Caron 
 #
 # Redistribution and use in source and binary forms, with or without
 # modification, are permitted provided that the following conditions are met:
@@ -26,84 +26,15 @@
 # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 # POSSIBILITY OF SUCH DAMAGE.
 
-import re
-from os import listdir
-from os.path import dirname
-from os.path import abspath
-from ..logger import logger
-from ..common import CONFIG_SCHEMA_MAP
+# Used for testing
+from .base import ConfigBase
+from ..manager_config import ConfigurationManager
 
-__all__ = []
+# Initalize our Config Manager Singleton
+C_MGR = ConfigurationManager()
 
-
-# Load our Lookup Matrix
-def __load_matrix(path=abspath(dirname(__file__)), name='apprise.config'):
-    """
-    Dynamically load our schema map; this allows us to gracefully
-    skip over modules we simply don't have the dependencies for.
-
-    """
-    # Used for the detection of additional Configuration Services objects
-    # The .py extension is optional as we support loading directories too
-    module_re = re.compile(r'^(?PConfig[a-z0-9]+)(\.py)?$', re.I)
-
-    for f in listdir(path):
-        match = module_re.match(f)
-        if not match:
-            # keep going
-            continue
-
-        # Store our notification/plugin name:
-        plugin_name = match.group('name')
-        try:
-            module = __import__(
-                '{}.{}'.format(name, plugin_name),
-                globals(), locals(),
-                fromlist=[plugin_name])
-
-        except ImportError:
-            # No problem, we can't use this object
-            continue
-
-        if not hasattr(module, plugin_name):
-            # Not a library we can load as it doesn't follow the simple rule
-            # that the class must bear the same name as the notification
-            # file itself.
-            continue
-
-        # Get our plugin
-        plugin = getattr(module, plugin_name)
-        if not hasattr(plugin, 'app_id'):
-            # Filter out non-notification modules
-            continue
-
-        elif plugin_name in __all__:
-            # we're already handling this object
-            continue
-
-        # Add our module name to our __all__
-        __all__.append(plugin_name)
-
-        # Ensure we provide the class as the reference to this directory and
-        # not the module:
-        globals()[plugin_name] = plugin
-
-        fn = getattr(plugin, 'schemas', None)
-        schemas = set([]) if not callable(fn) else fn(plugin)
-
-        # map our schema to our plugin
-        for schema in schemas:
-            if schema in CONFIG_SCHEMA_MAP:
-                logger.error(
-                    "Config schema ({}) mismatch detected - {} to {}"
-                    .format(schema, CONFIG_SCHEMA_MAP[schema], plugin))
-                continue
-
-            # Assign plugin
-            CONFIG_SCHEMA_MAP[schema] = plugin
-
-    return CONFIG_SCHEMA_MAP
-
-
-# Dynamically build our schema base
-__load_matrix()
+__all__ = [
+    # Reference
+    'ConfigBase',
+    'ConfigurationManager',
+]
diff --git a/lib/apprise/config/ConfigBase.py b/lib/apprise/config/base.py
similarity index 96%
rename from lib/apprise/config/ConfigBase.py
rename to lib/apprise/config/base.py
index 0da7a8be..01a9dbff 100644
--- a/lib/apprise/config/ConfigBase.py
+++ b/lib/apprise/config/base.py
@@ -2,7 +2,7 @@
 # BSD 2-Clause License
 #
 # Apprise - Push Notification Library.
-# Copyright (c) 2023, Chris Caron 
+# Copyright (c) 2024, Chris Caron 
 #
 # Redistribution and use in source and binary forms, with or without
 # modification, are permitted provided that the following conditions are met:
@@ -32,18 +32,26 @@ import time
 
 from .. import plugins
 from .. import common
-from ..AppriseAsset import AppriseAsset
-from ..URLBase import URLBase
+from ..asset import AppriseAsset
+from ..url import URLBase
 from ..utils import GET_SCHEMA_RE
 from ..utils import parse_list
 from ..utils import parse_bool
 from ..utils import parse_urls
 from ..utils import cwe312_url
+from ..manager_config import ConfigurationManager
+from ..manager_plugins import NotificationManager
 
 # Test whether token is valid or not
 VALID_TOKEN = re.compile(
     r'(?P[a-z0-9][a-z0-9_]+)', re.I)
 
+# Grant access to our Notification Manager Singleton
+N_MGR = NotificationManager()
+
+# Grant access to our Configuration Manager Singleton
+C_MGR = ConfigurationManager()
+
 
 class ConfigBase(URLBase):
     """
@@ -228,7 +236,7 @@ class ConfigBase(URLBase):
                     schema = schema.group('schema').lower()
 
                     # Some basic validation
-                    if schema not in common.CONFIG_SCHEMA_MAP:
+                    if schema not in C_MGR:
                         ConfigBase.logger.warning(
                             'Unsupported include schema {}.'.format(schema))
                         continue
@@ -239,7 +247,7 @@ class ConfigBase(URLBase):
 
                 # Parse our url details of the server object as dictionary
                 # containing all of the information parsed from our URL
-                results = common.CONFIG_SCHEMA_MAP[schema].parse_url(url)
+                results = C_MGR[schema].parse_url(url)
                 if not results:
                     # Failed to parse the server URL
                     self.logger.warning(
@@ -247,11 +255,10 @@ class ConfigBase(URLBase):
                     continue
 
                 # Handle cross inclusion based on allow_cross_includes rules
-                if (common.CONFIG_SCHEMA_MAP[schema].allow_cross_includes ==
+                if (C_MGR[schema].allow_cross_includes ==
                         common.ContentIncludeMode.STRICT
                         and schema not in self.schemas()
-                        and not self.insecure_includes) or \
-                        common.CONFIG_SCHEMA_MAP[schema] \
+                        and not self.insecure_includes) or C_MGR[schema] \
                         .allow_cross_includes == \
                         common.ContentIncludeMode.NEVER:
 
@@ -279,8 +286,7 @@ class ConfigBase(URLBase):
                 try:
                     # Attempt to create an instance of our plugin using the
                     # parsed URL information
-                    cfg_plugin = \
-                        common.CONFIG_SCHEMA_MAP[results['schema']](**results)
+                    cfg_plugin = C_MGR[results['schema']](**results)
 
                 except Exception as e:
                     # the arguments are invalid or can not be used.
@@ -392,7 +398,11 @@ class ConfigBase(URLBase):
                 # Track our groups
                 groups.add(tag)
 
-                # Store what we know is worth keping
+                # Store what we know is worth keeping
+                if tag not in group_tags:  # pragma: no cover
+                    # handle cases where the tag doesn't exist
+                    group_tags[tag] = set()
+
                 results |= group_tags[tag] - tag_groups
 
                 # Get simple tag assignments
@@ -753,8 +763,7 @@ class ConfigBase(URLBase):
             try:
                 # Attempt to create an instance of our plugin using the
                 # parsed URL information
-                plugin = common.NOTIFY_SCHEMA_MAP[
-                    results['schema']](**results)
+                plugin = N_MGR[results['schema']](**results)
 
                 # Create log entry of loaded URL
                 ConfigBase.logger.debug(
@@ -807,8 +816,7 @@ class ConfigBase(URLBase):
         # Create a copy of our dictionary
         tokens = tokens.copy()
 
-        for kw, meta in common.NOTIFY_SCHEMA_MAP[schema]\
-                .template_kwargs.items():
+        for kw, meta in N_MGR[schema].template_kwargs.items():
 
             # Determine our prefix:
             prefix = meta.get('prefix', '+')
@@ -851,8 +859,7 @@ class ConfigBase(URLBase):
         #
         # This function here allows these mappings to take place within the
         # YAML file as independant arguments.
-        class_templates = \
-            plugins.details(common.NOTIFY_SCHEMA_MAP[schema])
+        class_templates = plugins.details(N_MGR[schema])
 
         for key in list(tokens.keys()):
 
diff --git a/lib/apprise/config/ConfigBase.pyi b/lib/apprise/config/base.pyi
similarity index 100%
rename from lib/apprise/config/ConfigBase.pyi
rename to lib/apprise/config/base.pyi
diff --git a/lib/apprise/config/ConfigFile.py b/lib/apprise/config/file.py
similarity index 97%
rename from lib/apprise/config/ConfigFile.py
rename to lib/apprise/config/file.py
index a0b9bf69..52ff8eba 100644
--- a/lib/apprise/config/ConfigFile.py
+++ b/lib/apprise/config/file.py
@@ -2,7 +2,7 @@
 # BSD 2-Clause License
 #
 # Apprise - Push Notification Library.
-# Copyright (c) 2023, Chris Caron 
+# Copyright (c) 2024, Chris Caron 
 #
 # Redistribution and use in source and binary forms, with or without
 # modification, are permitted provided that the following conditions are met:
@@ -28,10 +28,10 @@
 
 import re
 import os
-from .ConfigBase import ConfigBase
+from .base import ConfigBase
 from ..common import ConfigFormat
 from ..common import ContentIncludeMode
-from ..AppriseLocale import gettext_lazy as _
+from ..locale import gettext_lazy as _
 
 
 class ConfigFile(ConfigBase):
diff --git a/lib/apprise/config/ConfigHTTP.py b/lib/apprise/config/http.py
similarity index 98%
rename from lib/apprise/config/ConfigHTTP.py
rename to lib/apprise/config/http.py
index 82cb1f63..5b9e7375 100644
--- a/lib/apprise/config/ConfigHTTP.py
+++ b/lib/apprise/config/http.py
@@ -2,7 +2,7 @@
 # BSD 2-Clause License
 #
 # Apprise - Push Notification Library.
-# Copyright (c) 2023, Chris Caron 
+# Copyright (c) 2024, Chris Caron 
 #
 # Redistribution and use in source and binary forms, with or without
 # modification, are permitted provided that the following conditions are met:
@@ -28,11 +28,11 @@
 
 import re
 import requests
-from .ConfigBase import ConfigBase
+from .base import ConfigBase
 from ..common import ConfigFormat
 from ..common import ContentIncludeMode
-from ..URLBase import PrivacyMode
-from ..AppriseLocale import gettext_lazy as _
+from ..url import PrivacyMode
+from ..locale import gettext_lazy as _
 
 # Support TEXT formats
 # text/plain
diff --git a/lib/apprise/config/ConfigMemory.py b/lib/apprise/config/memory.py
similarity index 95%
rename from lib/apprise/config/ConfigMemory.py
rename to lib/apprise/config/memory.py
index 110e04a3..181d7623 100644
--- a/lib/apprise/config/ConfigMemory.py
+++ b/lib/apprise/config/memory.py
@@ -2,7 +2,7 @@
 # BSD 2-Clause License
 #
 # Apprise - Push Notification Library.
-# Copyright (c) 2023, Chris Caron 
+# Copyright (c) 2024, Chris Caron 
 #
 # Redistribution and use in source and binary forms, with or without
 # modification, are permitted provided that the following conditions are met:
@@ -26,8 +26,8 @@
 # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 # POSSIBILITY OF SUCH DAMAGE.
 
-from .ConfigBase import ConfigBase
-from ..AppriseLocale import gettext_lazy as _
+from .base import ConfigBase
+from ..locale import gettext_lazy as _
 
 
 class ConfigMemory(ConfigBase):
diff --git a/lib/apprise/conversion.py b/lib/apprise/conversion.py
index ffa3e3a0..0943d382 100644
--- a/lib/apprise/conversion.py
+++ b/lib/apprise/conversion.py
@@ -2,7 +2,7 @@
 # BSD 2-Clause License
 #
 # Apprise - Push Notification Library.
-# Copyright (c) 2023, Chris Caron 
+# Copyright (c) 2024, Chris Caron 
 #
 # Redistribution and use in source and binary forms, with or without
 # modification, are permitted provided that the following conditions are met:
@@ -29,7 +29,7 @@
 import re
 #from markdown import markdown
 from .common import NotifyFormat
-from .URLBase import URLBase
+from .url import URLBase
 
 from html.parser import HTMLParser
 
@@ -58,8 +58,8 @@ def convert_between(from_format, to_format, content):
 #    """
 #    Converts specified content from markdown to HTML.
 #    """
-
-#    return markdown(content)
+#    return markdown(content, extensions=[
+#        'markdown.extensions.nl2br', 'markdown.extensions.tables'])
 
 
 def text_to_html(content):
diff --git a/lib/apprise/decorators/__init__.py b/lib/apprise/decorators/__init__.py
index 5b089bbf..db9a15a0 100644
--- a/lib/apprise/decorators/__init__.py
+++ b/lib/apprise/decorators/__init__.py
@@ -2,7 +2,7 @@
 # BSD 2-Clause License
 #
 # Apprise - Push Notification Library.
-# Copyright (c) 2023, Chris Caron 
+# Copyright (c) 2024, Chris Caron 
 #
 # Redistribution and use in source and binary forms, with or without
 # modification, are permitted provided that the following conditions are met:
diff --git a/lib/apprise/decorators/CustomNotifyPlugin.py b/lib/apprise/decorators/base.py
similarity index 76%
rename from lib/apprise/decorators/CustomNotifyPlugin.py
rename to lib/apprise/decorators/base.py
index 5ccfded5..2661db0a 100644
--- a/lib/apprise/decorators/CustomNotifyPlugin.py
+++ b/lib/apprise/decorators/base.py
@@ -2,7 +2,7 @@
 # BSD 2-Clause License
 #
 # Apprise - Push Notification Library.
-# Copyright (c) 2023, Chris Caron 
+# Copyright (c) 2024, Chris Caron 
 #
 # Redistribution and use in source and binary forms, with or without
 # modification, are permitted provided that the following conditions are met:
@@ -27,7 +27,8 @@
 # POSSIBILITY OF SUCH DAMAGE.USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 # POSSIBILITY OF SUCH DAMAGE.
 
-from ..plugins.NotifyBase import NotifyBase
+from ..plugins.base import NotifyBase
+from ..manager_plugins import NotificationManager
 from ..utils import URL_DETAILS_RE
 from ..utils import parse_url
 from ..utils import url_assembly
@@ -36,6 +37,9 @@ from .. import common
 from ..logger import logger
 import inspect
 
+# Grant access to our Notification Manager Singleton
+N_MGR = NotificationManager()
+
 
 class CustomNotifyPlugin(NotifyBase):
     """
@@ -51,6 +55,9 @@ class CustomNotifyPlugin(NotifyBase):
     # should be treated differently.
     category = 'custom'
 
+    # Support Attachments
+    attachment_support = True
+
     # Define object templates
     templates = (
         '{schema}://',
@@ -91,17 +98,17 @@ class CustomNotifyPlugin(NotifyBase):
             logger.warning(msg)
             return None
 
-        # Acquire our plugin name
-        plugin_name = re_match.group('schema').lower()
+        # Acquire our schema
+        schema = re_match.group('schema').lower()
 
         if not re_match.group('base'):
-            url = '{}://'.format(plugin_name)
+            url = '{}://'.format(schema)
 
         # Keep a default set of arguments to apply to all called references
         base_args = parse_url(
-            url, default_schema=plugin_name, verify_host=False, simple=True)
+            url, default_schema=schema, verify_host=False, simple=True)
 
-        if plugin_name in common.NOTIFY_SCHEMA_MAP:
+        if schema in N_MGR:
             # we're already handling this object
             msg = 'The schema ({}) is already defined and could not be ' \
                 'loaded from custom notify function {}.' \
@@ -117,10 +124,10 @@ class CustomNotifyPlugin(NotifyBase):
 
             # Our Service Name
             service_name = name if isinstance(name, str) \
-                and name else 'Custom - {}'.format(plugin_name)
+                and name else 'Custom - {}'.format(schema)
 
             # Store our matched schema
-            secure_protocol = plugin_name
+            secure_protocol = schema
 
             requirements = {
                 # Define our required packaging in order to work
@@ -143,6 +150,10 @@ class CustomNotifyPlugin(NotifyBase):
 
                 self._default_args = {}
 
+                # Some variables do not need to be set
+                if 'secure' in kwargs:
+                    del kwargs['secure']
+
                 # Apply our updates based on what was parsed
                 dict_full_update(self._default_args, self._base_args)
                 dict_full_update(self._default_args, kwargs)
@@ -181,51 +192,26 @@ class CustomNotifyPlugin(NotifyBase):
                     # Unhandled Exception
                     self.logger.warning(
                         'An exception occured sending a %s notification.',
-                        common.
-                        NOTIFY_SCHEMA_MAP[self.secure_protocol].service_name)
+                        N_MGR[self.secure_protocol].service_name)
                     self.logger.debug(
                         '%s Exception: %s',
-                        common.NOTIFY_SCHEMA_MAP[self.secure_protocol], str(e))
+                        N_MGR[self.secure_protocol], str(e))
                     return False
 
                 if response:
                     self.logger.info(
                         'Sent %s notification.',
-                        common.
-                        NOTIFY_SCHEMA_MAP[self.secure_protocol].service_name)
+                        N_MGR[self.secure_protocol].service_name)
                 else:
                     self.logger.warning(
                         'Failed to send %s notification.',
-                        common.
-                        NOTIFY_SCHEMA_MAP[self.secure_protocol].service_name)
+                        N_MGR[self.secure_protocol].service_name)
                 return response
 
         # Store our plugin into our core map file
-        common.NOTIFY_SCHEMA_MAP[plugin_name] = CustomNotifyPluginWrapper
-
-        # Update our custom plugin map
-        module_pyname = str(send_func.__module__)
-        if module_pyname not in common.NOTIFY_CUSTOM_MODULE_MAP:
-            # Support non-dynamic includes as well...
-            common.NOTIFY_CUSTOM_MODULE_MAP[module_pyname] = {
-                'path': inspect.getfile(send_func),
-
-                # Initialize our template
-                'notify': {},
-            }
-
-        common.\
-            NOTIFY_CUSTOM_MODULE_MAP[module_pyname]['notify'][plugin_name] = {
-                # Our Serivice Description (for API and CLI --details view)
-                'name': CustomNotifyPluginWrapper.service_name,
-                # The name of the send function the @notify decorator wrapped
-                'fn_name': send_func.__name__,
-                # The URL that was provided in the @notify decorator call
-                # associated with the 'on='
-                'url': url,
-                # The Initialized Plugin that was generated based on the above
-                # parameters
-                'plugin': CustomNotifyPluginWrapper}
-
-        # return our plugin
-        return common.NOTIFY_SCHEMA_MAP[plugin_name]
+        return N_MGR.add(
+            plugin=CustomNotifyPluginWrapper,
+            schemas=schema,
+            send_func=send_func,
+            url=url,
+        )
diff --git a/lib/apprise/decorators/notify.py b/lib/apprise/decorators/notify.py
index 07b4ceb1..892c3adf 100644
--- a/lib/apprise/decorators/notify.py
+++ b/lib/apprise/decorators/notify.py
@@ -2,7 +2,7 @@
 # BSD 2-Clause License
 #
 # Apprise - Push Notification Library.
-# Copyright (c) 2023, Chris Caron 
+# Copyright (c) 2024, Chris Caron 
 #
 # Redistribution and use in source and binary forms, with or without
 # modification, are permitted provided that the following conditions are met:
@@ -26,7 +26,7 @@
 # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 # POSSIBILITY OF SUCH DAMAGE.
 
-from .CustomNotifyPlugin import CustomNotifyPlugin
+from .base import CustomNotifyPlugin
 
 
 def notify(on, name=None):
diff --git a/lib/apprise/emojis.py b/lib/apprise/emojis.py
new file mode 100644
index 00000000..d8a82481
--- /dev/null
+++ b/lib/apprise/emojis.py
@@ -0,0 +1,2273 @@
+# -*- coding: utf-8 -*-
+# BSD 2-Clause License
+#
+# Apprise - Push Notification Library.
+# Copyright (c) 2024, Chris Caron 
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met:
+#
+# 1. Redistributions of source code must retain the above copyright notice,
+#    this list of conditions and the following disclaimer.
+#
+# 2. Redistributions in binary form must reproduce the above copyright notice,
+#    this list of conditions and the following disclaimer in the documentation
+#    and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+
+import re
+import time
+from .logger import logger
+
+# All Emoji's are wrapped in this character
+DELIM = ':'
+
+# the map simply contains the emoji that should be mapped to the regular
+# expression it should be swapped on.
+# This list was based on: https://github.com/ikatyang/emoji-cheat-sheet
+EMOJI_MAP = {
+    #
+    # Face Smiling
+    #
+    DELIM + r'grinning' + DELIM: '😄',
+    DELIM + r'smile' + DELIM: '😄',
+    DELIM + r'(laughing|satisfied)' + DELIM: '😆',
+    DELIM + r'rofl' + DELIM: '🤣',
+    DELIM + r'slightly_smiling_face' + DELIM: '🙂',
+    DELIM + r'wink' + DELIM: '😉',
+    DELIM + r'innocent' + DELIM: '😇',
+    DELIM + r'smiley' + DELIM: '😃',
+    DELIM + r'grin' + DELIM: '😃',
+    DELIM + r'sweat_smile' + DELIM: '😅',
+    DELIM + r'joy' + DELIM: '😂',
+    DELIM + r'upside_down_face' + DELIM: '🙃',
+    DELIM + r'blush' + DELIM: '😊',
+
+    #
+    # Face Affection
+    #
+    DELIM + r'smiling_face_with_three_hearts' + DELIM: '🥰',
+    DELIM + r'star_struck' + DELIM: '🤩',
+    DELIM + r'kissing' + DELIM: '😗',
+    DELIM + r'kissing_closed_eyes' + DELIM: '😚',
+    DELIM + r'smiling_face_with_tear' + DELIM: '🥲',
+    DELIM + r'heart_eyes' + DELIM: '😍',
+    DELIM + r'kissing_heart' + DELIM: '😘',
+    DELIM + r'relaxed' + DELIM: '☺️',
+    DELIM + r'kissing_smiling_eyes' + DELIM: '😙',
+
+    #
+    # Face Tongue
+    #
+    DELIM + r'yum' + DELIM: '😋',
+    DELIM + r'stuck_out_tongue_winking_eye' + DELIM: '😜',
+    DELIM + r'stuck_out_tongue_closed_eyes' + DELIM: '😝',
+    DELIM + r'stuck_out_tongue' + DELIM: '😛',
+    DELIM + r'zany_face' + DELIM: '🤪',
+    DELIM + r'money_mouth_face' + DELIM: '🤑',
+
+    #
+    # Face Hand
+    #
+    DELIM + r'hugs' + DELIM: '🤗',
+    DELIM + r'shushing_face' + DELIM: '🤫',
+    DELIM + r'hand_over_mouth' + DELIM: '🤭',
+    DELIM + r'thinking' + DELIM: '🤔',
+
+    #
+    # Face Neutral Skeptical
+    #
+    DELIM + r'zipper_mouth_face' + DELIM: '🤐',
+    DELIM + r'neutral_face' + DELIM: '😐',
+    DELIM + r'no_mouth' + DELIM: '😶',
+    DELIM + r'smirk' + DELIM: '😏',
+    DELIM + r'roll_eyes' + DELIM: '🙄',
+    DELIM + r'face_exhaling' + DELIM: '😮‍💨',
+    DELIM + r'raised_eyebrow' + DELIM: '🤨',
+    DELIM + r'expressionless' + DELIM: '😑',
+    DELIM + r'face_in_clouds' + DELIM: '😶‍🌫️',
+    DELIM + r'unamused' + DELIM: '😒',
+    DELIM + r'grimacing' + DELIM: '😬',
+    DELIM + r'lying_face' + DELIM: '🤥',
+
+    #
+    # Face Sleepy
+    #
+    DELIM + r'relieved' + DELIM: '😌',
+    DELIM + r'sleepy' + DELIM: '😪',
+    DELIM + r'sleeping' + DELIM: '😴',
+    DELIM + r'pensive' + DELIM: '😔',
+    DELIM + r'drooling_face' + DELIM: '🤤',
+
+    #
+    # Face Unwell
+    #
+    DELIM + r'mask' + DELIM: '😷',
+    DELIM + r'face_with_head_bandage' + DELIM: '🤕',
+    DELIM + r'vomiting_face' + DELIM: '🤮',
+    DELIM + r'hot_face' + DELIM: '🥵',
+    DELIM + r'woozy_face' + DELIM: '🥴',
+    DELIM + r'face_with_spiral_eyes' + DELIM: '😵‍💫',
+    DELIM + r'face_with_thermometer' + DELIM: '🤒',
+    DELIM + r'nauseated_face' + DELIM: '🤢',
+    DELIM + r'sneezing_face' + DELIM: '🤧',
+    DELIM + r'cold_face' + DELIM: '🥶',
+    DELIM + r'dizzy_face' + DELIM: '😵',
+    DELIM + r'exploding_head' + DELIM: '🤯',
+
+    #
+    # Face Hat
+    #
+    DELIM + r'cowboy_hat_face' + DELIM: '🤠',
+    DELIM + r'disguised_face' + DELIM: '🥸',
+    DELIM + r'partying_face' + DELIM: '🥳',
+
+    #
+    # Face Glasses
+    #
+    DELIM + r'sunglasses' + DELIM: '😎',
+    DELIM + r'monocle_face' + DELIM: '🧐',
+    DELIM + r'nerd_face' + DELIM: '🤓',
+
+    #
+    # Face Concerned
+    #
+    DELIM + r'confused' + DELIM: '😕',
+    DELIM + r'slightly_frowning_face' + DELIM: '🙁',
+    DELIM + r'open_mouth' + DELIM: '😮',
+    DELIM + r'astonished' + DELIM: '😲',
+    DELIM + r'pleading_face' + DELIM: '🥺',
+    DELIM + r'anguished' + DELIM: '😧',
+    DELIM + r'cold_sweat' + DELIM: '😰',
+    DELIM + r'cry' + DELIM: '😢',
+    DELIM + r'scream' + DELIM: '😱',
+    DELIM + r'persevere' + DELIM: '😣',
+    DELIM + r'sweat' + DELIM: '😓',
+    DELIM + r'tired_face' + DELIM: '😫',
+    DELIM + r'worried' + DELIM: '😟',
+    DELIM + r'frowning_face' + DELIM: '☹️',
+    DELIM + r'hushed' + DELIM: '😯',
+    DELIM + r'flushed' + DELIM: '😳',
+    DELIM + r'frowning' + DELIM: '😦',
+    DELIM + r'fearful' + DELIM: '😨',
+    DELIM + r'disappointed_relieved' + DELIM: '😥',
+    DELIM + r'sob' + DELIM: '😭',
+    DELIM + r'confounded' + DELIM: '😖',
+    DELIM + r'disappointed' + DELIM: '😞',
+    DELIM + r'weary' + DELIM: '😩',
+    DELIM + r'yawning_face' + DELIM: '🥱',
+
+    #
+    # Face Negative
+    #
+    DELIM + r'triumph' + DELIM: '😤',
+    DELIM + r'angry' + DELIM: '😠',
+    DELIM + r'smiling_imp' + DELIM: '😈',
+    DELIM + r'skull' + DELIM: '💀',
+    DELIM + r'(pout|rage)' + DELIM: '😡',
+    DELIM + r'cursing_face' + DELIM: '🤬',
+    DELIM + r'imp' + DELIM: '👿',
+    DELIM + r'skull_and_crossbones' + DELIM: '☠️',
+
+    #
+    # Face Costume
+    #
+    DELIM + r'(hankey|poop|shit)' + DELIM: '💩',
+    DELIM + r'japanese_ogre' + DELIM: '👹',
+    DELIM + r'ghost' + DELIM: '👻',
+    DELIM + r'space_invader' + DELIM: '👾',
+    DELIM + r'clown_face' + DELIM: '🤡',
+    DELIM + r'japanese_goblin' + DELIM: '👺',
+    DELIM + r'alien' + DELIM: '👽',
+    DELIM + r'robot' + DELIM: '🤖',
+
+    #
+    # Cat Face
+    #
+    DELIM + r'smiley_cat' + DELIM: '😺',
+    DELIM + r'joy_cat' + DELIM: '😹',
+    DELIM + r'smirk_cat' + DELIM: '😼',
+    DELIM + r'scream_cat' + DELIM: '🙀',
+    DELIM + r'pouting_cat' + DELIM: '😾',
+    DELIM + r'smile_cat' + DELIM: '😸',
+    DELIM + r'heart_eyes_cat' + DELIM: '😻',
+    DELIM + r'kissing_cat' + DELIM: '😽',
+    DELIM + r'crying_cat_face' + DELIM: '😿',
+
+    #
+    # Monkey Face
+    #
+    DELIM + r'see_no_evil' + DELIM: '🙈',
+    DELIM + r'speak_no_evil' + DELIM: '🙊',
+    DELIM + r'hear_no_evil' + DELIM: '🙉',
+
+    #
+    # Heart
+    #
+    DELIM + r'love_letter' + DELIM: '💌',
+    DELIM + r'gift_heart' + DELIM: '💝',
+    DELIM + r'heartpulse' + DELIM: '💗',
+    DELIM + r'revolving_hearts' + DELIM: '💞',
+    DELIM + r'heart_decoration' + DELIM: '💟',
+    DELIM + r'broken_heart' + DELIM: '💔',
+    DELIM + r'mending_heart' + DELIM: '❤️‍🩹',
+    DELIM + r'orange_heart' + DELIM: '🧡',
+    DELIM + r'green_heart' + DELIM: '💚',
+    DELIM + r'purple_heart' + DELIM: '💜',
+    DELIM + r'black_heart' + DELIM: '🖤',
+    DELIM + r'cupid' + DELIM: '💘',
+    DELIM + r'sparkling_heart' + DELIM: '💖',
+    DELIM + r'heartbeat' + DELIM: '💓',
+    DELIM + r'two_hearts' + DELIM: '💕',
+    DELIM + r'heavy_heart_exclamation' + DELIM: '❣️',
+    DELIM + r'heart_on_fire' + DELIM: '❤️‍🔥',
+    DELIM + r'heart' + DELIM: '❤️',
+    DELIM + r'yellow_heart' + DELIM: '💛',
+    DELIM + r'blue_heart' + DELIM: '💙',
+    DELIM + r'brown_heart' + DELIM: '🤎',
+    DELIM + r'white_heart' + DELIM: '🤍',
+
+    #
+    # Emotion
+    #
+    DELIM + r'kiss' + DELIM: '💋',
+    DELIM + r'anger' + DELIM: '💢',
+    DELIM + r'dizzy' + DELIM: '💫',
+    DELIM + r'dash' + DELIM: '💨',
+    DELIM + r'speech_balloon' + DELIM: '💬',
+    DELIM + r'left_speech_bubble' + DELIM: '🗨️',
+    DELIM + r'thought_balloon' + DELIM: '💭',
+    DELIM + r'100' + DELIM: '💯',
+    DELIM + r'(boom|collision)' + DELIM: '💥',
+    DELIM + r'sweat_drops' + DELIM: '💦',
+    DELIM + r'hole' + DELIM: '🕳️',
+    DELIM + r'eye_speech_bubble' + DELIM: '👁️‍🗨️',
+    DELIM + r'right_anger_bubble' + DELIM: '🗯️',
+    DELIM + r'zzz' + DELIM: '💤',
+
+    #
+    # Hand Fingers Open
+    #
+    DELIM + r'wave' + DELIM: '👋',
+    DELIM + r'raised_hand_with_fingers_splayed' + DELIM: '🖐️',
+    DELIM + r'vulcan_salute' + DELIM: '🖖',
+    DELIM + r'raised_back_of_hand' + DELIM: '🤚',
+    DELIM + r'(raised_)?hand' + DELIM: '✋',
+
+    #
+    # Hand Fingers Partial
+    #
+    DELIM + r'ok_hand' + DELIM: '👌',
+    DELIM + r'pinched_fingers' + DELIM: '🤌',
+    DELIM + r'pinching_hand' + DELIM: '🤏',
+    DELIM + r'v' + DELIM: '✌️',
+    DELIM + r'crossed_fingers' + DELIM: '🤞',
+    DELIM + r'love_you_gesture' + DELIM: '🤟',
+    DELIM + r'metal' + DELIM: '🤘',
+    DELIM + r'call_me_hand' + DELIM: '🤙',
+
+    #
+    # Hand Single Finger
+    #
+    DELIM + r'point_left' + DELIM: '👈',
+    DELIM + r'point_right' + DELIM: '👉',
+    DELIM + r'point_up_2' + DELIM: '👆',
+    DELIM + r'(fu|middle_finger)' + DELIM: '🖕',
+    DELIM + r'point_down' + DELIM: '👇',
+    DELIM + r'point_up' + DELIM: '☝️',
+
+    #
+    # Hand Fingers Closed
+    #
+    DELIM + r'(\+1|thumbsup)' + DELIM: '👍',
+    DELIM + r'(-1|thumbsdown)' + DELIM: '👎',
+    DELIM + r'fist' + DELIM: '✊',
+    DELIM + r'(fist_(raised|oncoming)|(face)?punch)' + DELIM: '👊',
+    DELIM + r'fist_left' + DELIM: '🤛',
+    DELIM + r'fist_right' + DELIM: '🤜',
+
+    #
+    # Hands
+    #
+    DELIM + r'clap' + DELIM: '👏',
+    DELIM + r'raised_hands' + DELIM: '🙌',
+    DELIM + r'open_hands' + DELIM: '👐',
+    DELIM + r'palms_up_together' + DELIM: '🤲',
+    DELIM + r'handshake' + DELIM: '🤝',
+    DELIM + r'pray' + DELIM: '🙏',
+
+    #
+    # Hand Prop
+    #
+    DELIM + r'writing_hand' + DELIM: '✍️',
+    DELIM + r'nail_care' + DELIM: '💅',
+    DELIM + r'selfie' + DELIM: '🤳',
+
+    #
+    # Body Parts
+    #
+    DELIM + r'muscle' + DELIM: '💪',
+    DELIM + r'mechanical_arm' + DELIM: '🦾',
+    DELIM + r'mechanical_leg' + DELIM: '🦿',
+    DELIM + r'leg' + DELIM: '🦵',
+    DELIM + r'foot' + DELIM: '🦶',
+    DELIM + r'ear' + DELIM: '👂',
+    DELIM + r'ear_with_hearing_aid' + DELIM: '🦻',
+    DELIM + r'nose' + DELIM: '👃',
+    DELIM + r'brain' + DELIM: '🧠',
+    DELIM + r'anatomical_heart' + DELIM: '🫀',
+    DELIM + r'lungs' + DELIM: '🫁',
+    DELIM + r'tooth' + DELIM: '🦷',
+    DELIM + r'bone' + DELIM: '🦴',
+    DELIM + r'eyes' + DELIM: '👀',
+    DELIM + r'eye' + DELIM: '👁️',
+    DELIM + r'tongue' + DELIM: '👅',
+    DELIM + r'lips' + DELIM: '👄',
+
+    #
+    # Person
+    #
+    DELIM + r'baby' + DELIM: '👶',
+    DELIM + r'child' + DELIM: '🧒',
+    DELIM + r'boy' + DELIM: '👦',
+    DELIM + r'girl' + DELIM: '👧',
+    DELIM + r'adult' + DELIM: '🧑',
+    DELIM + r'blond_haired_person' + DELIM: '👱',
+    DELIM + r'man' + DELIM: '👨',
+    DELIM + r'bearded_person' + DELIM: '🧔',
+    DELIM + r'man_beard' + DELIM: '🧔‍♂️',
+    DELIM + r'woman_beard' + DELIM: '🧔‍♀️',
+    DELIM + r'red_haired_man' + DELIM: '👨‍🦰',
+    DELIM + r'curly_haired_man' + DELIM: '👨‍🦱',
+    DELIM + r'white_haired_man' + DELIM: '👨‍🦳',
+    DELIM + r'bald_man' + DELIM: '👨‍🦲',
+    DELIM + r'woman' + DELIM: '👩',
+    DELIM + r'red_haired_woman' + DELIM: '👩‍🦰',
+    DELIM + r'person_red_hair' + DELIM: '🧑‍🦰',
+    DELIM + r'curly_haired_woman' + DELIM: '👩‍🦱',
+    DELIM + r'person_curly_hair' + DELIM: '🧑‍🦱',
+    DELIM + r'white_haired_woman' + DELIM: '👩‍🦳',
+    DELIM + r'person_white_hair' + DELIM: '🧑‍🦳',
+    DELIM + r'bald_woman' + DELIM: '👩‍🦲',
+    DELIM + r'person_bald' + DELIM: '🧑‍🦲',
+    DELIM + r'blond_(haired_)?woman' + DELIM: '👱‍♀️',
+    DELIM + r'blond_haired_man' + DELIM: '👱‍♂️',
+    DELIM + r'older_adult' + DELIM: '🧓',
+    DELIM + r'older_man' + DELIM: '👴',
+    DELIM + r'older_woman' + DELIM: '👵',
+
+    #
+    # Person Gesture
+    #
+    DELIM + r'frowning_person' + DELIM: '🙍',
+    DELIM + r'frowning_man' + DELIM: '🙍‍♂️',
+    DELIM + r'frowning_woman' + DELIM: '🙍‍♀️',
+    DELIM + r'pouting_face' + DELIM: '🙎',
+    DELIM + r'pouting_man' + DELIM: '🙎‍♂️',
+    DELIM + r'pouting_woman' + DELIM: '🙎‍♀️',
+    DELIM + r'no_good' + DELIM: '🙅',
+    DELIM + r'(ng|no_good)_man' + DELIM: '🙅‍♂️',
+    DELIM + r'(ng_woman|no_good_woman)' + DELIM: '🙅‍♀️',
+    DELIM + r'ok_person' + DELIM: '🙆',
+    DELIM + r'ok_man' + DELIM: '🙆‍♂️',
+    DELIM + r'ok_woman' + DELIM: '🙆‍♀️',
+    DELIM + r'(information_desk|tipping_hand_)person' + DELIM: '💁',
+    DELIM + r'(sassy_man|tipping_hand_man)' + DELIM: '💁‍♂️',
+    DELIM + r'(sassy_woman|tipping_hand_woman)' + DELIM: '💁‍♀️',
+    DELIM + r'raising_hand' + DELIM: '🙋',
+    DELIM + r'raising_hand_man' + DELIM: '🙋‍♂️',
+    DELIM + r'raising_hand_woman' + DELIM: '🙋‍♀️',
+    DELIM + r'deaf_person' + DELIM: '🧏',
+    DELIM + r'deaf_man' + DELIM: '🧏‍♂️',
+    DELIM + r'deaf_woman' + DELIM: '🧏‍♀️',
+    DELIM + r'bow' + DELIM: '🙇',
+    DELIM + r'bowing_man' + DELIM: '🙇‍♂️',
+    DELIM + r'bowing_woman' + DELIM: '🙇‍♀️',
+    DELIM + r'facepalm' + DELIM: '🤦',
+    DELIM + r'man_facepalming' + DELIM: '🤦‍♂️',
+    DELIM + r'woman_facepalming' + DELIM: '🤦‍♀️',
+    DELIM + r'shrug' + DELIM: '🤷',
+    DELIM + r'man_shrugging' + DELIM: '🤷‍♂️',
+    DELIM + r'woman_shrugging' + DELIM: '🤷‍♀️',
+
+    #
+    # Person Role
+    #
+    DELIM + r'health_worker' + DELIM: '🧑‍⚕️',
+    DELIM + r'man_health_worker' + DELIM: '👨‍⚕️',
+    DELIM + r'woman_health_worker' + DELIM: '👩‍⚕️',
+    DELIM + r'student' + DELIM: '🧑‍🎓',
+    DELIM + r'man_student' + DELIM: '👨‍🎓',
+    DELIM + r'woman_student' + DELIM: '👩‍🎓',
+    DELIM + r'teacher' + DELIM: '🧑‍🏫',
+    DELIM + r'man_teacher' + DELIM: '👨‍🏫',
+    DELIM + r'woman_teacher' + DELIM: '👩‍🏫',
+    DELIM + r'judge' + DELIM: '🧑‍⚖️',
+    DELIM + r'man_judge' + DELIM: '👨‍⚖️',
+    DELIM + r'woman_judge' + DELIM: '👩‍⚖️',
+    DELIM + r'farmer' + DELIM: '🧑‍🌾',
+    DELIM + r'man_farmer' + DELIM: '👨‍🌾',
+    DELIM + r'woman_farmer' + DELIM: '👩‍🌾',
+    DELIM + r'cook' + DELIM: '🧑‍🍳',
+    DELIM + r'man_cook' + DELIM: '👨‍🍳',
+    DELIM + r'woman_cook' + DELIM: '👩‍🍳',
+    DELIM + r'mechanic' + DELIM: '🧑‍🔧',
+    DELIM + r'man_mechanic' + DELIM: '👨‍🔧',
+    DELIM + r'woman_mechanic' + DELIM: '👩‍🔧',
+    DELIM + r'factory_worker' + DELIM: '🧑‍🏭',
+    DELIM + r'man_factory_worker' + DELIM: '👨‍🏭',
+    DELIM + r'woman_factory_worker' + DELIM: '👩‍🏭',
+    DELIM + r'office_worker' + DELIM: '🧑‍💼',
+    DELIM + r'man_office_worker' + DELIM: '👨‍💼',
+    DELIM + r'woman_office_worker' + DELIM: '👩‍💼',
+    DELIM + r'scientist' + DELIM: '🧑‍🔬',
+    DELIM + r'man_scientist' + DELIM: '👨‍🔬',
+    DELIM + r'woman_scientist' + DELIM: '👩‍🔬',
+    DELIM + r'technologist' + DELIM: '🧑‍💻',
+    DELIM + r'man_technologist' + DELIM: '👨‍💻',
+    DELIM + r'woman_technologist' + DELIM: '👩‍💻',
+    DELIM + r'singer' + DELIM: '🧑‍🎤',
+    DELIM + r'man_singer' + DELIM: '👨‍🎤',
+    DELIM + r'woman_singer' + DELIM: '👩‍🎤',
+    DELIM + r'artist' + DELIM: '🧑‍🎨',
+    DELIM + r'man_artist' + DELIM: '👨‍🎨',
+    DELIM + r'woman_artist' + DELIM: '👩‍🎨',
+    DELIM + r'pilot' + DELIM: '🧑‍✈️',
+    DELIM + r'man_pilot' + DELIM: '👨‍✈️',
+    DELIM + r'woman_pilot' + DELIM: '👩‍✈️',
+    DELIM + r'astronaut' + DELIM: '🧑‍🚀',
+    DELIM + r'man_astronaut' + DELIM: '👨‍🚀',
+    DELIM + r'woman_astronaut' + DELIM: '👩‍🚀',
+    DELIM + r'firefighter' + DELIM: '🧑‍🚒',
+    DELIM + r'man_firefighter' + DELIM: '👨‍🚒',
+    DELIM + r'woman_firefighter' + DELIM: '👩‍🚒',
+    DELIM + r'cop' + DELIM: '👮',
+    DELIM + r'police(_officer|man)' + DELIM: '👮‍♂️',
+    DELIM + r'policewoman' + DELIM: '👮‍♀️',
+    DELIM + r'detective' + DELIM: '🕵️',
+    DELIM + r'male_detective' + DELIM: '🕵️‍♂️',
+    DELIM + r'female_detective' + DELIM: '🕵️‍♀️',
+    DELIM + r'guard' + DELIM: '💂',
+    DELIM + r'guardsman' + DELIM: '💂‍♂️',
+    DELIM + r'guardswoman' + DELIM: '💂‍♀️',
+    DELIM + r'ninja' + DELIM: '🥷',
+    DELIM + r'construction_worker' + DELIM: '👷',
+    DELIM + r'construction_worker_man' + DELIM: '👷‍♂️',
+    DELIM + r'construction_worker_woman' + DELIM: '👷‍♀️',
+    DELIM + r'prince' + DELIM: '🤴',
+    DELIM + r'princess' + DELIM: '👸',
+    DELIM + r'person_with_turban' + DELIM: '👳',
+    DELIM + r'man_with_turban' + DELIM: '👳‍♂️',
+    DELIM + r'woman_with_turban' + DELIM: '👳‍♀️',
+    DELIM + r'man_with_gua_pi_mao' + DELIM: '👲',
+    DELIM + r'woman_with_headscarf' + DELIM: '🧕',
+    DELIM + r'person_in_tuxedo' + DELIM: '🤵',
+    DELIM + r'man_in_tuxedo' + DELIM: '🤵‍♂️',
+    DELIM + r'woman_in_tuxedo' + DELIM: '🤵‍♀️',
+    DELIM + r'person_with_veil' + DELIM: '👰',
+    DELIM + r'man_with_veil' + DELIM: '👰‍♂️',
+    DELIM + r'(bride|woman)_with_veil' + DELIM: '👰‍♀️',
+    DELIM + r'pregnant_woman' + DELIM: '🤰',
+    DELIM + r'breast_feeding' + DELIM: '🤱',
+    DELIM + r'woman_feeding_baby' + DELIM: '👩‍🍼',
+    DELIM + r'man_feeding_baby' + DELIM: '👨‍🍼',
+    DELIM + r'person_feeding_baby' + DELIM: '🧑‍🍼',
+
+    #
+    # Person Fantasy
+    #
+    DELIM + r'angel' + DELIM: '👼',
+    DELIM + r'santa' + DELIM: '🎅',
+    DELIM + r'mrs_claus' + DELIM: '🤶',
+    DELIM + r'mx_claus' + DELIM: '🧑‍🎄',
+    DELIM + r'superhero' + DELIM: '🦸',
+    DELIM + r'superhero_man' + DELIM: '🦸‍♂️',
+    DELIM + r'superhero_woman' + DELIM: '🦸‍♀️',
+    DELIM + r'supervillain' + DELIM: '🦹',
+    DELIM + r'supervillain_man' + DELIM: '🦹‍♂️',
+    DELIM + r'supervillain_woman' + DELIM: '🦹‍♀️',
+    DELIM + r'mage' + DELIM: '🧙',
+    DELIM + r'mage_man' + DELIM: '🧙‍♂️',
+    DELIM + r'mage_woman' + DELIM: '🧙‍♀️',
+    DELIM + r'fairy' + DELIM: '🧚',
+    DELIM + r'fairy_man' + DELIM: '🧚‍♂️',
+    DELIM + r'fairy_woman' + DELIM: '🧚‍♀️',
+    DELIM + r'vampire' + DELIM: '🧛',
+    DELIM + r'vampire_man' + DELIM: '🧛‍♂️',
+    DELIM + r'vampire_woman' + DELIM: '🧛‍♀️',
+    DELIM + r'merperson' + DELIM: '🧜',
+    DELIM + r'merman' + DELIM: '🧜‍♂️',
+    DELIM + r'mermaid' + DELIM: '🧜‍♀️',
+    DELIM + r'elf' + DELIM: '🧝',
+    DELIM + r'elf_man' + DELIM: '🧝‍♂️',
+    DELIM + r'elf_woman' + DELIM: '🧝‍♀️',
+    DELIM + r'genie' + DELIM: '🧞',
+    DELIM + r'genie_man' + DELIM: '🧞‍♂️',
+    DELIM + r'genie_woman' + DELIM: '🧞‍♀️',
+    DELIM + r'zombie' + DELIM: '🧟',
+    DELIM + r'zombie_man' + DELIM: '🧟‍♂️',
+    DELIM + r'zombie_woman' + DELIM: '🧟‍♀️',
+
+    #
+    # Person Activity
+    #
+    DELIM + r'massage' + DELIM: '💆',
+    DELIM + r'massage_man' + DELIM: '💆‍♂️',
+    DELIM + r'massage_woman' + DELIM: '💆‍♀️',
+    DELIM + r'haircut' + DELIM: '💇',
+    DELIM + r'haircut_man' + DELIM: '💇‍♂️',
+    DELIM + r'haircut_woman' + DELIM: '💇‍♀️',
+    DELIM + r'walking' + DELIM: '🚶',
+    DELIM + r'walking_man' + DELIM: '🚶‍♂️',
+    DELIM + r'walking_woman' + DELIM: '🚶‍♀️',
+    DELIM + r'standing_person' + DELIM: '🧍',
+    DELIM + r'standing_man' + DELIM: '🧍‍♂️',
+    DELIM + r'standing_woman' + DELIM: '🧍‍♀️',
+    DELIM + r'kneeling_person' + DELIM: '🧎',
+    DELIM + r'kneeling_man' + DELIM: '🧎‍♂️',
+    DELIM + r'kneeling_woman' + DELIM: '🧎‍♀️',
+    DELIM + r'person_with_probing_cane' + DELIM: '🧑‍🦯',
+    DELIM + r'man_with_probing_cane' + DELIM: '👨‍🦯',
+    DELIM + r'woman_with_probing_cane' + DELIM: '👩‍🦯',
+    DELIM + r'person_in_motorized_wheelchair' + DELIM: '🧑‍🦼',
+    DELIM + r'man_in_motorized_wheelchair' + DELIM: '👨‍🦼',
+    DELIM + r'woman_in_motorized_wheelchair' + DELIM: '👩‍🦼',
+    DELIM + r'person_in_manual_wheelchair' + DELIM: '🧑‍🦽',
+    DELIM + r'man_in_manual_wheelchair' + DELIM: '👨‍🦽',
+    DELIM + r'woman_in_manual_wheelchair' + DELIM: '👩‍🦽',
+    DELIM + r'runn(er|ing)' + DELIM: '🏃',
+    DELIM + r'running_man' + DELIM: '🏃‍♂️',
+    DELIM + r'running_woman' + DELIM: '🏃‍♀️',
+    DELIM + r'(dancer|woman_dancing)' + DELIM: '💃',
+    DELIM + r'man_dancing' + DELIM: '🕺',
+    DELIM + r'business_suit_levitating' + DELIM: '🕴️',
+    DELIM + r'dancers' + DELIM: '👯',
+    DELIM + r'dancing_men' + DELIM: '👯‍♂️',
+    DELIM + r'dancing_women' + DELIM: '👯‍♀️',
+    DELIM + r'sauna_person' + DELIM: '🧖',
+    DELIM + r'sauna_man' + DELIM: '🧖‍♂️',
+    DELIM + r'sauna_woman' + DELIM: '🧖‍♀️',
+    DELIM + r'climbing' + DELIM: '🧗',
+    DELIM + r'climbing_man' + DELIM: '🧗‍♂️',
+    DELIM + r'climbing_woman' + DELIM: '🧗‍♀️',
+
+    #
+    # Person Sport
+    #
+    DELIM + r'person_fencing' + DELIM: '🤺',
+    DELIM + r'horse_racing' + DELIM: '🏇',
+    DELIM + r'skier' + DELIM: '⛷️',
+    DELIM + r'snowboarder' + DELIM: '🏂',
+    DELIM + r'golfing' + DELIM: '🏌️',
+    DELIM + r'golfing_man' + DELIM: '🏌️‍♂️',
+    DELIM + r'golfing_woman' + DELIM: '🏌️‍♀️',
+    DELIM + r'surfer' + DELIM: '🏄',
+    DELIM + r'surfing_man' + DELIM: '🏄‍♂️',
+    DELIM + r'surfing_woman' + DELIM: '🏄‍♀️',
+    DELIM + r'rowboat' + DELIM: '🚣',
+    DELIM + r'rowing_man' + DELIM: '🚣‍♂️',
+    DELIM + r'rowing_woman' + DELIM: '🚣‍♀️',
+    DELIM + r'swimmer' + DELIM: '🏊',
+    DELIM + r'swimming_man' + DELIM: '🏊‍♂️',
+    DELIM + r'swimming_woman' + DELIM: '🏊‍♀️',
+    DELIM + r'bouncing_ball_person' + DELIM: '⛹️',
+    DELIM + r'(basketball|bouncing_ball)_man' + DELIM: '⛹️‍♂️',
+    DELIM + r'(basketball|bouncing_ball)_woman' + DELIM: '⛹️‍♀️',
+    DELIM + r'weight_lifting' + DELIM: '🏋️',
+    DELIM + r'weight_lifting_man' + DELIM: '🏋️‍♂️',
+    DELIM + r'weight_lifting_woman' + DELIM: '🏋️‍♀️',
+    DELIM + r'bicyclist' + DELIM: '🚴',
+    DELIM + r'biking_man' + DELIM: '🚴‍♂️',
+    DELIM + r'biking_woman' + DELIM: '🚴‍♀️',
+    DELIM + r'mountain_bicyclist' + DELIM: '🚵',
+    DELIM + r'mountain_biking_man' + DELIM: '🚵‍♂️',
+    DELIM + r'mountain_biking_woman' + DELIM: '🚵‍♀️',
+    DELIM + r'cartwheeling' + DELIM: '🤸',
+    DELIM + r'man_cartwheeling' + DELIM: '🤸‍♂️',
+    DELIM + r'woman_cartwheeling' + DELIM: '🤸‍♀️',
+    DELIM + r'wrestling' + DELIM: '🤼',
+    DELIM + r'men_wrestling' + DELIM: '🤼‍♂️',
+    DELIM + r'women_wrestling' + DELIM: '🤼‍♀️',
+    DELIM + r'water_polo' + DELIM: '🤽',
+    DELIM + r'man_playing_water_polo' + DELIM: '🤽‍♂️',
+    DELIM + r'woman_playing_water_polo' + DELIM: '🤽‍♀️',
+    DELIM + r'handball_person' + DELIM: '🤾',
+    DELIM + r'man_playing_handball' + DELIM: '🤾‍♂️',
+    DELIM + r'woman_playing_handball' + DELIM: '🤾‍♀️',
+    DELIM + r'juggling_person' + DELIM: '🤹',
+    DELIM + r'man_juggling' + DELIM: '🤹‍♂️',
+    DELIM + r'woman_juggling' + DELIM: '🤹‍♀️',
+
+    #
+    # Person Resting
+    #
+    DELIM + r'lotus_position' + DELIM: '🧘',
+    DELIM + r'lotus_position_man' + DELIM: '🧘‍♂️',
+    DELIM + r'lotus_position_woman' + DELIM: '🧘‍♀️',
+    DELIM + r'bath' + DELIM: '🛀',
+    DELIM + r'sleeping_bed' + DELIM: '🛌',
+
+    #
+    # Family
+    #
+    DELIM + r'people_holding_hands' + DELIM: '🧑‍🤝‍🧑',
+    DELIM + r'two_women_holding_hands' + DELIM: '👭',
+    DELIM + r'couple' + DELIM: '👫',
+    DELIM + r'two_men_holding_hands' + DELIM: '👬',
+    DELIM + r'couplekiss' + DELIM: '💏',
+    DELIM + r'couplekiss_man_woman' + DELIM: '👩‍❤️‍💋‍👨',
+    DELIM + r'couplekiss_man_man' + DELIM: '👨‍❤️‍💋‍👨',
+    DELIM + r'couplekiss_woman_woman' + DELIM: '👩‍❤️‍💋‍👩',
+    DELIM + r'couple_with_heart' + DELIM: '💑',
+    DELIM + r'couple_with_heart_woman_man' + DELIM: '👩‍❤️‍👨',
+    DELIM + r'couple_with_heart_man_man' + DELIM: '👨‍❤️‍👨',
+    DELIM + r'couple_with_heart_woman_woman' + DELIM: '👩‍❤️‍👩',
+    DELIM + r'family_man_woman_boy' + DELIM: '👨‍👩‍👦',
+    DELIM + r'family_man_woman_girl' + DELIM: '👨‍👩‍👧',
+    DELIM + r'family_man_woman_girl_boy' + DELIM: '👨‍👩‍👧‍👦',
+    DELIM + r'family_man_woman_boy_boy' + DELIM: '👨‍👩‍👦‍👦',
+    DELIM + r'family_man_woman_girl_girl' + DELIM: '👨‍👩‍👧‍👧',
+    DELIM + r'family_man_man_boy' + DELIM: '👨‍👨‍👦',
+    DELIM + r'family_man_man_girl' + DELIM: '👨‍👨‍👧',
+    DELIM + r'family_man_man_girl_boy' + DELIM: '👨‍👨‍👧‍👦',
+    DELIM + r'family_man_man_boy_boy' + DELIM: '👨‍👨‍👦‍👦',
+    DELIM + r'family_man_man_girl_girl' + DELIM: '👨‍👨‍👧‍👧',
+    DELIM + r'family_woman_woman_boy' + DELIM: '👩‍👩‍👦',
+    DELIM + r'family_woman_woman_girl' + DELIM: '👩‍👩‍👧',
+    DELIM + r'family_woman_woman_girl_boy' + DELIM: '👩‍👩‍👧‍👦',
+    DELIM + r'family_woman_woman_boy_boy' + DELIM: '👩‍👩‍👦‍👦',
+    DELIM + r'family_woman_woman_girl_girl' + DELIM: '👩‍👩‍👧‍👧',
+    DELIM + r'family_man_boy' + DELIM: '👨‍👦',
+    DELIM + r'family_man_boy_boy' + DELIM: '👨‍👦‍👦',
+    DELIM + r'family_man_girl' + DELIM: '👨‍👧',
+    DELIM + r'family_man_girl_boy' + DELIM: '👨‍👧‍👦',
+    DELIM + r'family_man_girl_girl' + DELIM: '👨‍👧‍👧',
+    DELIM + r'family_woman_boy' + DELIM: '👩‍👦',
+    DELIM + r'family_woman_boy_boy' + DELIM: '👩‍👦‍👦',
+    DELIM + r'family_woman_girl' + DELIM: '👩‍👧',
+    DELIM + r'family_woman_girl_boy' + DELIM: '👩‍👧‍👦',
+    DELIM + r'family_woman_girl_girl' + DELIM: '👩‍👧‍👧',
+
+    #
+    # Person Symbol
+    #
+    DELIM + r'speaking_head' + DELIM: '🗣️',
+    DELIM + r'bust_in_silhouette' + DELIM: '👤',
+    DELIM + r'busts_in_silhouette' + DELIM: '👥',
+    DELIM + r'people_hugging' + DELIM: '🫂',
+    DELIM + r'family' + DELIM: '👪',
+    DELIM + r'footprints' + DELIM: '👣',
+
+    #
+    # Animal Mammal
+    #
+    DELIM + r'monkey_face' + DELIM: '🐵',
+    DELIM + r'monkey' + DELIM: '🐒',
+    DELIM + r'gorilla' + DELIM: '🦍',
+    DELIM + r'orangutan' + DELIM: '🦧',
+    DELIM + r'dog' + DELIM: '🐶',
+    DELIM + r'dog2' + DELIM: '🐕',
+    DELIM + r'guide_dog' + DELIM: '🦮',
+    DELIM + r'service_dog' + DELIM: '🐕‍🦺',
+    DELIM + r'poodle' + DELIM: '🐩',
+    DELIM + r'wolf' + DELIM: '🐺',
+    DELIM + r'fox_face' + DELIM: '🦊',
+    DELIM + r'raccoon' + DELIM: '🦝',
+    DELIM + r'cat' + DELIM: '🐱',
+    DELIM + r'cat2' + DELIM: '🐈',
+    DELIM + r'black_cat' + DELIM: '🐈‍⬛',
+    DELIM + r'lion' + DELIM: '🦁',
+    DELIM + r'tiger' + DELIM: '🐯',
+    DELIM + r'tiger2' + DELIM: '🐅',
+    DELIM + r'leopard' + DELIM: '🐆',
+    DELIM + r'horse' + DELIM: '🐴',
+    DELIM + r'racehorse' + DELIM: '🐎',
+    DELIM + r'unicorn' + DELIM: '🦄',
+    DELIM + r'zebra' + DELIM: '🦓',
+    DELIM + r'deer' + DELIM: '🦌',
+    DELIM + r'bison' + DELIM: '🦬',
+    DELIM + r'cow' + DELIM: '🐮',
+    DELIM + r'ox' + DELIM: '🐂',
+    DELIM + r'water_buffalo' + DELIM: '🐃',
+    DELIM + r'cow2' + DELIM: '🐄',
+    DELIM + r'pig' + DELIM: '🐷',
+    DELIM + r'pig2' + DELIM: '🐖',
+    DELIM + r'boar' + DELIM: '🐗',
+    DELIM + r'pig_nose' + DELIM: '🐽',
+    DELIM + r'ram' + DELIM: '🐏',
+    DELIM + r'sheep' + DELIM: '🐑',
+    DELIM + r'goat' + DELIM: '🐐',
+    DELIM + r'dromedary_camel' + DELIM: '🐪',
+    DELIM + r'camel' + DELIM: '🐫',
+    DELIM + r'llama' + DELIM: '🦙',
+    DELIM + r'giraffe' + DELIM: '🦒',
+    DELIM + r'elephant' + DELIM: '🐘',
+    DELIM + r'mammoth' + DELIM: '🦣',
+    DELIM + r'rhinoceros' + DELIM: '🦏',
+    DELIM + r'hippopotamus' + DELIM: '🦛',
+    DELIM + r'mouse' + DELIM: '🐭',
+    DELIM + r'mouse2' + DELIM: '🐁',
+    DELIM + r'rat' + DELIM: '🐀',
+    DELIM + r'hamster' + DELIM: '🐹',
+    DELIM + r'rabbit' + DELIM: '🐰',
+    DELIM + r'rabbit2' + DELIM: '🐇',
+    DELIM + r'chipmunk' + DELIM: '🐿️',
+    DELIM + r'beaver' + DELIM: '🦫',
+    DELIM + r'hedgehog' + DELIM: '🦔',
+    DELIM + r'bat' + DELIM: '🦇',
+    DELIM + r'bear' + DELIM: '🐻',
+    DELIM + r'polar_bear' + DELIM: '🐻‍❄️',
+    DELIM + r'koala' + DELIM: '🐨',
+    DELIM + r'panda_face' + DELIM: '🐼',
+    DELIM + r'sloth' + DELIM: '🦥',
+    DELIM + r'otter' + DELIM: '🦦',
+    DELIM + r'skunk' + DELIM: '🦨',
+    DELIM + r'kangaroo' + DELIM: '🦘',
+    DELIM + r'badger' + DELIM: '🦡',
+    DELIM + r'(feet|paw_prints)' + DELIM: '🐾',
+
+    #
+    # Animal Bird
+    #
+    DELIM + r'turkey' + DELIM: '🦃',
+    DELIM + r'chicken' + DELIM: '🐔',
+    DELIM + r'rooster' + DELIM: '🐓',
+    DELIM + r'hatching_chick' + DELIM: '🐣',
+    DELIM + r'baby_chick' + DELIM: '🐤',
+    DELIM + r'hatched_chick' + DELIM: '🐥',
+    DELIM + r'bird' + DELIM: '🐦',
+    DELIM + r'penguin' + DELIM: '🐧',
+    DELIM + r'dove' + DELIM: '🕊️',
+    DELIM + r'eagle' + DELIM: '🦅',
+    DELIM + r'duck' + DELIM: '🦆',
+    DELIM + r'swan' + DELIM: '🦢',
+    DELIM + r'owl' + DELIM: '🦉',
+    DELIM + r'dodo' + DELIM: '🦤',
+    DELIM + r'feather' + DELIM: '🪶',
+    DELIM + r'flamingo' + DELIM: '🦩',
+    DELIM + r'peacock' + DELIM: '🦚',
+    DELIM + r'parrot' + DELIM: '🦜',
+
+    #
+    # Animal Amphibian
+    #
+    DELIM + r'frog' + DELIM: '🐸',
+
+    #
+    # Animal Reptile
+    #
+    DELIM + r'crocodile' + DELIM: '🐊',
+    DELIM + r'turtle' + DELIM: '🐢',
+    DELIM + r'lizard' + DELIM: '🦎',
+    DELIM + r'snake' + DELIM: '🐍',
+    DELIM + r'dragon_face' + DELIM: '🐲',
+    DELIM + r'dragon' + DELIM: '🐉',
+    DELIM + r'sauropod' + DELIM: '🦕',
+    DELIM + r't-rex' + DELIM: '🦖',
+
+    #
+    # Animal Marine
+    #
+    DELIM + r'whale' + DELIM: '🐳',
+    DELIM + r'whale2' + DELIM: '🐋',
+    DELIM + r'dolphin' + DELIM: '🐬',
+    DELIM + r'(seal|flipper)' + DELIM: '🦭',
+    DELIM + r'fish' + DELIM: '🐟',
+    DELIM + r'tropical_fish' + DELIM: '🐠',
+    DELIM + r'blowfish' + DELIM: '🐡',
+    DELIM + r'shark' + DELIM: '🦈',
+    DELIM + r'octopus' + DELIM: '🐙',
+    DELIM + r'shell' + DELIM: '🐚',
+
+    #
+    # Animal Bug
+    #
+    DELIM + r'snail' + DELIM: '🐌',
+    DELIM + r'butterfly' + DELIM: '🦋',
+    DELIM + r'bug' + DELIM: '🐛',
+    DELIM + r'ant' + DELIM: '🐜',
+    DELIM + r'bee' + DELIM: '🐝',
+    DELIM + r'honeybee' + DELIM: '🪲',
+    DELIM + r'(lady_)?beetle' + DELIM: '🐞',
+    DELIM + r'cricket' + DELIM: '🦗',
+    DELIM + r'cockroach' + DELIM: '🪳',
+    DELIM + r'spider' + DELIM: '🕷️',
+    DELIM + r'spider_web' + DELIM: '🕸️',
+    DELIM + r'scorpion' + DELIM: '🦂',
+    DELIM + r'mosquito' + DELIM: '🦟',
+    DELIM + r'fly' + DELIM: '🪰',
+    DELIM + r'worm' + DELIM: '🪱',
+    DELIM + r'microbe' + DELIM: '🦠',
+
+    #
+    # Plant Flower
+    #
+    DELIM + r'bouquet' + DELIM: '💐',
+    DELIM + r'cherry_blossom' + DELIM: '🌸',
+    DELIM + r'white_flower' + DELIM: '💮',
+    DELIM + r'rosette' + DELIM: '🏵️',
+    DELIM + r'rose' + DELIM: '🌹',
+    DELIM + r'wilted_flower' + DELIM: '🥀',
+    DELIM + r'hibiscus' + DELIM: '🌺',
+    DELIM + r'sunflower' + DELIM: '🌻',
+    DELIM + r'blossom' + DELIM: '🌼',
+    DELIM + r'tulip' + DELIM: '🌷',
+
+    #
+    # Plant Other
+    #
+    DELIM + r'seedling' + DELIM: '🌱',
+    DELIM + r'potted_plant' + DELIM: '🪴',
+    DELIM + r'evergreen_tree' + DELIM: '🌲',
+    DELIM + r'deciduous_tree' + DELIM: '🌳',
+    DELIM + r'palm_tree' + DELIM: '🌴',
+    DELIM + r'cactus' + DELIM: '🌵',
+    DELIM + r'ear_of_rice' + DELIM: '🌾',
+    DELIM + r'herb' + DELIM: '🌿',
+    DELIM + r'shamrock' + DELIM: '☘️',
+    DELIM + r'four_leaf_clover' + DELIM: '🍀',
+    DELIM + r'maple_leaf' + DELIM: '🍁',
+    DELIM + r'fallen_leaf' + DELIM: '🍂',
+    DELIM + r'leaves' + DELIM: '🍃',
+    DELIM + r'mushroom' + DELIM: '🍄',
+
+    #
+    # Food Fruit
+    #
+    DELIM + r'grapes' + DELIM: '🍇',
+    DELIM + r'melon' + DELIM: '🍈',
+    DELIM + r'watermelon' + DELIM: '🍉',
+    DELIM + r'(orange|mandarin|tangerine)' + DELIM: '🍊',
+    DELIM + r'lemon' + DELIM: '🍋',
+    DELIM + r'banana' + DELIM: '🍌',
+    DELIM + r'pineapple' + DELIM: '🍍',
+    DELIM + r'mango' + DELIM: '🥭',
+    DELIM + r'apple' + DELIM: '🍎',
+    DELIM + r'green_apple' + DELIM: '🍏',
+    DELIM + r'pear' + DELIM: '🍐',
+    DELIM + r'peach' + DELIM: '🍑',
+    DELIM + r'cherries' + DELIM: '🍒',
+    DELIM + r'strawberry' + DELIM: '🍓',
+    DELIM + r'blueberries' + DELIM: '🫐',
+    DELIM + r'kiwi_fruit' + DELIM: '🥝',
+    DELIM + r'tomato' + DELIM: '🍅',
+    DELIM + r'olive' + DELIM: '🫒',
+    DELIM + r'coconut' + DELIM: '🥥',
+
+    #
+    # Food Vegetable
+    #
+    DELIM + r'avocado' + DELIM: '🥑',
+    DELIM + r'eggplant' + DELIM: '🍆',
+    DELIM + r'potato' + DELIM: '🥔',
+    DELIM + r'carrot' + DELIM: '🥕',
+    DELIM + r'corn' + DELIM: '🌽',
+    DELIM + r'hot_pepper' + DELIM: '🌶️',
+    DELIM + r'bell_pepper' + DELIM: '🫑',
+    DELIM + r'cucumber' + DELIM: '🥒',
+    DELIM + r'leafy_green' + DELIM: '🥬',
+    DELIM + r'broccoli' + DELIM: '🥦',
+    DELIM + r'garlic' + DELIM: '🧄',
+    DELIM + r'onion' + DELIM: '🧅',
+    DELIM + r'peanuts' + DELIM: '🥜',
+    DELIM + r'chestnut' + DELIM: '🌰',
+
+    #
+    # Food Prepared
+    #
+    DELIM + r'bread' + DELIM: '🍞',
+    DELIM + r'croissant' + DELIM: '🥐',
+    DELIM + r'baguette_bread' + DELIM: '🥖',
+    DELIM + r'flatbread' + DELIM: '🫓',
+    DELIM + r'pretzel' + DELIM: '🥨',
+    DELIM + r'bagel' + DELIM: '🥯',
+    DELIM + r'pancakes' + DELIM: '🥞',
+    DELIM + r'waffle' + DELIM: '🧇',
+    DELIM + r'cheese' + DELIM: '🧀',
+    DELIM + r'meat_on_bone' + DELIM: '🍖',
+    DELIM + r'poultry_leg' + DELIM: '🍗',
+    DELIM + r'cut_of_meat' + DELIM: '🥩',
+    DELIM + r'bacon' + DELIM: '🥓',
+    DELIM + r'hamburger' + DELIM: '🍔',
+    DELIM + r'fries' + DELIM: '🍟',
+    DELIM + r'pizza' + DELIM: '🍕',
+    DELIM + r'hotdog' + DELIM: '🌭',
+    DELIM + r'sandwich' + DELIM: '🥪',
+    DELIM + r'taco' + DELIM: '🌮',
+    DELIM + r'burrito' + DELIM: '🌯',
+    DELIM + r'tamale' + DELIM: '🫔',
+    DELIM + r'stuffed_flatbread' + DELIM: '🥙',
+    DELIM + r'falafel' + DELIM: '🧆',
+    DELIM + r'egg' + DELIM: '🥚',
+    DELIM + r'fried_egg' + DELIM: '🍳',
+    DELIM + r'shallow_pan_of_food' + DELIM: '🥘',
+    DELIM + r'stew' + DELIM: '🍲',
+    DELIM + r'fondue' + DELIM: '🫕',
+    DELIM + r'bowl_with_spoon' + DELIM: '🥣',
+    DELIM + r'green_salad' + DELIM: '🥗',
+    DELIM + r'popcorn' + DELIM: '🍿',
+    DELIM + r'butter' + DELIM: '🧈',
+    DELIM + r'salt' + DELIM: '🧂',
+    DELIM + r'canned_food' + DELIM: '🥫',
+
+    #
+    # Food Asian
+    #
+    DELIM + r'bento' + DELIM: '🍱',
+    DELIM + r'rice_cracker' + DELIM: '🍘',
+    DELIM + r'rice_ball' + DELIM: '🍙',
+    DELIM + r'rice' + DELIM: '🍚',
+    DELIM + r'curry' + DELIM: '🍛',
+    DELIM + r'ramen' + DELIM: '🍜',
+    DELIM + r'spaghetti' + DELIM: '🍝',
+    DELIM + r'sweet_potato' + DELIM: '🍠',
+    DELIM + r'oden' + DELIM: '🍢',
+    DELIM + r'sushi' + DELIM: '🍣',
+    DELIM + r'fried_shrimp' + DELIM: '🍤',
+    DELIM + r'fish_cake' + DELIM: '🍥',
+    DELIM + r'moon_cake' + DELIM: '🥮',
+    DELIM + r'dango' + DELIM: '🍡',
+    DELIM + r'dumpling' + DELIM: '🥟',
+    DELIM + r'fortune_cookie' + DELIM: '🥠',
+    DELIM + r'takeout_box' + DELIM: '🥡',
+
+    #
+    # Food Marine
+    #
+    DELIM + r'crab' + DELIM: '🦀',
+    DELIM + r'lobster' + DELIM: '🦞',
+    DELIM + r'shrimp' + DELIM: '🦐',
+    DELIM + r'squid' + DELIM: '🦑',
+    DELIM + r'oyster' + DELIM: '🦪',
+
+    #
+    # Food Sweet
+    #
+    DELIM + r'icecream' + DELIM: '🍦',
+    DELIM + r'shaved_ice' + DELIM: '🍧',
+    DELIM + r'ice_cream' + DELIM: '🍨',
+    DELIM + r'doughnut' + DELIM: '🍩',
+    DELIM + r'cookie' + DELIM: '🍪',
+    DELIM + r'birthday' + DELIM: '🎂',
+    DELIM + r'cake' + DELIM: '🍰',
+    DELIM + r'cupcake' + DELIM: '🧁',
+    DELIM + r'pie' + DELIM: '🥧',
+    DELIM + r'chocolate_bar' + DELIM: '🍫',
+    DELIM + r'candy' + DELIM: '🍬',
+    DELIM + r'lollipop' + DELIM: '🍭',
+    DELIM + r'custard' + DELIM: '🍮',
+    DELIM + r'honey_pot' + DELIM: '🍯',
+
+    #
+    # Drink
+    #
+    DELIM + r'baby_bottle' + DELIM: '🍼',
+    DELIM + r'milk_glass' + DELIM: '🥛',
+    DELIM + r'coffee' + DELIM: '☕',
+    DELIM + r'teapot' + DELIM: '🫖',
+    DELIM + r'tea' + DELIM: '🍵',
+    DELIM + r'sake' + DELIM: '🍶',
+    DELIM + r'champagne' + DELIM: '🍾',
+    DELIM + r'wine_glass' + DELIM: '🍷',
+    DELIM + r'cocktail' + DELIM: '🍸',
+    DELIM + r'tropical_drink' + DELIM: '🍹',
+    DELIM + r'beer' + DELIM: '🍺',
+    DELIM + r'beers' + DELIM: '🍻',
+    DELIM + r'clinking_glasses' + DELIM: '🥂',
+    DELIM + r'tumbler_glass' + DELIM: '🥃',
+    DELIM + r'cup_with_straw' + DELIM: '🥤',
+    DELIM + r'bubble_tea' + DELIM: '🧋',
+    DELIM + r'beverage_box' + DELIM: '🧃',
+    DELIM + r'mate' + DELIM: '🧉',
+    DELIM + r'ice_cube' + DELIM: '🧊',
+
+    #
+    # Dishware
+    #
+    DELIM + r'chopsticks' + DELIM: '🥢',
+    DELIM + r'plate_with_cutlery' + DELIM: '🍽️',
+    DELIM + r'fork_and_knife' + DELIM: '🍴',
+    DELIM + r'spoon' + DELIM: '🥄',
+    DELIM + r'(hocho|knife)' + DELIM: '🔪',
+    DELIM + r'amphora' + DELIM: '🏺',
+
+    #
+    # Place Map
+    #
+    DELIM + r'earth_africa' + DELIM: '🌍',
+    DELIM + r'earth_americas' + DELIM: '🌎',
+    DELIM + r'earth_asia' + DELIM: '🌏',
+    DELIM + r'globe_with_meridians' + DELIM: '🌐',
+    DELIM + r'world_map' + DELIM: '🗺️',
+    DELIM + r'japan' + DELIM: '🗾',
+    DELIM + r'compass' + DELIM: '🧭',
+
+    #
+    # Place Geographic
+    #
+    DELIM + r'mountain_snow' + DELIM: '🏔️',
+    DELIM + r'mountain' + DELIM: '⛰️',
+    DELIM + r'volcano' + DELIM: '🌋',
+    DELIM + r'mount_fuji' + DELIM: '🗻',
+    DELIM + r'camping' + DELIM: '🏕️',
+    DELIM + r'beach_umbrella' + DELIM: '🏖️',
+    DELIM + r'desert' + DELIM: '🏜️',
+    DELIM + r'desert_island' + DELIM: '🏝️',
+    DELIM + r'national_park' + DELIM: '🏞️',
+
+    #
+    # Place Building
+    #
+    DELIM + r'stadium' + DELIM: '🏟️',
+    DELIM + r'classical_building' + DELIM: '🏛️',
+    DELIM + r'building_construction' + DELIM: '🏗️',
+    DELIM + r'bricks' + DELIM: '🧱',
+    DELIM + r'rock' + DELIM: '🪨',
+    DELIM + r'wood' + DELIM: '🪵',
+    DELIM + r'hut' + DELIM: '🛖',
+    DELIM + r'houses' + DELIM: '🏘️',
+    DELIM + r'derelict_house' + DELIM: '🏚️',
+    DELIM + r'house' + DELIM: '🏠',
+    DELIM + r'house_with_garden' + DELIM: '🏡',
+    DELIM + r'office' + DELIM: '🏢',
+    DELIM + r'post_office' + DELIM: '🏣',
+    DELIM + r'european_post_office' + DELIM: '🏤',
+    DELIM + r'hospital' + DELIM: '🏥',
+    DELIM + r'bank' + DELIM: '🏦',
+    DELIM + r'hotel' + DELIM: '🏨',
+    DELIM + r'love_hotel' + DELIM: '🏩',
+    DELIM + r'convenience_store' + DELIM: '🏪',
+    DELIM + r'school' + DELIM: '🏫',
+    DELIM + r'department_store' + DELIM: '🏬',
+    DELIM + r'factory' + DELIM: '🏭',
+    DELIM + r'japanese_castle' + DELIM: '🏯',
+    DELIM + r'european_castle' + DELIM: '🏰',
+    DELIM + r'wedding' + DELIM: '💒',
+    DELIM + r'tokyo_tower' + DELIM: '🗼',
+    DELIM + r'statue_of_liberty' + DELIM: '🗽',
+
+    #
+    # Place Religious
+    #
+    DELIM + r'church' + DELIM: '⛪',
+    DELIM + r'mosque' + DELIM: '🕌',
+    DELIM + r'hindu_temple' + DELIM: '🛕',
+    DELIM + r'synagogue' + DELIM: '🕍',
+    DELIM + r'shinto_shrine' + DELIM: '⛩️',
+    DELIM + r'kaaba' + DELIM: '🕋',
+
+    #
+    # Place Other
+    #
+    DELIM + r'fountain' + DELIM: '⛲',
+    DELIM + r'tent' + DELIM: '⛺',
+    DELIM + r'foggy' + DELIM: '🌁',
+    DELIM + r'night_with_stars' + DELIM: '🌃',
+    DELIM + r'cityscape' + DELIM: '🏙️',
+    DELIM + r'sunrise_over_mountains' + DELIM: '🌄',
+    DELIM + r'sunrise' + DELIM: '🌅',
+    DELIM + r'city_sunset' + DELIM: '🌆',
+    DELIM + r'city_sunrise' + DELIM: '🌇',
+    DELIM + r'bridge_at_night' + DELIM: '🌉',
+    DELIM + r'hotsprings' + DELIM: '♨️',
+    DELIM + r'carousel_horse' + DELIM: '🎠',
+    DELIM + r'ferris_wheel' + DELIM: '🎡',
+    DELIM + r'roller_coaster' + DELIM: '🎢',
+    DELIM + r'barber' + DELIM: '💈',
+    DELIM + r'circus_tent' + DELIM: '🎪',
+
+    #
+    # Transport Ground
+    #
+    DELIM + r'steam_locomotive' + DELIM: '🚂',
+    DELIM + r'railway_car' + DELIM: '🚃',
+    DELIM + r'bullettrain_side' + DELIM: '🚄',
+    DELIM + r'bullettrain_front' + DELIM: '🚅',
+    DELIM + r'train2' + DELIM: '🚆',
+    DELIM + r'metro' + DELIM: '🚇',
+    DELIM + r'light_rail' + DELIM: '🚈',
+    DELIM + r'station' + DELIM: '🚉',
+    DELIM + r'tram' + DELIM: '🚊',
+    DELIM + r'monorail' + DELIM: '🚝',
+    DELIM + r'mountain_railway' + DELIM: '🚞',
+    DELIM + r'train' + DELIM: '🚋',
+    DELIM + r'bus' + DELIM: '🚌',
+    DELIM + r'oncoming_bus' + DELIM: '🚍',
+    DELIM + r'trolleybus' + DELIM: '🚎',
+    DELIM + r'minibus' + DELIM: '🚐',
+    DELIM + r'ambulance' + DELIM: '🚑',
+    DELIM + r'fire_engine' + DELIM: '🚒',
+    DELIM + r'police_car' + DELIM: '🚓',
+    DELIM + r'oncoming_police_car' + DELIM: '🚔',
+    DELIM + r'taxi' + DELIM: '🚕',
+    DELIM + r'oncoming_taxi' + DELIM: '🚖',
+    DELIM + r'car' + DELIM: '🚗',
+    DELIM + r'(red_car|oncoming_automobile)' + DELIM: '🚘',
+    DELIM + r'blue_car' + DELIM: '🚙',
+    DELIM + r'pickup_truck' + DELIM: '🛻',
+    DELIM + r'truck' + DELIM: '🚚',
+    DELIM + r'articulated_lorry' + DELIM: '🚛',
+    DELIM + r'tractor' + DELIM: '🚜',
+    DELIM + r'racing_car' + DELIM: '🏎️',
+    DELIM + r'motorcycle' + DELIM: '🏍️',
+    DELIM + r'motor_scooter' + DELIM: '🛵',
+    DELIM + r'manual_wheelchair' + DELIM: '🦽',
+    DELIM + r'motorized_wheelchair' + DELIM: '🦼',
+    DELIM + r'auto_rickshaw' + DELIM: '🛺',
+    DELIM + r'bike' + DELIM: '🚲',
+    DELIM + r'kick_scooter' + DELIM: '🛴',
+    DELIM + r'skateboard' + DELIM: '🛹',
+    DELIM + r'roller_skate' + DELIM: '🛼',
+    DELIM + r'busstop' + DELIM: '🚏',
+    DELIM + r'motorway' + DELIM: '🛣️',
+    DELIM + r'railway_track' + DELIM: '🛤️',
+    DELIM + r'oil_drum' + DELIM: '🛢️',
+    DELIM + r'fuelpump' + DELIM: '⛽',
+    DELIM + r'rotating_light' + DELIM: '🚨',
+    DELIM + r'traffic_light' + DELIM: '🚥',
+    DELIM + r'vertical_traffic_light' + DELIM: '🚦',
+    DELIM + r'stop_sign' + DELIM: '🛑',
+    DELIM + r'construction' + DELIM: '🚧',
+
+    #
+    # Transport Water
+    #
+    DELIM + r'anchor' + DELIM: '⚓',
+    DELIM + r'(sailboat|boat)' + DELIM: '⛵',
+    DELIM + r'canoe' + DELIM: '🛶',
+    DELIM + r'speedboat' + DELIM: '🚤',
+    DELIM + r'passenger_ship' + DELIM: '🛳️',
+    DELIM + r'ferry' + DELIM: '⛴️',
+    DELIM + r'motor_boat' + DELIM: '🛥️',
+    DELIM + r'ship' + DELIM: '🚢',
+
+    #
+    # Transport Air
+    #
+    DELIM + r'airplane' + DELIM: '✈️',
+    DELIM + r'small_airplane' + DELIM: '🛩️',
+    DELIM + r'flight_departure' + DELIM: '🛫',
+    DELIM + r'flight_arrival' + DELIM: '🛬',
+    DELIM + r'parachute' + DELIM: '🪂',
+    DELIM + r'seat' + DELIM: '💺',
+    DELIM + r'helicopter' + DELIM: '🚁',
+    DELIM + r'suspension_railway' + DELIM: '🚟',
+    DELIM + r'mountain_cableway' + DELIM: '🚠',
+    DELIM + r'aerial_tramway' + DELIM: '🚡',
+    DELIM + r'artificial_satellite' + DELIM: '🛰️',
+    DELIM + r'rocket' + DELIM: '🚀',
+    DELIM + r'flying_saucer' + DELIM: '🛸',
+
+    #
+    # Hotel
+    #
+    DELIM + r'bellhop_bell' + DELIM: '🛎️',
+    DELIM + r'luggage' + DELIM: '🧳',
+
+    #
+    # Time
+    #
+    DELIM + r'hourglass' + DELIM: '⌛',
+    DELIM + r'hourglass_flowing_sand' + DELIM: '⏳',
+    DELIM + r'watch' + DELIM: '⌚',
+    DELIM + r'alarm_clock' + DELIM: '⏰',
+    DELIM + r'stopwatch' + DELIM: '⏱️',
+    DELIM + r'timer_clock' + DELIM: '⏲️',
+    DELIM + r'mantelpiece_clock' + DELIM: '🕰️',
+    DELIM + r'clock12' + DELIM: '🕛',
+    DELIM + r'clock1230' + DELIM: '🕧',
+    DELIM + r'clock1' + DELIM: '🕐',
+    DELIM + r'clock130' + DELIM: '🕜',
+    DELIM + r'clock2' + DELIM: '🕑',
+    DELIM + r'clock230' + DELIM: '🕝',
+    DELIM + r'clock3' + DELIM: '🕒',
+    DELIM + r'clock330' + DELIM: '🕞',
+    DELIM + r'clock4' + DELIM: '🕓',
+    DELIM + r'clock430' + DELIM: '🕟',
+    DELIM + r'clock5' + DELIM: '🕔',
+    DELIM + r'clock530' + DELIM: '🕠',
+    DELIM + r'clock6' + DELIM: '🕕',
+    DELIM + r'clock630' + DELIM: '🕡',
+    DELIM + r'clock7' + DELIM: '🕖',
+    DELIM + r'clock730' + DELIM: '🕢',
+    DELIM + r'clock8' + DELIM: '🕗',
+    DELIM + r'clock830' + DELIM: '🕣',
+    DELIM + r'clock9' + DELIM: '🕘',
+    DELIM + r'clock930' + DELIM: '🕤',
+    DELIM + r'clock10' + DELIM: '🕙',
+    DELIM + r'clock1030' + DELIM: '🕥',
+    DELIM + r'clock11' + DELIM: '🕚',
+    DELIM + r'clock1130' + DELIM: '🕦',
+
+    # Sky & Weather
+    DELIM + r'new_moon' + DELIM: '🌑',
+    DELIM + r'waxing_crescent_moon' + DELIM: '🌒',
+    DELIM + r'first_quarter_moon' + DELIM: '🌓',
+    DELIM + r'moon' + DELIM: '🌔',
+    DELIM + r'(waxing_gibbous_moon|full_moon)' + DELIM: '🌕',
+    DELIM + r'waning_gibbous_moon' + DELIM: '🌖',
+    DELIM + r'last_quarter_moon' + DELIM: '🌗',
+    DELIM + r'waning_crescent_moon' + DELIM: '🌘',
+    DELIM + r'crescent_moon' + DELIM: '🌙',
+    DELIM + r'new_moon_with_face' + DELIM: '🌚',
+    DELIM + r'first_quarter_moon_with_face' + DELIM: '🌛',
+    DELIM + r'last_quarter_moon_with_face' + DELIM: '🌜',
+    DELIM + r'thermometer' + DELIM: '🌡️',
+    DELIM + r'sunny' + DELIM: '☀️',
+    DELIM + r'full_moon_with_face' + DELIM: '🌝',
+    DELIM + r'sun_with_face' + DELIM: '🌞',
+    DELIM + r'ringed_planet' + DELIM: '🪐',
+    DELIM + r'star' + DELIM: '⭐',
+    DELIM + r'star2' + DELIM: '🌟',
+    DELIM + r'stars' + DELIM: '🌠',
+    DELIM + r'milky_way' + DELIM: '🌌',
+    DELIM + r'cloud' + DELIM: '☁️',
+    DELIM + r'partly_sunny' + DELIM: '⛅',
+    DELIM + r'cloud_with_lightning_and_rain' + DELIM: '⛈️',
+    DELIM + r'sun_behind_small_cloud' + DELIM: '🌤️',
+    DELIM + r'sun_behind_large_cloud' + DELIM: '🌥️',
+    DELIM + r'sun_behind_rain_cloud' + DELIM: '🌦️',
+    DELIM + r'cloud_with_rain' + DELIM: '🌧️',
+    DELIM + r'cloud_with_snow' + DELIM: '🌨️',
+    DELIM + r'cloud_with_lightning' + DELIM: '🌩️',
+    DELIM + r'tornado' + DELIM: '🌪️',
+    DELIM + r'fog' + DELIM: '🌫️',
+    DELIM + r'wind_face' + DELIM: '🌬️',
+    DELIM + r'cyclone' + DELIM: '🌀',
+    DELIM + r'rainbow' + DELIM: '🌈',
+    DELIM + r'closed_umbrella' + DELIM: '🌂',
+    DELIM + r'open_umbrella' + DELIM: '☂️',
+    DELIM + r'umbrella' + DELIM: '☔',
+    DELIM + r'parasol_on_ground' + DELIM: '⛱️',
+    DELIM + r'zap' + DELIM: '⚡',
+    DELIM + r'snowflake' + DELIM: '❄️',
+    DELIM + r'snowman_with_snow' + DELIM: '☃️',
+    DELIM + r'snowman' + DELIM: '⛄',
+    DELIM + r'comet' + DELIM: '☄️',
+    DELIM + r'fire' + DELIM: '🔥',
+    DELIM + r'droplet' + DELIM: '💧',
+    DELIM + r'ocean' + DELIM: '🌊',
+
+    #
+    # Event
+    #
+    DELIM + r'jack_o_lantern' + DELIM: '🎃',
+    DELIM + r'christmas_tree' + DELIM: '🎄',
+    DELIM + r'fireworks' + DELIM: '🎆',
+    DELIM + r'sparkler' + DELIM: '🎇',
+    DELIM + r'firecracker' + DELIM: '🧨',
+    DELIM + r'sparkles' + DELIM: '✨',
+    DELIM + r'balloon' + DELIM: '🎈',
+    DELIM + r'tada' + DELIM: '🎉',
+    DELIM + r'confetti_ball' + DELIM: '🎊',
+    DELIM + r'tanabata_tree' + DELIM: '🎋',
+    DELIM + r'bamboo' + DELIM: '🎍',
+    DELIM + r'dolls' + DELIM: '🎎',
+    DELIM + r'flags' + DELIM: '🎏',
+    DELIM + r'wind_chime' + DELIM: '🎐',
+    DELIM + r'rice_scene' + DELIM: '🎑',
+    DELIM + r'red_envelope' + DELIM: '🧧',
+    DELIM + r'ribbon' + DELIM: '🎀',
+    DELIM + r'gift' + DELIM: '🎁',
+    DELIM + r'reminder_ribbon' + DELIM: '🎗️',
+    DELIM + r'tickets' + DELIM: '🎟️',
+    DELIM + r'ticket' + DELIM: '🎫',
+
+    #
+    # Award Medal
+    #
+    DELIM + r'medal_military' + DELIM: '🎖️',
+    DELIM + r'trophy' + DELIM: '🏆',
+    DELIM + r'medal_sports' + DELIM: '🏅',
+    DELIM + r'1st_place_medal' + DELIM: '🥇',
+    DELIM + r'2nd_place_medal' + DELIM: '🥈',
+    DELIM + r'3rd_place_medal' + DELIM: '🥉',
+
+    #
+    # Sport
+    #
+    DELIM + r'soccer' + DELIM: '⚽',
+    DELIM + r'baseball' + DELIM: '⚾',
+    DELIM + r'softball' + DELIM: '🥎',
+    DELIM + r'basketball' + DELIM: '🏀',
+    DELIM + r'volleyball' + DELIM: '🏐',
+    DELIM + r'football' + DELIM: '🏈',
+    DELIM + r'rugby_football' + DELIM: '🏉',
+    DELIM + r'tennis' + DELIM: '🎾',
+    DELIM + r'flying_disc' + DELIM: '🥏',
+    DELIM + r'bowling' + DELIM: '🎳',
+    DELIM + r'cricket_game' + DELIM: '🏏',
+    DELIM + r'field_hockey' + DELIM: '🏑',
+    DELIM + r'ice_hockey' + DELIM: '🏒',
+    DELIM + r'lacrosse' + DELIM: '🥍',
+    DELIM + r'ping_pong' + DELIM: '🏓',
+    DELIM + r'badminton' + DELIM: '🏸',
+    DELIM + r'boxing_glove' + DELIM: '🥊',
+    DELIM + r'martial_arts_uniform' + DELIM: '🥋',
+    DELIM + r'goal_net' + DELIM: '🥅',
+    DELIM + r'golf' + DELIM: '⛳',
+    DELIM + r'ice_skate' + DELIM: '⛸️',
+    DELIM + r'fishing_pole_and_fish' + DELIM: '🎣',
+    DELIM + r'diving_mask' + DELIM: '🤿',
+    DELIM + r'running_shirt_with_sash' + DELIM: '🎽',
+    DELIM + r'ski' + DELIM: '🎿',
+    DELIM + r'sled' + DELIM: '🛷',
+    DELIM + r'curling_stone' + DELIM: '🥌',
+
+    #
+    # Game
+    #
+    DELIM + r'dart' + DELIM: '🎯',
+    DELIM + r'yo_yo' + DELIM: '🪀',
+    DELIM + r'kite' + DELIM: '🪁',
+    DELIM + r'gun' + DELIM: '🔫',
+    DELIM + r'8ball' + DELIM: '🎱',
+    DELIM + r'crystal_ball' + DELIM: '🔮',
+    DELIM + r'magic_wand' + DELIM: '🪄',
+    DELIM + r'video_game' + DELIM: '🎮',
+    DELIM + r'joystick' + DELIM: '🕹️',
+    DELIM + r'slot_machine' + DELIM: '🎰',
+    DELIM + r'game_die' + DELIM: '🎲',
+    DELIM + r'jigsaw' + DELIM: '🧩',
+    DELIM + r'teddy_bear' + DELIM: '🧸',
+    DELIM + r'pinata' + DELIM: '🪅',
+    DELIM + r'nesting_dolls' + DELIM: '🪆',
+    DELIM + r'spades' + DELIM: '♠️',
+    DELIM + r'hearts' + DELIM: '♥️',
+    DELIM + r'diamonds' + DELIM: '♦️',
+    DELIM + r'clubs' + DELIM: '♣️',
+    DELIM + r'chess_pawn' + DELIM: '♟️',
+    DELIM + r'black_joker' + DELIM: '🃏',
+    DELIM + r'mahjong' + DELIM: '🀄',
+    DELIM + r'flower_playing_cards' + DELIM: '🎴',
+
+    #
+    # Arts & Crafts
+    #
+    DELIM + r'performing_arts' + DELIM: '🎭',
+    DELIM + r'framed_picture' + DELIM: '🖼️',
+    DELIM + r'art' + DELIM: '🎨',
+    DELIM + r'thread' + DELIM: '🧵',
+    DELIM + r'sewing_needle' + DELIM: '🪡',
+    DELIM + r'yarn' + DELIM: '🧶',
+    DELIM + r'knot' + DELIM: '🪢',
+
+    #
+    # Clothing
+    #
+    DELIM + r'eyeglasses' + DELIM: '👓',
+    DELIM + r'dark_sunglasses' + DELIM: '🕶️',
+    DELIM + r'goggles' + DELIM: '🥽',
+    DELIM + r'lab_coat' + DELIM: '🥼',
+    DELIM + r'safety_vest' + DELIM: '🦺',
+    DELIM + r'necktie' + DELIM: '👔',
+    DELIM + r't?shirt' + DELIM: '👕',
+    DELIM + r'jeans' + DELIM: '👖',
+    DELIM + r'scarf' + DELIM: '🧣',
+    DELIM + r'gloves' + DELIM: '🧤',
+    DELIM + r'coat' + DELIM: '🧥',
+    DELIM + r'socks' + DELIM: '🧦',
+    DELIM + r'dress' + DELIM: '👗',
+    DELIM + r'kimono' + DELIM: '👘',
+    DELIM + r'sari' + DELIM: '🥻',
+    DELIM + r'one_piece_swimsuit' + DELIM: '🩱',
+    DELIM + r'swim_brief' + DELIM: '🩲',
+    DELIM + r'shorts' + DELIM: '🩳',
+    DELIM + r'bikini' + DELIM: '👙',
+    DELIM + r'womans_clothes' + DELIM: '👚',
+    DELIM + r'purse' + DELIM: '👛',
+    DELIM + r'handbag' + DELIM: '👜',
+    DELIM + r'pouch' + DELIM: '👝',
+    DELIM + r'shopping' + DELIM: '🛍️',
+    DELIM + r'school_satchel' + DELIM: '🎒',
+    DELIM + r'thong_sandal' + DELIM: '🩴',
+    DELIM + r'(mans_)?shoe' + DELIM: '👞',
+    DELIM + r'athletic_shoe' + DELIM: '👟',
+    DELIM + r'hiking_boot' + DELIM: '🥾',
+    DELIM + r'flat_shoe' + DELIM: '🥿',
+    DELIM + r'high_heel' + DELIM: '👠',
+    DELIM + r'sandal' + DELIM: '👡',
+    DELIM + r'ballet_shoes' + DELIM: '🩰',
+    DELIM + r'boot' + DELIM: '👢',
+    DELIM + r'crown' + DELIM: '👑',
+    DELIM + r'womans_hat' + DELIM: '👒',
+    DELIM + r'tophat' + DELIM: '🎩',
+    DELIM + r'mortar_board' + DELIM: '🎓',
+    DELIM + r'billed_cap' + DELIM: '🧢',
+    DELIM + r'military_helmet' + DELIM: '🪖',
+    DELIM + r'rescue_worker_helmet' + DELIM: '⛑️',
+    DELIM + r'prayer_beads' + DELIM: '📿',
+    DELIM + r'lipstick' + DELIM: '💄',
+    DELIM + r'ring' + DELIM: '💍',
+    DELIM + r'gem' + DELIM: '💎',
+
+    #
+    # Sound
+    #
+    DELIM + r'mute' + DELIM: '🔇',
+    DELIM + r'speaker' + DELIM: '🔈',
+    DELIM + r'sound' + DELIM: '🔉',
+    DELIM + r'loud_sound' + DELIM: '🔊',
+    DELIM + r'loudspeaker' + DELIM: '📢',
+    DELIM + r'mega' + DELIM: '📣',
+    DELIM + r'postal_horn' + DELIM: '📯',
+    DELIM + r'bell' + DELIM: '🔔',
+    DELIM + r'no_bell' + DELIM: '🔕',
+
+    #
+    # Music
+    #
+    DELIM + r'musical_score' + DELIM: '🎼',
+    DELIM + r'musical_note' + DELIM: '🎵',
+    DELIM + r'notes' + DELIM: '🎶',
+    DELIM + r'studio_microphone' + DELIM: '🎙️',
+    DELIM + r'level_slider' + DELIM: '🎚️',
+    DELIM + r'control_knobs' + DELIM: '🎛️',
+    DELIM + r'microphone' + DELIM: '🎤',
+    DELIM + r'headphones' + DELIM: '🎧',
+    DELIM + r'radio' + DELIM: '📻',
+
+    #
+    # Musical Instrument
+    #
+    DELIM + r'saxophone' + DELIM: '🎷',
+    DELIM + r'accordion' + DELIM: '🪗',
+    DELIM + r'guitar' + DELIM: '🎸',
+    DELIM + r'musical_keyboard' + DELIM: '🎹',
+    DELIM + r'trumpet' + DELIM: '🎺',
+    DELIM + r'violin' + DELIM: '🎻',
+    DELIM + r'banjo' + DELIM: '🪕',
+    DELIM + r'drum' + DELIM: '🥁',
+    DELIM + r'long_drum' + DELIM: '🪘',
+
+    #
+    # Phone
+    #
+    DELIM + r'iphone' + DELIM: '📱',
+    DELIM + r'calling' + DELIM: '📲',
+    DELIM + r'phone' + DELIM: '☎️',
+    DELIM + r'telephone(_receiver)?' + DELIM: '📞',
+    DELIM + r'pager' + DELIM: '📟',
+    DELIM + r'fax' + DELIM: '📠',
+
+    #
+    # Computer
+    #
+    DELIM + r'battery' + DELIM: '🔋',
+    DELIM + r'electric_plug' + DELIM: '🔌',
+    DELIM + r'computer' + DELIM: '💻',
+    DELIM + r'desktop_computer' + DELIM: '🖥️',
+    DELIM + r'printer' + DELIM: '🖨️',
+    DELIM + r'keyboard' + DELIM: '⌨️',
+    DELIM + r'computer_mouse' + DELIM: '🖱️',
+    DELIM + r'trackball' + DELIM: '🖲️',
+    DELIM + r'minidisc' + DELIM: '💽',
+    DELIM + r'floppy_disk' + DELIM: '💾',
+    DELIM + r'cd' + DELIM: '💿',
+    DELIM + r'dvd' + DELIM: '📀',
+    DELIM + r'abacus' + DELIM: '🧮',
+
+    #
+    # Light & Video
+    #
+    DELIM + r'movie_camera' + DELIM: '🎥',
+    DELIM + r'film_strip' + DELIM: '🎞️',
+    DELIM + r'film_projector' + DELIM: '📽️',
+    DELIM + r'clapper' + DELIM: '🎬',
+    DELIM + r'tv' + DELIM: '📺',
+    DELIM + r'camera' + DELIM: '📷',
+    DELIM + r'camera_flash' + DELIM: '📸',
+    DELIM + r'video_camera' + DELIM: '📹',
+    DELIM + r'vhs' + DELIM: '📼',
+    DELIM + r'mag' + DELIM: '🔍',
+    DELIM + r'mag_right' + DELIM: '🔎',
+    DELIM + r'candle' + DELIM: '🕯️',
+    DELIM + r'bulb' + DELIM: '💡',
+    DELIM + r'flashlight' + DELIM: '🔦',
+    DELIM + r'(izakaya_)?lantern' + DELIM: '🏮',
+    DELIM + r'diya_lamp' + DELIM: '🪔',
+
+    #
+    # Book Paper
+    #
+    DELIM + r'notebook_with_decorative_cover' + DELIM: '📔',
+    DELIM + r'closed_book' + DELIM: '📕',
+    DELIM + r'(open_)?book' + DELIM: '📖',
+    DELIM + r'green_book' + DELIM: '📗',
+    DELIM + r'blue_book' + DELIM: '📘',
+    DELIM + r'orange_book' + DELIM: '📙',
+    DELIM + r'books' + DELIM: '📚',
+    DELIM + r'notebook' + DELIM: '📓',
+    DELIM + r'ledger' + DELIM: '📒',
+    DELIM + r'page_with_curl' + DELIM: '📃',
+    DELIM + r'scroll' + DELIM: '📜',
+    DELIM + r'page_facing_up' + DELIM: '📄',
+    DELIM + r'newspaper' + DELIM: '📰',
+    DELIM + r'newspaper_roll' + DELIM: '🗞️',
+    DELIM + r'bookmark_tabs' + DELIM: '📑',
+    DELIM + r'bookmark' + DELIM: '🔖',
+    DELIM + r'label' + DELIM: '🏷️',
+
+    #
+    # Money
+    #
+    DELIM + r'moneybag' + DELIM: '💰',
+    DELIM + r'coin' + DELIM: '🪙',
+    DELIM + r'yen' + DELIM: '💴',
+    DELIM + r'dollar' + DELIM: '💵',
+    DELIM + r'euro' + DELIM: '💶',
+    DELIM + r'pound' + DELIM: '💷',
+    DELIM + r'money_with_wings' + DELIM: '💸',
+    DELIM + r'credit_card' + DELIM: '💳',
+    DELIM + r'receipt' + DELIM: '🧾',
+    DELIM + r'chart' + DELIM: '💹',
+
+    #
+    # Mail
+    #
+    DELIM + r'envelope' + DELIM: '✉️',
+    DELIM + r'e-?mail' + DELIM: '📧',
+    DELIM + r'incoming_envelope' + DELIM: '📨',
+    DELIM + r'envelope_with_arrow' + DELIM: '📩',
+    DELIM + r'outbox_tray' + DELIM: '📤',
+    DELIM + r'inbox_tray' + DELIM: '📥',
+    DELIM + r'package' + DELIM: '📦',
+    DELIM + r'mailbox' + DELIM: '📫',
+    DELIM + r'mailbox_closed' + DELIM: '📪',
+    DELIM + r'mailbox_with_mail' + DELIM: '📬',
+    DELIM + r'mailbox_with_no_mail' + DELIM: '📭',
+    DELIM + r'postbox' + DELIM: '📮',
+    DELIM + r'ballot_box' + DELIM: '🗳️',
+
+    #
+    # Writing
+    #
+    DELIM + r'pencil2' + DELIM: '✏️',
+    DELIM + r'black_nib' + DELIM: '✒️',
+    DELIM + r'fountain_pen' + DELIM: '🖋️',
+    DELIM + r'pen' + DELIM: '🖊️',
+    DELIM + r'paintbrush' + DELIM: '🖌️',
+    DELIM + r'crayon' + DELIM: '🖍️',
+    DELIM + r'(memo|pencil)' + DELIM: '📝',
+
+    #
+    # Office
+    #
+    DELIM + r'briefcase' + DELIM: '💼',
+    DELIM + r'file_folder' + DELIM: '📁',
+    DELIM + r'open_file_folder' + DELIM: '📂',
+    DELIM + r'card_index_dividers' + DELIM: '🗂️',
+    DELIM + r'date' + DELIM: '📅',
+    DELIM + r'calendar' + DELIM: '📆',
+    DELIM + r'spiral_notepad' + DELIM: '🗒️',
+    DELIM + r'spiral_calendar' + DELIM: '🗓️',
+    DELIM + r'card_index' + DELIM: '📇',
+    DELIM + r'chart_with_upwards_trend' + DELIM: '📈',
+    DELIM + r'chart_with_downwards_trend' + DELIM: '📉',
+    DELIM + r'bar_chart' + DELIM: '📊',
+    DELIM + r'clipboard' + DELIM: '📋',
+    DELIM + r'pushpin' + DELIM: '📌',
+    DELIM + r'round_pushpin' + DELIM: '📍',
+    DELIM + r'paperclip' + DELIM: '📎',
+    DELIM + r'paperclips' + DELIM: '🖇️',
+    DELIM + r'straight_ruler' + DELIM: '📏',
+    DELIM + r'triangular_ruler' + DELIM: '📐',
+    DELIM + r'scissors' + DELIM: '✂️',
+    DELIM + r'card_file_box' + DELIM: '🗃️',
+    DELIM + r'file_cabinet' + DELIM: '🗄️',
+    DELIM + r'wastebasket' + DELIM: '🗑️',
+
+    #
+    # Lock
+    #
+    DELIM + r'lock' + DELIM: '🔒',
+    DELIM + r'unlock' + DELIM: '🔓',
+    DELIM + r'lock_with_ink_pen' + DELIM: '🔏',
+    DELIM + r'closed_lock_with_key' + DELIM: '🔐',
+    DELIM + r'key' + DELIM: '🔑',
+    DELIM + r'old_key' + DELIM: '🗝️',
+
+    #
+    # Tool
+    #
+    DELIM + r'hammer' + DELIM: '🔨',
+    DELIM + r'axe' + DELIM: '🪓',
+    DELIM + r'pick' + DELIM: '⛏️',
+    DELIM + r'hammer_and_pick' + DELIM: '⚒️',
+    DELIM + r'hammer_and_wrench' + DELIM: '🛠️',
+    DELIM + r'dagger' + DELIM: '🗡️',
+    DELIM + r'crossed_swords' + DELIM: '⚔️',
+    DELIM + r'bomb' + DELIM: '💣',
+    DELIM + r'boomerang' + DELIM: '🪃',
+    DELIM + r'bow_and_arrow' + DELIM: '🏹',
+    DELIM + r'shield' + DELIM: '🛡️',
+    DELIM + r'carpentry_saw' + DELIM: '🪚',
+    DELIM + r'wrench' + DELIM: '🔧',
+    DELIM + r'screwdriver' + DELIM: '🪛',
+    DELIM + r'nut_and_bolt' + DELIM: '🔩',
+    DELIM + r'gear' + DELIM: '⚙️',
+    DELIM + r'clamp' + DELIM: '🗜️',
+    DELIM + r'balance_scale' + DELIM: '⚖️',
+    DELIM + r'probing_cane' + DELIM: '🦯',
+    DELIM + r'link' + DELIM: '🔗',
+    DELIM + r'chains' + DELIM: '⛓️',
+    DELIM + r'hook' + DELIM: '🪝',
+    DELIM + r'toolbox' + DELIM: '🧰',
+    DELIM + r'magnet' + DELIM: '🧲',
+    DELIM + r'ladder' + DELIM: '🪜',
+
+    #
+    # Science
+    #
+    DELIM + r'alembic' + DELIM: '⚗️',
+    DELIM + r'test_tube' + DELIM: '🧪',
+    DELIM + r'petri_dish' + DELIM: '🧫',
+    DELIM + r'dna' + DELIM: '🧬',
+    DELIM + r'microscope' + DELIM: '🔬',
+    DELIM + r'telescope' + DELIM: '🔭',
+    DELIM + r'satellite' + DELIM: '📡',
+
+    #
+    # Medical
+    #
+    DELIM + r'syringe' + DELIM: '💉',
+    DELIM + r'drop_of_blood' + DELIM: '🩸',
+    DELIM + r'pill' + DELIM: '💊',
+    DELIM + r'adhesive_bandage' + DELIM: '🩹',
+    DELIM + r'stethoscope' + DELIM: '🩺',
+
+    #
+    # Household
+    #
+    DELIM + r'door' + DELIM: '🚪',
+    DELIM + r'elevator' + DELIM: '🛗',
+    DELIM + r'mirror' + DELIM: '🪞',
+    DELIM + r'window' + DELIM: '🪟',
+    DELIM + r'bed' + DELIM: '🛏️',
+    DELIM + r'couch_and_lamp' + DELIM: '🛋️',
+    DELIM + r'chair' + DELIM: '🪑',
+    DELIM + r'toilet' + DELIM: '🚽',
+    DELIM + r'plunger' + DELIM: '🪠',
+    DELIM + r'shower' + DELIM: '🚿',
+    DELIM + r'bathtub' + DELIM: '🛁',
+    DELIM + r'mouse_trap' + DELIM: '🪤',
+    DELIM + r'razor' + DELIM: '🪒',
+    DELIM + r'lotion_bottle' + DELIM: '🧴',
+    DELIM + r'safety_pin' + DELIM: '🧷',
+    DELIM + r'broom' + DELIM: '🧹',
+    DELIM + r'basket' + DELIM: '🧺',
+    DELIM + r'roll_of_paper' + DELIM: '🧻',
+    DELIM + r'bucket' + DELIM: '🪣',
+    DELIM + r'soap' + DELIM: '🧼',
+    DELIM + r'toothbrush' + DELIM: '🪥',
+    DELIM + r'sponge' + DELIM: '🧽',
+    DELIM + r'fire_extinguisher' + DELIM: '🧯',
+    DELIM + r'shopping_cart' + DELIM: '🛒',
+
+    #
+    # Other Object
+    #
+    DELIM + r'smoking' + DELIM: '🚬',
+    DELIM + r'coffin' + DELIM: '⚰️',
+    DELIM + r'headstone' + DELIM: '🪦',
+    DELIM + r'funeral_urn' + DELIM: '⚱️',
+    DELIM + r'nazar_amulet' + DELIM: '🧿',
+    DELIM + r'moyai' + DELIM: '🗿',
+    DELIM + r'placard' + DELIM: '🪧',
+
+    #
+    # Transport Sign
+    #
+    DELIM + r'atm' + DELIM: '🏧',
+    DELIM + r'put_litter_in_its_place' + DELIM: '🚮',
+    DELIM + r'potable_water' + DELIM: '🚰',
+    DELIM + r'wheelchair' + DELIM: '♿',
+    DELIM + r'mens' + DELIM: '🚹',
+    DELIM + r'womens' + DELIM: '🚺',
+    DELIM + r'restroom' + DELIM: '🚻',
+    DELIM + r'baby_symbol' + DELIM: '🚼',
+    DELIM + r'wc' + DELIM: '🚾',
+    DELIM + r'passport_control' + DELIM: '🛂',
+    DELIM + r'customs' + DELIM: '🛃',
+    DELIM + r'baggage_claim' + DELIM: '🛄',
+    DELIM + r'left_luggage' + DELIM: '🛅',
+
+    #
+    # Warning
+    #
+    DELIM + r'warning' + DELIM: '⚠️',
+    DELIM + r'children_crossing' + DELIM: '🚸',
+    DELIM + r'no_entry' + DELIM: '⛔',
+    DELIM + r'no_entry_sign' + DELIM: '🚫',
+    DELIM + r'no_bicycles' + DELIM: '🚳',
+    DELIM + r'no_smoking' + DELIM: '🚭',
+    DELIM + r'do_not_litter' + DELIM: '🚯',
+    DELIM + r'non-potable_water' + DELIM: '🚱',
+    DELIM + r'no_pedestrians' + DELIM: '🚷',
+    DELIM + r'no_mobile_phones' + DELIM: '📵',
+    DELIM + r'underage' + DELIM: '🔞',
+    DELIM + r'radioactive' + DELIM: '☢️',
+    DELIM + r'biohazard' + DELIM: '☣️',
+
+    #
+    # Arrow
+    #
+    DELIM + r'arrow_up' + DELIM: '⬆️',
+    DELIM + r'arrow_upper_right' + DELIM: '↗️',
+    DELIM + r'arrow_right' + DELIM: '➡️',
+    DELIM + r'arrow_lower_right' + DELIM: '↘️',
+    DELIM + r'arrow_down' + DELIM: '⬇️',
+    DELIM + r'arrow_lower_left' + DELIM: '↙️',
+    DELIM + r'arrow_left' + DELIM: '⬅️',
+    DELIM + r'arrow_upper_left' + DELIM: '↖️',
+    DELIM + r'arrow_up_down' + DELIM: '↕️',
+    DELIM + r'left_right_arrow' + DELIM: '↔️',
+    DELIM + r'leftwards_arrow_with_hook' + DELIM: '↩️',
+    DELIM + r'arrow_right_hook' + DELIM: '↪️',
+    DELIM + r'arrow_heading_up' + DELIM: '⤴️',
+    DELIM + r'arrow_heading_down' + DELIM: '⤵️',
+    DELIM + r'arrows_clockwise' + DELIM: '🔃',
+    DELIM + r'arrows_counterclockwise' + DELIM: '🔄',
+    DELIM + r'back' + DELIM: '🔙',
+    DELIM + r'end' + DELIM: '🔚',
+    DELIM + r'on' + DELIM: '🔛',
+    DELIM + r'soon' + DELIM: '🔜',
+    DELIM + r'top' + DELIM: '🔝',
+
+    #
+    # Religion
+    #
+    DELIM + r'place_of_worship' + DELIM: '🛐',
+    DELIM + r'atom_symbol' + DELIM: '⚛️',
+    DELIM + r'om' + DELIM: '🕉️',
+    DELIM + r'star_of_david' + DELIM: '✡️',
+    DELIM + r'wheel_of_dharma' + DELIM: '☸️',
+    DELIM + r'yin_yang' + DELIM: '☯️',
+    DELIM + r'latin_cross' + DELIM: '✝️',
+    DELIM + r'orthodox_cross' + DELIM: '☦️',
+    DELIM + r'star_and_crescent' + DELIM: '☪️',
+    DELIM + r'peace_symbol' + DELIM: '☮️',
+    DELIM + r'menorah' + DELIM: '🕎',
+    DELIM + r'six_pointed_star' + DELIM: '🔯',
+
+    #
+    # Zodiac
+    #
+    DELIM + r'aries' + DELIM: '♈',
+    DELIM + r'taurus' + DELIM: '♉',
+    DELIM + r'gemini' + DELIM: '♊',
+    DELIM + r'cancer' + DELIM: '♋',
+    DELIM + r'leo' + DELIM: '♌',
+    DELIM + r'virgo' + DELIM: '♍',
+    DELIM + r'libra' + DELIM: '♎',
+    DELIM + r'scorpius' + DELIM: '♏',
+    DELIM + r'sagittarius' + DELIM: '♐',
+    DELIM + r'capricorn' + DELIM: '♑',
+    DELIM + r'aquarius' + DELIM: '♒',
+    DELIM + r'pisces' + DELIM: '♓',
+    DELIM + r'ophiuchus' + DELIM: '⛎',
+
+    #
+    # Av Symbol
+    #
+    DELIM + r'twisted_rightwards_arrows' + DELIM: '🔀',
+    DELIM + r'repeat' + DELIM: '🔁',
+    DELIM + r'repeat_one' + DELIM: '🔂',
+    DELIM + r'arrow_forward' + DELIM: '▶️',
+    DELIM + r'fast_forward' + DELIM: '⏩',
+    DELIM + r'next_track_button' + DELIM: '⏭️',
+    DELIM + r'play_or_pause_button' + DELIM: '⏯️',
+    DELIM + r'arrow_backward' + DELIM: '◀️',
+    DELIM + r'rewind' + DELIM: '⏪',
+    DELIM + r'previous_track_button' + DELIM: '⏮️',
+    DELIM + r'arrow_up_small' + DELIM: '🔼',
+    DELIM + r'arrow_double_up' + DELIM: '⏫',
+    DELIM + r'arrow_down_small' + DELIM: '🔽',
+    DELIM + r'arrow_double_down' + DELIM: '⏬',
+    DELIM + r'pause_button' + DELIM: '⏸️',
+    DELIM + r'stop_button' + DELIM: '⏹️',
+    DELIM + r'record_button' + DELIM: '⏺️',
+    DELIM + r'eject_button' + DELIM: '⏏️',
+    DELIM + r'cinema' + DELIM: '🎦',
+    DELIM + r'low_brightness' + DELIM: '🔅',
+    DELIM + r'high_brightness' + DELIM: '🔆',
+    DELIM + r'signal_strength' + DELIM: '📶',
+    DELIM + r'vibration_mode' + DELIM: '📳',
+    DELIM + r'mobile_phone_off' + DELIM: '📴',
+
+    #
+    # Gender
+    #
+    DELIM + r'female_sign' + DELIM: '♀️',
+    DELIM + r'male_sign' + DELIM: '♂️',
+    DELIM + r'transgender_symbol' + DELIM: '⚧️',
+
+    #
+    # Math
+    #
+    DELIM + r'heavy_multiplication_x' + DELIM: '✖️',
+    DELIM + r'heavy_plus_sign' + DELIM: '➕',
+    DELIM + r'heavy_minus_sign' + DELIM: '➖',
+    DELIM + r'heavy_division_sign' + DELIM: '➗',
+    DELIM + r'infinity' + DELIM: '♾️',
+
+    #
+    # Punctuation
+    #
+    DELIM + r'bangbang' + DELIM: '‼️',
+    DELIM + r'interrobang' + DELIM: '⁉️',
+    DELIM + r'question' + DELIM: '❓',
+    DELIM + r'grey_question' + DELIM: '❔',
+    DELIM + r'grey_exclamation' + DELIM: '❕',
+    DELIM + r'(heavy_exclamation_mark|exclamation)' + DELIM: '❗',
+    DELIM + r'wavy_dash' + DELIM: '〰️',
+
+    #
+    # Currency
+    #
+    DELIM + r'currency_exchange' + DELIM: '💱',
+    DELIM + r'heavy_dollar_sign' + DELIM: '💲',
+
+    #
+    # Other Symbol
+    #
+    DELIM + r'medical_symbol' + DELIM: '⚕️',
+    DELIM + r'recycle' + DELIM: '♻️',
+    DELIM + r'fleur_de_lis' + DELIM: '⚜️',
+    DELIM + r'trident' + DELIM: '🔱',
+    DELIM + r'name_badge' + DELIM: '📛',
+    DELIM + r'beginner' + DELIM: '🔰',
+    DELIM + r'o' + DELIM: '⭕',
+    DELIM + r'white_check_mark' + DELIM: '✅',
+    DELIM + r'ballot_box_with_check' + DELIM: '☑️',
+    DELIM + r'heavy_check_mark' + DELIM: '✔️',
+    DELIM + r'x' + DELIM: '❌',
+    DELIM + r'negative_squared_cross_mark' + DELIM: '❎',
+    DELIM + r'curly_loop' + DELIM: '➰',
+    DELIM + r'loop' + DELIM: '➿',
+    DELIM + r'part_alternation_mark' + DELIM: '〽️',
+    DELIM + r'eight_spoked_asterisk' + DELIM: '✳️',
+    DELIM + r'eight_pointed_black_star' + DELIM: '✴️',
+    DELIM + r'sparkle' + DELIM: '❇️',
+    DELIM + r'copyright' + DELIM: '©️',
+    DELIM + r'registered' + DELIM: '®️',
+    DELIM + r'tm' + DELIM: '™️',
+
+    #
+    # Keycap
+    #
+    DELIM + r'hash' + DELIM: '#️⃣',
+    DELIM + r'asterisk' + DELIM: '*️⃣',
+    DELIM + r'zero' + DELIM: '0️⃣',
+    DELIM + r'one' + DELIM: '1️⃣',
+    DELIM + r'two' + DELIM: '2️⃣',
+    DELIM + r'three' + DELIM: '3️⃣',
+    DELIM + r'four' + DELIM: '4️⃣',
+    DELIM + r'five' + DELIM: '5️⃣',
+    DELIM + r'six' + DELIM: '6️⃣',
+    DELIM + r'seven' + DELIM: '7️⃣',
+    DELIM + r'eight' + DELIM: '8️⃣',
+    DELIM + r'nine' + DELIM: '9️⃣',
+    DELIM + r'keycap_ten' + DELIM: '🔟',
+
+    #
+    # Alphanum
+    #
+    DELIM + r'capital_abcd' + DELIM: '🔠',
+    DELIM + r'abcd' + DELIM: '🔡',
+    DELIM + r'1234' + DELIM: '🔢',
+    DELIM + r'symbols' + DELIM: '🔣',
+    DELIM + r'abc' + DELIM: '🔤',
+    DELIM + r'a' + DELIM: '🅰️',
+    DELIM + r'ab' + DELIM: '🆎',
+    DELIM + r'b' + DELIM: '🅱️',
+    DELIM + r'cl' + DELIM: '🆑',
+    DELIM + r'cool' + DELIM: '🆒',
+    DELIM + r'free' + DELIM: '🆓',
+    DELIM + r'information_source' + DELIM: 'ℹ️',
+    DELIM + r'id' + DELIM: '🆔',
+    DELIM + r'm' + DELIM: 'Ⓜ️',
+    DELIM + r'new' + DELIM: '🆕',
+    DELIM + r'ng' + DELIM: '🆖',
+    DELIM + r'o2' + DELIM: '🅾️',
+    DELIM + r'ok' + DELIM: '🆗',
+    DELIM + r'parking' + DELIM: '🅿️',
+    DELIM + r'sos' + DELIM: '🆘',
+    DELIM + r'up' + DELIM: '🆙',
+    DELIM + r'vs' + DELIM: '🆚',
+    DELIM + r'koko' + DELIM: '🈁',
+    DELIM + r'sa' + DELIM: '🈂️',
+    DELIM + r'u6708' + DELIM: '🈷️',
+    DELIM + r'u6709' + DELIM: '🈶',
+    DELIM + r'u6307' + DELIM: '🈯',
+    DELIM + r'ideograph_advantage' + DELIM: '🉐',
+    DELIM + r'u5272' + DELIM: '🈹',
+    DELIM + r'u7121' + DELIM: '🈚',
+    DELIM + r'u7981' + DELIM: '🈲',
+    DELIM + r'accept' + DELIM: '🉑',
+    DELIM + r'u7533' + DELIM: '🈸',
+    DELIM + r'u5408' + DELIM: '🈴',
+    DELIM + r'u7a7a' + DELIM: '🈳',
+    DELIM + r'congratulations' + DELIM: '㊗️',
+    DELIM + r'secret' + DELIM: '㊙️',
+    DELIM + r'u55b6' + DELIM: '🈺',
+    DELIM + r'u6e80' + DELIM: '🈵',
+
+    #
+    # Geometric
+    #
+    DELIM + r'red_circle' + DELIM: '🔴',
+    DELIM + r'orange_circle' + DELIM: '🟠',
+    DELIM + r'yellow_circle' + DELIM: '🟡',
+    DELIM + r'green_circle' + DELIM: '🟢',
+    DELIM + r'large_blue_circle' + DELIM: '🔵',
+    DELIM + r'purple_circle' + DELIM: '🟣',
+    DELIM + r'brown_circle' + DELIM: '🟤',
+    DELIM + r'black_circle' + DELIM: '⚫',
+    DELIM + r'white_circle' + DELIM: '⚪',
+    DELIM + r'red_square' + DELIM: '🟥',
+    DELIM + r'orange_square' + DELIM: '🟧',
+    DELIM + r'yellow_square' + DELIM: '🟨',
+    DELIM + r'green_square' + DELIM: '🟩',
+    DELIM + r'blue_square' + DELIM: '🟦',
+    DELIM + r'purple_square' + DELIM: '🟪',
+    DELIM + r'brown_square' + DELIM: '🟫',
+    DELIM + r'black_large_square' + DELIM: '⬛',
+    DELIM + r'white_large_square' + DELIM: '⬜',
+    DELIM + r'black_medium_square' + DELIM: '◼️',
+    DELIM + r'white_medium_square' + DELIM: '◻️',
+    DELIM + r'black_medium_small_square' + DELIM: '◾',
+    DELIM + r'white_medium_small_square' + DELIM: '◽',
+    DELIM + r'black_small_square' + DELIM: '▪️',
+    DELIM + r'white_small_square' + DELIM: '▫️',
+    DELIM + r'large_orange_diamond' + DELIM: '🔶',
+    DELIM + r'large_blue_diamond' + DELIM: '🔷',
+    DELIM + r'small_orange_diamond' + DELIM: '🔸',
+    DELIM + r'small_blue_diamond' + DELIM: '🔹',
+    DELIM + r'small_red_triangle' + DELIM: '🔺',
+    DELIM + r'small_red_triangle_down' + DELIM: '🔻',
+    DELIM + r'diamond_shape_with_a_dot_inside' + DELIM: '💠',
+    DELIM + r'radio_button' + DELIM: '🔘',
+    DELIM + r'white_square_button' + DELIM: '🔳',
+    DELIM + r'black_square_button' + DELIM: '🔲',
+
+    #
+    # Flag
+    #
+    DELIM + r'checkered_flag' + DELIM: '🏁',
+    DELIM + r'triangular_flag_on_post' + DELIM: '🚩',
+    DELIM + r'crossed_flags' + DELIM: '🎌',
+    DELIM + r'black_flag' + DELIM: '🏴',
+    DELIM + r'white_flag' + DELIM: '🏳️',
+    DELIM + r'rainbow_flag' + DELIM: '🏳️‍🌈',
+    DELIM + r'transgender_flag' + DELIM: '🏳️‍⚧️',
+    DELIM + r'pirate_flag' + DELIM: '🏴‍☠️',
+
+    #
+    # Country Flag
+    #
+    DELIM + r'ascension_island' + DELIM: '🇦🇨',
+    DELIM + r'andorra' + DELIM: '🇦🇩',
+    DELIM + r'united_arab_emirates' + DELIM: '🇦🇪',
+    DELIM + r'afghanistan' + DELIM: '🇦🇫',
+    DELIM + r'antigua_barbuda' + DELIM: '🇦🇬',
+    DELIM + r'anguilla' + DELIM: '🇦🇮',
+    DELIM + r'albania' + DELIM: '🇦🇱',
+    DELIM + r'armenia' + DELIM: '🇦🇲',
+    DELIM + r'angola' + DELIM: '🇦🇴',
+    DELIM + r'antarctica' + DELIM: '🇦🇶',
+    DELIM + r'argentina' + DELIM: '🇦🇷',
+    DELIM + r'american_samoa' + DELIM: '🇦🇸',
+    DELIM + r'austria' + DELIM: '🇦🇹',
+    DELIM + r'australia' + DELIM: '🇦🇺',
+    DELIM + r'aruba' + DELIM: '🇦🇼',
+    DELIM + r'aland_islands' + DELIM: '🇦🇽',
+    DELIM + r'azerbaijan' + DELIM: '🇦🇿',
+    DELIM + r'bosnia_herzegovina' + DELIM: '🇧🇦',
+    DELIM + r'barbados' + DELIM: '🇧🇧',
+    DELIM + r'bangladesh' + DELIM: '🇧🇩',
+    DELIM + r'belgium' + DELIM: '🇧🇪',
+    DELIM + r'burkina_faso' + DELIM: '🇧🇫',
+    DELIM + r'bulgaria' + DELIM: '🇧🇬',
+    DELIM + r'bahrain' + DELIM: '🇧🇭',
+    DELIM + r'burundi' + DELIM: '🇧🇮',
+    DELIM + r'benin' + DELIM: '🇧🇯',
+    DELIM + r'st_barthelemy' + DELIM: '🇧🇱',
+    DELIM + r'bermuda' + DELIM: '🇧🇲',
+    DELIM + r'brunei' + DELIM: '🇧🇳',
+    DELIM + r'bolivia' + DELIM: '🇧🇴',
+    DELIM + r'caribbean_netherlands' + DELIM: '🇧🇶',
+    DELIM + r'brazil' + DELIM: '🇧🇷',
+    DELIM + r'bahamas' + DELIM: '🇧🇸',
+    DELIM + r'bhutan' + DELIM: '🇧🇹',
+    DELIM + r'bouvet_island' + DELIM: '🇧🇻',
+    DELIM + r'botswana' + DELIM: '🇧🇼',
+    DELIM + r'belarus' + DELIM: '🇧🇾',
+    DELIM + r'belize' + DELIM: '🇧🇿',
+    DELIM + r'canada' + DELIM: '🇨🇦',
+    DELIM + r'cocos_islands' + DELIM: '🇨🇨',
+    DELIM + r'congo_kinshasa' + DELIM: '🇨🇩',
+    DELIM + r'central_african_republic' + DELIM: '🇨🇫',
+    DELIM + r'congo_brazzaville' + DELIM: '🇨🇬',
+    DELIM + r'switzerland' + DELIM: '🇨🇭',
+    DELIM + r'cote_divoire' + DELIM: '🇨🇮',
+    DELIM + r'cook_islands' + DELIM: '🇨🇰',
+    DELIM + r'chile' + DELIM: '🇨🇱',
+    DELIM + r'cameroon' + DELIM: '🇨🇲',
+    DELIM + r'cn' + DELIM: '🇨🇳',
+    DELIM + r'colombia' + DELIM: '🇨🇴',
+    DELIM + r'clipperton_island' + DELIM: '🇨🇵',
+    DELIM + r'costa_rica' + DELIM: '🇨🇷',
+    DELIM + r'cuba' + DELIM: '🇨🇺',
+    DELIM + r'cape_verde' + DELIM: '🇨🇻',
+    DELIM + r'curacao' + DELIM: '🇨🇼',
+    DELIM + r'christmas_island' + DELIM: '🇨🇽',
+    DELIM + r'cyprus' + DELIM: '🇨🇾',
+    DELIM + r'czech_republic' + DELIM: '🇨🇿',
+    DELIM + r'de' + DELIM: '🇩🇪',
+    DELIM + r'diego_garcia' + DELIM: '🇩🇬',
+    DELIM + r'djibouti' + DELIM: '🇩🇯',
+    DELIM + r'denmark' + DELIM: '🇩🇰',
+    DELIM + r'dominica' + DELIM: '🇩🇲',
+    DELIM + r'dominican_republic' + DELIM: '🇩🇴',
+    DELIM + r'algeria' + DELIM: '🇩🇿',
+    DELIM + r'ceuta_melilla' + DELIM: '🇪🇦',
+    DELIM + r'ecuador' + DELIM: '🇪🇨',
+    DELIM + r'estonia' + DELIM: '🇪🇪',
+    DELIM + r'egypt' + DELIM: '🇪🇬',
+    DELIM + r'western_sahara' + DELIM: '🇪🇭',
+    DELIM + r'eritrea' + DELIM: '🇪🇷',
+    DELIM + r'es' + DELIM: '🇪🇸',
+    DELIM + r'ethiopia' + DELIM: '🇪🇹',
+    DELIM + r'(eu|european_union)' + DELIM: '🇪🇺',
+    DELIM + r'finland' + DELIM: '🇫🇮',
+    DELIM + r'fiji' + DELIM: '🇫🇯',
+    DELIM + r'falkland_islands' + DELIM: '🇫🇰',
+    DELIM + r'micronesia' + DELIM: '🇫🇲',
+    DELIM + r'faroe_islands' + DELIM: '🇫🇴',
+    DELIM + r'fr' + DELIM: '🇫🇷',
+    DELIM + r'gabon' + DELIM: '🇬🇦',
+    DELIM + r'(uk|gb)' + DELIM: '🇬🇧',
+    DELIM + r'grenada' + DELIM: '🇬🇩',
+    DELIM + r'georgia' + DELIM: '🇬🇪',
+    DELIM + r'french_guiana' + DELIM: '🇬🇫',
+    DELIM + r'guernsey' + DELIM: '🇬🇬',
+    DELIM + r'ghana' + DELIM: '🇬🇭',
+    DELIM + r'gibraltar' + DELIM: '🇬🇮',
+    DELIM + r'greenland' + DELIM: '🇬🇱',
+    DELIM + r'gambia' + DELIM: '🇬🇲',
+    DELIM + r'guinea' + DELIM: '🇬🇳',
+    DELIM + r'guadeloupe' + DELIM: '🇬🇵',
+    DELIM + r'equatorial_guinea' + DELIM: '🇬🇶',
+    DELIM + r'greece' + DELIM: '🇬🇷',
+    DELIM + r'south_georgia_south_sandwich_islands' + DELIM: '🇬🇸',
+    DELIM + r'guatemala' + DELIM: '🇬🇹',
+    DELIM + r'guam' + DELIM: '🇬🇺',
+    DELIM + r'guinea_bissau' + DELIM: '🇬🇼',
+    DELIM + r'guyana' + DELIM: '🇬🇾',
+    DELIM + r'hong_kong' + DELIM: '🇭🇰',
+    DELIM + r'heard_mcdonald_islands' + DELIM: '🇭🇲',
+    DELIM + r'honduras' + DELIM: '🇭🇳',
+    DELIM + r'croatia' + DELIM: '🇭🇷',
+    DELIM + r'haiti' + DELIM: '🇭🇹',
+    DELIM + r'hungary' + DELIM: '🇭🇺',
+    DELIM + r'canary_islands' + DELIM: '🇮🇨',
+    DELIM + r'indonesia' + DELIM: '🇮🇩',
+    DELIM + r'ireland' + DELIM: '🇮🇪',
+    DELIM + r'israel' + DELIM: '🇮🇱',
+    DELIM + r'isle_of_man' + DELIM: '🇮🇲',
+    DELIM + r'india' + DELIM: '🇮🇳',
+    DELIM + r'british_indian_ocean_territory' + DELIM: '🇮🇴',
+    DELIM + r'iraq' + DELIM: '🇮🇶',
+    DELIM + r'iran' + DELIM: '🇮🇷',
+    DELIM + r'iceland' + DELIM: '🇮🇸',
+    DELIM + r'it' + DELIM: '🇮🇹',
+    DELIM + r'jersey' + DELIM: '🇯🇪',
+    DELIM + r'jamaica' + DELIM: '🇯🇲',
+    DELIM + r'jordan' + DELIM: '🇯🇴',
+    DELIM + r'jp' + DELIM: '🇯🇵',
+    DELIM + r'kenya' + DELIM: '🇰🇪',
+    DELIM + r'kyrgyzstan' + DELIM: '🇰🇬',
+    DELIM + r'cambodia' + DELIM: '🇰🇭',
+    DELIM + r'kiribati' + DELIM: '🇰🇮',
+    DELIM + r'comoros' + DELIM: '🇰🇲',
+    DELIM + r'st_kitts_nevis' + DELIM: '🇰🇳',
+    DELIM + r'north_korea' + DELIM: '🇰🇵',
+    DELIM + r'kr' + DELIM: '🇰🇷',
+    DELIM + r'kuwait' + DELIM: '🇰🇼',
+    DELIM + r'cayman_islands' + DELIM: '🇰🇾',
+    DELIM + r'kazakhstan' + DELIM: '🇰🇿',
+    DELIM + r'laos' + DELIM: '🇱🇦',
+    DELIM + r'lebanon' + DELIM: '🇱🇧',
+    DELIM + r'st_lucia' + DELIM: '🇱🇨',
+    DELIM + r'liechtenstein' + DELIM: '🇱🇮',
+    DELIM + r'sri_lanka' + DELIM: '🇱🇰',
+    DELIM + r'liberia' + DELIM: '🇱🇷',
+    DELIM + r'lesotho' + DELIM: '🇱🇸',
+    DELIM + r'lithuania' + DELIM: '🇱🇹',
+    DELIM + r'luxembourg' + DELIM: '🇱🇺',
+    DELIM + r'latvia' + DELIM: '🇱🇻',
+    DELIM + r'libya' + DELIM: '🇱🇾',
+    DELIM + r'morocco' + DELIM: '🇲🇦',
+    DELIM + r'monaco' + DELIM: '🇲🇨',
+    DELIM + r'moldova' + DELIM: '🇲🇩',
+    DELIM + r'montenegro' + DELIM: '🇲🇪',
+    DELIM + r'st_martin' + DELIM: '🇲🇫',
+    DELIM + r'madagascar' + DELIM: '🇲🇬',
+    DELIM + r'marshall_islands' + DELIM: '🇲🇭',
+    DELIM + r'macedonia' + DELIM: '🇲🇰',
+    DELIM + r'mali' + DELIM: '🇲🇱',
+    DELIM + r'myanmar' + DELIM: '🇲🇲',
+    DELIM + r'mongolia' + DELIM: '🇲🇳',
+    DELIM + r'macau' + DELIM: '🇲🇴',
+    DELIM + r'northern_mariana_islands' + DELIM: '🇲🇵',
+    DELIM + r'martinique' + DELIM: '🇲🇶',
+    DELIM + r'mauritania' + DELIM: '🇲🇷',
+    DELIM + r'montserrat' + DELIM: '🇲🇸',
+    DELIM + r'malta' + DELIM: '🇲🇹',
+    DELIM + r'mauritius' + DELIM: '🇲🇺',
+    DELIM + r'maldives' + DELIM: '🇲🇻',
+    DELIM + r'malawi' + DELIM: '🇲🇼',
+    DELIM + r'mexico' + DELIM: '🇲🇽',
+    DELIM + r'malaysia' + DELIM: '🇲🇾',
+    DELIM + r'mozambique' + DELIM: '🇲🇿',
+    DELIM + r'namibia' + DELIM: '🇳🇦',
+    DELIM + r'new_caledonia' + DELIM: '🇳🇨',
+    DELIM + r'niger' + DELIM: '🇳🇪',
+    DELIM + r'norfolk_island' + DELIM: '🇳🇫',
+    DELIM + r'nigeria' + DELIM: '🇳🇬',
+    DELIM + r'nicaragua' + DELIM: '🇳🇮',
+    DELIM + r'netherlands' + DELIM: '🇳🇱',
+    DELIM + r'norway' + DELIM: '🇳🇴',
+    DELIM + r'nepal' + DELIM: '🇳🇵',
+    DELIM + r'nauru' + DELIM: '🇳🇷',
+    DELIM + r'niue' + DELIM: '🇳🇺',
+    DELIM + r'new_zealand' + DELIM: '🇳🇿',
+    DELIM + r'oman' + DELIM: '🇴🇲',
+    DELIM + r'panama' + DELIM: '🇵🇦',
+    DELIM + r'peru' + DELIM: '🇵🇪',
+    DELIM + r'french_polynesia' + DELIM: '🇵🇫',
+    DELIM + r'papua_new_guinea' + DELIM: '🇵🇬',
+    DELIM + r'philippines' + DELIM: '🇵🇭',
+    DELIM + r'pakistan' + DELIM: '🇵🇰',
+    DELIM + r'poland' + DELIM: '🇵🇱',
+    DELIM + r'st_pierre_miquelon' + DELIM: '🇵🇲',
+    DELIM + r'pitcairn_islands' + DELIM: '🇵🇳',
+    DELIM + r'puerto_rico' + DELIM: '🇵🇷',
+    DELIM + r'palestinian_territories' + DELIM: '🇵🇸',
+    DELIM + r'portugal' + DELIM: '🇵🇹',
+    DELIM + r'palau' + DELIM: '🇵🇼',
+    DELIM + r'paraguay' + DELIM: '🇵🇾',
+    DELIM + r'qatar' + DELIM: '🇶🇦',
+    DELIM + r'reunion' + DELIM: '🇷🇪',
+    DELIM + r'romania' + DELIM: '🇷🇴',
+    DELIM + r'serbia' + DELIM: '🇷🇸',
+    DELIM + r'ru' + DELIM: '🇷🇺',
+    DELIM + r'rwanda' + DELIM: '🇷🇼',
+    DELIM + r'saudi_arabia' + DELIM: '🇸🇦',
+    DELIM + r'solomon_islands' + DELIM: '🇸🇧',
+    DELIM + r'seychelles' + DELIM: '🇸🇨',
+    DELIM + r'sudan' + DELIM: '🇸🇩',
+    DELIM + r'sweden' + DELIM: '🇸🇪',
+    DELIM + r'singapore' + DELIM: '🇸🇬',
+    DELIM + r'st_helena' + DELIM: '🇸🇭',
+    DELIM + r'slovenia' + DELIM: '🇸🇮',
+    DELIM + r'svalbard_jan_mayen' + DELIM: '🇸🇯',
+    DELIM + r'slovakia' + DELIM: '🇸🇰',
+    DELIM + r'sierra_leone' + DELIM: '🇸🇱',
+    DELIM + r'san_marino' + DELIM: '🇸🇲',
+    DELIM + r'senegal' + DELIM: '🇸🇳',
+    DELIM + r'somalia' + DELIM: '🇸🇴',
+    DELIM + r'suriname' + DELIM: '🇸🇷',
+    DELIM + r'south_sudan' + DELIM: '🇸🇸',
+    DELIM + r'sao_tome_principe' + DELIM: '🇸🇹',
+    DELIM + r'el_salvador' + DELIM: '🇸🇻',
+    DELIM + r'sint_maarten' + DELIM: '🇸🇽',
+    DELIM + r'syria' + DELIM: '🇸🇾',
+    DELIM + r'swaziland' + DELIM: '🇸🇿',
+    DELIM + r'tristan_da_cunha' + DELIM: '🇹🇦',
+    DELIM + r'turks_caicos_islands' + DELIM: '🇹🇨',
+    DELIM + r'chad' + DELIM: '🇹🇩',
+    DELIM + r'french_southern_territories' + DELIM: '🇹🇫',
+    DELIM + r'togo' + DELIM: '🇹🇬',
+    DELIM + r'thailand' + DELIM: '🇹🇭',
+    DELIM + r'tajikistan' + DELIM: '🇹🇯',
+    DELIM + r'tokelau' + DELIM: '🇹🇰',
+    DELIM + r'timor_leste' + DELIM: '🇹🇱',
+    DELIM + r'turkmenistan' + DELIM: '🇹🇲',
+    DELIM + r'tunisia' + DELIM: '🇹🇳',
+    DELIM + r'tonga' + DELIM: '🇹🇴',
+    DELIM + r'tr' + DELIM: '🇹🇷',
+    DELIM + r'trinidad_tobago' + DELIM: '🇹🇹',
+    DELIM + r'tuvalu' + DELIM: '🇹🇻',
+    DELIM + r'taiwan' + DELIM: '🇹🇼',
+    DELIM + r'tanzania' + DELIM: '🇹🇿',
+    DELIM + r'ukraine' + DELIM: '🇺🇦',
+    DELIM + r'uganda' + DELIM: '🇺🇬',
+    DELIM + r'us_outlying_islands' + DELIM: '🇺🇲',
+    DELIM + r'united_nations' + DELIM: '🇺🇳',
+    DELIM + r'us' + DELIM: '🇺🇸',
+    DELIM + r'uruguay' + DELIM: '🇺🇾',
+    DELIM + r'uzbekistan' + DELIM: '🇺🇿',
+    DELIM + r'vatican_city' + DELIM: '🇻🇦',
+    DELIM + r'st_vincent_grenadines' + DELIM: '🇻🇨',
+    DELIM + r'venezuela' + DELIM: '🇻🇪',
+    DELIM + r'british_virgin_islands' + DELIM: '🇻🇬',
+    DELIM + r'us_virgin_islands' + DELIM: '🇻🇮',
+    DELIM + r'vietnam' + DELIM: '🇻🇳',
+    DELIM + r'vanuatu' + DELIM: '🇻🇺',
+    DELIM + r'wallis_futuna' + DELIM: '🇼🇫',
+    DELIM + r'samoa' + DELIM: '🇼🇸',
+    DELIM + r'kosovo' + DELIM: '🇽🇰',
+    DELIM + r'yemen' + DELIM: '🇾🇪',
+    DELIM + r'mayotte' + DELIM: '🇾🇹',
+    DELIM + r'south_africa' + DELIM: '🇿🇦',
+    DELIM + r'zambia' + DELIM: '🇿🇲',
+    DELIM + r'zimbabwe' + DELIM: '🇿🇼',
+
+    #
+    # Subdivision Flag
+    #
+    DELIM + r'england' + DELIM: '🏴󠁧󠁢󠁥󠁮󠁧󠁿',
+    DELIM + r'scotland' + DELIM: '🏴󠁧󠁢󠁳󠁣󠁴󠁿',
+    DELIM + r'wales' + DELIM: '🏴󠁧󠁢󠁷󠁬󠁳󠁿',
+}
+
+# Define our singlton
+EMOJI_COMPILED_MAP = None
+
+
+def apply_emojis(content):
+    """
+    Takes the content and swaps any matched emoji's found with their
+    utf-8 encoded mapping
+    """
+
+    global EMOJI_COMPILED_MAP
+
+    if EMOJI_COMPILED_MAP is None:
+        t_start = time.time()
+        # Perform our compilation
+        EMOJI_COMPILED_MAP = re.compile(
+            r'(' + '|'.join(EMOJI_MAP.keys()) + r')',
+            re.IGNORECASE)
+        logger.trace(
+            'Emoji engine loaded in {:.4f}s'.format((time.time() - t_start)))
+
+    try:
+        return EMOJI_COMPILED_MAP.sub(lambda x: EMOJI_MAP[x.group()], content)
+
+    except TypeError:
+        # No change; but force string return
+        return ''
diff --git a/lib/apprise/i18n/apprise.pot b/lib/apprise/i18n/apprise.pot
index 434ce91d..f81df84d 100644
--- a/lib/apprise/i18n/apprise.pot
+++ b/lib/apprise/i18n/apprise.pot
@@ -1,21 +1,21 @@
 # Translations template for apprise.
-# Copyright (C) 2023 Chris Caron
+# Copyright (C) 2024 Chris Caron
 # This file is distributed under the same license as the apprise project.
-# FIRST AUTHOR , 2023.
+# FIRST AUTHOR , 2024.
 #
 #, fuzzy
 msgid ""
 msgstr ""
-"Project-Id-Version: apprise 1.6.0\n"
+"Project-Id-Version: apprise 1.8.0\n"
 "Report-Msgid-Bugs-To: lead2gold@gmail.com\n"
-"POT-Creation-Date: 2023-10-15 15:56-0400\n"
+"POT-Creation-Date: 2024-05-11 16:13-0400\n"
 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
 "Last-Translator: FULL NAME \n"
 "Language-Team: LANGUAGE \n"
 "MIME-Version: 1.0\n"
 "Content-Type: text/plain; charset=utf-8\n"
 "Content-Transfer-Encoding: 8bit\n"
-"Generated-By: Babel 2.11.0\n"
+"Generated-By: Babel 2.13.1\n"
 
 msgid "A local Gnome environment is required."
 msgstr ""
@@ -32,6 +32,9 @@ msgstr ""
 msgid "API Secret"
 msgstr ""
 
+msgid "API Token"
+msgstr ""
+
 msgid "Access Key"
 msgstr ""
 
@@ -101,9 +104,6 @@ msgstr ""
 msgid "Authentication Type"
 msgstr ""
 
-msgid "Authorization Token"
-msgstr ""
-
 msgid "Avatar Image"
 msgstr ""
 
@@ -125,6 +125,9 @@ msgstr ""
 msgid "Bot Token"
 msgstr ""
 
+msgid "Bot Webhook Key"
+msgstr ""
+
 msgid "Cache Age"
 msgstr ""
 
@@ -140,9 +143,15 @@ msgstr ""
 msgid "Category"
 msgstr ""
 
+msgid "Channel ID"
+msgstr ""
+
 msgid "Channels"
 msgstr ""
 
+msgid "Chantify"
+msgstr ""
+
 msgid "Class"
 msgstr ""
 
@@ -230,6 +239,9 @@ msgstr ""
 msgid "Email Header"
 msgstr ""
 
+msgid "Embed URL"
+msgstr ""
+
 msgid "Entity"
 msgstr ""
 
@@ -245,6 +257,9 @@ msgstr ""
 msgid "Facility"
 msgstr ""
 
+msgid "Feishu"
+msgstr ""
+
 msgid "Fetch Method"
 msgstr ""
 
@@ -266,6 +281,9 @@ msgstr ""
 msgid "Forced Mime Type"
 msgstr ""
 
+msgid "Free-Mobile"
+msgstr ""
+
 msgid "From Email"
 msgstr ""
 
@@ -281,6 +299,12 @@ msgstr ""
 msgid "GET Params"
 msgstr ""
 
+msgid "Gateway"
+msgstr ""
+
+msgid "Gateway ID"
+msgstr ""
+
 msgid "Gnome Notification"
 msgstr ""
 
@@ -299,6 +323,9 @@ msgstr ""
 msgid "Icon Type"
 msgstr ""
 
+msgid "Icon URL"
+msgstr ""
+
 msgid "Idempotency-Key"
 msgstr ""
 
@@ -323,6 +350,9 @@ msgstr ""
 msgid "Integration Key"
 msgstr ""
 
+msgid "Interpret Emojis"
+msgstr ""
+
 msgid "Is Ad?"
 msgstr ""
 
@@ -344,6 +374,9 @@ msgstr ""
 msgid "Local File"
 msgstr ""
 
+msgid "Locale"
+msgstr ""
+
 msgid "Log PID"
 msgstr ""
 
@@ -356,6 +389,9 @@ msgstr ""
 msgid "MacOSX Notification"
 msgstr ""
 
+msgid "Markdown Version"
+msgstr ""
+
 msgid "Master Key"
 msgstr ""
 
@@ -490,6 +526,9 @@ msgstr ""
 msgid "Reply To Email"
 msgstr ""
 
+msgid "Resend Delay"
+msgstr ""
+
 msgid "Resubmit Flag"
 msgstr ""
 
@@ -661,6 +700,9 @@ msgstr ""
 msgid "Target Team"
 msgstr ""
 
+msgid "Target Threema ID"
+msgstr ""
+
 msgid "Target Topic"
 msgstr ""
 
@@ -757,6 +799,9 @@ msgstr ""
 msgid "Unicode Characters"
 msgstr ""
 
+msgid "Upload"
+msgstr ""
+
 msgid "Urgency"
 msgstr ""
 
@@ -775,9 +820,6 @@ msgstr ""
 msgid "User Email"
 msgstr ""
 
-msgid "User ID"
-msgstr ""
-
 msgid "User Key"
 msgstr ""
 
diff --git a/lib/apprise/AppriseLocale.py b/lib/apprise/locale.py
similarity index 97%
rename from lib/apprise/AppriseLocale.py
rename to lib/apprise/locale.py
index c80afae2..e900ce5b 100644
--- a/lib/apprise/AppriseLocale.py
+++ b/lib/apprise/locale.py
@@ -2,7 +2,7 @@
 # BSD 2-Clause License
 #
 # Apprise - Push Notification Library.
-# Copyright (c) 2023, Chris Caron 
+# Copyright (c) 2024, Chris Caron 
 #
 # Redistribution and use in source and binary forms, with or without
 # modification, are permitted provided that the following conditions are met:
@@ -219,6 +219,9 @@ class AppriseLocale:
             try:
                 # Acquire our locale
                 lang = locale.getlocale()[0]
+                # Compatibility for Python >= 3.12
+                if lang == 'C':
+                    lang = AppriseLocale._default_language
 
             except (ValueError, TypeError) as e:
                 # This occurs when an invalid locale was parsed from the
diff --git a/lib/apprise/logger.py b/lib/apprise/logger.py
index 6a594ec6..d9efe47c 100644
--- a/lib/apprise/logger.py
+++ b/lib/apprise/logger.py
@@ -2,7 +2,7 @@
 # BSD 2-Clause License
 #
 # Apprise - Push Notification Library.
-# Copyright (c) 2023, Chris Caron 
+# Copyright (c) 2024, Chris Caron 
 #
 # Redistribution and use in source and binary forms, with or without
 # modification, are permitted provided that the following conditions are met:
diff --git a/lib/apprise/manager.py b/lib/apprise/manager.py
new file mode 100644
index 00000000..70dc1070
--- /dev/null
+++ b/lib/apprise/manager.py
@@ -0,0 +1,756 @@
+# -*- coding: utf-8 -*-
+# BSD 2-Clause License
+#
+# Apprise - Push Notification Library.
+# Copyright (c) 2024, Chris Caron 
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met:
+#
+# 1. Redistributions of source code must retain the above copyright notice,
+#    this list of conditions and the following disclaimer.
+#
+# 2. Redistributions in binary form must reproduce the above copyright notice,
+#    this list of conditions and the following disclaimer in the documentation
+#    and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+
+import os
+import re
+import sys
+import time
+import hashlib
+import inspect
+import threading
+from .utils import import_module
+from .utils import Singleton
+from .utils import parse_list
+from os.path import dirname
+from os.path import abspath
+from os.path import join
+
+from .logger import logger
+
+
+class PluginManager(metaclass=Singleton):
+    """
+    Designed to be a singleton object to maintain all initialized loading
+    of modules in memory.
+    """
+
+    # Description (used for logging)
+    name = 'Singleton Plugin'
+
+    # Memory Space
+    _id = 'undefined'
+
+    # Our Module Python path name
+    module_name_prefix = f'apprise.{_id}'
+
+    # The module path to scan
+    module_path = join(abspath(dirname(__file__)), _id)
+
+    # For filtering our result when scanning a module
+    module_filter_re = re.compile(r'^(?P((?!_)[A-Za-z0-9]+))$')
+
+    # thread safe loading
+    _lock = threading.Lock()
+
+    def __init__(self, *args, **kwargs):
+        """
+        Over-ride our class instantiation to provide a singleton
+        """
+
+        self._module_map = None
+        self._schema_map = None
+
+        # This contains a mapping of all plugins dynamicaly loaded at runtime
+        # from external modules such as the @notify decorator
+        #
+        # The elements here will be additionally added to the _schema_map if
+        # there is no conflict otherwise.
+        # The structure looks like the following:
+        # Module path, e.g. /usr/share/apprise/plugins/my_notify_hook.py
+        # {
+        #   'path': path,
+        #
+        #   'notify': {
+        #     'schema': {
+        #       'name': 'Custom schema name',
+        #       'fn_name': 'name_of_function_decorator_was_found_on',
+        #       'url': 'schema://any/additional/info/found/on/url'
+        #       'plugin': 
+        #    },
+        #     'schema2': {
+        #       'name': 'Custom schema name',
+        #       'fn_name': 'name_of_function_decorator_was_found_on',
+        #       'url': 'schema://any/additional/info/found/on/url'
+        #       'plugin': 
+        #    }
+        #  }
+        # Note: that the  inherits from
+        #       NotifyBase
+        self._custom_module_map = {}
+
+        # Track manually disabled modules (by their schema)
+        self._disabled = set()
+
+        # Hash of all paths previously scanned so we don't waste
+        # effort/overhead doing it again
+        self._paths_previously_scanned = set()
+
+        # Track loaded module paths to prevent from loading them again
+        self._loaded = set()
+
+    def unload_modules(self, disable_native=False):
+        """
+        Reset our object and unload all modules
+        """
+
+        with self._lock:
+            if self._custom_module_map:
+                # Handle Custom Module Assignments
+                for meta in self._custom_module_map.values():
+                    if meta['name'] not in self._module_map:
+                        # Nothing to remove
+                        continue
+
+                    # For the purpose of tidying up un-used modules in memory
+                    loaded = [m for m in sys.modules.keys()
+                              if m.startswith(
+                                  self._module_map[meta['name']]['path'])]
+
+                    for module_path in loaded:
+                        del sys.modules[module_path]
+
+            # Reset disabled plugins (if any)
+            for schema in self._disabled:
+                self._schema_map[schema].enabled = True
+            self._disabled.clear()
+
+            # Reset our variables
+            self._schema_map = {}
+            self._custom_module_map = {}
+            if disable_native:
+                self._module_map = {}
+
+            else:
+                self._module_map = None
+                self._loaded = set()
+
+            # Reset our path cache
+            self._paths_previously_scanned = set()
+
+    def load_modules(self, path=None, name=None, force=False):
+        """
+        Load our modules into memory
+        """
+
+        # Default value
+        module_name_prefix = self.module_name_prefix if name is None else name
+        module_path = self.module_path if path is None else path
+
+        with self._lock:
+            if not force and module_path in self._loaded:
+                # We're done
+                return
+
+            # Our base reference
+            module_count = len(self._module_map) if self._module_map else 0
+            schema_count = len(self._schema_map) if self._schema_map else 0
+
+            if not self:
+                # Initialize our maps
+                self._module_map = {}
+                self._schema_map = {}
+                self._custom_module_map = {}
+
+            # Used for the detection of additional Notify Services objects
+            # The .py extension is optional as we support loading directories
+            # too
+            module_re = re.compile(
+                r'^(?P(?!base|_)[a-z0-9_]+)(\.py)?$',
+                re.I)
+
+            t_start = time.time()
+            for f in os.listdir(module_path):
+                tl_start = time.time()
+                match = module_re.match(f)
+                if not match:
+                    # keep going
+                    continue
+
+                # Store our notification/plugin name:
+                module_name = match.group('name')
+                module_pyname = '{}.{}'.format(module_name_prefix, module_name)
+
+                if module_name in self._module_map:
+                    logger.warning(
+                        "%s(s) (%s) already loaded; ignoring %s",
+                        self.name, module_name, os.path.join(module_path, f))
+                    continue
+
+                try:
+                    module = __import__(
+                        module_pyname,
+                        globals(), locals(),
+                        fromlist=[module_name])
+
+                except ImportError:
+                    # No problem, we can try again another way...
+                    module = import_module(
+                        os.path.join(module_path, f), module_pyname)
+                    if not module:
+                        # logging found in import_module and not needed here
+                        continue
+
+                module_class = None
+                for m_class in [obj for obj in dir(module)
+                                if self.module_filter_re.match(obj)]:
+                    # Get our plugin
+                    plugin = getattr(module, m_class)
+                    if not hasattr(plugin, 'app_id'):
+                        # Filter out non-notification modules
+                        logger.trace(
+                            "(%s) import failed; no app_id defined in %s",
+                            self.name, m_class, os.path.join(module_path, f))
+                        continue
+
+                    # Add our plugin name to our module map
+                    self._module_map[module_name] = {
+                        'plugin': set([plugin]),
+                        'module': module,
+                        'path': '{}.{}'.format(
+                            module_name_prefix, module_name),
+                        'native': True,
+                    }
+
+                    fn = getattr(plugin, 'schemas', None)
+                    schemas = set([]) if not callable(fn) else fn(plugin)
+
+                    # map our schema to our plugin
+                    for schema in schemas:
+                        if schema in self._schema_map:
+                            logger.error(
+                                "{} schema ({}) mismatch detected - {} to {}"
+                                .format(self.name, schema, self._schema_map,
+                                        plugin))
+                            continue
+
+                        # Assign plugin
+                        self._schema_map[schema] = plugin
+
+                    # Store our class
+                    module_class = m_class
+                    break
+
+                if not module_class:
+                    # Not a library we can load as it doesn't follow the simple
+                    # rule that the class must bear the same name as the
+                    # notification file itself.
+                    logger.trace(
+                        "%s (%s) import failed; no filename/Class "
+                        "match found in %s",
+                        self.name, module_name, os.path.join(module_path, f))
+                    continue
+
+                logger.trace(
+                    '{} {} loaded in {:.6f}s'.format(
+                        self.name, module_name, (time.time() - tl_start)))
+
+            # Track the directory loaded so we never load it again
+            self._loaded.add(module_path)
+
+            logger.debug(
+                '{} {}(s) and {} Schema(s) loaded in {:.4f}s'
+                .format(
+                    self.name,
+                    len(self._module_map) - module_count,
+                    len(self._schema_map) - schema_count,
+                    (time.time() - t_start)))
+
+    def module_detection(self, paths, cache=True):
+        """
+        Leverage the @notify decorator and load all objects found matching
+        this.
+        """
+        # A simple restriction that we don't allow periods in the filename at
+        # all so it can't be hidden (Linux OS's) and it won't conflict with
+        # Python path naming.  This also prevents us from loading any python
+        # file that starts with an underscore or dash
+        # We allow for __init__.py as well
+        module_re = re.compile(
+            r'^(?P[_a-z0-9][a-z0-9._-]+)?(\.py)?$', re.I)
+
+        # Validate if we're a loadable Python file or not
+        valid_python_file_re = re.compile(r'.+\.py(o|c)?$', re.IGNORECASE)
+
+        if isinstance(paths, str):
+            paths = [paths, ]
+
+        if not paths or not isinstance(paths, (tuple, list)):
+            # We're done
+            return
+
+        def _import_module(path):
+            # Since our plugin name can conflict (as a module) with another
+            # we want to generate random strings to avoid steping on
+            # another's namespace
+            if not (path and valid_python_file_re.match(path)):
+                # Ignore file/module type
+                logger.trace('Plugin Scan: Skipping %s', path)
+                return
+
+            t_start = time.time()
+            module_name = hashlib.sha1(path.encode('utf-8')).hexdigest()
+            module_pyname = "{prefix}.{name}".format(
+                prefix='apprise.custom.module', name=module_name)
+
+            if module_pyname in self._custom_module_map:
+                # First clear out existing entries
+                for schema in \
+                        self._custom_module_map[module_pyname]['notify']\
+                        .keys():
+
+                    # Remove any mapped modules to this file
+                    del self._schema_map[schema]
+
+                # Reset
+                del self._custom_module_map[module_pyname]
+
+            # Load our module
+            module = import_module(path, module_pyname)
+            if not module:
+                # No problem, we can't use this object
+                logger.warning('Failed to load custom module: %s', _path)
+                return
+
+            # Print our loaded modules if any
+            if module_pyname in self._custom_module_map:
+                logger.debug(
+                    'Custom module %s - %d schema(s) (name=%s) '
+                    'loaded in %.6fs', _path,
+                    len(self._custom_module_map[module_pyname]['notify']),
+                    module_name, (time.time() - t_start))
+
+                # Add our plugin name to our module map
+                self._module_map[module_name] = {
+                    'plugin': set(),
+                    'module': module,
+                    'path': module_pyname,
+                    'native': False,
+                }
+
+                for schema, meta in\
+                        self._custom_module_map[module_pyname]['notify']\
+                        .items():
+
+                    # For mapping purposes; map our element in our main list
+                    self._module_map[module_name]['plugin'].add(
+                        self._schema_map[schema])
+
+                    # Log our success
+                    logger.info('Loaded custom notification: %s://', schema)
+            else:
+                # The code reaches here if we successfully loaded the Python
+                # module but no hooks/triggers were found. So we can safely
+                # just remove/ignore this entry
+                del sys.modules[module_pyname]
+                return
+
+            # end of _import_module()
+            return
+
+        for _path in paths:
+            path = os.path.abspath(os.path.expanduser(_path))
+            if (cache and path in self._paths_previously_scanned) \
+                    or not os.path.exists(path):
+                # We're done as we've already scanned this
+                continue
+
+            # Store our path as a way of hashing it has been handled
+            self._paths_previously_scanned.add(path)
+
+            if os.path.isdir(path) and not \
+                    os.path.isfile(os.path.join(path, '__init__.py')):
+
+                logger.debug('Scanning for custom plugins in: %s', path)
+                for entry in os.listdir(path):
+                    re_match = module_re.match(entry)
+                    if not re_match:
+                        # keep going
+                        logger.trace('Plugin Scan: Ignoring %s', entry)
+                        continue
+
+                    new_path = os.path.join(path, entry)
+                    if os.path.isdir(new_path):
+                        # Update our path
+                        new_path = os.path.join(path, entry, '__init__.py')
+                        if not os.path.isfile(new_path):
+                            logger.trace(
+                                'Plugin Scan: Ignoring %s',
+                                os.path.join(path, entry))
+                            continue
+
+                    if not cache or \
+                            (cache and new_path not in
+                             self._paths_previously_scanned):
+                        # Load our module
+                        _import_module(new_path)
+
+                        # Add our subdir path
+                        self._paths_previously_scanned.add(new_path)
+            else:
+                if os.path.isdir(path):
+                    # This logic is safe to apply because we already
+                    # validated the directories state above; update our
+                    # path
+                    path = os.path.join(path, '__init__.py')
+                    if cache and path in self._paths_previously_scanned:
+                        continue
+
+                    self._paths_previously_scanned.add(path)
+
+                # directly load as is
+                re_match = module_re.match(os.path.basename(path))
+                # must be a match and must have a .py extension
+                if not re_match or not re_match.group(1):
+                    # keep going
+                    logger.trace('Plugin Scan: Ignoring %s', path)
+                    continue
+
+                # Load our module
+                _import_module(path)
+
+        return None
+
+    def add(self, plugin, schemas=None, url=None, send_func=None):
+        """
+        Ability to manually add Notification services to our stack
+        """
+
+        if not self:
+            # Lazy load
+            self.load_modules()
+
+        # Acquire a list of schemas
+        p_schemas = parse_list(plugin.secure_protocol, plugin.protocol)
+        if isinstance(schemas, str):
+            schemas = [schemas, ]
+
+        elif schemas is None:
+            # Default
+            schemas = p_schemas
+
+        if not schemas or not isinstance(schemas, (set, tuple, list)):
+            # We're done
+            logger.error(
+                'The schemas provided (type %s) is unsupported; '
+                'loaded from %s.',
+                type(schemas),
+                send_func.__name__ if send_func else plugin.__class__.__name__)
+            return False
+
+        # Convert our schemas into a set
+        schemas = set([s.lower() for s in schemas]) | set(p_schemas)
+
+        # Valdation
+        conflict = [s for s in schemas if s in self]
+        if conflict:
+            # we're already handling this schema
+            logger.warning(
+                'The schema(s) (%s) are already defined and could not be '
+                'loaded from %s%s.',
+                ', '.join(conflict),
+                'custom notify function ' if send_func else '',
+                send_func.__name__ if send_func else plugin.__class__.__name__)
+            return False
+
+        if send_func:
+            # Acquire the function name
+            fn_name = send_func.__name__
+
+            # Acquire the python filename path
+            path = inspect.getfile(send_func)
+
+            # Acquire our path to our module
+            module_name = str(send_func.__module__)
+
+            if module_name not in self._custom_module_map:
+                # Support non-dynamic includes as well...
+                self._custom_module_map[module_name] = {
+                    # Name can be useful for indexing back into the
+                    # _module_map object; this is the key to do it with:
+                    'name': module_name.split('.')[-1],
+
+                    # The path to the module loaded
+                    'path': path,
+
+                    # Initialize our template
+                    'notify': {},
+                }
+
+            for schema in schemas:
+                self._custom_module_map[module_name]['notify'][schema] = {
+                    # The name of the send function the @notify decorator
+                    # wrapped
+                    'fn_name': fn_name,
+                    # The URL that was provided in the @notify decorator call
+                    # associated with the 'on='
+                    'url': url,
+                }
+
+        else:
+            module_name = hashlib.sha1(
+                ''.join(schemas).encode('utf-8')).hexdigest()
+            module_pyname = "{prefix}.{name}".format(
+                prefix='apprise.adhoc.module', name=module_name)
+
+            # Add our plugin name to our module map
+            self._module_map[module_name] = {
+                'plugin': set([plugin]),
+                'module': None,
+                'path': module_pyname,
+                'native': False,
+            }
+
+        for schema in schemas:
+            # Assign our mapping
+            self._schema_map[schema] = plugin
+
+        return True
+
+    def remove(self, *schemas):
+        """
+        Removes a loaded element (if defined)
+        """
+        if not self:
+            # Lazy load
+            self.load_modules()
+
+        for schema in schemas:
+            try:
+                del self[schema]
+
+            except KeyError:
+                pass
+
+    def plugins(self, include_disabled=True):
+        """
+        Return all of our loaded plugins
+        """
+        if not self:
+            # Lazy load
+            self.load_modules()
+
+        for module in self._module_map.values():
+            for plugin in module['plugin']:
+                if not include_disabled and not plugin.enabled:
+                    continue
+                yield plugin
+
+    def schemas(self, include_disabled=True):
+        """
+        Return all of our loaded schemas
+
+        if include_disabled == True, then even disabled notifications are
+        returned
+        """
+        if not self:
+            # Lazy load
+            self.load_modules()
+
+        # Return our list
+        return list(self._schema_map.keys()) if include_disabled else \
+            [s for s in self._schema_map.keys() if self._schema_map[s].enabled]
+
+    def disable(self, *schemas):
+        """
+        Disables the modules associated with the specified schemas
+        """
+        if not self:
+            # Lazy load
+            self.load_modules()
+
+        for schema in schemas:
+            if schema not in self._schema_map:
+                continue
+
+            if not self._schema_map[schema].enabled:
+                continue
+
+            # Disable
+            self._schema_map[schema].enabled = False
+            self._disabled.add(schema)
+
+    def enable_only(self, *schemas):
+        """
+        Disables the modules associated with the specified schemas
+        """
+        if not self:
+            # Lazy load
+            self.load_modules()
+
+        # convert to set for faster indexing
+        schemas = set(schemas)
+
+        for plugin in self.plugins():
+            # Get our plugin's schema list
+            p_schemas = set(
+                parse_list(plugin.secure_protocol, plugin.protocol))
+
+            if not schemas & p_schemas:
+                if plugin.enabled:
+                    # Disable it (only if previously enabled); this prevents us
+                    # from adjusting schemas that were disabled due to missing
+                    # libraries or other environment reasons
+                    plugin.enabled = False
+                    self._disabled |= p_schemas
+                continue
+
+            # If we reach here, our schema was flagged to be enabled
+            if p_schemas & self._disabled:
+                # Previously disabled; no worries, let's clear this up
+                self._disabled -= p_schemas
+                plugin.enabled = True
+
+    def __contains__(self, schema):
+        """
+        Checks if a schema exists
+        """
+        if not self:
+            # Lazy load
+            self.load_modules()
+
+        return schema in self._schema_map
+
+    def __delitem__(self, schema):
+        if not self:
+            # Lazy load
+            self.load_modules()
+
+        # Get our plugin (otherwise we throw a KeyError) which is
+        # intended on del action that doesn't align
+        plugin = self._schema_map[schema]
+
+        # Our list of all schema entries
+        p_schemas = set([schema])
+
+        for key in list(self._module_map.keys()):
+            if plugin in self._module_map[key]['plugin']:
+                # Remove our plugin
+                self._module_map[key]['plugin'].remove(plugin)
+
+                # Custom Plugin Entry; Clean up cross reference
+                module_pyname = self._module_map[key]['path']
+                if not self._module_map[key]['native'] and \
+                        module_pyname in self._custom_module_map:
+
+                    del self.\
+                        _custom_module_map[module_pyname]['notify'][schema]
+
+                    if not self._custom_module_map[module_pyname]['notify']:
+                        #
+                        # Last custom loaded element
+                        #
+
+                        # Free up custom object entry
+                        del self._custom_module_map[module_pyname]
+
+                if not self._module_map[key]['plugin']:
+                    #
+                    # Last element
+                    #
+                    if self._module_map[key]['native']:
+                        # Get our plugin's schema list
+                        p_schemas = \
+                            set([s for s in parse_list(
+                                 plugin.secure_protocol, plugin.protocol)
+                                 if s in self._schema_map])
+
+                    # free system memory
+                    if self._module_map[key]['module']:
+                        del sys.modules[self._module_map[key]['path']]
+
+                    # free last remaining pointer in module map
+                    del self._module_map[key]
+
+        for schema in p_schemas:
+            # Final Tidy
+            del self._schema_map[schema]
+
+    def __setitem__(self, schema, plugin):
+        """
+        Support fast assigning of Plugin/Notification Objects
+        """
+        if not self:
+            # Lazy load
+            self.load_modules()
+
+        # Set default values if not otherwise set
+        if not plugin.service_name:
+            # Assign service name if one doesn't exist
+            plugin.service_name = f'{schema}://'
+
+        p_schemas = set(
+            parse_list(plugin.secure_protocol, plugin.protocol))
+        if not p_schemas:
+            # Assign our protocol
+            plugin.secure_protocol = schema
+            p_schemas.add(schema)
+
+        elif schema not in p_schemas:
+            # Add our others (if defined)
+            plugin.secure_protocol = \
+                set([schema] + parse_list(plugin.secure_protocol))
+            p_schemas.add(schema)
+
+        if not self.add(plugin, schemas=p_schemas):
+            raise KeyError('Conflicting Assignment')
+
+    def __getitem__(self, schema):
+        """
+        Returns the indexed plugin identified by the schema specified
+        """
+        if not self:
+            # Lazy load
+            self.load_modules()
+
+        return self._schema_map[schema]
+
+    def __iter__(self):
+        """
+        Returns an iterator so we can iterate over our loaded modules
+        """
+        if not self:
+            # Lazy load
+            self.load_modules()
+
+        return iter(self._module_map.values())
+
+    def __len__(self):
+        """
+        Returns the number of modules/plugins loaded
+        """
+        if not self:
+            # Lazy load
+            self.load_modules()
+
+        return len(self._module_map)
+
+    def __bool__(self):
+        """
+        Determines if object has loaded or not
+        """
+        return True if self._loaded and self._module_map is not None else False
diff --git a/lib/apprise/manager_attachment.py b/lib/apprise/manager_attachment.py
new file mode 100644
index 00000000..d1288a94
--- /dev/null
+++ b/lib/apprise/manager_attachment.py
@@ -0,0 +1,59 @@
+# -*- coding: utf-8 -*-
+# BSD 2-Clause License
+#
+# Apprise - Push Notification Library.
+# Copyright (c) 2024, Chris Caron 
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met:
+#
+# 1. Redistributions of source code must retain the above copyright notice,
+#    this list of conditions and the following disclaimer.
+#
+# 2. Redistributions in binary form must reproduce the above copyright notice,
+#    this list of conditions and the following disclaimer in the documentation
+#    and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+
+import re
+from os.path import dirname
+from os.path import abspath
+from os.path import join
+from .manager import PluginManager
+
+
+class AttachmentManager(PluginManager):
+    """
+    Designed to be a singleton object to maintain all initialized
+    attachment plugins/modules in memory.
+    """
+
+    # Description (used for logging)
+    name = 'Attachment Plugin'
+
+    # Filename Prefix to filter on
+    fname_prefix = 'Attach'
+
+    # Memory Space
+    _id = 'attachment'
+
+    # Our Module Python path name
+    module_name_prefix = f'apprise.{_id}'
+
+    # The module path to scan
+    module_path = join(abspath(dirname(__file__)), _id)
+
+    # For filtering our result set
+    module_filter_re = re.compile(
+        r'^(?P' + fname_prefix + r'(?!Base)[A-Za-z0-9]+)$')
diff --git a/lib/apprise/manager_config.py b/lib/apprise/manager_config.py
new file mode 100644
index 00000000..69a6bedb
--- /dev/null
+++ b/lib/apprise/manager_config.py
@@ -0,0 +1,59 @@
+# -*- coding: utf-8 -*-
+# BSD 2-Clause License
+#
+# Apprise - Push Notification Library.
+# Copyright (c) 2024, Chris Caron 
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met:
+#
+# 1. Redistributions of source code must retain the above copyright notice,
+#    this list of conditions and the following disclaimer.
+#
+# 2. Redistributions in binary form must reproduce the above copyright notice,
+#    this list of conditions and the following disclaimer in the documentation
+#    and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+
+import re
+from os.path import dirname
+from os.path import abspath
+from os.path import join
+from .manager import PluginManager
+
+
+class ConfigurationManager(PluginManager):
+    """
+    Designed to be a singleton object to maintain all initialized
+    configuration plugins/modules in memory.
+    """
+
+    # Description (used for logging)
+    name = 'Configuration Plugin'
+
+    # Filename Prefix to filter on
+    fname_prefix = 'Config'
+
+    # Memory Space
+    _id = 'config'
+
+    # Our Module Python path name
+    module_name_prefix = f'apprise.{_id}'
+
+    # The module path to scan
+    module_path = join(abspath(dirname(__file__)), _id)
+
+    # For filtering our result set
+    module_filter_re = re.compile(
+        r'^(?P' + fname_prefix + r'(?!Base)[A-Za-z0-9]+)$')
diff --git a/lib/apprise/manager_plugins.py b/lib/apprise/manager_plugins.py
new file mode 100644
index 00000000..74ed370e
--- /dev/null
+++ b/lib/apprise/manager_plugins.py
@@ -0,0 +1,60 @@
+# -*- coding: utf-8 -*-
+# BSD 2-Clause License
+#
+# Apprise - Push Notification Library.
+# Copyright (c) 2024, Chris Caron 
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met:
+#
+# 1. Redistributions of source code must retain the above copyright notice,
+#    this list of conditions and the following disclaimer.
+#
+# 2. Redistributions in binary form must reproduce the above copyright notice,
+#    this list of conditions and the following disclaimer in the documentation
+#    and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+
+import re
+from os.path import dirname
+from os.path import abspath
+from os.path import join
+from .manager import PluginManager
+
+
+class NotificationManager(PluginManager):
+    """
+    Designed to be a singleton object to maintain all initialized notifications
+    in memory.
+    """
+
+    # Description (used for logging)
+    name = 'Notification Plugin'
+
+    # Filename Prefix to filter on
+    fname_prefix = 'Notify'
+
+    # Memory Space
+    _id = 'plugins'
+
+    # Our Module Python path name
+    module_name_prefix = f'apprise.{_id}'
+
+    # The module path to scan
+    module_path = join(abspath(dirname(__file__)), _id)
+
+    # For filtering our result set
+    module_filter_re = re.compile(
+        r'^(?P' + fname_prefix +
+        r'(?!Base|ImageSize|Type)[A-Za-z0-9]+)$')
diff --git a/lib/apprise/plugins/NotifyGrowl/gntp/__init__.py b/lib/apprise/plugins/NotifyGrowl/gntp/__init__.py
deleted file mode 100644
index e69de29b..00000000
diff --git a/lib/apprise/plugins/NotifySpontit.py b/lib/apprise/plugins/NotifySpontit.py
deleted file mode 100644
index 4705fc05..00000000
--- a/lib/apprise/plugins/NotifySpontit.py
+++ /dev/null
@@ -1,386 +0,0 @@
-# -*- coding: utf-8 -*-
-# BSD 2-Clause License
-#
-# Apprise - Push Notification Library.
-# Copyright (c) 2023, Chris Caron 
-#
-# Redistribution and use in source and binary forms, with or without
-# modification, are permitted provided that the following conditions are met:
-#
-# 1. Redistributions of source code must retain the above copyright notice,
-#    this list of conditions and the following disclaimer.
-#
-# 2. Redistributions in binary form must reproduce the above copyright notice,
-#    this list of conditions and the following disclaimer in the documentation
-#    and/or other materials provided with the distribution.
-#
-# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
-# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
-# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
-# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
-# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
-# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
-# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
-# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
-# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
-# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
-# POSSIBILITY OF SUCH DAMAGE.
-
-# To use this service you will need a Spontit account from their website
-# at https://spontit.com/
-#
-# After you have an account created:
-#   - Visit your profile at https://spontit.com/profile and take note of your
-#     {username}.  It might look something like: user12345678901
-#   - Next generate an API key at https://spontit.com/secret_keys. This will
-#     generate a very long alpha-numeric string we'll refer to as the
-#     {apikey}
-
-# The Spontit Syntax is as follows:
-# spontit://{username}@{apikey}
-
-import re
-import requests
-from json import loads
-
-from .NotifyBase import NotifyBase
-from ..common import NotifyType
-from ..utils import parse_list
-from ..utils import validate_regex
-from ..AppriseLocale import gettext_lazy as _
-
-# Syntax suggests you use a hashtag '#' to help distinguish we're dealing
-# with a channel.
-# Secondly we extract the user information only if it's
-# specified.  If not, we use the user of the person sending the notification
-# Finally the channel identifier is detected
-CHANNEL_REGEX = re.compile(
-    r'^\s*(\#|\%23)?((\@|\%40)?(?P[a-z0-9_]+)([/\\]|\%2F))?'
-    r'(?P[a-z0-9_-]+)\s*$', re.I)
-
-
-class NotifySpontit(NotifyBase):
-    """
-    A wrapper for Spontit Notifications
-    """
-
-    # The default descriptive name associated with the Notification
-    service_name = 'Spontit'
-
-    # The services URL
-    service_url = 'https://spontit.com/'
-
-    # All notification requests are secure
-    secure_protocol = 'spontit'
-
-    # Allow 300 requests per minute.
-    # 60/300 = 0.2
-    request_rate_per_sec = 0.20
-
-    # A URL that takes you to the setup/help of the specific protocol
-    setup_url = 'https://github.com/caronc/apprise/wiki/Notify_spontit'
-
-    # Spontit single notification URL
-    notify_url = 'https://api.spontit.com/v3/push'
-
-    # The maximum length of the body
-    body_maxlen = 5000
-
-    # The maximum length of the title
-    title_maxlen = 100
-
-    # If we don't have the specified min length, then we don't bother using
-    # the body directive
-    spontit_body_minlen = 100
-
-    # Subtitle support; this is the maximum allowed characters defined by
-    # the API page
-    spontit_subtitle_maxlen = 20
-
-    # Define object templates
-    templates = (
-        '{schema}://{user}@{apikey}',
-        '{schema}://{user}@{apikey}/{targets}',
-    )
-
-    # Define our template tokens
-    template_tokens = dict(NotifyBase.template_tokens, **{
-        'user': {
-            'name': _('User ID'),
-            'type': 'string',
-            'required': True,
-            'regex': (r'^[a-z0-9_-]+$', 'i'),
-        },
-        'apikey': {
-            'name': _('API Key'),
-            'type': 'string',
-            'required': True,
-            'private': True,
-            'regex': (r'^[a-z0-9]+$', 'i'),
-        },
-        # Target Channel ID's
-        # If a slash is used; you must escape it
-        # If no slash is used; channel is presumed to be your own
-        'target_channel': {
-            'name': _('Target Channel ID'),
-            'type': 'string',
-            'prefix': '#',
-            'regex': (r'^[0-9\s)(+-]+$', 'i'),
-            'map_to': 'targets',
-        },
-        'targets': {
-            'name': _('Targets'),
-            'type': 'list:string',
-        },
-    })
-
-    # Define our template arguments
-    template_args = dict(NotifyBase.template_args, **{
-        'to': {
-            'alias_of': 'targets',
-        },
-        'subtitle': {
-            # Subtitle is available for MacOS users
-            'name': _('Subtitle'),
-            'type': 'string',
-        },
-    })
-
-    def __init__(self, apikey, targets=None, subtitle=None, **kwargs):
-        """
-        Initialize Spontit Object
-        """
-        super().__init__(**kwargs)
-
-        # User ID (associated with project)
-        user = validate_regex(
-            self.user, *self.template_tokens['user']['regex'])
-        if not user:
-            msg = 'An invalid Spontit User ID ' \
-                  '({}) was specified.'.format(self.user)
-            self.logger.warning(msg)
-            raise TypeError(msg)
-        # use cleaned up version
-        self.user = user
-
-        # API Key (associated with project)
-        self.apikey = validate_regex(
-            apikey, *self.template_tokens['apikey']['regex'])
-        if not self.apikey:
-            msg = 'An invalid Spontit API Key ' \
-                  '({}) was specified.'.format(apikey)
-            self.logger.warning(msg)
-            raise TypeError(msg)
-
-        # Save our subtitle information
-        self.subtitle = subtitle
-
-        # Parse our targets
-        self.targets = list()
-
-        for target in parse_list(targets):
-            # Validate targets and drop bad ones:
-            result = CHANNEL_REGEX.match(target)
-            if result:
-                # Just extract the channel
-                self.targets.append(
-                    '{}'.format(result.group('channel')))
-                continue
-
-            self.logger.warning(
-                'Dropped invalid channel/user ({}) specified.'.format(target))
-
-        return
-
-    def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
-        """
-        Sends Message
-        """
-
-        # error tracking (used for function return)
-        has_error = False
-
-        # Prepare our headers
-        headers = {
-            'User-Agent': self.app_id,
-            'Content-Type': 'application/json',
-            'X-Authorization': self.apikey,
-            'X-UserId': self.user,
-        }
-
-        # use the list directly
-        targets = list(self.targets)
-
-        if not len(targets):
-            # The user did not specify a channel and therefore wants to notify
-            # the main account only.  We just set a substitute marker of
-            # None so that our while loop below can still process one iteration
-            targets = [None, ]
-
-        while len(targets):
-            # Get our target(s) to notify
-            target = targets.pop(0)
-
-            # Prepare our payload
-            payload = {
-                'message': body,
-            }
-
-            # Use our body directive if we exceed the minimum message
-            # limitation
-            if len(body) > self.spontit_body_minlen:
-                payload['message'] = '{}...'.format(
-                    body[:self.spontit_body_minlen - 3])
-                payload['body'] = body
-
-            if self.subtitle:
-                # Set title if specified
-                payload['subtitle'] = \
-                    self.subtitle[:self.spontit_subtitle_maxlen]
-
-            elif self.app_desc:
-                # fall back to app description
-                payload['subtitle'] = \
-                    self.app_desc[:self.spontit_subtitle_maxlen]
-
-            elif self.app_id:
-                # fall back to app id
-                payload['subtitle'] = \
-                    self.app_id[:self.spontit_subtitle_maxlen]
-
-            if title:
-                # Set title if specified
-                payload['pushTitle'] = title
-
-            if target is not None:
-                payload['channelName'] = target
-
-            # Some Debug Logging
-            self.logger.debug(
-                'Spontit POST URL: {} (cert_verify={})'.format(
-                    self.notify_url, self.verify_certificate))
-            self.logger.debug('Spontit Payload: {}' .format(payload))
-
-            # Always call throttle before any remote server i/o is made
-            self.throttle()
-            try:
-                r = requests.post(
-                    self.notify_url,
-                    params=payload,
-                    headers=headers,
-                    verify=self.verify_certificate,
-                    timeout=self.request_timeout,
-                )
-
-                if r.status_code not in (
-                        requests.codes.created, requests.codes.ok):
-                    status_str = \
-                        NotifyBase.http_response_code_lookup(
-                            r.status_code)
-
-                    try:
-                        # Update our status response if we can
-                        json_response = loads(r.content)
-                        status_str = json_response.get('message', status_str)
-
-                    except (AttributeError, TypeError, ValueError):
-                        # ValueError = r.content is Unparsable
-                        # TypeError = r.content is None
-                        # AttributeError = r is None
-
-                        # We could not parse JSON response.
-                        # We will just use the status we already have.
-                        pass
-
-                    self.logger.warning(
-                        'Failed to send Spontit notification to {}: '
-                        '{}{}error={}.'.format(
-                            target,
-                            status_str,
-                            ', ' if status_str else '',
-                            r.status_code))
-
-                    self.logger.debug(
-                        'Response Details:\r\n{}'.format(r.content))
-
-                    # Mark our failure
-                    has_error = True
-                    continue
-
-                # If we reach here; the message was sent
-                self.logger.info(
-                    'Sent Spontit notification to {}.'.format(target))
-
-                self.logger.debug(
-                    'Response Details:\r\n{}'.format(r.content))
-
-            except requests.RequestException as e:
-                self.logger.warning(
-                    'A Connection error occurred sending Spontit:%s ' % (
-                        ', '.join(self.targets)) + 'notification.'
-                )
-                self.logger.debug('Socket Exception: %s' % str(e))
-                # Mark our failure
-                has_error = True
-                continue
-
-        return not has_error
-
-    def url(self, privacy=False, *args, **kwargs):
-        """
-        Returns the URL built dynamically based on specified arguments.
-        """
-
-        # Our URL parameters
-        params = self.url_parameters(privacy=privacy, *args, **kwargs)
-
-        if self.subtitle:
-            params['subtitle'] = self.subtitle
-
-        return '{schema}://{userid}@{apikey}/{targets}?{params}'.format(
-            schema=self.secure_protocol,
-            userid=self.user,
-            apikey=self.pprint(self.apikey, privacy, safe=''),
-            targets='/'.join(
-                [NotifySpontit.quote(x, safe='') for x in self.targets]),
-            params=NotifySpontit.urlencode(params))
-
-    def __len__(self):
-        """
-        Returns the number of targets associated with this notification
-        """
-        targets = len(self.targets)
-        return targets if targets > 0 else 1
-
-    @staticmethod
-    def parse_url(url):
-        """
-        Parses the URL and returns enough arguments that can allow
-        us to re-instantiate this object.
-
-        """
-
-        results = NotifyBase.parse_url(url, verify_host=False)
-        if not results:
-            # We're done early as we couldn't load the results
-            return results
-
-        # Get our entries; split_path() looks after unquoting content for us
-        # by default
-        results['targets'] = NotifySpontit.split_path(results['fullpath'])
-
-        # The hostname is our authentication key
-        results['apikey'] = NotifySpontit.unquote(results['host'])
-
-        # Support MacOS subtitle option
-        if 'subtitle' in results['qsd'] and len(results['qsd']['subtitle']):
-            results['subtitle'] = \
-                NotifySpontit.unquote(results['qsd']['subtitle'])
-
-        # Support the 'to' variable so that we can support targets this way too
-        # The 'to' makes it easier to use yaml configuration
-        if 'to' in results['qsd'] and len(results['qsd']['to']):
-            results['targets'] += \
-                NotifySpontit.parse_list(results['qsd']['to'])
-
-        return results
diff --git a/lib/apprise/plugins/__init__.py b/lib/apprise/plugins/__init__.py
index 27afef05..bfce1437 100644
--- a/lib/apprise/plugins/__init__.py
+++ b/lib/apprise/plugins/__init__.py
@@ -2,7 +2,7 @@
 # BSD 2-Clause License
 #
 # Apprise - Push Notification Library.
-# Copyright (c) 2023, Chris Caron 
+# Copyright (c) 2024, Chris Caron 
 #
 # Redistribution and use in source and binary forms, with or without
 # modification, are permitted provided that the following conditions are met:
@@ -27,26 +27,26 @@
 # POSSIBILITY OF SUCH DAMAGE.
 
 import os
-import re
 import copy
 
-from os.path import dirname
-from os.path import abspath
-
 # Used for testing
-from .NotifyBase import NotifyBase
+from .base import NotifyBase
 
 from ..common import NotifyImageSize
 from ..common import NOTIFY_IMAGE_SIZES
 from ..common import NotifyType
 from ..common import NOTIFY_TYPES
-from .. import common
 from ..utils import parse_list
 from ..utils import cwe312_url
 from ..utils import GET_SCHEMA_RE
 from ..logger import logger
-from ..AppriseLocale import gettext_lazy as _
-from ..AppriseLocale import LazyTranslation
+from ..locale import gettext_lazy as _
+from ..locale import LazyTranslation
+from ..manager_plugins import NotificationManager
+
+
+# Grant access to our Notification Manager Singleton
+N_MGR = NotificationManager()
 
 __all__ = [
     # Reference
@@ -58,101 +58,6 @@ __all__ = [
 ]
 
 
-# Load our Lookup Matrix
-def __load_matrix(path=abspath(dirname(__file__)), name='apprise.plugins'):
-    """
-    Dynamically load our schema map; this allows us to gracefully
-    skip over modules we simply don't have the dependencies for.
-
-    """
-    # Used for the detection of additional Notify Services objects
-    # The .py extension is optional as we support loading directories too
-    module_re = re.compile(r'^(?PNotify[a-z0-9]+)(\.py)?$', re.I)
-
-    for f in os.listdir(path):
-        match = module_re.match(f)
-        if not match:
-            # keep going
-            continue
-
-        # Store our notification/plugin name:
-        plugin_name = match.group('name')
-        try:
-            module = __import__(
-                '{}.{}'.format(name, plugin_name),
-                globals(), locals(),
-                fromlist=[plugin_name])
-
-        except ImportError:
-            # No problem, we can't use this object
-            continue
-
-        if not hasattr(module, plugin_name):
-            # Not a library we can load as it doesn't follow the simple rule
-            # that the class must bear the same name as the notification
-            # file itself.
-            continue
-
-        # Get our plugin
-        plugin = getattr(module, plugin_name)
-        if not hasattr(plugin, 'app_id'):
-            # Filter out non-notification modules
-            continue
-
-        elif plugin_name in common.NOTIFY_MODULE_MAP:
-            # we're already handling this object
-            continue
-
-        # Add our plugin name to our module map
-        common.NOTIFY_MODULE_MAP[plugin_name] = {
-            'plugin': plugin,
-            'module': module,
-        }
-
-        # Add our module name to our __all__
-        __all__.append(plugin_name)
-
-        fn = getattr(plugin, 'schemas', None)
-        schemas = set([]) if not callable(fn) else fn(plugin)
-
-        # map our schema to our plugin
-        for schema in schemas:
-            if schema in common.NOTIFY_SCHEMA_MAP:
-                logger.error(
-                    "Notification schema ({}) mismatch detected - {} to {}"
-                    .format(schema, common.NOTIFY_SCHEMA_MAP[schema], plugin))
-                continue
-
-            # Assign plugin
-            common.NOTIFY_SCHEMA_MAP[schema] = plugin
-
-    return common.NOTIFY_SCHEMA_MAP
-
-
-# Reset our Lookup Matrix
-def __reset_matrix():
-    """
-    Restores the Lookup matrix to it's base setting. This is only used through
-    testing and should not be directly called.
-    """
-
-    # Reset our schema map
-    common.NOTIFY_SCHEMA_MAP.clear()
-
-    # Iterate over our module map so we can clear out our __all__ and globals
-    for plugin_name in common.NOTIFY_MODULE_MAP.keys():
-
-        # Remove element from plugins
-        __all__.remove(plugin_name)
-
-    # Clear out our module map
-    common.NOTIFY_MODULE_MAP.clear()
-
-
-# Dynamically build our schema base
-__load_matrix()
-
-
 def _sanitize_token(tokens, default_delimiter):
     """
     This is called by the details() function and santizes the output by
@@ -176,6 +81,10 @@ def _sanitize_token(tokens, default_delimiter):
             # Do not touch this field
             continue
 
+        elif 'name' not in tokens[key]:
+            # Default to key
+            tokens[key]['name'] = key
+
         if 'map_to' not in tokens[key]:
             # Default type to key
             tokens[key]['map_to'] = key
@@ -538,16 +447,16 @@ def url_to_dict(url, secure_logging=True):
 
     # Ensure our schema is always in lower case
     schema = schema.group('schema').lower()
-    if schema not in common.NOTIFY_SCHEMA_MAP:
+    if schema not in N_MGR:
         # Give the user the benefit of the doubt that the user may be using
         # one of the URLs provided to them by their notification service.
         # Before we fail for good, just scan all the plugins that support the
         # native_url() parse function
-        results = \
-            next((r['plugin'].parse_native_url(_url)
-                  for r in common.NOTIFY_MODULE_MAP.values()
-                  if r['plugin'].parse_native_url(_url) is not None),
-                 None)
+        results = None
+        for plugin in N_MGR.plugins():
+            results = plugin.parse_native_url(_url)
+            if results:
+                break
 
         if not results:
             logger.error('Unparseable URL {}'.format(loggable_url))
@@ -560,14 +469,14 @@ def url_to_dict(url, secure_logging=True):
     else:
         # Parse our url details of the server object as dictionary
         # containing all of the information parsed from our URL
-        results = common.NOTIFY_SCHEMA_MAP[schema].parse_url(_url)
+        results = N_MGR[schema].parse_url(_url)
         if not results:
             logger.error('Unparseable {} URL {}'.format(
-                common.NOTIFY_SCHEMA_MAP[schema].service_name, loggable_url))
+                N_MGR[schema].service_name, loggable_url))
             return None
 
         logger.trace('{} URL {} unpacked as:{}{}'.format(
-            common.NOTIFY_SCHEMA_MAP[schema].service_name, url,
+            N_MGR[schema].service_name, url,
             os.linesep, os.linesep.join(
                 ['{}="{}"'.format(k, v) for k, v in results.items()])))
 
diff --git a/lib/apprise/plugins/NotifyAppriseAPI.py b/lib/apprise/plugins/apprise_api.py
similarity index 98%
rename from lib/apprise/plugins/NotifyAppriseAPI.py
rename to lib/apprise/plugins/apprise_api.py
index 3c85b8ac..fd71236b 100644
--- a/lib/apprise/plugins/NotifyAppriseAPI.py
+++ b/lib/apprise/plugins/apprise_api.py
@@ -2,7 +2,7 @@
 # BSD 2-Clause License
 #
 # Apprise - Push Notification Library.
-# Copyright (c) 2023, Chris Caron 
+# Copyright (c) 2024, Chris Caron 
 #
 # Redistribution and use in source and binary forms, with or without
 # modification, are permitted provided that the following conditions are met:
@@ -31,12 +31,12 @@ import requests
 from json import dumps
 import base64
 
-from .NotifyBase import NotifyBase
-from ..URLBase import PrivacyMode
+from .base import NotifyBase
+from ..url import PrivacyMode
 from ..common import NotifyType
 from ..utils import parse_list
 from ..utils import validate_regex
-from ..AppriseLocale import gettext_lazy as _
+from ..locale import gettext_lazy as _
 
 
 class AppriseAPIMethod:
@@ -123,7 +123,7 @@ class NotifyAppriseAPI(NotifyBase):
             'type': 'string',
             'required': True,
             'private': True,
-            'regex': (r'^[A-Z0-9_-]{1,32}$', 'i'),
+            'regex': (r'^[A-Z0-9_-]{1,128}$', 'i'),
         },
     })
 
diff --git a/lib/apprise/plugins/aprs.py b/lib/apprise/plugins/aprs.py
new file mode 100644
index 00000000..b8adef5a
--- /dev/null
+++ b/lib/apprise/plugins/aprs.py
@@ -0,0 +1,778 @@
+# -*- coding: utf-8 -*-
+# BSD 2-Clause License
+#
+# Apprise - Push Notification Library.
+# Copyright (c) 2024, Chris Caron 
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met:
+#
+# 1. Redistributions of source code must retain the above copyright notice,
+#    this list of conditions and the following disclaimer.
+#
+# 2. Redistributions in binary form must reproduce the above copyright notice,
+#    this list of conditions and the following disclaimer in the documentation
+#    and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+#
+# To use this plugin, you need to be a licensed ham radio operator
+#
+# Plugin constraints:
+#
+# - message length = 67 chars max.
+# - message content = ASCII 7 bit
+# - APRS messages will be sent without msg ID, meaning that
+#   ham radio operators cannot acknowledge them
+# - Bring your own APRS-IS passcode. If you don't know what
+#   this is or how to get it, then this plugin is not for you
+# - Do NOT change the Device/ToCall ID setting UNLESS this
+#   module is used outside of Apprise. This identifier helps
+#   the ham radio community with determining the software behind
+#   a given APRS message.
+# - With great (ham radio) power comes great responsibility; do
+#   not use this plugin for spamming other ham radio operators
+
+#
+# In order to digest text input which is not in plain English,
+# users can install the optional 'unidecode' package as part
+# of their venv environment. Details: see plugin description
+#
+
+#
+# You're done at this point, you only need to know your user/pass that
+# you signed up with.
+
+#  The following URLs would be accepted by Apprise:
+#   - aprs://{user}:{password}@{callsign}
+#   - aprs://{user}:{password}@{callsign1}/{callsign2}
+
+# Optional parameters:
+#   - locale --> APRS-IS target server to connect with
+#                Default: EURO --> 'euro.aprs2.net'
+#                Details: https://www.aprs2.net/
+
+#
+# APRS message format specification:
+# http://www.aprs.org/doc/APRS101.PDF
+#
+
+import socket
+import sys
+from itertools import chain
+from .base import NotifyBase
+from ..locale import gettext_lazy as _
+from ..url import PrivacyMode
+from ..common import NotifyType
+from ..utils import is_call_sign
+from ..utils import parse_call_sign
+from .. import __version__
+import re
+
+# Fixed APRS-IS server locales
+# Default is 'EURO'
+# See https://www.aprs2.net/ for details
+# Select the rotating server in case you
+# don"t care about a specific locale
+APRS_LOCALES = {
+    "NOAM": "noam.aprs2.net",
+    "SOAM": "soam.aprs2.net",
+    "EURO": "euro.aprs2.net",
+    "ASIA": "asia.aprs2.net",
+    "AUNZ": "aunz.aprs2.net",
+    "ROTA": "rotate.aprs2.net",
+}
+
+# Identify all unsupported characters
+APRS_BAD_CHARMAP = {
+    r"Ä": "Ae",
+    r"Ö": "Oe",
+    r"Ü": "Ue",
+    r"ä": "ae",
+    r"ö": "oe",
+    r"ü": "ue",
+    r"ß": "ss",
+}
+
+# Our compiled mapping of bad characters
+APRS_COMPILED_MAP = re.compile(
+    r'(' + '|'.join(APRS_BAD_CHARMAP.keys()) + r')')
+
+
+class NotifyAprs(NotifyBase):
+    """
+    A wrapper for APRS Notifications via APRS-IS
+    """
+
+    # The default descriptive name associated with the Notification
+    service_name = "Aprs"
+
+    # The services URL
+    service_url = "https://www.aprs2.net/"
+
+    # The default secure protocol
+    secure_protocol = "aprs"
+
+    # A URL that takes you to the setup/help of the specific protocol
+    setup_url = "https://github.com/caronc/apprise/wiki/Notify_aprs"
+
+    # APRS default port, supported by all core servers
+    # Details: https://www.aprs-is.net/Connecting.aspx
+    notify_port = 10152
+
+    # The maximum length of the APRS message body
+    body_maxlen = 67
+
+    # Apprise APRS Device ID / TOCALL ID
+    # This is a FIXED value which is associated with this plugin.
+    # Its value MUST NOT be changed. If you use this APRS plugin
+    # code OUTSIDE of Apprise, please request your own TOCALL ID.
+    # Details: see https://github.com/aprsorg/aprs-deviceid
+    #
+    # Do NOT use the generic "APRS" TOCALL ID !!!!!
+    #
+    device_id = "APPRIS"
+
+    # A title can not be used for APRS Messages.  Setting this to zero will
+    # cause any title (if defined) to get placed into the message body.
+    title_maxlen = 0
+
+    # Helps to reduce the number of login-related errors where the
+    # APRS-IS server "isn't ready yet". If we try to receive the rx buffer
+    # without this grace perid in place, we may receive "incomplete" responses
+    # where the login response lacks information. In case you receive too many
+    # "Rx: APRS-IS msg is too short - needs to have at least two lines" error
+    # messages, you might want to increase this value to a larger time span
+    # Per previous experience, do not use values lower than 0.5 (seconds)
+    request_rate_per_sec = 0.8
+
+    # Encoding of retrieved content
+    aprs_encoding = 'latin-1'
+
+    # Define object templates
+    templates = ("{schema}://{user}:{password}@{targets}",)
+
+    # Define our template tokens
+    template_tokens = dict(
+        NotifyBase.template_tokens,
+        **{
+            "user": {
+                "name": _("User Name"),
+                "type": "string",
+                "required": True,
+            },
+            "password": {
+                "name": _("Password"),
+                "type": "string",
+                "private": True,
+                "required": True,
+            },
+            "target_callsign": {
+                "name": _("Target Callsign"),
+                "type": "string",
+                "regex": (
+                    r"^[a-z0-9]{2,5}(-[a-z0-9]{1,2})?$",
+                    "i",
+                ),
+                "map_to": "targets",
+            },
+            "targets": {
+                "name": _("Targets"),
+                "type": "list:string",
+                "required": True,
+            },
+        }
+    )
+
+    # Define our template arguments
+    template_args = dict(
+        NotifyBase.template_args,
+        **{
+            "to": {
+                "name": _("Target Callsign"),
+                "type": "string",
+                "map_to": "targets",
+            },
+            "delay": {
+                "name": _("Resend Delay"),
+                "type": "float",
+                "min": 0.0,
+                "max": 5.0,
+                "default": 0.0,
+            },
+            "locale": {
+                "name": _("Locale"),
+                "type": "choice:string",
+                "values": APRS_LOCALES,
+                "default": "EURO",
+            },
+        }
+    )
+
+    def __init__(self, targets=None, locale=None, delay=None, **kwargs):
+        """
+        Initialize APRS Object
+        """
+        super().__init__(**kwargs)
+
+        # Our (future) socket sobject
+        self.sock = None
+
+        # Parse our targets
+        self.targets = list()
+
+        """
+        Check if the user has provided credentials
+        """
+        if not (self.user and self.password):
+            msg = "An APRS user/pass was not provided."
+            self.logger.warning(msg)
+            raise TypeError(msg)
+
+        """
+        Check if the user tries to use a read-only access
+        to APRS-IS. We need to send content, meaning that
+        read-only access will not work
+        """
+        if self.password == "-1":
+            msg = "APRS read-only passwords are not supported."
+            self.logger.warning(msg)
+            raise TypeError(msg)
+
+        """
+        Check if the password is numeric
+        """
+        if not self.password.isnumeric():
+            msg = "Invalid APRS-IS password"
+            self.logger.warning(msg)
+            raise TypeError(msg)
+
+        """
+        Convert given user name (FROM callsign) and
+        device ID to to uppercase
+        """
+        self.user = self.user.upper()
+        self.device_id = self.device_id.upper()
+
+        """
+        Check if the user has provided a locale for the
+        APRS-IS-server and validate it, if necessary
+        """
+        if locale:
+            if locale.upper() not in APRS_LOCALES:
+                msg = (
+                    "Unsupported APRS-IS server locale. "
+                    "Received: {}. Valid: {}".format(
+                        locale, ", ".join(str(x) for x in APRS_LOCALES.keys())
+                    )
+                )
+                self.logger.warning(msg)
+                raise TypeError(msg)
+
+        # Update our delay
+        if delay is None:
+            self.delay = NotifyAprs.template_args["delay"]["default"]
+
+        else:
+            try:
+                self.delay = float(delay)
+                if self.delay < NotifyAprs.template_args["delay"]["min"]:
+                    raise ValueError()
+
+                elif self.delay >= NotifyAprs.template_args["delay"]["max"]:
+                    raise ValueError()
+
+            except (TypeError, ValueError):
+                msg = "Unsupported APRS-IS delay ({}) specified. ".format(
+                    delay)
+                self.logger.warning(msg)
+                raise TypeError(msg)
+
+        # Bump up our request_rate
+        self.request_rate_per_sec += self.delay
+
+        # Set the transmitter group
+        self.locale = \
+            NotifyAprs.template_args["locale"]["default"] \
+            if not locale else locale.upper()
+
+        # Used for URL generation afterwards only
+        self.invalid_targets = list()
+
+        for target in parse_call_sign(targets):
+            # Validate targets and drop bad ones
+            # We just need to know if the call sign (including SSID, if
+            # provided) is valid and can then process the input as is
+            result = is_call_sign(target)
+            if not result:
+                self.logger.warning(
+                    "Dropping invalid Amateur radio call sign ({}).".format(
+                        target
+                    ),
+                )
+                self.invalid_targets.append(target.upper())
+                continue
+
+            # Store entry
+            self.targets.append(target.upper())
+
+        return
+
+    def socket_close(self):
+        """
+        Closes the socket connection whereas present
+        """
+        if self.sock:
+            try:
+                self.sock.close()
+
+            except Exception:
+                # No worries if socket exception thrown on close()
+                pass
+
+            self.sock = None
+
+    def socket_open(self):
+        """
+        Establishes the connection to the APRS-IS
+        socket server
+        """
+        self.logger.debug(
+            "Creating socket connection with APRS-IS {}:{}".format(
+                APRS_LOCALES[self.locale], self.notify_port
+            )
+        )
+
+        try:
+            self.sock = socket.create_connection(
+                (APRS_LOCALES[self.locale], self.notify_port),
+                self.socket_connect_timeout,
+            )
+
+        except ConnectionError as e:
+            self.logger.debug("Socket Exception socket_open: %s", str(e))
+            self.sock = None
+            return False
+
+        except socket.gaierror as e:
+            self.logger.debug("Socket Exception socket_open: %s", str(e))
+            self.sock = None
+            return False
+
+        except socket.timeout as e:
+            self.logger.debug(
+                "Socket Timeout Exception socket_open: %s", str(e))
+            self.sock = None
+            return False
+
+        except Exception as e:
+            self.logger.debug("General Exception socket_open: %s", str(e))
+            self.sock = None
+            return False
+
+        # We are connected.
+        # getpeername() is not supported by every OS. Therefore,
+        # we MAY receive an exception even though we are
+        # connected successfully.
+        try:
+            # Get the physical host/port of the server
+            host, port = self.sock.getpeername()
+            # and create debug info
+            self.logger.debug("Connected to {}:{}".format(host, port))
+
+        except ValueError:
+            # Seens as if we are running on an operating
+            # system that does not support getpeername()
+            # Create a minimal log file entry
+            self.logger.debug("Connected to APRS-IS")
+
+        # Return success
+        return True
+
+    def aprsis_login(self):
+        """
+        Generate the APRS-IS login string, send it to the server
+        and parse the response
+
+        Returns True/False wrt whether the login was successful
+        """
+        self.logger.debug("socket_login: init")
+
+        # Check if we are connected
+        if not self.sock:
+            self.logger.warning("socket_login: Not connected to APRS-IS")
+            return False
+
+        # APRS-IS login string, see https://www.aprs-is.net/Connecting.aspx
+        login_str = "user {0} pass {1} vers apprise {2}\r\n".format(
+            self.user, self.password, __version__
+        )
+
+        # Send the data & abort in case of error
+        if not self.socket_send(login_str):
+            self.logger.warning(
+                "socket_login: Login to APRS-IS unsuccessful,"
+                " exception occurred"
+            )
+            self.socket_close()
+            return False
+
+        rx_buf = self.socket_receive(len(login_str) + 100)
+        # Abort the remaining process in case an error has occurred
+        if not rx_buf:
+            self.logger.warning(
+                "socket_login: Login to APRS-IS "
+                "unsuccessful, exception occurred"
+            )
+            self.socket_close()
+            return False
+
+        # APRS-IS sends at least two lines of data
+        # The data that we need is in line #2 so
+        # let's split the  content and see what we have
+        rx_lines = rx_buf.splitlines()
+        if len(rx_lines) < 2:
+            self.logger.warning(
+                "socket_login: APRS-IS msg is too short"
+                " - needs to have at least two lines"
+            )
+            self.socket_close()
+            return False
+
+        # Now split the 2nd line's content and extract
+        # both call sign and login status
+        try:
+            _, _, callsign, status, _ = rx_lines[1].split(" ", 4)
+
+        except ValueError:
+            # ValueError is returned if there were not enough elements to
+            # populate the response
+            self.logger.warning(
+                "socket_login: " "received invalid response from APRS-IS"
+            )
+            self.socket_close()
+            return False
+
+        if callsign != self.user:
+            self.logger.warning(
+                "socket_login: " "call signs differ: %s" % callsign
+            )
+            self.socket_close()
+            return False
+
+        if status.startswith("unverified"):
+            self.logger.warning(
+                "socket_login: "
+                "invalid APRS-IS password for given call sign"
+            )
+            self.socket_close()
+            return False
+
+        # all validations are successful; we are connected
+        return True
+
+    def socket_send(self, tx_data):
+        """
+        Generic "Send data to a socket"
+        """
+        self.logger.debug("socket_send: init")
+
+        # Check if we are connected
+        if not self.sock:
+            self.logger.warning("socket_send: Not connected to APRS-IS")
+            return False
+
+        # Encode our data if we are on Python3 or later
+        payload = (
+            tx_data.encode("utf-8") if sys.version_info[0] >= 3 else tx_data
+        )
+
+        # Always call throttle before any remote server i/o is made
+        self.throttle()
+
+        # Try to open the socket
+        # Send the content to APRS-IS
+        try:
+            self.sock.setblocking(True)
+            self.sock.settimeout(self.socket_connect_timeout)
+            self.sock.sendall(payload)
+
+        except socket.gaierror as e:
+            self.logger.warning("Socket Exception socket_send: %s" % str(e))
+            self.sock = None
+            return False
+
+        except socket.timeout as e:
+            self.logger.warning(
+                "Socket Timeout Exception " "socket_send: %s" % str(e)
+            )
+            self.sock = None
+            return False
+
+        except Exception as e:
+            self.logger.warning(
+                "General Exception " "socket_send: %s" % str(e)
+            )
+            self.sock = None
+            return False
+
+        self.logger.debug("socket_send: successful")
+
+        # mandatory on several APRS-IS servers
+        # helps to reduce the number of errors where
+        # the server only returns an abbreviated message
+        return True
+
+    def socket_reset(self):
+        """
+        Resets the socket's buffer
+        """
+        self.logger.debug("socket_reset: init")
+        _ = self.socket_receive(0)
+        self.logger.debug("socket_reset: successful")
+        return True
+
+    def socket_receive(self, rx_len):
+        """
+        Generic "Receive data from a socket"
+        """
+        self.logger.debug("socket_receive: init")
+
+        # Check if we are connected
+        if not self.sock:
+            self.logger.warning("socket_receive: not connected to APRS-IS")
+            return False
+
+        # len is zero in case we intend to
+        # reset the socket
+        if rx_len > 0:
+            self.logger.debug("socket_receive: Receiving data from APRS-IS")
+
+        # Receive content from the socket
+        try:
+            self.sock.setblocking(False)
+            self.sock.settimeout(self.socket_connect_timeout)
+            rx_buf = self.sock.recv(rx_len)
+
+        except socket.gaierror as e:
+            self.logger.warning(
+                "Socket Exception socket_receive: %s" % str(e)
+            )
+            self.sock = None
+            return False
+
+        except socket.timeout as e:
+            self.logger.warning(
+                "Socket Timeout Exception " "socket_receive: %s" % str(e)
+            )
+            self.sock = None
+            return False
+
+        except Exception as e:
+            self.logger.warning(
+                "General Exception " "socket_receive: %s" % str(e)
+            )
+            self.sock = None
+            return False
+
+        rx_buf = (
+            rx_buf.decode(self.aprs_encoding)
+            if sys.version_info[0] >= 3 else rx_buf
+        )
+
+        # There will be no data in case we reset the socket
+        if rx_len > 0:
+            self.logger.debug("Received content: {}".format(rx_buf))
+
+        self.logger.debug("socket_receive: successful")
+
+        return rx_buf.rstrip()
+
+    def send(self, body, title="", notify_type=NotifyType.INFO, **kwargs):
+        """
+        Perform APRS Notification
+        """
+
+        if not self.targets:
+            # There is no one to notify; we're done
+            self.logger.warning(
+                "There are no amateur radio call signs to notify"
+            )
+            return False
+
+        # prepare payload
+        payload = body
+
+        # sock object is "None" if we were unable to establish a connection
+        # In case of errors, the error message has already been sent
+        # to the logger object
+        if not self.socket_open():
+            return False
+
+        # We have established a successful connection
+        # to the socket server. Now send the login information
+        if not self.aprsis_login():
+            return False
+
+        # Login & authorization confirmed
+        # reset what is in our buffer
+        self.socket_reset()
+
+        # error tracking (used for function return)
+        has_error = False
+
+        # Create a copy of the targets list
+        targets = list(self.targets)
+
+        self.logger.debug("Starting Payload setup")
+
+        # Prepare the outgoing message
+        # Due to APRS's contraints, we need to do
+        # a lot of filtering before we can send
+        # the actual message
+        #
+        # First remove all characters from the
+        # payload that would break APRS
+        # see https://www.aprs.org/doc/APRS101.PDF pg. 71
+        payload = re.sub("[{}|~]+", "", payload)
+
+        payload = (  # pragma: no branch
+            APRS_COMPILED_MAP.sub(
+                lambda x: APRS_BAD_CHARMAP[x.group()], payload)
+        )
+
+        # Finally, constrain output string to 67 characters as
+        # APRS messages are limited in length
+        payload = payload[:67]
+
+        # Our outgoing message MUST end with a CRLF so
+        # let's amend our payload respectively
+        payload = payload.rstrip("\r\n") + "\r\n"
+
+        self.logger.debug("Payload setup complete: {}".format(payload))
+
+        # send the message to our target call sign(s)
+        for index in range(0, len(targets)):
+            # prepare the output string
+            # Format:
+            # Device ID/TOCALL - our call sign - target call sign - body
+            buffer = "{}>{}::{:9}:{}".format(
+                self.user, self.device_id, targets[index], payload
+            )
+
+            # and send the content to the socket
+            # Note that there will be no response from APRS and
+            # that all exceptions are handled within the 'send' method
+            self.logger.debug("Sending APRS message: {}".format(buffer))
+
+            # send the content
+            if not self.socket_send(buffer):
+                has_error = True
+                break
+
+            # Finally, reset our socket buffer
+            # we DO NOT read from the socket as we
+            # would simply listen to the default APRS-IS stream
+            self.socket_reset()
+
+        self.logger.debug("Closing socket.")
+        self.socket_close()
+        self.logger.info(
+            "Sent %d/%d APRS-IS notification(s)", index + 1, len(targets))
+        return not has_error
+
+    def url(self, privacy=False, *args, **kwargs):
+        """
+        Returns the URL built dynamically based on specified arguments.
+        """
+
+        # Define any URL parameters
+        params = {}
+
+        if self.locale != NotifyAprs.template_args["locale"]["default"]:
+            # Store our locale if not default
+            params['locale'] = self.locale
+
+        if self.delay != NotifyAprs.template_args["delay"]["default"]:
+            # Store our locale if not default
+            params['delay'] = "{:.2f}".format(self.delay)
+
+        # Extend our parameters
+        params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
+
+        # Setup Authentication
+        auth = "{user}:{password}@".format(
+            user=NotifyAprs.quote(self.user, safe=""),
+            password=self.pprint(
+                self.password, privacy, mode=PrivacyMode.Secret, safe=""
+            ),
+        )
+
+        return "{schema}://{auth}{targets}?{params}".format(
+            schema=self.secure_protocol,
+            auth=auth,
+            targets="/".join(chain(
+                [self.pprint(x, privacy, safe="") for x in self.targets],
+                [self.pprint(x, privacy, safe="")
+                 for x in self.invalid_targets],
+            )),
+            params=NotifyAprs.urlencode(params),
+        )
+
+    def __len__(self):
+        """
+        Returns the number of targets associated with this notification
+        """
+        targets = len(self.targets)
+        return targets if targets > 0 else 1
+
+    def __del__(self):
+        """
+        Ensure we close any lingering connections
+        """
+        self.socket_close()
+
+    @staticmethod
+    def parse_url(url):
+        """
+        Parses the URL and returns enough arguments that can allow
+        us to re-instantiate this object.
+
+        """
+        results = NotifyBase.parse_url(url, verify_host=False)
+        if not results:
+            # We're done early as we couldn't load the results
+            return results
+
+        # All elements are targets
+        results["targets"] = [NotifyAprs.unquote(results["host"])]
+
+        # All entries after the hostname are additional targets
+        results["targets"].extend(NotifyAprs.split_path(results["fullpath"]))
+
+        # Get Delay (if set)
+        if 'delay' in results['qsd'] and len(results['qsd']['delay']):
+            results['delay'] = NotifyAprs.unquote(results['qsd']['delay'])
+
+        # Support the 'to' variable so that we can support rooms this way too
+        # The 'to' makes it easier to use yaml configuration
+        if "to" in results["qsd"] and len(results["qsd"]["to"]):
+            results["targets"] += NotifyAprs.parse_list(results["qsd"]["to"])
+
+        # Set our APRS-IS server locale's key value and convert it to uppercase
+        if "locale" in results["qsd"] and len(results["qsd"]["locale"]):
+            results["locale"] = NotifyAprs.unquote(
+                results["qsd"]["locale"]
+            ).upper()
+
+        return results
diff --git a/lib/apprise/plugins/NotifyBark.py b/lib/apprise/plugins/bark.py
similarity index 98%
rename from lib/apprise/plugins/NotifyBark.py
rename to lib/apprise/plugins/bark.py
index edef82bd..e2f5bbfb 100644
--- a/lib/apprise/plugins/NotifyBark.py
+++ b/lib/apprise/plugins/bark.py
@@ -2,7 +2,7 @@
 # BSD 2-Clause License
 #
 # Apprise - Push Notification Library.
-# Copyright (c) 2023, Chris Caron 
+# Copyright (c) 2024, Chris Caron 
 #
 # Redistribution and use in source and binary forms, with or without
 # modification, are permitted provided that the following conditions are met:
@@ -32,13 +32,13 @@
 import requests
 import json
 
-from .NotifyBase import NotifyBase
-from ..URLBase import PrivacyMode
+from .base import NotifyBase
+from ..url import PrivacyMode
 from ..common import NotifyImageSize
 from ..common import NotifyType
 from ..utils import parse_list
 from ..utils import parse_bool
-from ..AppriseLocale import gettext_lazy as _
+from ..locale import gettext_lazy as _
 
 
 # Sounds generated off of: https://github.com/Finb/Bark/tree/master/Sounds
diff --git a/lib/apprise/plugins/NotifyBase.py b/lib/apprise/plugins/base.py
similarity index 68%
rename from lib/apprise/plugins/NotifyBase.py
rename to lib/apprise/plugins/base.py
index 5138c15c..d18f0af0 100644
--- a/lib/apprise/plugins/NotifyBase.py
+++ b/lib/apprise/plugins/base.py
@@ -2,7 +2,7 @@
 # BSD 2-Clause License
 #
 # Apprise - Push Notification Library.
-# Copyright (c) 2023, Chris Caron 
+# Copyright (c) 2024, Chris Caron 
 #
 # Redistribution and use in source and binary forms, with or without
 # modification, are permitted provided that the following conditions are met:
@@ -30,15 +30,16 @@ import asyncio
 import re
 from functools import partial
 
-from ..URLBase import URLBase
+from ..url import URLBase
 from ..common import NotifyType
+from ..utils import parse_bool
 from ..common import NOTIFY_TYPES
 from ..common import NotifyFormat
 from ..common import NOTIFY_FORMATS
 from ..common import OverflowMode
 from ..common import OVERFLOW_MODES
-from ..AppriseLocale import gettext_lazy as _
-from ..AppriseAttachment import AppriseAttachment
+from ..locale import gettext_lazy as _
+from ..apprise_attachment import AppriseAttachment
 
 
 class NotifyBase(URLBase):
@@ -135,6 +136,9 @@ class NotifyBase(URLBase):
     # Default Overflow Mode
     overflow_mode = OverflowMode.UPSTREAM
 
+    # Default Emoji Interpretation
+    interpret_emojis = False
+
     # Support Attachments; this defaults to being disabled.
     # Since apprise allows you to send attachments without a body or title
     # defined, by letting Apprise know the plugin won't support attachments
@@ -183,8 +187,66 @@ class NotifyBase(URLBase):
             # runtime.
             '_lookup_default': 'notify_format',
         },
+        'emojis': {
+            'name': _('Interpret Emojis'),
+            # SSL Certificate Authority Verification
+            'type': 'bool',
+            # Provide a default
+            'default': interpret_emojis,
+            # look up default using the following parent class value at
+            # runtime.
+            '_lookup_default': 'interpret_emojis',
+        },
     })
 
+    #
+    # Overflow Defaults / Configuration applicable to SPLIT mode only
+    #
+
+    # Display Count  [X/X]
+    #               ^^^^^^
+    #               \\\\\\
+    #               6 characters (space + count)
+    # Display Count  [XX/XX]
+    #               ^^^^^^^^
+    #               \\\\\\\\
+    #               8 characters (space + count)
+    # Display Count  [XXX/XXX]
+    #               ^^^^^^^^^^
+    #               \\\\\\\\\\
+    #               10 characters (space + count)
+    # Display Count  [XXXX/XXXX]
+    #               ^^^^^^^^^^^^
+    #               \\\\\\\\\\\\
+    #               12 characters (space + count)
+    #
+    # Given the above + some buffer we come up with the following:
+    # If this value is exceeded, display counts automatically shut off
+    overflow_max_display_count_width = 12
+
+    # The number of characters to reserver for whitespace buffering
+    # This is detected automatically, but you can enforce a value if
+    # you desire:
+    overflow_buffer = 0
+
+    # the min accepted length of a title to allow for a counter display
+    overflow_display_count_threshold = 130
+
+    # Whether or not when over-flow occurs, if the title should be repeated
+    # each time the message is split up
+    #   - None: Detect
+    #   - True: Always display title once
+    #   - False: Display the title for each occurance
+    overflow_display_title_once = None
+
+    # If this is set to to True:
+    #   The title_maxlen should be considered as a subset of the body_maxlen
+    #    Hence: len(title) + len(body) should never be greater then body_maxlen
+    #
+    # If set to False, then there is no corrorlation between title_maxlen
+    #  restrictions and that of body_maxlen
+    overflow_amalgamate_title = False
+
     def __init__(self, **kwargs):
         """
         Initialize some general configuration that will keep things consistent
@@ -194,6 +256,29 @@ class NotifyBase(URLBase):
 
         super().__init__(**kwargs)
 
+        # Store our interpret_emoji's setting
+        # If asset emoji value is set to a default of True and the user
+        #   specifies it to be false, this is accepted and False over-rides.
+        #
+        # If asset emoji value is set to a default of None, a user may
+        #   optionally over-ride this and set it to True from the Apprise
+        #   URL. ?emojis=yes
+        #
+        # If asset emoji value is set to a default of False, then all emoji's
+        # are turned off (no user over-rides allowed)
+        #
+
+        # Take a default
+        self.interpret_emojis = self.asset.interpret_emojis
+        if 'emojis' in kwargs:
+            # possibly over-ride default
+            self.interpret_emojis = True if self.interpret_emojis \
+                in (None, True) and \
+                parse_bool(
+                    kwargs.get('emojis', False),
+                    default=NotifyBase.template_args['emojis']['default']) \
+                else False
+
         if 'format' in kwargs:
             # Store the specified format if specified
             notify_format = kwargs.get('format', '')
@@ -279,6 +364,17 @@ class NotifyBase(URLBase):
             color_type=color_type,
         )
 
+    def ascii(self, notify_type):
+        """
+        Returns the ascii characters associated with the notify_type
+        """
+        if notify_type not in NOTIFY_TYPES:
+            return None
+
+        return self.asset.ascii(
+            notify_type=notify_type,
+        )
+
     def notify(self, *args, **kwargs):
         """
         Performs notification
@@ -372,6 +468,19 @@ class NotifyBase(URLBase):
         # Handle situations where the title is None
         title = '' if not title else title
 
+        # Truncate flag set with attachments ensures that only 1
+        # attachment passes through. In the event there could be many
+        # services specified, we only want to do this logic once.
+        # The logic is only applicable if ther was more then 1 attachment
+        # specified
+        overflow = self.overflow_mode if overflow is None else overflow
+        if attach and len(attach) > 1 and overflow == OverflowMode.TRUNCATE:
+            # Save first attachment
+            _attach = AppriseAttachment(attach[0], asset=self.asset)
+        else:
+            # reference same attachment
+            _attach = attach
+
         # Apply our overflow (if defined)
         for chunk in self._apply_overflow(
                 body=body, title=title, overflow=overflow,
@@ -380,7 +489,7 @@ class NotifyBase(URLBase):
             # Send notification
             yield dict(
                 body=chunk['body'], title=chunk['title'],
-                notify_type=notify_type, attach=attach,
+                notify_type=notify_type, attach=_attach,
                 body_format=body_format
             )
 
@@ -400,7 +509,7 @@ class NotifyBase(URLBase):
                 },
                 {
                     title: 'the title goes here',
-                    body: 'the message body goes here',
+                    body: 'the continued message body goes here',
                 },
 
             ]
@@ -417,7 +526,6 @@ class NotifyBase(URLBase):
             overflow = self.overflow_mode
 
         if self.title_maxlen <= 0 and len(title) > 0:
-
             if self.notify_format == NotifyFormat.HTML:
                 # Content is appended to body as html
                 body = '<{open_tag}>{title}' \
@@ -453,29 +561,148 @@ class NotifyBase(URLBase):
             response.append({'body': body, 'title': title})
             return response
 
-        elif len(title) > self.title_maxlen:
-            # Truncate our Title
-            title = title[:self.title_maxlen]
+        # a value of '2' allows for the \r\n that is applied when
+        # amalgamating the title
+        overflow_buffer = max(2, self.overflow_buffer) \
+            if (self.title_maxlen == 0 and len(title)) \
+            else self.overflow_buffer
 
-        if self.body_maxlen > 0 and len(body) <= self.body_maxlen:
+        #
+        # If we reach here in our code, then we're using TRUNCATE, or SPLIT
+        # actions which require some math to handle the data
+        #
+
+        # Handle situations where our body and title are amalamated into one
+        # calculation
+        title_maxlen = self.title_maxlen \
+            if not self.overflow_amalgamate_title \
+            else min(len(title) + self.overflow_max_display_count_width,
+                     self.title_maxlen, self.body_maxlen)
+
+        if len(title) > title_maxlen:
+            # Truncate our Title
+            title = title[:title_maxlen].rstrip()
+
+        if self.overflow_amalgamate_title and (
+                self.body_maxlen - overflow_buffer) >= title_maxlen:
+            body_maxlen = (self.body_maxlen if not title else (
+                self.body_maxlen - title_maxlen)) - overflow_buffer
+        else:
+            # status quo
+            body_maxlen = self.body_maxlen \
+                if not self.overflow_amalgamate_title else \
+                (self.body_maxlen - overflow_buffer)
+
+        if body_maxlen > 0 and len(body) <= body_maxlen:
             response.append({'body': body, 'title': title})
             return response
 
         if overflow == OverflowMode.TRUNCATE:
             # Truncate our body and return
             response.append({
-                'body': body[:self.body_maxlen],
+                'body': body[:body_maxlen].lstrip('\r\n\x0b\x0c').rstrip(),
                 'title': title,
             })
             # For truncate mode, we're done now
             return response
 
+        if self.overflow_display_title_once is None:
+            # Detect if we only display our title once or not:
+            overflow_display_title_once = \
+                True if self.overflow_amalgamate_title and \
+                body_maxlen < self.overflow_display_count_threshold \
+                else False
+        else:
+            # Take on defined value
+
+            overflow_display_title_once = self.overflow_display_title_once
+
         # If we reach here, then we are in SPLIT mode.
         # For here, we want to split the message as many times as we have to
         # in order to fit it within the designated limits.
-        response = [{
-            'body': body[i: i + self.body_maxlen],
-            'title': title} for i in range(0, len(body), self.body_maxlen)]
+        if not overflow_display_title_once and not (
+                # edge case that can occur when overflow_display_title_once is
+                # forced off, but no body exists
+                self.overflow_amalgamate_title and body_maxlen <= 0):
+
+            show_counter = title and len(body) > body_maxlen and \
+                ((self.overflow_amalgamate_title and
+                  body_maxlen >= self.overflow_display_count_threshold) or
+                 (not self.overflow_amalgamate_title and
+                  title_maxlen > self.overflow_display_count_threshold)) and (
+                title_maxlen > (self.overflow_max_display_count_width +
+                                overflow_buffer) and
+                self.title_maxlen >= self.overflow_display_count_threshold)
+
+            count = 0
+            template = ''
+            if show_counter:
+                # introduce padding
+                body_maxlen -= overflow_buffer
+
+                count = int(len(body) / body_maxlen) \
+                    + (1 if len(body) % body_maxlen else 0)
+
+                # Detect padding and prepare template
+                digits = len(str(count))
+                template = ' [{:0%d}/{:0%d}]' % (digits, digits)
+
+                # Update our counter
+                overflow_display_count_width = 4 + (digits * 2)
+                if overflow_display_count_width <= \
+                        self.overflow_max_display_count_width:
+                    if len(title) > \
+                            title_maxlen - overflow_display_count_width:
+                        # Truncate our title further
+                        title = title[:title_maxlen -
+                                      overflow_display_count_width]
+
+                else:  # Way to many messages to display
+                    show_counter = False
+
+            response = [{
+                'body': body[i: i + body_maxlen]
+                .lstrip('\r\n\x0b\x0c').rstrip(),
+                'title': title + (
+                    '' if not show_counter else
+                    template.format(idx, count))} for idx, i in
+                enumerate(range(0, len(body), body_maxlen), start=1)]
+
+        else:   # Display title once and move on
+            response = []
+            try:
+                i = range(0, len(body), body_maxlen)[0]
+
+                response.append({
+                    'body': body[i: i + body_maxlen]
+                    .lstrip('\r\n\x0b\x0c').rstrip(),
+                    'title': title,
+                })
+
+            except (ValueError, IndexError):
+                # IndexError:
+                #  - This happens if there simply was no body to display
+
+                # ValueError:
+                #  - This happens when body_maxlen < 0 (due to title being
+                #    so large)
+
+                # No worries; send title along
+                response.append({
+                    'body': '',
+                    'title': title,
+                })
+
+                # Ensure our start is set properly
+                body_maxlen = 0
+
+            # Now re-calculate based on the increased length
+            for i in range(body_maxlen, len(body), self.body_maxlen):
+                response.append({
+                    'body': body[i: i + self.body_maxlen]
+                    .lstrip('\r\n\x0b\x0c').rstrip(),
+                    'title': '',
+                })
 
         return response
 
@@ -548,6 +775,10 @@ class NotifyBase(URLBase):
                         results['overflow']))
                 del results['overflow']
 
+        # Allow emoji's override
+        if 'emojis' in results['qsd']:
+            results['emojis'] = parse_bool(results['qsd'].get('emojis'))
+
         return results
 
     @staticmethod
diff --git a/lib/apprise/plugins/NotifyBase.pyi b/lib/apprise/plugins/base.pyi
similarity index 100%
rename from lib/apprise/plugins/NotifyBase.pyi
rename to lib/apprise/plugins/base.pyi
diff --git a/lib/apprise/plugins/NotifyBoxcar.py b/lib/apprise/plugins/boxcar.py
similarity index 97%
rename from lib/apprise/plugins/NotifyBoxcar.py
rename to lib/apprise/plugins/boxcar.py
index 9d3be6ae..851cdd3d 100644
--- a/lib/apprise/plugins/NotifyBoxcar.py
+++ b/lib/apprise/plugins/boxcar.py
@@ -2,7 +2,7 @@
 # BSD 2-Clause License
 #
 # Apprise - Push Notification Library.
-# Copyright (c) 2023, Chris Caron 
+# Copyright (c) 2024, Chris Caron 
 #
 # Redistribution and use in source and binary forms, with or without
 # modification, are permitted provided that the following conditions are met:
@@ -33,20 +33,16 @@ from json import dumps
 from time import time
 from hashlib import sha1
 from itertools import chain
-try:
-    from urlparse import urlparse
+from urllib.parse import urlparse
 
-except ImportError:
-    from urllib.parse import urlparse
-
-from .NotifyBase import NotifyBase
-from ..URLBase import PrivacyMode
+from .base import NotifyBase
+from ..url import PrivacyMode
 from ..utils import parse_bool
 from ..utils import parse_list
 from ..utils import validate_regex
 from ..common import NotifyType
 from ..common import NotifyImageSize
-from ..AppriseLocale import gettext_lazy as _
+from ..locale import gettext_lazy as _
 
 # Default to sending to all devices if nothing is specified
 DEFAULT_TAG = '@all'
diff --git a/lib/apprise/plugins/NotifyBulkSMS.py b/lib/apprise/plugins/bulksms.py
similarity index 98%
rename from lib/apprise/plugins/NotifyBulkSMS.py
rename to lib/apprise/plugins/bulksms.py
index cf82a87a..29c4d7fa 100644
--- a/lib/apprise/plugins/NotifyBulkSMS.py
+++ b/lib/apprise/plugins/bulksms.py
@@ -2,7 +2,7 @@
 # BSD 2-Clause License
 #
 # Apprise - Push Notification Library.
-# Copyright (c) 2023, Chris Caron 
+# Copyright (c) 2024, Chris Caron 
 #
 # Redistribution and use in source and binary forms, with or without
 # modification, are permitted provided that the following conditions are met:
@@ -36,13 +36,13 @@ import re
 import requests
 import json
 from itertools import chain
-from .NotifyBase import NotifyBase
-from ..URLBase import PrivacyMode
+from .base import NotifyBase
+from ..url import PrivacyMode
 from ..common import NotifyType
 from ..utils import is_phone_no
 from ..utils import parse_phone_no
 from ..utils import parse_bool
-from ..AppriseLocale import gettext_lazy as _
+from ..locale import gettext_lazy as _
 
 
 IS_GROUP_RE = re.compile(
@@ -248,7 +248,7 @@ class NotifyBulkSMS(NotifyBase):
 
         if not (self.targets or self.groups):
             # We have nothing to notify
-            self.logger.warning('There are no Twist targets to notify')
+            self.logger.warning('There are no BulkSMS targets to notify')
             return False
 
         # Send in batches if identified to do so
diff --git a/lib/apprise/plugins/bulkvs.py b/lib/apprise/plugins/bulkvs.py
new file mode 100644
index 00000000..53a36300
--- /dev/null
+++ b/lib/apprise/plugins/bulkvs.py
@@ -0,0 +1,394 @@
+# -*- coding: utf-8 -*-
+# BSD 2-Clause License
+#
+# Apprise - Push Notification Library.
+# Copyright (c) 2024, Chris Caron 
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met:
+#
+# 1. Redistributions of source code must retain the above copyright notice,
+#    this list of conditions and the following disclaimer.
+#
+# 2. Redistributions in binary form must reproduce the above copyright notice,
+#    this list of conditions and the following disclaimer in the documentation
+#    and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+
+# To use this service you will need a BulkVS account
+# You will need credits (new accounts start with a few)
+#     https://www.bulkvs.com/
+
+# API is documented here:
+#   - https://portal.bulkvs.com/api/v1.0/documentation#/\
+#             Messaging/post_messageSend
+import requests
+import json
+from .base import NotifyBase
+from ..url import PrivacyMode
+from ..common import NotifyType
+from ..utils import is_phone_no
+from ..utils import parse_phone_no
+from ..utils import parse_bool
+from ..locale import gettext_lazy as _
+
+
+class NotifyBulkVS(NotifyBase):
+    """
+    A wrapper for BulkVS Notifications
+    """
+
+    # The default descriptive name associated with the Notification
+    service_name = 'BulkVS'
+
+    # The services URL
+    service_url = 'https://www.bulkvs.com/'
+
+    # All notification requests are secure
+    secure_protocol = 'bulkvs'
+
+    # A URL that takes you to the setup/help of the specific protocol
+    setup_url = 'https://github.com/caronc/apprise/wiki/Notify_bulkvs'
+
+    # BulkVS uses the http protocol with JSON requests
+    notify_url = 'https://portal.bulkvs.com/api/v1.0/messageSend'
+
+    # The maximum length of the body
+    body_maxlen = 160
+
+    # The maximum amount of texts that can go out in one batch
+    default_batch_size = 4000
+
+    # A title can not be used for SMS Messages.  Setting this to zero will
+    # cause any title (if defined) to get placed into the message body.
+    title_maxlen = 0
+
+    # Define object templates
+    templates = (
+        '{schema}://{user}:{password}@{from_phone}/{targets}',
+        '{schema}://{user}:{password}@{from_phone}',
+    )
+
+    # Define our template tokens
+    template_tokens = dict(NotifyBase.template_tokens, **{
+        'user': {
+            'name': _('User Name'),
+            'type': 'string',
+            'required': True,
+        },
+        'password': {
+            'name': _('Password'),
+            'type': 'string',
+            'private': True,
+            'required': True,
+        },
+        'from_phone': {
+            'name': _('From Phone No'),
+            'type': 'string',
+            'regex': (r'^\+?[0-9\s)(+-]+$', 'i'),
+            'map_to': 'source',
+            'required': True,
+        },
+        'target_phone': {
+            'name': _('Target Phone No'),
+            'type': 'string',
+            'prefix': '+',
+            'regex': (r'^[0-9\s)(+-]+$', 'i'),
+            'map_to': 'targets',
+        },
+        'targets': {
+            'name': _('Targets'),
+            'type': 'list:string',
+            'required': True,
+        },
+    })
+
+    # Define our template arguments
+    template_args = dict(NotifyBase.template_args, **{
+        'to': {
+            'alias_of': 'targets',
+        },
+        'from': {
+            'name': _('From Phone No'),
+            'type': 'string',
+            'regex': (r'^\+?[0-9\s)(+-]+$', 'i'),
+            'map_to': 'source',
+        },
+        'batch': {
+            'name': _('Batch Mode'),
+            'type': 'bool',
+            'default': False,
+        },
+    })
+
+    def __init__(self, source=None, targets=None, batch=None, **kwargs):
+        """
+        Initialize BulkVS Object
+        """
+        super(NotifyBulkVS, self).__init__(**kwargs)
+
+        if not (self.user and self.password):
+            msg = 'A BulkVS user/pass was not provided.'
+            self.logger.warning(msg)
+            raise TypeError(msg)
+
+        result = is_phone_no(source)
+        if not result:
+            msg = 'The Account (From) Phone # specified ' \
+                  '({}) is invalid.'.format(source)
+            self.logger.warning(msg)
+            raise TypeError(msg)
+
+        # Tidy source
+        self.source = result['full']
+
+        # Define whether or not we should operate in a batch mode
+        self.batch = self.template_args['batch']['default'] \
+            if batch is None else bool(batch)
+
+        # Parse our targets
+        self.targets = list()
+
+        has_error = False
+        for target in parse_phone_no(targets):
+            # Parse each phone number we found
+            result = is_phone_no(target)
+            if result:
+                self.targets.append(result['full'])
+                continue
+
+            has_error = True
+            self.logger.warning(
+                'Dropped invalid phone # ({}) specified.'.format(target),
+            )
+
+        if not targets and not has_error:
+            # Default the SMS Message to ourselves
+            self.targets.append(self.source)
+
+        return
+
+    def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
+        """
+        Perform BulkVS Notification
+        """
+
+        if not self.targets:
+            # We have nothing to notify
+            self.logger.warning('There are no BulkVS targets to notify')
+            return False
+
+        # Send in batches if identified to do so
+        batch_size = 1 if not self.batch else self.default_batch_size
+
+        # error tracking (used for function return)
+        has_error = False
+
+        # Prepare our headers
+        headers = {
+            'User-Agent': self.app_id,
+            'Accept': 'application/json',
+            'Content-Type': 'application/json',
+        }
+
+        # Prepare our payload
+        payload = {
+            # The To gets populated in the loop below
+            'From': self.source,
+            'To': None,
+            'Message': body,
+        }
+
+        # Authentication
+        auth = (self.user, self.password)
+
+        # Prepare our targets
+        targets = list(self.targets) if batch_size == 1 else \
+            [self.targets[index:index + batch_size]
+             for index in range(0, len(self.targets), batch_size)]
+
+        while len(targets):
+            # Get our target to notify
+            target = targets.pop(0)
+
+            # Prepare our user
+            payload['To'] = target
+
+            # Printable reference
+            if isinstance(target, list):
+                p_target = '{} targets'.format(len(target))
+
+            else:
+                p_target = target
+
+            # Some Debug Logging
+            self.logger.debug('BulkVS POST URL: {} (cert_verify={})'.format(
+                self.notify_url, self.verify_certificate))
+            self.logger.debug('BulkVS Payload: {}' .format(payload))
+
+            # Always call throttle before any remote server i/o is made
+            self.throttle()
+            try:
+                r = requests.post(
+                    self.notify_url,
+                    data=json.dumps(payload),
+                    headers=headers,
+                    auth=auth,
+                    verify=self.verify_certificate,
+                    timeout=self.request_timeout,
+                )
+
+                # A Response may look like:
+                # {
+                #   "RefId": "5a66dee6-ff7a-40ee-8218-5805c074dc01",
+                #   "From": "13109060901",
+                #   "MessageType": "SMS|MMS",
+                #   "Results": [
+                #     {
+                #       "To": "13105551212",
+                #       "Status": "SUCCESS"
+                #     },
+                #     {
+                #       "To": "13105551213",
+                #       "Status": "SUCCESS"
+                #     }
+                #   ]
+                # }
+                if r.status_code != requests.codes.ok:
+                    # We had a problem
+                    status_str = \
+                        NotifyBase.http_response_code_lookup(r.status_code)
+
+                    # set up our status code to use
+                    status_code = r.status_code
+
+                    self.logger.warning(
+                        'Failed to send BulkVS notification to {}: '
+                        '{}{}error={}.'.format(
+                            p_target,
+                            status_str,
+                            ', ' if status_str else '',
+                            status_code))
+
+                    self.logger.debug(
+                        'Response Details:\r\n{}'.format(r.content))
+
+                    # Mark our failure
+                    has_error = True
+                    continue
+
+                else:
+                    self.logger.info(
+                        'Sent BulkVS notification to {}.'.format(p_target))
+
+            except requests.RequestException as e:
+                self.logger.warning(
+                    'A Connection error occurred sending BulkVS: to %s ',
+                    p_target)
+                self.logger.debug('Socket Exception: %s' % str(e))
+
+                # Mark our failure
+                has_error = True
+                continue
+
+        return not has_error
+
+    def url(self, privacy=False, *args, **kwargs):
+        """
+        Returns the URL built dynamically based on specified arguments.
+        """
+
+        # Define any URL parameters
+        params = {
+            'batch': 'yes' if self.batch else 'no',
+        }
+
+        # Extend our parameters
+        params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
+
+        # A nice way of cleaning up the URL length a bit
+        targets = [] if len(self.targets) == 1 \
+            and self.targets[0] == self.source else self.targets
+
+        return '{schema}://{user}:{password}@{source}/{targets}' \
+            '?{params}'.format(
+                schema=self.secure_protocol,
+                source=self.source,
+                user=self.pprint(self.user, privacy, safe=''),
+                password=self.pprint(
+                    self.password, privacy, mode=PrivacyMode.Secret, safe=''),
+                targets='/'.join([
+                    NotifyBulkVS.quote('{}'.format(x), safe='+')
+                    for x in targets]),
+                params=NotifyBulkVS.urlencode(params))
+
+    def __len__(self):
+        """
+        Returns the number of targets associated with this notification
+        """
+
+        #
+        # Factor batch into calculation
+        #
+        batch_size = 1 if not self.batch else self.default_batch_size
+        targets = len(self.targets) if self.targets else 1
+        if batch_size > 1:
+            targets = int(targets / batch_size) + \
+                (1 if targets % batch_size else 0)
+
+        return targets
+
+    @staticmethod
+    def parse_url(url):
+        """
+        Parses the URL and returns enough arguments that can allow
+        us to re-instantiate this object.
+
+        """
+        results = NotifyBase.parse_url(url, verify_host=False)
+        if not results:
+            # We're done early as we couldn't load the results
+            return results
+
+        # Support the 'from'  and 'source' variable so that we can support
+        # targets this way too.
+        # The 'from' makes it easier to use yaml configuration
+        if 'from' in results['qsd'] and len(results['qsd']['from']):
+            results['source'] = \
+                NotifyBulkVS.unquote(results['qsd']['from'])
+
+            # hostname will also be a target in this case
+            results['targets'] = [
+                *NotifyBulkVS.parse_phone_no(results['host']),
+                *NotifyBulkVS.split_path(results['fullpath'])]
+
+        else:
+            # store our source
+            results['source'] = NotifyBulkVS.unquote(results['host'])
+
+            # store targets
+            results['targets'] = NotifyBulkVS.split_path(results['fullpath'])
+
+        # Support the 'to' variable so that we can support targets this way too
+        # The 'to' makes it easier to use yaml configuration
+        if 'to' in results['qsd'] and len(results['qsd']['to']):
+            results['targets'] += \
+                NotifyBulkVS.parse_phone_no(results['qsd']['to'])
+
+        # Get Batch Mode Flag
+        results['batch'] = \
+            parse_bool(results['qsd'].get(
+                'batch', NotifyBulkVS.template_args['batch']['default']))
+
+        return results
diff --git a/lib/apprise/plugins/NotifyBurstSMS.py b/lib/apprise/plugins/burstsms.py
similarity index 98%
rename from lib/apprise/plugins/NotifyBurstSMS.py
rename to lib/apprise/plugins/burstsms.py
index 59219b3d..eb19df8e 100644
--- a/lib/apprise/plugins/NotifyBurstSMS.py
+++ b/lib/apprise/plugins/burstsms.py
@@ -2,7 +2,7 @@
 # BSD 2-Clause License
 #
 # Apprise - Push Notification Library.
-# Copyright (c) 2023, Chris Caron 
+# Copyright (c) 2024, Chris Caron 
 #
 # Redistribution and use in source and binary forms, with or without
 # modification, are permitted provided that the following conditions are met:
@@ -33,14 +33,14 @@
 #
 import requests
 
-from .NotifyBase import NotifyBase
-from ..URLBase import PrivacyMode
+from .base import NotifyBase
+from ..url import PrivacyMode
 from ..common import NotifyType
 from ..utils import is_phone_no
 from ..utils import parse_phone_no
 from ..utils import parse_bool
 from ..utils import validate_regex
-from ..AppriseLocale import gettext_lazy as _
+from ..locale import gettext_lazy as _
 
 
 class BurstSMSCountryCode:
diff --git a/lib/apprise/plugins/NotifyFaast.py b/lib/apprise/plugins/chantify.py
similarity index 55%
rename from lib/apprise/plugins/NotifyFaast.py
rename to lib/apprise/plugins/chantify.py
index be3eff28..d549a59f 100644
--- a/lib/apprise/plugins/NotifyFaast.py
+++ b/lib/apprise/plugins/chantify.py
@@ -2,7 +2,7 @@
 # BSD 2-Clause License
 #
 # Apprise - Push Notification Library.
-# Copyright (c) 2023, Chris Caron 
+# Copyright (c) 2024, Chris Caron 
 #
 # Redistribution and use in source and binary forms, with or without
 # modification, are permitted provided that the following conditions are met:
@@ -26,118 +26,111 @@
 # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 # POSSIBILITY OF SUCH DAMAGE.
 
+# Chantify
+#   1. Visit https://chanify.net/
+
+# The API URL will look something like this:
+#    https://api.chanify.net/v1/sender/token
+#
+
 import requests
 
-from .NotifyBase import NotifyBase
-from ..common import NotifyImageSize
+from .base import NotifyBase
 from ..common import NotifyType
-from ..utils import parse_bool
-from ..AppriseLocale import gettext_lazy as _
 from ..utils import validate_regex
+from ..locale import gettext_lazy as _
 
 
-class NotifyFaast(NotifyBase):
+class NotifyChantify(NotifyBase):
     """
-    A wrapper for Faast Notifications
+    A wrapper for Chantify Notifications
     """
 
     # The default descriptive name associated with the Notification
-    service_name = 'Faast'
+    service_name = _('Chantify')
 
     # The services URL
-    service_url = 'http://www.faast.io/'
+    service_url = 'https://chanify.net/'
 
-    # The default protocol (this is secure for faast)
-    protocol = 'faast'
+    # The default secure protocol
+    secure_protocol = 'chantify'
 
     # A URL that takes you to the setup/help of the specific protocol
-    setup_url = 'https://github.com/caronc/apprise/wiki/Notify_faast'
+    setup_url = 'https://github.com/caronc/apprise/wiki/Notify_chantify'
 
-    # Faast uses the http protocol with JSON requests
-    notify_url = 'https://www.appnotifications.com/account/notifications.json'
-
-    # Allows the user to specify the NotifyImageSize object
-    image_size = NotifyImageSize.XY_72
+    # Notification URL
+    notify_url = 'https://api.chanify.net/v1/sender/{token}/'
 
     # Define object templates
     templates = (
-        '{schema}://{authtoken}',
+        '{schema}://{token}',
     )
 
-    # Define our template tokens
+    # The title is not used
+    title_maxlen = 0
+
+    # Define our tokens; these are the minimum tokens required required to
+    # be passed into this function (as arguments). The syntax appends any
+    # previously defined in the base package and builds onto them
     template_tokens = dict(NotifyBase.template_tokens, **{
-        'authtoken': {
-            'name': _('Authorization Token'),
+        'token': {
+            'name': _('Token'),
             'type': 'string',
             'private': True,
             'required': True,
+            'regex': (r'^[A-Z0-9_-]+$', 'i'),
         },
     })
 
     # Define our template arguments
     template_args = dict(NotifyBase.template_args, **{
-        'image': {
-            'name': _('Include Image'),
-            'type': 'bool',
-            'default': True,
-            'map_to': 'include_image',
+        'token': {
+            'alias_of': 'token',
         },
     })
 
-    def __init__(self, authtoken, include_image=True, **kwargs):
+    def __init__(self, token, **kwargs):
         """
-        Initialize Faast Object
+        Initialize Chantify Object
         """
         super().__init__(**kwargs)
 
-        # Store the Authentication Token
-        self.authtoken = validate_regex(authtoken)
-        if not self.authtoken:
-            msg = 'An invalid Faast Authentication Token ' \
-                  '({}) was specified.'.format(authtoken)
+        self.token = validate_regex(
+            token, *self.template_tokens['token']['regex'])
+        if not self.token:
+            msg = 'The Chantify token specified ({}) is invalid.'\
+                .format(token)
             self.logger.warning(msg)
             raise TypeError(msg)
 
-        # Associate an image with our post
-        self.include_image = include_image
-
         return
 
     def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
         """
-        Perform Faast Notification
+        Send our notification
         """
 
+        # prepare our headers
         headers = {
             'User-Agent': self.app_id,
-            'Content-Type': 'multipart/form-data'
+            'Content-Type': 'application/x-www-form-urlencoded',
         }
 
-        # prepare JSON Object
+        # Our Message
         payload = {
-            'user_credentials': self.authtoken,
-            'title': title,
-            'message': body,
+            'text': body
         }
 
-        # Acquire our image if we're configured to do so
-        image_url = None if not self.include_image \
-            else self.image_url(notify_type)
-
-        if image_url:
-            payload['icon_url'] = image_url
-
-        self.logger.debug('Faast POST URL: %s (cert_verify=%r)' % (
-            self.notify_url, self.verify_certificate,
-        ))
-        self.logger.debug('Faast Payload: %s' % str(payload))
+        self.logger.debug('Chantify GET URL: %s (cert_verify=%r)' % (
+            self.notify_url, self.verify_certificate))
+        self.logger.debug('Chantify Payload: %s' % str(payload))
 
         # Always call throttle before any remote server i/o is made
         self.throttle()
 
         try:
             r = requests.post(
-                self.notify_url,
+                self.notify_url.format(token=self.token),
                 data=payload,
                 headers=headers,
                 verify=self.verify_certificate,
@@ -146,10 +139,10 @@ class NotifyFaast(NotifyBase):
             if r.status_code != requests.codes.ok:
                 # We had a problem
                 status_str = \
-                    NotifyFaast.http_response_code_lookup(r.status_code)
+                    NotifyChantify.http_response_code_lookup(r.status_code)
 
                 self.logger.warning(
-                    'Failed to send Faast notification:'
+                    'Failed to send Chantify notification: '
                     '{}{}error={}.'.format(
                         status_str,
                         ', ' if status_str else '',
@@ -161,12 +154,12 @@ class NotifyFaast(NotifyBase):
                 return False
 
             else:
-                self.logger.info('Sent Faast notification.')
+                self.logger.info('Sent Chantify notification.')
 
         except requests.RequestException as e:
             self.logger.warning(
-                'A Connection error occurred sending Faast notification.',
-            )
+                'A Connection error occurred sending Chantify '
+                'notification.')
             self.logger.debug('Socket Exception: %s' % str(e))
 
             # Return; we're done
@@ -179,18 +172,13 @@ class NotifyFaast(NotifyBase):
         Returns the URL built dynamically based on specified arguments.
         """
 
-        # Define any URL parameters
-        params = {
-            'image': 'yes' if self.include_image else 'no',
-        }
+        # Prepare our parameters
+        params = self.url_parameters(privacy=privacy, *args, **kwargs)
 
-        # Extend our parameters
-        params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
-
-        return '{schema}://{authtoken}/?{params}'.format(
-            schema=self.protocol,
-            authtoken=self.pprint(self.authtoken, privacy, safe=''),
-            params=NotifyFaast.urlencode(params),
+        return '{schema}://{token}/?{params}'.format(
+            schema=self.secure_protocol,
+            token=self.pprint(self.token, privacy, safe=''),
+            params=NotifyChantify.urlencode(params),
         )
 
     @staticmethod
@@ -200,16 +188,19 @@ class NotifyFaast(NotifyBase):
         us to re-instantiate this object.
 
         """
+
+        # parse_url already handles getting the `user` and `password` fields
+        # populated.
         results = NotifyBase.parse_url(url, verify_host=False)
         if not results:
             # We're done early as we couldn't load the results
             return results
 
-        # Store our authtoken using the host
-        results['authtoken'] = NotifyFaast.unquote(results['host'])
+        # Allow over-ride
+        if 'token' in results['qsd'] and len(results['qsd']['token']):
+            results['token'] = NotifyChantify.unquote(results['qsd']['token'])
 
-        # Include image with our post
-        results['include_image'] = \
-            parse_bool(results['qsd'].get('image', True))
+        else:
+            results['token'] = NotifyChantify.unquote(results['host'])
 
         return results
diff --git a/lib/apprise/plugins/NotifyClickSend.py b/lib/apprise/plugins/clicksend.py
similarity index 94%
rename from lib/apprise/plugins/NotifyClickSend.py
rename to lib/apprise/plugins/clicksend.py
index 670e74e8..9ade1055 100644
--- a/lib/apprise/plugins/NotifyClickSend.py
+++ b/lib/apprise/plugins/clicksend.py
@@ -2,7 +2,7 @@
 # BSD 2-Clause License
 #
 # Apprise - Push Notification Library.
-# Copyright (c) 2023, Chris Caron 
+# Copyright (c) 2024, Chris Caron 
 #
 # Redistribution and use in source and binary forms, with or without
 # modification, are permitted provided that the following conditions are met:
@@ -41,15 +41,14 @@
 #
 import requests
 from json import dumps
-from base64 import b64encode
 
-from .NotifyBase import NotifyBase
-from ..URLBase import PrivacyMode
+from .base import NotifyBase
+from ..url import PrivacyMode
 from ..common import NotifyType
 from ..utils import is_phone_no
 from ..utils import parse_phone_no
 from ..utils import parse_bool
-from ..AppriseLocale import gettext_lazy as _
+from ..locale import gettext_lazy as _
 
 # Extend HTTP Error Messages
 CLICKSEND_HTTP_ERROR_MAP = {
@@ -89,7 +88,7 @@ class NotifyClickSend(NotifyBase):
 
     # Define object templates
     templates = (
-        '{schema}://{user}:{password}@{targets}',
+        '{schema}://{user}:{apikey}@{targets}',
     )
 
     # Define our template tokens
@@ -99,11 +98,12 @@ class NotifyClickSend(NotifyBase):
             'type': 'string',
             'required': True,
         },
-        'password': {
-            'name': _('Password'),
+        'apikey': {
+            'name': _('API Key'),
             'type': 'string',
             'private': True,
             'required': True,
+            'map_to': 'password',
         },
         'target_phone': {
             'name': _('Target Phone No'),
@@ -124,6 +124,9 @@ class NotifyClickSend(NotifyBase):
         'to': {
             'alias_of': 'targets',
         },
+        'key': {
+            'alias_of': 'apikey',
+        },
         'batch': {
             'name': _('Batch Mode'),
             'type': 'bool',
@@ -174,9 +177,6 @@ class NotifyClickSend(NotifyBase):
         headers = {
             'User-Agent': self.app_id,
             'Content-Type': 'application/json; charset=utf-8',
-            'Authorization': 'Basic {}'.format(
-                b64encode('{}:{}'.format(
-                    self.user, self.password).encode('utf-8'))),
         }
 
         # error tracking (used for function return)
@@ -208,6 +208,7 @@ class NotifyClickSend(NotifyBase):
                 r = requests.post(
                     self.notify_url,
                     data=dumps(payload),
+                    auth=(self.user, self.password),
                     headers=headers,
                     verify=self.verify_certificate,
                     timeout=self.request_timeout,
@@ -322,6 +323,12 @@ class NotifyClickSend(NotifyBase):
         results['batch'] = \
             parse_bool(results['qsd'].get('batch', False))
 
+        # API Key
+        if 'key' in results['qsd'] and len(results['qsd']['key']):
+            # Extract the API Key from an argument
+            results['password'] = \
+                NotifyClickSend.unquote(results['qsd']['key'])
+
         # Support the 'to' variable so that we can support rooms this way too
         # The 'to' makes it easier to use yaml configuration
         if 'to' in results['qsd'] and len(results['qsd']['to']):
diff --git a/lib/apprise/plugins/NotifyForm.py b/lib/apprise/plugins/custom_form.py
similarity index 99%
rename from lib/apprise/plugins/NotifyForm.py
rename to lib/apprise/plugins/custom_form.py
index 066f299b..0f36643f 100644
--- a/lib/apprise/plugins/NotifyForm.py
+++ b/lib/apprise/plugins/custom_form.py
@@ -2,7 +2,7 @@
 # BSD 2-Clause License
 #
 # Apprise - Push Notification Library.
-# Copyright (c) 2023, Chris Caron 
+# Copyright (c) 2024, Chris Caron 
 #
 # Redistribution and use in source and binary forms, with or without
 # modification, are permitted provided that the following conditions are met:
@@ -29,11 +29,11 @@
 import re
 import requests
 
-from .NotifyBase import NotifyBase
-from ..URLBase import PrivacyMode
+from .base import NotifyBase
+from ..url import PrivacyMode
 from ..common import NotifyImageSize
 from ..common import NotifyType
-from ..AppriseLocale import gettext_lazy as _
+from ..locale import gettext_lazy as _
 
 
 class FORMPayloadField:
diff --git a/lib/apprise/plugins/NotifyJSON.py b/lib/apprise/plugins/custom_json.py
similarity index 98%
rename from lib/apprise/plugins/NotifyJSON.py
rename to lib/apprise/plugins/custom_json.py
index a8ab7adc..e0d7a675 100644
--- a/lib/apprise/plugins/NotifyJSON.py
+++ b/lib/apprise/plugins/custom_json.py
@@ -2,7 +2,7 @@
 # BSD 2-Clause License
 #
 # Apprise - Push Notification Library.
-# Copyright (c) 2023, Chris Caron 
+# Copyright (c) 2024, Chris Caron 
 #
 # Redistribution and use in source and binary forms, with or without
 # modification, are permitted provided that the following conditions are met:
@@ -30,11 +30,11 @@ import requests
 import base64
 from json import dumps
 
-from .NotifyBase import NotifyBase
-from ..URLBase import PrivacyMode
+from .base import NotifyBase
+from ..url import PrivacyMode
 from ..common import NotifyImageSize
 from ..common import NotifyType
-from ..AppriseLocale import gettext_lazy as _
+from ..locale import gettext_lazy as _
 
 
 class JSONPayloadField:
diff --git a/lib/apprise/plugins/NotifyXML.py b/lib/apprise/plugins/custom_xml.py
similarity index 98%
rename from lib/apprise/plugins/NotifyXML.py
rename to lib/apprise/plugins/custom_xml.py
index 20eeb114..b7928fce 100644
--- a/lib/apprise/plugins/NotifyXML.py
+++ b/lib/apprise/plugins/custom_xml.py
@@ -2,7 +2,7 @@
 # BSD 2-Clause License
 #
 # Apprise - Push Notification Library.
-# Copyright (c) 2023, Chris Caron 
+# Copyright (c) 2024, Chris Caron 
 #
 # Redistribution and use in source and binary forms, with or without
 # modification, are permitted provided that the following conditions are met:
@@ -30,11 +30,11 @@ import re
 import requests
 import base64
 
-from .NotifyBase import NotifyBase
-from ..URLBase import PrivacyMode
+from .base import NotifyBase
+from ..url import PrivacyMode
 from ..common import NotifyImageSize
 from ..common import NotifyType
-from ..AppriseLocale import gettext_lazy as _
+from ..locale import gettext_lazy as _
 
 
 class XMLPayloadField:
diff --git a/lib/apprise/plugins/NotifyD7Networks.py b/lib/apprise/plugins/d7networks.py
similarity index 99%
rename from lib/apprise/plugins/NotifyD7Networks.py
rename to lib/apprise/plugins/d7networks.py
index 3e7787da..ad55e219 100644
--- a/lib/apprise/plugins/NotifyD7Networks.py
+++ b/lib/apprise/plugins/d7networks.py
@@ -2,7 +2,7 @@
 # BSD 2-Clause License
 #
 # Apprise - Push Notification Library.
-# Copyright (c) 2023, Chris Caron 
+# Copyright (c) 2024, Chris Caron 
 #
 # Redistribution and use in source and binary forms, with or without
 # modification, are permitted provided that the following conditions are met:
@@ -39,13 +39,13 @@ import requests
 from json import dumps
 from json import loads
 
-from .NotifyBase import NotifyBase
+from .base import NotifyBase
 from ..common import NotifyType
 from ..utils import is_phone_no
 from ..utils import parse_phone_no
 from ..utils import validate_regex
 from ..utils import parse_bool
-from ..AppriseLocale import gettext_lazy as _
+from ..locale import gettext_lazy as _
 
 # Extend HTTP Error Messages
 D7NETWORKS_HTTP_ERROR_MAP = {
diff --git a/lib/apprise/plugins/NotifyDapnet.py b/lib/apprise/plugins/dapnet.py
similarity index 98%
rename from lib/apprise/plugins/NotifyDapnet.py
rename to lib/apprise/plugins/dapnet.py
index 5848b688..60a18acd 100644
--- a/lib/apprise/plugins/NotifyDapnet.py
+++ b/lib/apprise/plugins/dapnet.py
@@ -2,7 +2,7 @@
 # BSD 2-Clause License
 #
 # Apprise - Push Notification Library.
-# Copyright (c) 2023, Chris Caron 
+# Copyright (c) 2024, Chris Caron 
 #
 # Redistribution and use in source and binary forms, with or without
 # modification, are permitted provided that the following conditions are met:
@@ -51,9 +51,9 @@ from json import dumps
 import requests
 from requests.auth import HTTPBasicAuth
 
-from .NotifyBase import NotifyBase
-from ..AppriseLocale import gettext_lazy as _
-from ..URLBase import PrivacyMode
+from .base import NotifyBase
+from ..locale import gettext_lazy as _
+from ..url import PrivacyMode
 from ..common import NotifyType
 from ..utils import is_call_sign
 from ..utils import parse_call_sign
diff --git a/lib/apprise/plugins/NotifyDBus.py b/lib/apprise/plugins/dbus.py
similarity index 98%
rename from lib/apprise/plugins/NotifyDBus.py
rename to lib/apprise/plugins/dbus.py
index 46f8b9d0..6be4fc2d 100644
--- a/lib/apprise/plugins/NotifyDBus.py
+++ b/lib/apprise/plugins/dbus.py
@@ -2,7 +2,7 @@
 # BSD 2-Clause License
 #
 # Apprise - Push Notification Library.
-# Copyright (c) 2023, Chris Caron 
+# Copyright (c) 2024, Chris Caron 
 #
 # Redistribution and use in source and binary forms, with or without
 # modification, are permitted provided that the following conditions are met:
@@ -26,15 +26,12 @@
 # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 # POSSIBILITY OF SUCH DAMAGE.
 
-from __future__ import absolute_import
-from __future__ import print_function
-
 import sys
-from .NotifyBase import NotifyBase
+from .base import NotifyBase
 from ..common import NotifyImageSize
 from ..common import NotifyType
 from ..utils import parse_bool
-from ..AppriseLocale import gettext_lazy as _
+from ..locale import gettext_lazy as _
 
 # Default our global support flag
 NOTIFY_DBUS_SUPPORT_ENABLED = False
diff --git a/lib/apprise/plugins/NotifyDingTalk.py b/lib/apprise/plugins/dingtalk.py
similarity index 98%
rename from lib/apprise/plugins/NotifyDingTalk.py
rename to lib/apprise/plugins/dingtalk.py
index 91bfcd6f..2ca1bc55 100644
--- a/lib/apprise/plugins/NotifyDingTalk.py
+++ b/lib/apprise/plugins/dingtalk.py
@@ -2,7 +2,7 @@
 # BSD 2-Clause License
 #
 # Apprise - Push Notification Library.
-# Copyright (c) 2023, Chris Caron 
+# Copyright (c) 2024, Chris Caron 
 #
 # Redistribution and use in source and binary forms, with or without
 # modification, are permitted provided that the following conditions are met:
@@ -34,13 +34,13 @@ import base64
 import requests
 from json import dumps
 
-from .NotifyBase import NotifyBase
-from ..URLBase import PrivacyMode
+from .base import NotifyBase
+from ..url import PrivacyMode
 from ..common import NotifyFormat
 from ..common import NotifyType
 from ..utils import parse_list
 from ..utils import validate_regex
-from ..AppriseLocale import gettext_lazy as _
+from ..locale import gettext_lazy as _
 
 # Register at https://dingtalk.com
 #   - Download their PC based software as it is the only way you can create
diff --git a/lib/apprise/plugins/NotifyDiscord.py b/lib/apprise/plugins/discord.py
similarity index 93%
rename from lib/apprise/plugins/NotifyDiscord.py
rename to lib/apprise/plugins/discord.py
index f87b6694..14c6152b 100644
--- a/lib/apprise/plugins/NotifyDiscord.py
+++ b/lib/apprise/plugins/discord.py
@@ -2,7 +2,7 @@
 # BSD 2-Clause License
 #
 # Apprise - Push Notification Library.
-# Copyright (c) 2023, Chris Caron 
+# Copyright (c) 2024, Chris Caron 
 #
 # Redistribution and use in source and binary forms, with or without
 # modification, are permitted provided that the following conditions are met:
@@ -50,14 +50,19 @@ from datetime import timedelta
 from datetime import datetime
 from datetime import timezone
 
-from .NotifyBase import NotifyBase
+from .base import NotifyBase
 from ..common import NotifyImageSize
 from ..common import NotifyFormat
 from ..common import NotifyType
 from ..utils import parse_bool
 from ..utils import validate_regex
-from ..AppriseLocale import gettext_lazy as _
-from ..attachment.AttachBase import AttachBase
+from ..locale import gettext_lazy as _
+from ..attachment.base import AttachBase
+
+
+# Used to detect user/role IDs
+USER_ROLE_DETECTION_RE = re.compile(
+    r'\s*(?:<@(?P&?)(?P[0-9]+)>|@(?P[a-z0-9]+))', re.I)
 
 
 class NotifyDiscord(NotifyBase):
@@ -100,6 +105,10 @@ class NotifyDiscord(NotifyBase):
     # The maximum allowable characters allowed in the body per message
     body_maxlen = 2000
 
+    # The 2000 characters above defined by the body_maxlen include that of the
+    # title.  Setting this to True ensures overflow options behave properly
+    overflow_amalgamate_title = True
+
     # Discord has a limit of the number of fields you can include in an
     # embeds message. This value allows the discord message to safely
     # break into multiple messages to handle these cases.
@@ -336,6 +345,33 @@ class NotifyDiscord(NotifyBase):
                 payload['content'] = \
                     body if not title else "{}\r\n{}".format(title, body)
 
+            # parse for user id's <@123> and role IDs <@&456>
+            results = USER_ROLE_DETECTION_RE.findall(body)
+            if results:
+                payload['allow_mentions'] = {
+                    'parse': [],
+                    'users': [],
+                    'roles': [],
+                }
+
+                _content = []
+                for (is_role, no, value) in results:
+                    if value:
+                        payload['allow_mentions']['parse'].append(value)
+                        _content.append(f'@{value}')
+
+                    elif is_role:
+                        payload['allow_mentions']['roles'].append(no)
+                        _content.append(f'<@&{no}>')
+
+                    else:  # is_user
+                        payload['allow_mentions']['users'].append(no)
+                        _content.append(f'<@{no}>')
+
+                if self.notify_format == NotifyFormat.MARKDOWN:
+                    # Add pingable elements to content field
+                    payload['content'] = '👉 ' + ' '.join(_content)
+
             if not self._send(payload, params=params):
                 # We failed to post our message
                 return False
@@ -360,16 +396,21 @@ class NotifyDiscord(NotifyBase):
                 'wait': True,
             })
 
+            #
             # Remove our text/title based content for attachment use
+            #
             if 'embeds' in payload:
-                # Markdown
                 del payload['embeds']
 
             if 'content' in payload:
-                # Markdown
                 del payload['content']
 
+            if 'allow_mentions' in payload:
+                del payload['allow_mentions']
+
+            #
             # Send our attachments
+            #
             for attachment in attach:
                 self.logger.info(
                     'Posting Discord Attachment {}'.format(attachment.name))
diff --git a/lib/apprise/plugins/NotifyEmail.py b/lib/apprise/plugins/email.py
similarity index 94%
rename from lib/apprise/plugins/NotifyEmail.py
rename to lib/apprise/plugins/email.py
index db70c8ef..142c93cf 100644
--- a/lib/apprise/plugins/NotifyEmail.py
+++ b/lib/apprise/plugins/email.py
@@ -2,7 +2,7 @@
 # BSD 2-Clause License
 #
 # Apprise - Push Notification Library.
-# Copyright (c) 2023, Chris Caron 
+# Copyright (c) 2024, Chris Caron 
 #
 # Redistribution and use in source and binary forms, with or without
 # modification, are permitted provided that the following conditions are met:
@@ -41,12 +41,12 @@ from socket import error as SocketError
 from datetime import datetime
 from datetime import timezone
 
-from .NotifyBase import NotifyBase
-from ..URLBase import PrivacyMode
+from .base import NotifyBase
+from ..url import PrivacyMode
 from ..common import NotifyFormat, NotifyType
 from ..conversion import convert_between
-from ..utils import is_email, parse_emails
-from ..AppriseLocale import gettext_lazy as _
+from ..utils import is_ipaddr, is_email, parse_emails, is_hostname
+from ..locale import gettext_lazy as _
 from ..logger import logger
 
 # Globally Default encoding mode set to Quoted Printable.
@@ -295,6 +295,21 @@ EMAIL_TEMPLATES = (
         },
     ),
 
+    # Comcast.net
+    (
+        'Comcast.net',
+        re.compile(
+            r'^((?P