diff --git a/oembedpy/adapters/sphinx.py b/oembedpy/adapters/sphinx.py index d37d73c..9fe8538 100644 --- a/oembedpy/adapters/sphinx.py +++ b/oembedpy/adapters/sphinx.py @@ -71,6 +71,10 @@ def has_cache(self, key: Tuple[str, Union[int, None], Union[int, None]]) -> bool if key not in self.caches: return False content: Content = self.caches[key] + if hasattr( + content, "_expired" + ): # NOTE: For if pickled object does not have _expired. + return now < content._expired if "cache_age" not in content._extra: return True return now < content._extra["cache_age"] diff --git a/oembedpy/application.py b/oembedpy/application.py index e71b7ea..3044f19 100644 --- a/oembedpy/application.py +++ b/oembedpy/application.py @@ -4,10 +4,9 @@ import logging import pickle import time -from typing import Dict, Optional +from typing import Dict, Optional, Union import httpx - from platformdirs import PlatformDirs from oembedpy import consumer, discovery @@ -21,7 +20,7 @@ class Oembed: """Application of oEmbed.""" _registry: ProviderRegistry - _cache: Dict[consumer.RequestParameters, CachedContent] + _cache: Dict[consumer.RequestParameters, Union[Content, CachedContent]] _fallback_type: bool def __init__(self, fallback_type: bool = False): # noqa: D107 @@ -52,11 +51,17 @@ def fetch( params.max_height = max_height # now = time.mktime(time.localtime()) - if params in self._cache and now <= self._cache[params].expired: - return self._cache[params].content + if params in self._cache: + # For comptibility CachedContent + val = self._cache[params] + if isinstance(val, CachedContent): + if now <= val.expired: + return val.content + elif now <= val._expired: + return val content = consumer.fetch_content(api_url, params, self._fallback_type) if content.cache_age: - self._cache[params] = CachedContent(now + int(content.cache_age), content) + self._cache[params] = content return content diff --git a/oembedpy/consumer.py b/oembedpy/consumer.py index 970610e..7321bd2 100644 --- a/oembedpy/consumer.py +++ b/oembedpy/consumer.py @@ -1,7 +1,9 @@ """For consumer request.""" import logging +import time from dataclasses import dataclass +from datetime import datetime from typing import Dict, Optional import httpx @@ -48,6 +50,7 @@ def fetch_content( * OK: ``text/xml`` * NG: ``text/plain`` (even if body is JSON string) """ + now = time.mktime(time.localtime()) resp = httpx.get(url, params=params.to_dict(), follow_redirects=True) resp.raise_for_status() content_type = resp.headers.get("content-type", "").split(";")[0] # Exclude chaset @@ -75,4 +78,6 @@ def fetch_content( if not fallback_type: raise err content = types.HtmlOnly.from_dict(data) + if content.cache_age: + content._expired = now + int(content.cache_age) return content diff --git a/oembedpy/types.py b/oembedpy/types.py index ed9e91f..a344ab7 100644 --- a/oembedpy/types.py +++ b/oembedpy/types.py @@ -60,6 +60,12 @@ class _Optionals: thumbnail_height: Optional[int] = None +class _Internals: + """Fields of internal parameters for any types.""" + + _expired: int = 0 + + @dataclass class _Photo: """Required fields for ``photo`` types.""" @@ -95,27 +101,27 @@ class _HtmlOnly(_BaseType): @dataclass -class Photo(_Optionals, _Photo, _Required): +class Photo(_Internals, _Optionals, _Photo, _Required): """oEmbed content for photo object.""" @dataclass -class Video(_Optionals, _Video, _Required): +class Video(_Internals, _Optionals, _Video, _Required): """oEmbed content for vhoto object.""" @dataclass -class Link(_Optionals, _Required): +class Link(_Internals, _Optionals, _Required): """oEmbed content for generic object.""" @dataclass -class Rich(_Optionals, _Rich, _Required): +class Rich(_Internals, _Optionals, _Rich, _Required): """oEmbed content for rich HTML object.""" @dataclass -class HtmlOnly(_Optionals, _HtmlOnly): +class HtmlOnly(_Internals, _Optionals, _HtmlOnly): """Fallback type for invalid scheme.""" @@ -124,5 +130,14 @@ class HtmlOnly(_Optionals, _HtmlOnly): class CachedContent(NamedTuple): + """Content object with expired timestamp for cache. + + .. deprecated:: 0.9.0 + + This is internal class for cache, so it keeps to avoid breaking cache data. + I will remove for v1. + Use :class:`Content` instead if you use in other projects. + """ + expired: float content: Content diff --git a/tests/test_consumer.py b/tests/test_consumer.py index 03631a8..54bda66 100644 --- a/tests/test_consumer.py +++ b/tests/test_consumer.py @@ -50,6 +50,7 @@ def test_json_content(self, httpx_mock): ) assert isinstance(content, types.Video) assert content.author_name == "attakei" + assert content._expired == 0 def test_xml_content(self, httpx_mock): httpx_mock.add_response( @@ -127,3 +128,17 @@ def test_invalid_xml(self, httpx_mock): format="xml", url="https://www.youtube.com/watch&v=Oyh8nuaLASA" ), ) + + def test_json_has_cache_age(self, httpx_mock): + resp_data = deepcopy(self.content_json) + resp_data["cache_age"] = "3600" + httpx_mock.add_response(json=resp_data) + content = consumer.fetch_content( + "https://www.youtube.com/oembed", + consumer.RequestParameters( + format="json", url="https://www.youtube.com/watch&v=Oyh8nuaLASA" + ), + ) + assert isinstance(content, types.Video) + assert content.author_name == "attakei" + assert content._expired != 0