from collections.abc import Mapping
from typing import TYPE_CHECKING, ClassVar, overload
from imgproxykit.serialization import PathSerializer
from imgproxykit.signing import UrlSigner
from imgproxykit.transform import Transform
from imgproxykit.types import SourceEncoding
from imgproxykit.variants import DataclassVariantsResolver
if TYPE_CHECKING:
from _typeshed import DataclassInstance
type _VariantsResult[Key: str] = (
dict[Key, str] | dict[str, str] | DataclassInstance
)
[docs]
class Client:
"""
Build imgproxy URLs from Python transforms or server presets.
Stores the imgproxy address and signing configuration for reuse across
images. Each call to :meth:`url` returns a URL string; imgproxy loads
the source and processes the image when that URL is requested.
Pass processing options through :class:`~imgproxykit.Transform` as
option instances or scalar keyword arguments. Preset options reference
names configured on the imgproxy server.
Subclass this client and override :attr:`path_serializer_cls`,
:attr:`url_signer_cls`, or :attr:`variants_resolver_cls` to customize
path construction, signing, or dataclass declaration resolution.
Each client creates its own serializer and signer. A variants resolver
is created for each dataclass declaration passed to :meth:`variants`.
"""
__slots__ = (
'_base_url',
'_path_serializer',
'_url_signer',
)
#: Component that resolves processing parameters and builds unsigned paths.
path_serializer_cls: ClassVar[type[PathSerializer]] = PathSerializer
#: Component that signs paths or returns the unsigned URL marker.
url_signer_cls: ClassVar[type[UrlSigner]] = UrlSigner
#: Component that validates dataclass variant declarations.
variants_resolver_cls: ClassVar[type[DataclassVariantsResolver]] = (
DataclassVariantsResolver
)
@overload
def __init__(
self,
base_url: str,
/,
*,
key: None = None,
salt: None = None,
source_encoding: SourceEncoding = 'base64',
unsigned_marker: str = 'unsafe',
argument_separator: str = ':',
use_short_options: bool = False,
) -> None: ...
@overload
def __init__(
self,
base_url: str,
/,
*,
key: str,
salt: str,
source_encoding: SourceEncoding = 'base64',
signature_size: int = 32,
argument_separator: str = ':',
use_short_options: bool = False,
) -> None: ...
def __init__( # noqa: WPS211
self,
base_url: str,
/,
*,
key: str | None = None,
salt: str | None = None,
source_encoding: SourceEncoding = 'base64',
signature_size: int = 32,
unsigned_marker: str = 'unsafe',
argument_separator: str = ':',
use_short_options: bool = False,
) -> None:
"""
Configure the imgproxy address and create the client components.
Supply both ``key`` and ``salt`` for signed URLs. Omit both for
URLs with the configured unsigned marker (``unsafe`` by default).
The server must accept unsigned requests or trust that marker.
Args:
base_url: Public imgproxy address, including any deployment path
prefix. Trailing slashes are removed; the address is otherwise
stored as supplied, without validation.
key: Nonempty hex-encoded signing key matching the server.
Supply together with ``salt``; ``None`` selects unsigned mode.
See `IMGPROXY_KEY
<https://docs.imgproxy.net/configuration/options#url-signature>`__.
salt: Nonempty hex-encoded signing salt matching the server.
Supply together with ``key``; ``None`` selects unsigned mode.
See `IMGPROXY_SALT
<https://docs.imgproxy.net/configuration/options#url-signature>`__.
signature_size: Positive digest byte count to encode. Must match
the server's `IMGPROXY_SIGNATURE_SIZE
<https://docs.imgproxy.net/latest/configuration/options#url-signature>`__.
Defaults to ``32``.
Values of 32 or more use the full digest.
unsigned_marker: URL marker used when key and salt are omitted.
Defaults to ``'unsafe'``. With server signing enabled,
configure this marker in `IMGPROXY_TRUSTED_SIGNATURES
<https://docs.imgproxy.net/latest/configuration/options#url-signature>`__.
argument_separator: Separator between processing option names
and their arguments. Must match the server's
`IMGPROXY_ARGUMENTS_SEPARATOR
<https://docs.imgproxy.net/latest/configuration/options#miscellaneous>`__.
Defaults to ``':'``.
source_encoding: Source URL representation: ``'base64'`` or
``'plain'`` with full percent-encoding. See
`imgproxy source URLs
<https://docs.imgproxy.net/usage/processing#source-url>`__.
Defaults to ``'base64'``.
use_short_options: Use short processing option aliases when
available, falling back to full names. Defaults to ``False``.
Raises:
ValueError: With the default signer, if only one signing
credential is supplied, either is empty or invalid hex,
or ``signature_size`` is not positive, even in unsigned mode.
Examples:
Create an unsigned client:
>>> from imgproxykit import Client
>>> client = Client('https://imgproxy.example.com')
Create a signed client with example credentials:
>>> client = Client(
... 'https://imgproxy.example.com',
... key='736563726574',
... salt='73616c74',
... )
"""
# Prepare `base_url` only once pre instance
self._base_url = base_url.rstrip('/')
self._path_serializer = self.path_serializer_cls(
argument_separator=argument_separator,
use_short_options=use_short_options,
source_encoding=source_encoding,
)
self._url_signer = self.url_signer_cls(
key=key,
salt=salt,
signature_size=signature_size,
unsigned_marker=unsigned_marker,
)
[docs]
def url(
self,
source: str,
*,
transform: Transform | None = None,
) -> str:
"""
Build a URL using processing options and server preset references.
Pass option instances or scalar keyword arguments through
:class:`~imgproxykit.Transform`. Use
:class:`~imgproxykit.options.Preset` to reference server presets.
Preset definitions remain on the server and are not expanded here.
Set the output format with ``Transform(format='webp')`` or a
:class:`~imgproxykit.options.Format` option. Omitted processing
parameters are left to the server.
Args:
source: Complete source URI understood by the imgproxy server.
The default serializer uses URL-safe Base64 without padding
or percent-encoding according to ``source_encoding``. The URI
is neither validated nor resolved against ``base_url``.
transform: Reusable processing options and optional output format.
Omit or pass ``None`` to add no processing options.
Returns:
The complete imgproxy URL, signed when credentials were supplied
or containing the unsigned marker otherwise. No network request
is made.
"""
path = self._path_serializer(source, transform=transform)
signature = self._url_signer(path)
return f'{self._base_url}/{signature}{path}'
@overload
def variants[Key: str](
self,
source: str,
*,
variants: Mapping[Key, Transform],
) -> dict[Key, str]: ...
@overload
def variants[VariantSet: DataclassInstance](
self,
source: str,
*,
variants: type[VariantSet],
) -> VariantSet: ...
[docs]
def variants[Key: str](
self,
source: str,
*,
variants: Mapping[Key, Transform] | type['DataclassInstance'],
) -> _VariantsResult[Key]:
"""
Build named URLs for one source using complete transformations.
Args:
source: Complete source URI understood by the imgproxy server.
variants: Names mapped to the transformation for each variant.
Alternatively, a dataclass class with fields declared as
``Annotated[str, Transform(...)]``. Each transformation is
passed unchanged to :meth:`url`.
Returns:
A new dictionary preserving the input keys and iteration order.
The key type is preserved, including literal string unions.
An empty mapping produces an empty dictionary.
A dataclass class produces a new instance of that exact class,
with a generated URL in every field.
No network request is made.
"""
transforms = (
variants
if isinstance(variants, Mapping)
else self.variants_resolver_cls(model=variants)()
)
urls = {
name: self.url(source, transform=transform) # ty: ignore[invalid-argument-type]
for name, transform in transforms.items()
}
return urls if isinstance(variants, Mapping) else variants(**urls) # ty: ignore[invalid-return-type, invalid-argument-type]