diff --git a/kazoo/client.py b/kazoo/client.py index 27b7c384..b9f566be 100644 --- a/kazoo/client.py +++ b/kazoo/client.py @@ -1,10 +1,13 @@ """Kazoo Zookeeper Client""" +from __future__ import annotations + from collections import defaultdict, deque from functools import partial import inspect import logging from os.path import split import re +from typing import TYPE_CHECKING, overload import warnings from kazoo.exceptions import ( @@ -63,6 +66,20 @@ from kazoo.recipe.queue import Queue, LockingQueue from kazoo.recipe.watchers import ChildrenWatch, DataWatch +if TYPE_CHECKING: + from typing import ( + Any, + List, + Optional, + Sequence, + Tuple, + Union, + Callable, + Literal, + ) + from kazoo.protocol.states import ZnodeStat + + WatchListener = Callable[[WatchedEvent], None] CLOSED_STATES = ( KeeperState.EXPIRED_SESSION, @@ -268,7 +285,7 @@ def __init__( self._stopped.set() self._writer_stopped.set() - self.retry = self._conn_retry = None + _retry = self._conn_retry = None if type(connection_retry) is dict: self._conn_retry = KazooRetry(**connection_retry) @@ -276,9 +293,9 @@ def __init__( self._conn_retry = connection_retry if type(command_retry) is dict: - self.retry = KazooRetry(**command_retry) + _retry = KazooRetry(**command_retry) elif type(command_retry) is KazooRetry: - self.retry = command_retry + _retry = command_retry if type(self._conn_retry) is KazooRetry: if self.handler.sleep_func != self._conn_retry.sleep_func: @@ -287,14 +304,14 @@ def __init__( " must use the same sleep func" ) - if type(self.retry) is KazooRetry: - if self.handler.sleep_func != self.retry.sleep_func: + if type(_retry) is KazooRetry: + if self.handler.sleep_func != _retry.sleep_func: raise ConfigurationError( "Command retry handler and event handler " "must use the same sleep func" ) - if self.retry is None or self._conn_retry is None: + if _retry is None or self._conn_retry is None: old_retry_keys = dict(_RETRY_COMPAT_DEFAULTS) for key in old_retry_keys: try: @@ -310,7 +327,7 @@ def __init__( except KeyError: pass - retry_keys = {} + retry_keys: Any = {} for oldname, value in old_retry_keys.items(): retry_keys[_RETRY_COMPAT_MAPPING[oldname]] = value @@ -318,8 +335,8 @@ def __init__( self._conn_retry = KazooRetry( sleep_func=self.handler.sleep_func, **retry_keys ) - if self.retry is None: - self.retry = KazooRetry( + if _retry is None: + _retry = KazooRetry( sleep_func=self.handler.sleep_func, **retry_keys ) @@ -364,14 +381,7 @@ def __init__( sasl_options=sasl_options, ) - # Every retry call should have its own copy of the retry helper - # to avoid shared retry counts - self._retry = self.retry - - def _retry(*args, **kwargs): - return self._retry.copy()(*args, **kwargs) - - self.retry = _retry + self._retry = _retry self.Barrier = partial(Barrier, self) self.Counter = partial(Counter, self) @@ -398,6 +408,12 @@ def _retry(*args, **kwargs): % (kwargs.keys(),) ) + @property + def retry(self) -> KazooRetry: + # Every retry call should have its own copy of the retry helper + # to avoid shared retry counts + return self._retry.copy() + def _reset(self): """Resets a variety of client states for a new connection.""" self._queue = deque() @@ -910,14 +926,14 @@ def sync(self, path): def create( self, - path, - value=b"", - acl=None, - ephemeral=False, - sequence=False, - makepath=False, - include_data=False, - ): + path: str, + value: bytes = b"", + acl: Optional[Sequence[ACL]] = None, + ephemeral: bool = False, + sequence: bool = False, + makepath: bool = False, + include_data: bool = False, + ) -> Union[str, Tuple[str, ZnodeStat]]: """Create a node with the given value as its data. Optionally set an ACL on the node. @@ -1122,7 +1138,7 @@ def _create_async_inner( raise async_result.exception return async_result - def ensure_path(self, path, acl=None): + def ensure_path(self, path: str, acl: Optional[List[ACL]] = None) -> bool: """Recursively create a path if it doesn't exist. :param path: Path of node. @@ -1171,7 +1187,9 @@ def exists_completion(path, result): return async_result - def exists(self, path, watch=None): + def exists( + self, path: str, watch: Optional[WatchListener] = None + ) -> Optional[ZnodeStat]: """Check if a node exists. If a watch is provided, it will be left on the node with the @@ -1211,7 +1229,9 @@ def exists_async(self, path, watch=None): ) return async_result - def get(self, path, watch=None): + def get( + self, path: str, watch: Optional[WatchListener] = None + ) -> Tuple[bytes, ZnodeStat]: """Get the value of a node. If a watch is provided, it will be left on the node with the @@ -1254,7 +1274,53 @@ def get_async(self, path, watch=None): ) return async_result - def get_children(self, path, watch=None, include_data=False): + @overload + def get_children( # noqa: F811 + self, + path: str, + ) -> List[str]: + ... + + @overload + def get_children( # noqa: F811 + self, + path: str, + watch: WatchListener, + ) -> List[str]: + ... + + @overload + def get_children( # noqa: F811 + self, + path: str, + watch: Optional[WatchListener], + ) -> List[str]: + ... + + @overload + def get_children( # noqa: F811 + self, + path: str, + watch: Optional[WatchListener], + include_data: Literal[True], + ) -> List[Tuple[str, ZnodeStat]]: + ... + + @overload + def get_children( # noqa: F811 + self, + path: str, + watch: Optional[WatchListener] = None, + include_data: Literal[False] = False, + ) -> List[str]: + ... + + def get_children( # noqa: F811 + self, + path: str, + watch: Optional[WatchListener] = None, + include_data: bool = False, + ) -> Union[List[Tuple[str, ZnodeStat]], List[str]]: """Get a list of child nodes of a path. If a watch is provided it will be left on the node with the @@ -1400,7 +1466,7 @@ def set_acls_async(self, path, acls, version=-1): ) return async_result - def set(self, path, value, version=-1): + def set(self, path: str, value: bytes, version: int = -1) -> ZnodeStat: """Set the value of a node. If the version of the node being updated is newer than the @@ -1473,7 +1539,9 @@ def transaction(self): """ return TransactionRequest(self) - def delete(self, path, version=-1, recursive=False): + def delete( + self, path: str, version: int = -1, recursive: bool = False + ) -> Optional[bool]: """Delete a node. The call will succeed if such a node exists, and the given diff --git a/kazoo/recipe/barrier.py b/kazoo/recipe/barrier.py index 683e807b..41bbd95f 100644 --- a/kazoo/recipe/barrier.py +++ b/kazoo/recipe/barrier.py @@ -4,13 +4,24 @@ :Status: Unknown """ +from __future__ import annotations + import os import socket +from threading import Event +from typing import TYPE_CHECKING, cast import uuid from kazoo.exceptions import KazooException, NoNodeError, NodeExistsError from kazoo.protocol.states import EventType +if TYPE_CHECKING: + from typing import Optional + from typing_extensions import Literal + + from kazoo.client import KazooClient + from kazoo.protocol.states import WatchedEvent + class Barrier(object): """Kazoo Barrier @@ -27,7 +38,7 @@ class Barrier(object): """ - def __init__(self, client, path): + def __init__(self, client: KazooClient, path: str): """Create a Kazoo Barrier :param client: A :class:`~kazoo.client.KazooClient` instance. @@ -37,11 +48,11 @@ def __init__(self, client, path): self.client = client self.path = path - def create(self): + def create(self) -> None: """Establish the barrier if it doesn't exist already""" self.client.retry(self.client.ensure_path, self.path) - def remove(self): + def remove(self) -> bool: """Remove the barrier :returns: Whether the barrier actually needed to be removed. @@ -54,7 +65,7 @@ def remove(self): except NoNodeError: return False - def wait(self, timeout=None): + def wait(self, timeout: Optional[float] = None) -> bool: """Wait on the barrier to be cleared :returns: True if the barrier has been cleared, otherwise @@ -62,9 +73,9 @@ def wait(self, timeout=None): :rtype: bool """ - cleared = self.client.handler.event_object() + cleared = cast(Event, self.client.handler.event_object()) - def wait_for_clear(event): + def wait_for_clear(event: WatchedEvent) -> None: if event.type == EventType.DELETED: cleared.set() @@ -93,7 +104,13 @@ class DoubleBarrier(object): """ - def __init__(self, client, path, num_clients, identifier=None): + def __init__( + self, + client: KazooClient, + path: str, + num_clients: int, + identifier: Optional[str] = None, + ): """Create a Double Barrier :param client: A :class:`~kazoo.client.KazooClient` instance. @@ -118,7 +135,7 @@ def __init__(self, client, path, num_clients, identifier=None): self.node_name = uuid.uuid4().hex self.create_path = self.path + "/" + self.node_name - def enter(self): + def enter(self) -> None: """Enter the barrier, blocks until all nodes have entered""" try: self.client.retry(self._inner_enter) @@ -128,7 +145,7 @@ def enter(self): self._best_effort_cleanup() self.participating = False - def _inner_enter(self): + def _inner_enter(self) -> Literal[True]: # make sure our barrier parent node exists if not self.assured_path: self.client.ensure_path(self.path) @@ -145,7 +162,7 @@ def _inner_enter(self): except NodeExistsError: pass - def created(event): + def created(event: WatchedEvent) -> None: if event.type == EventType.CREATED: ready.set() @@ -159,7 +176,7 @@ def created(event): self.client.ensure_path(self.path + "/ready") return True - def leave(self): + def leave(self) -> None: """Leave the barrier, blocks until all nodes have left""" try: self.client.retry(self._inner_leave) @@ -168,7 +185,7 @@ def leave(self): self._best_effort_cleanup() self.participating = False - def _inner_leave(self): + def _inner_leave(self) -> Literal[True]: # Delete the ready node if its around try: self.client.delete(self.path + "/ready") @@ -188,7 +205,7 @@ def _inner_leave(self): ready = self.client.handler.event_object() - def deleted(event): + def deleted(event: WatchedEvent) -> None: if event.type == EventType.DELETED: ready.set() @@ -214,7 +231,7 @@ def deleted(event): # Wait for the lowest to be deleted ready.wait() - def _best_effort_cleanup(self): + def _best_effort_cleanup(self) -> None: try: self.client.retry(self.client.delete, self.create_path) except NoNodeError: diff --git a/kazoo/recipe/counter.py b/kazoo/recipe/counter.py index 3b2cc339..3e5a4c4b 100644 --- a/kazoo/recipe/counter.py +++ b/kazoo/recipe/counter.py @@ -4,9 +4,20 @@ :Status: Unknown """ +from __future__ import annotations + +import struct +from typing import cast, TYPE_CHECKING + from kazoo.exceptions import BadVersionError from kazoo.retry import ForceRetryError -import struct + +if TYPE_CHECKING: + from typing import Optional, Tuple, Type, Union + + from kazoo.client import KazooClient + + CountT = Union[int, float] class Counter(object): @@ -58,7 +69,13 @@ class Counter(object): """ - def __init__(self, client, path, default=0, support_curator=False): + def __init__( + self, + client: KazooClient, + path: str, + default: CountT = 0, + support_curator: bool = False, + ): """Create a Kazoo Counter :param client: A :class:`~kazoo.client.KazooClient` instance. @@ -70,46 +87,50 @@ def __init__(self, client, path, default=0, support_curator=False): """ self.client = client self.path = path - self.default = default - self.default_type = type(default) + self.default: CountT = default + self.default_type: Type[CountT] = type(default) self.support_curator = support_curator self._ensured_path = False - self.pre_value = None - self.post_value = None + self.pre_value: Optional[CountT] = None + self.post_value: Optional[CountT] = None if self.support_curator and not isinstance(self.default, int): raise TypeError( "when support_curator is enabled the default " "type must be an int" ) - def _ensure_node(self): + def _ensure_node(self) -> None: if not self._ensured_path: # make sure our node exists self.client.ensure_path(self.path) self._ensured_path = True - def _value(self): + def _value(self) -> Tuple[CountT, int]: self._ensure_node() old, stat = self.client.get(self.path) if self.support_curator: - old = struct.unpack(">i", old)[0] if old != b"" else self.default + parsed_old: Union[int, float, str] = ( + cast(int, struct.unpack(">i", old)[0]) + if old != b"" + else self.default + ) else: - old = old.decode("ascii") if old != b"" else self.default + parsed_old = old.decode("ascii") if old != b"" else self.default version = stat.version - data = self.default_type(old) + data = self.default_type(parsed_old) return data, version @property - def value(self): + def value(self) -> CountT: return self._value()[0] - def _change(self, value): + def _change(self, value: CountT) -> "Counter": if not isinstance(value, self.default_type): raise TypeError("invalid type for value change") self.client.retry(self._inner_change, value) return self - def _inner_change(self, value): + def _inner_change(self, value: CountT) -> None: self.pre_value, version = self._value() post_value = self.pre_value + value if self.support_curator: @@ -123,10 +144,10 @@ def _inner_change(self, value): raise ForceRetryError() self.post_value = post_value - def __add__(self, value): + def __add__(self, value: CountT) -> "Counter": """Add value to counter.""" return self._change(value) - def __sub__(self, value): + def __sub__(self, value: CountT) -> "Counter": """Subtract value from counter.""" return self._change(-value) diff --git a/kazoo/recipe/election.py b/kazoo/recipe/election.py index 93bb7258..c2940134 100644 --- a/kazoo/recipe/election.py +++ b/kazoo/recipe/election.py @@ -4,8 +4,19 @@ :Status: Unknown """ +from __future__ import annotations + +from typing import TYPE_CHECKING + from kazoo.exceptions import CancelledError +if TYPE_CHECKING: + from kazoo.client import KazooClient + from typing import Callable, List, Optional + from typing_extensions import ParamSpec + + P = ParamSpec("P") + class Election(object): """Kazoo Basic Leader Election @@ -22,7 +33,9 @@ class Election(object): """ - def __init__(self, client, path, identifier=None): + def __init__( + self, client: KazooClient, path: str, identifier: Optional[str] = None + ): """Create a Kazoo Leader Election :param client: A :class:`~kazoo.client.KazooClient` instance. @@ -34,7 +47,9 @@ def __init__(self, client, path, identifier=None): """ self.lock = client.Lock(path, identifier) - def run(self, func, *args, **kwargs): + def run( + self, func: Callable[P, None], *args: P.args, **kwargs: P.kwargs + ) -> None: """Contend for the leadership This call will block until either this contender is cancelled @@ -57,7 +72,7 @@ def run(self, func, *args, **kwargs): except CancelledError: pass - def cancel(self): + def cancel(self) -> None: """Cancel participation in the election .. note:: @@ -68,7 +83,7 @@ def cancel(self): """ self.lock.cancel() - def contenders(self): + def contenders(self) -> List[str]: """Return an ordered list of the current contenders in the election diff --git a/kazoo/recipe/lease.py b/kazoo/recipe/lease.py index ce7fe567..683528d9 100644 --- a/kazoo/recipe/lease.py +++ b/kazoo/recipe/lease.py @@ -5,12 +5,27 @@ :Status: Beta """ +from __future__ import annotations + import datetime import json import socket +from typing import cast, TYPE_CHECKING from kazoo.exceptions import CancelledError +from typing_extensions import TypedDict + +if TYPE_CHECKING: + from kazoo.client import KazooClient + from typing import Callable, Optional + + +class LeaseData(TypedDict): + version: int + holder: str + end: str + class NonBlockingLease(object): """Exclusive lease that does not block. @@ -48,11 +63,11 @@ class NonBlockingLease(object): def __init__( self, - client, - path, - duration, - identifier=None, - utcnow=datetime.datetime.utcnow, + client: KazooClient, + path: str, + duration: datetime.timedelta, + identifier: Optional[str] = None, + utcnow: Callable[[], datetime.datetime] = datetime.datetime.utcnow, ): """Create a non-blocking lease. @@ -71,7 +86,14 @@ def __init__( self.obtained = False self._attempt_obtaining(client, path, duration, ident, utcnow) - def _attempt_obtaining(self, client, path, duration, ident, utcnow): + def _attempt_obtaining( + self, + client: KazooClient, + path: str, + duration: datetime.timedelta, + ident: str, + utcnow: Callable[[], datetime.datetime], + ) -> None: client.ensure_path(path) holder_path = path + "/lease_holder" lock = client.Lock(path, ident) @@ -92,7 +114,7 @@ def _attempt_obtaining(self, client, path, duration, ident, utcnow): return client.delete(holder_path) end_lease = (now + duration).strftime(self._date_format) - new_data = { + new_data: LeaseData = { "version": self._version, "holder": ident, "end": end_lease, @@ -103,18 +125,18 @@ def _attempt_obtaining(self, client, path, duration, ident, utcnow): except CancelledError: pass - def _encode(self, data_dict): + def _encode(self, data_dict: LeaseData) -> bytes: return json.dumps(data_dict).encode(self._byte_encoding) - def _decode(self, raw): - return json.loads(raw.decode(self._byte_encoding)) + def _decode(self, raw: bytes) -> LeaseData: + return cast(LeaseData, json.loads(raw.decode(self._byte_encoding))) # Python 2.x - def __nonzero__(self): + def __nonzero__(self) -> bool: return self.obtained # Python 3.x - def __bool__(self): + def __bool__(self) -> bool: return self.obtained @@ -140,12 +162,12 @@ class MultiNonBlockingLease(object): def __init__( self, - client, - count, - path, - duration, - identifier=None, - utcnow=datetime.datetime.utcnow, + client: KazooClient, + count: int, + path: str, + duration: datetime.timedelta, + identifier: Optional[str] = None, + utcnow: Callable[[], datetime.datetime] = datetime.datetime.utcnow, ): self.obtained = False for num in range(count): @@ -161,9 +183,9 @@ def __init__( break # Python 2.x - def __nonzero__(self): + def __nonzero__(self) -> bool: return self.obtained # Python 3.x - def __bool__(self): + def __bool__(self) -> bool: return self.obtained diff --git a/kazoo/recipe/lock.py b/kazoo/recipe/lock.py index 8a490394..06c75833 100644 --- a/kazoo/recipe/lock.py +++ b/kazoo/recipe/lock.py @@ -14,8 +14,11 @@ and/or the lease has been lost. """ +from __future__ import annotations + import re import time +from typing import TYPE_CHECKING import uuid from kazoo.exceptions import ( @@ -31,6 +34,9 @@ RetryFailedError, ) +if TYPE_CHECKING: + from typing import List + class _Watch(object): def __init__(self, duration=None): @@ -133,7 +139,7 @@ def _ensure_path(self): self.client.ensure_path(self.path) self.assured_path = True - def cancel(self): + def cancel(self) -> None: """Cancel a pending lock acquire.""" self.cancelled = True self.wake_event.set() @@ -343,7 +349,7 @@ def _inner_release(self): self.node = None return True - def contenders(self): + def contenders(self) -> List[str]: """Return an ordered list of the current contenders for the lock. @@ -549,7 +555,7 @@ def _ensure_path(self): else: self.client.set(self.path, str(self.max_leases).encode("utf-8")) - def cancel(self): + def cancel(self) -> None: """Cancel a pending semaphore acquire.""" self.cancelled = True self.wake_event.set() diff --git a/kazoo/recipe/party.py b/kazoo/recipe/party.py index 2a0f5dfb..6ec940ac 100644 --- a/kazoo/recipe/party.py +++ b/kazoo/recipe/party.py @@ -7,15 +7,26 @@ used for determining members of a party. """ +from __future__ import annotations + +from abc import ABC, abstractmethod +from typing import TYPE_CHECKING import uuid from kazoo.exceptions import NodeExistsError, NoNodeError +if TYPE_CHECKING: + from typing import Generator, List, Optional + + from kazoo.client import KazooClient + -class BaseParty(object): +class BaseParty(ABC): """Base implementation of a party.""" - def __init__(self, client, path, identifier=None): + def __init__( + self, client: KazooClient, path: str, identifier: Optional[str] = None + ): """ :param client: A :class:`~kazoo.client.KazooClient` instance. :param path: The party path to use. @@ -29,17 +40,17 @@ def __init__(self, client, path, identifier=None): self.ensured_path = False self.participating = False - def _ensure_parent(self): + def _ensure_parent(self) -> None: if not self.ensured_path: # make sure our parent node exists self.client.ensure_path(self.path) self.ensured_path = True - def join(self): + def join(self) -> None: """Join the party""" return self.client.retry(self._inner_join) - def _inner_join(self): + def _inner_join(self) -> None: self._ensure_parent() try: self.client.create(self.create_path, self.data, ephemeral=True) @@ -49,38 +60,49 @@ def _inner_join(self): # suspended connection self.participating = True - def leave(self): + def leave(self) -> bool: """Leave the party""" self.participating = False return self.client.retry(self._inner_leave) - def _inner_leave(self): + def _inner_leave(self) -> bool: try: self.client.delete(self.create_path) except NoNodeError: return False return True - def __len__(self): + def __len__(self) -> int: """Return a count of participating clients""" self._ensure_parent() return len(self._get_children()) - def _get_children(self): + def _get_children(self) -> List[str]: return self.client.retry(self.client.get_children, self.path) + @property + @abstractmethod + def create_path(self) -> str: + ... + class Party(BaseParty): """Simple pool of participating processes""" _NODE_NAME = "__party__" - def __init__(self, client, path, identifier=None): + def __init__( + self, client: KazooClient, path: str, identifier: Optional[str] = None + ): BaseParty.__init__(self, client, path, identifier=identifier) self.node = uuid.uuid4().hex + self._NODE_NAME - self.create_path = self.path + "/" + self.node + self._create_path = self.path + "/" + self.node + + @property + def create_path(self) -> str: + return self._create_path - def __iter__(self): + def __iter__(self) -> Generator[str, None, None]: """Get a list of participating clients' data values""" self._ensure_parent() children = self._get_children() @@ -93,7 +115,7 @@ def __iter__(self): except NoNodeError: # pragma: nocover pass - def _get_children(self): + def _get_children(self) -> List[str]: children = BaseParty._get_children(self) return [c for c in children if self._NODE_NAME in c] @@ -109,12 +131,18 @@ class ShallowParty(BaseParty): """ - def __init__(self, client, path, identifier=None): + def __init__( + self, client: KazooClient, path: str, identifier: Optional[str] = None + ): BaseParty.__init__(self, client, path, identifier=identifier) self.node = "-".join([uuid.uuid4().hex, self.data.decode("utf-8")]) - self.create_path = self.path + "/" + self.node + self._create_path = self.path + "/" + self.node + + @property + def create_path(self) -> str: + return self._create_path - def __iter__(self): + def __iter__(self) -> Generator[str, None, None]: """Get a list of participating clients' identifiers""" self._ensure_parent() children = self._get_children() diff --git a/kazoo/retry.py b/kazoo/retry.py index a51a1b56..34ec7903 100644 --- a/kazoo/retry.py +++ b/kazoo/retry.py @@ -1,6 +1,9 @@ +from __future__ import annotations + import logging import random import time +from typing import TYPE_CHECKING from kazoo.exceptions import ( ConnectionClosedError, @@ -11,6 +14,14 @@ ) +if TYPE_CHECKING: + from typing import Callable, TypeVar + from typing_extensions import ParamSpec + + P = ParamSpec("P") + R = TypeVar("R") + + log = logging.getLogger(__name__) @@ -109,7 +120,9 @@ def copy(self): obj.retry_exceptions = self.retry_exceptions return obj - def __call__(self, func, *args, **kwargs): + def __call__( + self, func: Callable[P, R], *args: P.args, **kwargs: P.kwargs + ) -> R: """Call a function with arguments until it completes without throwing a Kazoo exception diff --git a/pyproject.toml b/pyproject.toml index db3890c5..c0b79a60 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,6 +39,9 @@ timeout = 180 # Import Discovery ignore_missing_imports = false +# Platform configuration +python_version = "3.7" + # Disallow dynamic typing disallow_any_unimported = true disallow_any_expr = false @@ -105,14 +108,9 @@ module = [ 'kazoo.protocol.paths', 'kazoo.protocol.serialization', 'kazoo.protocol.states', - 'kazoo.recipe.barrier', 'kazoo.recipe.cache', - 'kazoo.recipe.counter', - 'kazoo.recipe.election', - 'kazoo.recipe.lease', 'kazoo.recipe.lock', 'kazoo.recipe.partitioner', - 'kazoo.recipe.party', 'kazoo.recipe.queue', 'kazoo.recipe.watchers', 'kazoo.retry', diff --git a/setup.cfg b/setup.cfg index e110a3b5..295daccb 100644 --- a/setup.cfg +++ b/setup.cfg @@ -40,6 +40,8 @@ project_urls = zip_safe = false include_package_data = true packages = find: +install_requires = + typing-extensions [aliases] release = sdist bdist_wheel @@ -87,4 +89,3 @@ alldeps = %(sasl)s %(docs)s %(typing)s -