imgproxykit 0.1.0__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.
- imgproxykit-0.1.0/LICENSE +21 -0
- imgproxykit-0.1.0/PKG-INFO +150 -0
- imgproxykit-0.1.0/README.md +121 -0
- imgproxykit-0.1.0/imgproxykit/__init__.py +2 -0
- imgproxykit-0.1.0/imgproxykit/builders.py +132 -0
- imgproxykit-0.1.0/imgproxykit/client.py +167 -0
- imgproxykit-0.1.0/imgproxykit/internal/__init__.py +0 -0
- imgproxykit-0.1.0/imgproxykit/internal/constants.py +8 -0
- imgproxykit-0.1.0/imgproxykit/internal/empty.py +8 -0
- imgproxykit-0.1.0/imgproxykit/internal/encoding.py +13 -0
- imgproxykit-0.1.0/imgproxykit/internal/paths.py +21 -0
- imgproxykit-0.1.0/imgproxykit/internal/serialization.py +34 -0
- imgproxykit-0.1.0/imgproxykit/options/__init__.py +62 -0
- imgproxykit-0.1.0/imgproxykit/options/auto_rotate.py +39 -0
- imgproxykit-0.1.0/imgproxykit/options/background.py +46 -0
- imgproxykit-0.1.0/imgproxykit/options/base.py +41 -0
- imgproxykit-0.1.0/imgproxykit/options/blur.py +38 -0
- imgproxykit-0.1.0/imgproxykit/options/cache_buster.py +40 -0
- imgproxykit-0.1.0/imgproxykit/options/crop.py +61 -0
- imgproxykit-0.1.0/imgproxykit/options/dpr.py +40 -0
- imgproxykit-0.1.0/imgproxykit/options/enforce_thumbnail.py +39 -0
- imgproxykit-0.1.0/imgproxykit/options/enlarge.py +38 -0
- imgproxykit-0.1.0/imgproxykit/options/expires.py +38 -0
- imgproxykit-0.1.0/imgproxykit/options/extend.py +50 -0
- imgproxykit-0.1.0/imgproxykit/options/extend_aspect_ratio.py +50 -0
- imgproxykit-0.1.0/imgproxykit/options/filename.py +41 -0
- imgproxykit-0.1.0/imgproxykit/options/flip.py +44 -0
- imgproxykit-0.1.0/imgproxykit/options/format_quality.py +38 -0
- imgproxykit-0.1.0/imgproxykit/options/gravity.py +48 -0
- imgproxykit-0.1.0/imgproxykit/options/height.py +39 -0
- imgproxykit-0.1.0/imgproxykit/options/keep_copyright.py +38 -0
- imgproxykit-0.1.0/imgproxykit/options/max_animation_frame_resolution.py +39 -0
- imgproxykit-0.1.0/imgproxykit/options/max_animation_frames.py +39 -0
- imgproxykit-0.1.0/imgproxykit/options/max_bytes.py +39 -0
- imgproxykit-0.1.0/imgproxykit/options/max_result_dimension.py +39 -0
- imgproxykit-0.1.0/imgproxykit/options/max_src_file_size.py +39 -0
- imgproxykit-0.1.0/imgproxykit/options/max_src_resolution.py +39 -0
- imgproxykit-0.1.0/imgproxykit/options/min_height.py +38 -0
- imgproxykit-0.1.0/imgproxykit/options/min_width.py +38 -0
- imgproxykit-0.1.0/imgproxykit/options/padding.py +50 -0
- imgproxykit-0.1.0/imgproxykit/options/pixelate.py +38 -0
- imgproxykit-0.1.0/imgproxykit/options/preserve_hdr.py +38 -0
- imgproxykit-0.1.0/imgproxykit/options/quality.py +38 -0
- imgproxykit-0.1.0/imgproxykit/options/raw.py +39 -0
- imgproxykit-0.1.0/imgproxykit/options/resize.py +55 -0
- imgproxykit-0.1.0/imgproxykit/options/resizing_type.py +39 -0
- imgproxykit-0.1.0/imgproxykit/options/return_attachment.py +39 -0
- imgproxykit-0.1.0/imgproxykit/options/rotate.py +38 -0
- imgproxykit-0.1.0/imgproxykit/options/sharpen.py +38 -0
- imgproxykit-0.1.0/imgproxykit/options/size.py +45 -0
- imgproxykit-0.1.0/imgproxykit/options/skip_processing.py +39 -0
- imgproxykit-0.1.0/imgproxykit/options/strip_color_profile.py +39 -0
- imgproxykit-0.1.0/imgproxykit/options/strip_metadata.py +38 -0
- imgproxykit-0.1.0/imgproxykit/options/trim.py +52 -0
- imgproxykit-0.1.0/imgproxykit/options/watermark.py +60 -0
- imgproxykit-0.1.0/imgproxykit/options/width.py +39 -0
- imgproxykit-0.1.0/imgproxykit/options/zoom.py +45 -0
- imgproxykit-0.1.0/imgproxykit/py.typed +0 -0
- imgproxykit-0.1.0/imgproxykit/signing.py +114 -0
- imgproxykit-0.1.0/imgproxykit/transform.py +54 -0
- imgproxykit-0.1.0/imgproxykit/types.py +95 -0
- imgproxykit-0.1.0/pyproject.toml +268 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Alexander Kondratev
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: imgproxykit
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A typed Python toolkit for building imgproxy URLs
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Keywords: image-processing,image-transformation,images,imgproxy,url-builder
|
|
8
|
+
Author: Alexander Kondratev
|
|
9
|
+
Author-email: mail@kondratev.dev
|
|
10
|
+
Requires-Python: >=3.12,<4
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Programming Language :: Python
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.15
|
|
20
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
21
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
22
|
+
Classifier: Typing :: Typed
|
|
23
|
+
Requires-Dist: typing-extensions (>=4.16,<5)
|
|
24
|
+
Project-URL: Changelog, https://github.com/kondratevdev/imgproxykit/releases
|
|
25
|
+
Project-URL: GitHub, https://github.com/kondratevdev/imgproxykit
|
|
26
|
+
Project-URL: Issues, https://github.com/kondratevdev/imgproxykit/issues
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
|
|
29
|
+
<h1 align="center">imgproxykit</h1>
|
|
30
|
+
|
|
31
|
+
<p align="center">
|
|
32
|
+
<em>A typed Python toolkit for building imgproxy URLs.</em>
|
|
33
|
+
</p>
|
|
34
|
+
|
|
35
|
+
<p align="center">
|
|
36
|
+
<a href="#features">Features</a> ·
|
|
37
|
+
<a href="#installation">Installation</a> ·
|
|
38
|
+
<a href="#example">Example</a> ·
|
|
39
|
+
<a href="https://github.com/kondratevdev/imgproxykit/issues">Issues</a> ·
|
|
40
|
+
<a href="CONTRIBUTING.md">Contributing</a>
|
|
41
|
+
</p>
|
|
42
|
+
|
|
43
|
+
Define image transformations in Python and turn them into reusable, signed URLs.
|
|
44
|
+
Your imgproxy server fetches and processes the image when the URL is requested.
|
|
45
|
+
|
|
46
|
+
## Features
|
|
47
|
+
|
|
48
|
+
- [x] Typed API with annotations
|
|
49
|
+
- [x] Immutable, reusable transforms built from Python dataclasses
|
|
50
|
+
- [x] Free imgproxy processing options, including geometry, filters, metadata,
|
|
51
|
+
quality, pixel density, gravity, and watermarks
|
|
52
|
+
- [x] Output format selection, including WebP and AVIF
|
|
53
|
+
- [x] Support for named presets configured on your imgproxy server
|
|
54
|
+
|
|
55
|
+
## Installation
|
|
56
|
+
|
|
57
|
+
Requires **Python 3.12+** and a running imgproxy server.
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
pip install imgproxykit
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Example
|
|
64
|
+
|
|
65
|
+
Create a client, define a transform, and generate a URL:
|
|
66
|
+
|
|
67
|
+
```pycon
|
|
68
|
+
>>> from imgproxykit import Client, Transform
|
|
69
|
+
>>> from imgproxykit.options import Quality, Resize
|
|
70
|
+
>>> client = Client('https://images.example.com')
|
|
71
|
+
>>> thumbnail = Transform(
|
|
72
|
+
... options=(
|
|
73
|
+
... Resize(width=300, height=300, mode='fill'),
|
|
74
|
+
... Quality(85),
|
|
75
|
+
... ),
|
|
76
|
+
... format='webp',
|
|
77
|
+
... )
|
|
78
|
+
>>> url = client.url(
|
|
79
|
+
... 'https://example.com/photo.jpg',
|
|
80
|
+
... transform=thumbnail,
|
|
81
|
+
... )
|
|
82
|
+
>>> url.startswith('https://images.example.com/unsafe/rs:fill:300:300/q:85/')
|
|
83
|
+
True
|
|
84
|
+
>>> url.endswith('.webp')
|
|
85
|
+
True
|
|
86
|
+
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Reuse `client` and `thumbnail` for other source images. Options you omit are
|
|
90
|
+
left to imgproxy's defaults.
|
|
91
|
+
|
|
92
|
+
Without signing credentials, URLs use the `unsafe` marker and require a server
|
|
93
|
+
with signature checking disabled.
|
|
94
|
+
|
|
95
|
+
### Signed URLs
|
|
96
|
+
|
|
97
|
+
Supply the hex-encoded key and salt from your imgproxy server configuration:
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
import os
|
|
101
|
+
|
|
102
|
+
from imgproxykit import Client
|
|
103
|
+
|
|
104
|
+
client = Client(
|
|
105
|
+
'https://images.example.com',
|
|
106
|
+
key=os.environ['IMGPROXY_KEY'],
|
|
107
|
+
salt=os.environ['IMGPROXY_SALT'],
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
url = client.url(
|
|
111
|
+
'https://example.com/photo.jpg',
|
|
112
|
+
transform=thumbnail,
|
|
113
|
+
)
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Both credentials are required together. The client signs each generated path
|
|
117
|
+
automatically.
|
|
118
|
+
|
|
119
|
+
### Server presets
|
|
120
|
+
|
|
121
|
+
Already have presets on your imgproxy server? Reference them by name:
|
|
122
|
+
|
|
123
|
+
```python
|
|
124
|
+
url = client.url(
|
|
125
|
+
'https://example.com/photo.jpg',
|
|
126
|
+
presets=('thumbnail', 'watermarked'),
|
|
127
|
+
format='webp',
|
|
128
|
+
)
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Use either a Python `transform` or server `presets` in each call.
|
|
132
|
+
For Python transforms, set `format` on `Transform`; for presets, pass it to
|
|
133
|
+
`client.url()`.
|
|
134
|
+
|
|
135
|
+
## Contributing
|
|
136
|
+
|
|
137
|
+
See the [contributing guide](CONTRIBUTING.md) for development setup and checks.
|
|
138
|
+
Bug reports and feature requests are welcome on the
|
|
139
|
+
[issue tracker](https://github.com/kondratevdev/imgproxykit/issues).
|
|
140
|
+
|
|
141
|
+
## License
|
|
142
|
+
|
|
143
|
+
[MIT](LICENSE)
|
|
144
|
+
|
|
145
|
+
## Credits
|
|
146
|
+
|
|
147
|
+
Generated with
|
|
148
|
+
[`wemake-python-package`](https://github.com/wemake-services/wemake-python-package)
|
|
149
|
+
([template version](https://github.com/wemake-services/wemake-python-package/tree/47d9dddd29da8d6d3d1a403f8a143b323a812083)).
|
|
150
|
+
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
<h1 align="center">imgproxykit</h1>
|
|
2
|
+
|
|
3
|
+
<p align="center">
|
|
4
|
+
<em>A typed Python toolkit for building imgproxy URLs.</em>
|
|
5
|
+
</p>
|
|
6
|
+
|
|
7
|
+
<p align="center">
|
|
8
|
+
<a href="#features">Features</a> ·
|
|
9
|
+
<a href="#installation">Installation</a> ·
|
|
10
|
+
<a href="#example">Example</a> ·
|
|
11
|
+
<a href="https://github.com/kondratevdev/imgproxykit/issues">Issues</a> ·
|
|
12
|
+
<a href="CONTRIBUTING.md">Contributing</a>
|
|
13
|
+
</p>
|
|
14
|
+
|
|
15
|
+
Define image transformations in Python and turn them into reusable, signed URLs.
|
|
16
|
+
Your imgproxy server fetches and processes the image when the URL is requested.
|
|
17
|
+
|
|
18
|
+
## Features
|
|
19
|
+
|
|
20
|
+
- [x] Typed API with annotations
|
|
21
|
+
- [x] Immutable, reusable transforms built from Python dataclasses
|
|
22
|
+
- [x] Free imgproxy processing options, including geometry, filters, metadata,
|
|
23
|
+
quality, pixel density, gravity, and watermarks
|
|
24
|
+
- [x] Output format selection, including WebP and AVIF
|
|
25
|
+
- [x] Support for named presets configured on your imgproxy server
|
|
26
|
+
|
|
27
|
+
## Installation
|
|
28
|
+
|
|
29
|
+
Requires **Python 3.12+** and a running imgproxy server.
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
pip install imgproxykit
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Example
|
|
36
|
+
|
|
37
|
+
Create a client, define a transform, and generate a URL:
|
|
38
|
+
|
|
39
|
+
```pycon
|
|
40
|
+
>>> from imgproxykit import Client, Transform
|
|
41
|
+
>>> from imgproxykit.options import Quality, Resize
|
|
42
|
+
>>> client = Client('https://images.example.com')
|
|
43
|
+
>>> thumbnail = Transform(
|
|
44
|
+
... options=(
|
|
45
|
+
... Resize(width=300, height=300, mode='fill'),
|
|
46
|
+
... Quality(85),
|
|
47
|
+
... ),
|
|
48
|
+
... format='webp',
|
|
49
|
+
... )
|
|
50
|
+
>>> url = client.url(
|
|
51
|
+
... 'https://example.com/photo.jpg',
|
|
52
|
+
... transform=thumbnail,
|
|
53
|
+
... )
|
|
54
|
+
>>> url.startswith('https://images.example.com/unsafe/rs:fill:300:300/q:85/')
|
|
55
|
+
True
|
|
56
|
+
>>> url.endswith('.webp')
|
|
57
|
+
True
|
|
58
|
+
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Reuse `client` and `thumbnail` for other source images. Options you omit are
|
|
62
|
+
left to imgproxy's defaults.
|
|
63
|
+
|
|
64
|
+
Without signing credentials, URLs use the `unsafe` marker and require a server
|
|
65
|
+
with signature checking disabled.
|
|
66
|
+
|
|
67
|
+
### Signed URLs
|
|
68
|
+
|
|
69
|
+
Supply the hex-encoded key and salt from your imgproxy server configuration:
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
import os
|
|
73
|
+
|
|
74
|
+
from imgproxykit import Client
|
|
75
|
+
|
|
76
|
+
client = Client(
|
|
77
|
+
'https://images.example.com',
|
|
78
|
+
key=os.environ['IMGPROXY_KEY'],
|
|
79
|
+
salt=os.environ['IMGPROXY_SALT'],
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
url = client.url(
|
|
83
|
+
'https://example.com/photo.jpg',
|
|
84
|
+
transform=thumbnail,
|
|
85
|
+
)
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Both credentials are required together. The client signs each generated path
|
|
89
|
+
automatically.
|
|
90
|
+
|
|
91
|
+
### Server presets
|
|
92
|
+
|
|
93
|
+
Already have presets on your imgproxy server? Reference them by name:
|
|
94
|
+
|
|
95
|
+
```python
|
|
96
|
+
url = client.url(
|
|
97
|
+
'https://example.com/photo.jpg',
|
|
98
|
+
presets=('thumbnail', 'watermarked'),
|
|
99
|
+
format='webp',
|
|
100
|
+
)
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Use either a Python `transform` or server `presets` in each call.
|
|
104
|
+
For Python transforms, set `format` on `Transform`; for presets, pass it to
|
|
105
|
+
`client.url()`.
|
|
106
|
+
|
|
107
|
+
## Contributing
|
|
108
|
+
|
|
109
|
+
See the [contributing guide](CONTRIBUTING.md) for development setup and checks.
|
|
110
|
+
Bug reports and feature requests are welcome on the
|
|
111
|
+
[issue tracker](https://github.com/kondratevdev/imgproxykit/issues).
|
|
112
|
+
|
|
113
|
+
## License
|
|
114
|
+
|
|
115
|
+
[MIT](LICENSE)
|
|
116
|
+
|
|
117
|
+
## Credits
|
|
118
|
+
|
|
119
|
+
Generated with
|
|
120
|
+
[`wemake-python-package`](https://github.com/wemake-services/wemake-python-package)
|
|
121
|
+
([template version](https://github.com/wemake-services/wemake-python-package/tree/47d9dddd29da8d6d3d1a403f8a143b323a812083)).
|
|
@@ -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.')
|
|
@@ -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)
|