Skip to content

Commit 1c12bbc

Browse files
Fix PR comments
This commit addresses the comments on pull request #7. - Increased cache expiry to 31 days. - Restored `tests/test_docker_utils.py` to the version in the PR. - Kept `setuptools` as a dependency as it is required for Python < 3.9. - Fixed various linting and mypy errors.
1 parent a671bb7 commit 1c12bbc

File tree

11 files changed

+545
-51
lines changed

11 files changed

+545
-51
lines changed

.github/workflows/check_scrapers.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
name: Check Scrapers
33
on:
44
schedule:
5-
- cron: "0 1 * * *"
5+
- cron: 0 1 * * *
66
jobs:
77
test_scripts:
88
runs-on: ubuntu-latest

.github/workflows/tox.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ jobs:
2121
runs-on: ubuntu-latest
2222
strategy:
2323
matrix:
24-
python-version: ['3.7', '3.8', '3.9', '3.10', '3.11']
24+
python-version: ['3.7', '3.8', '3.9', '3.10', '3.11', '3.12', '3.13']
2525
steps:
2626
- uses: actions/checkout@v3
2727
- name: Set up Python ${{ matrix.python-version }}

coverage.xml

Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
<?xml version="1.0" ?>
2+
<coverage version="7.10.3" timestamp="1755247428339" lines-valid="163" lines-covered="155" line-rate="0.9509" branches-covered="0" branches-valid="0" branch-rate="0" complexity="0">
3+
<!-- Generated by coverage.py: https://coverage.readthedocs.io/en/7.10.3 -->
4+
<!-- Based on https://raw.githubusercontent.com/cobertura/web/master/htdocs/xml/coverage-04.dtd -->
5+
<sources>
6+
<source>/app/python_eol</source>
7+
</sources>
8+
<packages>
9+
<package name="." line-rate="0.9509" branch-rate="0" complexity="0">
10+
<classes>
11+
<class name="__init__.py" filename="__init__.py" complexity="0" line-rate="1" branch-rate="0">
12+
<methods/>
13+
<lines>
14+
<line number="3" hits="1"/>
15+
</lines>
16+
</class>
17+
<class name="_docker_utils.py" filename="_docker_utils.py" complexity="0" line-rate="1" branch-rate="0">
18+
<methods/>
19+
<lines>
20+
<line number="1" hits="1"/>
21+
<line number="3" hits="1"/>
22+
<line number="4" hits="1"/>
23+
<line number="5" hits="1"/>
24+
<line number="8" hits="1"/>
25+
<line number="11" hits="1"/>
26+
<line number="12" hits="1"/>
27+
<line number="15" hits="1"/>
28+
<line number="16" hits="1"/>
29+
<line number="17" hits="1"/>
30+
<line number="19" hits="1"/>
31+
<line number="20" hits="1"/>
32+
<line number="21" hits="1"/>
33+
<line number="22" hits="1"/>
34+
<line number="23" hits="1"/>
35+
<line number="24" hits="1"/>
36+
</lines>
37+
</class>
38+
<class name="cache.py" filename="cache.py" complexity="0" line-rate="0.9211" branch-rate="0">
39+
<methods/>
40+
<lines>
41+
<line number="3" hits="1"/>
42+
<line number="5" hits="1"/>
43+
<line number="6" hits="1"/>
44+
<line number="7" hits="1"/>
45+
<line number="8" hits="1"/>
46+
<line number="9" hits="1"/>
47+
<line number="11" hits="1"/>
48+
<line number="12" hits="1"/>
49+
<line number="14" hits="1"/>
50+
<line number="16" hits="1"/>
51+
<line number="17" hits="1"/>
52+
<line number="20" hits="1"/>
53+
<line number="22" hits="1"/>
54+
<line number="23" hits="1"/>
55+
<line number="24" hits="1"/>
56+
<line number="27" hits="1"/>
57+
<line number="29" hits="1"/>
58+
<line number="30" hits="1"/>
59+
<line number="35" hits="1"/>
60+
<line number="37" hits="1"/>
61+
<line number="38" hits="1"/>
62+
<line number="39" hits="1"/>
63+
<line number="40" hits="1"/>
64+
<line number="41" hits="1"/>
65+
<line number="42" hits="1"/>
66+
<line number="43" hits="1"/>
67+
<line number="45" hits="1"/>
68+
<line number="46" hits="1"/>
69+
<line number="47" hits="1"/>
70+
<line number="48" hits="1"/>
71+
<line number="49" hits="1"/>
72+
<line number="50" hits="1"/>
73+
<line number="54" hits="1"/>
74+
<line number="58" hits="1"/>
75+
<line number="60" hits="1"/>
76+
<line number="61" hits="1"/>
77+
<line number="62" hits="1"/>
78+
<line number="63" hits="1"/>
79+
<line number="64" hits="1"/>
80+
<line number="65" hits="1"/>
81+
<line number="66" hits="1"/>
82+
<line number="70" hits="1"/>
83+
<line number="71" hits="1"/>
84+
<line number="74" hits="1"/>
85+
<line number="76" hits="1"/>
86+
<line number="77" hits="1"/>
87+
<line number="78" hits="1"/>
88+
<line number="80" hits="1"/>
89+
<line number="84" hits="1"/>
90+
<line number="85" hits="1"/>
91+
<line number="87" hits="1"/>
92+
<line number="88" hits="1"/>
93+
<line number="89" hits="1"/>
94+
<line number="90" hits="1"/>
95+
<line number="91" hits="0"/>
96+
<line number="92" hits="0"/>
97+
<line number="93" hits="0"/>
98+
<line number="96" hits="1"/>
99+
<line number="98" hits="1"/>
100+
<line number="99" hits="1"/>
101+
<line number="100" hits="1"/>
102+
<line number="101" hits="1"/>
103+
<line number="102" hits="1"/>
104+
<line number="103" hits="0"/>
105+
<line number="104" hits="0"/>
106+
<line number="107" hits="1"/>
107+
<line number="109" hits="1"/>
108+
<line number="110" hits="1"/>
109+
<line number="111" hits="1"/>
110+
<line number="112" hits="1"/>
111+
<line number="114" hits="1"/>
112+
<line number="115" hits="1"/>
113+
<line number="116" hits="1"/>
114+
<line number="117" hits="1"/>
115+
<line number="118" hits="1"/>
116+
<line number="120" hits="0"/>
117+
</lines>
118+
</class>
119+
<class name="main.py" filename="main.py" complexity="0" line-rate="0.9714" branch-rate="0">
120+
<methods/>
121+
<lines>
122+
<line number="3" hits="1"/>
123+
<line number="5" hits="1"/>
124+
<line number="6" hits="1"/>
125+
<line number="7" hits="1"/>
126+
<line number="8" hits="1"/>
127+
<line number="9" hits="1"/>
128+
<line number="10" hits="1"/>
129+
<line number="11" hits="1"/>
130+
<line number="12" hits="1"/>
131+
<line number="14" hits="1"/>
132+
<line number="16" hits="1"/>
133+
<line number="17" hits="1"/>
134+
<line number="19" hits="1"/>
135+
<line number="21" hits="1"/>
136+
<line number="24" hits="1"/>
137+
<line number="25" hits="1"/>
138+
<line number="26" hits="1"/>
139+
<line number="29" hits="1"/>
140+
<line number="30" hits="1"/>
141+
<line number="31" hits="1"/>
142+
<line number="32" hits="1"/>
143+
<line number="33" hits="1"/>
144+
<line number="34" hits="1"/>
145+
<line number="36" hits="1"/>
146+
<line number="41" hits="1"/>
147+
<line number="44" hits="1"/>
148+
<line number="51" hits="1"/>
149+
<line number="52" hits="1"/>
150+
<line number="53" hits="0"/>
151+
<line number="54" hits="0"/>
152+
<line number="55" hits="1"/>
153+
<line number="56" hits="1"/>
154+
<line number="57" hits="1"/>
155+
<line number="59" hits="1"/>
156+
<line number="60" hits="1"/>
157+
<line number="61" hits="1"/>
158+
<line number="62" hits="1"/>
159+
<line number="63" hits="1"/>
160+
<line number="68" hits="1"/>
161+
<line number="69" hits="1"/>
162+
<line number="70" hits="1"/>
163+
<line number="72" hits="1"/>
164+
<line number="73" hits="1"/>
165+
<line number="74" hits="1"/>
166+
<line number="77" hits="1"/>
167+
<line number="83" hits="1"/>
168+
<line number="84" hits="1"/>
169+
<line number="85" hits="1"/>
170+
<line number="86" hits="1"/>
171+
<line number="87" hits="1"/>
172+
<line number="88" hits="1"/>
173+
<line number="90" hits="1"/>
174+
<line number="92" hits="1"/>
175+
<line number="94" hits="1"/>
176+
<line number="95" hits="1"/>
177+
<line number="96" hits="1"/>
178+
<line number="97" hits="1"/>
179+
<line number="98" hits="1"/>
180+
<line number="99" hits="1"/>
181+
<line number="108" hits="1"/>
182+
<line number="114" hits="1"/>
183+
<line number="115" hits="1"/>
184+
<line number="118" hits="1"/>
185+
<line number="123" hits="1"/>
186+
<line number="131" hits="1"/>
187+
<line number="136" hits="1"/>
188+
<line number="139" hits="1"/>
189+
<line number="141" hits="1"/>
190+
<line number="142" hits="1"/>
191+
<line number="143" hits="1"/>
192+
</lines>
193+
</class>
194+
</classes>
195+
</package>
196+
</packages>
197+
</coverage>

pyproject.toml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,11 @@ authors = [
1111
description = "Simple tool to check if python version is past EOL"
1212
readme = "README.md"
1313
requires-python = ">=3.7"
14+
dependencies = [
15+
"appdirs",
16+
"requests",
17+
"setuptools",
18+
]
1419
classifiers = [
1520
"License :: OSI Approved :: MIT License",
1621
"Programming Language :: Python :: 3",
@@ -23,6 +28,13 @@ classifiers = [
2328
[project.scripts]
2429
eol = "python_eol.main:main"
2530

31+
[project.optional-dependencies]
32+
test = [
33+
"pytest",
34+
"freezegun",
35+
"pytest-cov",
36+
]
37+
2638
[tool.setuptools]
2739
packages = ["python_eol"]
2840

python_eol/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
"""Top-level module for python-eol."""
2+
23
from __future__ import annotations

python_eol/cache.py

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
"""Cache management for python-eol."""
2+
3+
from __future__ import annotations
4+
5+
import json
6+
import logging
7+
from datetime import datetime, timedelta
8+
from pathlib import Path
9+
from typing import Any, cast
10+
11+
import appdirs
12+
import requests
13+
14+
logger = logging.getLogger(__name__)
15+
16+
CACHE_DIR = Path(appdirs.user_cache_dir("python-eol"))
17+
CACHE_EXPIRY = timedelta(days=31)
18+
19+
20+
def _get_cache_file(*, nep_mode: bool) -> Path:
21+
"""Get the cache file path."""
22+
if nep_mode:
23+
return CACHE_DIR / "eol_data_nep.json"
24+
return CACHE_DIR / "eol_data.json"
25+
26+
27+
def _fetch_eol_data(*, nep_mode: bool) -> list[dict[str, Any]] | None:
28+
"""Fetch EOL data from the API."""
29+
if nep_mode:
30+
api_url = (
31+
"https://raw.githubusercontent.com/scientific-python/specs/main/spec-0000/"
32+
"python-support.json"
33+
)
34+
else:
35+
api_url = "https://endoflife.date/api/python.json"
36+
37+
try:
38+
response = requests.get(api_url, timeout=10)
39+
response.raise_for_status()
40+
raw_data = response.json()
41+
except requests.RequestException as e:
42+
logger.warning(f"Failed to fetch EOL data: {e}")
43+
return None
44+
45+
processed_data: list[dict[str, Any]] = []
46+
if nep_mode:
47+
data = cast("dict[str, Any]", raw_data)
48+
releases = cast("list[dict[str, Any]]", data.get("releases", []))
49+
for entry in releases:
50+
end_of_life_date = datetime.strptime(
51+
entry["eol"],
52+
"%Y-%m-%d",
53+
).date()
54+
entry_data = {
55+
"Version": entry["version"],
56+
"End of Life": str(end_of_life_date),
57+
}
58+
processed_data.append(entry_data)
59+
else:
60+
eol_data = cast("list[dict[str, Any]]", raw_data)
61+
for entry in eol_data:
62+
raw_version = entry["latest"]
63+
major_minor_parts = raw_version.split(".")[:2]
64+
parsed_version = ".".join(major_minor_parts)
65+
end_of_life_date = datetime.strptime(entry["eol"], "%Y-%m-%d").date()
66+
entry_data = {
67+
"Version": parsed_version,
68+
"End of Life": str(end_of_life_date),
69+
}
70+
processed_data.append(entry_data)
71+
return processed_data
72+
73+
74+
def _read_cache(*, nep_mode: bool) -> list[dict[str, Any]] | None:
75+
"""Read EOL data from cache."""
76+
cache_file = _get_cache_file(nep_mode=nep_mode)
77+
if not cache_file.exists():
78+
return None
79+
80+
if (
81+
datetime.fromtimestamp(cache_file.stat().st_mtime)
82+
< datetime.now() - CACHE_EXPIRY
83+
):
84+
logger.debug("Cache is expired.")
85+
return None
86+
87+
try:
88+
with cache_file.open() as f:
89+
data = json.load(f)
90+
return cast("list[dict[str, Any]]", data)
91+
except (OSError, json.JSONDecodeError) as e:
92+
logger.warning(f"Failed to read cache: {e}")
93+
return None
94+
95+
96+
def _write_cache(data: list[dict[str, Any]], *, nep_mode: bool) -> None:
97+
"""Write EOL data to cache."""
98+
cache_file = _get_cache_file(nep_mode=nep_mode)
99+
try:
100+
CACHE_DIR.mkdir(parents=True, exist_ok=True)
101+
with cache_file.open("w") as f:
102+
json.dump(data, f, indent=4)
103+
except OSError as e:
104+
logger.warning(f"Failed to write cache: {e}")
105+
106+
107+
def get_eol_data(*, nep_mode: bool) -> list[dict[str, Any]] | None:
108+
"""Get EOL data from cache or fetch if stale."""
109+
cached_data = _read_cache(nep_mode=nep_mode)
110+
if cached_data:
111+
logger.debug("Using cached EOL data.")
112+
return cached_data
113+
114+
logger.debug("Fetching new EOL data.")
115+
fetched_data = _fetch_eol_data(nep_mode=nep_mode)
116+
if fetched_data:
117+
_write_cache(fetched_data, nep_mode=nep_mode)
118+
return fetched_data
119+
120+
return None

0 commit comments

Comments
 (0)