imgproxykit 0.1.0__py3-none-any.whl

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.
Files changed (62) hide show
  1. imgproxykit/__init__.py +2 -0
  2. imgproxykit/builders.py +132 -0
  3. imgproxykit/client.py +167 -0
  4. imgproxykit/internal/__init__.py +0 -0
  5. imgproxykit/internal/constants.py +8 -0
  6. imgproxykit/internal/empty.py +8 -0
  7. imgproxykit/internal/encoding.py +13 -0
  8. imgproxykit/internal/paths.py +21 -0
  9. imgproxykit/internal/serialization.py +34 -0
  10. imgproxykit/options/__init__.py +62 -0
  11. imgproxykit/options/auto_rotate.py +39 -0
  12. imgproxykit/options/background.py +46 -0
  13. imgproxykit/options/base.py +41 -0
  14. imgproxykit/options/blur.py +38 -0
  15. imgproxykit/options/cache_buster.py +40 -0
  16. imgproxykit/options/crop.py +61 -0
  17. imgproxykit/options/dpr.py +40 -0
  18. imgproxykit/options/enforce_thumbnail.py +39 -0
  19. imgproxykit/options/enlarge.py +38 -0
  20. imgproxykit/options/expires.py +38 -0
  21. imgproxykit/options/extend.py +50 -0
  22. imgproxykit/options/extend_aspect_ratio.py +50 -0
  23. imgproxykit/options/filename.py +41 -0
  24. imgproxykit/options/flip.py +44 -0
  25. imgproxykit/options/format_quality.py +38 -0
  26. imgproxykit/options/gravity.py +48 -0
  27. imgproxykit/options/height.py +39 -0
  28. imgproxykit/options/keep_copyright.py +38 -0
  29. imgproxykit/options/max_animation_frame_resolution.py +39 -0
  30. imgproxykit/options/max_animation_frames.py +39 -0
  31. imgproxykit/options/max_bytes.py +39 -0
  32. imgproxykit/options/max_result_dimension.py +39 -0
  33. imgproxykit/options/max_src_file_size.py +39 -0
  34. imgproxykit/options/max_src_resolution.py +39 -0
  35. imgproxykit/options/min_height.py +38 -0
  36. imgproxykit/options/min_width.py +38 -0
  37. imgproxykit/options/padding.py +50 -0
  38. imgproxykit/options/pixelate.py +38 -0
  39. imgproxykit/options/preserve_hdr.py +38 -0
  40. imgproxykit/options/quality.py +38 -0
  41. imgproxykit/options/raw.py +39 -0
  42. imgproxykit/options/resize.py +55 -0
  43. imgproxykit/options/resizing_type.py +39 -0
  44. imgproxykit/options/return_attachment.py +39 -0
  45. imgproxykit/options/rotate.py +38 -0
  46. imgproxykit/options/sharpen.py +38 -0
  47. imgproxykit/options/size.py +45 -0
  48. imgproxykit/options/skip_processing.py +39 -0
  49. imgproxykit/options/strip_color_profile.py +39 -0
  50. imgproxykit/options/strip_metadata.py +38 -0
  51. imgproxykit/options/trim.py +52 -0
  52. imgproxykit/options/watermark.py +60 -0
  53. imgproxykit/options/width.py +39 -0
  54. imgproxykit/options/zoom.py +45 -0
  55. imgproxykit/py.typed +0 -0
  56. imgproxykit/signing.py +114 -0
  57. imgproxykit/transform.py +54 -0
  58. imgproxykit/types.py +95 -0
  59. imgproxykit-0.1.0.dist-info/METADATA +150 -0
  60. imgproxykit-0.1.0.dist-info/RECORD +62 -0
  61. imgproxykit-0.1.0.dist-info/WHEEL +4 -0
  62. imgproxykit-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,2 @@
1
+ from imgproxykit.client import Client as Client
2
+ from imgproxykit.transform import Transform as Transform
@@ -0,0 +1,132 @@
1
+ from imgproxykit.internal.constants import PRESET
2
+ from imgproxykit.internal.paths import build_path
3
+ from imgproxykit.internal.serialization import (
4
+ serialize_option,
5
+ serialize_segment,
6
+ )
7
+ from imgproxykit.transform import Transform
8
+ from imgproxykit.types import EMPTY, FormatType
9
+
10
+
11
+ class PathBuilder:
12
+ """
13
+ Resolve processing options and build an unsigned imgproxy path.
14
+
15
+ Used by :class:`~imgproxykit.Client` before signing the resulting path.
16
+ Sources are Base64-encoded without padding; processing options and an
17
+ optional output extension are included in the same path.
18
+
19
+ Python transforms and server presets are mutually exclusive.
20
+ Preset definitions remain on the imgproxy server.
21
+
22
+ Subclass this builder and override :meth:`__call__` to customize path
23
+ construction. Set the subclass on
24
+ :attr:`imgproxykit.Client.path_builder_cls` to use it in a client.
25
+
26
+ """
27
+
28
+ __slots__ = ()
29
+
30
+ def __call__(
31
+ self,
32
+ source: str,
33
+ *,
34
+ transform: Transform | EMPTY = EMPTY,
35
+ presets: tuple[str, ...] | EMPTY = EMPTY,
36
+ output_format: FormatType | EMPTY = EMPTY,
37
+ ) -> str:
38
+ """
39
+ Build a path from a Python transform or server preset names.
40
+
41
+ Args:
42
+ source: Complete source URI understood by imgproxy. Its contents
43
+ are encoded as supplied, without resolving relative paths.
44
+ transform: Processing options and output format defined in Python.
45
+ Omit together with ``presets`` to add no processing options.
46
+ presets: Server preset names in the order they should be supplied.
47
+ Cannot be combined with ``transform``.
48
+ output_format: Output extension when using ``presets``.
49
+ For a Python transform, set its ``format`` instead.
50
+
51
+ Returns:
52
+ Unsigned path beginning with ``/``, ready for signing.
53
+ Contains neither the imgproxy address nor a signature segment.
54
+
55
+ Raises:
56
+ ValueError: If ``transform`` and ``presets`` are both supplied,
57
+ or ``output_format`` is supplied without ``presets``.
58
+
59
+ Usage:
60
+
61
+ >>> from imgproxykit import Transform
62
+ >>> from imgproxykit.builders import PathBuilder
63
+ >>> from imgproxykit.options import Resize
64
+ >>> builder = PathBuilder()
65
+ >>> source = 'https://example.com/photo.jpg'
66
+ >>> thumbnail = Transform(
67
+ ... options=(Resize(width=96, height=96, mode='fill'),),
68
+ ... format='webp',
69
+ ... )
70
+ >>> local_path = builder(source, transform=thumbnail)
71
+ >>> preset_path = builder(
72
+ ... source,
73
+ ... presets=('thumbnail',),
74
+ ... output_format='webp',
75
+ ... )
76
+
77
+ """
78
+ options, selected_format = self._resolve_options(
79
+ transform,
80
+ presets,
81
+ output_format,
82
+ )
83
+ return build_path(
84
+ source,
85
+ options=options,
86
+ output_format=selected_format,
87
+ )
88
+
89
+ def _resolve_options(
90
+ self,
91
+ transform: Transform | EMPTY,
92
+ presets: tuple[str, ...] | EMPTY,
93
+ output_format: FormatType | EMPTY,
94
+ ) -> tuple[tuple[str, ...], FormatType | EMPTY]:
95
+ """
96
+ Resolve mutually exclusive inputs into path segments and a format.
97
+
98
+ Python options are serialized in their supplied order. Server preset
99
+ names form a single ``pr`` segment. Omitted processing parameters
100
+ produce no segments and leave the output format unspecified.
101
+
102
+ """
103
+ self._validate_options(transform, presets, output_format)
104
+ if presets is not EMPTY:
105
+ return (serialize_segment(PRESET, presets),), output_format
106
+
107
+ if transform is EMPTY:
108
+ return (), EMPTY
109
+
110
+ return (
111
+ tuple(serialize_option(option) for option in transform.options),
112
+ transform.format,
113
+ )
114
+
115
+ def _validate_options(
116
+ self,
117
+ transform: Transform | EMPTY,
118
+ presets: tuple[str, ...] | EMPTY,
119
+ output_format: FormatType | EMPTY,
120
+ ) -> None:
121
+ """
122
+ Validate processing options before path serialization.
123
+
124
+ Python transforms cannot be combined with server presets.
125
+ A separate output format is only accepted with server presets.
126
+
127
+ """
128
+ if transform is not EMPTY and presets is not EMPTY:
129
+ raise ValueError('Choose transform or presets.')
130
+
131
+ if presets is EMPTY and output_format is not EMPTY:
132
+ raise ValueError('Set format on the Python transform.')
imgproxykit/client.py ADDED
@@ -0,0 +1,167 @@
1
+ from typing import ClassVar, overload
2
+
3
+ from imgproxykit.builders import PathBuilder
4
+ from imgproxykit.signing import URLSigner
5
+ from imgproxykit.transform import Transform
6
+ from imgproxykit.types import EMPTY, FormatType
7
+
8
+
9
+ class Client:
10
+ """
11
+ Build imgproxy URLs from Python transforms or server presets.
12
+
13
+ Stores the imgproxy address and signing configuration for reuse across
14
+ images. Each call to :meth:`url` returns a URL string; imgproxy loads
15
+ the source and processes the image when that URL is requested.
16
+
17
+ Processing parameters can come from a :class:`~imgproxykit.Transform`
18
+ defined in Python or from preset names configured on the imgproxy server.
19
+
20
+ Subclass this client and override :attr:`path_builder_cls` or
21
+ :attr:`signer_cls` to customize path construction or signing.
22
+ Each client creates its own instances of these components.
23
+
24
+ """
25
+
26
+ __slots__ = (
27
+ '_base_url',
28
+ '_path_builder',
29
+ '_signer',
30
+ )
31
+
32
+ #: Component that resolves processing parameters and builds unsigned paths.
33
+ path_builder_cls: ClassVar[type[PathBuilder]] = PathBuilder
34
+ #: Component that signs paths or returns the unsigned URL marker.
35
+ signer_cls: ClassVar[type[URLSigner]] = URLSigner
36
+
37
+ @overload
38
+ def __init__(self, base_url: str, /) -> None: ...
39
+
40
+ @overload
41
+ def __init__(self, base_url: str, /, *, key: str, salt: str) -> None: ...
42
+
43
+ def __init__(
44
+ self,
45
+ base_url: str,
46
+ /,
47
+ *,
48
+ key: str | EMPTY = EMPTY,
49
+ salt: str | EMPTY = EMPTY,
50
+ ) -> None:
51
+ """
52
+ Configure the imgproxy address and create the client components.
53
+
54
+ Supply both ``key`` and ``salt`` for signed URLs. Omit both for
55
+ URLs with an ``unsafe`` signature marker, which can be used with
56
+ an imgproxy server that has signature checking disabled.
57
+
58
+ Args:
59
+ base_url: Public imgproxy address, including any deployment path
60
+ prefix. Required positional argument. Trailing slashes
61
+ are removed.
62
+ key: Hex-encoded signing key matching the server configuration.
63
+ salt: Hex-encoded signing salt matching the server configuration.
64
+
65
+ Raises:
66
+ ValueError: With the default signer, if only one signing
67
+ credential is supplied, or either is empty or invalid hex.
68
+
69
+ Usage:
70
+
71
+ >>> from imgproxykit import Client
72
+ >>> unsigned = Client('https://images.example.com')
73
+ >>> signed = Client(
74
+ ... 'https://images.example.com',
75
+ ... key='736563726574',
76
+ ... salt='73616c74',
77
+ ... )
78
+
79
+ The signing credentials above are examples for local development.
80
+
81
+ """
82
+ # TODO: Define runtime validation for base URLs and empty sources.
83
+ self._base_url = base_url.rstrip('/')
84
+ self._path_builder = self.path_builder_cls()
85
+ self._signer = self.signer_cls(key=key, salt=salt)
86
+
87
+ @overload
88
+ def url(
89
+ self,
90
+ source: str,
91
+ *,
92
+ transform: Transform | EMPTY = ...,
93
+ ) -> str: ...
94
+
95
+ @overload
96
+ def url(
97
+ self,
98
+ source: str,
99
+ *,
100
+ presets: tuple[str, ...],
101
+ format: FormatType | EMPTY = EMPTY,
102
+ ) -> str: ...
103
+
104
+ def url(
105
+ self,
106
+ source: str,
107
+ *,
108
+ transform: Transform | EMPTY = EMPTY,
109
+ presets: tuple[str, ...] | EMPTY = EMPTY,
110
+ format: FormatType | EMPTY = EMPTY, # noqa: A002
111
+ ) -> str:
112
+ """
113
+ Build a URL for one source using a transform or server presets.
114
+
115
+ Pass ``transform`` for processing parameters defined in Python.
116
+ Pass ``presets`` to reference named server presets instead.
117
+ Preset definitions remain on the server and are not expanded here.
118
+
119
+ With a Python transform, set the output format on that transform.
120
+ With server presets, use the ``format`` argument. Omitted processing
121
+ parameters and formats are left to the server.
122
+
123
+ Args:
124
+ source: Complete source URI understood by the imgproxy server.
125
+ The default builder Base64-encodes it without changing its
126
+ contents or resolving storage-relative paths.
127
+ transform: Reusable processing options and optional output format.
128
+ Omitting both ``transform`` and ``presets`` adds no options.
129
+ presets: Names of server presets, in the supplied order.
130
+ Mutually exclusive with ``transform``.
131
+ format: Output format when using ``presets``. Must be omitted
132
+ when using a Python transform.
133
+
134
+ Returns:
135
+ The complete imgproxy URL, signed when credentials were supplied.
136
+
137
+ Raises:
138
+ ValueError: With the default builder, if ``transform`` and
139
+ ``presets`` are supplied together, or ``format`` is supplied
140
+ without ``presets``.
141
+
142
+ Usage:
143
+
144
+ >>> from imgproxykit import Client, Transform
145
+ >>> from imgproxykit.options import Resize
146
+ >>> client = Client('https://images.example.com')
147
+ >>> thumbnail = Transform(
148
+ ... options=(Resize(width=300, height=300, mode='fill'),),
149
+ ... format='webp',
150
+ ... )
151
+ >>> source = 'https://example.com/photo.jpg'
152
+ >>> local_options_url = client.url(source, transform=thumbnail)
153
+ >>> server_presets_url = client.url(
154
+ ... source,
155
+ ... presets=('thumbnail', 'watermarked'),
156
+ ... format='webp',
157
+ ... )
158
+
159
+ """
160
+ path = self._path_builder(
161
+ source,
162
+ transform=transform,
163
+ presets=presets,
164
+ output_format=format,
165
+ )
166
+ signature = self._signer(path)
167
+ return f'{self._base_url}/{signature}{path}'
File without changes
@@ -0,0 +1,8 @@
1
+ from typing import Final
2
+
3
+ # imgproxy verifies URL signatures using HMAC-SHA256.
4
+ SIGNATURE_DIGEST: Final = 'sha256'
5
+ # A URL still needs a signature segment when server signature checking is off.
6
+ UNSAFE: Final = 'unsafe'
7
+ # imgproxy's short option name for referencing server-side presets.
8
+ PRESET: Final = 'pr'
@@ -0,0 +1,8 @@
1
+ from typing_extensions import Sentinel
2
+
3
+ # NOTE: this module must not import anything from `imgproxykit`,
4
+ # so it can be used anywhere without creating import cycles.
5
+
6
+ #: Default singleton for empty values.
7
+ #: Re-exported as ``imgproxykit.types.EMPTY``.
8
+ EMPTY = Sentinel('EMPTY')
@@ -0,0 +1,13 @@
1
+ import base64
2
+
3
+
4
+ def encode_base64(value: str | bytes) -> str:
5
+ """
6
+ Encode text or bytes as URL-safe Base64 without padding.
7
+
8
+ Text is first encoded as UTF-8. Byte values, such as signature digests,
9
+ are encoded directly. Trailing ``=`` padding is removed for URL paths.
10
+
11
+ """
12
+ raw = value.encode() if isinstance(value, str) else value
13
+ return base64.urlsafe_b64encode(raw).rstrip(b'=').decode()
@@ -0,0 +1,21 @@
1
+ from imgproxykit.internal.encoding import encode_base64
2
+ from imgproxykit.types import EMPTY, FormatType
3
+
4
+
5
+ def build_path(
6
+ source: str,
7
+ *,
8
+ options: tuple[str, ...],
9
+ output_format: FormatType | EMPTY = EMPTY,
10
+ ) -> str:
11
+ """
12
+ Assemble serialized options, an encoded source, and an output extension.
13
+
14
+ The leading ``/`` is part of the path supplied to the signer.
15
+ The source is encoded as URL-safe Base64 without padding. An omitted
16
+ output format adds no extension, leaving format selection to imgproxy.
17
+
18
+ """
19
+ segments = '/'.join((*options, encode_base64(source)))
20
+ path = f'/{segments}'
21
+ return path if output_format is EMPTY else f'{path}.{output_format}'
@@ -0,0 +1,34 @@
1
+ from imgproxykit.options import Option
2
+ from imgproxykit.types import EMPTY, Argument
3
+
4
+
5
+ def serialize_option(option: Option) -> str:
6
+ """Serialize a typed processing option into an imgproxy path segment."""
7
+ return serialize_segment(option.name, option.arguments)
8
+
9
+
10
+ def serialize_segment(name: str, arguments: tuple[Argument, ...]) -> str:
11
+ """
12
+ Serialize an option name and positional arguments into a path segment.
13
+
14
+ Internal :data:`~imgproxykit.types.EMPTY` values become empty positions;
15
+ trailing empty values are omitted. Explicit zeroes and empty strings
16
+ retain their positions. Booleans are serialized as ``1`` and ``0``.
17
+
18
+ """
19
+ # TODO: Verify reserved characters in string arguments and preset names
20
+ # against imgproxy before choosing encoding or rejection. Cover colons,
21
+ # slashes, percent signs, Unicode, and empty strings.
22
+ end = len(arguments)
23
+ while end and arguments[end - 1] is EMPTY:
24
+ end -= 1
25
+ return ':'.join((name, *map(_serialize_argument, arguments[:end])))
26
+
27
+
28
+ def _serialize_argument(value: Argument) -> str:
29
+ """Serialize one argument without treating false values as omitted."""
30
+ if value is EMPTY:
31
+ return ''
32
+ if isinstance(value, bool):
33
+ return str(int(value))
34
+ return str(value)
@@ -0,0 +1,62 @@
1
+ from imgproxykit.options.auto_rotate import AutoRotate as AutoRotate
2
+ from imgproxykit.options.background import Background as Background
3
+ from imgproxykit.options.base import Option as Option
4
+ from imgproxykit.options.blur import Blur as Blur
5
+ from imgproxykit.options.cache_buster import CacheBuster as CacheBuster
6
+ from imgproxykit.options.crop import Crop as Crop
7
+ from imgproxykit.options.dpr import DPR as DPR
8
+ from imgproxykit.options.enforce_thumbnail import (
9
+ EnforceThumbnail as EnforceThumbnail,
10
+ )
11
+ from imgproxykit.options.enlarge import Enlarge as Enlarge
12
+ from imgproxykit.options.expires import Expires as Expires
13
+ from imgproxykit.options.extend import Extend as Extend
14
+ from imgproxykit.options.extend_aspect_ratio import (
15
+ ExtendAspectRatio as ExtendAspectRatio,
16
+ )
17
+ from imgproxykit.options.filename import Filename as Filename
18
+ from imgproxykit.options.flip import Flip as Flip
19
+ from imgproxykit.options.format_quality import FormatQuality as FormatQuality
20
+ from imgproxykit.options.gravity import Gravity as Gravity
21
+ from imgproxykit.options.height import Height as Height
22
+ from imgproxykit.options.keep_copyright import KeepCopyright as KeepCopyright
23
+ from imgproxykit.options.max_animation_frame_resolution import (
24
+ MaxAnimationFrameResolution as MaxAnimationFrameResolution,
25
+ )
26
+ from imgproxykit.options.max_animation_frames import (
27
+ MaxAnimationFrames as MaxAnimationFrames,
28
+ )
29
+ from imgproxykit.options.max_bytes import MaxBytes as MaxBytes
30
+ from imgproxykit.options.max_result_dimension import (
31
+ MaxResultDimension as MaxResultDimension,
32
+ )
33
+ from imgproxykit.options.max_src_file_size import (
34
+ MaxSrcFileSize as MaxSrcFileSize,
35
+ )
36
+ from imgproxykit.options.max_src_resolution import (
37
+ MaxSrcResolution as MaxSrcResolution,
38
+ )
39
+ from imgproxykit.options.min_height import MinHeight as MinHeight
40
+ from imgproxykit.options.min_width import MinWidth as MinWidth
41
+ from imgproxykit.options.padding import Padding as Padding
42
+ from imgproxykit.options.pixelate import Pixelate as Pixelate
43
+ from imgproxykit.options.preserve_hdr import PreserveHDR as PreserveHDR
44
+ from imgproxykit.options.quality import Quality as Quality
45
+ from imgproxykit.options.raw import Raw as Raw
46
+ from imgproxykit.options.resize import Resize as Resize
47
+ from imgproxykit.options.resizing_type import ResizingType as ResizingType
48
+ from imgproxykit.options.return_attachment import (
49
+ ReturnAttachment as ReturnAttachment,
50
+ )
51
+ from imgproxykit.options.rotate import Rotate as Rotate
52
+ from imgproxykit.options.sharpen import Sharpen as Sharpen
53
+ from imgproxykit.options.size import Size as Size
54
+ from imgproxykit.options.skip_processing import SkipProcessing as SkipProcessing
55
+ from imgproxykit.options.strip_color_profile import (
56
+ StripColorProfile as StripColorProfile,
57
+ )
58
+ from imgproxykit.options.strip_metadata import StripMetadata as StripMetadata
59
+ from imgproxykit.options.trim import Trim as Trim
60
+ from imgproxykit.options.watermark import Watermark as Watermark
61
+ from imgproxykit.options.width import Width as Width
62
+ from imgproxykit.options.zoom import Zoom as Zoom
@@ -0,0 +1,39 @@
1
+ from dataclasses import dataclass
2
+ from typing import ClassVar, final, override
3
+
4
+ from imgproxykit.options.base import Option
5
+ from imgproxykit.types import Argument, OptionType
6
+
7
+
8
+ @final
9
+ @dataclass(frozen=True, slots=True, kw_only=True)
10
+ class AutoRotate(Option):
11
+ """
12
+ Apply the source EXIF orientation before processing.
13
+
14
+ Overrides automatic EXIF orientation handling for this request.
15
+ The orientation tag is removed regardless of this setting.
16
+
17
+ Attributes:
18
+ value: Whether to apply the source EXIF orientation.
19
+
20
+ Usage:
21
+
22
+ >>> option = AutoRotate(value=False)
23
+ >>> option.arguments
24
+ (False,)
25
+
26
+ See `imgproxy auto rotate
27
+ <https://docs.imgproxy.net/usage/processing#auto-rotate>`_.
28
+
29
+ """
30
+
31
+ value: bool
32
+
33
+ _name: ClassVar[OptionType] = 'ar'
34
+
35
+ @property
36
+ @override
37
+ def arguments(self) -> tuple[Argument, ...]:
38
+ """Positional arguments in imgproxy protocol order."""
39
+ return (self.value,)
@@ -0,0 +1,46 @@
1
+ from dataclasses import dataclass
2
+ from typing import ClassVar, final, override
3
+
4
+ from imgproxykit.options.base import Option
5
+ from imgproxykit.types import EMPTY, Argument, OptionType
6
+
7
+
8
+ @final
9
+ @dataclass(frozen=True, slots=True, kw_only=True)
10
+ class Background(Option):
11
+ """
12
+ Fill the background with a hex color or RGB tuple; omit to disable.
13
+
14
+ Useful when converting transparent images to JPEG. Omit the color
15
+ to disable background manipulation.
16
+
17
+ Attributes:
18
+ color: Hex color without a leading hash, or an RGB tuple with
19
+ channel values from 0 to 255. Omission disables filling.
20
+
21
+ Usage:
22
+
23
+ >>> option = Background(color='ffffff')
24
+ >>> option.arguments
25
+ ('ffffff',)
26
+ >>> option = Background(color=(255, 255, 255))
27
+ >>> option.arguments
28
+ (255, 255, 255)
29
+ >>> option = Background()
30
+ >>> option.arguments
31
+ (EMPTY,)
32
+
33
+ See `imgproxy background
34
+ <https://docs.imgproxy.net/usage/processing#background>`_.
35
+
36
+ """
37
+
38
+ color: str | tuple[int, int, int] | EMPTY = EMPTY
39
+
40
+ _name: ClassVar[OptionType] = 'bg'
41
+
42
+ @property
43
+ @override
44
+ def arguments(self) -> tuple[Argument, ...]:
45
+ """Positional arguments in imgproxy protocol order."""
46
+ return self.color if isinstance(self.color, tuple) else (self.color,)
@@ -0,0 +1,41 @@
1
+ from abc import ABC, abstractmethod
2
+ from typing import ClassVar
3
+
4
+ from imgproxykit.types import Argument, OptionType
5
+
6
+
7
+ class Option(ABC):
8
+ """
9
+ Base class for typed imgproxy processing options.
10
+
11
+ Concrete options provide a fixed name and positional arguments.
12
+ Pass their instances to :class:`~imgproxykit.Transform`.
13
+ This abstract class cannot be instantiated directly.
14
+
15
+ Arguments retain :data:`~imgproxykit.types.EMPTY` values until
16
+ serialization: internal gaps keep their positions, while trailing
17
+ empty arguments are omitted.
18
+
19
+ See `imgproxy processing options
20
+ <https://docs.imgproxy.net/usage/processing#processing-options>`_.
21
+
22
+ """
23
+
24
+ __slots__ = ()
25
+
26
+ # TODO: Define runtime validation boundaries for processing option values.
27
+ # TODO: Allow custom option names without type ignores; decide whether
28
+ # OptionType should remain limited to built-in processing options.
29
+
30
+ # Private API:
31
+ _name: ClassVar[OptionType]
32
+
33
+ @property
34
+ def name(self) -> OptionType:
35
+ """The fixed imgproxy processing option name."""
36
+ return self._name
37
+
38
+ @property
39
+ @abstractmethod
40
+ def arguments(self) -> tuple[Argument, ...]:
41
+ """Positional arguments in imgproxy protocol order."""
@@ -0,0 +1,38 @@
1
+ from dataclasses import dataclass
2
+ from typing import ClassVar, final, override
3
+
4
+ from imgproxykit.options.base import Option
5
+ from imgproxykit.types import Argument, OptionType
6
+
7
+
8
+ @final
9
+ @dataclass(frozen=True, slots=True, kw_only=True)
10
+ class Blur(Option):
11
+ """
12
+ Apply Gaussian blur with the given sigma.
13
+
14
+ The sigma controls the size of the Gaussian blur mask.
15
+
16
+ Attributes:
17
+ value: Gaussian blur sigma.
18
+
19
+ Usage:
20
+
21
+ >>> option = Blur(value=2.0)
22
+ >>> option.arguments
23
+ (2.0,)
24
+
25
+ See `imgproxy blur
26
+ <https://docs.imgproxy.net/usage/processing#blur>`_.
27
+
28
+ """
29
+
30
+ value: float
31
+
32
+ _name: ClassVar[OptionType] = 'bl'
33
+
34
+ @property
35
+ @override
36
+ def arguments(self) -> tuple[Argument, ...]:
37
+ """Positional arguments in imgproxy protocol order."""
38
+ return (self.value,)
@@ -0,0 +1,40 @@
1
+ from dataclasses import dataclass
2
+ from typing import ClassVar, final, override
3
+ from urllib.parse import quote
4
+
5
+ from imgproxykit.options.base import Option
6
+ from imgproxykit.types import Argument, OptionType
7
+
8
+
9
+ @final
10
+ @dataclass(frozen=True, slots=True, kw_only=True)
11
+ class CacheBuster(Option):
12
+ """
13
+ Add an escaped version string to the signed URL to bypass caches.
14
+
15
+ Changing the value produces a new URL for browser and CDN caches
16
+ without changing image processing. Special characters are escaped.
17
+
18
+ Attributes:
19
+ value: Cache version string included in the signed path.
20
+
21
+ Usage:
22
+
23
+ >>> option = CacheBuster(value='v2')
24
+ >>> option.arguments
25
+ ('v2',)
26
+
27
+ See `imgproxy cache buster
28
+ <https://docs.imgproxy.net/usage/processing#cache-buster>`_.
29
+
30
+ """
31
+
32
+ value: str
33
+
34
+ _name: ClassVar[OptionType] = 'cb'
35
+
36
+ @property
37
+ @override
38
+ def arguments(self) -> tuple[Argument, ...]:
39
+ """Positional arguments in imgproxy protocol order."""
40
+ return (quote(self.value, safe=''),)