Skip to content

Commit 92fa2ba

Browse files
authored
Merge pull request #13 from feelpp/12-fix-font-download
fix: use requests library for font downloads and update URLs
2 parents 9cb847d + 755d78a commit 92fa2ba

File tree

2 files changed

+56
-38
lines changed

2 files changed

+56
-38
lines changed

src/article_cli/fonts.py

Lines changed: 48 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -9,26 +9,38 @@
99
import tempfile
1010
from pathlib import Path
1111
from typing import List, Dict, Optional, Any
12-
from urllib.request import urlopen, Request
13-
from urllib.error import URLError, HTTPError
12+
13+
import requests
1414

1515
from .zotero import print_error, print_info, print_success, print_warning
1616

1717

1818
# Default font sources for common themes
19+
# Note: Marianne font from French government requires manual download due to Cloudflare protection.
20+
# Configure custom URL in pyproject.toml if you have a mirror or local copy.
1921
DEFAULT_FONT_SOURCES = [
2022
{
21-
"name": "Marianne",
22-
"url": "https://www.systeme-de-design.gouv.fr/uploads/Marianne_fd0ba9c190.zip",
23-
"description": "French government official font (Système de Design de l'État)",
23+
"name": "Roboto",
24+
"url": "https://github.com/googlefonts/roboto/releases/download/v2.138/roboto-unhinted.zip",
25+
"description": "Google's sans-serif font family",
2426
},
2527
{
2628
"name": "Roboto Mono",
27-
"url": "https://fonts.google.com/download?family=Roboto+Mono",
29+
"url": "https://github.com/googlefonts/RobotoMono/archive/refs/heads/main.zip",
2830
"description": "Google's monospace font, good for code",
2931
},
3032
]
3133

34+
# Additional font sources that may require special handling
35+
OPTIONAL_FONT_SOURCES = {
36+
"Marianne": {
37+
"name": "Marianne",
38+
"url": "https://www.info.gouv.fr/upload/media/content/0001/14/e9dd2398914853b3c21c402245866bc74cd3d3c5.zip",
39+
"description": "French government official font - TTF version (Système de Design de l'État)",
40+
"note": "May require manual download due to Cloudflare protection",
41+
},
42+
}
43+
3244

3345
class FontInstaller:
3446
"""Handles downloading and installing fonts for LaTeX projects"""
@@ -163,25 +175,30 @@ def _download_file(self, url: str, dest: Path) -> None:
163175
url: URL to download
164176
dest: Destination path
165177
"""
166-
# Create request with user agent to avoid blocks
167-
headers = {"User-Agent": "Mozilla/5.0 (compatible; article-cli font installer)"}
168-
request = Request(url, headers=headers)
178+
# Use requests library for better redirect and header handling
179+
headers = {
180+
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
181+
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
182+
"Accept-Language": "en-US,en;q=0.5",
183+
}
169184

170185
try:
171-
with urlopen(request, timeout=60) as response:
172-
# Get file size if available
173-
content_length = response.headers.get("Content-Length")
174-
total_size = int(content_length) if content_length else None
175-
176-
# Download in chunks
177-
chunk_size = 8192
178-
downloaded = 0
179-
180-
with open(dest, "wb") as f:
181-
while True:
182-
chunk = response.read(chunk_size)
183-
if not chunk:
184-
break
186+
response = requests.get(
187+
url, headers=headers, allow_redirects=True, stream=True, timeout=60
188+
)
189+
response.raise_for_status()
190+
191+
# Get file size if available
192+
content_length = response.headers.get("Content-Length")
193+
total_size = int(content_length) if content_length else None
194+
195+
# Download in chunks
196+
chunk_size = 8192
197+
downloaded = 0
198+
199+
with open(dest, "wb") as f:
200+
for chunk in response.iter_content(chunk_size=chunk_size):
201+
if chunk:
185202
f.write(chunk)
186203
downloaded += len(chunk)
187204

@@ -200,14 +217,16 @@ def _download_file(self, url: str, dest: Path) -> None:
200217
flush=True,
201218
)
202219

203-
print() # New line after progress
220+
print() # New line after progress
204221

205-
except HTTPError as e:
206-
raise RuntimeError(f"HTTP error {e.code}: {e.reason}")
207-
except URLError as e:
208-
raise RuntimeError(f"URL error: {e.reason}")
209-
except TimeoutError:
222+
except requests.exceptions.HTTPError as e:
223+
raise RuntimeError(f"HTTP error: {e}")
224+
except requests.exceptions.ConnectionError as e:
225+
raise RuntimeError(f"Connection error: {e}")
226+
except requests.exceptions.Timeout:
210227
raise RuntimeError("Download timed out")
228+
except requests.exceptions.RequestException as e:
229+
raise RuntimeError(f"Download failed: {e}")
211230

212231
def _extract_zip(self, zip_path: Path, target_dir: Path) -> None:
213232
"""

tests/test_fonts.py

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -107,16 +107,15 @@ def test_get_font_files_specific_font(self, tmp_path):
107107
assert len(files) == 1
108108
assert files[0].name == "font1.ttf"
109109

110-
@patch("article_cli.fonts.urlopen")
111-
def test_download_file_success(self, mock_urlopen, tmp_path):
110+
@patch("article_cli.fonts.requests.get")
111+
def test_download_file_success(self, mock_get, tmp_path):
112112
"""Test successful file download"""
113113
# Create mock response
114114
mock_response = MagicMock()
115115
mock_response.headers.get.return_value = "1000"
116-
mock_response.read.side_effect = [b"test content", b""]
117-
mock_response.__enter__ = MagicMock(return_value=mock_response)
118-
mock_response.__exit__ = MagicMock(return_value=False)
119-
mock_urlopen.return_value = mock_response
116+
mock_response.iter_content.return_value = [b"test content"]
117+
mock_response.raise_for_status = MagicMock()
118+
mock_get.return_value = mock_response
120119

121120
installer = FontInstaller(fonts_dir=tmp_path / "fonts")
122121
dest = tmp_path / "test.zip"
@@ -228,10 +227,10 @@ def test_default_sources_structure(self):
228227
assert "url" in source
229228
assert source["url"].startswith("http")
230229

231-
def test_default_sources_include_marianne(self):
232-
"""Test that Marianne font is included"""
230+
def test_default_sources_include_roboto(self):
231+
"""Test that Roboto font is included"""
233232
names = [s["name"] for s in DEFAULT_FONT_SOURCES]
234-
assert "Marianne" in names
233+
assert "Roboto" in names
235234

236235
def test_default_sources_include_roboto_mono(self):
237236
"""Test that Roboto Mono font is included"""

0 commit comments

Comments
 (0)