Skip to content

API Reference

func_to_web

cleanup_temp_file(file_id)

Remove temp file and its registry entry.

Parameters:

Name Type Description Default
file_id str

Unique identifier for the file.

required
Source code in func_to_web\__init__.py
def cleanup_temp_file(file_id: str) -> None:
    """Remove temp file and its registry entry.

    Args:
        file_id: Unique identifier for the file.
    """
    try:
        if not TEMP_FILES_REGISTRY.exists():
            return

        with open(TEMP_FILES_REGISTRY, 'r') as f:
            registry = json.load(f)

        if file_id in registry:
            path = registry[file_id]['path']
            try:
                os.unlink(path)
            except:
                pass

            del registry[file_id]

            with open(TEMP_FILES_REGISTRY, 'w') as f:
                json.dump(registry, f)
    except:
        pass

create_response_with_files(processed)

Create JSON response with file downloads.

Parameters:

Name Type Description Default
processed dict[str, Any]

Processed result from process_result().

required

Returns:

Type Description
dict[str, Any]

Response dictionary with file IDs and metadata.

Source code in func_to_web\__init__.py
def create_response_with_files(processed: dict[str, Any]) -> dict[str, Any]:
    """Create JSON response with file downloads.

    Args:
        processed: Processed result from process_result().

    Returns:
        Response dictionary with file IDs and metadata.
    """
    response = {"success": True, "result_type": processed['type']}

    if processed['type'] == 'download':
        file_id = str(uuid.uuid4())
        register_temp_file(file_id, processed['path'], processed['filename'])
        response['file_id'] = file_id
        response['filename'] = processed['filename']
    elif processed['type'] == 'downloads':
        files = []
        for f in processed['files']:
            file_id = str(uuid.uuid4())
            register_temp_file(file_id, f['path'], f['filename'])
            files.append({
                'file_id': file_id,
                'filename': f['filename']
            })
        response['files'] = files
    else:
        response['result'] = processed['data']

    return response

get_temp_file(file_id)

Get temp file info from registry.

Parameters:

Name Type Description Default
file_id str

Unique identifier for the file.

required

Returns:

Type Description
dict[str, str] | None

Dictionary with 'path' and 'filename' keys, or None if not found.

Source code in func_to_web\__init__.py
def get_temp_file(file_id: str) -> dict[str, str] | None:
    """Get temp file info from registry.

    Args:
        file_id: Unique identifier for the file.

    Returns:
        Dictionary with 'path' and 'filename' keys, or None if not found.
    """
    try:
        if not TEMP_FILES_REGISTRY.exists():
            return None

        with open(TEMP_FILES_REGISTRY, 'r') as f:
            registry = json.load(f)

        return registry.get(file_id)
    except:
        return None

handle_form_submission(request, func, params) async

Handle form submission for any function.

Parameters:

Name Type Description Default
request Request

FastAPI request object.

required
func Callable

Function to call with validated parameters.

required
params dict[str, ParamInfo]

Parameter metadata from analyze().

required

Returns:

Type Description
JSONResponse

JSON response with result or error.

Source code in func_to_web\__init__.py
async def handle_form_submission(request: Request, func: Callable, params: dict[str, ParamInfo]) -> JSONResponse:
    """Handle form submission for any function.

    Args:
        request: FastAPI request object.
        func: Function to call with validated parameters.
        params: Parameter metadata from analyze().

    Returns:
        JSON response with result or error.
    """
    try:
        form_data = await request.form()
        data = {}

        for name, value in form_data.items():
            if hasattr(value, 'filename'):
                suffix = os.path.splitext(value.filename)[1]
                data[name] = await save_uploaded_file(value, suffix)
            else:
                data[name] = value

        validated = validate_params(data, params)
        result = func(**validated)
        processed = process_result(result)
        response = create_response_with_files(processed)

        return JSONResponse(response)
    except Exception as e:
        return JSONResponse({"success": False, "error": str(e)}, status_code=400)

register_temp_file(file_id, path, filename)

Register a temp file for download.

Parameters:

Name Type Description Default
file_id str

Unique identifier for the file.

required
path str

File system path to the temporary file.

required
filename str

Original filename for download.

required
Source code in func_to_web\__init__.py
def register_temp_file(file_id: str, path: str, filename: str) -> None:
    """Register a temp file for download.

    Args:
        file_id: Unique identifier for the file.
        path: File system path to the temporary file.
        filename: Original filename for download.
    """
    try:
        if TEMP_FILES_REGISTRY.exists():
            with open(TEMP_FILES_REGISTRY, 'r') as f:
                registry = json.load(f)
        else:
            registry = {}

        registry[file_id] = {'path': path, 'filename': filename}

        with open(TEMP_FILES_REGISTRY, 'w') as f:
            json.dump(registry, f)
    except:
        pass

run(func_or_list, host='0.0.0.0', port=8000, template_dir=None)

Generate and run a web UI for one or more Python functions.

Single function mode: Creates a form at root (/) for the function. Multiple functions mode: Creates an index page with links to individual function forms.

Parameters:

Name Type Description Default
func_or_list Callable[..., Any] | list[Callable[..., Any]]

A single function or list of functions to wrap.

required
host str

Server host address (default: "0.0.0.0").

'0.0.0.0'
port int

Server port (default: 8000).

8000
template_dir str | Path | None

Optional custom template directory.

None

Raises:

Type Description
FileNotFoundError

If template directory doesn't exist.

TypeError

If function parameters use unsupported types.

Source code in func_to_web\__init__.py
def run(
    func_or_list: Callable[..., Any] | list[Callable[..., Any]], 
    host: str = "0.0.0.0", 
    port: int = 8000, 
    template_dir: str | Path | None = None
) -> None:
    """Generate and run a web UI for one or more Python functions.

    Single function mode: Creates a form at root (/) for the function.
    Multiple functions mode: Creates an index page with links to individual function forms.

    Args:
        func_or_list: A single function or list of functions to wrap.
        host: Server host address (default: "0.0.0.0").
        port: Server port (default: 8000).
        template_dir: Optional custom template directory.

    Raises:
        FileNotFoundError: If template directory doesn't exist.
        TypeError: If function parameters use unsupported types.
    """

    funcs = func_or_list if isinstance(func_or_list, list) else [func_or_list]

    app = FastAPI()

    if template_dir is None:
        template_dir = Path(__file__).parent / "templates"
    else:
        template_dir = Path(template_dir)

    if not template_dir.exists():
        raise FileNotFoundError(
            f"Template directory '{template_dir}' not found."
        )

    templates = Jinja2Templates(directory=str(template_dir))
    app.mount("/static", StaticFiles(directory=template_dir / "static"), name="static")

    # Download endpoint for streaming files
    @app.get("/download/{file_id}")
    async def download_file(file_id: str):
        file_info = get_temp_file(file_id)

        if not file_info:
            return JSONResponse({"error": "File not found"}, status_code=404)

        path = file_info['path']
        filename = file_info['filename']

        if not os.path.exists(path):
            cleanup_temp_file(file_id)
            return JSONResponse({"error": "File expired"}, status_code=404)

        response = FastAPIFileResponse(
            path=path,
            filename=filename,
            media_type='application/octet-stream'
        )

        # Cleanup after sending
        async def cleanup():
            cleanup_temp_file(file_id)

        response.background = cleanup

        return response

    # Single function mode
    if len(funcs) == 1:
        func = funcs[0]
        params = analyze(func)
        func_name = func.__name__.replace('_', ' ').title()
        description = inspect.getdoc(func)

        @app.get("/")
        async def form(request: Request):
            fields = build_form_fields(params)
            return templates.TemplateResponse(
                "form.html",
                {
                    "request": request, 
                    "title": func_name, 
                    "description": description,
                    "fields": fields, 
                    "submit_url": "/submit"
                }
            )

        @app.post("/submit")
        async def submit(request: Request):
            return await handle_form_submission(request, func, params)

    # Multiple functions mode
    else:
        @app.get("/")
        async def index(request: Request):
            tools = [{
                "name": f.__name__.replace('_', ' ').title(),
                "path": f"/{f.__name__}"
            } for f in funcs]
            return templates.TemplateResponse(
                "index.html",
                {"request": request, "tools": tools}
            )

        for func in funcs:
            params = analyze(func)
            func_name = func.__name__.replace('_', ' ').title()
            description = inspect.getdoc(func)
            route = f"/{func.__name__}"
            submit_route = f"{route}/submit"

            def make_form_handler(title: str, prms: dict[str, ParamInfo], desc: str | None, submit_path: str):
                async def form_view(request: Request):
                    flds = build_form_fields(prms)
                    return templates.TemplateResponse(
                        "form.html",
                        {
                            "request": request, 
                            "title": title, 
                            "description": desc,
                            "fields": flds, 
                            "submit_url": submit_path
                        }
                    )
                return form_view

            def make_submit_handler(fn: Callable, prms: dict[str, ParamInfo]):
                async def submit_view(request: Request):
                    return await handle_form_submission(request, fn, prms)
                return submit_view

            app.get(route)(make_form_handler(func_name, params, description, submit_route))
            app.post(submit_route)(make_submit_handler(func, params))

    config = uvicorn.Config(
        app, 
        host=host, 
        port=port, 
        reload=False,
        limit_concurrency=100,
        limit_max_requests=1000,
        timeout_keep_alive=30,
        h11_max_incomplete_event_size=16 * 1024 * 1024
    )
    server = uvicorn.Server(config)
    asyncio.run(server.serve())

save_uploaded_file(uploaded_file, suffix) async

Save an uploaded file to a temporary location.

Parameters:

Name Type Description Default
uploaded_file Any

The uploaded file object from FastAPI.

required
suffix str

File extension to use for the temp file.

required

Returns:

Type Description
str

Path to the saved temporary file.

Source code in func_to_web\__init__.py
async def save_uploaded_file(uploaded_file: Any, suffix: str) -> str:
    """Save an uploaded file to a temporary location.

    Args:
        uploaded_file: The uploaded file object from FastAPI.
        suffix: File extension to use for the temp file.

    Returns:
        Path to the saved temporary file.
    """
    with tempfile.NamedTemporaryFile(delete=False, suffix=suffix, buffering=FILE_BUFFER_SIZE) as tmp:
        while chunk := await uploaded_file.read(CHUNK_SIZE):
            tmp.write(chunk)
        return tmp.name

func_to_web.types

FileResponse

Bases: BaseModel

Model for file response.

Source code in func_to_web\types.py
class FileResponse(BaseModel):
    """Model for file response."""
    data: bytes
    filename: str

func_to_web.analyze_function

ParamInfo dataclass

Metadata about a function parameter extracted by analyze().

This dataclass stores all the information needed to generate form fields, validate input, and call the function with the correct parameters.

Attributes:

Name Type Description
type type

The base Python type of the parameter. Must be one of: int, float, str, bool, date, or time. Example: int, str, date

default Any

The default value specified in the parameter. - None if the parameter has no default - The actual default value if specified (e.g., 42, "hello", True) - Independent of is_optional (a parameter can be optional with or without a default) Example: For age: int = 25, default is 25 Example: For name: str, default is None

field_info Any

Additional metadata from Pydantic Field or Literal. - For Annotated types: Contains the Field object with constraints (e.g., Field(ge=0, le=100) for numeric bounds, Field(min_length=3) for strings) - For Literal types: Contains the Literal type with valid options - None for basic types without constraints Example: Field(ge=18, le=100) for age constraints Example: Literal['light', 'dark'] for dropdown options

dynamic_func Any

Function for dynamic Literal options. - Only set for Literal[callable] type hints - Called at runtime to generate dropdown options dynamically - Returns a list, tuple, or single value - None for static Literals or non-Literal types Example: A function that returns database options

is_optional bool

Whether the parameter type includes None. - True for Type | None or Union[Type, None] syntax - False for regular required parameters (even if they have a default) - Affects UI: optional fields get a toggle switch to enable/disable - Default: False Example: name: str | None has is_optional=True Example: age: int = 25 has is_optional=False (even with default)

optional_enabled bool

Initial state of optional toggle. - Only relevant when is_optional=True - True: toggle starts enabled (field active) - False: toggle starts disabled (field inactive, sends None) - Determined by: explicit marker > default value > False - Default: False Example: name: str | OptionalEnabled starts enabled Example: name: str | OptionalDisabled starts disabled Example: name: str | None = "John" starts enabled (has default) Example: name: str | None starts disabled (no default)

is_list bool

Whether the parameter is a list type. - True for list[Type] syntax - False for regular parameters - When True, 'type' contains the item type, not list - Default: False

list_field_info Any

Metadata for the list itself (min_items, max_items). - Only relevant when is_list=True - Contains Field constraints for the list container - None if no list-level constraints Example: Field(min_items=2, max_items=5)

Source code in func_to_web\analyze_function.py
@dataclass
class ParamInfo:
    """Metadata about a function parameter extracted by analyze().

    This dataclass stores all the information needed to generate form fields,
    validate input, and call the function with the correct parameters.

    Attributes:
        type: The base Python type of the parameter. Must be one of:
            int, float, str, bool, date, or time.
            Example: int, str, date
        default: The default value specified in the parameter.
            - None if the parameter has no default
            - The actual default value if specified (e.g., 42, "hello", True)
            - Independent of is_optional (a parameter can be optional with or without a default)
            Example: For `age: int = 25`, default is 25
            Example: For `name: str`, default is None
        field_info: Additional metadata from Pydantic Field or Literal.
            - For Annotated types: Contains the Field object with constraints
              (e.g., Field(ge=0, le=100) for numeric bounds, Field(min_length=3) for strings)
            - For Literal types: Contains the Literal type with valid options
            - None for basic types without constraints
            Example: Field(ge=18, le=100) for age constraints
            Example: Literal['light', 'dark'] for dropdown options
        dynamic_func: Function for dynamic Literal options.
            - Only set for Literal[callable] type hints
            - Called at runtime to generate dropdown options dynamically
            - Returns a list, tuple, or single value
            - None for static Literals or non-Literal types
            Example: A function that returns database options
        is_optional: Whether the parameter type includes None.
            - True for Type | None or Union[Type, None] syntax
            - False for regular required parameters (even if they have a default)
            - Affects UI: optional fields get a toggle switch to enable/disable
            - Default: False
            Example: `name: str | None` has is_optional=True
            Example: `age: int = 25` has is_optional=False (even with default)
        optional_enabled: Initial state of optional toggle.
            - Only relevant when is_optional=True
            - True: toggle starts enabled (field active)
            - False: toggle starts disabled (field inactive, sends None)
            - Determined by: explicit marker > default value > False
            - Default: False
            Example: `name: str | OptionalEnabled` starts enabled
            Example: `name: str | OptionalDisabled` starts disabled
            Example: `name: str | None = "John"` starts enabled (has default)
            Example: `name: str | None` starts disabled (no default)
        is_list: Whether the parameter is a list type.
            - True for list[Type] syntax
            - False for regular parameters
            - When True, 'type' contains the item type, not list
            - Default: False
        list_field_info: Metadata for the list itself (min_items, max_items).
            - Only relevant when is_list=True
            - Contains Field constraints for the list container
            - None if no list-level constraints
            Example: Field(min_items=2, max_items=5)
    """
    type: type
    default: Any = None
    field_info: Any = None
    dynamic_func: Any = None
    is_optional: bool = False
    optional_enabled: bool = False
    is_list: bool = False
    list_field_info: Any = None

analyze(func)

Analyze a function's signature and extract parameter metadata.

Parameters:

Name Type Description Default
func Callable[..., Any]

The function to analyze.

required

Returns:

Type Description
dict[str, ParamInfo]

Mapping of parameter names to ParamInfo objects.

Raises:

Type Description
TypeError

If parameter type is not supported.

TypeError

If list has no type parameter.

TypeError

If list item type is not supported.

TypeError

If list of Literal is used (conceptually confusing).

TypeError

If list default is not a list.

TypeError

If list default items have wrong type.

ValueError

If default value doesn't match Literal options.

ValueError

If Literal options are invalid.

ValueError

If Union has multiple non-None types.

ValueError

If default value type doesn't match parameter type.

Source code in func_to_web\analyze_function.py
def analyze(func: Callable[..., Any]) -> dict[str, ParamInfo]:
    """Analyze a function's signature and extract parameter metadata.

    Args:
        func: The function to analyze.

    Returns:
        Mapping of parameter names to ParamInfo objects.

    Raises:
        TypeError: If parameter type is not supported.
        TypeError: If list has no type parameter.
        TypeError: If list item type is not supported.
        TypeError: If list of Literal is used (conceptually confusing).
        TypeError: If list default is not a list.
        TypeError: If list default items have wrong type.
        ValueError: If default value doesn't match Literal options.
        ValueError: If Literal options are invalid.
        ValueError: If Union has multiple non-None types.
        ValueError: If default value type doesn't match parameter type.
    """

    result = {}

    for name, p in inspect.signature(func).parameters.items():
        default = None if p.default == inspect.Parameter.empty else p.default
        t = p.annotation
        f = None
        list_f = None  # Field info for the list itself
        dynamic_func = None
        is_optional = False
        optional_default_enabled = None  # None = auto, True = enabled, False = disabled
        is_list = False

        # 1. Extract base type from Annotated (OUTER level)
        # This could be constraints for the list itself
        if get_origin(t) is Annotated:
            args = get_args(t)
            t = args[0]
            if len(args) > 1:
                # Store this temporarily - we'll decide if it's for list or item later
                list_f = args[1]

        # 2. Check for Union types (including | None syntax) BEFORE list detection
        if get_origin(t) is types.UnionType or str(get_origin(t)) == 'typing.Union':
            union_args = get_args(t)

            # First pass: detect markers and check for None
            has_none = type(None) in union_args

            for arg in union_args:
                if get_origin(arg) is Annotated:
                    annotated_args = get_args(arg)
                    # Check if this is Annotated[None, Marker]
                    if annotated_args[0] is type(None) and len(annotated_args) > 1:
                        for marker in annotated_args[1:]:
                            if isinstance(marker, _OptionalEnabledMarker):
                                optional_default_enabled = True
                                is_optional = True
                            elif isinstance(marker, _OptionalDisabledMarker):
                                optional_default_enabled = False
                                is_optional = True

            # Second pass: extract the actual type (not None, not markers)
            if has_none or is_optional:
                is_optional = True
                non_none_types = []

                for arg in union_args:
                    # Skip plain None
                    if arg is type(None):
                        continue

                    # Skip Annotated[None, Marker] (the markers)
                    if get_origin(arg) is Annotated:
                        annotated_args = get_args(arg)
                        if annotated_args[0] is type(None):
                            continue

                    # This is the actual type
                    non_none_types.append(arg)

                if len(non_none_types) == 0:
                    raise TypeError(f"'{name}': Cannot have only None type")
                elif len(non_none_types) > 1:
                    raise TypeError(f"'{name}': Union with multiple non-None types not supported")

                # Extract the actual type
                t = non_none_types[0]

                # Check again if this is Annotated (for Field constraints)
                if get_origin(t) is Annotated:
                    args = get_args(t)
                    t = args[0]
                    if len(args) > 1 and list_f is None:
                        list_f = args[1]

        # 3. Detect list type
        if get_origin(t) is list:
            is_list = True
            list_args = get_args(t)

            if not list_args:
                raise TypeError(f"'{name}': list must have type parameter (e.g., list[int])")

            # Extract item type
            t = list_args[0]

            # Check if item type is Literal (before extracting Annotated)
            if get_origin(t) is Literal:
                raise TypeError(f"'{name}': list of Literal not supported")

            # 4. Extract Annotated from ITEM type
            if get_origin(t) is Annotated:
                args = get_args(t)
                t = args[0]

                # Check again for Literal after extracting Annotated
                if get_origin(t) is Literal:
                    raise TypeError(f"'{name}': list of Literal not supported")

                if len(args) > 1:
                    f = args[1]  # Field constraints for EACH ITEM
        elif t is list:
            # Handle bare 'list' without type parameter
            raise TypeError(f"'{name}': list must have type parameter (e.g., list[int])")

        # If not a list, then list_f is actually the field_info for the item
        if not is_list and list_f is not None:
            f = list_f
            list_f = None

        # 5. Handle Literal types (dropdowns)
        if get_origin(t) is Literal:
            opts = get_args(t)

            # Check if opts contains a single callable (dynamic Literal)
            if len(opts) == 1 and callable(opts[0]):
                dynamic_func = opts[0]
                result_value = dynamic_func()

                # Convert result to tuple properly
                if isinstance(result_value, (list, tuple)):
                    opts = tuple(result_value)
                else:
                    opts = (result_value,)

            # Validate options
            if opts:
                types_set = {type(o) for o in opts}
                if len(types_set) > 1:
                    raise TypeError(f"'{name}': mixed types in Literal")

                # For lists, we can't validate default against Literal here (it's a list)
                if not is_list and default is not None and default not in opts:
                    raise ValueError(f"'{name}': default '{default}' not in options {opts}")

                f = Literal[opts] if len(opts) > 0 else t
                t = types_set.pop() if types_set else type(None)
            else:
                t = type(None)

        # 6. Validate base type
        if t not in VALID:
            raise TypeError(f"'{name}': {t} not supported")

        # 7. Validate default value
        if default is not None:
            if is_list:
                # Must be a list
                if not isinstance(default, list):
                    raise TypeError(f"'{name}': default must be a list")

                # Validate list-level constraints BEFORE converting empty list to None
                if list_f and hasattr(list_f, 'metadata'):
                    TypeAdapter(Annotated[list[t], list_f]).validate_python(default)

                # Validate each item
                for item in default:
                    # Check type
                    if not isinstance(item, t):
                        raise TypeError(f"'{name}': list item type mismatch in default")

                    # Validate against Field constraints (for items)
                    if f and hasattr(f, 'metadata'):
                        TypeAdapter(Annotated[t, f]).validate_python(item)

                # Convert empty list to None AFTER validation
                if len(default) == 0:
                    default = None
            else:
                # Non-list validation (existing logic)
                if not is_optional and get_origin(f) is not Literal:
                    if not isinstance(default, t):
                        raise TypeError(f"'{name}': default value type mismatch")

                # Validate default value against field constraints
                if f and hasattr(f, 'metadata'):
                    TypeAdapter(Annotated[t, f]).validate_python(default)

        # 8. Determine optional_enabled state
        # Priority: explicit marker > default value presence > False
        if optional_default_enabled is not None:
            # Explicit marker takes priority
            final_optional_enabled = optional_default_enabled
        elif default is not None:
            # Has default value, start enabled
            final_optional_enabled = True
        else:
            # No default, start disabled
            final_optional_enabled = False

        result[name] = ParamInfo(t, default, f, dynamic_func, is_optional, final_optional_enabled, is_list, list_f)

    return result

func_to_web.build_form_fields

build_form_fields(params_info)

Build form field specifications from parameter metadata.

Re-executes dynamic functions to get fresh options.

This function takes the analyzed parameter information from analyze() and converts it into a list of field specifications that can be used by the template engine to generate HTML form inputs.

Process
  1. Iterate through each parameter's ParamInfo
  2. Determine the appropriate HTML input type (text, number, select, etc.)
  3. Extract constraints and convert them to HTML attributes
  4. Handle special cases (optional fields, dynamic literals, files, etc.)
  5. Serialize defaults to JSON-safe format
  6. Return list of field dictionaries ready for template rendering

Parameters:

Name Type Description Default
params_info dict

Mapping of parameter names to ParamInfo objects. Keys are parameter names (str), values are ParamInfo objects with type, default, field_info, etc.

required

Returns:

Type Description
list[dict[str, Any]]

List of field dictionaries for template rendering. Each dictionary contains:

list[dict[str, Any]]
  • name (str): Parameter name
list[dict[str, Any]]
  • type (str): HTML input type ('text', 'number', 'select', etc.)
list[dict[str, Any]]
  • default (Any): Default value for the field (JSON-serialized)
list[dict[str, Any]]
  • required (bool): Whether field is required (lists are ALWAYS required)
list[dict[str, Any]]
  • is_optional (bool): Whether field has optional toggle
list[dict[str, Any]]
  • optional_enabled (bool): Whether optional field starts enabled
list[dict[str, Any]]
  • is_list (bool): Whether this is a list field
list[dict[str, Any]]
  • list_min_length (int): For list fields, minimum number of items
list[dict[str, Any]]
  • list_max_length (int): For list fields, maximum number of items
list[dict[str, Any]]
  • options (tuple): For select fields, the dropdown options
list[dict[str, Any]]
  • min/max (int/float): For number fields, numeric constraints
list[dict[str, Any]]
  • minlength/maxlength (int): For text fields, length constraints
list[dict[str, Any]]
  • pattern (str): Regex pattern for validation
list[dict[str, Any]]
  • accept (str): For file fields, accepted file extensions
list[dict[str, Any]]
  • step (str): For number fields, '1' for int, 'any' for float
Field Type Detection
  • Literal types → 'select' (dropdown)
  • bool → 'checkbox'
  • date → 'date' (date picker)
  • time → 'time' (time picker)
  • int/float → 'number' (with constraints)
  • str with file pattern → 'file' (file upload)
  • str with color pattern → 'color' (color picker)
  • str with email pattern → 'email' (email input)
  • str (default) → 'text' (text input)
Source code in func_to_web\build_form_fields.py
def build_form_fields(params_info: dict) -> list[dict[str, Any]]:
    """Build form field specifications from parameter metadata.

    Re-executes dynamic functions to get fresh options.

    This function takes the analyzed parameter information from analyze() and
    converts it into a list of field specifications that can be used by the
    template engine to generate HTML form inputs.

    Process:
        1. Iterate through each parameter's ParamInfo
        2. Determine the appropriate HTML input type (text, number, select, etc.)
        3. Extract constraints and convert them to HTML attributes
        4. Handle special cases (optional fields, dynamic literals, files, etc.)
        5. Serialize defaults to JSON-safe format
        6. Return list of field dictionaries ready for template rendering

    Args:
        params_info: Mapping of parameter names to ParamInfo objects.
            Keys are parameter names (str), values are ParamInfo objects with 
            type, default, field_info, etc.

    Returns:
        List of field dictionaries for template rendering. Each dictionary contains:

        - name (str): Parameter name
        - type (str): HTML input type ('text', 'number', 'select', etc.)
        - default (Any): Default value for the field (JSON-serialized)
        - required (bool): Whether field is required (lists are ALWAYS required)
        - is_optional (bool): Whether field has optional toggle
        - optional_enabled (bool): Whether optional field starts enabled
        - is_list (bool): Whether this is a list field
        - list_min_length (int): For list fields, minimum number of items
        - list_max_length (int): For list fields, maximum number of items
        - options (tuple): For select fields, the dropdown options
        - min/max (int/float): For number fields, numeric constraints
        - minlength/maxlength (int): For text fields, length constraints
        - pattern (str): Regex pattern for validation
        - accept (str): For file fields, accepted file extensions
        - step (str): For number fields, '1' for int, 'any' for float

    Field Type Detection:
        - Literal types → 'select' (dropdown)
        - bool → 'checkbox'
        - date → 'date' (date picker)
        - time → 'time' (time picker)
        - int/float → 'number' (with constraints)
        - str with file pattern → 'file' (file upload)
        - str with color pattern → 'color' (color picker)
        - str with email pattern → 'email' (email input)
        - str (default) → 'text' (text input)
    """
    fields = []

    for name, info in params_info.items():
        # Serialize default value to JSON-safe format
        serialized_default = serialize_for_json(info.default)

        field = {
            'name': name, 
            'default': serialized_default,
            # REGLA SIMPLE: Las listas SIEMPRE son required=True
            # Si el campo está habilitado (no opcional O toggle ON), debe tener valor
            # Las listas nunca pueden estar vacías, si se quiere vacío usar None
            'required': True if info.is_list else not info.is_optional,
            'is_optional': info.is_optional,
            'optional_enabled': info.optional_enabled,
            'is_list': info.is_list
        }

        # Si es una lista, extraer los constraints de lista (min_length, max_length)
        if info.is_list and info.list_field_info and hasattr(info.list_field_info, 'metadata'):
            for c in info.list_field_info.metadata:
                cn = type(c).__name__
                if cn == 'MinLen':
                    field['list_min_length'] = c.min_length
                if cn == 'MaxLen':
                    field['list_max_length'] = c.max_length

        # Dropdown select
        if get_origin(info.field_info) is Literal:
            field['type'] = 'select'

            # Re-execute dynamic function if present
            if info.dynamic_func is not None:
                result_value = info.dynamic_func()

                # Convert result to tuple properly
                if isinstance(result_value, (list, tuple)):
                    fresh_options = tuple(result_value)
                else:
                    fresh_options = (result_value,)

                field['options'] = fresh_options
                info.field_info = Literal[fresh_options]
            else:
                field['options'] = get_args(info.field_info)

        # Checkbox
        elif info.type is bool:
            field['type'] = 'checkbox'
            field['required'] = False

        # Date picker
        elif info.type is date:
            field['type'] = 'date'
            # Already serialized above, no need to do it again

        # Time picker
        elif info.type is time:
            field['type'] = 'time'
            # Already serialized above, no need to do it again

        # Number input
        elif info.type in (int, float):
            field['type'] = 'number'
            field['step'] = '1' if info.type is int else 'any'

            # Extract numeric constraints from Pydantic Field
            if info.field_info and hasattr(info.field_info, 'metadata'):
                for c in info.field_info.metadata:
                    cn = type(c).__name__
                    if cn == 'Ge': field['min'] = c.ge
                    elif cn == 'Le': field['max'] = c.le
                    elif cn == 'Gt': field['min'] = c.gt + (1 if info.type is int else 0.01)
                    elif cn == 'Lt': field['max'] = c.lt - (1 if info.type is int else 0.01)

        # Text/email/color/file input
        else:
            field['type'] = 'text'

            if info.field_info and hasattr(info.field_info, 'metadata'):
                for c in info.field_info.metadata:
                    cn = type(c).__name__

                    # Check for pattern constraints
                    if hasattr(c, 'pattern') and c.pattern:
                        pattern = c.pattern

                        # File input detection
                        if pattern.startswith(r'^.+\.(') and pattern.endswith(r')$'):
                            field['type'] = 'file'
                            exts = pattern[6:-2].split('|')
                            field['accept'] = '.' + ',.'.join(exts)
                        # Special input types (color, email)
                        elif pattern in PATTERN_TO_HTML_TYPE:
                            field['type'] = PATTERN_TO_HTML_TYPE[pattern]

                        field['pattern'] = pattern

                    # String length constraints
                    if cn == 'MinLen': 
                        field['minlength'] = c.min_length
                    if cn == 'MaxLen':
                        field['maxlength'] = c.max_length

        fields.append(field)

    return fields

serialize_for_json(value)

Serialize a value to be JSON-safe for template rendering.

Converts date/time objects to ISO format strings.

Parameters:

Name Type Description Default
value Any

The value to serialize (can be any type).

required

Returns:

Type Description
Any

JSON-safe serialized value (str for dates/times, or original type).

Source code in func_to_web\build_form_fields.py
def serialize_for_json(value: Any) -> Any:
    """Serialize a value to be JSON-safe for template rendering.

    Converts date/time objects to ISO format strings.

    Args:
        value: The value to serialize (can be any type).

    Returns:
        JSON-safe serialized value (str for dates/times, or original type).
    """
    if value is None:
        return None

    if isinstance(value, date):
        return value.isoformat()

    if isinstance(value, time):
        return value.isoformat()

    if isinstance(value, list):
        return [serialize_for_json(item) for item in value]

    if isinstance(value, dict):
        return {k: serialize_for_json(v) for k, v in value.items()}

    return value

func_to_web.process_result

process_result(result)

Convert function result to appropriate display format.

Handles images (PIL, Matplotlib), single/multiple files, and text. Returns a dictionary with 'type' and relevant data. Files are saved to temporary files and paths are returned.

Source code in func_to_web\process_result.py
def process_result(result):
    """
    Convert function result to appropriate display format.

    Handles images (PIL, Matplotlib), single/multiple files, and text.
    Returns a dictionary with 'type' and relevant data.
    Files are saved to temporary files and paths are returned.
    """
    # PIL Image detection
    try:
        from PIL import Image
        if isinstance(result, Image.Image):
            buffer = io.BytesIO()
            result.save(buffer, format='PNG')
            buffer.seek(0)
            img_base64 = base64.b64encode(buffer.read()).decode()
            return {
                'type': 'image',
                'data': f'data:image/png;base64,{img_base64}'
            }
    except ImportError:
        pass

    # Matplotlib Figure detection
    try:
        import matplotlib.pyplot as plt
        from matplotlib.figure import Figure
        if isinstance(result, Figure):
            buffer = io.BytesIO()
            result.savefig(buffer, format='png', bbox_inches='tight')
            buffer.seek(0)
            img_base64 = base64.b64encode(buffer.read()).decode()
            plt.close(result)
            return {
                'type': 'image',
                'data': f'data:image/png;base64,{img_base64}'
            }
    except ImportError:
        pass

    # Single file - save to temp
    if isinstance(result, UserFileResponse):
        with tempfile.NamedTemporaryFile(delete=False, suffix=f"_{result.filename}") as tmp:
            tmp.write(result.data)
            return {
                'type': 'download',
                'path': tmp.name,
                'filename': result.filename
            }

    # Multiple files - save to temp
    if isinstance(result, list) and len(result) > 0 and all(isinstance(f, UserFileResponse) for f in result):
        files = []
        for f in result:
            with tempfile.NamedTemporaryFile(delete=False, suffix=f"_{f.filename}") as tmp:
                tmp.write(f.data)
                files.append({
                    'path': tmp.name,
                    'filename': f.filename
                })
        return {
            'type': 'downloads',
            'files': files
        }

    # Default: convert to string
    return {
        'type': 'text',
        'data': str(result)
    }

func_to_web.validate_params

validate_list_param(value, info, param_name)

Validate and convert a JSON string to a typed list.

Parameters:

Name Type Description Default
value str | None

JSON string like "[1, 2, 3]" or "[]".

required
info ParamInfo

ParamInfo with type and constraints for list items.

required
param_name str

Name of the parameter (for error messages).

required

Returns:

Type Description
list

Validated list with proper types.

Raises:

Type Description
TypeError

If value is not a valid list.

ValueError

If items don't pass validation or list size constraints are violated.

JSONDecodeError

If JSON is invalid.

Source code in func_to_web\validate_params.py
def validate_list_param(value: str | None, info: ParamInfo, param_name: str) -> list:
    """Validate and convert a JSON string to a typed list.

    Args:
        value: JSON string like "[1, 2, 3]" or "[]".
        info: ParamInfo with type and constraints for list items.
        param_name: Name of the parameter (for error messages).

    Returns:
        Validated list with proper types.

    Raises:
        TypeError: If value is not a valid list.
        ValueError: If items don't pass validation or list size constraints are violated.
        json.JSONDecodeError: If JSON is invalid.
    """
    # Parse JSON
    if not value or value == "":
        list_value = []
    else:
        try:
            list_value = json.loads(value)
        except json.JSONDecodeError as e:
            raise ValueError(f"'{param_name}': Invalid list format: {e}")

    if not isinstance(list_value, list):
        raise TypeError(f"'{param_name}': Expected list, got {type(list_value).__name__}")

    # Validate list-level constraints (min_length, max_length)
    if info.list_field_info and hasattr(info.list_field_info, 'metadata'):
        min_length = None
        max_length = None

        for constraint in info.list_field_info.metadata:
            constraint_name = type(constraint).__name__

            if constraint_name == 'MinLen':
                min_length = constraint.min_length
            elif constraint_name == 'MaxLen':
                max_length = constraint.max_length
            elif hasattr(constraint, 'min_length'):
                min_length = constraint.min_length
            elif hasattr(constraint, 'max_length'):
                max_length = constraint.max_length

        # Validate min_length
        if min_length is not None and len(list_value) < min_length:
            raise ValueError(
                f"'{param_name}': List must have at least {min_length} item{'s' if min_length != 1 else ''}, "
                f"got {len(list_value)}"
            )

        # Validate max_length
        if max_length is not None and len(list_value) > max_length:
            raise ValueError(
                f"'{param_name}': List must have at most {max_length} item{'s' if max_length != 1 else ''}, "
                f"got {len(list_value)}"
            )

    # Validate each item
    validated_list = []
    for i, item in enumerate(list_value):
        try:
            validated_item = validate_single_item(item, info)
            validated_list.append(validated_item)
        except (ValueError, TypeError) as e:
            raise ValueError(f"'{param_name}': List item at index {i}: {e}")

    return validated_list

validate_params(form_data, params_info)

Validate and convert form data to function parameters.

This function takes raw form data (where everything is a string) and converts it to the proper Python types based on the parameter metadata from analyze(). It handles type conversion, optional field toggles, and validates against constraints defined in Pydantic Field or Literal types.

Process
  1. Check if optional fields are enabled via toggle
  2. Convert strings to proper types (int, float, date, time, bool)
  3. For lists: parse JSON and validate each item
  4. Validate Literal values against allowed options
  5. Validate against Pydantic Field constraints (ge, le, min_length, etc.)
  6. Handle special cases (hex color expansion, empty values)

Parameters:

Name Type Description Default
form_data dict

Raw form data from HTTP request. - Keys are parameter names (str) - Values are form values (str, or None for checkboxes) - For lists: JSON string like "[1, 2, 3]" - Optional toggles have keys like "{param}_optional_toggle"

required
params_info dict[str, ParamInfo]

Parameter metadata from analyze(). - Keys are parameter names (str) - Values are ParamInfo objects with type and validation info

required

Returns:

Type Description
dict

Validated parameters ready for function call.

dict

Keys are parameter names (str), values are properly typed Python objects.

Raises:

Type Description
ValueError

If a value doesn't match Literal options or Field constraints.

TypeError

If type conversion fails.

JSONDecodeError

If list JSON is invalid.

Source code in func_to_web\validate_params.py
def validate_params(form_data: dict, params_info: dict[str, ParamInfo]) -> dict:
    """Validate and convert form data to function parameters.

    This function takes raw form data (where everything is a string) and converts
    it to the proper Python types based on the parameter metadata from analyze().
    It handles type conversion, optional field toggles, and validates against
    constraints defined in Pydantic Field or Literal types.

    Process:
        1. Check if optional fields are enabled via toggle
        2. Convert strings to proper types (int, float, date, time, bool)
        3. For lists: parse JSON and validate each item
        4. Validate Literal values against allowed options
        5. Validate against Pydantic Field constraints (ge, le, min_length, etc.)
        6. Handle special cases (hex color expansion, empty values)

    Args:
        form_data: Raw form data from HTTP request.
            - Keys are parameter names (str)
            - Values are form values (str, or None for checkboxes)
            - For lists: JSON string like "[1, 2, 3]"
            - Optional toggles have keys like "{param}_optional_toggle"
        params_info: Parameter metadata from analyze().
            - Keys are parameter names (str)
            - Values are ParamInfo objects with type and validation info

    Returns:
        Validated parameters ready for function call.
        Keys are parameter names (str), values are properly typed Python objects.

    Raises:
        ValueError: If a value doesn't match Literal options or Field constraints.
        TypeError: If type conversion fails.
        json.JSONDecodeError: If list JSON is invalid.
    """
    validated = {}

    for name, info in params_info.items():
        value = form_data.get(name)

        # Check if optional field is disabled
        optional_toggle_name = f"{name}_optional_toggle"
        if info.is_optional and optional_toggle_name not in form_data:
            # Optional field is disabled, send None
            validated[name] = None
            continue

        # Handle list fields
        if info.is_list:
            validated[name] = validate_list_param(value, info, name)
            continue

        # Checkbox handling
        if info.type is bool:
            validated[name] = value is not None
            continue

        # Date conversion
        if info.type is date:
            if value:
                validated[name] = date.fromisoformat(value)
            else:
                validated[name] = None
            continue

        # Time conversion
        if info.type is time:
            if value:
                validated[name] = time.fromisoformat(value)
            else:
                validated[name] = None
            continue

        # Literal validation
        if get_origin(info.field_info) is Literal:
            # Convert to correct type
            if info.type is int:
                value = int(value)
            elif info.type is float:
                value = float(value)

            # Only validate against options if Literal is NOT dynamic
            # Dynamic literals can change between form render and submit
            if info.dynamic_func is None:
                # Static literal - validate against fixed options
                opts = get_args(info.field_info)
                if value not in opts:
                    raise ValueError(f"'{name}': value '{value}' not in {opts}")
            # else: Dynamic literal - skip validation, trust the value from the form

            validated[name] = value
            continue

        # Expand shorthand hex colors (#RGB -> #RRGGBB)
        if value and isinstance(value, str) and value.startswith('#') and len(value) == 4:
            value = '#' + ''.join(c*2 for c in value[1:])

        # Pydantic validation with constraints
        if info.field_info and hasattr(info.field_info, 'metadata'):
            adapter = TypeAdapter(Annotated[info.type, info.field_info])
            validated[name] = adapter.validate_python(value)
        else:
            validated[name] = info.type(value) if value else None

    return validated

validate_single_item(item, info)

Validate a single list item.

Reuses the same validation logic as non-list parameters.

Parameters:

Name Type Description Default
item Any

The item value from the JSON array.

required
info ParamInfo

ParamInfo with type and constraints.

required

Returns:

Type Description
Any

Validated and converted item.

Source code in func_to_web\validate_params.py
def validate_single_item(item: Any, info: ParamInfo) -> Any:
    """Validate a single list item.

    Reuses the same validation logic as non-list parameters.

    Args:
        item: The item value from the JSON array.
        info: ParamInfo with type and constraints.

    Returns:
        Validated and converted item.
    """
    # Handle None/null values
    if item is None:
        return None

    # Bool (already bool from JSON)
    if info.type is bool:
        return bool(item)

    # Date (comes as string from JSON)
    if info.type is date:
        if isinstance(item, str):
            return date.fromisoformat(item)
        return item

    # Time (comes as string from JSON)
    if info.type is time:
        if isinstance(item, str):
            return time.fromisoformat(item)
        return item

    # Literal in lists is not supported (prohibited by analyze())
    if get_origin(info.field_info) is Literal:
        raise TypeError("list[Literal[...]] is not supported")

    # Expand shorthand hex colors (#RGB -> #RRGGBB)
    if item and isinstance(item, str) and item.startswith('#') and len(item) == 4:
        item = '#' + ''.join(c*2 for c in item[1:])

    # Number types: ensure conversion from string if needed
    if info.type in (int, float):
        if isinstance(item, str):
            item = info.type(item)
        elif isinstance(item, (int, float)):
            # JSON already parsed it as number
            item = info.type(item)

    # Pydantic validation with constraints
    if info.field_info and hasattr(info.field_info, 'metadata'):
        adapter = TypeAdapter(Annotated[info.type, info.field_info])
        return adapter.validate_python(item)
    else:
        # Basic type conversion for types without constraints
        if info.type in (int, float):
            # Already converted above
            return item
        elif info.type is str:
            return str(item) if item is not None else None
        else:
            # For other types (shouldn't reach here normally)
            return info.type(item) if item is not None else None