olapp 0.2.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.
olapp/__init__.py ADDED
@@ -0,0 +1,71 @@
1
+ """olapp — A modern alternative to Gradio."""
2
+
3
+ from .version import __version__
4
+ from .components import (
5
+ Component,
6
+ Textbox,
7
+ Number,
8
+ Slider,
9
+ Checkbox,
10
+ Dropdown,
11
+ Radio,
12
+ Button,
13
+ Image,
14
+ Audio,
15
+ Video,
16
+ File,
17
+ Dataframe,
18
+ Markdown,
19
+ HTML,
20
+ Chatbot,
21
+ State,
22
+ ColorPicker,
23
+ DateTime,
24
+ Code,
25
+ Gallery,
26
+ Label,
27
+ HighlightedText,
28
+ JSON,
29
+ Progress,
30
+ )
31
+ from .interface import Interface
32
+ from .blocks import Blocks, Row, Column, Group, Tab, Tabs, Accordion
33
+ from .server import OlappServer
34
+
35
+ __all__ = [
36
+ "__version__",
37
+ "Interface",
38
+ "Blocks",
39
+ "Row",
40
+ "Column",
41
+ "Group",
42
+ "Tab",
43
+ "Tabs",
44
+ "Accordion",
45
+ "Component",
46
+ "Textbox",
47
+ "Number",
48
+ "Slider",
49
+ "Checkbox",
50
+ "Dropdown",
51
+ "Radio",
52
+ "Button",
53
+ "Image",
54
+ "Audio",
55
+ "Video",
56
+ "File",
57
+ "Dataframe",
58
+ "Markdown",
59
+ "HTML",
60
+ "Chatbot",
61
+ "State",
62
+ "ColorPicker",
63
+ "DateTime",
64
+ "Code",
65
+ "Gallery",
66
+ "Label",
67
+ "HighlightedText",
68
+ "JSON",
69
+ "Progress",
70
+ "OlappServer",
71
+ ]
olapp/blocks.py ADDED
@@ -0,0 +1,431 @@
1
+ """olapp Blocks — flexible layout system for building complex interfaces."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import json
7
+ import logging
8
+ import threading
9
+ from contextlib import contextmanager
10
+ from typing import Any, Callable, Dict, List, Optional, Tuple, Union
11
+
12
+ from .components import (
13
+ Component, Textbox, Number, Slider, Checkbox, Dropdown, Radio, Button,
14
+ Image, Audio, Video, File, Dataframe, Markdown, HTML, Chatbot, State,
15
+ )
16
+ from .server import OlappServer
17
+ from .routes import RouteManager
18
+ from .utils import validate_function, NumpyEncoder
19
+
20
+ logger = logging.getLogger("olapp")
21
+
22
+ # Global context stack for auto-registering components
23
+ _context_stack: List["Block"] = []
24
+
25
+
26
+ def get_current_context() -> Optional["Block"]:
27
+ """Get the current block context, if any."""
28
+ return _context_stack[-1] if _context_stack else None
29
+
30
+
31
+ def push_context(block: "Block"):
32
+ """Push a block onto the context stack."""
33
+ _context_stack.append(block)
34
+
35
+
36
+ def pop_context():
37
+ """Pop the current block from the context stack."""
38
+ if _context_stack:
39
+ _context_stack.pop()
40
+
41
+
42
+ class Block:
43
+ """Base class for layout blocks."""
44
+
45
+ def __init__(self, elem_id: str = None):
46
+ from .utils import generate_id
47
+ self.elem_id = elem_id or generate_id()
48
+ self.children: List[Union[Block, Component]] = []
49
+ self._parent = None
50
+
51
+ def add(self, child: Union["Block", Component]) -> "Block":
52
+ """Add a child to this block."""
53
+ child._parent = self
54
+ self.children.append(child)
55
+ return self
56
+
57
+ def get_layout(self) -> Dict[str, Any]:
58
+ """Get the layout configuration."""
59
+ children_configs = []
60
+ for child in self.children:
61
+ if isinstance(child, Block):
62
+ children_configs.append(child.get_layout())
63
+ elif isinstance(child, Component):
64
+ children_configs.append(child.get_config())
65
+ return {
66
+ "type": self.__class__.__name__.lower(),
67
+ "id": self.elem_id,
68
+ "children": children_configs,
69
+ }
70
+
71
+ def get_components(self) -> List[Component]:
72
+ """Get all components in this block."""
73
+ components = []
74
+ for child in self.children:
75
+ if isinstance(child, Block):
76
+ components.extend(child.get_components())
77
+ elif isinstance(child, Component):
78
+ components.append(child)
79
+ return components
80
+
81
+
82
+ class Row(Block):
83
+ """Horizontal row layout."""
84
+
85
+ def __init__(self, equal_height: bool = True, elem_id: str = None):
86
+ super().__init__(elem_id=elem_id)
87
+ self.equal_height = equal_height
88
+
89
+ def get_layout(self) -> Dict[str, Any]:
90
+ layout = super().get_layout()
91
+ layout["equal_height"] = self.equal_height
92
+ return layout
93
+
94
+
95
+ class Column(Block):
96
+ """Vertical column layout."""
97
+
98
+ def __init__(self, scale: int = 1, min_width: int = 0, elem_id: str = None):
99
+ super().__init__(elem_id=elem_id)
100
+ self.scale = scale
101
+ self.min_width = min_width
102
+
103
+ def get_layout(self) -> Dict[str, Any]:
104
+ layout = super().get_layout()
105
+ layout["scale"] = self.scale
106
+ layout["min_width"] = self.min_width
107
+ return layout
108
+
109
+
110
+ class Group(Block):
111
+ """Group container with optional border."""
112
+
113
+ def __init__(self, visible: bool = True, elem_id: str = None):
114
+ super().__init__(elem_id=elem_id)
115
+ self.visible = visible
116
+
117
+
118
+ class Tab(Block):
119
+ """Tab panel."""
120
+
121
+ def __init__(self, label: str = "Tab", elem_id: str = None):
122
+ super().__init__(elem_id=elem_id)
123
+ self.label = label
124
+
125
+ def get_layout(self) -> Dict[str, Any]:
126
+ layout = super().get_layout()
127
+ layout["label"] = self.label
128
+ return layout
129
+
130
+
131
+ class Tabs(Block):
132
+ """Tabbed container."""
133
+
134
+ def __init__(self, elem_id: str = None):
135
+ super().__init__(elem_id=elem_id)
136
+
137
+ def add_tab(self, tab: Tab) -> "Tabs":
138
+ """Add a tab."""
139
+ self.add(tab)
140
+ return self
141
+
142
+
143
+ class Accordion(Block):
144
+ """Collapsible section."""
145
+
146
+ def __init__(self, label: str = "Accordion", open: bool = False, elem_id: str = None):
147
+ super().__init__(elem_id=elem_id)
148
+ self.label = label
149
+ self.is_open = open
150
+
151
+ def get_layout(self) -> Dict[str, Any]:
152
+ layout = super().get_layout()
153
+ layout["label"] = self.label
154
+ layout["open"] = self.is_open
155
+ return layout
156
+
157
+
158
+ class Blocks:
159
+ """Flexible Blocks API for building complex interfaces."""
160
+
161
+ def __init__(
162
+ self,
163
+ title: str = "Olapp",
164
+ theme: str = "default",
165
+ css: str = None,
166
+ **kwargs,
167
+ ):
168
+ self.title = title
169
+ self.theme = theme
170
+ self.css = css
171
+ self.blocks: List[Union[Block, Component]] = []
172
+ self.fns: Dict[str, Callable] = {}
173
+ self.dependencies: List[Dict[str, Any]] = []
174
+ self._context_stack: List[Block] = []
175
+ self.server: Optional[OlappServer] = None
176
+
177
+ def add(self, item: Union[Block, Component]) -> "Blocks":
178
+ """Add a block or component to the layout."""
179
+ if self._context_stack:
180
+ self._context_stack[-1].add(item)
181
+ else:
182
+ self.blocks.append(item)
183
+ return self
184
+
185
+ @contextmanager
186
+ def row(self, **kwargs):
187
+ """Context manager for row layout."""
188
+ row = Row(**kwargs)
189
+ self.add(row)
190
+ self._context_stack.append(row)
191
+ push_context(row)
192
+ yield row
193
+ pop_context()
194
+ self._context_stack.pop()
195
+
196
+ @contextmanager
197
+ def column(self, **kwargs):
198
+ """Context manager for column layout."""
199
+ col = Column(**kwargs)
200
+ self.add(col)
201
+ self._context_stack.append(col)
202
+ push_context(col)
203
+ yield col
204
+ pop_context()
205
+ self._context_stack.pop()
206
+
207
+ @contextmanager
208
+ def group(self, **kwargs):
209
+ """Context manager for group."""
210
+ group = Group(**kwargs)
211
+ self.add(group)
212
+ self._context_stack.append(group)
213
+ push_context(group)
214
+ yield group
215
+ pop_context()
216
+ self._context_stack.pop()
217
+
218
+ @contextmanager
219
+ def tab(self, label: str = "Tab", **kwargs):
220
+ """Context manager for a tab panel."""
221
+ tab = Tab(label=label, **kwargs)
222
+ self.add(tab)
223
+ self._context_stack.append(tab)
224
+ push_context(tab)
225
+ yield tab
226
+ pop_context()
227
+ self._context_stack.pop()
228
+
229
+ @contextmanager
230
+ def tabs(self, **kwargs):
231
+ """Context manager for tabbed container."""
232
+ tabs = Tabs(**kwargs)
233
+ self.add(tabs)
234
+ self._context_stack.append(tabs)
235
+ push_context(tabs)
236
+ yield tabs
237
+ pop_context()
238
+ self._context_stack.pop()
239
+
240
+ def click(
241
+ self,
242
+ fn: Callable,
243
+ inputs: Union[Component, List[Component]] = None,
244
+ outputs: Union[Component, List[Component]] = None,
245
+ api_name: str = None,
246
+ **kwargs,
247
+ ):
248
+ """Register a click event handler."""
249
+ if not isinstance(inputs, list):
250
+ inputs = [inputs] if inputs else []
251
+ if not isinstance(outputs, list):
252
+ outputs = [outputs] if outputs else []
253
+
254
+ from .utils import generate_id
255
+ api_name = api_name or f"click_{generate_id()}"
256
+ self.fns[api_name] = fn
257
+ self.dependencies.append({
258
+ "trigger": "click",
259
+ "inputs": [c.elem_id for c in inputs if isinstance(c, Component)],
260
+ "outputs": [c.elem_id for c in outputs if isinstance(c, Component)],
261
+ "api_name": api_name,
262
+ "fn": fn,
263
+ })
264
+
265
+ def change(
266
+ self,
267
+ fn: Callable,
268
+ inputs: Union[Component, List[Component]] = None,
269
+ outputs: Union[Component, List[Component]] = None,
270
+ api_name: str = None,
271
+ **kwargs,
272
+ ):
273
+ """Register a change event handler."""
274
+ if not isinstance(inputs, list):
275
+ inputs = [inputs] if inputs else []
276
+ if not isinstance(outputs, list):
277
+ outputs = [outputs] if outputs else []
278
+
279
+ from .utils import generate_id
280
+ api_name = api_name or f"change_{generate_id()}"
281
+ self.fns[api_name] = fn
282
+ self.dependencies.append({
283
+ "trigger": "change",
284
+ "inputs": [c.elem_id for c in inputs if isinstance(c, Component)],
285
+ "outputs": [c.elem_id for c in outputs if isinstance(c, Component)],
286
+ "api_name": api_name,
287
+ "fn": fn,
288
+ })
289
+
290
+ def submit(
291
+ self,
292
+ fn: Callable,
293
+ inputs: Union[Component, List[Component]] = None,
294
+ outputs: Union[Component, List[Component]] = None,
295
+ api_name: str = None,
296
+ **kwargs,
297
+ ):
298
+ """Register a submit event handler."""
299
+ if not isinstance(inputs, list):
300
+ inputs = [inputs] if inputs else []
301
+ if not isinstance(outputs, list):
302
+ outputs = [outputs] if outputs else []
303
+
304
+ from .utils import generate_id
305
+ api_name = api_name or f"submit_{generate_id()}"
306
+ self.fns[api_name] = fn
307
+ self.dependencies.append({
308
+ "trigger": "submit",
309
+ "inputs": [c.elem_id for c in inputs if isinstance(c, Component)],
310
+ "outputs": [c.elem_id for c in outputs if isinstance(c, Component)],
311
+ "api_name": api_name,
312
+ "fn": fn,
313
+ })
314
+
315
+ def get_all_components(self) -> List[Component]:
316
+ """Get all components in the layout."""
317
+ components = []
318
+ for item in self.blocks:
319
+ if isinstance(item, Block):
320
+ components.extend(item.get_components())
321
+ elif isinstance(item, Component):
322
+ components.append(item)
323
+ return components
324
+
325
+ def get_component_by_id(self, elem_id: str) -> Optional[Component]:
326
+ """Find a component by its ID."""
327
+ for comp in self.get_all_components():
328
+ if comp.elem_id == elem_id:
329
+ return comp
330
+ return None
331
+
332
+ def get_config(self) -> Dict[str, Any]:
333
+ """Get the full app configuration."""
334
+ layout = []
335
+ for item in self.blocks:
336
+ if isinstance(item, Block):
337
+ layout.append(item.get_layout())
338
+ elif isinstance(item, Component):
339
+ layout.append(item.get_config())
340
+
341
+ deps = []
342
+ for dep in self.dependencies:
343
+ deps.append({
344
+ "trigger": dep["trigger"],
345
+ "inputs": dep["inputs"],
346
+ "outputs": dep["outputs"],
347
+ "api_name": dep["api_name"],
348
+ })
349
+
350
+ return {
351
+ "title": self.title,
352
+ "theme": self.theme,
353
+ "mode": "blocks",
354
+ "layout": layout,
355
+ "dependencies": deps,
356
+ "css": self.css,
357
+ }
358
+
359
+ def launch(
360
+ self,
361
+ server_name: str = "127.0.0.1",
362
+ server_port: int = 7860,
363
+ share: bool = False,
364
+ prevent_thread_lock: bool = False,
365
+ **kwargs,
366
+ ):
367
+ """Launch the blocks app."""
368
+ self.server = OlappServer(
369
+ host=server_name,
370
+ port=server_port,
371
+ title=self.title,
372
+ theme=self.theme,
373
+ )
374
+ self.server._get_config = self.get_config
375
+ self.server._process_predict = self._handle_predict
376
+
377
+ # Register dependency endpoints
378
+ for dep in self.dependencies:
379
+ api_name = dep["api_name"]
380
+ fn = dep["fn"]
381
+ self.server.add_route(
382
+ f"/api/{api_name}",
383
+ lambda data, _fn=fn: self._call_fn(_fn, data),
384
+ )
385
+
386
+ if prevent_thread_lock:
387
+ thread = threading.Thread(target=self.server.run, daemon=True)
388
+ thread.start()
389
+ return f"http://{server_name}:{server_port}", None
390
+ else:
391
+ self.server.run()
392
+ return f"http://{server_name}:{server_port}", None
393
+
394
+ async def _handle_predict(self, data: Dict[str, Any]) -> Any:
395
+ """Handle a generic prediction request."""
396
+ api_name = data.get("api_name", "default")
397
+ fn_data = data.get("data", {})
398
+
399
+ if api_name in self.fns:
400
+ fn = self.fns[api_name]
401
+ else:
402
+ # Try to find the first dependency
403
+ if self.dependencies:
404
+ fn = self.dependencies[0]["fn"]
405
+ else:
406
+ raise ValueError("No function registered")
407
+
408
+ return self._call_fn(fn, fn_data)
409
+
410
+ def _call_fn(self, fn: Callable, data: Any) -> Any:
411
+ """Call a function with data."""
412
+ if isinstance(data, dict) and "data" in data:
413
+ data = data["data"]
414
+ if not isinstance(data, list):
415
+ data = [data]
416
+ result = fn(*data)
417
+ if not isinstance(result, (list, tuple)):
418
+ result = [result]
419
+ return result
420
+
421
+ def close(self):
422
+ """Close the server."""
423
+ if self.server:
424
+ asyncio.get_event_loop().run_until_complete(self.server.stop())
425
+
426
+ def __enter__(self):
427
+ push_context(self)
428
+ return self
429
+
430
+ def __exit__(self, *args):
431
+ pop_context()