fetch: remove the now-unused fetch_image function
- ID
1b2da07- date
2026-04-10 21:40:59+00:00- author
Alex Chan <alex@alexwlchan.net>- parent
9881a6a- message
fetch: remove the now-unused `fetch_image` function- changed files
9 files, 117 additions, 403 deletionsCHANGELOG.mdsrc/chives/__init__.pysrc/chives/fetch.pytests/fixtures/cassettes/TestDownloadImage.test_no_content_type_header.ymltests/fixtures/cassettes/TestDownloadImage.test_non_image.ymltests/fixtures/cassettes/TestFetchImage.test_http_200.ymltests/fixtures/cassettes/TestFetchImage.test_no_content_type_header.ymltests/fixtures/cassettes/TestFetchImage.test_non_image.ymltests/test_fetch.py
Changed files
CHANGELOG.md (3972) → CHANGELOG.md (4093)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d03c8fe..5c9922e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,9 @@
# CHANGELOG
+## v33 - 2026-04-10
+
+Remove the `fetch_image` function from `chives.fetch`; either use `fetch_url` or `download_image`.
+
## v32 - 2026-04-01
Add a `download_image` function to `chives.fetch`.
src/chives/__init__.py (391) → src/chives/__init__.py (391)
diff --git a/src/chives/__init__.py b/src/chives/__init__.py
index a3ab715..5378754 100644
--- a/src/chives/__init__.py
+++ b/src/chives/__init__.py
@@ -11,4 +11,4 @@ I share across multiple sites.
"""
-__version__ = "32"
+__version__ = "33"
src/chives/fetch.py (3246) → src/chives/fetch.py (2853)
diff --git a/src/chives/fetch.py b/src/chives/fetch.py
index 3d60e78..71f896c 100644
--- a/src/chives/fetch.py
+++ b/src/chives/fetch.py
@@ -11,7 +11,7 @@ import urllib.request
import certifi
-__all__ = ["download_image", "fetch_url", "fetch_image", "ImageFormat"]
+__all__ = ["download_image", "fetch_url"]
ssl_context = ssl.create_default_context(cafile=certifi.where())
@@ -81,15 +81,20 @@ def _guess_image_format(content_type: str | None) -> ImageFormat:
raise ValueError(f"unrecognised image format: {content_type}")
-def fetch_image(
+def download_image(
url: str,
+ out_prefix: Path,
*,
params: dict[str, str] | None = None,
headers: dict[str, str] | None = None,
-) -> tuple[bytes, ImageFormat]:
+) -> Path:
"""
- Fetch an image from the given URL and return the image data and
- image format.
+ Download an image from the given URL to the target path, and return
+ the path of the downloaded file.
+
+ Add the appropriate file extension, based on the image's Content-Type.
+
+ Throws a FileExistsError if you try to overwrite an existing file.
"""
ssl_context = ssl.create_default_context(cafile=certifi.where())
@@ -101,30 +106,11 @@ def fetch_image(
img_format = _guess_image_format(content_type=resp.headers["content-type"])
- return img_data, img_format
-
-
-def download_image(
- url: str,
- out_prefix: Path,
- *,
- params: dict[str, str] | None = None,
- headers: dict[str, str] | None = None,
-) -> Path:
- """
- Download an image from the given URL to the target path, and return
- the path of the downloaded file.
-
- Add the appropriate file extension, based on the image's Content-Type.
-
- Throws a FileExistsError if you try to overwrite an existing file.
- """
- im_data, im_format = fetch_image(url, params=params, headers=headers)
- out_path = out_prefix.with_suffix("." + im_format)
+ out_path = out_prefix.with_suffix("." + img_format)
out_path.parent.mkdir(exist_ok=True, parents=True)
with open(out_path, "xb") as out_file:
- out_file.write(im_data)
+ out_file.write(img_data)
return out_path
tests/fixtures/cassettes/TestDownloadImage.test_no_content_type_header.yml (0) → tests/fixtures/cassettes/TestDownloadImage.test_no_content_type_header.yml (605)
diff --git a/tests/fixtures/cassettes/TestDownloadImage.test_no_content_type_header.yml b/tests/fixtures/cassettes/TestDownloadImage.test_no_content_type_header.yml
new file mode 100644
index 0000000..f6d7647
--- /dev/null
+++ b/tests/fixtures/cassettes/TestDownloadImage.test_no_content_type_header.yml
@@ -0,0 +1,33 @@
+interactions:
+- request:
+ body: null
+ headers:
+ Connection:
+ - close
+ Host:
+ - httpbin.org
+ User-Agent:
+ - Python-urllib/3.13
+ method: GET
+ uri: http://httpbin.org/status/200
+ response:
+ body:
+ string: ''
+ headers:
+ Access-Control-Allow-Credentials:
+ - 'true'
+ Access-Control-Allow-Origin:
+ - '*'
+ Connection:
+ - close
+ Content-Length:
+ - '0'
+ Content-Type: []
+ Date:
+ - Fri, 10 Apr 2026 21:39:22 GMT
+ Server:
+ - gunicorn/19.9.0
+ status:
+ code: 200
+ message: OK
+version: 1
tests/fixtures/cassettes/TestDownloadImage.test_non_image.yml (0) → tests/fixtures/cassettes/TestDownloadImage.test_non_image.yml (635)
diff --git a/tests/fixtures/cassettes/TestDownloadImage.test_non_image.yml b/tests/fixtures/cassettes/TestDownloadImage.test_non_image.yml
new file mode 100644
index 0000000..3ad0077
--- /dev/null
+++ b/tests/fixtures/cassettes/TestDownloadImage.test_non_image.yml
@@ -0,0 +1,34 @@
+interactions:
+- request:
+ body: null
+ headers:
+ Connection:
+ - close
+ Host:
+ - httpbin.org
+ User-Agent:
+ - Python-urllib/3.13
+ method: GET
+ uri: http://httpbin.org/status/200
+ response:
+ body:
+ string: ''
+ headers:
+ Access-Control-Allow-Credentials:
+ - 'true'
+ Access-Control-Allow-Origin:
+ - '*'
+ Connection:
+ - close
+ Content-Length:
+ - '0'
+ Content-Type:
+ - text/html; charset=utf-8
+ Date:
+ - Fri, 10 Apr 2026 21:39:22 GMT
+ Server:
+ - gunicorn/19.9.0
+ status:
+ code: 200
+ message: OK
+version: 1
tests/fixtures/cassettes/TestFetchImage.test_http_200.yml (16041) → tests/fixtures/cassettes/TestFetchImage.test_http_200.yml (0)
diff --git a/tests/fixtures/cassettes/TestFetchImage.test_http_200.yml b/tests/fixtures/cassettes/TestFetchImage.test_http_200.yml
deleted file mode 100644
index 31a0d50..0000000
--- a/tests/fixtures/cassettes/TestFetchImage.test_http_200.yml
+++ /dev/null
@@ -1,257 +0,0 @@
-interactions:
-- request:
- body: null
- headers:
- Connection:
- - close
- Host:
- - api.tumblr.com
- User-Agent:
- - Python-urllib/3.14
- method: GET
- uri: https://api.tumblr.com/v2/blog/thecroissantgirl.tumblr.com/avatar
- response:
- body:
- string: '{"meta":{"status":302,"msg":"Found"},"response":{"avatar_url":"https://64.media.tumblr.com/b9db8c282532e62b4009e9b41e9b0cb0/1773c30d064a7637-b4/s64x64u_c1/f88f4f722bf06397895453a98ac976eaf767de08.png"}}'
- headers:
- Alt-Svc:
- - h3=":443"; ma=86400
- Connection:
- - close
- Content-Length:
- - '202'
- Content-Type:
- - application/json
- Date:
- - Mon, 30 Mar 2026 05:04:45 GMT
- Location:
- - https://64.media.tumblr.com/b9db8c282532e62b4009e9b41e9b0cb0/1773c30d064a7637-b4/s64x64u_c1/f88f4f722bf06397895453a98ac976eaf767de08.png
- Server:
- - nginx
- Server-Timing:
- - a8c-cdn, dc;desc=lhr, cache;desc=BYPASS;dur=243.0
- Strict-Transport-Security:
- - max-age=31536000; preload
- X-Cache-Avatar:
- - 'true'
- X-Rid:
- - 35e6c63c1df237d105ecbc7e70c6e78f
- X-UA-Compatible:
- - IE=Edge,chrome=1
- status:
- code: 302
- message: Found
-- request:
- body: null
- headers:
- Connection:
- - close
- Host:
- - 64.media.tumblr.com
- User-Agent:
- - Python-urllib/3.14
- method: GET
- uri: https://64.media.tumblr.com/b9db8c282532e62b4009e9b41e9b0cb0/1773c30d064a7637-b4/s64x64u_c1/f88f4f722bf06397895453a98ac976eaf767de08.png
- response:
- body:
- string: !!binary |
- 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
- b5UTb+T6FQAAAABJRU5ErkJggg==
- headers:
- Access-Control-Allow-Methods:
- - GET
- Access-Control-Allow-Origin:
- - '*'
- Access-Control-Max-Age:
- - '86400'
- Alt-Svc:
- - h3=":443"; ma=86400
- Cache-Control:
- - max-age=315360000
- Connection:
- - close
- Content-Disposition:
- - inline; filename="tumblr_b9db8c282532e62b4009e9b41e9b0cb0_f88f4f72_64.png"
- Content-Length:
- - '9025'
- Content-Type:
- - image/png
- Date:
- - Mon, 30 Mar 2026 05:04:45 GMT
- Etag:
- - '"7c7a5affc81421707c94439f0333734c-1498089600-0de4da0"'
- Last-Modified:
- - Fri, 02 Jun 2023 20:33:11 GMT
- Server:
- - nginx
- Server-Timing:
- - a8c-cdn, dc;desc=lhr, cache;desc=HIT;dur=0.0
- Strict-Transport-Security:
- - max-age=31536000; preload
- Timing-Allow-Origin:
- - '*'
- X-nc:
- - HIT lhr 24
- x-frames:
- - '1'
- status:
- code: 200
- message: OK
-version: 1
tests/fixtures/cassettes/TestFetchImage.test_no_content_type_header.yml (582) → tests/fixtures/cassettes/TestFetchImage.test_no_content_type_header.yml (0)
diff --git a/tests/fixtures/cassettes/TestFetchImage.test_no_content_type_header.yml b/tests/fixtures/cassettes/TestFetchImage.test_no_content_type_header.yml
deleted file mode 100644
index 2cbcf91..0000000
--- a/tests/fixtures/cassettes/TestFetchImage.test_no_content_type_header.yml
+++ /dev/null
@@ -1,32 +0,0 @@
-interactions:
-- request:
- body: null
- headers:
- Connection:
- - close
- Host:
- - httpbin.org
- User-Agent:
- - Python-urllib/3.14
- method: GET
- uri: http://httpbin.org/status/200
- response:
- body:
- string: ''
- headers:
- Access-Control-Allow-Credentials:
- - 'true'
- Access-Control-Allow-Origin:
- - '*'
- Connection:
- - close
- Content-Length:
- - '0'
- Date:
- - Mon, 30 Mar 2026 05:20:46 GMT
- Server:
- - gunicorn/19.9.0
- status:
- code: 200
- message: OK
-version: 1
tests/fixtures/cassettes/TestFetchImage.test_non_image.yml (635) → tests/fixtures/cassettes/TestFetchImage.test_non_image.yml (0)
diff --git a/tests/fixtures/cassettes/TestFetchImage.test_non_image.yml b/tests/fixtures/cassettes/TestFetchImage.test_non_image.yml
deleted file mode 100644
index 706342a..0000000
--- a/tests/fixtures/cassettes/TestFetchImage.test_non_image.yml
+++ /dev/null
@@ -1,34 +0,0 @@
-interactions:
-- request:
- body: null
- headers:
- Connection:
- - close
- Host:
- - httpbin.org
- User-Agent:
- - Python-urllib/3.14
- method: GET
- uri: http://httpbin.org/status/200
- response:
- body:
- string: ''
- headers:
- Access-Control-Allow-Credentials:
- - 'true'
- Access-Control-Allow-Origin:
- - '*'
- Connection:
- - close
- Content-Length:
- - '0'
- Content-Type:
- - text/html; charset=utf-8
- Date:
- - Mon, 30 Mar 2026 05:21:30 GMT
- Server:
- - gunicorn/19.9.0
- status:
- code: 200
- message: OK
-version: 1
tests/test_fetch.py (5164) → tests/test_fetch.py (4709)
diff --git a/tests/test_fetch.py b/tests/test_fetch.py
index 442de02..18786be 100644
--- a/tests/test_fetch.py
+++ b/tests/test_fetch.py
@@ -3,18 +3,16 @@ Tests for `chives.fetch`.
"""
import filecmp
-from io import BytesIO
import json
from pathlib import Path
from typing import Any
from urllib.error import HTTPError
-from PIL import Image
import pytest
import vcr
from vcr.cassette import Cassette
-from chives.fetch import download_image, fetch_image, fetch_url
+from chives.fetch import download_image, fetch_url
class TestFetchUrl:
@@ -68,56 +66,6 @@ class TestFetchUrl:
assert headers["X-Author"] == "alexwlchan"
-class TestFetchImage:
- """
- Tests for `fetch_image`.
- """
-
- def test_http_200(self, vcr_cassette: Cassette) -> None:
- """
- Fetch an image and check we get the correct format.
- """
- url = "https://api.tumblr.com/v2/blog/thecroissantgirl.tumblr.com/avatar"
-
- img_data, img_format = fetch_image(url)
- assert img_format == "png"
-
- im = Image.open(BytesIO(img_data))
- assert im.format == "PNG"
-
- def test_non_image(self, vcr_cassette: Cassette) -> None:
- """
- Fetching an "image" which has a non-image Content-Type header
- throws an error.
- """
- url = "http://httpbin.org/status/200"
-
- with pytest.raises(ValueError, match="unrecognised image format"):
- fetch_image(url)
-
- def test_no_content_type_header(self, cassette_name: str) -> None:
- """
- Fetching a URL which doesn't return a Content-Type header
- throws an error.
- """
- url = "http://httpbin.org/status/200"
-
- def delete_content_type_header(response: Any) -> Any:
- response["headers"]["Content-Type"] = []
- return response
-
- with vcr.use_cassette(
- cassette_name,
- cassette_library_dir="tests/fixtures/cassettes",
- decode_compressed_response=True,
- before_record_response=delete_content_type_header,
- ):
- with pytest.raises(
- RuntimeError, match="no Content-Type header in response"
- ):
- fetch_image(url)
-
-
class TestDownloadImage:
"""
Tests for `download_image`.
@@ -167,3 +115,35 @@ class TestDownloadImage:
# The file contents are the same as the first download.
assert filecmp.cmp(out_path, "tests/fixtures/media/470906.png", shallow=False)
+
+ def test_non_image(self, vcr_cassette: Cassette) -> None:
+ """
+ Fetching an "image" which has a non-image Content-Type header
+ throws an error.
+ """
+ url = "http://httpbin.org/status/200"
+
+ with pytest.raises(ValueError, match="unrecognised image format"):
+ download_image(url, out_prefix=Path("example"))
+
+ def test_no_content_type_header(self, cassette_name: str) -> None:
+ """
+ Fetching a URL which doesn't return a Content-Type header
+ throws an error.
+ """
+ url = "http://httpbin.org/status/200"
+
+ def delete_content_type_header(response: Any) -> Any:
+ response["headers"]["Content-Type"] = []
+ return response
+
+ with vcr.use_cassette(
+ cassette_name,
+ cassette_library_dir="tests/fixtures/cassettes",
+ decode_compressed_response=True,
+ before_record_response=delete_content_type_header,
+ ):
+ with pytest.raises(
+ RuntimeError, match="no Content-Type header in response"
+ ):
+ download_image(url, out_prefix=Path("example"))