thumbrella-client 0.5.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.
@@ -0,0 +1,3 @@
1
+ include README.md
2
+ recursive-include examples *.py
3
+ recursive-include src/thumbrella *.jpeg
@@ -0,0 +1,143 @@
1
+ Metadata-Version: 2.4
2
+ Name: thumbrella-client
3
+ Version: 0.5.0
4
+ Summary: Python client for the Thumbrella thumbnail API — typed results, async streaming, pluggable caching
5
+ License-Expression: Apache-2.0
6
+ Project-URL: Homepage, https://thumbrella.dev
7
+ Project-URL: Repository, https://github.com/thumbrella-dev/clients
8
+ Project-URL: Issues, https://github.com/thumbrella-dev/clients/issues
9
+ Project-URL: Documentation, https://thumbrella.dev/docs/client
10
+ Keywords: thumbrella,thumbnail,thumbnails,image,media,streaming
11
+ Classifier: Development Status :: 2 - Pre-Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Programming Language :: Python :: 3.14
19
+ Classifier: Topic :: Multimedia :: Graphics
20
+ Classifier: Topic :: Internet :: WWW/HTTP
21
+ Classifier: Typing :: Typed
22
+ Requires-Python: >=3.10
23
+ Description-Content-Type: text/markdown
24
+ Requires-Dist: requests>=2.32.0
25
+ Provides-Extra: async
26
+ Requires-Dist: aiohttp>=3.9.0; extra == "async"
27
+
28
+ # thumbrella-client
29
+
30
+ Python client for [Thumbrella](https://thumbrella.dev) — a fast thumbnail API
31
+ for images, video, documents, and more.
32
+
33
+ [![PyPI version](https://img.shields.io/pypi/v/thumbrella-client)](https://pypi.org/project/thumbrella-client/)
34
+ [![Python](https://img.shields.io/pypi/pyversions/thumbrella-client)](https://pypi.org/project/thumbrella-client/)
35
+ [![License](https://img.shields.io/badge/license-Apache%202.0-blue)](https://github.com/thumbrella-dev/clients/blob/main/LICENSE)
36
+
37
+ ## Install
38
+
39
+ ```bash
40
+ pip install thumbrella-client
41
+ ```
42
+
43
+ Or with uv:
44
+
45
+ ```bash
46
+ uv add thumbrella-client
47
+ ```
48
+
49
+ Async streaming needs `aiohttp`:
50
+
51
+ ```bash
52
+ pip install "thumbrella-client[async]"
53
+ ```
54
+
55
+ ## Quickstart
56
+
57
+ ```python
58
+ import thumbrella
59
+
60
+ tbr = thumbrella.Client().verify()
61
+ result = tbr.thumb("https://example.com/photo.jpg")
62
+
63
+ print(result.status, len(result.media.thumbnail), "bytes")
64
+
65
+ # Use with Pillow
66
+ from PIL import Image
67
+ img = Image.open(result.media.thumbnail.io)
68
+ ```
69
+
70
+
71
+
72
+ ## How It Works
73
+
74
+ Create a `Client`, call `verify()` to check connectivity, then use `thumb()`,
75
+ `batch()`, or `stream()` to generate thumbnails.
76
+
77
+ The client reads `$TBR_CONNECT` for server config. Override it with a connect
78
+ string:
79
+
80
+ ```python
81
+ tbr = thumbrella.Client("http://localhost:3114") # local dev
82
+ tbr = thumbrella.Client("https://cloud.thumbrella.dev,tbr_s_...") # cloud token
83
+ ```
84
+
85
+ Every URL gets a `Result` — failures get a placeholder image too. Use
86
+ `result.verify()` to raise on failure, or check `result.is_success()` for inline
87
+ handling. See the [client docs](https://thumbrella.dev/docs/client/) for the
88
+ full `Result` and `Media` field reference.
89
+
90
+ ### Errors
91
+
92
+ ```python
93
+ from thumbrella import ThumbError, ConnectionError, TimeoutError, VerifyError
94
+ ```
95
+
96
+ All errors extend `ThumbError`. `Client.verify()` raises `VerifyError` on bad
97
+ config. Network issues raise `ConnectionError` or `TimeoutError`. Per-result
98
+ failures return a `Result` with a failure status — call `result.verify()` to
99
+ convert to an exception.
100
+
101
+ ### Caching
102
+
103
+ Each `Client` defaults to an in-memory LRU cache (256 entries). Disable with
104
+ `caches=[]` or layer custom backends:
105
+
106
+ ```python
107
+ tbr = thumbrella.Client(caches=[]) # no caching
108
+ tbr = thumbrella.Client(caches=[thumbrella.MemoryCache(max_items=1000)])
109
+ ```
110
+
111
+ Implement `thumbrella.Cache` to add persistent storage (SQLite, S3, etc.).
112
+
113
+
114
+ ## Examples
115
+
116
+ ```bash
117
+ # Download one thumbnail to disk (with PIL inspection)
118
+ python examples/basic.py https://demo.thumbrella.dev/media/raw-canon.cr2 cam.jpeg
119
+
120
+ # Stream batch progress
121
+ python examples/stream.py https://example.com/a.jpg https://example.com/b.png
122
+
123
+ # Build a collage grid from streamed thumbnails
124
+ python examples/collage.py urls.txt
125
+
126
+ # Batch download with persistent caching
127
+ python examples/gallery.py https://example.com/a.jpg https://example.com/b.png
128
+ ```
129
+
130
+ See [`examples/`](./examples) for full source.
131
+
132
+
133
+ ## Next Steps
134
+
135
+ - **[Client docs](https://thumbrella.dev/docs/client/)** — full API reference and examples
136
+ - **[Thumbrella](https://thumbrella.dev)** — main site
137
+ - **[GitHub](https://github.com/thumbrella-dev/clients)** — source and issues
138
+
139
+ ## License
140
+
141
+ Apache-2.0. See [LICENSE](https://github.com/thumbrella-dev/clients/blob/main/LICENSE).
142
+
143
+
@@ -0,0 +1,116 @@
1
+ # thumbrella-client
2
+
3
+ Python client for [Thumbrella](https://thumbrella.dev) — a fast thumbnail API
4
+ for images, video, documents, and more.
5
+
6
+ [![PyPI version](https://img.shields.io/pypi/v/thumbrella-client)](https://pypi.org/project/thumbrella-client/)
7
+ [![Python](https://img.shields.io/pypi/pyversions/thumbrella-client)](https://pypi.org/project/thumbrella-client/)
8
+ [![License](https://img.shields.io/badge/license-Apache%202.0-blue)](https://github.com/thumbrella-dev/clients/blob/main/LICENSE)
9
+
10
+ ## Install
11
+
12
+ ```bash
13
+ pip install thumbrella-client
14
+ ```
15
+
16
+ Or with uv:
17
+
18
+ ```bash
19
+ uv add thumbrella-client
20
+ ```
21
+
22
+ Async streaming needs `aiohttp`:
23
+
24
+ ```bash
25
+ pip install "thumbrella-client[async]"
26
+ ```
27
+
28
+ ## Quickstart
29
+
30
+ ```python
31
+ import thumbrella
32
+
33
+ tbr = thumbrella.Client().verify()
34
+ result = tbr.thumb("https://example.com/photo.jpg")
35
+
36
+ print(result.status, len(result.media.thumbnail), "bytes")
37
+
38
+ # Use with Pillow
39
+ from PIL import Image
40
+ img = Image.open(result.media.thumbnail.io)
41
+ ```
42
+
43
+
44
+
45
+ ## How It Works
46
+
47
+ Create a `Client`, call `verify()` to check connectivity, then use `thumb()`,
48
+ `batch()`, or `stream()` to generate thumbnails.
49
+
50
+ The client reads `$TBR_CONNECT` for server config. Override it with a connect
51
+ string:
52
+
53
+ ```python
54
+ tbr = thumbrella.Client("http://localhost:3114") # local dev
55
+ tbr = thumbrella.Client("https://cloud.thumbrella.dev,tbr_s_...") # cloud token
56
+ ```
57
+
58
+ Every URL gets a `Result` — failures get a placeholder image too. Use
59
+ `result.verify()` to raise on failure, or check `result.is_success()` for inline
60
+ handling. See the [client docs](https://thumbrella.dev/docs/client/) for the
61
+ full `Result` and `Media` field reference.
62
+
63
+ ### Errors
64
+
65
+ ```python
66
+ from thumbrella import ThumbError, ConnectionError, TimeoutError, VerifyError
67
+ ```
68
+
69
+ All errors extend `ThumbError`. `Client.verify()` raises `VerifyError` on bad
70
+ config. Network issues raise `ConnectionError` or `TimeoutError`. Per-result
71
+ failures return a `Result` with a failure status — call `result.verify()` to
72
+ convert to an exception.
73
+
74
+ ### Caching
75
+
76
+ Each `Client` defaults to an in-memory LRU cache (256 entries). Disable with
77
+ `caches=[]` or layer custom backends:
78
+
79
+ ```python
80
+ tbr = thumbrella.Client(caches=[]) # no caching
81
+ tbr = thumbrella.Client(caches=[thumbrella.MemoryCache(max_items=1000)])
82
+ ```
83
+
84
+ Implement `thumbrella.Cache` to add persistent storage (SQLite, S3, etc.).
85
+
86
+
87
+ ## Examples
88
+
89
+ ```bash
90
+ # Download one thumbnail to disk (with PIL inspection)
91
+ python examples/basic.py https://demo.thumbrella.dev/media/raw-canon.cr2 cam.jpeg
92
+
93
+ # Stream batch progress
94
+ python examples/stream.py https://example.com/a.jpg https://example.com/b.png
95
+
96
+ # Build a collage grid from streamed thumbnails
97
+ python examples/collage.py urls.txt
98
+
99
+ # Batch download with persistent caching
100
+ python examples/gallery.py https://example.com/a.jpg https://example.com/b.png
101
+ ```
102
+
103
+ See [`examples/`](./examples) for full source.
104
+
105
+
106
+ ## Next Steps
107
+
108
+ - **[Client docs](https://thumbrella.dev/docs/client/)** — full API reference and examples
109
+ - **[Thumbrella](https://thumbrella.dev)** — main site
110
+ - **[GitHub](https://github.com/thumbrella-dev/clients)** — source and issues
111
+
112
+ ## License
113
+
114
+ Apache-2.0. See [LICENSE](https://github.com/thumbrella-dev/clients/blob/main/LICENSE).
115
+
116
+
@@ -0,0 +1,57 @@
1
+ """basic.py - download one thumbnail to a file.
2
+
3
+ Usage:
4
+ python basic.py https://www.python.org/static/img/python-logo.png thumb.jpeg
5
+
6
+ Gets a generated thumbnail for any URL. If there are problems connecting
7
+ to Thumbrella or accessing the media URL this will fail with an explanation.
8
+ """
9
+
10
+ import argparse
11
+ from pathlib import Path
12
+
13
+ import thumbrella
14
+
15
+
16
+ def thumbnail(url: str, path: Path):
17
+ """Generate thumbnail for url and save to disk."""
18
+ tbr = thumbrella.Client().verify()
19
+ result = tbr.thumb(url)
20
+
21
+ m = result.media
22
+ if m is None:
23
+ print("Thumbnail did not succeed:", result.status)
24
+
25
+ # Simple result metadata
26
+ print(
27
+ f"{m.kind if m else '?'} {m.file_size if m else '?':,} bytes -> "
28
+ f"{len(m.thumbnail) if m else 0:,} bytes {path}"
29
+ )
30
+
31
+ # Load thumbnail into PIL and process
32
+ try:
33
+ from PIL import Image
34
+ img = Image.open(m.thumbnail.io)
35
+ print("mode:", img.mode, "width:", img.width, "height:", img.height)
36
+ pixel = img.getpixel((img.width // 2, img.height // 2))
37
+ print("center pixel:", pixel)
38
+ except ImportError:
39
+ pass
40
+
41
+ # Write image to disk
42
+ path.write_bytes(m.thumbnail.bytes if m else b"")
43
+
44
+
45
+ def main():
46
+ parser = argparse.ArgumentParser(description="download a thumbnail")
47
+ parser.add_argument("url", help="media URL to thumbnail")
48
+ parser.add_argument("path", type=Path, help="output jpeg path")
49
+ args = parser.parse_args()
50
+ try:
51
+ thumbnail(args.url, args.path)
52
+ except thumbrella.ThumbError as err:
53
+ raise SystemExit(err)
54
+
55
+
56
+ if __name__ == "__main__":
57
+ main()
@@ -0,0 +1,112 @@
1
+ """collage.py — stream thumbnails into a grid collage.
2
+
3
+ Usage:
4
+ python collage.py urls.txt collage.jpg
5
+
6
+ urls.txt should contain one URL per line (# comments and blank lines ok).
7
+
8
+ Streams results live and reports timing + cache status. Thumbnails are
9
+ arranged on a 6-column grid (3 columns when fewer than 8 items).
10
+ """
11
+
12
+ import argparse
13
+ import asyncio
14
+ import time
15
+ from pathlib import Path
16
+
17
+ import thumbrella
18
+
19
+ THUMB_W, THUMB_H = 250, 200
20
+ SPACING = 4
21
+ COLS = 6
22
+ COLS_SMALL = 3
23
+
24
+
25
+ async def collage(urls_file: Path, out_path: Path) -> None:
26
+ urls = [
27
+ line.strip()
28
+ for line in urls_file.read_text().splitlines()
29
+ if line.strip() and not line.startswith("#")
30
+ ]
31
+
32
+ # Client reads TBR_CONNECT env var for server URL or cloud token.
33
+ tbr = thumbrella.Client().verify()
34
+
35
+ placed: list[thumbrella.Result] = []
36
+ placeholders = 0
37
+ t0 = time.monotonic()
38
+
39
+ async for result in tbr.stream(urls):
40
+ elapsed = time.monotonic() - t0
41
+
42
+ # Intermediate result — tier1 delegating to a renderer.
43
+ if result.status == "intermediate":
44
+ placeholders += 1
45
+ print(f" ... {result.url}")
46
+ continue
47
+
48
+ if result.is_success():
49
+ placed.append(result)
50
+ tag = _cache_tag(result)
51
+ print(
52
+ f" {tag:6s} {result.duration:>5.0f}ms "
53
+ f"{result.kind:8s} {result.mime or '':20s} {result.url}"
54
+ )
55
+ else:
56
+ print(f" FAIL {result.status:16s} {result.url}")
57
+
58
+ if not placed:
59
+ raise SystemExit("no thumbnails to collage")
60
+
61
+ # Build collage grid.
62
+ cols = COLS_SMALL if len(placed) < 8 else COLS
63
+ rows = (len(placed) + cols - 1) // cols
64
+ cw, ch = THUMB_W + SPACING, THUMB_H + SPACING
65
+ canvas_w = cols * cw + SPACING
66
+ canvas_h = rows * ch + SPACING
67
+
68
+ from PIL import Image
69
+
70
+ grid = Image.new("RGB", (canvas_w, canvas_h), (255, 255, 255))
71
+
72
+ for i, r in enumerate(placed):
73
+ col, row = i % cols, i // cols
74
+ x, y = SPACING + col * cw, SPACING + row * ch
75
+ img = Image.open(r.thumbnail.io)
76
+ img.load()
77
+ grid.paste(img, (x, y))
78
+
79
+ grid.save(out_path, quality=85)
80
+ print(
81
+ f"\n{len(placed)} thumbnails "
82
+ f"{rows}x{cols} "
83
+ f"{grid.size[0]}x{grid.size[1]} px "
84
+ f"{placeholders} placeholders -> {out_path}"
85
+ )
86
+
87
+
88
+ def _cache_tag(result: thumbrella.Result) -> str:
89
+ """Short label for how this thumbnail was produced."""
90
+ src = result.source
91
+ if src == thumbrella.Source.CLIENT:
92
+ return "client"
93
+ if src == thumbrella.Source.CACHE:
94
+ return "cached"
95
+ if src == thumbrella.Source.SHORTCUT:
96
+ return "embed"
97
+ return "render"
98
+
99
+
100
+ def main() -> None:
101
+ parser = argparse.ArgumentParser(description="stream thumbnails into a collage")
102
+ parser.add_argument("urls", type=Path, help="file with one media URL per line")
103
+ parser.add_argument("out", type=Path, help="output collage JPEG path")
104
+ args = parser.parse_args()
105
+ try:
106
+ asyncio.run(collage(args.urls, args.out))
107
+ except thumbrella.ThumbError as err:
108
+ raise SystemExit(err)
109
+
110
+
111
+ if __name__ == "__main__":
112
+ main()
@@ -0,0 +1,113 @@
1
+ """gallery.py — stream a batch of URLs with live progress and local caching.
2
+
3
+ Usage:
4
+ python gallery.py urls.txt thumbs/
5
+
6
+ urls.txt should contain one URL per line (# comments and blank lines ok).
7
+
8
+ Re-running with the same output directory skips unchanged thumbnails by
9
+ persisting cache tokens in thumbs/.thumbrella_cache.json.
10
+
11
+ """
12
+
13
+ import argparse
14
+ import asyncio
15
+ import json
16
+ from pathlib import Path
17
+
18
+ import thumbrella
19
+
20
+
21
+ async def gallery(urls_file: Path, out_dir: Path) -> None:
22
+ """Stream thumbnails for urls in *urls_file*, save to *out_dir*."""
23
+ out_dir.mkdir(parents=True, exist_ok=True)
24
+ urls = [
25
+ line.strip()
26
+ for line in urls_file.read_text().splitlines()
27
+ if line.strip() and not line.startswith("#")
28
+ ]
29
+
30
+ # Restore persistent cache tokens from last run.
31
+ cache_path = out_dir / ".thumbrella_cache.json"
32
+ prev_cache: dict[str, str] = {}
33
+ try:
34
+ prev_cache = json.loads(cache_path.read_text())
35
+ except (FileNotFoundError, json.JSONDecodeError):
36
+ pass
37
+
38
+ # Client reads TBR_CONNECT env var for server URL or cloud token.
39
+ tbr = thumbrella.Client().verify()
40
+ image_loader = await _ensure_pil()
41
+
42
+ image_cache: dict[int, object] = {} # key → PIL Image
43
+ new_cache: dict[str, str] = {}
44
+ new_count, unchanged, failed = 0, 0, 0
45
+
46
+ async for result in tbr.stream(urls):
47
+ # Restore cache token so the server can 304 on unchanged media.
48
+ if result.url in prev_cache:
49
+ result.cache = prev_cache[result.url]
50
+
51
+ if result.is_success():
52
+ if result.is_fresh():
53
+ unchanged += 1
54
+ else:
55
+ new_count += 1
56
+
57
+ if result.cache:
58
+ new_cache[result.url] = result.cache
59
+
60
+ # Decode with PIL, deduplicated by thumbnail content key.
61
+ img = image_cache.get(result.thumbnail.key)
62
+ if img is None and image_loader is not None:
63
+ img = image_loader(result.thumbnail.io)
64
+ image_cache[result.thumbnail.key] = img
65
+
66
+ # Save thumbnail.
67
+ stem = result.url.rstrip("/").split("/")[-1] or "thumb"
68
+ safe = "".join(c if c.isalnum() or c in "._-" else "_" for c in stem)
69
+ path = out_dir / f"{safe}.jpg"
70
+ path.write_bytes(result.thumbnail.bytes)
71
+
72
+ w, h = img.size if img else (0, 0)
73
+ tag = "(cached)" if result.is_fresh() else ""
74
+ print(f" {tag:9s}{result.kind:8s} {w:>4d}x{h:<4d} {result.mime:20s} {result.url}")
75
+ else:
76
+ failed += 1
77
+ print(f" FAIL {result.status:16s} {result.url}")
78
+
79
+ if new_cache:
80
+ cache_path.write_text(json.dumps(new_cache, indent=2))
81
+
82
+ print(f"\n{new_count} new, {unchanged} unchanged, {failed} failed")
83
+
84
+
85
+ async def _ensure_pil():
86
+ """Return an image loader function, or None if Pillow is missing."""
87
+ try:
88
+ from PIL import Image
89
+ except ImportError:
90
+ print("(install Pillow for image dimensions)")
91
+ return None
92
+
93
+ def load(io) -> object:
94
+ img = Image.open(io)
95
+ img.load()
96
+ return img
97
+
98
+ return load
99
+
100
+
101
+ def main() -> None:
102
+ parser = argparse.ArgumentParser(description="stream thumbnails from a URL list")
103
+ parser.add_argument("urls", type=Path, help="file with one media URL per line")
104
+ parser.add_argument("out_dir", type=Path, help="directory for output JPEGs")
105
+ args = parser.parse_args()
106
+ try:
107
+ asyncio.run(gallery(args.urls, args.out_dir))
108
+ except thumbrella.ThumbError as err:
109
+ raise SystemExit(err)
110
+
111
+
112
+ if __name__ == "__main__":
113
+ main()
@@ -0,0 +1,71 @@
1
+ """stream.py - show progress on async thumbnail rendering.
2
+
3
+ Usage:
4
+ python stream.py https://www.python.org/static/img/python-logo.png https://www.pygame.org/docs/_images/pygame_powered.png https://pypi.org/unknown.jpg
5
+
6
+ Gets a generated thumbnail for any URL. If there are problems connecting
7
+ to Thumbrella or accessing the media URL this will fail with an explanation.
8
+ """
9
+
10
+ import asyncio
11
+ import argparse
12
+ import time
13
+
14
+ import thumbrella
15
+
16
+
17
+
18
+ def main():
19
+ parser = argparse.ArgumentParser(description="monitor streamed results")
20
+ parser.add_argument("urls", nargs="+", help="media URLs to stream")
21
+ parser.add_argument("--batch", action="store_true", help="use sync back instead")
22
+ args = parser.parse_args()
23
+
24
+ tbr = thumbrella.Client().verify()
25
+ try:
26
+ if args.batch:
27
+ batched(tbr, args.urls)
28
+ else:
29
+ asyncio.run(streamed(tbr, args.urls))
30
+ except thumbrella.ThumbError as err:
31
+ raise SystemExit(err)
32
+
33
+
34
+ async def streamed(tbr: thumbrella.Client, urls: list[str]):
35
+ """Monitor streamed results."""
36
+ # async with thumbrella.Client() as tbr:
37
+ # tbr.verify()
38
+ async with tbr:
39
+ indices = _index(urls)
40
+ start = time.time()
41
+ stream = tbr.stream(urls)
42
+ async for result in stream:
43
+ _report(start, indices.get(result.url, "XXX"), result)
44
+
45
+
46
+ def batched(tbr: thumbrella.Client, urls: list[str]):
47
+ """Get all results in one single call"""
48
+ indices = _index(urls)
49
+ start = time.time()
50
+ batch = tbr.batch(urls)
51
+ for result in batch:
52
+ _report(start, indices.get(result.url, "XXX"), result)
53
+
54
+
55
+ def _index(urls: list[str]):
56
+ """Assign report index to each url"""
57
+ indices = {u: f"{i:03d}" for i, u in enumerate(urls, 1)}
58
+ for u, i in indices.items():
59
+ print(f"{i}: {u}")
60
+ return indices
61
+
62
+ def _report(start: float, idx: str, result: thumbrella.Result):
63
+ """One line report for each result"""
64
+ kind = f"{result.media.kind}({result.media.extension})" if result.media else "<nomedia>"
65
+ print(
66
+ f"{(time.time() - start) * 1000:,.0f}ms {idx}"
67
+ f" - {result.status} {kind} {result.source} {result.message or ''}"
68
+ )
69
+
70
+ if __name__ == "__main__":
71
+ main()
@@ -0,0 +1,45 @@
1
+ [project]
2
+ name = "thumbrella-client"
3
+ version = "0.5.0"
4
+ description = "Python client for the Thumbrella thumbnail API — typed results, async streaming, pluggable caching"
5
+ readme = "README.md"
6
+ requires-python = ">=3.10"
7
+ license = "Apache-2.0"
8
+ keywords = ["thumbrella", "thumbnail", "thumbnails", "image", "media", "streaming"]
9
+ classifiers = [
10
+ "Development Status :: 2 - Pre-Alpha",
11
+ "Intended Audience :: Developers",
12
+ "Programming Language :: Python :: 3",
13
+ "Programming Language :: Python :: 3.10",
14
+ "Programming Language :: Python :: 3.11",
15
+ "Programming Language :: Python :: 3.12",
16
+ "Programming Language :: Python :: 3.13",
17
+ "Programming Language :: Python :: 3.14",
18
+ "Topic :: Multimedia :: Graphics",
19
+ "Topic :: Internet :: WWW/HTTP",
20
+ "Typing :: Typed",
21
+ ]
22
+
23
+ dependencies = [
24
+ "requests>=2.32.0",
25
+ ]
26
+
27
+ [project.optional-dependencies]
28
+ async = ["aiohttp>=3.9.0"]
29
+
30
+ [project.urls]
31
+ Homepage = "https://thumbrella.dev"
32
+ Repository = "https://github.com/thumbrella-dev/clients"
33
+ Issues = "https://github.com/thumbrella-dev/clients/issues"
34
+ Documentation = "https://thumbrella.dev/docs/client"
35
+
36
+ [build-system]
37
+ requires = ["setuptools>=68", "wheel"]
38
+ build-backend = "setuptools.build_meta"
39
+
40
+ [tool.setuptools.packages.find]
41
+ where = ["src"]
42
+
43
+ [tool.setuptools.package-data]
44
+ thumbrella = ["*.jpg"]
45
+
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+