From 10de93e5730baf679839f4fee7cbfc8e929df982 Mon Sep 17 00:00:00 2001 From: Kazuya Takei Date: Sun, 21 Dec 2025 14:55:23 +0900 Subject: [PATCH 1/4] feat: Content always has expired property for caching - Default value is 0 --- oembedpy/consumer.py | 5 +++++ oembedpy/types.py | 16 +++++++++++----- tests/test_consumer.py | 15 +++++++++++++++ 3 files changed, 31 insertions(+), 5 deletions(-) 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..9e09484 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.""" 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 From a052d23a96706d5d7cc515829aca109b973dca70 Mon Sep 17 00:00:00 2001 From: Kazuya Takei Date: Sun, 21 Dec 2025 15:00:38 +0900 Subject: [PATCH 2/4] refactor: Format by ruff --- oembedpy/application.py | 1 - 1 file changed, 1 deletion(-) diff --git a/oembedpy/application.py b/oembedpy/application.py index e71b7ea..3478ba2 100644 --- a/oembedpy/application.py +++ b/oembedpy/application.py @@ -7,7 +7,6 @@ from typing import Dict, Optional import httpx - from platformdirs import PlatformDirs from oembedpy import consumer, discovery From 744512d6ed7bda913e3c5b038c421ef3750c3538 Mon Sep 17 00:00:00 2001 From: Kazuya Takei Date: Sun, 21 Dec 2025 15:18:07 +0900 Subject: [PATCH 3/4] fix: Application caching use Content class directly --- oembedpy/application.py | 16 +++++++++++----- oembedpy/types.py | 9 +++++++++ 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/oembedpy/application.py b/oembedpy/application.py index 3478ba2..3044f19 100644 --- a/oembedpy/application.py +++ b/oembedpy/application.py @@ -4,7 +4,7 @@ import logging import pickle import time -from typing import Dict, Optional +from typing import Dict, Optional, Union import httpx from platformdirs import PlatformDirs @@ -20,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 @@ -51,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/types.py b/oembedpy/types.py index 9e09484..a344ab7 100644 --- a/oembedpy/types.py +++ b/oembedpy/types.py @@ -130,5 +130,14 @@ class HtmlOnly(_Internals, _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 From 7a366edd774fbf52aac337c66c34f33f7a360446 Mon Sep 17 00:00:00 2001 From: Kazuya Takei Date: Sun, 21 Dec 2025 16:11:44 +0900 Subject: [PATCH 4/4] refactor: See _expired in Sphinx extension --- oembedpy/adapters/sphinx.py | 4 ++++ 1 file changed, 4 insertions(+) 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"]