Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

15 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

uf - UI Fast

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.

Features

  • 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

Installation

pip install uf

Quick Start

from 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!

How It Works

uf combines three powerful packages from the i2mint ecosystem:

  1. qh: Converts functions → HTTP endpoints
  2. ju.rjsf: Generates JSON Schema & RJSF specs from signatures
  3. i2: Provides signature introspection and manipulation

The result: A complete web UI with zero boilerplate!

Table of Contents

Basic Usage

Simple Example

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")

Object-Oriented Interface

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)

UI Decorators

Add rich metadata to your functions using decorators:

@ui_config - Complete UI Configuration

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}

@group - Simple Grouping

from uf import group


@group("Admin")
def delete_user(user_id: int):
    """Delete a user from the system."""
    pass

@field_config - Field-Level Configuration

from 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."""
    pass

@with_example - Provide Test Data

from uf import with_example


@with_example(x=10, y=20)
def add(x: int, y: int) -> int:
    """Add two numbers."""
    return x + y

Other Decorators

from 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):
    pass

Field Configuration

Predefined Field Configurations

from 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")

Custom Field Configuration

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):
    pass

Field Configuration Builder

from 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)

Function Grouping

Manual Grouping

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])

Auto-Grouping by Prefix

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="_"
)

Auto-Grouping by Module

from uf import auto_group_by_module

organizer = auto_group_by_module([func1, func2, func3])

Auto-Grouping by Tag

from uf import auto_group_by_tag


def my_function():
    pass


my_function.__uf_group__ = "Admin"

organizer = auto_group_by_tag([my_function])

Custom Types

Register custom type transformations for seamless JSON serialization:

Using the Global Registry

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")

Using a Custom Registry

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)

Pre-registered Types

The following types are automatically supported:

  • datetime.datetime
  • datetime.date
  • datetime.time
  • pathlib.Path
  • uuid.UUID
  • decimal.Decimal

Field Dependencies

Create dynamic forms where fields show/hide based on other field values:

Simple Dependency

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."""
    pass

Dependency Builder

from 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()

Available Actions

  • DependencyAction.SHOW - Show the field
  • DependencyAction.HIDE - Hide the field
  • DependencyAction.ENABLE - Enable the field
  • DependencyAction.DISABLE - Disable the field
  • DependencyAction.REQUIRE - Make the field required
  • DependencyAction.OPTIONAL - Make the field optional

Testing

Built-in testing utilities for your uf apps:

Test Client

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"] == 30

Test Context Manager

from 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)

Testing Individual Functions

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)

Form Data Builder

from uf import FormDataBuilder

form_data = (
    FormDataBuilder()
    .field("name", "John Doe")
    .field("email", "[email protected]")
    .fields(age=30, city="NYC")
    .build()
)

Schema Assertions

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")

Customization

Custom CSS

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,
)

Advanced qh Configuration

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,
)

Examples

See the examples/ directory for complete working examples:

  • basic_example.py: Simple math and text functions
  • advanced_example.py: Customization and object-oriented interface
  • full_featured_example.py: Complete showcase of all features

API Reference

Core Functions

mk_rjsf_app(funcs, **kwargs)

Main entry point for creating a web app from functions.

Parameters:

  • funcs: Iterable of callable functions
  • config: Optional qh.AppConfig for HTTP configuration
  • input_trans: Optional input transformation function
  • output_trans: Optional output transformation function
  • rjsf_config: Optional RJSF configuration dict
  • ui_schema_factory: Optional callable for custom UI schemas
  • page_title: Title for the web interface (default: "Function Interface")
  • custom_css: Optional custom CSS string
  • rjsf_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)

mk_grouped_app(groups, **kwargs)

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

Classes

UfApp(funcs, **kwargs)

Object-oriented wrapper for uf applications.

Methods:

  • run(host, port, **kwargs): Run the web server
  • call(func_name, **kwargs): Call a function by name
  • get_spec(func_name): Get RJSF spec for a function
  • list_functions(): List all function names

FunctionSpecStore(funcs, **kwargs)

Mapping-based interface to function specifications.

RjsfFieldConfig(**kwargs)

Configuration for individual form fields.

Attributes:

  • widget: Widget type
  • format: JSON Schema format
  • enum: List of allowed values
  • placeholder: Placeholder text
  • description: Help text
  • And more...

FunctionGroup(name, funcs, **kwargs)

Group of functions with metadata.

Attributes:

  • name: Group name
  • funcs: List of functions
  • description: Group description
  • icon: Icon identifier
  • order: Display order

InputTransformRegistry()

Registry for custom type transformations.

Methods:

  • register_type(py_type, **kwargs): Register a type
  • mk_input_trans_for_funcs(funcs): Create input transformation
  • mk_output_trans(): Create output transformation

FieldDependency(**kwargs)

Define a dependency between form fields.

DependencyBuilder()

Fluent interface for building field dependencies.

Decorators

  • @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

Testing Utilities

  • UfTestClient(app): Test client for uf apps
  • UfAppTester(app): Context manager for testing
  • test_ui_function(func, params, **kwargs): Test individual functions
  • FormDataBuilder(): Build test form data
  • assert_valid_rjsf_spec(spec): Assert spec is valid
  • assert_has_field(spec, name): Assert field exists
  • assert_field_type(spec, name, type): Assert field type
  • assert_field_required(spec, name): Assert field is required

Architecture

uf follows these design principles:

  1. Convention over Configuration: Works out-of-the-box with sensible defaults
  2. Mapping-based Interfaces: Access everything as dictionaries
  3. Lazy Evaluation: Generate specs only when needed
  4. Composition over Inheritance: Extend via decorators and transformations
  5. Progressive Enhancement: Start simple, customize as needed

Development Roadmap

✅ Milestone 1: MVP (Completed)

  • Core mk_rjsf_app function
  • FunctionSpecStore for spec management
  • HTML template generation
  • Essential API routes

✅ Milestone 2: Configuration (Completed)

  • RJSF customization layer
  • Input transformation registry
  • Custom field widgets

✅ Milestone 3: Enhancement (Completed)

  • Function grouping and organization
  • UI metadata decorators (@ui_config)
  • Auto-grouping utilities

✅ Milestone 4: Advanced (Completed)

  • Field dependencies and interactions
  • Testing utilities
  • Comprehensive examples

Dependencies

  • qh: HTTP service generation
  • ju: RJSF form generation and JSON utilities
  • i2: Signature introspection
  • dol: Mapping interfaces
  • meshed: Function composition utilities

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

License

MIT License - see LICENSE file for details

Related Projects

  • qh: HTTP services from functions
  • ju: JSON Schema and RJSF utilities
  • i2: Signature introspection
  • dol: Mapping interfaces
  • meshed: Function composition

Authors

Part of the i2mint ecosystem.

Releases

Packages

Contributors

Languages