(by https://github.com/Dr1985)
- Fixed a path traversal vulnerability in file uploads. The original
filename from the multipart request was joined into the save path
without sanitization, allowing
../sequences to escapeuploads_dirand write files to arbitrary locations. Filenames are now reduced to their final path component. Thanks to the reporter for the catch.
- The package no longer ships unrelated top-level folders —
setuptoolswas discovering every directory that looked like a package, sopip install func-to-webdropped_private/,docs/andexamples/into the user'ssite-packagesas global top-level packages (visible in the oldtop_level.txt). Packaging is now scoped withinclude = ["func_to_web*"]in[tool.setuptools.packages.find], so onlyfunc_to_webis installed. - Returned files are now stream-copied instead of being read fully into RAM —
save_returned_fileusedPath(...).read_bytes()+write_bytes()for theFileResponse(path=...)case, loading the entire file into memory just to rewrite it (a 2 GB return meant 2 GB of RAM), which defeated the whole point of passingpath=. It now copies in 8 MB chunks viashutil.copyfileobj. The in-memorydata=branch is unchanged. - Result serialization no longer blocks the event loop — the function's
return value was serialized (writing returned files, base64-encoding
PIL/matplotlib PNGs, building tables) directly inside the running coroutine.
For
asyncfunctions this always ran on the event loop; forsyncfunctions theto_threadwrapper only covered the call itself, not the serialization. A large output froze the server for every connected user.process_resultnow runs viaasyncio.to_thread, moving all heavy CPU/IO off the loop regardless of whether the user's function is sync or async. - The per-function param list is no longer mutated concurrently —
create_handlersanalyzed the function once into a singleparamslist that was captured by thepage_handler/submit_handlerclosures and shared across every request.page_handlerrewrote it in place on each render (refresh_params), so two concurrent requests — e.g. a slowDropdown(func)refresh racing another render, or a render racing a submit's validation — could serialize or validate against a half-refreshed list. The shared list is now an immutable template (base_params); each page render builds its own refreshed copy ([p.refresh_choices() for p in base_params]), and submit validation reads the template directly (dynamic dropdown options aren't validated server-side, so the submit never needed fresh choices). No shared mutable state, no locks — the same principle as the 1.5.0 globals cleanup, applied to the last place mutable shared state remained. - Invalid
Paramssetups are now rejected at startup with a clear error instead of misbehaving silently — three cases that previously slipped through: a field name colliding across the flat form (aParamsfield matching a function parameter or a field from anotherParamsclass) madeparams_by_namekeep only the last one, so the form rendered duplicate inputs and validation/reconstruction stole values between them; a nestedParamsfield and an optionalParamsparameter (data: UserData | None, bothtyping.Unionand PEP 604) fell through to the generic analyzer and crashed with a cryptic internal error. All three now raise an explicitValueErrorwhen routes are registered, naming the offending field/parameter and how to fix it (rename the field; flatten the nested class; make individual fields optional inside the class instead). Valid code is unaffected. - Server-side validation errors (HTTP 422/400) are now shown in the UI instead
of being silently dropped — the frontend fed every submit response through
the SSE parser; a 422 is plain JSON, matched no SSE blocks, and was discarded
without a trace, so submits failing server-only validation (notably a
Params__post_init__raisingValueError) appeared to do nothing. Non-200 responses are now detected in both submit paths (fetch and XHR upload) and rendered in the error block, listing each offending field and its message. Client-side validation masked this for ordinary field constraints, which is why it went unnoticed until server-only validation existed.
Paramssubclasses are now frozen dataclasses —Paramswas an empty marker class and instances were rebuilt internally viaobject.__new__+ attribute assignment, which silently skipped any user-defined__init__and produced half-constructed objects. SubclassingParamsnow applies@dataclass(frozen=True)automatically: instances are constructible anywhere (UserData(name=..., email=...)), comparable, hashable, and immutable, withdataclasses.replace()for variants. Cross-field validation goes in__post_init__; aValueErrorraised there surfaces as a 422 form error. Breaking: defining a custom__init__is no longer supported (the dataclass generates it), and mutating a Params instance now raisesFrozenInstanceError. Internally, the manual_reconstructstep is gone — construction is justYourClass(**fields).
aiofilesdependency dropped — it was used in a single place, the chunked write loop insave_uploaded_file. The same non-blocking behavior is now achieved with a plainopen()andawait asyncio.to_thread(f.write, chunk)(the upload read staysawait uploaded_file.read(...)via Starlette'sUploadFile). At 8 MB chunks the thread-hop overhead is negligible. One fewer dependency, no behavior change.
- Upload cleanup docs corrected —
files.mdclaimed FuncToWeb "skips cleanup on files that no longer exist in the original path", describing a mechanic that doesn't exist. The temporary upload folder is always removed after the function finishes;shutil.move()works because it moves the file out of that folder before deletion, not because of any skip. Paramscross-field validation documented —/docandapi-docs.mdnow note that a 422errorskey may be a single field or aParamsgroup whose__post_init__rejected an otherwise field-valid combination;params.mdadds that this validation runs server-side (the error appears on submit, not while typing) and aLimitationssection spelling out the nested / optional / duplicate-name cases that are rejected at startup.
This release is a big simplification pass. The goal: remove features that can be done more elegantly other ways, and make FuncToWeb composable.
1.5.0 takes FuncToWeb from "tool for spinning up mini programs" to "a library you can use both ways": run() serves your functions standalone, exactly as it always has, and the new create_app() returns a plain FastAPI app you can mount inside your own. Nothing from the original mode is lost — the auto-generated form UI, single/multi function apps, and everything you already use keep working exactly as before.
-
create_app(): builds the FuncToWeb FastAPI application without starting a server. This makes the library embeddable — mount it inside a larger app — and unlocksuvicorn --workers N/--reloadby serving via import string (both are rejected byrun(), which serves an app instance):from fastapi import FastAPI from func_to_web import create_app host = FastAPI() host.mount("/tools", create_app([add, multiply]))
All internal URLs are derived per request from the ASGI
root_path, so a mounted app works under any prefix with no configuration.run()is now a thin wrapper: guards + startup sweeps +create_app()+ Uvicorn.Note: the startup sweeps (leftover upload folders, expired returned files) run only in
run().create_app()deliberately skips them — under multiple workers, each worker builds its own app, and sweeping the shared directories on every build could delete a sibling worker's in-flight uploads during rolling restarts. Expired returned files are still cleaned opportunistically at runtime.
- Internal CSS/JS bundles are now built in memory and served from routes, not written to a temp dir —
create_pytypeinput_assets()(which concatenated pytypeinputweb +internal_staticassets and wrotestyles.css/scripts.jsto<temp>/func_to_web/static, mounted at/static) is replaced bybuild_static_assets(), which returns the two bundles as strings; they're served by two FastAPI routes captured in a closure. Why: the temp-dir files let two processes (or two installed versions) clobber each other's bundles, and causedPermissionErroron shared multi-user temp dirs on Linux. Nothing is written to disk anymore. The internal asset URLs changed:/static/styles.css→/_functoweb/static/styles.cssand/static/scripts.js→/_functoweb/static/scripts.js(internal URLs). - Returned-file cleanup is now opportunistic instead of timer-based — the per-process background daemon thread (
start_cleanup_timer) that sweptreturns_direveryreturns_lifetimeseconds has been removed. Cleanup now runs lazily on activity — when a file is saved (FileResponse) or downloaded — throttled by a.last_cleanupmarker file so it does real work at most once perreturns_lifetimewindow. Why: under the upcoming multi-processcreate_app()deployments, N workers would have spawned N redundant threads sweeping the same directory (and a library spawning daemon threads is undesirable in general); the marker approach is multi-process safe by being harmless (no locks — duplicate deletes are ignored). Observable contract change: expired files are now deleted on the next save/download after expiry rather than on a fixed timer; the boot-time cleanup inrun()still applies.returns_lifetimekeeps the same meaning. - Internals are now free of module-level config state — the upload/return directories, size limit and
stream_printsflag are no longer mutated onto module globals (save_file_handler.UPLOADS_DIR, etc.);run()resolves them as locals and passes them down explicitly through closures. Public behaviour is unchanged, but anyone who used to monkey-patchsave_file_handler.UPLOADS_DIR(or the other module globals) must pass the correspondingrun()keyword instead. - Default uploads and returned-files directories moved to the OS temp folder —
uploads_dirnow defaults to<os-temp-dir>/func_to_web_uploadsandreturns_dirto<os-temp-dir>/func_to_web_returned_files(resolved viatempfile.gettempdir()), instead of./uploadsand./returned_filesin the current working directory; transient files no longer pollute the project folder and the OS reclaims them automatically. Pass an explicituploads_dir=.../returns_dir=...to keep the previous behaviour root_pathpassed insidefastapi_configis now forwarded to FastAPI instead of being silently stripped — the internal filtering existed to protect a build-timeroot_paththat no longer exists (mounted apps get their prefix per request from Starlette;run()passesroot_paththrough to Uvicorn). Note that setting it there is normally unnecessary and can double prefixes if combined with mounting orrun(root_path=...).- Public namespace is now declared explicitly via
__all__infunc_to_web/types.py— previouslyfrom .types import *(no__all__) leaked every transitive import into the package root, sofunc_to_web.json,func_to_web.Path,func_to_web.BaseModel,func_to_web.dataclass,func_to_web.Any,func_to_web.Callableandfunc_to_web.model_validatorall existed as accidental, undocumented re-exports. These are no longer accessible from the package root (or viafrom func_to_web.types import *); import them from their real source (json,pathlib,pydantic) instead. The deliberate surface is unchanged:Field,Annotated,Literal,date,time,Params,FileResponseand all pytypeinput types (Color,Email,File,Slider, …) remain exported, and explicit imports likefrom func_to_web.types import Emailare unaffected. - Hardcoded Uvicorn deployment defaults removed
root_pathparameter removed fromrun()'s signature — now passed through to Uvicorn via**uvicorn_kwargs, which injects it into the ASGI scope.run(func, root_path="/tools")works exactly as before (no trailing slash; the previous auto-normalization is gone).- Sidebar navigation replaced by a "back to index" button — multi-function pages no longer render the left sidebar listing every function (and its mobile toggle/overlay). Each function page now shows only a small back button that returns to the index, which remains the single place that lists all functions. Simpler chrome, less code (the whole sidebar template, its JS and CSS are gone). Single-function apps are unchanged (no index, no button).
- FastAPI's Swagger UI / ReDoc / OpenAPI schema are off by default —
/docs,/redocand/openapi.jsonnow return404. The functions are exposed by name, not as typed OpenAPI operations, so that auto-generated schema misdescribed them;/docis the honest, machine-readable description. Re-enable viafastapi_config(e.g.create_app(func, fastapi_config={"openapi_url": "/openapi.json", "docs_url": "/docs"})). When mounted withcreate_app(), your host app's own docs are untouched. - Internal CSS/JS bundles are now browser-cacheable —
/_functoweb/static/styles.cssandscripts.jsare served withCache-Control: max-age=3600and a content-hashETag. Repeat loads within the window hit the browser cache (zero requests); after it expires the browser revalidates cheaply withIf-None-Matchand gets a304instead of re-downloading. A restart with new code changes the hash, so stale bundles invalidate themselves.
-
Authentication (
auth/secret_key). No compatibility shims remain: passing them now fails with a plainTypeError/Uvicorn error. Protect your app with a reverse proxy that handles auth (e.g. Nginx basic auth). -
front_dir/assets_dirfromrun(). Superseded by composition: mount your static site next to the tools with Starlette'sStaticFiles:host = FastAPI() host.mount("/tools", create_app(funcs)) host.mount("/", StaticFiles(directory="dist", html=True))
-
keep_uploadsparameter — removed fromrun(). Uploaded files were transient by design and are always cleaned up after the function finishes; persisting them is now done explicitly by moving the file out ofuploads_dir(e.g. withshutil.move()) before returning -
ActionTable— removed entirely, with no backward compatibility: returning one now falls through to the genericstr()text output, and theaction_tableSSE result type no longer exists. It coupled navigation to a table widget and had been marked experimental since its introduction. For standalone tools, functions remain directly reachable by URL with prefill; for CRUD apps with their own frontend, mountcreate_app()inside FastAPI and drive forms via URL prefill + embed mode. A first-class navigable output (Link) is planned after the API stabilizes. -
HiddenFunction— removed, with no backward compatibility: importing it now fails with anImportError, and every registered function always appears in the index and navigation. The flag had no audience: withrun()you want all your tools visible (the index is the UI), and when mounting viacreate_app()nobody looks at that index — if you need internal endpoints separated from visible tools, mount two apps (host.mount("/tools", create_app(visible))/host.mount("/api", create_app(internal))), which composes cleaner than a per-function flag. -
Function groups — passing a dict (or nested dicts) to
run()/create_app()to build collapsible, slug-prefixed navigation groups is gone.run()now accepts only a single function or a flat list; every function lives at its own top-level/<slug>. To separate sets of tools, mount severalcreate_app()apps under different FastAPI paths instead — it's clearer and removes a confusing nesting mechanism.
- Internal URLs are now prefix-aware (work under a
root_path/ when mounted) — the HTML/JS emitted absolute, root-anchored URLs (/submit,/download/<id>,/_functoweb/static/..., navigation/index links). Behind a reverse proxy with a realroot_path, or underapp.mount("/tools", ...), those pointed at the domain root and broke styling, form submit, navigation and downloads. URLs are now derived per request fromrequest.scope["root_path"]and prepended to internal paths (templates receive aprefix; the frontend readswindow.__functoweb_prefix). The SSE payload is unchanged, as is/doc, the API contract andrun()'s public signature. This also paves the way for the mountablecreate_app()added in this release. workersandreloadpassed torun()are now rejected instead of silently ignored —run()hands the app instance to Uvicorn, and Uvicorn only spawns multiple workers, or runs its reload supervisor, when given an import string ("module:app"). Aworkers=N(N > 1) orreload=Truein**uvicorn_kwargstherefore had no effect: callers believed they had N processes / auto-reload while actually running a single, non-reloading process.run(func, workers=2+)andrun(func, reload=True)now raise an explicitValueError. Migration:workers=1(or omitting it) is unchanged; for real multiprocess or auto-reload, build the app withcreate_app()(added in this release) and serve it by import string withuvicorn/gunicorn(e.g.uvicorn mymodule:app --workers 4 --reload).
No observable behaviour change; dead-code and vestigial cleanup.
- Package reorganized by responsibility — the arbitrary root-vs-
core/split is gone. The public API (__init__,run,types,models) and shared foundations (normalization,constants,utils) sit at the package root; the rest moved into themed subpackages:serving/(server, routes, request handlers),rendering/(templates/builder,/doc),execution/(function call, result/table serialization, print capture) andfiles/(upload/return persistence). Public imports (from func_to_web import ...,from func_to_web.types import ...) are unchanged; only internal module paths moved. FunctionMetadatais now the single source of truth for multi-function apps — the parallelnavigation_datalist of{name, slug, description, url}dicts (built bybuild_navigation_structure()and stored onNormalizedInput) duplicated theFunctionMetadataobjects already initems, since every URL is just/<slug>. It's gone: templates, the index redirect and route registration readitemsdirectly, slug-uniqueness is validated innormalize_items(), and the now-trivial helpersbuild_navigation_structure(),register_navigation_routes()anddetect_input_type()were removed (the last two inlined). No behaviour change.- Removed a dead destructure binding in
zz-form.js—getOrCreateContainerwas pulled out ofwindow.functoweb.resultbut never used (onlyclearContainerandrenderResultare); it stays exported byresult-renderer.js, which uses it internally.
ActionTablecell serialization for list / tuple / dict values — non-scalar cells were being rendered with Python'sstr(), producing invalid output like['34', 'aaa'](single quotes, not parseable as JSON)- Now serialized with
json.dumps, producing standard["34","aaa"]
- Now serialized with
ActionTablerow click sentNonecells as the literal string"None"in the URL — clicking a row produced URLs like?tags=None, which the prefill layer treated as a real value (activating optional toggles, failing to JSON-parse list fields)Noneis now preserved through serialization and the row-click handler omits the parameter entirely, matching the prefill contract (absent param == no value)
ActionTablerow click dropped embed mode when redirecting to another function — clicking a row in an embedded form (?__embed=1) navigated to the target function without the embed flag, so the destination rendered with full chrome (sidebar, theme toggle, opaque background) inside the iframe- The row-click handler now detects
__embed=1on the current page and propagates it to the redirect URL, keeping the whole action chain embedded
- The row-click handler now detects
- Single quotes / apostrophes in
Description(...)andPatternMessage(...)broke the form — param metadata is serialized to JSON and injected into the<pti-form params='...'>attribute, which is delimited by single quotes; any apostrophe in the text closed the attribute early and broke the rendered form- The serialized JSON is now HTML-escaped in the template (
params='{{ params_json | e }}'), so apostrophes (and",<,&) survive as entities and are decoded back to the original JSON when the<pti-form>web component reads the attribute
- The serialized JSON is now HTML-escaped in the template (
- Default
limit_max_requestsraised from 1000 to 10000 — the previous limit recycled the Uvicorn worker too aggressively for apps serving many static assets per page (e.g. image grids), causing the process to restart mid-session - Fixed pytypeinput and pytypeinputweb version numbers in dependencies — updated to the latest versions (1.0.2 and 1.0.3 respectively) to ensure compatibility with the new features and fixes in those libraries
- Uploads and returned-files directories are now created lazily —
uploads/andreturned_files/are no longer created at server startup; each directory is created on demand the first time a file is actually uploaded or returned, so apps with no file I/O never create them
-
Custom frontend hosting via
front_dirandassets_dir—run()now accepts two new parameters to serve a custom frontend from the same processfront_dir: directory mounted at/frontwithhtml=Truefor SPA-style routing — drop a static site, landing page, or built React/Vue/Svelte bundle next to your Python functionsassets_dir: directory mounted at/assetsfor images, fonts, downloads or any static files referenced by your frontend or forms- Both are excluded from the auth middleware so static content is reachable without login
- Lets a single FuncToWeb process host the form UI, the API and a full custom frontend — no separate web server needed
-
/docendpoint — auto-generated, machine-readable API documentation- Every app now exposes
GET /docreturning a single plain-text document - Lists all registered functions (visible and hidden) with their parameters,
constraints, choices, defaults, and a working
curlexample for each - Parameters are emitted as JSON, making the doc directly parseable
- File parameters include an
upload_infoblock describing the multipart transport, field name, and whether multiple files are accepted - Dynamic dropdowns (
Dropdown(func)) are flagged with"dynamic": trueso consumers know the listed options are a snapshot, not an exhaustive set - URLs in examples use a
<base_url>placeholder, making the doc portable across local, proxied and production deployments - Designed to be consumed by humans, scripts, or AI agents calling the API without prior knowledge of the app
- Every app now exposes
-
Embed mode for iframe integration — append
?__embed=1to any function URL- Strips the sidebar, theme toggle, and outer chrome at runtime
- Forces a transparent background so the form blends into the parent page
- Removes the container's max-width, padding, shadow and border
- Lets you drop a FuncToWeb form into an existing web app via
<iframe>with no visual seams — combine with URL prefill (?param=value) for a fully pre-configured embedded form
Biggest release so far. The library has been rewritten from the ground up — most existing code works without changes or with very minor ones.
The biggest structural change is that FuncToWeb is now split into three independent libraries:
- pytypeinput — Analyzes Python type hints and extracts UI metadata. No web dependency.
- pytypeinputweb — Renders HTML forms from
pytypeinputmetadata. Use it in your own server. - func-to-web — The full stack.
This opens up a lot of new possibilities — for example, using FuncToWeb as a support layer inside an existing web app, exposing individual utility functions without building a full tool. There are other interesting approaches worth exploring that the docs cover.
A full re-read of the documentation is recommended.
This is a stable beta. I'll be actively fixing issues and improving things over the coming days.
- Starlette compatibility issue
- Added explicit starlette<1.0.0
- Multiple file upload improvements for
list[FileType]- New "+" button next to file input allows adding files from different folders
- Visual file list shows selected files with names, sizes, and remove buttons
- Supports all file types:
ImageFile,VideoFile,AudioFile,DataFile,TextFile,DocumentFile,File - Files can be selected from one folder, then more added from other folders
- Individual files can be removed before upload
- File list automatically hides when optional field is disabled
- Improved UX for file uploads with real-time feedback and preview
- Only frontend changes, fully backwards compatible with existing backend logic
- New
Dropdown()type for dynamic dropdowns - cleaner, type-safe syntax for dropdowns with runtime-generated options- Use
Annotated[str, Dropdown(get_options)]instead ofLiteral[get_options] - Provides better IDE support and clearer intent
- Example:
- Use
from typing import Annotated
from func_to_web.types import Dropdown
def get_users():
return ['alice', 'bob', 'charlie']
def send_message(to: Annotated[str, Dropdown(get_users)]):
return f"Message sent to {to}"- Works with
str,int,float, andbooltypes - Fully backwards compatible -
Literal[func]syntax still supported
- Grouped functions feature: organize multiple functions into collapsible accordion groups
- Pass a dictionary to
run()with group names as keys and function lists as values - Example:
run({'Math': [add, multiply], 'Text': [upper, lower]}) - Groups display as accordion cards with badges showing function count
- Only one group can be open at a time for clean navigation
- Fully backwards compatible with existing single function and list modes
- Pass a dictionary to
- VideoFile and AudioFile types for file uploads
VideoFile: Accepts common video formats (mp4, mov, avi, mkv, wmv, flv, webm, mpeg, mpg)AudioFile: Accepts common audio formats (mp3, wav, aac, flac, ogg, m4a)
- Updated ImageFile type to include additional formats (raw, psd)
- FileResponse now accepts either binary data or file path
FileResponse(data=bytes, filename="file.ext")- for in-memory filesFileResponse(path="/path/to/file", filename="file.ext")- for existing files on disk- Files specified by path are copied to returns_dir for consistent management
- Both approaches result in automatic 1-hour cleanup
- Changed default title of index page from "Function Tools" to "Menu"
- Non-blocking Execution: Standard Python functions (
def) are now automatically executed in a thread pool. This prevents CPU-heavy tasks from blocking the main event loop. - Async Disk I/O: Offloaded
FileResponseprocessing and disk writing to background threads.- Generating and saving large files (GB+) no longer freezes the server.
- The UI remains responsive for other users while files are being written to disk.
- Improved Concurrency: The server can now handle multiple simultaneous heavy requests (calculations or downloads) without queue blocking.
- FileResponse Filename Limit: 150-character maximum (Pydantic validated)
- Filename Sanitization: User-uploaded files sanitized against directory traversal, reserved names, and special characters
- Format:
{sanitized_name}_{32char_uuid}.{ext} - 100-char limit on user portion, ~143 total length
- Preserves original name for identification
- Format:
- File Lists: Fixed bug where
list[File]would fail with JSON parsing error- Backend now uses
form_data.getlist()to properly group uploaded files validate_list_param()accepts pre-processed lists in addition to JSON strings
- Backend now uses
Version 0.9.6 introduced SQLite for file tracking, blocked multiple workers, and added complex configuration. This was overengineered. func-to-web should be simple, fast, and reliable.
0.9.7 returns to simplicity with filesystem-based tracking, multiple workers support, and sensible defaults.
-
SQLite Database
- No more database files or locks
- File metadata encoded directly in filenames
- Format:
{uuid}___{timestamp}___{filename}
-
Parameters Removed
db_location- no longer neededcleanup_hours- now hardcoded to 1 hour
-
Workers Limitation
workers > 1no longer blocked- Scale vertically without restrictions
-
Automatic Upload Cleanup
- New parameter:
auto_delete_uploads(default:True) - Uploaded files deleted after function completes
- Disable with
auto_delete_uploads=Falseif needed
- New parameter:
-
Directory Configuration
- New parameter:
uploads_dir(default:"./uploads") - New parameter:
returns_dir(default:"./returned_files")
- New parameter:
-
File Retention
- Returned files deleted 1 hour after creation (hardcoded)
- No download tracking needed
- Cleanup runs every hour automatically
-
Multiple Workers
- Now supported like any other Uvicorn option
- Each worker runs independent cleanup
- File operations are atomic, no conflicts
-
Architecture
- Removed
db_manager.pymodule - Simplified
file_handler.py - Faster startup (no database initialization)
- Removed
- Database lock errors eliminated
- Race conditions eliminated
- Improved startup performance
- Updated all docs to remove SQLite references
- Removed
db_locationandcleanup_hoursfrom examples - Added
auto_delete_uploadsdocumentation - Updated API reference (removed
db_manager)
Before:
run(my_function, db_location="/data", cleanup_hours=48)After:
run(my_function, uploads_dir="/data/uploads", returns_dir="/data/returns")
# Files now expire after 1 hour (hardcoded)Breaking Changes:
db_locationremoved (usereturns_dir)cleanup_hoursremoved (hardcoded to 1 hour)func_to_web.dbno longer created
Non-Breaking:
workersparameter now supported- All other parameters unchanged
0.9.6 was overengineered. 0.9.7 is simple again: no database, automatic cleanup, multiple workers supported. Filesystem operations are fast, atomic, and sufficient.
-
Automatic Periodic Cleanup: Files are now automatically cleaned up every hour while the server runs.
- No need to restart the server for cleanup to occur
- Cleanup task runs in background every 3600 seconds (1 hour)
- Files older than
cleanup_hoursare removed from both disk and database - Configurable via
cleanup_hoursparameter (default: 24 hours) - Set
cleanup_hours=0to disable periodic cleanup
-
Thread-Safe File Cleanup: Implemented threading locks to prevent race conditions during concurrent file cleanup operations.
- Per-file locks ensure only one thread can clean up a specific file at a time
- Lock registry automatically cleaned up after operations complete
- Prevents "file not found" errors when multiple threads/requests attempt cleanup simultaneously
- Safe for high-concurrency environments with async I/O
-
Database Health Monitoring: Added automatic monitoring for file registry size.
- Displays warning if database contains >10,000 file references on startup
- Helps identify when manual cleanup or configuration changes are needed
- New
get_file_count()function indb_managermodule
-
Enhanced Security: Added UUID validation for file download endpoints.
- Validates file IDs match UUID v4 format before database queries
- Returns 400 Bad Request for malformed file IDs
- Additional layer of protection against injection attempts
-
Workers Limitation: Multiple workers (
workers > 1) are now explicitly blocked and will raise a clear error.- Prevents SQLite database corruption from concurrent writes across processes
- Displays educational error message with scaling alternatives
- Recommends running multiple instances with Nginx instead
- Single worker can handle 500-1,000 req/s with async I/O (sufficient for most teams)
-
Database Location Validation: Improved validation and path handling for
db_locationparameter.- Automatically creates directories if path is a directory
- Validates parent directory exists for file paths
- Raises clear error messages with actionable guidance
- Better support for custom database locations
-
Database Connection Timeouts: Added 5-second timeout to SQLite connections to prevent deadlocks.
-
FileNotFoundError Handling: Improved error handling when uploaded or returned files are manually deleted from disk.
- Auto-healing: broken database references are cleaned up automatically
- Returns "File expired" instead of internal server error
- Graceful degradation when files are missing
-
Lock Cleanup: Threading locks are now properly cleaned up in
finallyblocks.- Prevents lock registry from growing indefinitely
- Eliminates potential memory leaks in long-running processes
-
File Upload Cleanup Clarification: Updated
files.mdto clearly explain OS cleanup behavior.- Added comparison table for Linux/macOS/Windows automatic cleanup
- Warning for Windows users about potential file accumulation
- Three cleanup strategies with code examples (OS, manual, in-memory)
- Clear distinction between uploaded files (not auto-cleaned) and returned files (auto-cleaned)
-
Scaling Guidelines: Added comprehensive scaling section to
server-configuration.md.- Explains why multiple workers aren't supported (SQLite limitations)
- Documents single worker performance capabilities (500-1,000 req/s)
- Provides step-by-step guide for horizontal scaling with multiple instances
- Nginx sticky sessions configuration for load balancing
- Enterprise alternatives for >1,000 concurrent users
-
Updated API Documentation: All docstrings revised for consistency.
- No inline comments (per style guide)
- Comprehensive function/class documentation
- Clear parameter descriptions with examples
-
Modular Architecture: Complete code reorganization into specialized modules for better maintainability.
server.py: Main server configuration and entry pointroutes.py: Routing setup and request handlingfile_handler.py: File upload/download operations with thread-safe cleanupdb_manager.py: SQLite database operations for file trackingauth.py: Authentication middleware and session managementanalyze_function.py: Function signature analysis and metadata extractionvalidate_params.py: Form data validation and type conversionbuild_form_fields.py: HTML form field generation from type hintsprocess_result.py: Result processing for different output typescheck_return_is_table.py: Table format detection and conversiontypes.py: Type definitions and custom types (Color, Email, File types)__init__.py: Clean public API with minimal exports (run, type helpers)
-
Benefits:
- Separation of concerns: Each module has a single, clear responsibility
- Easier testing: Modules can be tested independently
- Better code navigation: Find functionality quickly by module name
- Reduced coupling: Clear interfaces between components
- Future-proof: Easy to extend without touching unrelated code
- New Generic
FileType: Added support for a genericFiletype hint.- Use
from func_to_web.types import Fileto accept uploaded files of any extension.
- Use
- Expanded File Extensions: Significantly broadened the list of supported formats for specific file types.
- Python Enum Support: Full support for Python
Enumtypes as dropdown menus.- Use standard Python enums as type hints:
def func(theme: Theme) - Supports
str,int, andfloatenum values - Automatic conversion from form values back to Enum members
- Your function receives the actual Enum member (e.g.,
Theme.LIGHT), not just the string value - Access both
.nameand.valueproperties in your function - Optional enums with
Theme | Nonesyntax - Compatible with all enum features (methods, properties, iteration)
- Add tests covering enum handling, conversion, and edge cases
- Use standard Python enums as type hints:
- Type Safety: Full IDE autocomplete and type checking
- Reusability: Define enum once, use across multiple functions
- Rich Semantics: Access both enum name and value, add custom methods
- Clean Code: No repetition of
Literal['option1', 'option2']in every function signature
- Async Function Support: Fixed an issue where passing an
async deffunction displayed a<coroutine object>instead of the result.- The library now automatically detects
asyncfunctions andawaitsthem properly. - Enables seamless integration with async libraries (e.g.,
httpx,tortoise-orm,motor).
- The library now automatically detects
- Built-in Authentication: Robust, stateless authentication system.
- Enable simply by passing a dictionary
auth={"username": "password"}to therun()function. - Architecture based on Signed Cookies (no database required).
- Includes protection against Timing Attacks (
secrets.compare_digest) and CSRF (SameSite='Lax').
- Enable simply by passing a dictionary
- Session Management: New
secret_keyargument inrun()to control session persistence across server restarts. - Login UI:
- Dedicated, modern login page that automatically inherits the application's theme (Light/Dark).
- Responsive design matching the core library aesthetics.
- Logout Functionality: New logout button in the header navigation (automatically appears when auth is enabled).
- Dependencies: Added
itsdangerousto required packages (essential for session signing). - Templates: Updated
basetemplates to handle conditional rendering based on authentication state (has_authflag).
- Reverse Proxy Support: New
root_pathargument inrun()to properly handle deployments behind Nginx, Traefik, or Docker containers with path prefixes. - Advanced Server Configuration: Any extra keyword arguments passed to
run()(**kwargs) are now forwarded directly to Uvicorn.- Enables SSL/HTTPS support (
ssl_keyfile,ssl_certfile). - Allows performance tuning (
workers,limit_max_requests,timeout_keep_alive).
- Enables SSL/HTTPS support (
- Custom API Metadata: New
fastapi_configdictionary argument to customize the underlying FastAPI application (e.g., changing the API title, version, or disabling swagger docs).
- Table Rendering: Automatic HTML table generation from multiple data formats
list[dict]- Headers extracted from dictionary keyslist[tuple]- Auto-generated headers (Column 1, Column 2, etc.)- Pandas DataFrame - Direct support with column names as headers
- NumPy 2D Arrays - Renders with auto-generated headers
- Polars DataFrame - Native support with column names
- Tables can be combined with other outputs in tuples/lists
- Zebra striping for better readability
-
Form Container: Added horizontal resize capability on desktop (≥1025px)
- Default width: 500px
- Resizable from 400px to 1400px by dragging the edge
- Disabled on tablets and mobile devices
- Maintains responsive behavior with proper padding
-
Result Display: Enhanced UI/UX for output presentation
- Replaced text-based "Copy" button with a subtle, floating SVG icon in the top-right
- Optimized vertical alignment to perfectly center text relative to the button
- Removed enclosing quotes from string results (both in display and clipboard)
- Improved button state logic to handle rapid clicks and timeouts robustly
- Multiple Outputs: Functions can now return tuples or lists to display multiple outputs simultaneously
- Combine text, images, plots, and file downloads in a single response
- Example:
return ("Analysis complete", processed_image, plot_figure, report_file) - Nested tuples/lists are not supported (validation with clear error message)
- Each output type rendered in its own container with proper spacing
- Output Processing: Enhanced
process_result()to handle tuple/list returns recursively - Response Format: Backend now supports
result_type: 'multiple'with nested outputs array - Frontend Rendering: New
createMultipleOutputs()function in builders.js for recursive rendering
- Back Button Navigation: Added back button on form pages to return to tools index
-
Dark/Light Theme Toggle: Complete redesign with SVG icons
- Replaced emoji icons with SVG moon/sun icons for better alignment and aesthetics
-
Field Label Formatting: Labels now automatically replace underscores with spaces
user_namedisplays as "User Name"api_urldisplays as "Api Url"
-
Function Description Styling: Improved appearance of function docstrings
-
Button Alignment: Fixed vertical misalignment between theme toggle and back button
- Resolved CSS inheritance issue where global
buttonselector was addingmargin-top: 0.5rem
- Resolved CSS inheritance issue where global
-
Number Input Controls (Dark Mode): Fixed visibility of increment/decrement arrows in dark mode
- Applied
color-scheme: darkfor native dark mode styling - Arrows now properly visible against dark backgrounds
- Applied
-
Simplified optional field interface:
- Removed
optionalbadge labels from field names for cleaner design - Removed "Enable field" text label next to toggle switches
- Toggle switches now self-explanatory without redundant text
- Cleaner, more minimal form appearance
- Removed
- CSS Architecture: Enhanced maintainability and reduced inheritance issues
- Removed redundant CSS properties
- Cleaner separation between component styles
- Example Code: Better examples in /examples folder with improved comments
- Update examples images: Regenerated example images
- PyPI README: Fixed missing README.md display on PyPI package page
- Added
long_descriptionandlong_description_content_typeto package metadata
- Added
- Long text output handling: Fixed layout overflow when functions return long strings (e.g., 100+ character passwords)
- Added word-wrapping and proper text overflow handling in result containers
- Applied
word-break: break-allandoverflow-wrap: break-wordto prevent layout breaking - Improved responsive behavior for long outputs on mobile devices
- Function Descriptions: Functions with docstrings now display their description below the title in the web UI
- Extracted using
inspect.getdoc()for clean formatting - Centered text with improved contrast in dark mode
- Styled with left border accent matching the theme
- Extracted using
- Code Refactoring: Reduced code duplication in
run.py- Created
create_response_with_files()helper function for file download responses - Created
handle_form_submission()async function to consolidate form processing logic - Eliminated duplicate code between single and multiple function modes
- Improved maintainability and consistency across endpoints
- Created
- Complete Documentation Rewrite: Restructured entire documentation using MkDocs Material for better navigation and user experience
- Organized into clear categories: Input Types, Types Constraints, Output Types, and Other Features
- Added dedicated pages for each feature with visual examples and code snippets
- Improved progressive learning flow with "Next Steps" navigation
- Enhanced README with direct links to all major documentation sections
- Better mobile responsiveness and dark mode support
- Auto-focus First Field: Cursor automatically focuses on the first input field when the page loads, improving keyboard navigation
- Keyboard Shortcuts:
Ctrl+Enter(orCmd+Enteron Mac) to submit the form from any input field- Works only when submit button is not disabled
- Copy to Clipboard: JSON results now include a "Copy" button to copy output to clipboard
- Toast Notifications: Elegant toast messages for user feedback (e.g., "✓ Copied to clipboard!")
- Auto-dismisses after 2 seconds
- Adapts to light/dark themes using CSS variables
- Fallback for older browsers without Clipboard API
- Frontend Refactoring: Complete restructuring of JavaScript codebase for improved maintainability and code organization
- Extracted pure utility functions to
utils.js - Separated DOM construction logic to
builders.js - Isolated validation logic to
validators.js - Added DOM manipulation helpers in
main.jsfor cleaner state management - Reduced cognitive load with single-responsibility functions
- Improved code reusability and testability
- Extracted pure utility functions to
- Optional List Fields: Hide add (+) and remove (-) buttons when optional list fields are disabled
- Error Messages on Disabled Fields: Clear error messages when fields are disabled
- Initial State Consistency: Fixed inconsistent behavior between page load and toggle interactions
- Minimum List Items: Lists with minimum item requirements now auto-create all required items
- Dark Mode: Toggle between light and dark themes with persistent preference
- Floating theme toggle button (🌙/☀️) in top-right corner
- Theme preference saved in localStorage
- Smooth transitions between themes
- Optimized color scheme for dark mode with proper contrast
- Works on both form and index pages
- Animated toggle button with hover effects
- Mobile-responsive button sizing
- File Download Support: Return files from functions with automatic download buttons
- Return single file:
FileResponse(data=bytes, filename="file.txt") - Return multiple files:
[FileResponse(...), FileResponse(...)] - Streaming downloads: Efficient handling of large files (GB+) without memory issues
- Works with any file type: PDF, Excel, ZIP, images, binary data, etc.
- No size limits: Uses temporary files and streaming like file uploads
- Clean UI: File list with individual download buttons
- Automatic cleanup: Temp files deleted after download
- Example:
- Return single file:
def create_report(name: str):
pdf_bytes = generate_pdf(name)
return FileResponse(data=pdf_bytes, filename="report.pdf")- List Support: Full support for list parameters with dynamic add/remove items
- Syntax:
list[int],list[str],list[float],list[Color],list[ImageFile], etc. - Works with all basic types:
int,float,str,bool,date,time - Works with special types:
Color,Email,ImageFile,DataFile, etc. - Item-level constraints:
list[Annotated[int, Field(ge=1, le=100)]] - List-level constraints:
Annotated[list[int], Field(min_length=2, max_length=10)] - Combined constraints:
Annotated[list[Annotated[int, Field(ge=0)]], Field(min_length=2)] - Dynamic UI: Add/remove buttons to manage list items
- Optional lists:
list[str] | Noneorlist[str] | OptionalDisabled - Default values:
list[str] = ["hello", "world"] - Default behavior: Lists without explicit values default to
None(not[])list[int]→default = Nonelist[int] = []→default = None(empty lists converted toNone)list[int] = [1, 2]→default = [1, 2](only non-empty lists preserved)
- Item-level validation: Each list item validates against type constraints
- List-level validation: Validates
min_lengthandmax_lengthconstraints - Visual feedback: Individual error messages per list item
- Empty/whitespace values automatically filtered out
- Syntax:
- Color Picker UI Bug: Fixed color picker not opening when clicking on color preview box
- Removed CSS properties that prevented programmatic clicks (
pointer-events: none, extreme positioning) - Simplified hidden color input positioning using
width: 0,height: 0, andz-index: -1 - Maintained visual appearance while ensuring browser can open native color picker
- Color picker now properly opens on preview click for both regular and optional fields
- Removed CSS properties that prevented programmatic clicks (
- Explicit Optional Control: New
OptionalEnabledandOptionalDisabledmarkers for precise control over optional field initial stateType | OptionalEnabled: Field always starts enabled, regardless of default valueType | OptionalDisabled: Field always starts disabled, even with default value- Explicit markers override automatic behavior (presence of default value)
- Works with all types: basic types, special types (Color, Email), constraints, and Literals
- Backwards compatible: standard
Type | Nonesyntax continues working with automatic behavior
- Test Suite for Optional Markers: 44 tests covering explicit optional control
- All basic types with both markers (int, str, float, bool, date, time)
- Special types (Color, Email) with markers
- Constraints combined with markers
- Default value override behavior
- Mixed usage (automatic + explicit in same function)
- Edge cases (markers with
= None) - 316 total tests across all modules (130 + 88 + 88)
- ParamInfo dataclass: Added
optional_enabledfield to store initial toggle state - analyze(): Enhanced Union type detection to identify OptionalEnabled/OptionalDisabled markers
- types.py: Added marker classes and type aliases for explicit optional control
- Test Suite for build_form_fields(): 88 tests covering HTML field generation, constraint extraction, and edge cases
- All field types: text, number, checkbox, select, date, time, color, email, file
- Format conversions: date → ISO, time → HH:MM
- Constraint handling: min/max/step for numbers, minlength/maxlength for strings
- Dynamic Literal re-execution and error cases (empty lists, mixed types)
- Edge cases: Unicode (😀🚀), negative/large values (1e100), leap years, boundary constraints
- Complex scenarios: 9+ parameter functions, mixed optional states, order preservation
- All tests pass in 0.58s
- Code Refactoring: Extracted
build_form_fields()to dedicated module- New module:
build_form_fields.pywith pattern constants - New module:
process_result.pyfor result handling - New module for custom patterns:
custom_pydantic_types.py - Three core modules:
analyze_function.py,validate_params.py,build_form_fields.py - 272 total tests across all modules (96 + 88 + 88)
- New module:
- Test Suite for validate_params(): 88 tests covering type conversion, validation, and edge cases
- Type conversions: strings → int/float/bool/date/time
- Constraint validation: numeric bounds, string length, pattern matching
- Optional toggle behavior, checkbox handling, hex color expansion (#abc → #aabbcc)
- Edge cases: negative numbers, scientific notation (1.5e10), Unicode (Héllo 世界 🌍), leap years
- All tests pass in 0.53s
- Code Refactoring: Extracted
validate_params()to dedicated module- New module:
validate_params.py - 184 total tests (96 + 88)
- New module:
- Test Suite for analyze(): 96 tests covering function signature analysis
- All types, constraints, special types (Color, Email, Files), Literals, optionals
- Error cases: unsupported types, invalid defaults, type mismatches
- Default Value Type Validation: Added type checking for defaults in
analyze()
- Code Refactoring: Extracted
analyze()andParamInfotoanalyze_function.py
- Optional Parameters: Full
Type | Nonesupport with visual toggle switches- Fields with defaults start enabled, without defaults start disabled
- Works with all types and constraints
- Dynamic Literals: Single string returns no longer split into characters
- Dynamic Literal Validation: Skip validation since options can change between render and submit
- Frontend Refactoring: Separated CSS/JS from templates
form.html→ clean template onlyform.js→ all JavaScript logicstyles.css→ all styling
- Upload Progress: Real-time progress bar, file size display, status messages
- Debug Mode: Fixed uvicorn crash with asyncio debugger
- Upload Performance: 8MB chunk streaming, ~237 MB/s on localhost
- Replaced
fetch()withXMLHttpRequestfor progress tracking
- Replaced
- Dynamic Dropdowns: Functions in
Literalgenerate options at runtime
- Initial release with basic types, files, validation, images/plots, multi-function support