Skip to content

Commit

Permalink
Merge pull request #35 from jmolinski/caching
Browse files Browse the repository at this point in the history
Cache
  • Loading branch information
jmolinski committed Apr 13, 2019
2 parents b0afd5c + fb9426a commit ad424b2
Show file tree
Hide file tree
Showing 20 changed files with 374 additions and 132 deletions.
1 change: 0 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,5 +84,4 @@ Todo 0.2.0:
- sync
- methods on models (episode.rate() etc)
- user profile
- caching (networks, countries etc)
- http component retries
6 changes: 4 additions & 2 deletions tests/inferfaces_tests/test_comments.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@
from tests.test_data.shows import SHOW
from tests.utils import get_last_req, mk_mock_client
from trakt.core.exceptions import ArgumentError
from trakt.core.paths.response_structs import Sharing
from trakt.core.json_parser import parse_tree
from trakt.core.paths.response_structs import Comment, Sharing

PAG_H = {"X-Pagination-Page-Count": 1}

Expand Down Expand Up @@ -40,7 +41,8 @@ def test_post_comment():

def test_get_comment():
client = mk_mock_client({".*comments.*": [COMMENT, 200]})
comment = client.comments.get_comment(id=123)
comment = parse_tree(COMMENT, Comment)
comment = client.comments.get_comment(id=comment)

assert comment.user.name == COMMENT["user"]["name"]

Expand Down
30 changes: 28 additions & 2 deletions tests/test_executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from tests.utils import MockRequests, get_last_req, mk_mock_client
from trakt import Trakt, TraktCredentials
from trakt.core.components import DefaultHttpComponent
from trakt.core.components.cache import FrozenRequest
from trakt.core.exceptions import ArgumentError, ClientError
from trakt.core.executors import Executor, PaginationIterator
from trakt.core.paths import Path
Expand Down Expand Up @@ -75,7 +76,7 @@ def test_pagination():
p_nopag = Path("pag_off", [int])
p_pag = Path("pag_on", [int], pagination=True)

res_nopag = executor.run(path=p_nopag)
res_nopag = executor.run(path=p_nopag).parsed
res_pag = executor.run(path=p_pag, page=2, per_page=3)

assert isinstance(res_nopag, list)
Expand All @@ -99,7 +100,7 @@ def test_prefetch_off():


def test_prefetch_on():
data = list(range(10 ** 4))
data = list(range(10 ** 3))
client = mk_mock_client({"pag_on": [data, 200]}, paginated=["pag_on"])
executor = Executor(client)
p_pag = Path("pag_on", [int], pagination=True)
Expand Down Expand Up @@ -159,3 +160,28 @@ def test_chaining():

assert executor.run(path=p_pag, per_page=2).take_all() == data
assert executor.run(path=p_pag, per_page=2).prefetch_all().take_all() == data


def test_use_cached():
client = mk_mock_client({".*": [[], 200]})

client.networks.get_networks()
client.networks.get_networks()

assert len(client.http._requests.req_stack) == 1


def test_cache_timeout():
client = mk_mock_client({".*": [[], 200]}, cache={"timeout": -1})

client.networks.get_networks()
client.networks.get_networks()

assert len(client.http._requests.req_stack) == 2


def test_cache_get():
client = mk_mock_client({})

with pytest.raises(LookupError):
client.cache.get(FrozenRequest("", {}, {}, None))
21 changes: 13 additions & 8 deletions tests/test_http_component.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from tests.utils import MockRequests, mk_mock_client
from trakt import Trakt
from trakt.core.components import DefaultHttpComponent
from trakt.core.exceptions import BadRequest
from trakt.core.exceptions import BadRequest, RequestRelatedError
from trakt.core.executors import Executor
from trakt.core.paths.path import Path

Expand Down Expand Up @@ -42,14 +42,12 @@ def test_extra_info_return():
requests_dependency=MockRequests({".*": [{"a": "v"}, 200, resp_headers]}),
)

res, code, pagination = http.request(
"abc", return_code=True, return_pagination=True
)
res = http.request("abc")

assert res == {"a": "v"}
assert code == 200
assert pagination["limit"] == 1
assert pagination["page_count"] == 3
assert res.json == {"a": "v"}
assert res.original.status_code == 200
assert res.pagination["limit"] == 1
assert res.pagination["page_count"] == 3


def test_add_quargs():
Expand All @@ -63,3 +61,10 @@ def test_add_quargs():
req = client.http._requests.req_map["a"][0]

assert req["path"].endswith(r"/a?arg=abc")


def test_unexpected_code():
client = mk_mock_client({".*": [[], 455]})

with pytest.raises(RequestRelatedError):
client.networks.get_networks()
3 changes: 2 additions & 1 deletion tests/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,13 +131,14 @@ def wrapper(client):


def mk_mock_client(
endpoints, client_id="", client_secret="", user=False, paginated=None
endpoints, client_id="", client_secret="", user=False, paginated=None, **config
):
return Trakt(
client_id,
client_secret,
http_component=get_mock_http_component(endpoints, paginated=paginated),
user=USER if user is False else None,
**config,
)


Expand Down
12 changes: 11 additions & 1 deletion trakt/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@

from typing import TYPE_CHECKING, Any, Dict, List, Optional, Type, Union

from trakt.core.components import DefaultHttpComponent, DefaultOauthComponent
from trakt.core.components import (
CacheManager,
DefaultHttpComponent,
DefaultOauthComponent,
)
from trakt.core.config import Config, DefaultConfig, TraktCredentials
from trakt.core.executors import Executor
from trakt.core.models import AbstractBaseModel
Expand Down Expand Up @@ -31,12 +35,16 @@
from trakt.core.paths.suite_interface import SuiteInterface


CACHE_LEVELS = CacheManager.CACHE_LEVELS


class TraktApi:
client_id: str
client_secret: str
config: Config
http: DefaultHttpComponent
oauth: DefaultOauthComponent
cache: CacheManager
user: Optional[TraktCredentials]

countries: CountriesI
Expand Down Expand Up @@ -64,6 +72,7 @@ def __init__(
*,
http_component: Optional[Type[DefaultHttpComponent]] = None,
oauth_component: Optional[Type[DefaultOauthComponent]] = None,
cache_manager: Optional[Type[CacheManager]] = None,
interfaces: Dict[str, Type[SuiteInterface]] = None,
user: Optional[TraktCredentials] = None,
auto_refresh_token: bool = False,
Expand All @@ -85,6 +94,7 @@ def __init__(

self.http = (http_component or DefaultHttpComponent)(self)
self.oauth = (oauth_component or DefaultOauthComponent)(self)
self.cache = (cache_manager or CacheManager)(self)

interfaces = interfaces or {}
for i_name, default in DEFAULT_INTERFACES.items():
Expand Down
7 changes: 5 additions & 2 deletions trakt/core/components/__init__.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
from trakt.core.components.http_component import DefaultHttpComponent # NOQA
from trakt.core.components.oauth import DefaultOauthComponent # NOQA
# flake8: noqa: F403

from trakt.core.components.cache import CacheManager, FrozenRequest
from trakt.core.components.http_component import DefaultHttpComponent
from trakt.core.components.oauth import DefaultOauthComponent
85 changes: 85 additions & 0 deletions trakt/core/components/cache.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
from __future__ import annotations

from datetime import datetime, timedelta
from enum import Enum
from typing import TYPE_CHECKING, Any, Dict

if TYPE_CHECKING: # pragma: no cover
from trakt.api import TraktApi


class CacheLevel(Enum):
NO = "no"
BASIC = "basic"
FULL = "full"


class CacheManager:
client: TraktApi
_cache: Dict[FrozenRequest, datetime]

CACHE_LEVELS = (CacheLevel.NO, CacheLevel.BASIC, CacheLevel.FULL)

def __init__(self, client: TraktApi) -> None:
self.client = client
self._cache = {}

def accepted_level(self, level: CacheLevel) -> bool:
max_allowed = CacheLevel(self.client.config["cache"]["cache_level"])

if level == CacheLevel.NO:
return False
elif level == CacheLevel.BASIC:
return max_allowed in {CacheLevel.BASIC, CacheLevel.FULL}
else: # "full"
return max_allowed == CacheLevel.FULL

def get(self, wanted: FrozenRequest) -> FrozenRequest:
if not self.has(wanted):
raise LookupError("Request not in cache")

return [r for r in self._cache.keys() if r == wanted][0]

def set(self, req: FrozenRequest) -> None:
cache_timeout = self.client.config["cache"]["timeout"]
valid_till = datetime.now() + timedelta(seconds=cache_timeout)
self._cache[req] = valid_till

def has(self, req: FrozenRequest) -> bool:
if req not in self._cache:
return False

valid_till = self._cache[req]
if datetime.now() > valid_till:
del self._cache[req]
return False

return True


class FrozenRequest:
def __init__(
self,
path: str,
query_args: Dict[str, str],
headers: Dict[str, str],
response: Any = None,
) -> None:
self.path = path
self.query_args = query_args
self.headers = headers
self.response = response

@property
def _unique_id(self) -> str:
qargs_repr = repr(sorted(self.query_args.items()))
headers_repr = repr(sorted(self.headers.items()))
return self.path + qargs_repr + headers_repr

def __hash__(self):
return hash(self._unique_id)

def __eq__(self, other: Any) -> bool:
if isinstance(other, FrozenRequest):
return self._unique_id == other._unique_id
return False # pragma: no cover

0 comments on commit ad424b2

Please sign in to comment.