Minimal-boilerplate web UIs for Python functions
uf bridges functions → HTTP services (via qh) → Web UI forms (via ju.rjsf), following the "convention over configuration" philosophy.
- One-line app creation: Just pass your functions to
mk_rjsf_app() - Automatic form generation: RJSF forms created from function signatures
- Type-aware: Uses type hints to generate appropriate form fields
- Zero configuration required: Sensible defaults for everything
- Progressive enhancement: Customize only what you need
- Mapping-based interfaces: Access specs and configs as dictionaries
- Framework agnostic: Works with Bottle and FastAPI
- UI decorators: Rich metadata via
@ui_config,@group, etc. - Function grouping: Organize functions into categories
- Field customization: Configure widgets, validation, and interactions
- Custom type support: Register transformations for any Python type
- Field dependencies: Conditional display and dynamic forms
- Testing utilities: Built-in tools for testing your apps
pip install uffrom uf import mk_rjsf_app
def add(x: int, y: int) -> int:
"""Add two numbers"""
return x + y
def greet(name: str) -> str:
"""Greet a person"""
return f"Hello, {name}!"
# Create the app
app = mk_rjsf_app([add, greet])
# Run it (for Bottle apps)
app.run(host='localhost', port=8080)Then open http://localhost:8080 in your browser!
uf combines three powerful packages from the i2mint ecosystem:
- qh: Converts functions → HTTP endpoints
- ju.rjsf: Generates JSON Schema & RJSF specs from signatures
- i2: Provides signature introspection and manipulation
The result: A complete web UI with zero boilerplate!
- Basic Usage
- UI Decorators
- Field Configuration
- Function Grouping
- Custom Types
- Field Dependencies
- Testing
- API Reference
- Examples
from uf import mk_rjsf_app
def multiply(x: float, y: float) -> float:
"""Multiply two numbers"""
return x * y
app = mk_rjsf_app([multiply], page_title="Calculator")For more control, use the UfApp class:
from uf import UfApp
def fibonacci(n: int) -> list:
"""Generate Fibonacci sequence"""
if n <= 0:
return []
elif n == 1:
return [0]
fib = [0, 1]
for i in range(2, n):
fib.append(fib[i-1] + fib[i-2])
return fib
# Create app
uf_app = UfApp([fibonacci])
# Call functions programmatically
result = uf_app.call('fibonacci', n=10)
# Access specs
spec = uf_app.get_spec('fibonacci')
# List available functions
functions = uf_app.list_functions()
# Run the server
uf_app.run(host='localhost', port=8080)Add rich metadata to your functions using decorators:
from uf import ui_config, RjsfFieldConfig, get_field_config
@ui_config(
title="User Registration",
description="Create a new user account",
group="Admin",
icon="user-plus",
order=1,
fields={
'email': get_field_config('email'),
'bio': get_field_config('multiline_text'),
}
)
def register_user(email: str, name: str, bio: str = ''):
"""Register a new user."""
return {'email': email, 'name': name, 'bio': bio}from uf import group
@group("Admin")
def delete_user(user_id: int):
"""Delete a user from the system."""
passfrom uf import field_config, get_field_config
@field_config(
email=get_field_config('email'),
message=get_field_config('multiline_text'),
)
def send_message(email: str, message: str):
"""Send a message to a user."""
pass@hidden - Hide from UI
from uf import hidden
@hidden
def internal_function():
"""This won't appear in the UI but is accessible via API."""
passfrom uf import with_example
@with_example(x=10, y=20)
def add(x: int, y: int) -> int:
"""Add two numbers."""
return x + yfrom uf import deprecated, requires_auth, rate_limit
@deprecated("Use new_function instead")
def old_function():
pass
@requires_auth(roles=['admin'], permissions=['user:delete'])
def delete_user(user_id: int):
pass
@rate_limit(calls=10, period=60) # 10 calls per minute
def send_email(to: str, subject: str):
passfrom uf import get_field_config
# Available configurations:
email_config = get_field_config('email')
password_config = get_field_config('password')
url_config = get_field_config('url')
multiline_config = get_field_config('multiline_text')
long_text_config = get_field_config('long_text')
date_config = get_field_config('date')
datetime_config = get_field_config('datetime')
color_config = get_field_config('color')
file_config = get_field_config('file')from uf import RjsfFieldConfig
custom_field = RjsfFieldConfig(
widget='select',
enum=['option1', 'option2', 'option3'],
placeholder='Choose an option',
description='Please select one option',
)
@field_config(status=custom_field)
def update_status(status: str):
passfrom uf import RjsfConfigBuilder, RjsfFieldConfig
builder = RjsfConfigBuilder()
builder.field('name', RjsfFieldConfig(placeholder='Enter name'))
builder.field('email', RjsfFieldConfig(format='email'))
builder.order(['name', 'email', 'phone'])
builder.class_names('custom-form')
spec = builder.build(base_schema)from uf import FunctionGroup, mk_grouped_app
admin_group = FunctionGroup(
name="Admin",
funcs=[create_user, delete_user, update_user],
description="User administration functions",
icon="shield",
order=1,
)
reports_group = FunctionGroup(
name="Reports",
funcs=[generate_report, export_data],
description="Reporting functions",
icon="file-text",
order=2,
)
app = mk_grouped_app([admin_group, reports_group])from uf import auto_group_by_prefix
# Functions named user_create, user_delete, report_generate, etc.
# will be automatically grouped into "User", "Report", etc.
organizer = auto_group_by_prefix(
[user_create, user_delete, report_generate],
separator="_"
)from uf import auto_group_by_module
organizer = auto_group_by_module([func1, func2, func3])from uf import auto_group_by_tag
def my_function():
pass
my_function.__uf_group__ = "Admin"
organizer = auto_group_by_tag([my_function])Register custom type transformations for seamless JSON serialization:
from uf import register_type
from pathlib import Path
from decimal import Decimal
# Register Path type
register_type(
Path,
to_json=str,
from_json=Path,
json_schema_type='string'
)
# Register Decimal type
register_type(
Decimal,
to_json=float,
from_json=Decimal,
json_schema_type='number'
)from uf import InputTransformRegistry
registry = InputTransformRegistry()
registry.register_type(
MyCustomType,
to_json=lambda x: x.to_dict(),
from_json=MyCustomType.from_dict,
ui_widget='textarea',
json_schema_type='object'
)
# Use with mk_rjsf_app
input_trans = registry.mk_input_trans_for_funcs([my_func])
output_trans = registry.mk_output_trans()
app = mk_rjsf_app(
[my_func],
input_trans=input_trans,
output_trans=output_trans
)The following types are automatically supported:
datetime.datetimedatetime.datedatetime.timepathlib.Pathuuid.UUIDdecimal.Decimal
Create dynamic forms where fields show/hide based on other field values:
from uf import FieldDependency, DependencyAction, with_dependencies
@with_dependencies(
FieldDependency(
source_field='reason',
target_field='other_reason',
condition=lambda v: v == 'other',
action=DependencyAction.SHOW,
)
)
def submit_feedback(reason: str, other_reason: str = ''):
"""Submit feedback with conditional 'other' field."""
passfrom uf import DependencyBuilder
builder = DependencyBuilder()
builder.when('age').greater_than(18).enable('alcohol_consent')
builder.when('country').equals('US').show('state')
builder.when('priority').in_list(['high', 'urgent']).require('manager_approval')
dependencies = builder.build()DependencyAction.SHOW- Show the fieldDependencyAction.HIDE- Hide the fieldDependencyAction.ENABLE- Enable the fieldDependencyAction.DISABLE- Disable the fieldDependencyAction.REQUIRE- Make the field requiredDependencyAction.OPTIONAL- Make the field optional
Built-in testing utilities for your uf apps:
from uf import UfTestClient
client = UfTestClient(app)
# List functions
functions = client.list_functions()
# Get spec
spec = client.get_spec('my_function')
# Call function
result = client.call_function('my_function', {'x': 10, 'y': 20})
assert result['success']
assert result['result'] == 30from uf import UfAppTester
with UfAppTester(app) as tester:
result = tester.submit_form('add', {'x': 10, 'y': 20})
tester.assert_success(result)
tester.assert_result_equals(result, 30)from uf import test_ui_function
def add(x: int, y: int) -> int:
return x + y
# Test with expected output
test_ui_function(add, {'x': 10, 'y': 20}, expected_output=30)
# Test with expected exception
test_ui_function(
divide,
{'x': 10, 'y': 0},
expected_exception=ZeroDivisionError
)from uf import FormDataBuilder
form_data = (
FormDataBuilder()
.field('name', 'John Doe')
.field('email', 'john@example.com')
.fields(age=30, city='NYC')
.build()
)from uf import (
assert_valid_rjsf_spec,
assert_has_field,
assert_field_type,
assert_field_required,
)
spec = app.function_specs['my_function']
assert_valid_rjsf_spec(spec)
assert_has_field(spec, 'email')
assert_field_type(spec, 'age', 'integer')
assert_field_required(spec, 'name')CUSTOM_CSS = """
body {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
"""
app = mk_rjsf_app(
[func1, func2],
page_title="My Custom App",
custom_css=CUSTOM_CSS,
)from qh import AppConfig
qh_config = AppConfig(
cors=True,
log_requests=True,
)
app = mk_rjsf_app(
[my_func],
config=qh_config,
input_trans=my_input_transformer,
output_trans=my_output_transformer,
)See the examples/ directory for complete working examples:
basic_example.py: Simple math and text functionsadvanced_example.py: Customization and object-oriented interfacefull_featured_example.py: Complete showcase of all features
Main entry point for creating a web app from functions.
Parameters:
funcs: Iterable of callable functionsconfig: Optional qh.AppConfig for HTTP configurationinput_trans: Optional input transformation functionoutput_trans: Optional output transformation functionrjsf_config: Optional RJSF configuration dictui_schema_factory: Optional callable for custom UI schemaspage_title: Title for the web interface (default: "Function Interface")custom_css: Optional custom CSS stringrjsf_theme: RJSF theme name (default: "default")add_ui: Whether to add UI routes (default: True)**qh_kwargs: Additional arguments passed to qh.mk_app
Returns: Configured web application (Bottle or FastAPI)
Create a uf app with grouped function navigation.
Parameters:
groups: Iterable of FunctionGroup objects**kwargs: Same as mk_rjsf_app
Returns: Configured web application with grouped navigation
Object-oriented wrapper for uf applications.
Methods:
run(host, port, **kwargs): Run the web servercall(func_name, **kwargs): Call a function by nameget_spec(func_name): Get RJSF spec for a functionlist_functions(): List all function names
Mapping-based interface to function specifications.
Configuration for individual form fields.
Attributes:
widget: Widget typeformat: JSON Schema formatenum: List of allowed valuesplaceholder: Placeholder textdescription: Help text- And more...
Group of functions with metadata.
Attributes:
name: Group namefuncs: List of functionsdescription: Group descriptionicon: Icon identifierorder: Display order
Registry for custom type transformations.
Methods:
register_type(py_type, **kwargs): Register a typemk_input_trans_for_funcs(funcs): Create input transformationmk_output_trans(): Create output transformation
Define a dependency between form fields.
Fluent interface for building field dependencies.
@ui_config(...): Add complete UI configuration@group(name): Assign to a group@hidden: Hide from UI@field_config(**fields): Configure specific fields@with_example(**kwargs): Attach example data@deprecated(message): Mark as deprecated@requires_auth(...): Mark as requiring authentication@rate_limit(calls, period): Add rate limit metadata
UfTestClient(app): Test client for uf appsUfAppTester(app): Context manager for testingtest_ui_function(func, params, **kwargs): Test individual functionsFormDataBuilder(): Build test form dataassert_valid_rjsf_spec(spec): Assert spec is validassert_has_field(spec, name): Assert field existsassert_field_type(spec, name, type): Assert field typeassert_field_required(spec, name): Assert field is required
uf follows these design principles:
- Convention over Configuration: Works out-of-the-box with sensible defaults
- Mapping-based Interfaces: Access everything as dictionaries
- Lazy Evaluation: Generate specs only when needed
- Composition over Inheritance: Extend via decorators and transformations
- Progressive Enhancement: Start simple, customize as needed
- Core
mk_rjsf_appfunction - FunctionSpecStore for spec management
- HTML template generation
- Essential API routes
- RJSF customization layer
- Input transformation registry
- Custom field widgets
- Function grouping and organization
- UI metadata decorators (
@ui_config) - Auto-grouping utilities
- Field dependencies and interactions
- Testing utilities
- Comprehensive examples
qh: HTTP service generationju: RJSF form generation and JSON utilitiesi2: Signature introspectiondol: Mapping interfacesmeshed: Function composition utilities
Contributions are welcome! Please feel free to submit a Pull Request.
MIT License - see LICENSE file for details
- qh: HTTP services from functions
- ju: JSON Schema and RJSF utilities
- i2: Signature introspection
- dol: Mapping interfaces
- meshed: Function composition
Part of the i2mint ecosystem.