diff --git a/bot/kodiak/cli.py b/bot/kodiak/cli.py index 3ca0fdb24..d8f49135c 100644 --- a/bot/kodiak/cli.py +++ b/bot/kodiak/cli.py @@ -4,10 +4,10 @@ import click import requests -from httpx import AsyncClient from kodiak import app_config as conf from kodiak.config import V1 +from kodiak.http import HttpClient from kodiak.queries import generate_jwt, get_token_for_install @@ -66,7 +66,7 @@ def token_for_install(install_id: str) -> None: """ async def get_token() -> str: - async with AsyncClient() as http: + async with HttpClient() as http: return await get_token_for_install(session=http, installation_id=install_id) token = asyncio.run(get_token()) diff --git a/bot/kodiak/http.py b/bot/kodiak/http.py new file mode 100644 index 000000000..cdfa514b5 --- /dev/null +++ b/bot/kodiak/http.py @@ -0,0 +1,36 @@ +from __future__ import annotations + +import ssl + +from httpx import ( # noqa: I251 + AsyncClient, + HTTPError, + HTTPStatusError, + Request, + Response, +) +from httpx._config import DEFAULT_TIMEOUT_CONFIG # noqa: I251 +from httpx._types import TimeoutTypes # noqa: I251 + +__all__ = ["Response", "Request", "HTTPError", "HttpClient", "HTTPStatusError"] + +# NOTE: this has a cost to create so we may want to set this lazily on the first HttpClient creation +context = ssl.create_default_context() + + +class HttpClient(AsyncClient): + """ + HTTP Client with the SSL config cached at the module level to avoid perf issues. + see: https://github.com/encode/httpx/issues/838 + """ + + def __init__( + self, + *, + timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG, + ): + + super().__init__( + verify=context, + timeout=timeout, + ) diff --git a/bot/kodiak/pull_request.py b/bot/kodiak/pull_request.py index 7945c14e9..120adb9d9 100644 --- a/bot/kodiak/pull_request.py +++ b/bot/kodiak/pull_request.py @@ -5,7 +5,6 @@ from typing import Awaitable, Callable, List, Optional, Type import structlog -from httpx import HTTPStatusError as HTTPError from typing_extensions import Protocol import kodiak.app_config as conf @@ -16,6 +15,7 @@ RetryForSkippableChecks, ) from kodiak.evaluation import mergeable +from kodiak.http import HTTPStatusError as HTTPError from kodiak.queries import Client, EventInfoResponse logger = structlog.get_logger() diff --git a/bot/kodiak/queries/__init__.py b/bot/kodiak/queries/__init__.py index 139d82d97..a46153a78 100644 --- a/bot/kodiak/queries/__init__.py +++ b/bot/kodiak/queries/__init__.py @@ -6,7 +6,6 @@ from enum import Enum from typing import Any, Dict, List, Mapping, MutableMapping, Optional, Union, cast -import httpx as http import jwt import pydantic import structlog @@ -16,7 +15,9 @@ from typing_extensions import Literal, Protocol import kodiak.app_config as conf +from kodiak import http from kodiak.config import V1, MergeMethod +from kodiak.http import HttpClient from kodiak.queries.commits import Commit, CommitConnection, GitActor from kodiak.queries.commits import User as PullRequestCommitUser from kodiak.queries.commits import get_commits @@ -825,7 +826,7 @@ def __init__(self, *, owner: str, repo: str, installation_id: str): self.installation_id = installation_id # NOTE: We must call `await session.close()` when we are finished with our session. # We implement an async context manager this handle this. - self.session = http.AsyncClient( + self.session = HttpClient( # infinite timeout to match behavior of old, requests_async http # client. As a backup we have an asyncio timeout of 30 seconds. timeout=None diff --git a/bot/kodiak/refresh_pull_requests.py b/bot/kodiak/refresh_pull_requests.py index 5aa085b5c..69653aaa3 100644 --- a/bot/kodiak/refresh_pull_requests.py +++ b/bot/kodiak/refresh_pull_requests.py @@ -21,12 +21,12 @@ import structlog from asyncio_redis.connection import Connection as RedisConnection from asyncio_redis.replies import BlockingPopReply -from httpx import AsyncClient from pydantic import BaseModel from sentry_sdk.integrations.logging import LoggingIntegration from kodiak import app_config as conf from kodiak import redis_client +from kodiak.http import HttpClient from kodiak.logging import SentryProcessor, add_request_info_processor from kodiak.queries import generate_jwt, get_token_for_install from kodiak.queue import WebhookEvent @@ -106,7 +106,7 @@ """ -async def get_login_for_install(*, http: AsyncClient, installation_id: str) -> str: +async def get_login_for_install(*, http: HttpClient, installation_id: str) -> str: app_token = generate_jwt( private_key=conf.PRIVATE_KEY, app_identifier=conf.GITHUB_APP_ID ) @@ -124,7 +124,7 @@ async def get_login_for_install(*, http: AsyncClient, installation_id: str) -> s async def refresh_pull_requests_for_installation( *, installation_id: str, redis: RedisConnection ) -> None: - async with AsyncClient() as http: + async with HttpClient() as http: login = await get_login_for_install(http=http, installation_id=installation_id) token = await get_token_for_install( session=http, installation_id=installation_id diff --git a/bot/kodiak/test_pull_request.py b/bot/kodiak/test_pull_request.py index f26a947d8..a1d80bf73 100644 --- a/bot/kodiak/test_pull_request.py +++ b/bot/kodiak/test_pull_request.py @@ -2,13 +2,13 @@ from typing import Any, Awaitable, Callable, Dict, List, Mapping, Optional, Type, cast -import httpx as requests import pytest -from httpx import Request from typing_extensions import Protocol +import kodiak.http as requests from kodiak.config import V1, Merge, MergeMethod from kodiak.errors import ApiCallException +from kodiak.http import Request from kodiak.pull_request import PRV2, EventInfoResponse, QueueForMergeCallback from kodiak.queries import ( BranchProtectionRule, diff --git a/bot/kodiak/test_queries.py b/bot/kodiak/test_queries.py index 910aef679..4b152fb07 100644 --- a/bot/kodiak/test_queries.py +++ b/bot/kodiak/test_queries.py @@ -8,11 +8,11 @@ import asyncio_redis import pytest -from httpx import Request, Response from pytest_mock import MockFixture from kodiak import app_config as conf from kodiak.config import V1, Merge, MergeMethod +from kodiak.http import Request, Response from kodiak.queries import ( BranchProtectionRule, CheckConclusionState, diff --git a/bot/poetry.lock b/bot/poetry.lock index 5dcd866b4..c5e3f9d79 100644 --- a/bot/poetry.lock +++ b/bot/poetry.lock @@ -268,6 +268,18 @@ attrs = "*" flake8 = ">=3.2.1" pyflakes = ">=2.1.1" +[[package]] +name = "flake8-tidy-imports" +version = "4.8.0" +description = "A flake8 plugin that helps you write tidier imports." +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +flake8 = ">=3.8.0" +importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} + [[package]] name = "greenlet" version = "1.1.1" @@ -1082,7 +1094,7 @@ python-versions = "*" [metadata] lock-version = "1.1" python-versions = "^3.7" -content-hash = "f4e02b8ece566d62b4c991cf397ad09909bdbbeb97079bc19327b711b9bb4dc7" +content-hash = "2fdb0ff05e66863c34f2891656208a1cdc7ee5dc24511aea5b3ff9f7b541d1d0" [metadata.files] anyio = [ @@ -1276,6 +1288,7 @@ flake8-pyi = [ {file = "flake8-pyi-20.10.0.tar.gz", hash = "sha256:cee3b20a5123152c697870e7e800b60e3c95eb89e272a2b63d8cf55cafb0472c"}, {file = "flake8_pyi-20.10.0-py2.py3-none-any.whl", hash = "sha256:ff5dfc40bffa878f6ce95bcfd9a6ad14c44b85cbe99c4864e729301bf54267f0"}, ] +flake8-tidy-imports = [] greenlet = [ {file = "greenlet-1.1.1-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:476ba9435afaead4382fbab8f1882f75e3fb2285c35c9285abb3dd30237f9142"}, {file = "greenlet-1.1.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:44556302c0ab376e37939fd0058e1f0db2e769580d340fb03b01678d1ff25f68"}, diff --git a/bot/pyproject.toml b/bot/pyproject.toml index 12c0310ab..4ebb164be 100644 --- a/bot/pyproject.toml +++ b/bot/pyproject.toml @@ -47,6 +47,7 @@ pytest-cov = "^2.10" flake8-pyi = "^20.10" types-requests = "^2.28.0" types-toml = "^0.10.7" +flake8-tidy-imports = "^4.8.0" [tool.poetry.plugins."pytest11"] "pytest_plugin" = "pytest_plugin.plugin" diff --git a/bot/tox.ini b/bot/tox.ini index 76a155427..fcfd4e561 100644 --- a/bot/tox.ini +++ b/bot/tox.ini @@ -9,6 +9,9 @@ filterwarnings = ignore::pytest.PytestDeprecationWarning ignore::DeprecationWarning:asyncio_redis [flake8] +banned-modules = + httpx.* = Use kodiak.http +ban-relative-imports = true ignore = ; formatting handled by black ; https://pycodestyle.readthedocs.io/en/latest/intro.html#error-codes