uf 0.1.8__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.
uf/__init__.py ADDED
@@ -0,0 +1,315 @@
1
+ """uf - UI Fast: Minimal-boilerplate web UIs for Python functions.
2
+
3
+ uf bridges functions to HTTP services (via qh) to Web UI forms (via ju.rjsf),
4
+ following the "convention over configuration" philosophy.
5
+
6
+ Basic usage:
7
+ >>> from uf import mk_rjsf_app
8
+ >>>
9
+ >>> def add(x: int, y: int) -> int:
10
+ ... '''Add two numbers'''
11
+ ... return x + y
12
+ >>>
13
+ >>> app = mk_rjsf_app([add])
14
+ >>> # app.run() # Start the web server
15
+
16
+ The main entry points are:
17
+ - `mk_rjsf_app`: Create a web app from functions (functional interface)
18
+ - `UfApp`: Object-oriented wrapper with additional conveniences
19
+ - `FunctionSpecStore`: Manage function specifications (advanced usage)
20
+
21
+ Advanced features:
22
+ - `ui_config`: Decorator for UI metadata
23
+ - `RjsfFieldConfig`: Field configuration class
24
+ - `FunctionGroup`: Group functions for organization
25
+ - `InputTransformRegistry`: Custom type transformations
26
+ - Field dependencies and interactions
27
+ - Testing utilities
28
+ """
29
+
30
+ # Core functionality
31
+ from uf.base import mk_rjsf_app, UfApp
32
+ from uf.specs import FunctionSpecStore
33
+
34
+ # RJSF configuration
35
+ from uf.rjsf_config import (
36
+ RjsfFieldConfig,
37
+ RjsfConfigBuilder,
38
+ get_field_config,
39
+ apply_field_configs,
40
+ ConditionalFieldConfig,
41
+ )
42
+
43
+ # Input transformation
44
+ from uf.trans import (
45
+ InputTransformRegistry,
46
+ register_type,
47
+ get_global_registry,
48
+ )
49
+
50
+ # Organization
51
+ from uf.organization import (
52
+ FunctionGroup,
53
+ FunctionOrganizer,
54
+ mk_grouped_app,
55
+ auto_group_by_prefix,
56
+ auto_group_by_module,
57
+ auto_group_by_tag,
58
+ )
59
+
60
+ # Decorators
61
+ from uf.decorators import (
62
+ ui_config,
63
+ group,
64
+ hidden,
65
+ field_config,
66
+ with_example,
67
+ deprecated,
68
+ requires_auth,
69
+ rate_limit,
70
+ get_ui_config,
71
+ get_group,
72
+ get_field_configs,
73
+ is_hidden,
74
+ get_example,
75
+ )
76
+
77
+ # Field interactions
78
+ from uf.field_interactions import (
79
+ FieldDependency,
80
+ DependencyAction,
81
+ DependencyBuilder,
82
+ with_dependencies,
83
+ get_field_dependencies,
84
+ )
85
+
86
+ # Testing utilities
87
+ from uf.testing import (
88
+ UfTestClient,
89
+ UfAppTester,
90
+ test_ui_function,
91
+ FormDataBuilder,
92
+ assert_valid_rjsf_spec,
93
+ assert_has_field,
94
+ assert_field_type,
95
+ assert_field_required,
96
+ )
97
+
98
+ # Result rendering
99
+ from uf.renderers import (
100
+ ResultRenderer,
101
+ ResultRendererRegistry,
102
+ get_global_renderer_registry,
103
+ register_renderer,
104
+ render_result,
105
+ result_renderer,
106
+ get_result_renderer,
107
+ )
108
+
109
+ # Async support
110
+ from uf.async_support import (
111
+ is_async_function,
112
+ async_to_sync,
113
+ make_sync_compatible,
114
+ AsyncFunctionWrapper,
115
+ timeout_async,
116
+ retry_async,
117
+ )
118
+
119
+ # Pydantic integration
120
+ from uf.pydantic_support import (
121
+ is_pydantic_model,
122
+ pydantic_model_to_json_schema,
123
+ function_uses_pydantic,
124
+ wrap_pydantic_function,
125
+ pydantic_to_dict,
126
+ dict_to_pydantic,
127
+ )
128
+
129
+ # History and presets
130
+ from uf.history import (
131
+ FunctionCall,
132
+ CallHistory,
133
+ Preset,
134
+ PresetManager,
135
+ HistoryManager,
136
+ get_global_history_manager,
137
+ enable_history,
138
+ )
139
+
140
+ # Authentication
141
+ from uf.auth import (
142
+ User,
143
+ AuthBackend,
144
+ DictAuthBackend,
145
+ SessionManager,
146
+ ApiKey,
147
+ ApiKeyManager,
148
+ require_auth,
149
+ get_global_auth_backend,
150
+ )
151
+
152
+ # Caching
153
+ from uf.caching import (
154
+ CacheBackend,
155
+ MemoryCache,
156
+ DiskCache,
157
+ cached,
158
+ get_global_cache_backend,
159
+ )
160
+
161
+ # Background tasks
162
+ from uf.background import (
163
+ Task,
164
+ TaskStatus,
165
+ TaskQueue,
166
+ background,
167
+ get_global_task_queue,
168
+ )
169
+
170
+ # OpenAPI
171
+ from uf.openapi import (
172
+ generate_openapi_spec,
173
+ add_openapi_routes,
174
+ OpenAPIConfig,
175
+ )
176
+
177
+ # Webhooks
178
+ from uf.webhooks import (
179
+ WebhookEvent,
180
+ WebhookClient,
181
+ WebhookManager,
182
+ webhook,
183
+ get_global_webhook_manager,
184
+ )
185
+
186
+ # Themes
187
+ from uf.themes import (
188
+ Theme,
189
+ ThemeConfig,
190
+ get_theme,
191
+ LIGHT_THEME,
192
+ DARK_THEME,
193
+ )
194
+
195
+ __version__ = "0.0.1"
196
+
197
+ __all__ = [
198
+ # Core
199
+ "mk_rjsf_app",
200
+ "UfApp",
201
+ "FunctionSpecStore",
202
+ # RJSF Config
203
+ "RjsfFieldConfig",
204
+ "RjsfConfigBuilder",
205
+ "get_field_config",
206
+ "apply_field_configs",
207
+ "ConditionalFieldConfig",
208
+ # Transformation
209
+ "InputTransformRegistry",
210
+ "register_type",
211
+ "get_global_registry",
212
+ # Organization
213
+ "FunctionGroup",
214
+ "FunctionOrganizer",
215
+ "mk_grouped_app",
216
+ "auto_group_by_prefix",
217
+ "auto_group_by_module",
218
+ "auto_group_by_tag",
219
+ # Decorators
220
+ "ui_config",
221
+ "group",
222
+ "hidden",
223
+ "field_config",
224
+ "with_example",
225
+ "deprecated",
226
+ "requires_auth",
227
+ "rate_limit",
228
+ "get_ui_config",
229
+ "get_group",
230
+ "get_field_configs",
231
+ "is_hidden",
232
+ "get_example",
233
+ # Field Interactions
234
+ "FieldDependency",
235
+ "DependencyAction",
236
+ "DependencyBuilder",
237
+ "with_dependencies",
238
+ "get_field_dependencies",
239
+ # Testing
240
+ "UfTestClient",
241
+ "UfAppTester",
242
+ "test_ui_function",
243
+ "FormDataBuilder",
244
+ "assert_valid_rjsf_spec",
245
+ "assert_has_field",
246
+ "assert_field_type",
247
+ "assert_field_required",
248
+ # Renderers
249
+ "ResultRenderer",
250
+ "ResultRendererRegistry",
251
+ "get_global_renderer_registry",
252
+ "register_renderer",
253
+ "render_result",
254
+ "result_renderer",
255
+ "get_result_renderer",
256
+ # Async
257
+ "is_async_function",
258
+ "async_to_sync",
259
+ "make_sync_compatible",
260
+ "AsyncFunctionWrapper",
261
+ "timeout_async",
262
+ "retry_async",
263
+ # Pydantic
264
+ "is_pydantic_model",
265
+ "pydantic_model_to_json_schema",
266
+ "function_uses_pydantic",
267
+ "wrap_pydantic_function",
268
+ "pydantic_to_dict",
269
+ "dict_to_pydantic",
270
+ # History
271
+ "FunctionCall",
272
+ "CallHistory",
273
+ "Preset",
274
+ "PresetManager",
275
+ "HistoryManager",
276
+ "get_global_history_manager",
277
+ "enable_history",
278
+ # Authentication
279
+ "User",
280
+ "AuthBackend",
281
+ "DictAuthBackend",
282
+ "SessionManager",
283
+ "ApiKey",
284
+ "ApiKeyManager",
285
+ "require_auth",
286
+ "get_global_auth_backend",
287
+ # Caching
288
+ "CacheBackend",
289
+ "MemoryCache",
290
+ "DiskCache",
291
+ "cached",
292
+ "get_global_cache_backend",
293
+ # Background
294
+ "Task",
295
+ "TaskStatus",
296
+ "TaskQueue",
297
+ "background",
298
+ "get_global_task_queue",
299
+ # OpenAPI
300
+ "generate_openapi_spec",
301
+ "add_openapi_routes",
302
+ "OpenAPIConfig",
303
+ # Webhooks
304
+ "WebhookEvent",
305
+ "WebhookClient",
306
+ "WebhookManager",
307
+ "webhook",
308
+ "get_global_webhook_manager",
309
+ # Themes
310
+ "Theme",
311
+ "ThemeConfig",
312
+ "get_theme",
313
+ "LIGHT_THEME",
314
+ "DARK_THEME",
315
+ ]
uf/async_support.py ADDED
@@ -0,0 +1,390 @@
1
+ """Async function support for uf.
2
+
3
+ Provides utilities for detecting and handling async functions,
4
+ allowing seamless integration of async def functions in uf apps.
5
+ """
6
+
7
+ import asyncio
8
+ import inspect
9
+ from typing import Callable, Any
10
+ from functools import wraps
11
+
12
+
13
+ def is_async_function(func: Callable) -> bool:
14
+ """Check if a function is async.
15
+
16
+ Args:
17
+ func: Function to check
18
+
19
+ Returns:
20
+ True if function is async
21
+
22
+ Example:
23
+ >>> async def my_async_func():
24
+ ... pass
25
+ >>> is_async_function(my_async_func)
26
+ True
27
+ """
28
+ return asyncio.iscoroutinefunction(func)
29
+
30
+
31
+ def async_to_sync(async_func: Callable) -> Callable:
32
+ """Convert an async function to a synchronous wrapper.
33
+
34
+ Args:
35
+ async_func: Async function to wrap
36
+
37
+ Returns:
38
+ Synchronous wrapper function
39
+
40
+ Example:
41
+ >>> async def fetch_data(url: str):
42
+ ... # async operations
43
+ ... return data
44
+ >>> sync_fetch = async_to_sync(fetch_data)
45
+ >>> result = sync_fetch('https://example.com')
46
+ """
47
+ if not is_async_function(async_func):
48
+ return async_func
49
+
50
+ @wraps(async_func)
51
+ def sync_wrapper(*args, **kwargs):
52
+ """Synchronous wrapper that runs async function."""
53
+ try:
54
+ # Try to get existing event loop
55
+ loop = asyncio.get_event_loop()
56
+ if loop.is_running():
57
+ # Loop is already running, create a new one in a thread
58
+ import concurrent.futures
59
+
60
+ with concurrent.futures.ThreadPoolExecutor() as executor:
61
+ future = executor.submit(asyncio.run, async_func(*args, **kwargs))
62
+ return future.result()
63
+ else:
64
+ # Loop exists but not running
65
+ return loop.run_until_complete(async_func(*args, **kwargs))
66
+ except RuntimeError:
67
+ # No event loop, create one
68
+ return asyncio.run(async_func(*args, **kwargs))
69
+
70
+ # Preserve metadata
71
+ sync_wrapper.__uf_is_async__ = True
72
+ sync_wrapper.__uf_original_async__ = async_func
73
+
74
+ return sync_wrapper
75
+
76
+
77
+ def make_sync_compatible(funcs: list[Callable]) -> list[Callable]:
78
+ """Convert any async functions in the list to sync wrappers.
79
+
80
+ Args:
81
+ funcs: List of functions (may include async)
82
+
83
+ Returns:
84
+ List of functions (all synchronous)
85
+
86
+ Example:
87
+ >>> funcs = [sync_func, async_func, another_sync]
88
+ >>> compatible = make_sync_compatible(funcs)
89
+ >>> # All functions in compatible are now callable synchronously
90
+ """
91
+ return [async_to_sync(func) for func in funcs]
92
+
93
+
94
+ def create_async_handler(async_func: Callable) -> Callable:
95
+ """Create a handler for async functions in web frameworks.
96
+
97
+ This is useful for integrating with frameworks that may or may not
98
+ support async natively (like Bottle vs FastAPI).
99
+
100
+ Args:
101
+ async_func: Async function to create handler for
102
+
103
+ Returns:
104
+ Appropriate handler (async or sync wrapped)
105
+
106
+ Example:
107
+ >>> async def my_endpoint(param: str):
108
+ ... result = await some_async_operation(param)
109
+ ... return result
110
+ >>> handler = create_async_handler(my_endpoint)
111
+ """
112
+ if not is_async_function(async_func):
113
+ return async_func
114
+
115
+ # For now, return sync wrapper
116
+ # In future, could detect framework and return async if supported
117
+ return async_to_sync(async_func)
118
+
119
+
120
+ class AsyncFunctionWrapper:
121
+ """Wrapper for async functions that provides both sync and async access.
122
+
123
+ This allows the same function to be called either way, depending on
124
+ the context.
125
+
126
+ Example:
127
+ >>> async def fetch_user(user_id: int):
128
+ ... # async database query
129
+ ... return user_data
130
+ >>>
131
+ >>> wrapper = AsyncFunctionWrapper(fetch_user)
132
+ >>> # Sync call
133
+ >>> user = wrapper.call_sync(user_id=123)
134
+ >>> # Async call
135
+ >>> user = await wrapper.call_async(user_id=123)
136
+ """
137
+
138
+ def __init__(self, func: Callable):
139
+ """Initialize the wrapper.
140
+
141
+ Args:
142
+ func: Function to wrap (sync or async)
143
+ """
144
+ self.func = func
145
+ self.is_async = is_async_function(func)
146
+
147
+ def call_sync(self, *args, **kwargs) -> Any:
148
+ """Call the function synchronously.
149
+
150
+ Args:
151
+ *args: Positional arguments
152
+ **kwargs: Keyword arguments
153
+
154
+ Returns:
155
+ Function result
156
+ """
157
+ if self.is_async:
158
+ return async_to_sync(self.func)(*args, **kwargs)
159
+ else:
160
+ return self.func(*args, **kwargs)
161
+
162
+ async def call_async(self, *args, **kwargs) -> Any:
163
+ """Call the function asynchronously.
164
+
165
+ Args:
166
+ *args: Positional arguments
167
+ **kwargs: Keyword arguments
168
+
169
+ Returns:
170
+ Function result
171
+ """
172
+ if self.is_async:
173
+ return await self.func(*args, **kwargs)
174
+ else:
175
+ # Run sync function in executor to avoid blocking
176
+ loop = asyncio.get_event_loop()
177
+ return await loop.run_in_executor(None, lambda: self.func(*args, **kwargs))
178
+
179
+ def __call__(self, *args, **kwargs):
180
+ """Call synchronously by default.
181
+
182
+ Args:
183
+ *args: Positional arguments
184
+ **kwargs: Keyword arguments
185
+
186
+ Returns:
187
+ Function result
188
+ """
189
+ return self.call_sync(*args, **kwargs)
190
+
191
+
192
+ def batch_async_calls(
193
+ async_func: Callable,
194
+ calls: list[dict],
195
+ ) -> list[Any]:
196
+ """Execute multiple async function calls concurrently.
197
+
198
+ Args:
199
+ async_func: Async function to call
200
+ calls: List of dicts with 'args' and 'kwargs' for each call
201
+
202
+ Returns:
203
+ List of results in same order as calls
204
+
205
+ Example:
206
+ >>> async def fetch_user(user_id: int):
207
+ ... return await db.get_user(user_id)
208
+ >>>
209
+ >>> calls = [
210
+ ... {'args': (), 'kwargs': {'user_id': 1}},
211
+ ... {'args': (), 'kwargs': {'user_id': 2}},
212
+ ... {'args': (), 'kwargs': {'user_id': 3}},
213
+ ... ]
214
+ >>> users = batch_async_calls(fetch_user, calls)
215
+ """
216
+ if not is_async_function(async_func):
217
+ # Sync function, just call sequentially
218
+ return [
219
+ async_func(*call.get("args", ()), **call.get("kwargs", {}))
220
+ for call in calls
221
+ ]
222
+
223
+ async def run_batch():
224
+ tasks = [
225
+ async_func(*call.get("args", ()), **call.get("kwargs", {}))
226
+ for call in calls
227
+ ]
228
+ return await asyncio.gather(*tasks)
229
+
230
+ return asyncio.run(run_batch())
231
+
232
+
233
+ def timeout_async(seconds: float):
234
+ """Decorator to add timeout to async functions.
235
+
236
+ Args:
237
+ seconds: Timeout in seconds
238
+
239
+ Returns:
240
+ Decorator function
241
+
242
+ Example:
243
+ >>> @timeout_async(5.0)
244
+ ... async def slow_operation():
245
+ ... await asyncio.sleep(10) # Will timeout after 5s
246
+ """
247
+
248
+ def decorator(async_func: Callable) -> Callable:
249
+ if not is_async_function(async_func):
250
+ return async_func
251
+
252
+ @wraps(async_func)
253
+ async def wrapper(*args, **kwargs):
254
+ try:
255
+ return await asyncio.wait_for(
256
+ async_func(*args, **kwargs), timeout=seconds
257
+ )
258
+ except asyncio.TimeoutError:
259
+ raise TimeoutError(
260
+ f"Function {async_func.__name__} timed out after {seconds} seconds"
261
+ )
262
+
263
+ return wrapper
264
+
265
+ return decorator
266
+
267
+
268
+ def retry_async(max_attempts: int = 3, delay: float = 1.0):
269
+ """Decorator to add retry logic to async functions.
270
+
271
+ Args:
272
+ max_attempts: Maximum number of attempts
273
+ delay: Delay between attempts in seconds
274
+
275
+ Returns:
276
+ Decorator function
277
+
278
+ Example:
279
+ >>> @retry_async(max_attempts=3, delay=2.0)
280
+ ... async def unreliable_api_call():
281
+ ... # May fail, will retry
282
+ ... return await external_api.fetch()
283
+ """
284
+
285
+ def decorator(async_func: Callable) -> Callable:
286
+ if not is_async_function(async_func):
287
+ return async_func
288
+
289
+ @wraps(async_func)
290
+ async def wrapper(*args, **kwargs):
291
+ last_exception = None
292
+
293
+ for attempt in range(max_attempts):
294
+ try:
295
+ return await async_func(*args, **kwargs)
296
+ except Exception as e:
297
+ last_exception = e
298
+ if attempt < max_attempts - 1:
299
+ await asyncio.sleep(delay)
300
+ else:
301
+ raise last_exception
302
+
303
+ return wrapper
304
+
305
+ return decorator
306
+
307
+
308
+ class AsyncContext:
309
+ """Context manager for handling async operations in uf.
310
+
311
+ This helps manage event loops and async resources properly.
312
+
313
+ Example:
314
+ >>> with AsyncContext() as ctx:
315
+ ... result = ctx.run(my_async_function(param))
316
+ """
317
+
318
+ def __init__(self):
319
+ """Initialize async context."""
320
+ self._loop = None
321
+ self._owned_loop = False
322
+
323
+ def __enter__(self):
324
+ """Enter the context."""
325
+ try:
326
+ self._loop = asyncio.get_event_loop()
327
+ self._owned_loop = False
328
+ except RuntimeError:
329
+ self._loop = asyncio.new_event_loop()
330
+ asyncio.set_event_loop(self._loop)
331
+ self._owned_loop = True
332
+
333
+ return self
334
+
335
+ def __exit__(self, exc_type, exc_val, exc_tb):
336
+ """Exit the context."""
337
+ if self._owned_loop and self._loop:
338
+ self._loop.close()
339
+
340
+ def run(self, coro):
341
+ """Run a coroutine in this context.
342
+
343
+ Args:
344
+ coro: Coroutine to run
345
+
346
+ Returns:
347
+ Result of the coroutine
348
+ """
349
+ if self._loop.is_running():
350
+ # Can't run_until_complete on a running loop
351
+ # Use asyncio.run in a thread
352
+ import concurrent.futures
353
+
354
+ with concurrent.futures.ThreadPoolExecutor() as executor:
355
+ future = executor.submit(asyncio.run, coro)
356
+ return future.result()
357
+ else:
358
+ return self._loop.run_until_complete(coro)
359
+
360
+
361
+ def detect_async_framework(app) -> str:
362
+ """Detect if the web framework supports async natively.
363
+
364
+ Args:
365
+ app: The web application
366
+
367
+ Returns:
368
+ Framework name: 'fastapi', 'bottle', 'unknown'
369
+ """
370
+ app_type = type(app).__name__
371
+
372
+ if "FastAPI" in app_type:
373
+ return "fastapi"
374
+ elif "Bottle" in app_type or hasattr(app, "route"):
375
+ return "bottle"
376
+ else:
377
+ return "unknown"
378
+
379
+
380
+ def is_framework_async_capable(app) -> bool:
381
+ """Check if the framework supports async handlers natively.
382
+
383
+ Args:
384
+ app: The web application
385
+
386
+ Returns:
387
+ True if framework supports async
388
+ """
389
+ framework = detect_async_framework(app)
390
+ return framework in ["fastapi", "aiohttp", "starlette"]