tteg 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.
tteg-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,74 @@
1
+ Metadata-Version: 2.4
2
+ Name: tteg
3
+ Version: 0.1.0
4
+ Summary: Agent-friendly stock image search CLI. No rate-limit BS.
5
+ Author: Kushal
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/kushal/tteg
8
+ Keywords: unsplash,stock-images,cli,ai-agents,image-search
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Topic :: Multimedia :: Graphics
14
+ Requires-Python: >=3.10
15
+ Description-Content-Type: text/markdown
16
+ Requires-Dist: click>=8.1.7
17
+ Requires-Dist: requests>=2.32.0
18
+ Requires-Dist: Pillow>=10.0.0
19
+
20
+ # tteg
21
+
22
+ `tteg` is a Python CLI for agent-friendly stock-image search.
23
+
24
+ Current shape:
25
+
26
+ - one command
27
+ - Unsplash-backed search
28
+ - JSON output by default
29
+ - inline stitched preview as base64 JPEG
30
+
31
+ ## Setup
32
+
33
+ `tteg` reads environment variables from the shell and also loads a local `.env` file from the current working directory if present.
34
+
35
+ Supported keys:
36
+
37
+ - `UNSPLASH_ACCESS_KEY`
38
+ - `ACCESS_KEY` as a fallback alias
39
+
40
+ ## Usage
41
+
42
+ ```bash
43
+ python3 -m tteg "mountain sunset"
44
+ python3 -m tteg "hero banner" --count 8 --orientation landscape --width 1920 --height 1080
45
+ ```
46
+
47
+ Example response shape:
48
+
49
+ ```json
50
+ {
51
+ "query": "mountain sunset",
52
+ "results": [
53
+ {
54
+ "id": "abc123",
55
+ "source": "unsplash",
56
+ "image_url": "https://images.unsplash.com/...",
57
+ "thumb_url": "https://images.unsplash.com/...",
58
+ "photographer": "Jane Doe",
59
+ "width": 4000,
60
+ "height": 3000,
61
+ "html_url": "https://unsplash.com/photos/...",
62
+ "download_location": "https://api.unsplash.com/photos/.../download"
63
+ }
64
+ ],
65
+ "preview": {
66
+ "mime_type": "image/jpeg",
67
+ "width": 320,
68
+ "height": 1108,
69
+ "data_base64": "..."
70
+ },
71
+ "warnings": []
72
+ }
73
+ ```
74
+
tteg-0.1.0/README.md ADDED
@@ -0,0 +1,55 @@
1
+ # tteg
2
+
3
+ `tteg` is a Python CLI for agent-friendly stock-image search.
4
+
5
+ Current shape:
6
+
7
+ - one command
8
+ - Unsplash-backed search
9
+ - JSON output by default
10
+ - inline stitched preview as base64 JPEG
11
+
12
+ ## Setup
13
+
14
+ `tteg` reads environment variables from the shell and also loads a local `.env` file from the current working directory if present.
15
+
16
+ Supported keys:
17
+
18
+ - `UNSPLASH_ACCESS_KEY`
19
+ - `ACCESS_KEY` as a fallback alias
20
+
21
+ ## Usage
22
+
23
+ ```bash
24
+ python3 -m tteg "mountain sunset"
25
+ python3 -m tteg "hero banner" --count 8 --orientation landscape --width 1920 --height 1080
26
+ ```
27
+
28
+ Example response shape:
29
+
30
+ ```json
31
+ {
32
+ "query": "mountain sunset",
33
+ "results": [
34
+ {
35
+ "id": "abc123",
36
+ "source": "unsplash",
37
+ "image_url": "https://images.unsplash.com/...",
38
+ "thumb_url": "https://images.unsplash.com/...",
39
+ "photographer": "Jane Doe",
40
+ "width": 4000,
41
+ "height": 3000,
42
+ "html_url": "https://unsplash.com/photos/...",
43
+ "download_location": "https://api.unsplash.com/photos/.../download"
44
+ }
45
+ ],
46
+ "preview": {
47
+ "mime_type": "image/jpeg",
48
+ "width": 320,
49
+ "height": 1108,
50
+ "data_base64": "..."
51
+ },
52
+ "warnings": []
53
+ }
54
+ ```
55
+
@@ -0,0 +1,37 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "tteg"
7
+ version = "0.1.0"
8
+ description = "Agent-friendly stock image search CLI. No rate-limit BS."
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = {text = "MIT"}
12
+ authors = [{name = "Kushal"}]
13
+ keywords = ["unsplash", "stock-images", "cli", "ai-agents", "image-search"]
14
+ classifiers = [
15
+ "Development Status :: 3 - Alpha",
16
+ "Intended Audience :: Developers",
17
+ "License :: OSI Approved :: MIT License",
18
+ "Programming Language :: Python :: 3",
19
+ "Topic :: Multimedia :: Graphics",
20
+ ]
21
+ dependencies = [
22
+ "click>=8.1.7",
23
+ "requests>=2.32.0",
24
+ "Pillow>=10.0.0",
25
+ ]
26
+
27
+ [project.urls]
28
+ Homepage = "https://github.com/kushal/tteg"
29
+
30
+ [project.scripts]
31
+ tteg = "tteg.cli:main"
32
+
33
+ [tool.setuptools]
34
+ include-package-data = true
35
+
36
+ [tool.setuptools.packages.find]
37
+ where = ["."]
tteg-0.1.0/setup.cfg ADDED
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,68 @@
1
+ from __future__ import annotations
2
+
3
+ import base64
4
+ import io
5
+ import unittest
6
+
7
+ from PIL import Image
8
+
9
+ from tteg.models import ImageResult
10
+ from tteg.preview import build_preview
11
+
12
+
13
+ def _make_image_bytes(color: str, size: tuple[int, int] = (120, 80)) -> bytes:
14
+ image = Image.new("RGB", size, color)
15
+ output = io.BytesIO()
16
+ image.save(output, format="JPEG")
17
+ return output.getvalue()
18
+
19
+
20
+ class PreviewTests(unittest.TestCase):
21
+ def test_build_preview_returns_base64_jpeg(self) -> None:
22
+ results = [
23
+ ImageResult(
24
+ id="1",
25
+ source="unsplash",
26
+ image_url="https://example.com/full-1.jpg",
27
+ thumb_url="https://example.com/thumb-1.jpg",
28
+ photographer="Jane",
29
+ width=1200,
30
+ height=800,
31
+ title="First image",
32
+ html_url="https://example.com/page-1",
33
+ download_location="https://example.com/download-1",
34
+ ),
35
+ ImageResult(
36
+ id="2",
37
+ source="unsplash",
38
+ image_url="https://example.com/full-2.jpg",
39
+ thumb_url="https://example.com/thumb-2.jpg",
40
+ photographer="John",
41
+ width=1400,
42
+ height=900,
43
+ title="Second image",
44
+ html_url="https://example.com/page-2",
45
+ download_location="https://example.com/download-2",
46
+ ),
47
+ ]
48
+ image_map = {
49
+ "https://example.com/thumb-1.jpg": _make_image_bytes("#cc5500"),
50
+ "https://example.com/thumb-2.jpg": _make_image_bytes("#0055cc"),
51
+ }
52
+
53
+ built = build_preview(results, downloader=image_map.__getitem__)
54
+
55
+ self.assertEqual(built.warnings, [])
56
+ self.assertIsNotNone(built.preview)
57
+ assert built.preview is not None
58
+ self.assertEqual(built.preview.mime_type, "image/jpeg")
59
+ decoded = base64.b64decode(built.preview.data_base64)
60
+ image = Image.open(io.BytesIO(decoded))
61
+ self.assertEqual(image.format, "JPEG")
62
+ self.assertGreater(image.width, 0)
63
+ self.assertGreater(image.height, 0)
64
+
65
+
66
+ if __name__ == "__main__":
67
+ unittest.main()
68
+
@@ -0,0 +1,53 @@
1
+ from __future__ import annotations
2
+
3
+ import unittest
4
+
5
+ from tteg.sources.unsplash import _build_image_url, _normalize_result
6
+
7
+
8
+ class UnsplashTests(unittest.TestCase):
9
+ def test_build_image_url_applies_size_params(self) -> None:
10
+ actual = _build_image_url(
11
+ "https://images.unsplash.com/photo-123",
12
+ "https://images.unsplash.com/fallback",
13
+ 1920,
14
+ 1080,
15
+ )
16
+ self.assertEqual(
17
+ actual,
18
+ "https://images.unsplash.com/photo-123?w=1920&h=1080&fit=crop&q=80",
19
+ )
20
+
21
+ def test_normalize_result_keeps_compliance_fields(self) -> None:
22
+ photo = {
23
+ "id": "abc",
24
+ "description": "Golden hour",
25
+ "width": 4000,
26
+ "height": 3000,
27
+ "urls": {
28
+ "raw": "https://images.unsplash.com/raw",
29
+ "regular": "https://images.unsplash.com/regular",
30
+ "thumb": "https://images.unsplash.com/thumb",
31
+ },
32
+ "links": {
33
+ "html": "https://unsplash.com/photos/abc",
34
+ "download_location": "https://api.unsplash.com/photos/abc/download",
35
+ },
36
+ "user": {"name": "Jane Doe"},
37
+ }
38
+
39
+ normalized = _normalize_result(photo, 1600, 900)
40
+
41
+ self.assertEqual(normalized.id, "abc")
42
+ self.assertEqual(normalized.photographer, "Jane Doe")
43
+ self.assertEqual(normalized.html_url, "https://unsplash.com/photos/abc")
44
+ self.assertEqual(
45
+ normalized.download_location,
46
+ "https://api.unsplash.com/photos/abc/download",
47
+ )
48
+ self.assertIn("w=1600", normalized.image_url)
49
+
50
+
51
+ if __name__ == "__main__":
52
+ unittest.main()
53
+
@@ -0,0 +1,4 @@
1
+ from .cli import main
2
+
3
+ __all__ = ["main"]
4
+
@@ -0,0 +1,6 @@
1
+ from .cli import main
2
+
3
+
4
+ if __name__ == "__main__":
5
+ main()
6
+
tteg-0.1.0/tteg/cli.py ADDED
@@ -0,0 +1,109 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import os
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+ import click
9
+
10
+ from tteg.preview import build_preview
11
+ from tteg.sources import search_unsplash
12
+
13
+
14
+ def _load_env_file() -> None:
15
+ candidates = [
16
+ Path.cwd() / ".env",
17
+ Path.home() / ".config" / "tteg" / ".env",
18
+ Path.home() / ".tteg.env",
19
+ ]
20
+ for env_path in candidates:
21
+ if not env_path.exists():
22
+ continue
23
+ for line in env_path.read_text(encoding="utf-8").splitlines():
24
+ stripped = line.strip()
25
+ if not stripped or stripped.startswith("#") or "=" not in stripped:
26
+ continue
27
+ key, value = stripped.split("=", 1)
28
+ key = key.strip()
29
+ value = value.strip().strip('"').strip("'")
30
+ if key and key not in os.environ:
31
+ os.environ[key] = value
32
+ break # use first env file found
33
+
34
+
35
+ def _resolve_access_key() -> str:
36
+ return os.environ.get("UNSPLASH_ACCESS_KEY") or os.environ.get("ACCESS_KEY") or ""
37
+
38
+
39
+ def _response_payload(
40
+ *,
41
+ query: str,
42
+ results: list[Any],
43
+ preview_enabled: bool,
44
+ ) -> dict[str, Any]:
45
+ payload: dict[str, Any] = {
46
+ "query": query,
47
+ "results": [item.to_dict() for item in results],
48
+ "preview": None,
49
+ "warnings": [],
50
+ }
51
+ if not preview_enabled:
52
+ return payload
53
+
54
+ preview_result = build_preview(results)
55
+ if preview_result.preview is not None:
56
+ payload["preview"] = preview_result.preview.to_dict()
57
+ if preview_result.warnings:
58
+ payload["warnings"].extend(preview_result.warnings)
59
+ return payload
60
+
61
+
62
+ @click.command(context_settings={"help_option_names": ["-h", "--help"]})
63
+ @click.argument("query")
64
+ @click.option("-n", "--count", default=5, show_default=True, type=click.IntRange(1, 10))
65
+ @click.option(
66
+ "--orientation",
67
+ type=click.Choice(["any", "landscape", "portrait", "square"], case_sensitive=False),
68
+ default="any",
69
+ show_default=True,
70
+ )
71
+ @click.option("--width", type=click.IntRange(1, 10000), default=None)
72
+ @click.option("--height", type=click.IntRange(1, 10000), default=None)
73
+ @click.option("--preview/--no-preview", default=False, show_default=True)
74
+ def main(
75
+ query: str,
76
+ count: int,
77
+ orientation: str,
78
+ width: int | None,
79
+ height: int | None,
80
+ preview: bool,
81
+ ) -> None:
82
+ """Search stock images and return URLs plus an inline preview artifact."""
83
+ _load_env_file()
84
+ access_key = _resolve_access_key()
85
+ if not access_key:
86
+ click.echo("Error: No Unsplash API key found.", err=True)
87
+ click.echo("Set UNSPLASH_ACCESS_KEY env var or put it in .env / ~/.config/tteg/.env", err=True)
88
+ raise SystemExit(1)
89
+
90
+ try:
91
+ results = search_unsplash(
92
+ access_key=access_key,
93
+ query=query,
94
+ count=count,
95
+ orientation=orientation,
96
+ width=width,
97
+ height=height,
98
+ )
99
+ except Exception as exc:
100
+ click.echo(f"Error: {exc}", err=True)
101
+ raise SystemExit(1)
102
+
103
+ payload = _response_payload(query=query, results=results, preview_enabled=preview)
104
+ click.echo(json.dumps(payload, indent=2))
105
+
106
+
107
+ if __name__ == "__main__":
108
+ main()
109
+
@@ -0,0 +1,33 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import asdict, dataclass
4
+ from typing import Any
5
+
6
+
7
+ @dataclass(slots=True)
8
+ class ImageResult:
9
+ id: str
10
+ source: str
11
+ image_url: str
12
+ thumb_url: str
13
+ photographer: str | None
14
+ width: int | None
15
+ height: int | None
16
+ title: str | None
17
+ html_url: str | None
18
+ download_location: str | None
19
+
20
+ def to_dict(self) -> dict[str, Any]:
21
+ return asdict(self)
22
+
23
+
24
+ @dataclass(slots=True)
25
+ class PreviewPayload:
26
+ mime_type: str
27
+ width: int
28
+ height: int
29
+ data_base64: str
30
+
31
+ def to_dict(self) -> dict[str, Any]:
32
+ return asdict(self)
33
+
@@ -0,0 +1,134 @@
1
+ from __future__ import annotations
2
+
3
+ import base64
4
+ import io
5
+ from dataclasses import dataclass
6
+ from typing import Callable
7
+
8
+ import requests
9
+ from PIL import Image, ImageDraw, ImageFont
10
+
11
+ from tteg.models import ImageResult, PreviewPayload
12
+
13
+ Downloader = Callable[[str], bytes]
14
+
15
+ CARD_WIDTH = 320
16
+ CARD_PADDING = 12
17
+ LABEL_SIZE = 30
18
+ LABEL_MARGIN = 10
19
+ BACKGROUND_COLOR = "#f4f1eb"
20
+ CARD_BACKGROUND = "#ffffff"
21
+ LABEL_FILL = "#111111"
22
+ LABEL_TEXT = "#ffffff"
23
+ TEXT_COLOR = "#1b1b1b"
24
+ JPEG_QUALITY = 78
25
+
26
+
27
+ @dataclass(slots=True)
28
+ class PreviewBuildResult:
29
+ preview: PreviewPayload | None
30
+ warnings: list[str]
31
+
32
+
33
+ def _default_downloader(url: str) -> bytes:
34
+ response = requests.get(url, timeout=15)
35
+ response.raise_for_status()
36
+ return response.content
37
+
38
+
39
+ def _open_and_resize(image_bytes: bytes) -> Image.Image:
40
+ image = Image.open(io.BytesIO(image_bytes)).convert("RGB")
41
+ scale = CARD_WIDTH / image.width
42
+ target_height = max(1, int(image.height * scale))
43
+ return image.resize((CARD_WIDTH, target_height))
44
+
45
+
46
+ def _measure_text(draw: ImageDraw.ImageDraw, text: str, font: ImageFont.ImageFont) -> tuple[int, int]:
47
+ left, top, right, bottom = draw.textbbox((0, 0), text, font=font)
48
+ return right - left, bottom - top
49
+
50
+
51
+ def build_preview(
52
+ results: list[ImageResult],
53
+ *,
54
+ downloader: Downloader | None = None,
55
+ ) -> PreviewBuildResult:
56
+ if not results:
57
+ return PreviewBuildResult(preview=None, warnings=["No results to preview."])
58
+
59
+ get_bytes = downloader or _default_downloader
60
+ font = ImageFont.load_default()
61
+ prepared: list[tuple[int, Image.Image, str | None]] = []
62
+ warnings: list[str] = []
63
+
64
+ for index, result in enumerate(results, start=1):
65
+ if not result.thumb_url:
66
+ warnings.append(f"Missing thumbnail URL for result {index}.")
67
+ continue
68
+ try:
69
+ prepared.append((index, _open_and_resize(get_bytes(result.thumb_url)), result.title))
70
+ except Exception as exc: # pragma: no cover - exercised through warning behavior
71
+ warnings.append(f"Failed to build preview tile {index}: {exc}")
72
+
73
+ if not prepared:
74
+ return PreviewBuildResult(preview=None, warnings=warnings or ["No preview tiles could be downloaded."])
75
+
76
+ title_height = 18 # approximate height for title text line
77
+ heights = [
78
+ image.height + (CARD_PADDING * 2) + (title_height if title else 0)
79
+ for _, image, title in prepared
80
+ ]
81
+ canvas_width = CARD_WIDTH + (CARD_PADDING * 2)
82
+ canvas_height = sum(heights) + (CARD_PADDING * (len(prepared) + 1))
83
+ canvas = Image.new("RGB", (canvas_width, canvas_height), BACKGROUND_COLOR)
84
+ draw = ImageDraw.Draw(canvas)
85
+
86
+ cursor_y = CARD_PADDING
87
+ for index, image, title in prepared:
88
+ card_left = CARD_PADDING
89
+ card_top = cursor_y
90
+ card_bottom = card_top + image.height + (CARD_PADDING * 2)
91
+ draw.rounded_rectangle(
92
+ [(card_left, card_top), (card_left + CARD_WIDTH + (CARD_PADDING * 2), card_bottom)],
93
+ radius=16,
94
+ fill=CARD_BACKGROUND,
95
+ )
96
+
97
+ image_x = card_left + CARD_PADDING
98
+ image_y = card_top + CARD_PADDING
99
+ canvas.paste(image, (image_x, image_y))
100
+
101
+ label_left = image_x + LABEL_MARGIN
102
+ label_top = image_y + LABEL_MARGIN
103
+ label_box = (label_left, label_top, label_left + LABEL_SIZE, label_top + LABEL_SIZE)
104
+ draw.rounded_rectangle(label_box, radius=12, fill=LABEL_FILL)
105
+ label_text = str(index)
106
+ text_width, text_height = _measure_text(draw, label_text, font)
107
+ draw.text(
108
+ (
109
+ label_left + ((LABEL_SIZE - text_width) / 2),
110
+ label_top + ((LABEL_SIZE - text_height) / 2) - 1,
111
+ ),
112
+ label_text,
113
+ font=font,
114
+ fill=LABEL_TEXT,
115
+ )
116
+
117
+ if title:
118
+ trimmed = title[:60]
119
+ text_y = image_y + image.height + 4
120
+ draw.text((image_x, text_y), trimmed, font=font, fill=TEXT_COLOR)
121
+
122
+ cursor_y = card_bottom + CARD_PADDING
123
+
124
+ output = io.BytesIO()
125
+ canvas.save(output, format="JPEG", quality=JPEG_QUALITY, optimize=True)
126
+ encoded = base64.b64encode(output.getvalue()).decode("ascii")
127
+ preview = PreviewPayload(
128
+ mime_type="image/jpeg",
129
+ width=canvas.width,
130
+ height=canvas.height,
131
+ data_base64=encoded,
132
+ )
133
+ return PreviewBuildResult(preview=preview, warnings=warnings)
134
+
@@ -0,0 +1,4 @@
1
+ from .unsplash import search_unsplash
2
+
3
+ __all__ = ["search_unsplash"]
4
+
@@ -0,0 +1,89 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ import requests
6
+
7
+ from tteg.models import ImageResult
8
+
9
+ UNSPLASH_SEARCH_URL = "https://api.unsplash.com/search/photos"
10
+ DEFAULT_TIMEOUT_SECONDS = 15
11
+
12
+
13
+ def _build_image_url(raw_url: str | None, fallback_url: str | None, width: int | None, height: int | None) -> str:
14
+ if width is None and height is None:
15
+ return fallback_url or raw_url or ""
16
+
17
+ if not raw_url:
18
+ return fallback_url or ""
19
+
20
+ params: list[str] = []
21
+ if width is not None:
22
+ params.append(f"w={width}")
23
+ if height is not None:
24
+ params.append(f"h={height}")
25
+ if width is not None and height is not None:
26
+ params.append("fit=crop")
27
+ else:
28
+ params.append("fit=max")
29
+ params.append("q=80")
30
+ separator = "&" if "?" in raw_url else "?"
31
+ return f"{raw_url}{separator}{'&'.join(params)}"
32
+
33
+
34
+ def _normalize_result(photo: dict[str, Any], width: int | None, height: int | None) -> ImageResult:
35
+ urls = photo.get("urls") or {}
36
+ links = photo.get("links") or {}
37
+ user = photo.get("user") or {}
38
+ title = photo.get("description") or photo.get("alt_description")
39
+
40
+ return ImageResult(
41
+ id=str(photo.get("id", "")),
42
+ source="unsplash",
43
+ image_url=_build_image_url(urls.get("raw"), urls.get("regular"), width, height),
44
+ thumb_url=urls.get("thumb") or urls.get("small") or urls.get("regular") or "",
45
+ photographer=user.get("name"),
46
+ width=photo.get("width"),
47
+ height=photo.get("height"),
48
+ title=title,
49
+ html_url=links.get("html"),
50
+ download_location=links.get("download_location"),
51
+ )
52
+
53
+
54
+ def search_unsplash(
55
+ *,
56
+ access_key: str,
57
+ query: str,
58
+ count: int,
59
+ orientation: str | None = None,
60
+ width: int | None = None,
61
+ height: int | None = None,
62
+ session: requests.Session | None = None,
63
+ ) -> list[ImageResult]:
64
+ if not access_key:
65
+ raise ValueError("Missing Unsplash access key. Set UNSPLASH_ACCESS_KEY or ACCESS_KEY.")
66
+
67
+ client = session or requests.Session()
68
+ params: dict[str, Any] = {
69
+ "query": query,
70
+ "per_page": max(1, min(count, 30)),
71
+ }
72
+ if orientation and orientation != "any":
73
+ params["orientation"] = "squarish" if orientation == "square" else orientation
74
+
75
+ response = client.get(
76
+ UNSPLASH_SEARCH_URL,
77
+ params=params,
78
+ headers={
79
+ "Authorization": f"Client-ID {access_key}",
80
+ "Accept-Version": "v1",
81
+ "User-Agent": "tteg/0.1.0",
82
+ },
83
+ timeout=DEFAULT_TIMEOUT_SECONDS,
84
+ )
85
+ response.raise_for_status()
86
+ payload = response.json()
87
+ raw_results = payload.get("results") or []
88
+ return [_normalize_result(item, width, height) for item in raw_results if item.get("urls")]
89
+
@@ -0,0 +1,74 @@
1
+ Metadata-Version: 2.4
2
+ Name: tteg
3
+ Version: 0.1.0
4
+ Summary: Agent-friendly stock image search CLI. No rate-limit BS.
5
+ Author: Kushal
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/kushal/tteg
8
+ Keywords: unsplash,stock-images,cli,ai-agents,image-search
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Topic :: Multimedia :: Graphics
14
+ Requires-Python: >=3.10
15
+ Description-Content-Type: text/markdown
16
+ Requires-Dist: click>=8.1.7
17
+ Requires-Dist: requests>=2.32.0
18
+ Requires-Dist: Pillow>=10.0.0
19
+
20
+ # tteg
21
+
22
+ `tteg` is a Python CLI for agent-friendly stock-image search.
23
+
24
+ Current shape:
25
+
26
+ - one command
27
+ - Unsplash-backed search
28
+ - JSON output by default
29
+ - inline stitched preview as base64 JPEG
30
+
31
+ ## Setup
32
+
33
+ `tteg` reads environment variables from the shell and also loads a local `.env` file from the current working directory if present.
34
+
35
+ Supported keys:
36
+
37
+ - `UNSPLASH_ACCESS_KEY`
38
+ - `ACCESS_KEY` as a fallback alias
39
+
40
+ ## Usage
41
+
42
+ ```bash
43
+ python3 -m tteg "mountain sunset"
44
+ python3 -m tteg "hero banner" --count 8 --orientation landscape --width 1920 --height 1080
45
+ ```
46
+
47
+ Example response shape:
48
+
49
+ ```json
50
+ {
51
+ "query": "mountain sunset",
52
+ "results": [
53
+ {
54
+ "id": "abc123",
55
+ "source": "unsplash",
56
+ "image_url": "https://images.unsplash.com/...",
57
+ "thumb_url": "https://images.unsplash.com/...",
58
+ "photographer": "Jane Doe",
59
+ "width": 4000,
60
+ "height": 3000,
61
+ "html_url": "https://unsplash.com/photos/...",
62
+ "download_location": "https://api.unsplash.com/photos/.../download"
63
+ }
64
+ ],
65
+ "preview": {
66
+ "mime_type": "image/jpeg",
67
+ "width": 320,
68
+ "height": 1108,
69
+ "data_base64": "..."
70
+ },
71
+ "warnings": []
72
+ }
73
+ ```
74
+
@@ -0,0 +1,17 @@
1
+ README.md
2
+ pyproject.toml
3
+ tests/test_preview.py
4
+ tests/test_unsplash.py
5
+ tteg/__init__.py
6
+ tteg/__main__.py
7
+ tteg/cli.py
8
+ tteg/models.py
9
+ tteg/preview.py
10
+ tteg.egg-info/PKG-INFO
11
+ tteg.egg-info/SOURCES.txt
12
+ tteg.egg-info/dependency_links.txt
13
+ tteg.egg-info/entry_points.txt
14
+ tteg.egg-info/requires.txt
15
+ tteg.egg-info/top_level.txt
16
+ tteg/sources/__init__.py
17
+ tteg/sources/unsplash.py
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ tteg = tteg.cli:main
@@ -0,0 +1,3 @@
1
+ click>=8.1.7
2
+ requests>=2.32.0
3
+ Pillow>=10.0.0
@@ -0,0 +1,3 @@
1
+ dist
2
+ tests
3
+ tteg