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 +315 -0
- uf/async_support.py +390 -0
- uf/auth.py +592 -0
- uf/background.py +493 -0
- uf/base.py +218 -0
- uf/caching.py +475 -0
- uf/decorators.py +401 -0
- uf/field_interactions.py +454 -0
- uf/history.py +571 -0
- uf/openapi.py +322 -0
- uf/optional.py +67 -0
- uf/organization.py +428 -0
- uf/pydantic_support.py +390 -0
- uf/renderers.py +409 -0
- uf/rjsf_config.py +338 -0
- uf/routes.py +230 -0
- uf/specs.py +366 -0
- uf/templates.py +424 -0
- uf/testing.py +488 -0
- uf/themes.py +415 -0
- uf/trans.py +348 -0
- uf/webhooks.py +342 -0
- uf-0.1.8.dist-info/METADATA +765 -0
- uf-0.1.8.dist-info/RECORD +26 -0
- uf-0.1.8.dist-info/WHEEL +4 -0
- uf-0.1.8.dist-info/licenses/LICENSE +21 -0
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"]
|