This reference manual covers advanced features of Funix. If you are new to Funix, please read the QuickStart Guide first.
def hello_world(username: str) -> str:
return f"Hello, {username}!"
Or if you used a Funix decorator to customize a Python function, e.g.,
import funix
@funix.funix(
argument_labels={
"x": "The number to be squared"
}
)
def square(x: int) -> int:
return x * x
Then you just need to run the following command:
funix my_app.py
If you don't want some of your functions to be decorated, you can do this in two ways:
- Use private functions (functions with names starting with
_
), e.g.,
def __hello_world(username: str) -> str:
return f"Hello, {username}!"
- Use the
funix
decorator with thedisable
attribute set toTrue
, e.g.,
import funix
@funix.funix(disable=True)
def hello_world(username: str) -> str:
return f"Hello, {username}!"
Funix can convert all .py
files recursively from a local path to apps, e.g.,
funix ./demos # ./demos is a folder, funix will handle all .py files under it
Note
The git
program needs to be installed, and the GitPython
package needs to be installed for Python.
To make life more convenient, Funix can directly convert functions in a Git repo to web apps, e.g.,
funix -g http://github.com/funix/funix \ # the repo
-r examples \ # the folder under the repo
./ # recursively
funix -g http://github.com/funix/funix \ # the repo
-r examples \ # the folder under the repo
demo.py # a particular file
Starting Funix with the flag -d
will enable the debug mode which monitors source code changes and automatically re-generate the app. Known bugs: the watchdog we used monitors all .py
files in the current folder, which will keep your CPU high.
When you have multiple functions, but you want to specify which one to be the default one, so that it will be displayed when the app is loaded, you can use the flag -D
to specify the default function, for example:
# Only for 1 file:
funix -D my_default_function my_app.py
# For multiple files:
funix -D "hello.py:hello_world" -R examples
But you can also use the funix.funix
decorator to specify the default function, for example:
from funix import funix
@funix(
default=True
)
def my_default_function():
pass
A list of functions with no default function set will use the first function.
The Zen of Funix is to choose widgets for function I/Os based on their types. This can be done via themes (which is cross-app/function) or per-variable.
Only the data types supported by Funix can be properly rendered into widgets.
Funix supports certain Python's built-in data types and those from common scientific libraries, such as Figure
of matplotlib
.
Funix does NOT recommend customizing the widgets per-variable. This is a major difference between Funix and other Python-based app building frameworks. Funix believes this should be done via themes, to consistently create UIs across apps. Read the themes section for more details. If you really want to manually pick UI components for a variable, please see Customizing the input widgets per-variable.
Here we list the supported input data types, and the widget names (str
) that a theme or a per-variable customization directive can use to map the type/variable to a specific UI widget.
int
andfloat
- allowed widget names:
input
(default): a text input box where only digits and up to one dot are allowed. The UI component is MUI's TextField type=number.slider
: a slider. You can optionally set the argumentsstart
,end
, andstep
using a function call syntaxslider[start, end, step]
or["slider", {"min":min, "max":max, "step":step}]
-- in the latter case, not all three parameters need to be customized. For integers, the default values arestart=0
,end=100
, andstep=1
. For floats, the default values arestart=0
,end=1
, andstep=0.1
. The UI component is MUI's Slider.
- allowed widget names:
str
- allowed widget names:
textarea
(default): Multiline text input with line breaks. The UI component is MUI's TextField with multiline support.input
: a text input box. Only oneline. The UI component is MUI's TextField.password
: a text input box with password mask. The UI component is MUI's TextField.
- allowed widget names:
bool
- allowed widget names:
checkbox
(default): a checkbox. The UI component is MUI's Checkbox.switch
: a switch. The UI component is MUI's Switch.
- allowed widget names:
range
-
slider
(default). Thestart
,end
, andstep
values forslider
are the same as those specified when initializing arange
-type argument. The UI component is MUI's Slider. -
Examples:
def input_types( prompt: str, advanced_features: bool = False, model: typing.Literal['GPT-3.5', 'GPT-4.0', 'Llama-2', 'Falcon-7B']= 'GPT-4.0', max_token: range(100, 200, 20)=140, ) -> str: return "This is a dummy function. It returns nothing. "
-
typing.List[T]
(typing
is Python's built-in module)T
can only beint
,float
,str
orbool
- Elements of
typing.List[T]
will be collectively displayed in one widget together. - allowed widget names:
simplearray
(default): The collective UI is RJSF ArrayField, while the UIs for elements are the default one for typeT
.[simplearray, WIDGET_OF_BASIC_TYPE]
: This option allows customizing the UI for elements.WIDGET_OF_BASIC_TYPE
is a widget name for a basic data type above. The collective UI is RJSF ArrayField while the UIs for elements are perWIDGET_OF_BASIC_TYPE
.sheet
: All function arguments of this type will be displayed in an Excel-like sheet. The collective UI is MUIX DataGrid while the UIs for elements are the default one for typeT
.[sheet, WIDGET_OF_BASIC_TYPE]
: This option allows customizing the UI for elements. Usage is similar to that of[simplearray, WIDGET_OF_BASIC_TYPE]
above.json
: JSON string laid out with toggles, indentations, and syntax highlights. The UI is React-Json-View.
- Examples (To add and must add)
typing.TypedDict
(typing
is Python's built-in module)- allowed widget names:
json
(default and only): Any value or key in the dict can be of typeint
,float
,str
orbool
only. The UI is React-Json-View
- allowed widget names:
list
,dict
,typing.Dict
(typing
is Python's built-in module)- allowed widget names:
json
(default and only): The UI is React-Json-View.
- allowed widget names:
typing.Literal
(typing
is Python's built-in module)- allowed widget names:
radio
(default): a radio button group. The UI is MUI's Radio.select
: a dropdown menu. The UI is MUI's Select.
- allowed widget names:
- Examples
Via the module funix.hint
, Funix adds widgets to allow users to drag-and-drop MIME files as a web app's inputs. They will be converted into Python's bytes
type.
There are four types: BytesImage
, BytesVideo
, BytesAudio
, and BytesFile
. They are all subclasses of Python's native bytes
type. The difference is that BytesImage
, BytesVideo
, and BytesAudio
will be rendered into image, video, and audio uploaders (and for output, will become a media viewer), respectively, while BytesFile
will be rendered into a file uploader.
- Examples:
-
ChatPaper, a web app using ChatGPT API to query information in a user-uploaded PDF file.
-
RGB2Gray converter
import io # Python's native import PIL # the Python Image Library import funix @funix.funix( title="Convert color images to grayscale images", ) def gray_it(image: funix.hint.BytesImage) -> funix.hint.Image: img = PIL.Image.open(io.BytesIO(image)) gray = PIL.ImageOps.grayscale(img) output = io.BytesIO() gray.save(output, format="PNG") return output.getvalue()
-
ipywidgets.password
: inputbox with password mask. The UI is MUI's TextField.ipython
(Output)ipython.display.Image
: image. Same asBytesImage
above.ipython.display.Video
: video. Same asBytesVideo
above.ipython.display.Audio
: audio. Same asBytesAudio
above.
pandera
custom data frame schema: The UI is MUIX DataGrid.
Although Funix does not recommand customizing the widgets per-variable (the preferred way is via themes), it is still possible to do so via the Funix decorator attribute widgets
. The value provided to widgets
must be a Dict[str, str|List]
, where a key represents a variable name (a str
) while a value is a Funix-supported widget name mentioned above (a str
, e.g., "slider"
), or optionally for a parametric widget, a list of a widget name (str
) and a Dict[str, str|int|float|bool]
dictionary (parameters and values), e.g., ["slider", {"min":0, "max":100, "step":2}]
.
For example, the code below shows three syntaxes to associate four int
/float
-type variables to sliders:
import funix
@funix.funix(
widgets={
"x": "slider", # default min, max and range
"y": "slider[-10, 100, 0.25]", # one str for all parameters
"z1": ["slider", {"min": 1, "max": 2, "step": 0.1}],
"z2": ["slider", {"max": 2, "step": 0.1}], # default min, custom max and step
}
)
def square(x: int, y: float, z1: float, z2: float) -> float:
return x + y + z1 + z2
Funix supports the following output types.
int
,float
: Displayed as<code></code>
str
: Displayed as<span></span>
bool
: Displayed as MUI Checkboxtyping.List
andlist
: Displayed in either React-JSON-Viewer or MUIX DataGrid. There will be a radio box on the front end for the user to switch between the two display options at any time, and the JSON Viewer will be used by default.typing.TypedDict
,typing.Dict
,dict
: Displayed as ibid.
-
matplotlib.figure.Figure
: For interactively displaying matplotlib plots. Currently, only 2D figures of matplotlib are supported. Rendered as Mpld3 Plot If mpld3 is not good for your use case, addfigure_to_image=True
to thefunix
decorator to convert the figure to an image. The image will be displayed in the output area. -
jaxtyping
: The typing library for Numpy, PyTorch, and Tensorflow. Coming soon! -
ipython
ipython.display.Markdown
: Markdown syntax. The UI is React-Markdown for output.ipython.display.HTML
: HTML syntax.ipython.display.Javascript
: JavaScript syntax.ipython.display.Image
,ipython.display.Video
,ipython.display.Audio
: For displaying images, videos, and audios. The URL(s) is(are) either a local path (absolute or relative) or web URI links, e.g., at AWS S3.
-
Examples
from typing import List import matplotlib.pyplot as plt from matplotlib.figure import Figure import random import funix @funix.funix( widgets={ "a": "sheet", "b": ["sheet", "slider"] } ) def table_plot( a: List[int]=list(range(20)), b: List[float]=[random.random() for _ in range(20)] ) -> Figure: fig = plt.figure() plt.plot(a, b) return fig
funix.hint.Markdown
: For rendering strings that are in Markdown syntax. It's okay if the type is simplystr
-- but you will lose the syntax rendering. Rendered into HTML via React-Markdownfunix.hint.HTML
: For rendering strings that are in HTML syntax. It's okay if the type is simplystr
-- but you will lose the syntax rendering. Displayed into a<div></div>
tag.funix.hint.Image
,funix.hint.Video
,funix.hint.Audio
,funix.hint.File
: For rendering URLs (either astr
or atyping.List[str]
) into images, vidoes, audios, and file downloaders. The URL(s) is(are) either a local path (absolute or relative) or web URI links, e.g., at AWS S3.funix.hint.Code
: For rendering a string as a code block. Rendered using Monaco Editor for React.- Examples
One principle of Funix is to select the widget for a variable automatically based on its type without manual intervention, which is tedious, redundant, and inconsistent across multiple apps/pages. The mapping from a type to a widget is defined in a theme.
Besides controlling the type-to-widget mapping, a theme also controls other appearances of widgets, such as color, size, font, etc. In particular, Funix renders widgets using Material UI, also called MUI. Thus a Funix theme gives users the direct access to MUI components and their properties (props
).
A theme definition is a JSON dictionary of four parts: name
, widgets
, props
, typography
, and palette
, like the example below:
{
"name": "test_theme",
"widgets": { // dict, map types to widgets
"str": "inputbox",
"int": "slider[0,100,2]",
"Literal": "radio",
"float": {
"widget": "@mui/material/Slider",
"props": {
"min": 0,
"max": 100,
"step": 0.1
}
}
},
"props": {
"slider": {
"color": "#99ff00"
},
"radio": {
"size": "medium"
}
},
"typograhpy": {
"fontSize": 16, // font size, px
"fontWeight[Light|Regular|Medium|Bold]": 500, // Font weight in light, regular, medium or bold
"h1": {
"fontFamily": "Droid Sans",
"letterSpacing": "0.2rem" // Word spacing
}
},
"palette": {
"background": {
"default": "#112233", // Default background color
"paper": "#112233" // In <Paper /> color
},
"primary": {
"main": "#ddcc11",
"contrastText": "#d01234"
}
}
}
A theme definition dictionary contains five fields, name
, widgets
, props
typography
, and palette
that are all optional:
- The value of the
name
field must be a string, defining the name of the theme. - The value of the
widgets
field is the same as the one for thewidgets
attribute in a Funix decorator which is aDict[str|List[str], str|List]
, where a key represents a type (astr
, e.g.,"str"
) while a value is a string (e.g.,"inputbox"
), or optionally for a parametric widget, a list of a string (widget name) and a dictionary (parameters and values), e.g.,["slider", {"max":10, "step":2}]
. - As a dictionary, the
props
field maps Funix widgets (astr
) to their Material-UI props (adict
). It gives a user direct access toprops
of an MUI component. In the example above, we set thecolor
prop of MUI'sslider
and thesize
prop of MUI'sradio
. - The
typograph
field is a subset of thetypograph
field in a MUI theme object, expressed as a nested dictionary. - Similar to the
typograph
field, thepalette
field is a subset of thepalette
field in a MUI theme object, expressed as a nested dictionary.
You can import a theme from
- a local file path
- a web URL, or
- a JSON string defined on-the-fly.
You can then use the name (provided in the theme definition) or the alias to refer to the theme later.
The example below renames two themes imported by the alias
argument.
import funix
# Importing from web URL
funix.import_theme(
"http://example.com/my_themes.json",
alias = "my_favorite_theme"
) # optional
# Importing from local file
funix.import_theme(
"../my_themes.json",
alias = "my_favorite_theme"
) # optional
The example below defines a theme on-the-fly and imports it (without aliasing it) for later use.
import funix
theme_json = {
# theme definition
"name": "grandma's secret theme"
"widgets" : {
"range" : "inputbox"
}
}
funix.import_theme(theme_dict = theme_json)
A theme can be applied to all functions in one .py
Python script from
- a web URL
- a local file path
- a theme name imported earlier
using the set_default_theme()
function.
If you have multiple set_default_theme()
calls, then the last one will overwrite all above ones.
import funix
funix.set_default_theme("https://raw.githubusercontent.com/TexteaInc/funix-doc/main/examples/sunset_v2.json") # from web URL
funix.set_default_theme("../..//sunset_v2.json") # from local file
funix.set_default_theme("my_favorite_theme") # from alias or name
Alternatively, a theme can be applied to a particular function, again, from a web URL, a local file path, or a theme name/alias:
import funix
@funix.funix(theme = "https://raw.githubusercontent.com/TexteaInc/funix-doc/main/examples/sunset.json")
def foo():
pass
@funix.funix(theme = "../../themes/sunset.json")
def foo():
pass
@funix.funix(theme = "grandma's secret theme") # "sunset" is a theme alias
def foo():
pass
Quite often, a web app has default or example values prefilled at widgets for convenience. Funix provides handy solutions to support them.
Default values can be set using Python's built-in default value for keyword arguments. Then the default value will be pre-populated in the web interface automatically.
There is no Python's built-in way to set example values. Funix provides a decorator attribute examples
to support it. The value to be provided to examples
attribute is a Dict[str, typing.List[Any]]
where the key is an argument name and the value is a list of example values.
Example 1:
@funix.funix(
examples={"arg3": [1, 5, 7]}
)
def argument_selection(
arg1: str = "prime",
arg2: typing.Literal["is", "is not"]="is",
arg3: int = 3,
) -> str:
return f"The number {arg3} {arg2} {arg1}."
Example 2:
import os
import funix
import typing
import openai
openai.api_key = os.environ.get("OPENAI_KEY")
@funix.funix(
examples = {"prompt": ["Who is Einstein?", "Tell me a joke. "]}
)
def ChatGPT_single_turn(
prompt: str,
model : typing.Literal['gpt-3.5-turbo', 'gpt-3.5-turbo-0301'] = 'gpt-3.5-turbo'
) -> str:
completion = openai.ChatCompletion.create(
messages=[{"role": "user", "content": prompt}],
model=model
)
return f'ChatGPT says: {completion["choices"][0]["message"]["content"]}'
To help users understand your app or widgets, you can explain them using the decorator attributes title
, description
and argument_labels
respectively. The value provided to title
or description
is a Markdown-syntax string. The title
will appear in the top banner as well as the right navigation bar. The value provided to argument_labels
is of the type Dict[str, str]
where the key is an argument name and the value is a Markdown-syntax string.
Example 1:
import funix
@funix.funix(
title="BMI Calculator",
description = "**Calculate** _your_ BMI",
argument_labels = {
"weight": "Weight (kg)",
"height": "Height (m)"
}
)
def BMI(weight: float, height: float) -> str:
bmi = weight / (height**2)
return f"Your BMI is: {bmi:.2f}"
Funix will check to see if your function has a docstring
, and if it does then docstring
will be used as the description of your function (but if you define a description then docstring
will not be set automatically).
If you don't want Funix to read your docstring, here are two ways to do it:
- Disable funix docstring feature:
from funix import funix
from funix.config.switch import GlobalSwitchOption
GlobalSwitchOption.AUTO_READ_DOCSTRING_TO_FUNCTION_DESCRIPTION = False
@funix()
def main():
"""
# Demo
Yes but no.
"""
print("Hello, World!")
-
Use two comments:
@funix() def main(): """Docstring to display in the description""" """This is a comment that will not be displayed""" print("Hello, World!")
By default, Funix puts the input and output widgets in two panels that are put side-by-side, respectively. In either panel, widgets are laid out in the order they appear in the function's signature, one-widget per line and top-down.
The input and output panel are by default placed left to right. You can change their order and orientation using the direction
attribute in a Funix decorator. The example below shall be self-explaining:
import funix
@funix.funix()
def foo_default(x:int) -> str:
return f"{x} appears to the row, default"
@funix.funix(
direction="column"
)
def foo_bottom(x:int) -> str:
return f"{x} appears at the bottom"
@funix.funix(
direction="column-reverse"
)
def foo_top(x:int) -> str:
return f"{x} appears at the top"
@funix.funix(
direction="row-reverse"
)
def foo_left(x:int) -> str:
return f"{x} appears to the left"
A more advanced example is our ChatGPT multiturn app where direction = "column-reverse"
so the message you type stays at the bottom. The source code can be found in $FUNIX_ROOT/examples/AI/chatGPT_multi_turn.py
. Here is the screenshot:
The input and output layout can be customized via the attribute input_layout
and output_layout
that use a row-based layout system where each row is a list of cells. Each cell is a dictionary that specifies the widget type & name and the number of columns it occupies.
The type of input_layout
and output_layout
is:
typing.List[ # each row
typing.List[ # each cell in the same row
typing.Dict[str, str|int] # see below
]
]
The per-cell dictionary must have one entry, whose
- key specifies the widget type, which is a string "argument" (if the widget is an input/argument), "return_index" (if the widget is an output/return), "markdown", "html", or "divider".
- value is the content of the widget
- If the widget type is "argument", then the value is the argument name as a
str
. - If the widget type is "return_index", then the value is the index of the return value as an
int
. - If the widget type is "markdown" or "html", then the value is a Markdown- or HTML-syntax string.
- If the widget type is "divider", then the value is the text to be displayed on the divider. When the text is an empty string, then nothing is displayed.
- If the widget type is "argument", then the value is the argument name as a
Optionally, the per-cell dictionary can contain an entry of the string key width
and the value being a float
. The value specifies the number of columns, as defined in MUI's Grid, the cell occupies. The default value is 1.
Note that widgets not covered in input_layout
or output_layout
will be displayed in the default order and after those covered in input_layout
and output_layout
.
Example 1: Source code: examples/layout_simple.py
import funix
@funix.funix(
input_layout=[
[{"markdown": "### Sender information"}], # row 1
[
{"argument": "first_name", "width": 3},
{"argument": "last_name", "width": 3},
], # row 2
[{"argument": "address", "width": 6}], # row 3
[ # row 4
{"argument": "city", "width": 2.5},
{"argument": "state", "width": 2},
{"argument": "zip_code", "width": 2},
],
[{"html": "<a href='http://funix.io'>We love Funix</a>"}], # row 5
],
output_layout=[
[{"divider": "zip code is "}],
[{"return_index": 2}],
[{"divider": "from the town"}],
[{"return_index": [0, 1]}],
],
)
def layout_shipping(
first_name: str, last_name: str, address: str, city: str, state: str, zip_code: str
) -> (str, str, str):
return city, state, zip_code
Example 2: Using EasyPost API: Source code: $FUNIX_ROOT/examples/layout_easypost_shipping.py
Funix allows controlling the appearance of input widgets based on the values of other widgets via the attribute conditional_visible
which is of the type:
typing.List[ # a series of rules
typing.TypedDict( # each rule
"show": typing.List[str] # arguments visible only
"when": typing.List[ # when conjuction of conditions holds
typing.Dict[str, Any] # each condition
]
)
]
show
's value is a list of argument name strings.
when
's value is a list of dictionaries, representing a series of conditions whose conjunction must be True for arguments in corresponding show
list to appear.
If when
's value is
{"argument1": value1, "argument2": value2}
,
then it means the condition
argument1 == value1 && argument2 == value2
Example (Source code examples/conditional_simple.py
):
import typing
import openai
import funix
openai.api_key = os.environ.get("OPENAI_KEY")
@funix.funix(
widgets={"prompt":"textarea", "model": "radio"},
conditional_visible=[
{
"when": {"show_advanced": True,},
"show": ["max_tokens", "model", "openai_key"]
}
]
)
def ChatGPT_advanced(
prompt: str,
show_advanced: bool = False,
model : typing.Literal['gpt-3.5-turbo', 'gpt-3.5-turbo-0301']= 'gpt-3.5-turbo',
max_tokens: range(100, 200, 20)=140,
openai_key: str = ""
) -> str:
completion = openai.ChatCompletion.create(
messages=[{"role": "user", "content": prompt}],
model=model,
max_tokens=max_tokens,
)
return completion["choices"][0]["message"]["content"]
In all examples above, we provide values for each attribute (such as widgets
, examples
, or argument_labels
) in a Funix decorator like this:
import funix
@funix.funix(
attribute_name_1 = {
argument_1: ...
argument_2: ...
},
attribute_name_2 = {
argument_1: ...
argument_2: ...
},
)
When there are many arguments, this may be inconvenient because different attributes of the same argument are scattered.
Hence, Funix introduces a new attribute argument_config
to support grouping all attributes of the same argument together. The example above can be rewritten as below:
import funix
@funix.funix(
argument_config = {
argument_1: {
attribute_name_1: ...
attribute_name_2: ...
},
argument_2: {
attribute_name_1: ...
attribute_name_2: ...
}
}
)
Funix automatically logs the calls of an app in your browser for you to review the history later. This can be particularly useful when you want to compare the outputs of an app given different inputs, e.g., different conversations with ChatGPT. Funix offers two ways to view the history: the rightbar and the comprehensive log.
The history rightbar can be toggled like in GIF below. All calls are timestamps. Clicking on one history call will popular the input and output of the call to the input and output widgets. You can further (re)name, delete and export (to JSON) each call.
A comprehensive log can be toggled by clicking the clock icon at the top right corner of a Funix-converted app. The inputs and outputs of each call are presented in JSON trees. You can jump from the JSON tree to the UI with the inputs and outputs populated by clicking the "View" button under a call. Note that the comprehensive log presents calls for all Funix-converted apps in your browser, unlike the history rightbar which displays history per-app.
If there is a data type that requires Bytes
type to be processed, then this call is not saved to the history to reduce front-end stress.
Building a multipage app in Funix is easy: one function becomes one page and you can switch between pages using the function selector. Passing data between pages are done via global variables. Simply use the global
keyword of Python.
Examples:
-
A simple global variable-based state management
import funix y = "The default value of y." @funix.funix( ) def set_y(x: str="123") -> str: global y y = x return "Y has been changed. Now check it in the get_y() page." @funix.funix( ) def get_y() -> str: return y
We have seen how to use a global variable to pass values between pages. However, the value of a global variable is shared among all users. This can be dangerous. For example, an API token key is a global variable set in one page and used in the other. Then once a user sets the API token key in the former page, all other users can it freely in the latter page though they may not be able to see the token value.
To avoid this situation, we need to sessionize each browser's connection to a Funix app. To do so, add the -t
option when launching the funix
command, e.g.,
funix session_simple.py -t
The video/GIF below shows that in the private and non-private modes of a browser (thus two separate sessions), the global variable y
in the code above has different values. Changing the value of y
in one window won't change its value in another window.
A more practical example is in $FUNIX_ROOT/examples/AI/openAI_minimal.py
where openAI key is sessionized for users to talk to OpenAI endpoints using their individual API keys.
Known bugs: However, there are many cases that our simple AST-based solution does not cover. If sessions are not properly maintained, you can use two Funix functions to manually set and get a session-level global variable.
from funix import funix
from funix.session import get_global_variable, set_global_variable, set_default_global_variable
set_default_global_variable("user_word", "Whereof one cannot speak, thereof one must be silent")
@funix()
def set_word(word: str) -> str:
set_global_variable("user_word", word)
return "Success"
@funix()
def get_word() -> str:
return get_global_variable("user_word")
Funix can dynamically modify the description of a function based on a session variable by passing in the session variable name via the session_description
argument.
from funix import funix
from funix.session import get_global_variable, set_global_variable, set_default_global_variable
set_default_global_variable("user_word", "I lay supine, gazing upwards at the lofty sky. White clouds, bashfully greeting me, interposed themselves between the stratosphere and myself.")
@funix()
def set_word(word: str) -> str:
set_global_variable("user_word", word)
return "Success"
@funix(
session_description="user_word"
)
def session_description() -> str:
return get_global_variable("user_word")
A special case of passing data across pages is to pass (part of) the output of a function to an input of another.
Funix supports this via the prefill
attribute of a Funix decorator. For example,
import funix
def first_action(x: int) -> int:
return x - 1
def second_action(message: str) -> list[str]:
return message.split(" ")
def third_action(x: int, y: int) -> dict:
return {"x": x, "y": y}
@funix.funix(
pre_fill={
"a": first_action,
"b": (second_action, -1),
"c": (third_action, "x")
}
)
def final_action(a: int, b: str, c: int) -> str:
return f"{a} {b} {c}"
This multi-page app has 4 pages/functions/steps. The results from {first, second, third}_action
are used collectively in the final one final_action
.
The prefill
attribute takes in a dictionary of type
Dict[
str, # string of a function argument
Callable | # callable returns an int, float, str, or bool
Tuple(Callable, int) | # callable returns a sequence, int is the index
Tuple (Callable, int|str) # callable returns a dict, int|str is the key
]
The key is a str
, corresponding to an argument of the function being prefilled.
The value can be of three cases:
- a callable, if the callable has a non-compound return value -- in this case, the return of the callable is sent to the corresponding argument of the function being prefilled.
- a tuple of a callable and an index, if the callable returns a sequence -- in this case, the return of the callable that match the index is sent to the corresponding argument of the function being prefilled.
- a tuple of a callable and a
str
, if the callable returns a dictionary -- in this case, the return of the callable and of the key is sent to the corresponding argument of the function being prefilled.
Some APIs may be expensive (e.g. ChatGPT), and if they are not rate-limited, you may have a large deficit on your credit card.
Funix provides an easy way to apply the rate limit.
# For easy configuration, you can pass a dict
# 10 requests per browser per minute
@funix(rate_limit={"per_browser":10})
def per_browser():
pass
# Or limit based on IP:
@funix(rate_limit={"per_ip":100})
def per_ip():
pass
# based on both IP and browser:
@funix(
rate_limit={
"per_ip": 100,
"per_browser": 10
}
)
def per_browser_and_ip():
pass
# If you want to set a custom period, pass a list of dict
@funix(
rate_limit=[
# 1 request per day per browser
{"per_browser": 1, "period": 60 * 60 * 24},
{"per_ip": 20},
],
)
def custom_period():
pass
Funix can keep the most recent user input arguments and output in this session:
from funix import funix
@funix(keep_last=True)
def keep_last(x: int, y: int) -> int:
return x + y
@funix()
def new_function_for_a_break(message: str) -> str:
return message
Some functions may take a long time to run, and you may want to see the intermediate results as they are generated. Funix provides a stream mode to support this. You can use yield
to return intermediate results. The return value of the function is the final result.
Or you want to implement some functions that can be refreshed in time, such as chat or real-time text generation, here is an example of ChatGPT:
from funix import funix
from funix.hint import Markdown
from openai import OpenAI
@funix()
def chatGPT(prompt: str) -> Markdown:
client = OpenAI(api_key="xxx")
stream = client.chat.completions.create(
messages=[{"role": "user", "content": prompt}],
model="gpt-3.5-turbo",
stream=True,
)
message = []
for part in stream:
message.append(part.choices[0].delta.content or "")
yield "".join(message)
You don't need turn on the stream mode manually, Funix will detect the yield
statement and turn on the stream mode automatically.
Funix allows the user to automatically rerun the function when the arguments are changed. Setting the autorun
argument to True
. However, it should be noted that this function can be highly computationally intensive, and developers should make sure that it does not cause performance degradation of the server when using it.
import funix
import matplotlib.pyplot, matplotlib.figure
import numpy
@funix.funix(
autorun=True,
)
def sine(omega: funix.hint.FloatSlider(0, 4, 0.1)) -> matplotlib.figure.Figure:
fig = matplotlib.pyplot.figure()
x = numpy.linspace(0, 20, 200)
y = numpy.sin(x*omega)
matplotlib.pyplot.plot(x, y, linewidth=5)
return fig
It's okay to use print
for just printing something to the web. Funix will capture the stdout and display it in the output panel. But you need turn on the print_to_web
mode in decorator manually.
from funix import funix
@funix(print_to_web=True)
def print_to_web():
print("Hello world!")
It will force the function to run in the stream mode, and don't care about your return annotations. Funix will force the return type to Markdown
, and the return value will be the appended to the output panel.
We can also use class to define a Funix app. It's good for stateful apps and some people don't like global variables. Just use the funix.funix_class
decorator.
from funix import funix_class
class A:
def __init__(self, value: int = 0):
self.value = value
def add(self, x: int) -> int:
self.value += x
return self.value
@funix_class()
A(1)
If you want user can construct the class by themselves, you can do like this:
from funix import funix_class
@funix_class()
class A:
def __init__(self, value: int = 0):
self.value = value
def add(self, x: int) -> int:
self.value += x
return self.value
For classes that have been built, funix will treat them as classes shared by all users. For classes that need to be built manually, funix will record a separate class for each user.
You can use funix.funix_method
decorator like funix
to configure methods in a class. Funix will read the config from the funix_method
decorator and apply it to the method.
from funix import funix_class, funix_method
@funix_class()
class A:
@funix_method(title="Create a new A instance")
def __init__(self, value: int = 0):
self.value = value
def add(self, x: int) -> int:
self.value += x
return self.value
Although you can add __
prefix to the method name to "hide" it, it's not a good way to disable a method in funix. Funix provides a better way to do this. Just use disable
argument in funix_method
decorator.
from funix import funix_class, funix_method
@funix_class()
class A:
@funix_method(title="Create a new A instance")
def __init__(self, value: int = 0):
self.value = value
def add(self, x: int) -> int:
self.value += x
return self.value
@funix_method(disable=True)
def need_private_but_i_do_not_want_to_add_underscore(self):
pass
Funix will skip the method with disable=True
in the class.
Very often you wanna protect the access to your app. Funix offers a simple way to do that: generating a random token that needs to be attached to the URL in order to open the app in a browser.
To do so, just toggle the command line option secret
when launching a Funix app. You can provide a token or let Funix generate one for you.
funix my_app.py --secret my_secret_token # use a token provided by you
or
funix my_app.py --secret True # randomly generate a token
The token, denoted as TOKEN
in the rest of this seciton, will be printed on the Terminal. For example,
$ funix hello.py --secret True
Secrets:
---------------
Name: hello
Secret: 8c9f55d0eb74adbb3c87a445ea0ae92f
Link: http://127.0.0.1:3000/hello?secret=8c9f55d0eb74adbb3c87a445ea0ae92f
To access the app, you just append ?secret=TOKEN
in the app URL. In the example above, the URL to properly open the app is http://127.0.0.1:3000/hello?secret=8c9f55d0eb74adbb3c87a445ea0ae92f
. Bad guys trying to access your app via http://127.0.0.1:3000/hello
(no secret in the URL) will not be able to run your app.
However, if you are not a bad guy but just a forgetful person, you can still access your app without the token in the URL. Just click the "secret" button on the top right corner of the app, and enter the secret in a pop-up window, then you can use the app.
Note: This is not a strong way to protect your app.
Funix can dynamically calculate other arguments based on what the user has not yet submitted, but is entering. This is extremely helpful for tax rate calculations, etc.
from funix import funix
@funix(disable=True)
def compute_tax(salary: float, income_tax_rate: float) -> int:
return salary * income_tax_rate
@funix(
reactive={"tax": compute_tax}
)
def after_tax_income_calculator(
salary: float,
income_tax_rate: float,
tax: float) -> str:
return f"Your take home money is {salary - tax} dollars,\
for a salary of {salary} dollars, \
after a {income_tax_rate*100}% income tax."
-
reactive accepts two types of parameters
dict[ArgumentName, ReactiveFunction]
: If the names of the arguments toReactiveFunction
is the same as the names of the arguments in the decorated function.dict[ArgumentName, Tuple[ReactiveFunction, Dict[ReactiveFunctionArgumentName, ArgumentName]]]
: If the names of the arguments toReactiveFunction
is different from the names of the arguments in the decorated function. For example:
from funix import funix @funix(disable=True) def compute_tax(salary_: float, income_tax_rate_: float) -> int: return salary_ * income_tax_rate_ @funix( reactive={"tax": (compute_tax, {"salary_": "salary", "income_tax_rate_": "income_tax_rate"})} ) def after_tax_income_calculator( salary: float, income_tax_rate: float, tax: float) -> str: return f"Your take home money is {salary - tax} dollars,\ for a salary of {salary} dollars, \ after a {income_tax_rate*100}% income tax."
TBD.