From abea3ffcc20956c0f699051548d8560708ed2a61 Mon Sep 17 00:00:00 2001 From: Mathias Brulatout Date: Thu, 7 Nov 2024 17:53:46 +0100 Subject: [PATCH] Partial typing annotation support + add mypy (#82) * cb: rename bool callback to boolean This avoids conflicting with bool type for further type annotations * code-style: add mypy * code-style: mypy: fix no-untyped-call * code-style: mypy: add simple types * mypy: exclude setup.py * mypy: fix 3.11/3.12 typing issue * mypy: move tox dir to avoid type issues * tests: increase min duration in reports * mypy: add setuptools types --------- Co-authored-by: Mathias Brulatout --- .pre-commit-config.yaml | 6 ++ conftest.py | 29 +++++++--- consul/aio.py | 26 ++++++--- consul/api/acl/__init__.py | 2 +- consul/api/acl/policy.py | 23 ++++---- consul/api/acl/token.py | 43 +++++++------- consul/api/agent.py | 102 +++++++++++++++------------------- consul/api/catalog.py | 26 +++++---- consul/api/connect.py | 18 +++--- consul/api/coordinates.py | 2 +- consul/api/event.py | 10 +++- consul/api/health.py | 14 +++-- consul/api/kv.py | 16 +++--- consul/api/operator.py | 2 +- consul/api/query.py | 37 ++++++------ consul/api/session.py | 24 +++++--- consul/api/status.py | 2 +- consul/api/txn.py | 2 +- consul/base.py | 45 +++++++++------ consul/callback.py | 20 ++++++- consul/check.py | 19 ++++--- consul/std.py | 13 +++-- pyproject.toml | 19 ++++++- tests-requirements.txt | 3 + tests/api/test_acl.py | 18 +++--- tests/api/test_agent.py | 26 ++++----- tests/api/test_coordinates.py | 2 +- tests/api/test_event.py | 4 +- tests/api/test_health.py | 10 ++-- tests/api/test_kv.py | 20 +++---- tests/api/test_operator.py | 2 +- tests/api/test_query.py | 2 +- tests/api/test_session.py | 4 +- tests/api/test_status.py | 4 +- tests/api/test_txn.py | 2 +- tests/test_aio.py | 26 ++++----- tests/test_base.py | 36 ++++++------ tests/test_callback.py | 12 ++-- tests/test_std.py | 4 +- tests/test_utils.py | 4 +- tests/utils.py | 2 +- tox.ini | 1 + 42 files changed, 391 insertions(+), 291 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c165334..32e3213 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -24,3 +24,9 @@ repos: entry: pylint -rn -sn # Only display messages, don't display the score language: system types: [python] + + - id: mypy + name: mypy + language: system + entry: mypy --non-interactive --install-types + types: [python] \ No newline at end of file diff --git a/conftest.py b/conftest.py index 9b757a2..a4ca474 100644 --- a/conftest.py +++ b/conftest.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import collections import json import os @@ -8,6 +10,8 @@ import docker import pytest import requests +from docker import DockerClient +from docker.errors import APIError, NotFound from requests import RequestException CONSUL_VERSIONS = ["1.16.1", "1.17.3"] @@ -19,7 +23,7 @@ os.makedirs(LOGS_DIR, exist_ok=True) -def get_free_ports(num, host=None): +def get_free_ports(num: int, host=None) -> list[int]: if not host: host = "127.0.0.1" sockets = [] @@ -40,7 +44,7 @@ def _unset_consul_token(): del os.environ["CONSUL_HTTP_TOKEN"] -def start_consul_container(version, acl_master_token=None): +def start_consul_container(version: str, acl_master_token: str | None = None): """ Starts a Consul container. If acl_master_token is None, ACL will be disabled for this server, otherwise it will be enabled and the master token will be @@ -87,9 +91,16 @@ def start_consul_container(version, acl_master_token=None): "acl": {"enabled": True, "tokens": {"initial_management": acl_master_token}}, } merged_config = {**base_config, **acl_config} - docker_config["environment"]["CONSUL_LOCAL_CONFIG"] = json.dumps(merged_config) - - def start_consul_container_with_retry(client, command, version, docker_config, max_retries=3, retry_delay=2): # pylint: disable=inconsistent-return-statements + docker_config["environment"]["CONSUL_LOCAL_CONFIG"] = json.dumps(merged_config) # type: ignore + + def start_consul_container_with_retry( # pylint: disable=inconsistent-return-statements + client: DockerClient, + command: str, + version: str, + docker_config: dict, + max_retries: int = 3, + retry_delay: int = 2, + ): """ Start a Consul container with retries as a few initial attempts sometimes fail. """ @@ -97,12 +108,12 @@ def start_consul_container_with_retry(client, command, version, docker_config, m try: container = client.containers.run(f"hashicorp/consul:{version}", command=command, **docker_config) return container - except docker.errors.APIError: + except APIError: # Cleanup that stray container as it might cause a naming conflict try: container = client.containers.get(docker_config["name"]) container.remove(force=True) - except docker.errors.NotFound: + except NotFound: pass if attempt == max_retries - 1: raise @@ -146,13 +157,13 @@ def start_consul_container_with_retry(client, command, version, docker_config, m raise Exception("Failed to verify Consul startup") # pylint: disable=broad-exception-raised -def get_consul_version(port): +def get_consul_version(port: int) -> str: base_uri = f"http://127.0.0.1:{port}/v1/" response = requests.get(base_uri + "agent/self", timeout=10) return response.json()["Config"]["Version"].strip() -def setup_and_teardown_consul(request, version, acl_master_token=None): +def setup_and_teardown_consul(request, version, acl_master_token: str | None = None): # Start the container, yield, get container logs, store them in logs/.log, stop the container container, port = start_consul_container(version=version, acl_master_token=acl_master_token) version = get_consul_version(port) diff --git a/consul/aio.py b/consul/aio.py index 7bcf335..48d9e93 100644 --- a/consul/aio.py +++ b/consul/aio.py @@ -11,7 +11,7 @@ class HTTPClient(base.HTTPClient): """Asyncio adapter for python consul using aiohttp library""" - def __init__(self, *args, loop=None, connections_limit=None, connections_timeout=None, **kwargs): + def __init__(self, *args, loop=None, connections_limit=None, connections_timeout=None, **kwargs) -> None: super().__init__(*args, **kwargs) self._loop = loop or asyncio.get_event_loop() connector_kwargs = {} @@ -22,7 +22,7 @@ def __init__(self, *args, loop=None, connections_limit=None, connections_timeout if connections_timeout: timeout = aiohttp.ClientTimeout(total=connections_timeout) session_kwargs["timeout"] = timeout - self._session = aiohttp.ClientSession(connector=connector, **session_kwargs) + self._session = aiohttp.ClientSession(connector=connector, **session_kwargs) # type: ignore async def _request( self, callback, method, uri, headers: Optional[Dict[str, str]], data=None, connections_timeout=None @@ -31,7 +31,7 @@ async def _request( if connections_timeout: timeout = aiohttp.ClientTimeout(total=connections_timeout) session_kwargs["timeout"] = timeout - resp = await self._session.request(method, uri, headers=headers, data=data, **session_kwargs) + resp = await self._session.request(method, uri, headers=headers, data=data, **session_kwargs) # type: ignore body = await resp.text(encoding="utf-8") if resp.status == 599: raise Timeout @@ -43,7 +43,13 @@ def get(self, callback, path, params=None, headers: Optional[Dict[str, str]] = N return self._request(callback, "GET", uri, headers=headers, connections_timeout=connections_timeout) def put( - self, callback, path, params=None, data="", headers: Optional[Dict[str, str]] = None, connections_timeout=None + self, + callback, + path, + params=None, + data: str = "", + headers: Optional[Dict[str, str]] = None, + connections_timeout=None, ): uri = self.uri(path, params) return self._request(callback, "PUT", uri, headers=headers, data=data, connections_timeout=connections_timeout) @@ -53,7 +59,13 @@ def delete(self, callback, path, params=None, headers: Optional[Dict[str, str]] return self._request(callback, "DELETE", uri, headers=headers, connections_timeout=connections_timeout) def post( - self, callback, path, params=None, data="", headers: Optional[Dict[str, str]] = None, connections_timeout=None + self, + callback, + path, + params=None, + data: str = "", + headers: Optional[Dict[str, str]] = None, + connections_timeout=None, ): uri = self.uri(path, params) return self._request(callback, "POST", uri, headers=headers, data=data, connections_timeout=connections_timeout) @@ -63,13 +75,13 @@ def close(self): class Consul(base.Consul): - def __init__(self, *args, loop=None, connections_limit=None, connections_timeout=None, **kwargs): + def __init__(self, *args, loop=None, connections_limit=None, connections_timeout=None, **kwargs) -> None: self._loop = loop or asyncio.get_event_loop() self.connections_limit = connections_limit self.connections_timeout = connections_timeout super().__init__(*args, **kwargs) - def http_connect(self, host, port, scheme, verify=True, cert=None): + def http_connect(self, host: str, port: int, scheme, verify: bool = True, cert=None): return HTTPClient( host, port, diff --git a/consul/api/acl/__init__.py b/consul/api/acl/__init__.py index d19ceda..06c9840 100644 --- a/consul/api/acl/__init__.py +++ b/consul/api/acl/__init__.py @@ -3,7 +3,7 @@ class ACL: - def __init__(self, agent): + def __init__(self, agent) -> None: self.agent = agent self.token = self.tokens = Token(agent) diff --git a/consul/api/acl/policy.py b/consul/api/acl/policy.py index 75f9941..2108839 100644 --- a/consul/api/acl/policy.py +++ b/consul/api/acl/policy.py @@ -1,46 +1,46 @@ +from __future__ import annotations + import json +from typing import Optional from consul.callback import CB class Policy: - def __init__(self, agent): + def __init__(self, agent) -> None: self.agent = agent - def list(self, token=None): + def list(self, token: str | None = None): """ Lists all the active ACL policies. This is a privileged endpoint, and requires a management token. *token* will override this client's default token. Requires a token with acl:read capability. ACLPermissionDenied raised otherwise """ - params = [] headers = self.agent.prepare_headers(token) - return self.agent.http.get(CB.json(), "/v1/acl/policies", params=params, headers=headers) + return self.agent.http.get(CB.json(), "/v1/acl/policies", headers=headers) - def read(self, uuid, token=None): + def read(self, uuid, token: str | None = None): """ Returns the policy information for *id*. Requires a token with acl:read capability. - :param accessor_id: Specifies the UUID of the policy you lookup. + :param uuid: Specifies the UUID of the policy you look up. :param token: token with acl:read capability :return: selected Polic information """ - params = [] headers = self.agent.prepare_headers(token) - return self.agent.http.get(CB.json(), f"/v1/acl/policy/{uuid}", params=params, headers=headers) + return self.agent.http.get(CB.json(), f"/v1/acl/policy/{uuid}", headers=headers) - def create(self, name, token=None, description=None, rules=None): + def create(self, name: str, token: str | None = None, description: Optional[str] = None, rules=None): """ Create a policy This is a privileged endpoint, and requires a token with acl:write. :param name: Specifies a name for the ACL policy. :param token: token with acl:write capability - :param description: Free form human readable description of the policy. + :param description: Free form human-readable description of the policy. :param rules: Specifies rules for the ACL policy. :return: The cloned token information """ - params = [] json_data = {"name": name} if rules: json_data["rules"] = json.dumps(rules) @@ -50,7 +50,6 @@ def create(self, name, token=None, description=None, rules=None): return self.agent.http.put( CB.json(), "/v1/acl/policy", - params=params, headers=headers, data=json.dumps(json_data), ) diff --git a/consul/api/acl/token.py b/consul/api/acl/token.py index 0f698d2..b2f5ea5 100644 --- a/consul/api/acl/token.py +++ b/consul/api/acl/token.py @@ -1,46 +1,46 @@ +from __future__ import annotations + import json +import typing from consul.callback import CB class Token: - def __init__(self, agent): + def __init__(self, agent) -> None: self.agent = agent - def list(self, token=None): + def list(self, token: str | None = None): """ Lists all the active ACL tokens. This is a privileged endpoint, and requires a management token. *token* will override this client's default token. Requires a token with acl:read capability. ACLPermissionDenied raised otherwise """ - params = [] headers = self.agent.prepare_headers(token) - return self.agent.http.get(CB.json(), "/v1/acl/tokens", params=params, headers=headers) + return self.agent.http.get(CB.json(), "/v1/acl/tokens", headers=headers) - def read(self, accessor_id, token=None): + def read(self, accessor_id: str, token: str | None = None): """ Returns the token information for *accessor_id*. Requires a token with acl:read capability. :param accessor_id: The accessor ID of the token to read :param token: token with acl:read capability :return: selected token information """ - params = [] headers = self.agent.prepare_headers(token) - return self.agent.http.get(CB.json(), f"/v1/acl/token/{accessor_id}", params=params, headers=headers) + return self.agent.http.get(CB.json(), f"/v1/acl/token/{accessor_id}", headers=headers) - def delete(self, accessor_id, token=None): + def delete(self, accessor_id: str, token: str | None = None): """ Deletes the token with *accessor_id*. This is a privileged endpoint, and requires a token with acl:write. :param accessor_id: The accessor ID of the token to delete :param token: token with acl:write capability :return: True if the token was deleted """ - params = [] headers = self.agent.prepare_headers(token) - return self.agent.http.delete(CB.bool(), f"/v1/acl/token/{accessor_id}", params=params, headers=headers) + return self.agent.http.delete(CB.boolean(), f"/v1/acl/token/{accessor_id}", headers=headers) - def clone(self, accessor_id, token=None, description=""): + def clone(self, accessor_id: str, token: str | None = None, description: str = ""): """ Clones the token identified by *accessor_id*. This is a privileged endpoint, and requires a token with acl:write. :param accessor_id: The accessor ID of the token to clone @@ -48,19 +48,24 @@ def clone(self, accessor_id, token=None, description=""): :param description: Optional new token description :return: The cloned token information """ - params = [] json_data = {"Description": description} headers = self.agent.prepare_headers(token) return self.agent.http.put( CB.json(), f"/v1/acl/token/{accessor_id}/clone", - params=params, headers=headers, data=json.dumps(json_data), ) - def create(self, token=None, accessor_id=None, secret_id=None, policies_id=None, description=""): + def create( + self, + token: str | None = None, + accessor_id: str | None = None, + secret_id: str | None = None, + policies_id: typing.List[str] | None = None, + description: str = "", + ): """ Create a token (optionally identified by *secret_id* and *accessor_id*). This is a privileged endpoint, and requires a token with acl:write. @@ -68,12 +73,11 @@ def create(self, token=None, accessor_id=None, secret_id=None, policies_id=None, :param accessor_id: The accessor ID of the token to create :param secret_id: The secret ID of the token to create :param description: Optional new token description - :param policies: Optional list of policies id + :param policies_id: Optional list of policies id :return: The cloned token information """ - params = [] - json_data = {} + json_data: dict[str, typing.Any] = {} if accessor_id: json_data["AccessorID"] = accessor_id if secret_id: @@ -87,12 +91,11 @@ def create(self, token=None, accessor_id=None, secret_id=None, policies_id=None, return self.agent.http.put( CB.json(), "/v1/acl/token", - params=params, headers=headers, data=json.dumps(json_data), ) - def update(self, accessor_id, token=None, secret_id=None, description=""): + def update(self, accessor_id: str, token: str | None = None, secret_id: str | None = None, description: str = ""): """ Update a token (optionally identified by *secret_id* and *accessor_id*). This is a privileged endpoint, and requires a token with acl:write. @@ -102,7 +105,6 @@ def update(self, accessor_id, token=None, secret_id=None, description=""): :param description: Optional new token description :return: The updated token information """ - params = [] json_data = {"AccessorID": accessor_id} if secret_id: @@ -113,7 +115,6 @@ def update(self, accessor_id, token=None, secret_id=None, description=""): return self.agent.http.put( CB.json(), f"/v1/acl/token/{accessor_id}", - params=params, headers=headers, data=json.dumps(json_data), ) diff --git a/consul/api/agent.py b/consul/api/agent.py index 7dc63ec..0996a17 100644 --- a/consul/api/agent.py +++ b/consul/api/agent.py @@ -1,4 +1,7 @@ +from __future__ import annotations + import json +from typing import Any, Optional from consul import Check from consul.callback import CB @@ -12,7 +15,7 @@ class Agent: anti-entropy to recover from outages. """ - def __init__(self, agent): + def __init__(self, agent) -> None: self.agent = agent self.service = Agent.Service(agent) self.check = Agent.Check(agent) @@ -24,7 +27,7 @@ def self(self): """ return self.agent.http.get(CB.json(), "/v1/agent/self") - def services(self): + def services(self) -> Any: """ Returns all the services that are registered with the local agent. These services were either provided through configuration files, or @@ -44,7 +47,7 @@ def service_definition(self, service_id): """ return self.agent.http.get(CB.json(), f"/v1/agent/service/{service_id}") - def checks(self): + def checks(self) -> Any: """ Returns all the checks that are registered with the local agent. These checks were either provided through configuration files, or @@ -57,7 +60,7 @@ def checks(self): """ return self.agent.http.get(CB.json(), "/v1/agent/checks") - def members(self, wan=False): + def members(self, wan: bool = False): """ Returns all the members that this agent currently sees. This may vary by agent, use the nodes api of Catalog to retrieve a cluster @@ -72,7 +75,7 @@ def members(self, wan=False): params.append(("wan", 1)) return self.agent.http.get(CB.json(), "/v1/agent/members", params=params) - def maintenance(self, enable, reason=None, token=None): + def maintenance(self, enable: bool, reason: Optional[str] = None, token: str | None = None): """ The node maintenance endpoint can place the agent into "maintenance mode". @@ -84,16 +87,16 @@ def maintenance(self, enable, reason=None, token=None): operators. """ - params = [] + params: list[tuple[str, Any]] = [] params.append(("enable", enable)) if reason: params.append(("reason", reason)) headers = self.agent.prepare_headers(token) - return self.agent.http.put(CB.bool(), "/v1/agent/maintenance", params=params, headers=headers) + return self.agent.http.put(CB.boolean(), "/v1/agent/maintenance", params=params, headers=headers) - def join(self, address, wan=False, token=None): + def join(self, address: str, wan: bool = False, token: str | None = None): """ This endpoint instructs the agent to attempt to connect to a given address. @@ -110,9 +113,9 @@ def join(self, address, wan=False, token=None): if wan: params.append(("wan", 1)) headers = self.agent.prepare_headers(token) - return self.agent.http.put(CB.bool(), f"/v1/agent/join/{address}", params=params, headers=headers) + return self.agent.http.put(CB.boolean(), f"/v1/agent/join/{address}", params=params, headers=headers) - def force_leave(self, node, token=None): + def force_leave(self, node: str, token: str | None = None): """ This endpoint instructs the agent to force a node into the left state. If a node fails unexpectedly, then it will be in a failed @@ -124,33 +127,31 @@ def force_leave(self, node, token=None): *node* is the node to change state for. """ - params = [] - headers = self.agent.prepare_headers(token) - return self.agent.http.put(CB.bool(), f"/v1/agent/force-leave/{node}", params=params, headers=headers) + return self.agent.http.put(CB.boolean(), f"/v1/agent/force-leave/{node}", headers=headers) class Service: - def __init__(self, agent): + def __init__(self, agent) -> None: self.agent = agent def register( self, - name, + name: str, service_id=None, address=None, - port=None, + port: Optional[int] = None, tags=None, check=None, - token=None, + token: str | None = None, meta=None, weights=None, # *deprecated* use check parameter script=None, interval=None, - ttl=None, + ttl: Optional[int] = None, http=None, timeout=None, - enable_tag_override=False, + enable_tag_override: bool = False, extra_checks=None, replace_existing_checks=False, ): @@ -201,7 +202,7 @@ def register( if extra_checks is None: extra_checks = [] - payload = {} + payload: dict[str, Any] = {} payload["name"] = name if enable_tag_override: @@ -231,23 +232,20 @@ def register( params.append(("replace-existing-checks", "true")) headers = self.agent.prepare_headers(token) return self.agent.http.put( - CB.bool(), "/v1/agent/service/register", params=params, headers=headers, data=json.dumps(payload) + CB.boolean(), "/v1/agent/service/register", params=params, headers=headers, data=json.dumps(payload) ) - def deregister(self, service_id, token=None): + def deregister(self, service_id: str, token: str | None = None): """ Used to remove a service from the local agent. The agent will take care of deregistering the service with the Catalog. If there is an associated check, that is also deregistered. """ - params = [] headers = self.agent.prepare_headers(token) - return self.agent.http.put( - CB.bool(), f"/v1/agent/service/deregister/{service_id}", params=params, headers=headers - ) + return self.agent.http.put(CB.boolean(), f"/v1/agent/service/deregister/{service_id}", headers=headers) - def maintenance(self, service_id, enable, reason=None, token=None): + def maintenance(self, service_id: str, enable: bool, reason: Optional[str] = None, token: str | None = None): """ The service maintenance endpoint allows placing a given service into "maintenance mode". @@ -262,7 +260,7 @@ def maintenance(self, service_id, enable, reason=None, token=None): operators. """ - params = [] + params: list[tuple[str, Any]] = [] params.append(("enable", enable)) if reason: @@ -271,25 +269,25 @@ def maintenance(self, service_id, enable, reason=None, token=None): headers = self.agent.prepare_headers(token) return self.agent.http.put( - CB.bool(), f"/v1/agent/service/maintenance/{service_id}", params=params, headers=headers + CB.boolean(), f"/v1/agent/service/maintenance/{service_id}", params=params, headers=headers ) class Check: - def __init__(self, agent): + def __init__(self, agent) -> None: self.agent = agent def register( self, - name, + name: str, check=None, check_id=None, notes=None, service_id=None, - token=None, + token: str | None = None, # *deprecated* use check parameter script=None, interval=None, - ttl=None, + ttl: Optional[int] = None, http=None, timeout=None, ): @@ -340,24 +338,20 @@ def register( if service_id: payload["serviceid"] = service_id - params = [] headers = self.agent.prepare_headers(token) return self.agent.http.put( - CB.bool(), "/v1/agent/check/register", params=params, headers=headers, data=json.dumps(payload) + CB.boolean(), "/v1/agent/check/register", headers=headers, data=json.dumps(payload) ) - def deregister(self, check_id, token=None): + def deregister(self, check_id: str, token: str | None = None): """ Remove a check from the local agent. """ - params = [] headers = self.agent.prepare_headers(token) - return self.agent.http.put( - CB.bool(), f"/v1/agent/check/deregister/{check_id}", params=params, headers=headers - ) + return self.agent.http.put(CB.boolean(), f"/v1/agent/check/deregister/{check_id}", headers=headers) - def ttl_pass(self, check_id, notes=None, token=None): + def ttl_pass(self, check_id: str, notes=None, token: str | None = None): """ Mark a ttl based check as passing. Optional notes can be attached to describe the status of the check. @@ -367,9 +361,9 @@ def ttl_pass(self, check_id, notes=None, token=None): params.append(("note", notes)) headers = self.agent.prepare_headers(token) - return self.agent.http.put(CB.bool(), f"/v1/agent/check/pass/{check_id}", params=params, headers=headers) + return self.agent.http.put(CB.boolean(), f"/v1/agent/check/pass/{check_id}", params=params, headers=headers) - def ttl_fail(self, check_id, notes=None, token=None): + def ttl_fail(self, check_id: str, notes=None, token: str | None = None): """ Mark a ttl based check as failing. Optional notes can be attached to describe why check is failing. The status of the @@ -380,9 +374,9 @@ def ttl_fail(self, check_id, notes=None, token=None): params.append(("note", notes)) headers = self.agent.prepare_headers(token) - return self.agent.http.put(CB.bool(), f"/v1/agent/check/fail/{check_id}", params=params, headers=headers) + return self.agent.http.put(CB.boolean(), f"/v1/agent/check/fail/{check_id}", params=params, headers=headers) - def ttl_warn(self, check_id, notes=None, token=None): + def ttl_warn(self, check_id: str, notes=None, token: str | None = None): """ Mark a ttl based check with warning. Optional notes can be attached to describe the warning. The status of the @@ -393,14 +387,14 @@ def ttl_warn(self, check_id, notes=None, token=None): params.append(("note", notes)) headers = self.agent.prepare_headers(token) - return self.agent.http.put(CB.bool(), f"/v1/agent/check/warn/{check_id}", params=params, headers=headers) + return self.agent.http.put(CB.boolean(), f"/v1/agent/check/warn/{check_id}", params=params, headers=headers) class Connect: - def __init__(self, agent): + def __init__(self, agent) -> None: self.agent = agent self.ca = Agent.Connect.CA(agent) - def authorize(self, target, client_cert_uri, client_cert_serial, token=None): + def authorize(self, target, client_cert_uri, client_cert_serial, token: str | None = None): """ Tests whether a connection attempt is authorized between two services. @@ -418,24 +412,20 @@ def authorize(self, target, client_cert_uri, client_cert_serial, token=None): payload = {"Target": target, "ClientCertURI": client_cert_uri, "ClientCertSerial": client_cert_serial} - params = [] headers = self.agent.prepare_headers(token) return self.agent.http.put( - CB.json(), "/v1/agent/connect/authorize", params=params, headers=headers, data=json.dumps(payload) + CB.json(), "/v1/agent/connect/authorize", headers=headers, data=json.dumps(payload) ) class CA: - def __init__(self, agent): + def __init__(self, agent) -> None: self.agent = agent def roots(self): return self.agent.http.get(CB.json(), "/v1/agent/connect/ca/roots") - def leaf(self, service, token=None): - params = [] + def leaf(self, service, token: str | None = None): headers = self.agent.prepare_headers(token) - return self.agent.http.get( - CB.json(), f"/v1/agent/connect/ca/leaf/{service}", params=params, headers=headers - ) + return self.agent.http.get(CB.json(), f"/v1/agent/connect/ca/leaf/{service}", headers=headers) diff --git a/consul/api/catalog.py b/consul/api/catalog.py index 8bba8f3..7053af0 100644 --- a/consul/api/catalog.py +++ b/consul/api/catalog.py @@ -1,13 +1,15 @@ +from __future__ import annotations + import json from consul.callback import CB class Catalog: - def __init__(self, agent): + def __init__(self, agent) -> None: self.agent = agent - def register(self, node, address, service=None, check=None, dc=None, token=None, node_meta=None): + def register(self, node, address, service=None, check=None, dc=None, token: str | None = None, node_meta=None): """ A low level mechanism for directly registering or updating entries in the catalog. It is usually recommended to use @@ -85,10 +87,10 @@ def register(self, node, address, service=None, check=None, dc=None, token=None, headers = self.agent.prepare_headers(token) return self.agent.http.put( - CB.bool(), "/v1/catalog/register", data=json.dumps(data), params=params, headers=headers + CB.boolean(), "/v1/catalog/register", data=json.dumps(data), params=params, headers=headers ) - def deregister(self, node, service_id=None, check_id=None, dc=None, token=None): + def deregister(self, node, service_id=None, check_id=None, dc=None, token: str | None = None): """ A low level mechanism for directly removing entries in the catalog. It is usually recommended to use the agent APIs, as they are @@ -117,7 +119,7 @@ def deregister(self, node, service_id=None, check_id=None, dc=None, token=None): if token: data["WriteRequest"] = {"Token": token} headers = self.agent.prepare_headers(token) - return self.agent.http.put(CB.bool(), "/v1/catalog/deregister", headers=headers, data=json.dumps(data)) + return self.agent.http.put(CB.boolean(), "/v1/catalog/deregister", headers=headers, data=json.dumps(data)) def datacenters(self): """ @@ -125,7 +127,9 @@ def datacenters(self): """ return self.agent.http.get(CB.json(), "/v1/catalog/datacenters") - def nodes(self, index=None, wait=None, consistency=None, dc=None, near=None, token=None, node_meta=None): + def nodes( + self, index=None, wait=None, consistency=None, dc=None, near=None, token: str | None = None, node_meta=None + ): """ Returns a tuple of (*index*, *nodes*) of all nodes known about in the *dc* datacenter. *dc* defaults to the current @@ -183,7 +187,7 @@ def nodes(self, index=None, wait=None, consistency=None, dc=None, near=None, tok headers = self.agent.prepare_headers(token) return self.agent.http.get(CB.json(index=True), "/v1/catalog/nodes", params=params, headers=headers) - def services(self, index=None, wait=None, consistency=None, dc=None, token=None, node_meta=None): + def services(self, index=None, wait=None, consistency=None, dc=None, token: str | None = None, node_meta=None): """ Returns a tuple of (*index*, *services*) of all services known about in the *dc* datacenter. *dc* defaults to the current @@ -236,7 +240,7 @@ def services(self, index=None, wait=None, consistency=None, dc=None, token=None, headers = self.agent.prepare_headers(token) return self.agent.http.get(CB.json(index=True), "/v1/catalog/services", params=params, headers=headers) - def node(self, node, index=None, wait=None, consistency=None, dc=None, token=None): + def node(self, node, index=None, wait=None, consistency=None, dc=None, token: str | None = None): """ Returns a tuple of (*index*, *services*) of all services provided by *node*. @@ -305,7 +309,7 @@ def _service( consistency=None, dc=None, near=None, - token=None, + token: str | None = None, node_meta=None, ): params = [] @@ -329,7 +333,7 @@ def _service( headers = self.agent.prepare_headers(token) return self.agent.http.get(CB.json(index=True), internal_uri, params=params, headers=headers) - def service(self, service, **kwargs): + def service(self, service: str, **kwargs): """ Returns a tuple of (*index*, *nodes*) of the nodes providing *service* in the *dc* datacenter. *dc* defaults to the current @@ -373,7 +377,7 @@ def service(self, service, **kwargs): internal_uri = f"/v1/catalog/service/{service}" return self._service(internal_uri=internal_uri, **kwargs) - def connect(self, service, **kwargs): + def connect(self, service: str, **kwargs): """ Returns a tuple of (*index*, *nodes*) of the nodes providing connect-capable *service* in the *dc* datacenter. *dc* defaults diff --git a/consul/api/connect.py b/consul/api/connect.py index 768d6ee..0ef9a9c 100644 --- a/consul/api/connect.py +++ b/consul/api/connect.py @@ -1,24 +1,26 @@ +from __future__ import annotations + +from typing import Any + from consul.callback import CB class Connect: - def __init__(self, agent): + def __init__(self, agent) -> None: self.agent = agent self.ca = Connect.CA(agent) class CA: - def __init__(self, agent): + def __init__(self, agent) -> None: self.agent = agent - def roots(self, pem=False, token=None): - params = [] + def roots(self, pem: bool = False, token: str | None = None): + params: list[tuple[str, Any]] = [] params.append(("pem", int(pem))) headers = self.agent.prepare_headers(token) return self.agent.http.get(CB.json(), "/v1/connect/ca/roots", params=params, headers=headers) - def configuration(self, token=None): - params = [] - + def configuration(self, token: str | None = None): headers = self.agent.prepare_headers(token) - return self.agent.http.get(CB.json(), "/v1/connect/ca/configuration", params=params, headers=headers) + return self.agent.http.get(CB.json(), "/v1/connect/ca/configuration", headers=headers) diff --git a/consul/api/coordinates.py b/consul/api/coordinates.py index 415a572..eb03872 100644 --- a/consul/api/coordinates.py +++ b/consul/api/coordinates.py @@ -2,7 +2,7 @@ class Coordinate: - def __init__(self, agent): + def __init__(self, agent) -> None: self.agent = agent def datacenters(self): diff --git a/consul/api/event.py b/consul/api/event.py index 9bf985f..152d610 100644 --- a/consul/api/event.py +++ b/consul/api/event.py @@ -1,3 +1,7 @@ +from __future__ import annotations + +from typing import Optional + from consul.callback import CB @@ -16,10 +20,10 @@ class Event: An advantage however is that events can still be used even in the absence of server nodes or during an outage.""" - def __init__(self, agent): + def __init__(self, agent) -> None: self.agent = agent - def fire(self, name, body="", node=None, service=None, tag=None, token=None): + def fire(self, name: str, body: str = "", node=None, service=None, tag=None, token: str | None = None): """ Sends an event to Consul's gossip protocol. @@ -54,7 +58,7 @@ def fire(self, name, body="", node=None, service=None, tag=None, token=None): headers = self.agent.prepare_headers(token) return self.agent.http.put(CB.json(), f"/v1/event/fire/{name}", params=params, headers=headers, data=body) - def list(self, name=None, index=None, wait=None): + def list(self, name: Optional[str] = None, index=None, wait=None): """ Returns a tuple of (*index*, *events*) Note: Since Consul's event protocol uses gossip, there is no diff --git a/consul/api/health.py b/consul/api/health.py index 97ba229..585ea7a 100644 --- a/consul/api/health.py +++ b/consul/api/health.py @@ -1,9 +1,11 @@ +from __future__ import annotations + from consul.callback import CB class Health: # TODO: All of the health endpoints support all consistency modes - def __init__(self, agent): + def __init__(self, agent) -> None: self.agent = agent def _service( @@ -15,7 +17,7 @@ def _service( tag=None, dc=None, near=None, - token=None, + token: str | None = None, node_meta=None, ): params = [] @@ -41,7 +43,7 @@ def _service( headers = self.agent.prepare_headers(token) return self.agent.http.get(CB.json(index=True), internal_uri, params=params, headers=headers) - def service(self, service, **kwargs): + def service(self, service: str, **kwargs): """ Returns a tuple of (*index*, *nodes*) @@ -85,7 +87,7 @@ def connect(self, service, **kwargs): internal_uri = f"/v1/health/connect/{service}" return self._service(internal_uri=internal_uri, **kwargs) - def checks(self, service, index=None, wait=None, dc=None, near=None, token=None, node_meta=None): + def checks(self, service, index=None, wait=None, dc=None, near=None, token: str | None = None, node_meta=None): """ Returns a tuple of (*index*, *checks*) with *checks* being the checks associated with the service. @@ -126,7 +128,7 @@ def checks(self, service, index=None, wait=None, dc=None, near=None, token=None, headers = self.agent.prepare_headers(token) return self.agent.http.get(CB.json(index=True), f"/v1/health/checks/{service}", params=params, headers=headers) - def state(self, name, index=None, wait=None, dc=None, near=None, token=None, node_meta=None): + def state(self, name: str, index=None, wait=None, dc=None, near=None, token: str | None = None, node_meta=None): """ Returns a tuple of (*index*, *nodes*) @@ -173,7 +175,7 @@ def state(self, name, index=None, wait=None, dc=None, near=None, token=None, nod headers = self.agent.prepare_headers(token) return self.agent.http.get(CB.json(index=True), f"/v1/health/state/{name}", params=params, headers=headers) - def node(self, node, index=None, wait=None, dc=None, token=None): + def node(self, node, index=None, wait=None, dc=None, token: str | None = None): """ Returns a tuple of (*index*, *checks*) diff --git a/consul/api/kv.py b/consul/api/kv.py index b162d3f..b989ad5 100644 --- a/consul/api/kv.py +++ b/consul/api/kv.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from consul.callback import CB @@ -8,18 +10,18 @@ class KV: way. """ - def __init__(self, agent): + def __init__(self, agent) -> None: self.agent = agent def get( self, key, index=None, - recurse=False, + recurse: bool = False, wait=None, - token=None, + token: str | None = None, consistency=None, - keys=False, + keys: bool = False, separator=None, dc=None, connections_timeout=None, @@ -83,7 +85,7 @@ def get( params.append((consistency, "1")) one = False - decode = False + decode: bool | str = False if not keys: decode = "Value" @@ -106,7 +108,7 @@ def put( flags=None, acquire=None, release=None, - token=None, + token: str | None = None, dc=None, connections_timeout=None, ): @@ -166,7 +168,7 @@ def put( CB.json(), f"/v1/kv/{key}", params=params, headers=headers, data=value, **http_kwargs ) - def delete(self, key, recurse=None, cas=None, token=None, dc=None, connections_timeout=None): + def delete(self, key, recurse=None, cas=None, token: str | None = None, dc=None, connections_timeout=None): """ Deletes a single key or if *recurse* is True, all keys sharing a prefix. diff --git a/consul/api/operator.py b/consul/api/operator.py index 3abf6d3..1b03033 100644 --- a/consul/api/operator.py +++ b/consul/api/operator.py @@ -2,7 +2,7 @@ class Operator: - def __init__(self, agent): + def __init__(self, agent) -> None: self.agent = agent def raft_config(self): diff --git a/consul/api/query.py b/consul/api/query.py index b9d01c0..57a921b 100644 --- a/consul/api/query.py +++ b/consul/api/query.py @@ -1,13 +1,16 @@ +from __future__ import annotations + import json +from typing import Optional from consul.callback import CB class Query: - def __init__(self, agent): + def __init__(self, agent) -> None: self.agent = agent - def list(self, dc=None, token=None): + def list(self, dc=None, token: str | None = None): """ Lists all the active queries. This is a privileged endpoint, therefore you will only be able to get the prepared queries @@ -28,14 +31,14 @@ def list(self, dc=None, token=None): def _query_data( self, service=None, - name=None, + name: Optional[str] = None, session=None, - token=None, + token: str | None = None, nearestn=None, datacenters=None, onlypassing=None, tags=None, - ttl=None, + ttl: Optional[int] = None, regexp=None, ): service_body = { @@ -68,15 +71,15 @@ def _query_data( def create( self, service, - name=None, + name: Optional[str] = None, dc=None, session=None, - token=None, + token: str | None = None, nearestn=None, datacenters=None, onlypassing=None, tags=None, - ttl=None, + ttl: Optional[int] = None, regexp=None, ): """ @@ -128,15 +131,15 @@ def update( self, query_id, service=None, - name=None, + name: Optional[str] = None, dc=None, session=None, - token=None, + token: str | None = None, nearestn=None, datacenters=None, onlypassing=None, tags=None, - ttl=None, + ttl: Optional[int] = None, regexp=None, ): """ @@ -149,9 +152,9 @@ def update( path = f"/v1/query/{query_id}" params = None if dc is None else [("dc", dc)] data = self._query_data(service, name, session, token, nearestn, datacenters, onlypassing, tags, ttl, regexp) - return self.agent.http.put(CB.bool(), path, params=params, data=data) + return self.agent.http.put(CB.boolean(), path, params=params, data=data) - def get(self, query_id, token=None, dc=None): + def get(self, query_id, token: str | None = None, dc=None): """ This endpoint will return information about a certain query @@ -168,7 +171,7 @@ def get(self, query_id, token=None, dc=None): headers = self.agent.prepare_headers(token) return self.agent.http.get(CB.json(), f"/v1/query/{query_id}", params=params, headers=headers) - def delete(self, query_id, token=None, dc=None): + def delete(self, query_id, token: str | None = None, dc=None): """ This endpoint will delete certain query @@ -183,9 +186,9 @@ def delete(self, query_id, token=None, dc=None): if dc: params.append(("dc", dc)) headers = self.agent.prepare_headers(token) - return self.agent.http.delete(CB.bool(), f"/v1/query/{query_id}", params=params, headers=headers) + return self.agent.http.delete(CB.boolean(), f"/v1/query/{query_id}", params=params, headers=headers) - def execute(self, query, token=None, dc=None, near=None, limit=None): + def execute(self, query, token: str | None = None, dc=None, near=None, limit: Optional[int] = None): """ This endpoint will execute certain query @@ -212,7 +215,7 @@ def execute(self, query, token=None, dc=None, near=None, limit=None): headers = self.agent.prepare_headers(token) return self.agent.http.get(CB.json(), f"/v1/query/{query}/execute", params=params, headers=headers) - def explain(self, query, token=None, dc=None): + def explain(self, query, token: str | None = None, dc=None): """ This endpoint shows a fully-rendered query for a given name diff --git a/consul/api/session.py b/consul/api/session.py index 48ab4f2..084c8ed 100644 --- a/consul/api/session.py +++ b/consul/api/session.py @@ -1,13 +1,23 @@ import json +from typing import Optional from consul.callback import CB class Session: - def __init__(self, agent): + def __init__(self, agent) -> None: self.agent = agent - def create(self, name=None, node=None, checks=None, lock_delay=15, behavior="release", ttl=None, dc=None): + def create( + self, + name: Optional[str] = None, + node=None, + checks=None, + lock_delay: int = 15, + behavior: str = "release", + ttl: Optional[int] = None, + dc=None, + ): """ Creates a new session. There is more documentation for sessions `here `_. @@ -59,9 +69,9 @@ def create(self, name=None, node=None, checks=None, lock_delay=15, behavior="rel if ttl: assert 10 <= ttl <= 86400 data["ttl"] = f"{ttl}s" - data = json.dumps(data) if data else "" + data_str = json.dumps(data) if data else "" - return self.agent.http.put(CB.json(is_id=True), "/v1/session/create", params=params, data=data) + return self.agent.http.put(CB.json(is_id=True), "/v1/session/create", params=params, data=data_str) def destroy(self, session_id, dc=None): """ @@ -73,7 +83,7 @@ def destroy(self, session_id, dc=None): dc = dc or self.agent.dc if dc: params.append(("dc", dc)) - return self.agent.http.put(CB.bool(), f"/v1/session/destroy/{session_id}", params=params) + return self.agent.http.put(CB.boolean(), f"/v1/session/destroy/{session_id}", params=params) def list(self, index=None, wait=None, consistency=None, dc=None): """ @@ -120,7 +130,7 @@ def list(self, index=None, wait=None, consistency=None, dc=None): params.append((consistency, "1")) return self.agent.http.get(CB.json(index=True), "/v1/session/list", params=params) - def node(self, node, index=None, wait=None, consistency=None, dc=None): + def node(self, node: str, index=None, wait=None, consistency=None, dc=None): """ Returns a tuple of (*index*, *sessions*) as per *session.list*, but filters the sessions returned to only those active for *node*. @@ -149,7 +159,7 @@ def node(self, node, index=None, wait=None, consistency=None, dc=None): params.append((consistency, "1")) return self.agent.http.get(CB.json(index=True), f"/v1/session/node/{node}", params=params) - def info(self, session_id, index=None, wait=None, consistency=None, dc=None): + def info(self, session_id: str, index=None, wait=None, consistency=None, dc=None): """ Returns a tuple of (*index*, *session*) for the session *session_id* in the *dc* datacenter. *dc* defaults to the current diff --git a/consul/api/status.py b/consul/api/status.py index 5c192dc..4fd7386 100644 --- a/consul/api/status.py +++ b/consul/api/status.py @@ -7,7 +7,7 @@ class Status: of the Consul cluster. """ - def __init__(self, agent): + def __init__(self, agent) -> None: self.agent = agent def leader(self): diff --git a/consul/api/txn.py b/consul/api/txn.py index 8c0dabd..f004b75 100644 --- a/consul/api/txn.py +++ b/consul/api/txn.py @@ -9,7 +9,7 @@ class Txn: inside a single, atomic transaction. """ - def __init__(self, agent): + def __init__(self, agent) -> None: self.agent = agent def put(self, payload): diff --git a/consul/base.py b/consul/base.py index 1c9df53..a00aa72 100644 --- a/consul/base.py +++ b/consul/base.py @@ -1,9 +1,11 @@ +from __future__ import annotations + import abc import collections import logging import os import urllib -from typing import Dict, Optional +from typing import TYPE_CHECKING, Any, Dict, Optional, Type from consul.api.acl import ACL from consul.api.agent import Agent @@ -20,6 +22,9 @@ from consul.api.txn import Txn from consul.exceptions import ConsulException +if TYPE_CHECKING: + from types import TracebackType + log = logging.getLogger(__name__) @@ -31,7 +36,9 @@ class HTTPClient(metaclass=abc.ABCMeta): - def __init__(self, host="127.0.0.1", port=8500, scheme="http", verify=True, cert=None): + def __init__( + self, host: str = "127.0.0.1", port: int = 8500, scheme: str = "http", verify: bool = True, cert=None + ) -> None: self.host = host self.port = port self.scheme = scheme @@ -39,7 +46,7 @@ def __init__(self, host="127.0.0.1", port=8500, scheme="http", verify=True, cert self.base_uri = f"{self.scheme}://{self.host}:{self.port}" self.cert = cert - def uri(self, path, params=None): + def uri(self, path: str, params: list[tuple[str, Any]] | None = None): uri = self.base_uri + urllib.parse.quote(path, safe="/:") if params: uri = f"{uri}?{urllib.parse.urlencode(params)}" @@ -50,7 +57,7 @@ def get(self, callback, path, params=None, headers: Optional[Dict[str, str]] = N raise NotImplementedError @abc.abstractmethod - def put(self, callback, path, params=None, data="", headers: Optional[Dict[str, str]] = None): + def put(self, callback, path, params=None, data: str = "", headers: Optional[Dict[str, str]] = None): raise NotImplementedError @abc.abstractmethod @@ -58,7 +65,7 @@ def delete(self, callback, path, params=None, headers: Optional[Dict[str, str]] raise NotImplementedError @abc.abstractmethod - def post(self, callback, path, params=None, data="", headers: Optional[Dict[str, str]] = None): + def post(self, callback, path, params=None, data: str = "", headers: Optional[Dict[str, str]] = None): raise NotImplementedError @abc.abstractmethod @@ -69,15 +76,15 @@ def close(self): class Consul: def __init__( self, - host="127.0.0.1", - port=8500, - token=None, - scheme="http", - consistency="default", + host: str = "127.0.0.1", + port: int = 8500, + token: str | None = None, + scheme: str = "http", + consistency: str = "default", dc=None, - verify=True, + verify: bool = True, cert=None, - ): + ) -> None: """ *token* is an optional `ACL token`_. If supplied it will be used by default for all requests made with this client session. It's still @@ -101,7 +108,7 @@ def __init__( if os.getenv("CONSUL_HTTP_ADDR"): try: - host, port = os.getenv("CONSUL_HTTP_ADDR").split(":") + host, port = os.getenv("CONSUL_HTTP_ADDR").split(":") # type: ignore except ValueError as err: raise ConsulException( f"CONSUL_HTTP_ADDR ({os.getenv('CONSUL_HTTP_ADDR')}) invalid, does not match :" @@ -143,18 +150,22 @@ def __enter__(self): async def __aenter__(self): return self - def __exit__(self, exc_type, exc_val, exc_tb): + def __exit__( + self, exc_type: Optional[Type[BaseException]], exc_val: Optional[BaseException], exc_tb: Optional[TracebackType] + ) -> None: self.http.close() - async def __aexit__(self, exc_type, exc, tb): + async def __aexit__( + self, exc_type: Optional[Type[BaseException]], exc: Optional[BaseException], tb: Optional[TracebackType] + ) -> None: await self.http.close() @abc.abstractmethod - def http_connect(self, host, port, scheme, verify=True, cert=None): + def http_connect(self, host: str, port: int, scheme, verify: bool = True, cert=None): pass def prepare_headers(self, token: Optional[str] = None) -> Dict[str, str]: headers = {} if token or self.token: headers["X-Consul-Token"] = token or self.token - return headers + return headers # type: ignore diff --git a/consul/callback.py b/consul/callback.py index 64c85c0..d4637a2 100644 --- a/consul/callback.py +++ b/consul/callback.py @@ -1,15 +1,21 @@ +from __future__ import annotations + import base64 import json +from typing import TYPE_CHECKING, Callable from consul.exceptions import ACLDisabled, ACLPermissionDenied, BadRequest, ClientError, ConsulException, NotFound +if TYPE_CHECKING: + from consul.base import Response + # # Conveniences to create consistent callback handlers for endpoints class CB: @classmethod - def _status(cls, response, allow_404=True): + def _status(cls, response: Response, allow_404: bool = True) -> None: # status checking if 400 <= response.code < 500: if response.code == 400: @@ -27,7 +33,7 @@ def _status(cls, response, allow_404=True): raise ConsulException(f"{response.code} {response.body}") @classmethod - def bool(cls): + def boolean(cls) -> Callable[[Response], bool]: # returns True on successful response def cb(response): CB._status(response) @@ -36,7 +42,15 @@ def cb(response): return cb @classmethod - def json(cls, postprocess=None, allow_404=True, one=False, decode=False, is_id=False, index=False): + def json( + cls, + postprocess=None, + allow_404: bool = True, + one: bool = False, + decode: bool | str = False, + is_id: bool = False, + index: bool = False, + ): """ *postprocess* is a function to apply to the final result. diff --git a/consul/check.py b/consul/check.py index eb8c519..d7ee206 100644 --- a/consul/check.py +++ b/consul/check.py @@ -1,5 +1,8 @@ +from __future__ import annotations + import logging import warnings +from typing import Any, Optional log = logging.getLogger(__name__) @@ -10,7 +13,7 @@ class Check: """ @classmethod - def script(cls, args, interval, deregister=None): + def script(cls, args, interval, deregister=None) -> dict[str, Any]: """ Run the script *args* every *interval* (e.g. "10s") to peform health check @@ -24,7 +27,7 @@ def script(cls, args, interval, deregister=None): return ret @classmethod - def http(cls, url, interval, timeout=None, deregister=None, header=None): + def http(cls, url, interval, timeout=None, deregister=None, header=None) -> dict[str, Any]: """ Peform a HTTP GET against *url* every *interval* (e.g. "10s") to peform health check with an optional *timeout* and optional *deregister* after @@ -43,7 +46,7 @@ def http(cls, url, interval, timeout=None, deregister=None, header=None): return ret @classmethod - def tcp(cls, host, port, interval, timeout=None, deregister=None): + def tcp(cls, host: str, port: int, interval, timeout=None, deregister=None) -> dict[str, Any]: """ Attempt to establish a tcp connection to the specified *host* and *port* at a specified *interval* with optional *timeout* and optional @@ -58,7 +61,7 @@ def tcp(cls, host, port, interval, timeout=None, deregister=None): return ret @classmethod - def ttl(cls, ttl): + def ttl(cls, ttl: str) -> dict[str, Any]: """ Set check to be marked as critical after *ttl* (e.g. "10s") unless the check is periodically marked as passing. @@ -66,7 +69,7 @@ def ttl(cls, ttl): return {"ttl": ttl} @classmethod - def docker(cls, container_id, shell, script, interval, deregister=None): + def docker(cls, container_id, shell, script, interval, deregister=None) -> dict[str, Any]: """ Invoke *script* packaged within a running docker container with *container_id* at a specified *interval* on the configured @@ -79,13 +82,15 @@ def docker(cls, container_id, shell, script, interval, deregister=None): return ret @classmethod - def _compat(cls, script=None, interval=None, ttl=None, http=None, timeout=None, deregister=None): + def _compat( + cls, script=None, interval=None, ttl: Optional[int] = None, http=None, timeout=None, deregister=None + ) -> dict[str, Any]: if not script and not http and not ttl: return {} log.warning("DEPRECATED: use consul.Check.script/http/ttl to specify check") - ret = {"check": {}} + ret: dict[str, Any] = {"check": {}} if script: assert interval diff --git a/consul/std.py b/consul/std.py index 5c2e159..30cfa66 100644 --- a/consul/std.py +++ b/consul/std.py @@ -1,6 +1,7 @@ from typing import Dict, Optional import requests +from requests import Response from consul import base @@ -8,11 +9,11 @@ class HTTPClient(base.HTTPClient): - def __init__(self, *args, **kwargs): + def __init__(self, *args, **kwargs) -> None: super().__init__(*args, **kwargs) self.session = requests.session() - def response(self, response): + def response(self, response: Response): response.encoding = "utf-8" return base.Response(response.status_code, response.headers, response.text) @@ -20,7 +21,7 @@ def get(self, callback, path, params=None, headers: Optional[Dict[str, str]] = N uri = self.uri(path, params) return callback(self.response(self.session.get(uri, headers=headers, verify=self.verify, cert=self.cert))) - def put(self, callback, path, params=None, data="", headers: Optional[Dict[str, str]] = None): + def put(self, callback, path, params=None, data: str = "", headers: Optional[Dict[str, str]] = None): uri = self.uri(path, params) return callback( self.response(self.session.put(uri, headers=headers, data=data, verify=self.verify, cert=self.cert)) @@ -30,16 +31,16 @@ def delete(self, callback, path, params=None, headers: Optional[Dict[str, str]] uri = self.uri(path, params) return callback(self.response(self.session.delete(uri, headers=headers, verify=self.verify, cert=self.cert))) - def post(self, callback, path, params=None, data="", headers: Optional[Dict[str, str]] = None): + def post(self, callback, path, params=None, data: str = "", headers: Optional[Dict[str, str]] = None): uri = self.uri(path, params) return callback( self.response(self.session.post(uri, headers=headers, data=data, verify=self.verify, cert=self.cert)) ) - def close(self): + def close(self) -> None: pass class Consul(base.Consul): - def http_connect(self, host, port, scheme, verify=True, cert=None): + def http_connect(self, host: str, port: int, scheme, verify: bool = True, cert=None): return HTTPClient(host, port, scheme, verify, cert) diff --git a/pyproject.toml b/pyproject.toml index 9c6b09b..7971de3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [tool.pytest.ini_options] -addopts = "--cov=. --cov-context=test --durations=0 --durations-min=1.0" +addopts = "--cov=. --cov-context=test --durations=0 --durations-min=3.0" asyncio_mode = "auto" asyncio_default_fixture_loop_scope = "function" @@ -93,6 +93,7 @@ fixable = [ "SIM", "SIM", "UP", + "TCH", ] # Never enforce some rules @@ -118,4 +119,18 @@ max-complexity = 15 [tool.ruff.lint.pyupgrade] # Preserve types, even if a file imports `from __future__ import annotations`. -keep-runtime-typing = true \ No newline at end of file +keep-runtime-typing = true + +[tool.mypy] +# error whenever it encounters a function definition without type annotations +#disallow_untyped_defs = true +# error whenever a function with type annotations calls a function defined without annotations +disallow_untyped_calls = true +# stop treating arguments with a None default value as having an implicit Optional type +no_implicit_optional = true +# error whenever your code uses an unnecessary cast that can safely be removed +warn_redundant_casts = true +# Shows a warning when encountering any code inferred to be unreachable or redundant after performing type analysis. +warn_unreachable = true +# Prohibit equality checks, identity checks, and container checks between non-overlapping types. +strict_equality = true \ No newline at end of file diff --git a/tests-requirements.txt b/tests-requirements.txt index 9a65c21..146def4 100644 --- a/tests-requirements.txt +++ b/tests-requirements.txt @@ -1,6 +1,7 @@ aiohttp asynctest docker +mypy pre-commit pyOpenSSL pylint @@ -14,6 +15,8 @@ setuptools tox tox-uv treq +types-docker +types-setuptools uv virtualenv wheel \ No newline at end of file diff --git a/tests/api/test_acl.py b/tests/api/test_acl.py index 367d5e4..4abc4e6 100644 --- a/tests/api/test_acl.py +++ b/tests/api/test_acl.py @@ -5,7 +5,7 @@ class TestConsulAcl: - def test_acl_token_permission_denied(self, acl_consul): + def test_acl_token_permission_denied(self, acl_consul) -> None: c, _master_token, _consul_version = acl_consul # No token @@ -48,7 +48,7 @@ def test_acl_token_permission_denied(self, acl_consul): token="anonymous", ) - def test_acl_token_list(self, acl_consul): + def test_acl_token_list(self, acl_consul) -> None: c, master_token, _consul_version = acl_consul # Make sure both master and anonymous tokens are created @@ -66,7 +66,7 @@ def test_acl_token_list(self, acl_consul): assert find_recursive(acls, master_token_repr) assert find_recursive(acls, anonymous_token_repr) - def test_acl_token_read(self, acl_consul): + def test_acl_token_read(self, acl_consul) -> None: c, master_token, _consul_version = acl_consul # Unknown token @@ -79,7 +79,7 @@ def test_acl_token_read(self, acl_consul): acl = c.acl.token.read(accessor_id="00000000-0000-0000-0000-000000000002", token=master_token) assert find_recursive(acl, anonymous_token_repr) - def test_acl_token_create(self, acl_consul): + def test_acl_token_create(self, acl_consul) -> None: c, master_token, _consul_version = acl_consul c.acl.token.create(accessor_id="00000000-DEAD-BEEF-0000-000000000000", token=master_token) @@ -112,7 +112,7 @@ def test_acl_token_create(self, acl_consul): acl = c.acl.token.list(token=master_token) assert find_recursive(acl, expected) - def test_acl_token_clone(self, acl_consul): + def test_acl_token_clone(self, acl_consul) -> None: c, master_token, _consul_version = acl_consul assert len(c.acl.token.list(token=master_token)) == 2 @@ -135,7 +135,7 @@ def test_acl_token_clone(self, acl_consul): acl = c.acl.token.list(token=master_token) assert find_recursive(acl, expected) - def test_acl_token_update(self, acl_consul): + def test_acl_token_update(self, acl_consul) -> None: c, master_token, _consul_version = acl_consul # Unknown token @@ -158,7 +158,7 @@ def test_acl_token_update(self, acl_consul): acl = c.acl.token.read(accessor_id="00000000-DEAD-BEEF-0000-000000000000", token=master_token) assert find_recursive(acl, expected) - def test_acl_token_delete(self, acl_consul): + def test_acl_token_delete(self, acl_consul) -> None: c, master_token, _consul_version = acl_consul assert len(c.acl.token.list(token=master_token)) == 2 @@ -176,14 +176,14 @@ def test_acl_token_delete(self, acl_consul): token=master_token, ) - def test_acl_policy_list(self, acl_consul): + def test_acl_policy_list(self, acl_consul) -> None: c, master_token, _consul_version = acl_consul # Make sure both master and anonymous tokens are created policies = c.acl.policy.list(token=master_token) assert find_recursive(policies, {"ID": "00000000-0000-0000-0000-000000000001", "Name": "global-management"}) - def test_acl_policy_read(self, acl_consul): + def test_acl_policy_read(self, acl_consul) -> None: c, master_token, _consul_version = acl_consul # Unknown token diff --git a/tests/api/test_agent.py b/tests/api/test_agent.py index 0fed2ed..41b34cf 100644 --- a/tests/api/test_agent.py +++ b/tests/api/test_agent.py @@ -10,16 +10,16 @@ class TestAgent: - def test_agent_checks(self, consul_port): + def test_agent_checks(self, consul_port) -> None: consul_port, _consul_version = consul_port c = consul.Consul(port=consul_port) - def verify_and_dereg_check(check_id): + def verify_and_dereg_check(check_id) -> None: assert set(c.agent.checks().keys()) == {check_id} assert c.agent.check.deregister(check_id) is True assert set(c.agent.checks().keys()) == set() - def verify_check_status(check_id, status, notes=None): + def verify_check_status(check_id, status, notes=None) -> None: checks = c.agent.checks() assert checks[check_id]["Status"] == status if notes: @@ -68,7 +68,7 @@ def verify_check_status(check_id, status, notes=None): verify_check_status("ttl_check", "critical") verify_and_dereg_check("ttl_check") - def test_service_multi_check(self, consul_port): + def test_service_multi_check(self, consul_port) -> None: consul_port, _consul_version = consul_port c = consul.Consul(port=consul_port) http_addr = "http://127.0.0.1:8500" @@ -98,7 +98,7 @@ def test_service_multi_check(self, consul_port): assert [check["CheckID"] for check in checks] == ["service:foo1:1", "service:foo1:2", "service:foo1:3"] assert [check["Status"] for check in checks] == ["passing", "passing", "passing"] - def test_service_dereg_issue_156(self, consul_port): + def test_service_dereg_issue_156(self, consul_port) -> None: consul_port, _consul_version = consul_port # https://github.com/cablehead/python-consul/issues/156 service_name = "app#127.0.0.1#3000" @@ -118,7 +118,7 @@ def test_service_dereg_issue_156(self, consul_port): _index, nodes = c.health.service(service_name) assert [node["Service"]["ID"] for node in nodes] == [] - def test_agent_checks_service_id(self, consul_obj): + def test_agent_checks_service_id(self, consul_obj) -> None: c, _consul_version = consul_obj c.agent.service.register("foo1") @@ -146,7 +146,7 @@ def test_agent_checks_service_id(self, consul_obj): time.sleep(40 / 1000.0) - def test_agent_register_check_no_service_id(self, consul_obj): + def test_agent_register_check_no_service_id(self, consul_obj) -> None: c, _consul_version = consul_obj _index, nodes = c.health.service("foo1") assert nodes == [] @@ -166,7 +166,7 @@ def test_agent_register_check_no_service_id(self, consul_obj): time.sleep(40 / 1000.0) - def test_agent_register_enable_tag_override(self, consul_obj): + def test_agent_register_enable_tag_override(self, consul_obj) -> None: c, _consul_version = consul_obj _index, nodes = c.health.service("foo1") assert nodes == [] @@ -177,7 +177,7 @@ def test_agent_register_enable_tag_override(self, consul_obj): # Cleanup tasks c.agent.check.deregister("foo") - def test_agent_service_maintenance(self, consul_obj): + def test_agent_service_maintenance(self, consul_obj) -> None: c, _consul_version = consul_obj c.agent.service.register("foo", check=Check.ttl("100ms")) @@ -204,7 +204,7 @@ def test_agent_service_maintenance(self, consul_obj): time.sleep(40 / 1000.0) - def test_agent_node_maintenance(self, consul_obj): + def test_agent_node_maintenance(self, consul_obj) -> None: c, _consul_version = consul_obj c.agent.maintenance("true", "test") @@ -222,7 +222,7 @@ def test_agent_node_maintenance(self, consul_obj): checks_post = c.agent.checks() assert "_node_maintenance" not in checks_post - def test_agent_members(self, consul_obj): + def test_agent_members(self, consul_obj) -> None: c, _consul_version = consul_obj members = c.agent.members() for x in members: @@ -235,7 +235,7 @@ def test_agent_members(self, consul_obj): for x in wan_members: assert "dc1" in x["Name"] - def test_agent_self(self, consul_obj): + def test_agent_self(self, consul_obj) -> None: c, _consul_version = consul_obj EXPECTED = { @@ -247,7 +247,7 @@ def test_agent_self(self, consul_obj): expected = EXPECTED["v2"] assert set(c.agent.self().keys()) == expected - def test_agent_services(self, consul_obj): + def test_agent_services(self, consul_obj) -> None: c, _consul_version = consul_obj assert c.agent.service.register("foo") is True assert set(c.agent.services().keys()) == {"foo"} diff --git a/tests/api/test_coordinates.py b/tests/api/test_coordinates.py index 11f4c5d..1437855 100644 --- a/tests/api/test_coordinates.py +++ b/tests/api/test_coordinates.py @@ -1,5 +1,5 @@ class TestCoordinates: - def test_coordinate(self, consul_obj): + def test_coordinate(self, consul_obj) -> None: c, _consul_version = consul_obj c.coordinate.nodes() c.coordinate.datacenters() diff --git a/tests/api/test_event.py b/tests/api/test_event.py index 3badbb4..e43a71e 100644 --- a/tests/api/test_event.py +++ b/tests/api/test_event.py @@ -1,5 +1,5 @@ class TestEvent: - def test_event(self, consul_obj): + def test_event(self, consul_obj) -> None: c, _consul_version = consul_obj assert c.event.fire("fooname", "foobody") @@ -7,7 +7,7 @@ def test_event(self, consul_obj): assert [x["Name"] == "fooname" for x in events] assert [x["Payload"] == "foobody" for x in events] - def test_event_targeted(self, consul_obj): + def test_event_targeted(self, consul_obj) -> None: c, _consul_version = consul_obj assert c.event.fire("fooname", "foobody") diff --git a/tests/api/test_health.py b/tests/api/test_health.py index b69c112..d8a068d 100644 --- a/tests/api/test_health.py +++ b/tests/api/test_health.py @@ -6,7 +6,7 @@ class TestHealth: - def test_health_service(self, consul_obj): + def test_health_service(self, consul_obj) -> None: c, _consul_version = consul_obj # check there are no nodes for the service 'foo' @@ -66,7 +66,7 @@ def test_health_service(self, consul_obj): _index, nodes = c.health.service("foo") assert nodes == [] - def test_health_state(self, consul_obj): + def test_health_state(self, consul_obj) -> None: c, _consul_version = consul_obj # The empty string is for the Serf Health Status check, which has an @@ -86,7 +86,7 @@ def test_health_state(self, consul_obj): # but that they aren't passing their health check _index, nodes = c.health.state("passing") - assert [node["ServiceID"] for node in nodes] != "foo" + assert "foo" not in [node["ServiceID"] for node in nodes] # ping the two node's health check c.agent.check.ttl_pass("service:foo:1") @@ -123,14 +123,14 @@ def test_health_state(self, consul_obj): _index, nodes = c.health.state("any") assert [node["ServiceID"] for node in nodes] == [""] - def test_health_node(self, consul_obj): + def test_health_node(self, consul_obj) -> None: c, _consul_version = consul_obj # grab local node name node = c.agent.self()["Config"]["NodeName"] _index, checks = c.health.node(node) assert node in [check["Node"] for check in checks] - def test_health_checks(self, consul_obj): + def test_health_checks(self, consul_obj) -> None: c, _consul_version = consul_obj c.agent.service.register("foobar", service_id="foobar", check=Check.ttl("10s")) diff --git a/tests/api/test_kv.py b/tests/api/test_kv.py index 695b682..fbb6b14 100644 --- a/tests/api/test_kv.py +++ b/tests/api/test_kv.py @@ -6,7 +6,7 @@ class TestConsul: - def test_kv(self, consul_obj): + def test_kv(self, consul_obj) -> None: c, _consul_version = consul_obj _index, data = c.kv.get("foo") assert data is None @@ -14,14 +14,14 @@ def test_kv(self, consul_obj): _index, data = c.kv.get("foo") assert data["Value"] == b"bar" - def test_kv_wait(self, consul_obj): + def test_kv_wait(self, consul_obj) -> None: c, _consul_version = consul_obj assert c.kv.put("foo", "bar") is True index, _data = c.kv.get("foo") check, _data = c.kv.get("foo", index=index, wait="20ms") assert index == check - def test_kv_encoding(self, consul_obj): + def test_kv_encoding(self, consul_obj) -> None: c, _consul_version = consul_obj # test binary @@ -47,7 +47,7 @@ def test_kv_encoding(self, consul_obj): # check unencoded values raises assert pytest.raises(AssertionError, c.kv.put, "foo", {1: 2}) - def test_kv_put_cas(self, consul_obj): + def test_kv_put_cas(self, consul_obj) -> None: c, _consul_version = consul_obj assert c.kv.put("foo", "bar", cas=50) is False assert c.kv.put("foo", "bar", cas=0) is True @@ -58,7 +58,7 @@ def test_kv_put_cas(self, consul_obj): _index, data = c.kv.get("foo") assert data["Value"] == b"bar2" - def test_kv_put_flags(self, consul_obj): + def test_kv_put_flags(self, consul_obj) -> None: c, _consul_version = consul_obj c.kv.put("foo", "bar") _index, data = c.kv.get("foo") @@ -68,7 +68,7 @@ def test_kv_put_flags(self, consul_obj): _index, data = c.kv.get("foo") assert data["Flags"] == 50 - def test_kv_recurse(self, consul_obj): + def test_kv_recurse(self, consul_obj) -> None: c, _consul_version = consul_obj _index, data = c.kv.get("foo/", recurse=True) assert data is None @@ -84,7 +84,7 @@ def test_kv_recurse(self, consul_obj): assert [x["Key"] for x in data] == ["foo/", "foo/bar1", "foo/bar2", "foo/bar3"] assert [x["Value"] for x in data] == [None, b"1", b"2", b"3"] - def test_kv_delete(self, consul_obj): + def test_kv_delete(self, consul_obj) -> None: c, _consul_version = consul_obj c.kv.put("foo1", "1") c.kv.put("foo2", "2") @@ -99,7 +99,7 @@ def test_kv_delete(self, consul_obj): _index, data = c.kv.get("foo", recurse=True) assert data is None - def test_kv_delete_cas(self, consul_obj): + def test_kv_delete_cas(self, consul_obj) -> None: c, _consul_version = consul_obj c.kv.put("foo", "bar") @@ -112,7 +112,7 @@ def test_kv_delete_cas(self, consul_obj): index, data = c.kv.get("foo") assert data is None - def test_kv_acquire_release(self, consul_obj): + def test_kv_acquire_release(self, consul_obj) -> None: c, _consul_version = consul_obj pytest.raises(ConsulException, c.kv.put, "foo", "bar", acquire="foo") @@ -130,7 +130,7 @@ def test_kv_acquire_release(self, consul_obj): c.session.destroy(s1) c.session.destroy(s2) - def test_kv_keys_only(self, consul_obj): + def test_kv_keys_only(self, consul_obj) -> None: c, _consul_version = consul_obj assert c.kv.put("bar", "4") is True diff --git a/tests/api/test_operator.py b/tests/api/test_operator.py index 8fcca95..b189275 100644 --- a/tests/api/test_operator.py +++ b/tests/api/test_operator.py @@ -2,7 +2,7 @@ class TestOperator: - def test_operator(self, consul_obj): + def test_operator(self, consul_obj) -> None: c, _consul_version = consul_obj config = c.operator.raft_config() diff --git a/tests/api/test_query.py b/tests/api/test_query.py index 2604c4d..87f1976 100644 --- a/tests/api/test_query.py +++ b/tests/api/test_query.py @@ -1,5 +1,5 @@ class testQuery: - def test_query(self, consul_obj): + def test_query(self, consul_obj) -> None: c, _consul_version = consul_obj # check that query list is empty diff --git a/tests/api/test_session.py b/tests/api/test_session.py index b3c5482..494cc4c 100644 --- a/tests/api/test_session.py +++ b/tests/api/test_session.py @@ -4,7 +4,7 @@ class TestSession: - def test_session(self, consul_obj): + def test_session(self, consul_obj) -> None: c, _consul_version = consul_obj # session.create @@ -36,7 +36,7 @@ def test_session(self, consul_obj): _, sessions = c.session.list() assert sessions == [] - def test_session_delete_ttl_renew(self, consul_obj): + def test_session_delete_ttl_renew(self, consul_obj) -> None: c, _consul_version = consul_obj s = c.session.create(behavior="delete", ttl=20) diff --git a/tests/api/test_status.py b/tests/api/test_status.py index 0952b37..c32cf1f 100644 --- a/tests/api/test_status.py +++ b/tests/api/test_status.py @@ -1,5 +1,5 @@ class TestStatus: - def test_status_leader(self, consul_obj): + def test_status_leader(self, consul_obj) -> None: c, _consul_version = consul_obj agent_self = c.agent.self() @@ -8,7 +8,7 @@ def test_status_leader(self, consul_obj): assert leader == addr_port, f"Leader value was {leader}, expected value was {addr_port}" - def test_status_peers(self, consul_obj): + def test_status_peers(self, consul_obj) -> None: c, _consul_version = consul_obj agent_self = c.agent.self() diff --git a/tests/api/test_txn.py b/tests/api/test_txn.py index 49c4d52..7ec44d5 100644 --- a/tests/api/test_txn.py +++ b/tests/api/test_txn.py @@ -2,7 +2,7 @@ class TestTxn: - def test_transaction(self, consul_obj): + def test_transaction(self, consul_obj) -> None: c, _consul_version = consul_obj value = base64.b64encode(b"1").decode("utf8") d = {"KV": {"Verb": "set", "Key": "asdf", "Value": value}} diff --git a/tests/test_aio.py b/tests/test_aio.py index 8e9767d..2c04191 100644 --- a/tests/test_aio.py +++ b/tests/test_aio.py @@ -29,7 +29,7 @@ async def consul_acl_obj(acl_consul): class TestAsyncioConsul: - async def test_kv(self, consul_obj): + async def test_kv(self, consul_obj) -> None: c, _consul_version = consul_obj _index, data = await c.kv.get("foo") assert data is None @@ -38,22 +38,22 @@ async def test_kv(self, consul_obj): _index, data = await c.kv.get("foo") assert data["Value"] == b"bar" - async def test_consul_ctor(self, consul_obj): + async def test_consul_ctor(self, consul_obj) -> None: c, _consul_version = consul_obj await c.kv.put("foo", struct.pack("i", 1000)) _index, data = await c.kv.get("foo") assert struct.unpack("i", data["Value"]) == (1000,) - async def test_kv_binary(self, consul_obj): + async def test_kv_binary(self, consul_obj) -> None: c, _consul_version = consul_obj await c.kv.put("foo", struct.pack("i", 1000)) _index, data = await c.kv.get("foo") assert struct.unpack("i", data["Value"]) == (1000,) - async def test_kv_missing(self, consul_obj): + async def test_kv_missing(self, consul_obj) -> None: c, _consul_version = consul_obj - async def put(): + async def put() -> None: await asyncio.sleep(2.0 / 100) await c.kv.put("foo", "bar") @@ -66,7 +66,7 @@ async def put(): await fut await c.close() - async def test_kv_put_flags(self, consul_obj): + async def test_kv_put_flags(self, consul_obj) -> None: c, _consul_version = consul_obj await c.kv.put("foo", "bar") _index, data = await c.kv.get("foo") @@ -77,7 +77,7 @@ async def test_kv_put_flags(self, consul_obj): _index, data = await c.kv.get("foo") assert data["Flags"] == 50 - async def test_kv_delete(self, consul_obj): + async def test_kv_delete(self, consul_obj) -> None: c, _consul_version = consul_obj await c.kv.put("foo1", "1") await c.kv.put("foo2", "2") @@ -94,10 +94,10 @@ async def test_kv_delete(self, consul_obj): _index, data = await c.kv.get("foo", recurse=True) assert data is None - async def test_kv_subscribe(self, consul_obj): + async def test_kv_subscribe(self, consul_obj) -> None: c, _consul_version = consul_obj - async def put(): + async def put() -> None: await asyncio.sleep(1.0 / 100) response = await c.kv.put("foo", "bar") assert response is True @@ -109,7 +109,7 @@ async def put(): assert data["Value"] == b"bar" await fut - async def test_transaction(self, consul_obj): + async def test_transaction(self, consul_obj) -> None: c, _consul_version = consul_obj value = base64.b64encode(b"1").decode("utf8") d = {"KV": {"Verb": "set", "Key": "asdf", "Value": value}} @@ -120,7 +120,7 @@ async def test_transaction(self, consul_obj): r = await c.txn.put([d]) assert r["Results"][0]["KV"]["Value"] == value - async def test_agent_services(self, consul_obj): + async def test_agent_services(self, consul_obj) -> None: c, _consul_version = consul_obj EXPECTED = { "v1": { @@ -195,10 +195,10 @@ async def test_agent_services(self, consul_obj): # assert [x["Node"] for x in nodes] == [] # await fut - async def test_session(self, consul_obj): + async def test_session(self, consul_obj) -> None: c, _consul_version = consul_obj - async def register(): + async def register() -> None: await asyncio.sleep(1.0 / 100) session_id = await c.session.create() await asyncio.sleep(50 / 1000.0) diff --git a/tests/test_base.py b/tests/test_base.py index f448701..c2e2ab6 100644 --- a/tests/test_base.py +++ b/tests/test_base.py @@ -1,22 +1,26 @@ +from __future__ import annotations + import collections import json +from typing import Any, Callable, Optional import pytest -import consul import consul.check Request = collections.namedtuple("Request", ["method", "path", "params", "headers", "data"]) class HTTPClient: - def __init__(self, host=None, port=None, scheme=None, verify=True, cert=None): + def __init__( + self, host: Optional[str] = None, port: Optional[int] = None, scheme=None, verify: bool = True, cert=None + ) -> None: pass def get(self, callback, path, params=None, headers=None): # pylint: disable=unused-argument return Request("get", path, params, headers, None) - def put(self, callback, path, params=None, headers=None, data=""): # pylint: disable=unused-argument + def put(self, callback, path, params=None, headers=None, data: str = ""): # pylint: disable=unused-argument return Request("put", path, params, headers, data) def delete(self, callback, path, params=None, headers=None): # pylint: disable=unused-argument @@ -24,11 +28,11 @@ def delete(self, callback, path, params=None, headers=None): # pylint: disable= class Consul(consul.base.Consul): - def http_connect(self, host, port, scheme, verify=True, cert=None): + def http_connect(self, host: str, port: int, scheme, verify: bool = True, cert=None): return HTTPClient(host, port, scheme, verify=verify, cert=None) -def _should_support(c): +def _should_support(c: Consul) -> tuple[Callable[..., Any], ...]: return ( # kv lambda **kw: c.kv.get("foo", **kw), @@ -44,7 +48,7 @@ def _should_support(c): ) -def _should_support_node_meta(c): +def _should_support_node_meta(c: Consul) -> tuple[Callable[..., Any], ...]: return ( # catalog c.catalog.nodes, @@ -58,7 +62,7 @@ def _should_support_node_meta(c): ) -def _should_support_meta(c): +def _should_support_meta(c: Consul) -> tuple[Callable[..., Any], ...]: return ( # agent lambda **kw: c.agent.service.register("foo", **kw), @@ -71,7 +75,7 @@ class TestIndex: Tests read requests that should support blocking on an index """ - def test_index(self): + def test_index(self) -> None: c = Consul() for r in _should_support(c): assert r().params == [] @@ -83,7 +87,7 @@ class TestConsistency: Tests read requests that should support consistency modes """ - def test_explict(self): + def test_explict(self) -> None: c = Consul() for r in _should_support(c): assert r().params == [] @@ -91,7 +95,7 @@ def test_explict(self): assert r(consistency="consistent").params == [("consistent", "1")] assert r(consistency="stale").params == [("stale", "1")] - def test_implicit(self): + def test_implicit(self) -> None: c = Consul(consistency="consistent") for r in _should_support(c): assert r().params == [("consistent", "1")] @@ -105,7 +109,7 @@ class TestNodemeta: Tests read requests that should support node_meta """ - def test_node_meta(self): + def test_node_meta(self) -> None: c = Consul() for r in _should_support_node_meta(c): assert r().params == [] @@ -120,7 +124,7 @@ class TestMeta: Tests read requests that should support meta """ - def test_meta(self): + def test_meta(self) -> None: c = Consul() for r in _should_support_meta(c): d = json.loads(r(meta={"env": "prod", "net": 1}).data) @@ -199,7 +203,7 @@ class TestChecks: ), ], ) - def test_http_check(self, url, interval, timeout, deregister, header, want): + def test_http_check(self, url, interval, timeout, deregister, header, want) -> None: ch = consul.check.Check.http(url, interval, timeout=timeout, deregister=deregister, header=header) assert ch == want @@ -256,7 +260,7 @@ def test_http_check(self, url, interval, timeout, deregister, header, want): ), ], ) - def test_tcp_check(self, host, port, interval, timeout, deregister, want): + def test_tcp_check(self, host: str, port: int, interval, timeout, deregister, want) -> None: ch = consul.check.Check.tcp(host, port, interval, timeout=timeout, deregister=deregister) assert ch == want @@ -292,10 +296,10 @@ def test_tcp_check(self, host, port, interval, timeout, deregister, want): ), ], ) - def test_docker_check(self, container_id, shell, script, interval, deregister, want): + def test_docker_check(self, container_id, shell, script, interval, deregister, want) -> None: ch = consul.check.Check.docker(container_id, shell, script, interval, deregister=deregister) assert ch == want - def test_ttl_check(self): + def test_ttl_check(self) -> None: ch = consul.check.Check.ttl("1m") assert ch == {"ttl": "1m"} diff --git a/tests/test_callback.py b/tests/test_callback.py index 0364165..cb8d3ea 100644 --- a/tests/test_callback.py +++ b/tests/test_callback.py @@ -8,7 +8,7 @@ class TestCB: # pylint: disable=protected-access - def test_status_200_passes(self): + def test_status_200_passes(self) -> None: response = consul.base.Response(200, None, None) CB._status(response) @@ -20,20 +20,20 @@ def test_status_200_passes(self): (Response(403, None, None), ACLPermissionDenied), ], ) - def test_status_4xx_raises_error(self, response, expected_exception): + def test_status_4xx_raises_error(self, response, expected_exception) -> None: with pytest.raises(expected_exception): CB._status(response) - def test_status_404_allow_404(self): + def test_status_404_allow_404(self) -> None: response = Response(404, None, None) CB._status(response, allow_404=True) - def test_status_404_dont_allow_404(self): + def test_status_404_dont_allow_404(self) -> None: response = Response(404, None, None) with pytest.raises(NotFound): CB._status(response, allow_404=False) - def test_status_405_raises_generic_ClientError(self): + def test_status_405_raises_generic_ClientError(self) -> None: response = Response(405, None, None) with pytest.raises(ClientError): CB._status(response) @@ -45,6 +45,6 @@ def test_status_405_raises_generic_ClientError(self): Response(599, None, None), ], ) - def test_status_5xx_raises_error(self, response): + def test_status_5xx_raises_error(self, response) -> None: with pytest.raises(consul.base.ConsulException): CB._status(response) diff --git a/tests/test_std.py b/tests/test_std.py index 1aaeee3..cb2930e 100644 --- a/tests/test_std.py +++ b/tests/test_std.py @@ -4,7 +4,7 @@ class TestHTTPClient: - def test_uri(self): + def test_uri(self) -> None: http = consul.std.HTTPClient() assert http.uri("/v1/kv") == "http://127.0.0.1:8500/v1/kv" - assert http.uri("/v1/kv", params={"index": 1}) == "http://127.0.0.1:8500/v1/kv?index=1" + assert http.uri("/v1/kv", params=[("index", 1)]) == "http://127.0.0.1:8500/v1/kv?index=1" diff --git a/tests/test_utils.py b/tests/test_utils.py index b6844f7..2bc0648 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -1,7 +1,7 @@ from tests.utils import find_recursive, should_skip -def test_find_recursive(): +def test_find_recursive() -> None: ret_value = [ { "AccessorID": "accessorid", @@ -51,7 +51,7 @@ def test_find_recursive(): assert not find_recursive(ret_value, unwanted) -def test_should_skip(): +def test_should_skip() -> None: test_cases = [ ("1.0.0", "<=", "1.0.0", False), ("1.0.1", "<=", "1.0.0", True), diff --git a/tests/utils.py b/tests/utils.py index 62a547f..59f2463 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -45,7 +45,7 @@ def matches_subdict(haystack: dict, needle: dict) -> bool: return all(any(matches_subdict(item, single_wanted) for item in list_or_single_dict) for single_wanted in wanted) -def should_skip(version_str, comparator, ref_version_str): +def should_skip(version_str: str, comparator: str, ref_version_str: str) -> bool: v = version.parse(version_str) ref_version = version.parse(ref_version_str) if ( diff --git a/tox.ini b/tox.ini index ab696eb..3672542 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,6 @@ [tox] envlist = lint,py38,py39,py10,py11,py12 +toxworkdir = {env:TEMPDIR:/tmp/.tox}-{env:JOB_NAME:build}-{env:BUILD_NUMBER:current}/{env:BUILD_ID:unknown} [testenv] deps =