python-fotoware-client 0.2.2__tar.gz → 0.2.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {python_fotoware_client-0.2.2/src/python_fotoware_client.egg-info → python_fotoware_client-0.2.3}/PKG-INFO +1 -1
- {python_fotoware_client-0.2.2 → python_fotoware_client-0.2.3}/pyproject.toml +1 -1
- {python_fotoware_client-0.2.2 → python_fotoware_client-0.2.3}/src/fotoware_client/__init__.py +1 -1
- {python_fotoware_client-0.2.2 → python_fotoware_client-0.2.3}/src/fotoware_client/client.py +67 -12
- {python_fotoware_client-0.2.2 → python_fotoware_client-0.2.3/src/python_fotoware_client.egg-info}/PKG-INFO +1 -1
- {python_fotoware_client-0.2.2 → python_fotoware_client-0.2.3}/tests/test_client.py +81 -2
- {python_fotoware_client-0.2.2 → python_fotoware_client-0.2.3}/LICENSE +0 -0
- {python_fotoware_client-0.2.2 → python_fotoware_client-0.2.3}/README.md +0 -0
- {python_fotoware_client-0.2.2 → python_fotoware_client-0.2.3}/setup.cfg +0 -0
- {python_fotoware_client-0.2.2 → python_fotoware_client-0.2.3}/src/fotoware_client/auth.py +0 -0
- {python_fotoware_client-0.2.2 → python_fotoware_client-0.2.3}/src/fotoware_client/constants.py +0 -0
- {python_fotoware_client-0.2.2 → python_fotoware_client-0.2.3}/src/fotoware_client/credentials.py +0 -0
- {python_fotoware_client-0.2.2 → python_fotoware_client-0.2.3}/src/fotoware_client/exceptions.py +0 -0
- {python_fotoware_client-0.2.2 → python_fotoware_client-0.2.3}/src/fotoware_client/limits.py +0 -0
- {python_fotoware_client-0.2.2 → python_fotoware_client-0.2.3}/src/fotoware_client/models.py +0 -0
- {python_fotoware_client-0.2.2 → python_fotoware_client-0.2.3}/src/fotoware_client/py.typed +0 -0
- {python_fotoware_client-0.2.2 → python_fotoware_client-0.2.3}/src/fotoware_client/retry.py +0 -0
- {python_fotoware_client-0.2.2 → python_fotoware_client-0.2.3}/src/fotoware_client/settings.py +0 -0
- {python_fotoware_client-0.2.2 → python_fotoware_client-0.2.3}/src/python_fotoware_client.egg-info/SOURCES.txt +0 -0
- {python_fotoware_client-0.2.2 → python_fotoware_client-0.2.3}/src/python_fotoware_client.egg-info/dependency_links.txt +0 -0
- {python_fotoware_client-0.2.2 → python_fotoware_client-0.2.3}/src/python_fotoware_client.egg-info/requires.txt +0 -0
- {python_fotoware_client-0.2.2 → python_fotoware_client-0.2.3}/src/python_fotoware_client.egg-info/top_level.txt +0 -0
- {python_fotoware_client-0.2.2 → python_fotoware_client-0.2.3}/tests/test_auth.py +0 -0
- {python_fotoware_client-0.2.2 → python_fotoware_client-0.2.3}/tests/test_credentials.py +0 -0
- {python_fotoware_client-0.2.2 → python_fotoware_client-0.2.3}/tests/test_exceptions.py +0 -0
- {python_fotoware_client-0.2.2 → python_fotoware_client-0.2.3}/tests/test_limits.py +0 -0
- {python_fotoware_client-0.2.2 → python_fotoware_client-0.2.3}/tests/test_models.py +0 -0
- {python_fotoware_client-0.2.2 → python_fotoware_client-0.2.3}/tests/test_retry.py +0 -0
- {python_fotoware_client-0.2.2 → python_fotoware_client-0.2.3}/tests/test_settings.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-fotoware-client
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.3
|
|
4
4
|
Summary: A thin, honest client for the FotoWare API: one call, one request; refuses queries FotoWare would answer wrongly; counts what each request costs
|
|
5
5
|
Author: Michael Hoppe
|
|
6
6
|
License-Expression: MIT
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "python-fotoware-client"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.3"
|
|
8
8
|
description = "A thin, honest client for the FotoWare API: one call, one request; refuses queries FotoWare would answer wrongly; counts what each request costs"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.14,<3.15"
|
{python_fotoware_client-0.2.2 → python_fotoware_client-0.2.3}/src/fotoware_client/__init__.py
RENAMED
|
@@ -23,7 +23,7 @@ from fotoware_client.models import Archive, Asset, SearchResult
|
|
|
23
23
|
from fotoware_client.retry import NO_RETRY, STANDARD_RETRY, RetryConfig
|
|
24
24
|
from fotoware_client.settings import ServerSettings
|
|
25
25
|
|
|
26
|
-
__version__ = "0.2.
|
|
26
|
+
__version__ = "0.2.3"
|
|
27
27
|
|
|
28
28
|
__all__ = [
|
|
29
29
|
# Client
|
|
@@ -4,7 +4,7 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
import logging
|
|
6
6
|
import time
|
|
7
|
-
from collections.abc import Iterator
|
|
7
|
+
from collections.abc import Callable, Iterator
|
|
8
8
|
from typing import Any, BinaryIO
|
|
9
9
|
from urllib.parse import urljoin
|
|
10
10
|
|
|
@@ -510,6 +510,57 @@ class FotoWareClient:
|
|
|
510
510
|
*,
|
|
511
511
|
timeout: int | None = None,
|
|
512
512
|
rendition: str | None = None,
|
|
513
|
+
) -> str:
|
|
514
|
+
"""The URL a prepared rendition can be downloaded from, the original's
|
|
515
|
+
unless *rendition* names another.
|
|
516
|
+
|
|
517
|
+
FotoWare prepares the file in a background task and serves it under
|
|
518
|
+
that task's address for a limited time; download it promptly.
|
|
519
|
+
|
|
520
|
+
Raises:
|
|
521
|
+
APIError: If the asset has no such rendition, or a request fails
|
|
522
|
+
"""
|
|
523
|
+
return self._renewing_gone_rendition(
|
|
524
|
+
lambda: self._get_rendition_url_once(asset_href, timeout=timeout, rendition=rendition)
|
|
525
|
+
)
|
|
526
|
+
|
|
527
|
+
def download_original(self, asset_href: str, *, timeout: int | None = None) -> bytes:
|
|
528
|
+
"""The original file of an asset, in memory.
|
|
529
|
+
|
|
530
|
+
Raises:
|
|
531
|
+
APIError: If the request or download fails
|
|
532
|
+
"""
|
|
533
|
+
return self._renewing_gone_rendition(
|
|
534
|
+
lambda: self._download_original_once(asset_href, timeout=timeout)
|
|
535
|
+
)
|
|
536
|
+
|
|
537
|
+
def _renewing_gone_rendition[T](self, attempt: Callable[[], T]) -> T:
|
|
538
|
+
"""Run a rendition *attempt*; if FotoWare says its result is gone, once more.
|
|
539
|
+
|
|
540
|
+
A rendition is prepared in a background task, and FotoWare answers
|
|
541
|
+
410 Gone once that task's result has been cleaned up. The address is
|
|
542
|
+
dead then; asking it again is pointless, but a new rendition request
|
|
543
|
+
starts a new task. Seen on 2026-10-24 for a few renditions in a hundred
|
|
544
|
+
while FotoWare was otherwise healthy. The repeat counts as a retry.
|
|
545
|
+
|
|
546
|
+
A 410 arrives before any byte is written, so a destination file is
|
|
547
|
+
still untouched when the attempt is repeated.
|
|
548
|
+
"""
|
|
549
|
+
try:
|
|
550
|
+
return attempt()
|
|
551
|
+
except APIError as exc:
|
|
552
|
+
if exc.status_code != 410:
|
|
553
|
+
raise
|
|
554
|
+
logger.info("Rendition gone (410); requesting it anew: %s", exc)
|
|
555
|
+
self._counters.count_retry(410, None, 0.0)
|
|
556
|
+
return attempt()
|
|
557
|
+
|
|
558
|
+
def _get_rendition_url_once(
|
|
559
|
+
self,
|
|
560
|
+
asset_href: str,
|
|
561
|
+
*,
|
|
562
|
+
timeout: int | None = None,
|
|
563
|
+
rendition: str | None = None,
|
|
513
564
|
) -> str:
|
|
514
565
|
"""Get download URL using FotoWare rendition API.
|
|
515
566
|
|
|
@@ -671,7 +722,7 @@ class FotoWareClient:
|
|
|
671
722
|
context={"asset_href": asset_href, "polling_url": polling_url},
|
|
672
723
|
)
|
|
673
724
|
|
|
674
|
-
def
|
|
725
|
+
def _download_original_once(self, asset_href: str, *, timeout: int | None = None) -> bytes:
|
|
675
726
|
"""Download original file using FotoWare rendition API (atomic operation).
|
|
676
727
|
|
|
677
728
|
This is the recommended way to download original files. It handles the
|
|
@@ -852,11 +903,13 @@ class FotoWareClient:
|
|
|
852
903
|
APIError: If rendition request or download fails
|
|
853
904
|
NotFoundError: If asset or original rendition not found
|
|
854
905
|
"""
|
|
855
|
-
self.
|
|
856
|
-
self.
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
906
|
+
self._renewing_gone_rendition(
|
|
907
|
+
lambda: self._stream_to_file(
|
|
908
|
+
self._get_rendition_url_once(asset_href, timeout=timeout),
|
|
909
|
+
destination,
|
|
910
|
+
timeout=timeout,
|
|
911
|
+
chunk_size=chunk_size,
|
|
912
|
+
)
|
|
860
913
|
)
|
|
861
914
|
|
|
862
915
|
def download_rendition_to_file(
|
|
@@ -875,11 +928,13 @@ class FotoWareClient:
|
|
|
875
928
|
APIError: If the asset has no such rendition, or its request or
|
|
876
929
|
download fails
|
|
877
930
|
"""
|
|
878
|
-
self.
|
|
879
|
-
self.
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
931
|
+
self._renewing_gone_rendition(
|
|
932
|
+
lambda: self._stream_to_file(
|
|
933
|
+
self._get_rendition_url_once(asset_href, timeout=timeout, rendition=rendition),
|
|
934
|
+
destination,
|
|
935
|
+
timeout=timeout,
|
|
936
|
+
chunk_size=chunk_size,
|
|
937
|
+
)
|
|
883
938
|
)
|
|
884
939
|
|
|
885
940
|
def _stream_to_file(
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-fotoware-client
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.3
|
|
4
4
|
Summary: A thin, honest client for the FotoWare API: one call, one request; refuses queries FotoWare would answer wrongly; counts what each request costs
|
|
5
5
|
Author: Michael Hoppe
|
|
6
6
|
License-Expression: MIT
|
|
@@ -417,7 +417,7 @@ class TestFotoWareClientDownloadOriginalToFile:
|
|
|
417
417
|
with (
|
|
418
418
|
patch.object(
|
|
419
419
|
client,
|
|
420
|
-
"
|
|
420
|
+
"_get_rendition_url_once",
|
|
421
421
|
return_value="https://fotoware.example.com/renditions/file.bin",
|
|
422
422
|
) as mock_get_rendition_url,
|
|
423
423
|
patch.object(
|
|
@@ -461,7 +461,7 @@ class TestFotoWareClientDownloadOriginalToFile:
|
|
|
461
461
|
with (
|
|
462
462
|
patch.object(
|
|
463
463
|
client,
|
|
464
|
-
"
|
|
464
|
+
"_get_rendition_url_once",
|
|
465
465
|
return_value="https://fotoware.example.com/renditions/medium.jpg",
|
|
466
466
|
) as mock_get_rendition_url,
|
|
467
467
|
patch.object(client, "_request", return_value=response),
|
|
@@ -543,3 +543,82 @@ class TestResponsesAreClosed:
|
|
|
543
543
|
|
|
544
544
|
requested.close.assert_called_once()
|
|
545
545
|
requested.json.assert_not_called()
|
|
546
|
+
|
|
547
|
+
|
|
548
|
+
class TestGoneRenditions:
|
|
549
|
+
"""A rendition whose background task is gone (410) is requested anew, once."""
|
|
550
|
+
|
|
551
|
+
@staticmethod
|
|
552
|
+
def _gone() -> APIError:
|
|
553
|
+
return APIError("HTTP 410 error", status_code=410)
|
|
554
|
+
|
|
555
|
+
@patch("fotoware_client.client.OAuthAuthenticator")
|
|
556
|
+
def test_a_gone_rendition_is_requested_anew(self, mock_auth: Mock) -> None:
|
|
557
|
+
client = FotoWareClient("https://fotoware.example.com", "id", "secret")
|
|
558
|
+
destination = io.BytesIO()
|
|
559
|
+
|
|
560
|
+
def stream(url: str, dest: io.BytesIO, **_: object) -> None:
|
|
561
|
+
if url.endswith("/task-1/file.jpg"):
|
|
562
|
+
raise self._gone()
|
|
563
|
+
dest.write(b"jpeg")
|
|
564
|
+
|
|
565
|
+
with (
|
|
566
|
+
patch.object(
|
|
567
|
+
client,
|
|
568
|
+
"_get_rendition_url_once",
|
|
569
|
+
side_effect=["https://f/task-1/file.jpg", "https://f/task-2/file.jpg"],
|
|
570
|
+
) as rendition_url,
|
|
571
|
+
patch.object(client, "_stream_to_file", side_effect=stream),
|
|
572
|
+
):
|
|
573
|
+
client.download_rendition_to_file(
|
|
574
|
+
"/a/1.jpg.info", destination, rendition="Medium (JPG, 3000px)"
|
|
575
|
+
)
|
|
576
|
+
|
|
577
|
+
assert rendition_url.call_count == 2
|
|
578
|
+
assert destination.getvalue() == b"jpeg"
|
|
579
|
+
assert client.cost_snapshot()["retries_by_status"] == {"410": 1}
|
|
580
|
+
|
|
581
|
+
@patch("fotoware_client.client.OAuthAuthenticator")
|
|
582
|
+
def test_a_rendition_gone_twice_is_an_error(self, mock_auth: Mock) -> None:
|
|
583
|
+
client = FotoWareClient("https://fotoware.example.com", "id", "secret")
|
|
584
|
+
|
|
585
|
+
with (
|
|
586
|
+
patch.object(client, "_get_rendition_url_once", return_value="https://f/t/x.jpg"),
|
|
587
|
+
patch.object(client, "_stream_to_file", side_effect=self._gone()) as stream,
|
|
588
|
+
):
|
|
589
|
+
with pytest.raises(APIError) as exc_info:
|
|
590
|
+
client.download_original_to_file("/a/1.jpg.info", io.BytesIO())
|
|
591
|
+
|
|
592
|
+
assert exc_info.value.status_code == 410
|
|
593
|
+
assert stream.call_count == 2
|
|
594
|
+
|
|
595
|
+
@patch("fotoware_client.client.OAuthAuthenticator")
|
|
596
|
+
def test_other_errors_are_not_repeated(self, mock_auth: Mock) -> None:
|
|
597
|
+
client = FotoWareClient("https://fotoware.example.com", "id", "secret")
|
|
598
|
+
|
|
599
|
+
with (
|
|
600
|
+
patch.object(client, "_get_rendition_url_once", return_value="https://f/t/x.jpg"),
|
|
601
|
+
patch.object(
|
|
602
|
+
client,
|
|
603
|
+
"_stream_to_file",
|
|
604
|
+
side_effect=APIError("HTTP 403 error", status_code=403),
|
|
605
|
+
) as stream,
|
|
606
|
+
):
|
|
607
|
+
with pytest.raises(APIError):
|
|
608
|
+
client.download_original_to_file("/a/1.jpg.info", io.BytesIO())
|
|
609
|
+
|
|
610
|
+
assert stream.call_count == 1
|
|
611
|
+
|
|
612
|
+
@patch("fotoware_client.client.OAuthAuthenticator")
|
|
613
|
+
def test_a_rendition_gone_while_polling_is_requested_anew(self, mock_auth: Mock) -> None:
|
|
614
|
+
client = FotoWareClient("https://fotoware.example.com", "id", "secret")
|
|
615
|
+
|
|
616
|
+
with patch.object(
|
|
617
|
+
client,
|
|
618
|
+
"_get_rendition_url_once",
|
|
619
|
+
side_effect=[self._gone(), "https://f/task-2/file.jpg"],
|
|
620
|
+
):
|
|
621
|
+
assert client.get_rendition_url("/a/1.jpg.info") == "https://f/task-2/file.jpg"
|
|
622
|
+
|
|
623
|
+
with patch.object(client, "_download_original_once", side_effect=[self._gone(), b"bytes"]):
|
|
624
|
+
assert client.download_original("/a/1.jpg.info") == b"bytes"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_fotoware_client-0.2.2 → python_fotoware_client-0.2.3}/src/fotoware_client/constants.py
RENAMED
|
File without changes
|
{python_fotoware_client-0.2.2 → python_fotoware_client-0.2.3}/src/fotoware_client/credentials.py
RENAMED
|
File without changes
|
{python_fotoware_client-0.2.2 → python_fotoware_client-0.2.3}/src/fotoware_client/exceptions.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_fotoware_client-0.2.2 → python_fotoware_client-0.2.3}/src/fotoware_client/settings.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|