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 +71 -0
- olapp/blocks.py +431 -0
- olapp/components.py +787 -0
- olapp/interface.py +150 -0
- olapp/routes.py +126 -0
- olapp/server.py +238 -0
- olapp/static/olapp.css +1287 -0
- olapp/static/olapp.js +718 -0
- olapp/templates/index.html +83 -0
- olapp/utils.py +126 -0
- olapp/version.py +1 -0
- olapp-0.2.0.dist-info/LICENSE +21 -0
- olapp-0.2.0.dist-info/METADATA +173 -0
- olapp-0.2.0.dist-info/RECORD +16 -0
- olapp-0.2.0.dist-info/WHEEL +5 -0
- olapp-0.2.0.dist-info/top_level.txt +1 -0
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()
|