Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

MockedClock -> freezegun #308

Open
wants to merge 18 commits into
base: master
Choose a base branch
from
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

- Fix compatibility with httpx. (#291)
- Use `SyncByteStream` instead of `ByteStream`. (#298)
- Remove `clock` argument to `Controller`. (#308)

## 0.1.1 (2nd Nov, 2024)

Expand Down
30 changes: 12 additions & 18 deletions hishel/_controller.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,18 @@
import logging
import typing as tp
from datetime import datetime, timezone

from httpcore import Request, Response

from hishel._headers import Vary, parse_cache_control

from ._utils import (
BaseClock,
Clock,
extract_header_values,
extract_header_values_decoded,
generate_key,
get_safe_url,
header_presents,
parse_date,
parse_date_to_epoch,
)

logger = logging.getLogger("hishel.controller")
Expand Down Expand Up @@ -59,20 +58,20 @@ def get_freshness_lifetime(response: Response) -> tp.Optional[int]:

if header_presents(response.headers, b"expires"):
expires = extract_header_values_decoded(response.headers, b"expires", single=True)[0]
expires_timestamp = parse_date(expires)
expires_timestamp = parse_date_to_epoch(expires)
date = extract_header_values_decoded(response.headers, b"date", single=True)[0]
date_timestamp = parse_date(date)
date_timestamp = parse_date_to_epoch(date)

return expires_timestamp - date_timestamp
return None


def get_heuristic_freshness(response: Response, clock: "BaseClock") -> int:
def get_heuristic_freshness(response: Response) -> int:
last_modified = extract_header_values_decoded(response.headers, b"last-modified", single=True)

if last_modified:
last_modified_timestamp = parse_date(last_modified[0])
now = clock.now()
last_modified_timestamp = parse_date_to_epoch(last_modified[0])
now = int(datetime.now(timezone.utc).timestamp())

ONE_WEEK = 604_800

Expand All @@ -82,18 +81,15 @@ def get_heuristic_freshness(response: Response, clock: "BaseClock") -> int:
return ONE_DAY


def get_age(response: Response, clock: "BaseClock") -> int:
def get_age(response: Response) -> int:
if not header_presents(response.headers, b"date"):
# If the response does not have a date header, then it is impossible to calculate the age.
# Instead of raising an exception, we return infinity to be sure that the response is not considered fresh.
return float("inf") # type: ignore

date = parse_date(extract_header_values_decoded(response.headers, b"date")[0])
date = parse_date_to_epoch(extract_header_values_decoded(response.headers, b"date")[0])

now = clock.now()

apparent_age = max(0, now - date)
return int(apparent_age)
return max(0, int(datetime.now(timezone.utc).timestamp()) - date)


def allowed_stale(response: Response) -> bool:
Expand All @@ -115,7 +111,6 @@ def __init__(
cacheable_status_codes: tp.Optional[tp.List[int]] = None,
cache_private: bool = True,
allow_heuristics: bool = False,
clock: tp.Optional[BaseClock] = None,
allow_stale: bool = False,
always_revalidate: bool = False,
force_cache: bool = False,
Expand All @@ -136,7 +131,6 @@ def __init__(

self._cacheable_status_codes = cacheable_status_codes if cacheable_status_codes else [200, 301, 308]
self._cache_private = cache_private
self._clock = clock if clock else Clock()
self._allow_heuristics = allow_heuristics
self._allow_stale = allow_stale
self._always_revalidate = always_revalidate
Expand Down Expand Up @@ -451,7 +445,7 @@ def construct_response_from_cache(
)
)
if self._allow_heuristics and response.status in HEURISTICALLY_CACHEABLE_STATUS_CODES:
freshness_lifetime = get_heuristic_freshness(response=response, clock=self._clock)
freshness_lifetime = get_heuristic_freshness(response=response)
logger.debug(
(
f"Successfully calculated the freshness lifetime of the resource located at "
Expand All @@ -470,7 +464,7 @@ def construct_response_from_cache(
self._make_request_conditional(request=request, response=response)
return request

age = get_age(response, self._clock)
age = get_age(response)
is_fresh = freshness_lifetime > age

# The min-fresh request directive indicates that the client
Expand Down
15 changes: 2 additions & 13 deletions hishel/_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,6 @@
HEADERS_ENCODING = "iso-8859-1"


class BaseClock:
def now(self) -> int:
raise NotImplementedError()


class Clock(BaseClock):
def now(self) -> int:
return int(time.time())


def normalized_url(url: tp.Union[httpcore.URL, str, bytes]) -> str:
if isinstance(url, str): # pragma: no cover
return url
Expand Down Expand Up @@ -82,10 +72,9 @@ def header_presents(headers: tp.List[tp.Tuple[bytes, bytes]], header_key: bytes)
return bool(extract_header_values(headers, header_key, single=True))


def parse_date(date: str) -> int:
def parse_date_to_epoch(date: str) -> int:
expires = parsedate_tz(date)
timestamp = calendar.timegm(expires[:6]) # type: ignore
return timestamp
return calendar.timegm(expires[:6]) # type: ignore


async def asleep(seconds: tp.Union[int, float]) -> None:
Expand Down
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ anyio==4.7.0
trio==0.28.0
coverage==7.6.10
types-PyYAML==6.0.12.20240311
freezegun==1.5.1

# build
hatch==1.9.3
Expand Down
Loading
Loading