-
Notifications
You must be signed in to change notification settings - Fork 4
/
eol.py
159 lines (131 loc) · 4.2 KB
/
eol.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
import datetime
import aiohttp
import aiohttp.client_exceptions
import dateutil.parser
import ci.util
import delivery.model as dm
import caching
def normalise_os_id(os_id: str) -> str:
'''
Some product identifiers differ from the ones we know.
This function translates known difference from "our" identifier to the
one EOL API can process.
'''
if os_id == 'amzn':
return 'amazon-linux'
return os_id
def os_release_info_from_release_cycle(
release_cycle: dict,
) -> dm.OsReleaseInfo:
def eol_date() -> bool | datetime.datetime | None:
eol_date = release_cycle.get('extendedSupport')
if eol_date is None:
eol_date = release_cycle.get('eol')
# unfortunately, eol-api yields inconsistent values for `eol` attribute (bool vs timestamp)
if isinstance(eol_date, bool):
return eol_date
elif isinstance(eol_date, str):
return dateutil.parser.isoparse(eol_date)
else:
return None
def reached_eol(
eol_date: datetime.datetime | bool=eol_date(),
) -> bool | None:
if isinstance(eol_date, bool):
return eol_date
elif isinstance(eol_date, datetime.datetime):
return eol_date < datetime.datetime.today()
else:
return None
return dm.OsReleaseInfo(
name=release_cycle['cycle'],
# not provided for all products
greatest_version=release_cycle.get('latest'),
eol_date=eol_date(),
reached_eol=reached_eol(),
)
class EolRoutes:
def __init__(
self,
base_url: str = 'https://endoflife.date/api',
):
self._base_url = base_url
def all_products(self):
return ci.util.urljoin(
self._base_url,
'all.json',
)
def cycles(
self,
product: str,
):
return ci.util.urljoin(
self._base_url,
f'{product}.json',
)
def cycle(
self,
cycle: int,
product: str,
):
return ci.util.urljoin(
self._base_url,
product,
f'{cycle}.json',
)
class EolClient:
'''
API client for https://endoflife.date/docs/api.
'''
def __init__(
self,
routes: EolRoutes = EolRoutes(),
):
self._routes = routes
self.session = aiohttp.ClientSession()
@caching.async_cached(caching.TTLFilesystemCache(ttl=60 * 60 * 24, max_total_size_mib=1)) # 24h
async def all_products(self) -> list[str]:
async with self.session.get(url=self._routes.all_products()) as res:
res.raise_for_status()
return await res.json()
@caching.async_cached(caching.TTLFilesystemCache(ttl=60 * 60 * 24, max_total_size_mib=200)) # 24h
async def cycles(
self,
product: str,
absent_ok: bool = False,
) -> list[dict] | None:
'''
Returns release_cycles as described here https://endoflife.date/docs/api.
If `absent_ok`, HTTP 404 returns `None`.
'''
async with self.session.get(url=self._routes.cycles(product)) as res:
try:
res.raise_for_status()
except aiohttp.client_exceptions.ClientResponseError as e:
if not absent_ok:
raise
if e.status == 404:
return None
raise
return await res.json()
@caching.async_cached(caching.TTLFilesystemCache(ttl=60 * 60 * 24, max_total_size_mib=200)) # 24h
async def cycle(
self,
product: str,
cycle: str,
absent_ok: bool = False,
) -> dict | None:
'''
Returns single release_cycle as described here https://endoflife.date/docs/api.
If `absent_ok`, HTTP 404 returns `None`.
'''
async with self.session.get(url=self._routes.cycle(cycle, product)) as res:
try:
res.raise_for_status()
except aiohttp.client_exceptions.ClientResponseError as e:
if not absent_ok:
raise
if e.status == 404:
return None
raise
return await res.json()