Coverage for r11k/httpcache.py: 59%
46 statements
« prev ^ index » next coverage.py v7.2.1, created at 2023-03-13 23:29 +0100
« prev ^ index » next coverage.py v7.2.1, created at 2023-03-13 23:29 +0100
1"""Extension of `r11k.cache.KeyValueStore`. Should possibly be merged."""
3import os
4import os.path
5from datetime import datetime
6import time
7import requests
8from pathlib import Path
10from typing import Any, Optional
12import r11k
13from r11k.cache import KeyValueStore
16class HTTPCache:
17 """
18 HTTP client with built in cache.
20 First caches any (successful) request for one hour (configurable),
21 after the local TTL has expired the request is re-fired, but with
22 a 'If-Modified-Since' header set.
24 :param path: Where in the filesystem cached files are stored.
25 :param default_ttl: How long (in seconds) we should cache files before re-fetching.
26 """
28 def __init__(self, path: str = '/tmp', default_ttl: int = 3600):
29 self.store = KeyValueStore(path)
30 self.default_ttl = default_ttl
32 def get(self, url: str) -> Optional[Any]:
33 """
34 Fetch (from internet or cache) the contents of url.
36 :return: A json blob
37 """
38 key = url
39 if existing := self.store.get(key):
40 path = self.store.path(key)
41 try:
42 st = os.stat(path)
43 last_modified = datetime.utcfromtimestamp(st.st_mtime)
44 now = datetime.utcfromtimestamp(time.time())
45 if abs(now - last_modified).seconds < self.default_ttl:
46 return existing
47 except FileNotFoundError:
48 return existing
50 response = self.__fetch(url, last_modified)
51 if response.status_code == 304:
52 Path(path).touch()
53 return existing
54 elif response.status_code != 200:
55 raise RuntimeError('Invalid response code', response.status_code, url)
57 data = response.json()
58 self.store.put(key, data)
59 return data
61 else:
62 response = self.__fetch(url)
63 if response.status_code != 200: 63 ↛ 64line 63 didn't jump to line 64, because the condition on line 63 was never true
64 raise RuntimeError('Invalid response code', response.status_code, url)
65 data = response.json()
66 self.store.put(key, data)
67 return data
69 def __fetch(self, url: str, last_modified: Optional[datetime] = None) -> requests.Response:
70 headers = {'User-Agent': f'r11k/{r11k.VERSION}'}
71 if last_modified: 71 ↛ 72line 71 didn't jump to line 72, because the condition on line 71 was never true
72 date = last_modified.strftime('%a, %d %b %Y %H:%M:%S GMT')
73 headers['If-Modified-Since'] = date
74 return requests.get(url, headers=headers)