lenslet 0.2.1__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.
lenslet/__init__.py ADDED
@@ -0,0 +1,7 @@
1
+ """Lenslet: A lightweight image gallery server."""
2
+ __version__ = "0.1.0"
3
+
4
+ from .api import launch
5
+
6
+ __all__ = ["launch", "__version__"]
7
+
lenslet/api.py ADDED
@@ -0,0 +1,157 @@
1
+ """Programmatic API for launching lenslet from Python/notebooks."""
2
+ from __future__ import annotations
3
+ import sys
4
+ import subprocess
5
+ import multiprocessing as mp
6
+ from typing import Any
7
+
8
+
9
+ def launch(
10
+ datasets: dict[str, list[str]],
11
+ blocking: bool = False,
12
+ port: int = 7070,
13
+ host: str = "127.0.0.1",
14
+ thumb_size: int = 256,
15
+ thumb_quality: int = 70,
16
+ verbose: bool = False,
17
+ ) -> None:
18
+ """
19
+ Launch lenslet with in-memory datasets.
20
+
21
+ Args:
22
+ datasets: Dict of {dataset_name: [list of image paths/URIs]}
23
+ Supports both local file paths and S3 URIs (s3://...)
24
+ blocking: If False (default), launches in subprocess. If True, runs in current process.
25
+ port: Port to listen on (default: 7070)
26
+ host: Host to bind to (default: 127.0.0.1)
27
+ thumb_size: Thumbnail short edge size in pixels (default: 256)
28
+ thumb_quality: Thumbnail WEBP quality 1-100 (default: 70)
29
+ verbose: If True, show all server logs. If False (default), only show errors.
30
+
31
+ Example:
32
+ >>> import lenslet
33
+ >>> datasets = {
34
+ ... "my_images": ["/path/to/img1.jpg", "/path/to/img2.png"],
35
+ ... "s3_images": ["s3://bucket/image1.jpg", "s3://bucket/image2.jpg"],
36
+ ... }
37
+ >>> lenslet.launch(datasets, blocking=False, port=7070)
38
+ """
39
+ if not datasets:
40
+ raise ValueError("datasets cannot be empty")
41
+
42
+ if blocking:
43
+ # Run in current process
44
+ _launch_blocking(
45
+ datasets=datasets,
46
+ port=port,
47
+ host=host,
48
+ thumb_size=thumb_size,
49
+ thumb_quality=thumb_quality,
50
+ verbose=verbose,
51
+ )
52
+ else:
53
+ # Launch in subprocess
54
+ _launch_subprocess(
55
+ datasets=datasets,
56
+ port=port,
57
+ host=host,
58
+ thumb_size=thumb_size,
59
+ thumb_quality=thumb_quality,
60
+ verbose=verbose,
61
+ )
62
+
63
+
64
+ def _launch_blocking(
65
+ datasets: dict[str, list[str]],
66
+ port: int,
67
+ host: str,
68
+ thumb_size: int,
69
+ thumb_quality: int,
70
+ verbose: bool,
71
+ ) -> None:
72
+ """Launch in current process (blocking)."""
73
+ import uvicorn
74
+ from .server import create_app_from_datasets
75
+
76
+ # Print startup banner
77
+ total_images = sum(len(paths) for paths in datasets.values())
78
+ dataset_list = ", ".join(datasets.keys())
79
+
80
+ print(f"""
81
+ ┌─────────────────────────────────────────────────┐
82
+ │ 🔍 Lenslet │
83
+ │ Lightweight Image Gallery Server │
84
+ ├─────────────────────────────────────────────────┤
85
+ │ Datasets: {dataset_list[:35]:<35} │
86
+ │ Images: {total_images:<35} │
87
+ │ Server: http://{host}:{port:<24} │
88
+ │ Mode: In-memory (programmatic API) │
89
+ └─────────────────────────────────────────────────┘
90
+ """)
91
+
92
+ app = create_app_from_datasets(
93
+ datasets=datasets,
94
+ thumb_size=thumb_size,
95
+ thumb_quality=thumb_quality,
96
+ )
97
+
98
+ uvicorn.run(
99
+ app,
100
+ host=host,
101
+ port=port,
102
+ log_level="info" if verbose else "warning",
103
+ )
104
+
105
+
106
+ def _launch_subprocess(
107
+ datasets: dict[str, list[str]],
108
+ port: int,
109
+ host: str,
110
+ thumb_size: int,
111
+ thumb_quality: int,
112
+ verbose: bool,
113
+ ) -> None:
114
+ """Launch in subprocess (non-blocking)."""
115
+ # We'll use multiprocessing to launch in a separate process
116
+ # This allows it to work in notebooks without blocking
117
+
118
+ def _worker():
119
+ # Don't print banner in worker - parent process will print it
120
+ import uvicorn
121
+ from .server import create_app_from_datasets
122
+
123
+ app = create_app_from_datasets(
124
+ datasets=datasets,
125
+ thumb_size=thumb_size,
126
+ thumb_quality=thumb_quality,
127
+ )
128
+
129
+ uvicorn.run(
130
+ app,
131
+ host=host,
132
+ port=port,
133
+ log_level="info" if verbose else "warning",
134
+ )
135
+
136
+ process = mp.Process(target=_worker, daemon=False)
137
+ process.start()
138
+
139
+ # Print single info message in parent process
140
+ total_images = sum(len(paths) for paths in datasets.values())
141
+ dataset_list = ", ".join(datasets.keys())
142
+
143
+ print(f"""
144
+ ┌─────────────────────────────────────────────────┐
145
+ │ 🔍 Lenslet │
146
+ │ Lightweight Image Gallery Server │
147
+ ├─────────────────────────────────────────────────┤
148
+ │ Datasets: {dataset_list[:35]:<35} │
149
+ │ Images: {total_images:<35} │
150
+ │ Server: http://{host}:{port:<24} │
151
+ │ Mode: Subprocess (non-blocking) │
152
+ │ PID: {process.pid:<35} │
153
+ └─────────────────────────────────────────────────┘
154
+
155
+ Gallery running at: http://{host}:{port}
156
+ """)
157
+
lenslet/cli.py ADDED
@@ -0,0 +1,121 @@
1
+ """CLI entry point for Lenslet."""
2
+ from __future__ import annotations
3
+ import argparse
4
+ import sys
5
+ import os
6
+ from pathlib import Path
7
+
8
+
9
+ def main():
10
+ parser = argparse.ArgumentParser(
11
+ prog="lenslet",
12
+ description="Lenslet - Lightweight image gallery server",
13
+ epilog="Example: lenslet ~/Pictures --port 7070",
14
+ )
15
+ parser.add_argument(
16
+ "directory",
17
+ type=str,
18
+ nargs="?", # Make optional for --version/--help
19
+ help="Directory containing images to serve",
20
+ )
21
+ parser.add_argument(
22
+ "-p", "--port",
23
+ type=int,
24
+ default=7070,
25
+ help="Port to listen on (default: 7070)",
26
+ )
27
+ parser.add_argument(
28
+ "-H", "--host",
29
+ type=str,
30
+ default="127.0.0.1",
31
+ help="Host to bind to (default: 127.0.0.1)",
32
+ )
33
+ parser.add_argument(
34
+ "--thumb-size",
35
+ type=int,
36
+ default=256,
37
+ help="Thumbnail short edge size in pixels (default: 256)",
38
+ )
39
+ parser.add_argument(
40
+ "--thumb-quality",
41
+ type=int,
42
+ default=70,
43
+ help="Thumbnail WEBP quality 1-100 (default: 70)",
44
+ )
45
+ parser.add_argument(
46
+ "--reload",
47
+ action="store_true",
48
+ help="Enable auto-reload for development",
49
+ )
50
+ parser.add_argument(
51
+ "--no-write",
52
+ action="store_true",
53
+ help="Disable workspace writes (.lenslet/) for one-off sessions",
54
+ )
55
+ parser.add_argument(
56
+ "--verbose",
57
+ action="store_true",
58
+ help="Show detailed server logs",
59
+ )
60
+ parser.add_argument(
61
+ "-v", "--version",
62
+ action="store_true",
63
+ help="Show version and exit",
64
+ )
65
+
66
+ args = parser.parse_args()
67
+
68
+ if args.version:
69
+ from . import __version__
70
+ print(f"lenslet {__version__}")
71
+ sys.exit(0)
72
+
73
+ # Directory is required unless --version
74
+ if not args.directory:
75
+ parser.print_help()
76
+ sys.exit(1)
77
+
78
+ # Resolve and validate directory
79
+ directory = Path(args.directory).expanduser().resolve()
80
+ if not directory.is_dir():
81
+ print(f"Error: '{args.directory}' is not a valid directory", file=sys.stderr)
82
+ sys.exit(1)
83
+
84
+ # Print startup banner
85
+ has_parquet = (directory / "items.parquet").is_file()
86
+ mode_label = "Parquet dataset" if has_parquet else "In-memory (no files written)"
87
+
88
+ print(f"""
89
+ ┌─────────────────────────────────────────────────┐
90
+ │ 🔍 Lenslet │
91
+ │ Lightweight Image Gallery Server │
92
+ ├─────────────────────────────────────────────────┤
93
+ │ Directory: {str(directory)[:35]:<35} │
94
+ │ Server: http://{args.host}:{args.port:<24} │
95
+ │ Mode: {mode_label:<35} │
96
+ │ No-write: {"ON" if args.no_write else "off":<35} │
97
+ └─────────────────────────────────────────────────┘
98
+ """)
99
+
100
+ # Start server
101
+ import uvicorn
102
+ from .server import create_app
103
+
104
+ app = create_app(
105
+ root_path=str(directory),
106
+ thumb_size=args.thumb_size,
107
+ thumb_quality=args.thumb_quality,
108
+ no_write=args.no_write,
109
+ )
110
+
111
+ uvicorn.run(
112
+ app,
113
+ host=args.host,
114
+ port=args.port,
115
+ reload=args.reload,
116
+ log_level="info" if args.verbose else "warning",
117
+ )
118
+
119
+
120
+ if __name__ == "__main__":
121
+ main()