From 3f9adcc7cbab5f9f2f84d2c3502deb088030dcf6 Mon Sep 17 00:00:00 2001 From: Ju4tCode <42488585+yanyongyu@users.noreply.github.com> Date: Sun, 24 May 2026 08:47:22 +0000 Subject: [PATCH 1/3] :sparkles: support hishel v1 --- githubkit/auth/app.py | 4 ++-- githubkit/cache/base.py | 25 ++++++--------------- githubkit/cache/mem_cache.py | 23 +++++++++---------- githubkit/cache/redis.py | 34 +++++++++------------------- githubkit/client.py | 43 ++++++++++++++++++++++++++++++++++++ githubkit/core.py | 17 +++++++------- githubkit/typing.py | 19 +--------------- githubkit/utils.py | 8 ------- pyproject.toml | 4 ++-- uv.lock | 20 ++++++++++------- 10 files changed, 97 insertions(+), 100 deletions(-) create mode 100644 githubkit/client.py diff --git a/githubkit/auth/app.py b/githubkit/auth/app.py index 33480d248..acd70e771 100644 --- a/githubkit/auth/app.py +++ b/githubkit/auth/app.py @@ -97,7 +97,7 @@ def get_jwt(self) -> str: return token async def aget_jwt(self) -> str: - cache = self.github.config.cache_strategy.get_async_cache_storage() + cache = await self.github.config.cache_strategy.get_async_cache_storage() cache_key = self._get_jwt_cache_key() if not (token := await cache.aget(cache_key)): token = self._create_jwt() @@ -246,7 +246,7 @@ async def async_auth_flow( yield request return - cache = self.github.config.cache_strategy.get_async_cache_storage() + cache = await self.github.config.cache_strategy.get_async_cache_storage() key = self._get_installation_cache_key() if not (token := await cache.aget(key)): token_request = self._build_installation_auth_request() diff --git a/githubkit/cache/base.py b/githubkit/cache/base.py index 66e2ad7d3..7eb38ece7 100644 --- a/githubkit/cache/base.py +++ b/githubkit/cache/base.py @@ -1,9 +1,7 @@ import abc from datetime import timedelta -from hishel import AsyncBaseStorage, BaseStorage, Controller - -from githubkit.typing import HishelControllerOptions +from hishel import AsyncBaseStorage, CachePolicy, SpecificationPolicy, SyncBaseStorage class BaseCache(abc.ABC): @@ -36,32 +34,23 @@ def get_cache_storage(self) -> BaseCache: raise NotImplementedError @abc.abstractmethod - def get_async_cache_storage(self) -> AsyncBaseCache: + async def get_async_cache_storage(self) -> AsyncBaseCache: """Get the cache storage instance used in async context raise CacheUnsupportedError if the strategy does not support async usage """ raise NotImplementedError - def get_hishel_controller_options(self) -> HishelControllerOptions: - """Get the hishel controller options""" - # set always revalidate by default - # See: https://hishel.com/examples/github/ - return HishelControllerOptions(always_revalidate=True) - - def get_hishel_controller(self) -> Controller: - """Get the hishel controller instance - - Get the controller options from `get_hishel_controller_options` method - """ - return Controller(**self.get_hishel_controller_options()) + def get_hishel_policy(self) -> CachePolicy: + """Get the hishel cache policy instance""" + return SpecificationPolicy() @abc.abstractmethod - def get_hishel_storage(self) -> BaseStorage: + def get_hishel_storage(self) -> SyncBaseStorage: """Get the hishel storage instance used in sync context""" raise NotImplementedError @abc.abstractmethod - def get_async_hishel_storage(self) -> AsyncBaseStorage: + async def get_async_hishel_storage(self) -> AsyncBaseStorage: """Get the hishel storage instance used in async context""" raise NotImplementedError diff --git a/githubkit/cache/mem_cache.py b/githubkit/cache/mem_cache.py index ee8f57fba..74da3a15a 100644 --- a/githubkit/cache/mem_cache.py +++ b/githubkit/cache/mem_cache.py @@ -1,8 +1,10 @@ from dataclasses import dataclass from datetime import datetime, timedelta, timezone +import sqlite3 from typing_extensions import override -from hishel import AsyncInMemoryStorage, InMemoryStorage +import anysqlite +from hishel import AsyncSqliteStorage, SyncSqliteStorage from .base import AsyncBaseCache, BaseCache, BaseCacheStrategy @@ -47,8 +49,6 @@ async def aset(self, key: str, value: str, ex: timedelta) -> None: class MemCacheStrategy(BaseCacheStrategy): def __init__(self) -> None: self._cache: MemCache | None = None - self._hishel_storage: InMemoryStorage | None = None - self._hishel_async_storage: AsyncInMemoryStorage | None = None @override def get_cache_storage(self) -> MemCache: @@ -57,17 +57,16 @@ def get_cache_storage(self) -> MemCache: return self._cache @override - def get_async_cache_storage(self) -> MemCache: + async def get_async_cache_storage(self) -> MemCache: return self.get_cache_storage() @override - def get_hishel_storage(self) -> InMemoryStorage: - if self._hishel_storage is None: - self._hishel_storage = InMemoryStorage() - return self._hishel_storage + def get_hishel_storage(self) -> SyncSqliteStorage: + # NOTE: although it's possible to use in-memory sqlite for hishel, + # the connection can not be shared between clients. + # the storage is closed when the client transport is closed. + return SyncSqliteStorage(connection=sqlite3.connect(":memory:")) @override - def get_async_hishel_storage(self) -> AsyncInMemoryStorage: - if self._hishel_async_storage is None: - self._hishel_async_storage = AsyncInMemoryStorage() - return self._hishel_async_storage + async def get_async_hishel_storage(self) -> AsyncSqliteStorage: + return AsyncSqliteStorage(connection=await anysqlite.connect(":memory:")) diff --git a/githubkit/cache/redis.py b/githubkit/cache/redis.py index e9380b8ac..29fda2c5c 100644 --- a/githubkit/cache/redis.py +++ b/githubkit/cache/redis.py @@ -1,13 +1,10 @@ from datetime import timedelta -from functools import partial from typing import TYPE_CHECKING, Any, NoReturn from typing_extensions import override from hishel import AsyncBaseStorage, AsyncRedisStorage, RedisStorage from githubkit.exception import CacheUnsupportedError -from githubkit.typing import HishelControllerOptions -from githubkit.utils import hishel_key_generator_with_prefix from .base import AsyncBaseCache, BaseCache, BaseCacheStrategy @@ -34,7 +31,7 @@ def __init__(self, client: "Redis", prefix: str | None = None) -> None: def _get_key(self, key: str) -> str: if self.prefix is not None: - return f"{self.prefix}{key}" + return f"{self.prefix}:{key}" return key @override @@ -54,7 +51,7 @@ def __init__(self, client: "AsyncRedis", prefix: str | None = None) -> None: def _get_key(self, key: str) -> str: if self.prefix is not None: - return f"{self.prefix}{key}" + return f"{self.prefix}:{key}" return key @override @@ -70,35 +67,24 @@ async def aset(self, key: str, value: str, ex: timedelta) -> None: class RedisCacheStrategy(BaseCacheStrategy): def __init__(self, client: "Redis", prefix: str | None = None) -> None: self.client = client - self.prefix = prefix + self.prefix = prefix.removesuffix(":") if prefix else None @override def get_cache_storage(self) -> RedisCache: return RedisCache(self.client, self.prefix) @override - def get_async_cache_storage(self) -> NoReturn: + async def get_async_cache_storage(self) -> NoReturn: raise CacheUnsupportedError( "Sync redis cache strategy does not support async usage" ) - @override - def get_hishel_controller_options(self) -> HishelControllerOptions: - options = super().get_hishel_controller_options() - - if self.prefix is not None: - options["key_generator"] = partial( - hishel_key_generator_with_prefix, prefix=self.prefix - ) - - return options - @override def get_hishel_storage(self) -> RedisStorage: - return RedisStorage(client=self.client) + return RedisStorage(client=self.client, key_prefix=self.prefix or "hishel") @override - def get_async_hishel_storage(self) -> NoReturn: + async def get_async_hishel_storage(self) -> NoReturn: raise CacheUnsupportedError( "Sync redis cache strategy does not support async usage" ) @@ -107,7 +93,7 @@ def get_async_hishel_storage(self) -> NoReturn: class AsyncRedisCacheStrategy(BaseCacheStrategy): def __init__(self, client: "AsyncRedis", prefix: str | None = None) -> None: self.client = client - self.prefix = prefix + self.prefix = prefix.removesuffix(":") if prefix else None @override def get_cache_storage(self) -> NoReturn: @@ -116,7 +102,7 @@ def get_cache_storage(self) -> NoReturn: ) @override - def get_async_cache_storage(self) -> AsyncRedisCache: + async def get_async_cache_storage(self) -> AsyncRedisCache: return AsyncRedisCache(self.client, self.prefix) @override @@ -126,5 +112,5 @@ def get_hishel_storage(self) -> NoReturn: ) @override - def get_async_hishel_storage(self) -> AsyncBaseStorage: - return AsyncRedisStorage(client=self.client) + async def get_async_hishel_storage(self) -> AsyncBaseStorage: + return AsyncRedisStorage(client=self.client, key_prefix=self.prefix or "hishel") diff --git a/githubkit/client.py b/githubkit/client.py new file mode 100644 index 000000000..7c3350069 --- /dev/null +++ b/githubkit/client.py @@ -0,0 +1,43 @@ +from typing import Any + +from hishel import AsyncBaseStorage, CachePolicy, SyncBaseStorage +from hishel.httpx import AsyncCacheTransport, SyncCacheTransport +import httpx + + +class SyncCacheClient(httpx.Client): + def __init__(self, *args: Any, **kwargs: Any): + self.storage: SyncBaseStorage | None = kwargs.pop("storage", None) + self.policy: CachePolicy | None = kwargs.pop("policy", None) + super().__init__(*args, **kwargs) + + def _init_transport(self, *args, **kwargs) -> httpx.BaseTransport: + _transport = super()._init_transport(*args, **kwargs) + return SyncCacheTransport( + next_transport=_transport, storage=self.storage, policy=self.policy + ) + + def _init_proxy_transport(self, *args, **kwargs) -> httpx.BaseTransport: + _transport = super()._init_proxy_transport(*args, **kwargs) + return SyncCacheTransport( + next_transport=_transport, storage=self.storage, policy=self.policy + ) + + +class AsyncCacheClient(httpx.AsyncClient): + def __init__(self, *args: Any, **kwargs: Any): + self.storage: AsyncBaseStorage | None = kwargs.pop("storage", None) + self.policy: CachePolicy | None = kwargs.pop("policy", None) + super().__init__(*args, **kwargs) + + def _init_transport(self, *args, **kwargs) -> httpx.AsyncBaseTransport: + _transport = super()._init_transport(*args, **kwargs) + return AsyncCacheTransport( + next_transport=_transport, storage=self.storage, policy=self.policy + ) + + def _init_proxy_transport(self, *args, **kwargs) -> httpx.AsyncBaseTransport: + _transport = super()._init_proxy_transport(*args, **kwargs) + return AsyncCacheTransport( + next_transport=_transport, storage=self.storage, policy=self.policy + ) diff --git a/githubkit/core.py b/githubkit/core.py index 79978e61c..6639bc0e2 100644 --- a/githubkit/core.py +++ b/githubkit/core.py @@ -7,11 +7,11 @@ from typing import TYPE_CHECKING, Any, Generic, TypeVar, cast, overload import anyio -import hishel import httpx from .auth import BaseAuthStrategy, TokenAuthStrategy, UnauthAuthStrategy from .cache import BaseCacheStrategy +from .client import AsyncCacheClient, SyncCacheClient from .compat import to_jsonable_python from .config import Config, get_config from .exception import ( @@ -250,6 +250,7 @@ def _get_client_defaults(self) -> dict[str, Any]: "headers": { "User-Agent": self.config.user_agent, "Accept": self.config.accept, + "Cache-Control": "no-cache", }, "timeout": self.config.timeout, "follow_redirects": self.config.follow_redirects, @@ -260,12 +261,12 @@ def _get_client_defaults(self) -> dict[str, Any]: def _create_sync_client(self) -> httpx.Client: if self.config.http_cache: - return hishel.CacheClient( + return SyncCacheClient( **self._get_client_defaults(), transport=self.config.transport, event_hooks=self.config.event_hooks, storage=self.config.cache_strategy.get_hishel_storage(), - controller=self.config.cache_strategy.get_hishel_controller(), + policy=self.config.cache_strategy.get_hishel_controller(), ) return httpx.Client( @@ -286,14 +287,14 @@ def get_sync_client(self) -> Generator[httpx.Client, None, None]: finally: client.close() - def _create_async_client(self) -> httpx.AsyncClient: + async def _create_async_client(self) -> httpx.AsyncClient: if self.config.http_cache: - return hishel.AsyncCacheClient( + return AsyncCacheClient( **self._get_client_defaults(), transport=self.config.async_transport, event_hooks=self.config.async_event_hooks, - storage=self.config.cache_strategy.get_async_hishel_storage(), - controller=self.config.cache_strategy.get_hishel_controller(), + storage=await self.config.cache_strategy.get_async_hishel_storage(), + policy=self.config.cache_strategy.get_hishel_controller(), ) return httpx.AsyncClient( @@ -308,7 +309,7 @@ async def get_async_client(self) -> AsyncGenerator[httpx.AsyncClient, None]: if client := self.__async_client.get(): yield client else: - client = self._create_async_client() + client = await self._create_async_client() try: yield client finally: diff --git a/githubkit/typing.py b/githubkit/typing.py index 0cb456747..d1ea8e094 100644 --- a/githubkit/typing.py +++ b/githubkit/typing.py @@ -7,13 +7,10 @@ Any, Literal, NamedTuple, - Optional, TypeAlias, - TypedDict, TypeVar, ) -import httpcore import httpx from pydantic import Field @@ -22,7 +19,7 @@ from .utils import Unset if TYPE_CHECKING: - from hishel._utils import BaseClock + pass T = TypeVar("T") H = TypeVar("H", bound=Hashable) @@ -94,17 +91,3 @@ class RetryOption(NamedTuple): EventHookTypes: TypeAlias = Mapping[str, list[Callable[..., Any]]] - - -class HishelControllerOptions(TypedDict, total=False): - """Options for the hishel controller.""" - - cacheable_methods: list[str] | None - cacheable_status_codes: list[int] | None - cache_private: bool - allow_heuristics: bool - clock: Optional["BaseClock"] - allow_stale: bool - always_revalidate: bool - force_cache: bool - key_generator: Callable[[httpcore.Request, bytes | None], str] | None diff --git a/githubkit/utils.py b/githubkit/utils.py index 27b47058e..864dce6c5 100644 --- a/githubkit/utils.py +++ b/githubkit/utils.py @@ -3,8 +3,6 @@ import inspect from typing import Any, Generic, Literal, TypeVar, final, overload -from hishel._utils import generate_key -import httpcore from pydantic import BaseModel from .compat import PYDANTIC_V2, custom_validation, type_validate_python @@ -130,9 +128,3 @@ def __get_pydantic_core_schema__( ), ), ) - - -def hishel_key_generator_with_prefix( - request: httpcore.Request, body: bytes | None, prefix: str = "" -) -> str: - return prefix + generate_key(request, b"" if body is None else body) diff --git a/pyproject.toml b/pyproject.toml index 84c947348..ed3225c9d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,7 +10,7 @@ requires-python = ">=3.10, <4.0" dependencies = [ "anyio >=3.6.1, <5.0.0", "httpx >=0.23.0, <1.0.0", - "hishel >=0.0.21, <=0.2.0", + "hishel[async,httpx] >=1.2.0, <=2.0.0", "typing-extensions >=4.11.0, <5.0.0", "pydantic >=1.9.1, <3.0.0, !=2.5.0, !=2.5.1", "githubkit-schemas >=26.5.7", @@ -31,7 +31,7 @@ Documentation = "https://github.com/yanyongyu/githubkit" "Bug Tracker" = "https://github.com/yanyongyu/githubkit/issues" [dependency-groups] -thirdparty = ["redis >=5.2.0, <8.0.0"] +thirdparty = ["hishel[redis] >=1.2.0, <=2.0.0"] dev = [ { include-group = "thirdparty" }, { include-group = "codegen" }, diff --git a/uv.lock b/uv.lock index c123d32e7..edcb2cc87 100644 --- a/uv.lock +++ b/uv.lock @@ -569,7 +569,7 @@ source = { editable = "." } dependencies = [ { name = "anyio" }, { name = "githubkit-schemas" }, - { name = "hishel" }, + { name = "hishel", extra = ["httpx"] }, { name = "httpx" }, { name = "pydantic", version = "1.10.26", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'group-9-githubkit-pydantic-v1'" }, { name = "pydantic", version = "2.13.4", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'group-9-githubkit-pydantic-v2' or extra != 'group-9-githubkit-pydantic-v1'" }, @@ -650,7 +650,7 @@ requires-dist = [ { name = "githubkit-schemas", editable = "packages/githubkit-schemas" }, { name = "githubkit-schemas", extras = ["all"], marker = "extra == 'all'", editable = "packages/githubkit-schemas" }, { name = "githubkit-schemas", extras = ["all"], marker = "extra == 'all-schemas'", editable = "packages/githubkit-schemas" }, - { name = "hishel", specifier = ">=0.0.21,<=0.2.0" }, + { name = "hishel", extras = ["httpx"], specifier = ">=1.2.0,<=2.0.0" }, { name = "httpx", specifier = ">=0.23.0,<1.0.0" }, { name = "pydantic", specifier = ">=1.9.1,!=2.5.0,!=2.5.1,<3.0.0" }, { name = "pyjwt", extras = ["crypto"], marker = "extra == 'all'", specifier = ">=2.4.0,<3.0.0" }, @@ -832,18 +832,22 @@ wheels = [ [[package]] name = "hishel" -version = "0.1.5" +version = "1.2.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "anyio" }, - { name = "anysqlite" }, - { name = "httpx" }, { name = "msgpack" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e5/64/a104ccac48f123f853254483617b16e0efc1649bd7e35bcdc5a5a5ef0ae2/hishel-0.1.5.tar.gz", hash = "sha256:9d40c682cd94fd6e1394fb05713ae20a75ed8aeba6f5272380444039ce6257f2", size = 75468, upload-time = "2025-10-18T13:32:41.854Z" } +sdist = { url = "https://files.pythonhosted.org/packages/2d/7a/ef9231da56bc04000f7343831deb1946f5fc270213080fa212f61ddbfe0a/hishel-1.2.1.tar.gz", hash = "sha256:87212cd31a7a6904352ec5bd7119ed3b43d0ab1259d5995751a2a2b173d7c305", size = 814761, upload-time = "2026-04-27T16:16:42.35Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/70/83/4f8b77839e62114bb034375ee8e08cfb6af1164754b925b271d3f1ec06ee/hishel-0.1.5-py3-none-any.whl", hash = "sha256:0bfbe9a2b9342090eba82ba6de88258092e1c4c7b730cd4cb4b570e4b40e44a7", size = 92486, upload-time = "2025-10-18T13:32:40.333Z" }, + { url = "https://files.pythonhosted.org/packages/c3/de/5f1bebcf644f2f350c39d2630f391399d0c39ff39fbfff98113889898827/hishel-1.2.1-py3-none-any.whl", hash = "sha256:5e1c1e38f4c970eeef69bc8528e85fd5c02eab15a22f9845d30fc880da32b881", size = 73223, upload-time = "2026-04-27T16:16:40.585Z" }, +] + +[package.optional-dependencies] +httpx = [ + { name = "anyio" }, + { name = "anysqlite" }, + { name = "httpx" }, ] [[package]] From 7ce03066613402decd3e4da351a926f6859c7e49 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 24 May 2026 08:49:25 +0000 Subject: [PATCH 2/3] :rotating_light: auto fix by pre-commit hooks --- tests/test_versions/test_models.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/tests/test_versions/test_models.py b/tests/test_versions/test_models.py index e5aaf430e..c7a828aad 100644 --- a/tests/test_versions/test_models.py +++ b/tests/test_versions/test_models.py @@ -14,7 +14,6 @@ from githubkit.versions.v2022_11_28.models import ( # noqa: F401 SimpleUser as BackportSimpleUserV2022_11_28, ) - from githubkit.versions.v2026_03_10.models import ( # noqa: F401 SimpleUser as BackportSimpleUserV2026_03_10, ) @@ -54,7 +53,6 @@ from githubkit.versions.v2022_11_28.models import ( # noqa: F401 Repository as BackportRepositoryV2022_11_28, ) - from githubkit.versions.v2026_03_10.models import ( # noqa: F401 Repository as BackportRepositoryV2026_03_10, ) @@ -94,7 +92,6 @@ from githubkit.versions.v2022_11_28.models import ( # noqa: F401 OrganizationFull as BackportOrganizationFullV2022_11_28, ) - from githubkit.versions.v2026_03_10.models import ( # noqa: F401 OrganizationFull as BackportOrganizationFullV2026_03_10, ) @@ -134,7 +131,6 @@ from githubkit.versions.v2022_11_28.models import ( # noqa: F401 CustomProperty as BackportCustomPropertyV2022_11_28, ) - from githubkit.versions.v2026_03_10.models import ( # noqa: F401 CustomProperty as BackportCustomPropertyV2026_03_10, ) From 5723c5aa19c48333c221166e516f3c28870cc133 Mon Sep 17 00:00:00 2001 From: Ju4tCode <42488585+yanyongyu@users.noreply.github.com> Date: Sun, 24 May 2026 16:50:46 +0800 Subject: [PATCH 3/3] :pencil2: fix policy typo --- githubkit/core.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/githubkit/core.py b/githubkit/core.py index 6639bc0e2..319584b01 100644 --- a/githubkit/core.py +++ b/githubkit/core.py @@ -266,7 +266,7 @@ def _create_sync_client(self) -> httpx.Client: transport=self.config.transport, event_hooks=self.config.event_hooks, storage=self.config.cache_strategy.get_hishel_storage(), - policy=self.config.cache_strategy.get_hishel_controller(), + policy=self.config.cache_strategy.get_hishel_policy(), ) return httpx.Client( @@ -294,7 +294,7 @@ async def _create_async_client(self) -> httpx.AsyncClient: transport=self.config.async_transport, event_hooks=self.config.async_event_hooks, storage=await self.config.cache_strategy.get_async_hishel_storage(), - policy=self.config.cache_strategy.get_hishel_controller(), + policy=self.config.cache_strategy.get_hishel_policy(), ) return httpx.AsyncClient(