Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions oembedpy/adapters/sphinx.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand Down
17 changes: 11 additions & 6 deletions oembedpy/application.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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


Expand Down
5 changes: 5 additions & 0 deletions oembedpy/consumer.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
25 changes: 20 additions & 5 deletions oembedpy/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand Down Expand Up @@ -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."""


Expand All @@ -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
15 changes: 15 additions & 0 deletions tests/test_consumer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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