├── .nvmrc ├── backend └── funix │ ├── py.typed │ ├── prep │ └── __init__.py │ ├── util │ ├── __init__.py │ ├── secret.py │ ├── text.py │ ├── uri.py │ ├── file.py │ ├── module.py │ └── network.py │ ├── build │ ├── favicon.ico │ ├── logo192.png │ ├── logo512.png │ ├── static │ │ ├── css │ │ │ └── main.4efb37a3.css │ │ └── js │ │ │ ├── 264.dfcca1b2.chunk.js.LICENSE.txt │ │ │ ├── main.d08a20a0.js.LICENSE.txt │ │ │ └── main.ff2fb548.js.LICENSE.txt │ ├── asset-manifest.json │ ├── manifest.json │ └── index.html │ ├── requirements.txt │ ├── decorator │ ├── encoder.py │ ├── response.py │ ├── all_of.py │ ├── pre_fill.py │ ├── theme.py │ ├── secret.py │ ├── layout.py │ └── reactive.py │ ├── jupyter │ └── __init__.py │ ├── app │ └── websocket.py │ ├── config │ ├── switch.py │ └── __init__.py │ ├── session │ └── __init__.py │ ├── __main__.py │ └── frontend │ └── __init__.py ├── examples ├── files │ ├── test.txt │ └── test.png ├── .funixignore ├── requirements.txt ├── hello_world.py ├── themes │ ├── comic_sans.json │ ├── pink.json │ ├── dark_test.json │ ├── sunset.json │ ├── kanagawa.json │ ├── on_the_fly_theme.py │ └── theme_showroom.py ├── marvin-test.py ├── widgets │ ├── output_widgets.py │ └── input_widgets.py ├── docstring_and_print.py ├── sine.py ├── multimedia │ ├── hashit.py │ ├── rgb2gray.py │ └── csv_reader.py ├── advanced_usage │ ├── sessions_manual.py │ ├── panel_direction.py │ ├── per_argument_config.py │ ├── conditional_visibility.py │ ├── pre_fill.py │ ├── vectorization.py │ └── sessions_simple.py ├── bmi.py ├── chemistry │ ├── inchi_and_smiles_to_structure.py │ └── Ketcher.py ├── new_type.py ├── tax_calculator_reactive.py ├── stream.py ├── default_and_candidate_values.py ├── archive │ └── memory.py ├── games │ ├── hangman.py │ └── wordle.py ├── class.py ├── AI │ ├── DallE.py │ ├── chatGPT.py │ ├── chatGPT_advanced.py │ ├── chatGPT_muilti_turn.py │ ├── huggingface.py │ └── use_your_own_openAI_token.py ├── table_and_plot.py ├── pydantic_test.py ├── layout │ └── layout_simple.py ├── jchem.py └── user_manage.py ├── MANIFEST.in ├── .gitattributes ├── .gitmodules ├── frontend ├── public │ ├── favicon.ico │ ├── logo192.png │ ├── logo512.png │ ├── _images │ │ ├── back.png │ │ ├── hand.png │ │ ├── help.png │ │ ├── home.png │ │ ├── move.png │ │ ├── filesave.png │ │ ├── forward.png │ │ ├── subplots.png │ │ ├── back_large.png │ │ ├── help_large.png │ │ ├── home_large.png │ │ ├── matplotlib.png │ │ ├── move_large.png │ │ ├── forward_large.png │ │ ├── zoom_to_rect.png │ │ ├── filesave_large.png │ │ ├── subplots_large.png │ │ ├── matplotlib_large.png │ │ ├── qt4_editor_options.png │ │ ├── zoom_to_rect_large.png │ │ └── qt4_editor_options_large.png │ ├── manifest.json │ └── index.html ├── src │ ├── components │ │ ├── FunixFunction │ │ │ ├── components │ │ │ │ ├── index.ts │ │ │ │ └── KetcherEditor.tsx │ │ │ ├── hooks │ │ │ │ ├── index.ts │ │ │ │ └── useKetcherEditor.ts │ │ │ ├── OutputComponents │ │ │ │ ├── OutputPlotImage.tsx │ │ │ │ ├── OutputError.tsx │ │ │ │ ├── OutputCode.tsx │ │ │ │ ├── OutputNextTo.tsx │ │ │ │ ├── OutputChem.tsx │ │ │ │ ├── OutputRedirectButton.tsx │ │ │ │ ├── OutputPlot.tsx │ │ │ │ ├── OutputMedias.tsx │ │ │ │ └── OutputFiles.tsx │ │ │ ├── DateTimePickerWidget.tsx │ │ │ ├── SwitchWidget.tsx │ │ │ ├── JSONEditorWidget.tsx │ │ │ ├── FunixCustom.tsx │ │ │ ├── ChemEditor.tsx │ │ │ └── MultipleInput.tsx │ │ ├── SheetComponents │ │ │ ├── SheetInterface.tsx │ │ │ ├── SheetCheckBox.tsx │ │ │ └── SheetSlider.tsx │ │ ├── Common │ │ │ ├── SliderValueLabel.tsx │ │ │ ├── InlineBox.tsx │ │ │ ├── ThemeReactJson.tsx │ │ │ ├── WidgetSyntaxParser.ts │ │ │ ├── PDFViewer.tsx │ │ │ ├── TemplateString.tsx │ │ │ └── ValueOperation.ts │ │ ├── History │ │ │ ├── HistoryLoader.tsx │ │ │ └── HistoryUtils.tsx │ │ ├── FunixFunctionSelected.tsx │ │ └── PrivacyDialog.tsx │ ├── shared │ │ ├── indigo-render.ts │ │ ├── type.d.ts │ │ ├── theme.ts │ │ └── media.ts │ ├── index.css │ ├── index.tsx │ ├── Key.tsx │ └── store.ts ├── .gitignore ├── tsconfig.json ├── eslint.config.mjs ├── config-overrides.js └── package.json ├── setup.cfg ├── .gitignore ├── .github ├── FUNDING.yml └── workflows │ ├── run-backend-test.yml │ └── build-and-push.yml ├── LICENSE ├── pyproject.toml └── Funix_vs_them.md /.nvmrc: -------------------------------------------------------------------------------- 1 | lts/* 2 | -------------------------------------------------------------------------------- /backend/funix/py.typed: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/funix/prep/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/funix/util/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/files/test.txt: -------------------------------------------------------------------------------- 1 | Wow! 2 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | graft backend/funix/build 2 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.funixignore linguist-language=Ignore-List 2 | -------------------------------------------------------------------------------- /examples/.funixignore: -------------------------------------------------------------------------------- 1 | # Same syntax as .gitignore 2 | archive/ -------------------------------------------------------------------------------- /examples/requirements.txt: -------------------------------------------------------------------------------- 1 | funix 2 | openai>=1.1.1 3 | requests -------------------------------------------------------------------------------- /examples/files/test.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TexteaInc/funix/HEAD/examples/files/test.png -------------------------------------------------------------------------------- /examples/hello_world.py: -------------------------------------------------------------------------------- 1 | def hello_world(name: str="Funix") -> str: 2 | return f"Hello {name}!" -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "docs"] 2 | path = docs 3 | url = https://github.com/TexteaInc/funix-doc/ 4 | -------------------------------------------------------------------------------- /frontend/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TexteaInc/funix/HEAD/frontend/public/favicon.ico -------------------------------------------------------------------------------- /frontend/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TexteaInc/funix/HEAD/frontend/public/logo192.png -------------------------------------------------------------------------------- /frontend/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TexteaInc/funix/HEAD/frontend/public/logo512.png -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = funix 3 | 4 | [options] 5 | packages_dir = backend 6 | packages = find: 7 | -------------------------------------------------------------------------------- /backend/funix/build/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TexteaInc/funix/HEAD/backend/funix/build/favicon.ico -------------------------------------------------------------------------------- /backend/funix/build/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TexteaInc/funix/HEAD/backend/funix/build/logo192.png -------------------------------------------------------------------------------- /backend/funix/build/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TexteaInc/funix/HEAD/backend/funix/build/logo512.png -------------------------------------------------------------------------------- /frontend/public/_images/back.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TexteaInc/funix/HEAD/frontend/public/_images/back.png -------------------------------------------------------------------------------- /frontend/public/_images/hand.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TexteaInc/funix/HEAD/frontend/public/_images/hand.png -------------------------------------------------------------------------------- /frontend/public/_images/help.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TexteaInc/funix/HEAD/frontend/public/_images/help.png -------------------------------------------------------------------------------- /frontend/public/_images/home.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TexteaInc/funix/HEAD/frontend/public/_images/home.png -------------------------------------------------------------------------------- /frontend/public/_images/move.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TexteaInc/funix/HEAD/frontend/public/_images/move.png -------------------------------------------------------------------------------- /frontend/public/_images/filesave.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TexteaInc/funix/HEAD/frontend/public/_images/filesave.png -------------------------------------------------------------------------------- /frontend/public/_images/forward.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TexteaInc/funix/HEAD/frontend/public/_images/forward.png -------------------------------------------------------------------------------- /frontend/public/_images/subplots.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TexteaInc/funix/HEAD/frontend/public/_images/subplots.png -------------------------------------------------------------------------------- /frontend/public/_images/back_large.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TexteaInc/funix/HEAD/frontend/public/_images/back_large.png -------------------------------------------------------------------------------- /frontend/public/_images/help_large.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TexteaInc/funix/HEAD/frontend/public/_images/help_large.png -------------------------------------------------------------------------------- /frontend/public/_images/home_large.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TexteaInc/funix/HEAD/frontend/public/_images/home_large.png -------------------------------------------------------------------------------- /frontend/public/_images/matplotlib.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TexteaInc/funix/HEAD/frontend/public/_images/matplotlib.png -------------------------------------------------------------------------------- /frontend/public/_images/move_large.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TexteaInc/funix/HEAD/frontend/public/_images/move_large.png -------------------------------------------------------------------------------- /frontend/public/_images/forward_large.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TexteaInc/funix/HEAD/frontend/public/_images/forward_large.png -------------------------------------------------------------------------------- /frontend/public/_images/zoom_to_rect.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TexteaInc/funix/HEAD/frontend/public/_images/zoom_to_rect.png -------------------------------------------------------------------------------- /examples/themes/comic_sans.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "comic_sans", 3 | "typography": { 4 | "fontFamily": "Comic Sans MS" 5 | } 6 | } -------------------------------------------------------------------------------- /frontend/public/_images/filesave_large.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TexteaInc/funix/HEAD/frontend/public/_images/filesave_large.png -------------------------------------------------------------------------------- /frontend/public/_images/subplots_large.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TexteaInc/funix/HEAD/frontend/public/_images/subplots_large.png -------------------------------------------------------------------------------- /frontend/public/_images/matplotlib_large.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TexteaInc/funix/HEAD/frontend/public/_images/matplotlib_large.png -------------------------------------------------------------------------------- /frontend/public/_images/qt4_editor_options.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TexteaInc/funix/HEAD/frontend/public/_images/qt4_editor_options.png -------------------------------------------------------------------------------- /frontend/public/_images/zoom_to_rect_large.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TexteaInc/funix/HEAD/frontend/public/_images/zoom_to_rect_large.png -------------------------------------------------------------------------------- /frontend/public/_images/qt4_editor_options_large.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TexteaInc/funix/HEAD/frontend/public/_images/qt4_editor_options_large.png -------------------------------------------------------------------------------- /examples/marvin-test.py: -------------------------------------------------------------------------------- 1 | import os 2 | from funix.hint import ChemStr, KetcherPopup 3 | 4 | def ketcher_demo(ketcher: KetcherPopup) -> ChemStr: 5 | return ketcher.inchi 6 | -------------------------------------------------------------------------------- /frontend/src/components/FunixFunction/components/index.ts: -------------------------------------------------------------------------------- 1 | export { default as KetcherEditor } from "./KetcherEditor"; 2 | export type { KetcherEditorProps } from "./KetcherEditor"; 3 | -------------------------------------------------------------------------------- /frontend/src/components/FunixFunction/hooks/index.ts: -------------------------------------------------------------------------------- 1 | export { useKetcherEditor } from "./useKetcherEditor"; 2 | export type { 3 | ChemEditorValue, 4 | UseKetcherEditorOptions, 5 | } from "./useKetcherEditor"; 6 | -------------------------------------------------------------------------------- /examples/themes/pink.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pink", 3 | "palette": { 4 | "primary": { 5 | "main": "#F596AA", 6 | "contrastText": "#FCFAF2" 7 | }, 8 | "background": { 9 | "default": "#F8C3CD" 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .vscode/ 3 | *.ignore.* 4 | .ignore 5 | *.ignore 6 | __pycache__ 7 | frontend/build 8 | *.egg-info 9 | venv/ 10 | dist/ 11 | .nova 12 | .DS_Store 13 | .lh 14 | .env 15 | backend/funix/build 16 | *.db 17 | *.db-* 18 | logs.jsonl 19 | *.key 20 | -------------------------------------------------------------------------------- /backend/funix/requirements.txt: -------------------------------------------------------------------------------- 1 | flask>=2.2.2 2 | functions-framework==3.* 3 | requests>=2.28.1 4 | plac>=1.3.5 5 | gitignore-parser>=0.1.9 6 | flask-sock>=0.7.0 7 | SQLAlchemy>=2.0.23 8 | docstring_parser>=0.16 9 | matplotlib>=3.4.3 10 | pandas>=2.0.3 11 | tornado>=6.4.2 12 | pydantic>=2.10.6 13 | GitPython>=3.1.31 14 | -------------------------------------------------------------------------------- /examples/themes/dark_test.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dark_test", 3 | "typography": { 4 | "fontFamily": "Courier New, monospace", 5 | "fontSize": 14, 6 | "fontWeightLight": 300, 7 | "fontWeightRegular": 400, 8 | "fontWeightMedium": 500 9 | }, 10 | "palette": { 11 | "mode": "dark" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /backend/funix/build/static/css/main.4efb37a3.css: -------------------------------------------------------------------------------- 1 | body{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,Fira Sans,Droid Sans,Helvetica Neue,sans-serif;margin:0}code{font-family:source-code-pro,Menlo,Monaco,Consolas,Courier New,monospace} -------------------------------------------------------------------------------- /frontend/src/components/SheetComponents/SheetInterface.tsx: -------------------------------------------------------------------------------- 1 | import { GridRenderCellParams, GridRowId } from "@mui/x-data-grid"; 2 | 3 | export interface SheetInterface { 4 | widget: string; 5 | type: string; 6 | params: GridRenderCellParams; 7 | customChange: (rowId: GridRowId, field: string, value: any) => void; 8 | } 9 | -------------------------------------------------------------------------------- /examples/widgets/output_widgets.py: -------------------------------------------------------------------------------- 1 | import typing 2 | import IPython 3 | 4 | # HTML and Markdown outputs 5 | def html_and_markdown() -> \ 6 | typing.Tuple[IPython.display.HTML, IPython.display.Markdown]: 7 | return IPython.display.HTML("

Funix rocks!

"), \ 8 | IPython.display.Markdown("## Funix rocks!") -------------------------------------------------------------------------------- /examples/docstring_and_print.py: -------------------------------------------------------------------------------- 1 | from funix import funix 2 | 3 | @funix( 4 | print_to_web=True 5 | ) 6 | def foo() -> None: 7 | """ 8 | ## What a great app in Funix! 9 | 10 | Funix won't let your docstring go to waste. 11 | """ 12 | print("It supports **Markdown**.") 13 | print("And HTML.") 14 | return None 15 | -------------------------------------------------------------------------------- /backend/funix/build/asset-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": { 3 | "main.css": "/static/css/main.4efb37a3.css", 4 | "main.js": "/static/js/main.d08a20a0.js", 5 | "static/js/264.dfcca1b2.chunk.js": "/static/js/264.dfcca1b2.chunk.js", 6 | "index.html": "/index.html" 7 | }, 8 | "entrypoints": [ 9 | "static/css/main.4efb37a3.css", 10 | "static/js/main.d08a20a0.js" 11 | ] 12 | } -------------------------------------------------------------------------------- /frontend/src/shared/indigo-render.ts: -------------------------------------------------------------------------------- 1 | const renderSvg = (data: string) => { 2 | const options = new window.indigo.MapStringString(); 3 | options.set("render-output-format", "svg"); 4 | const rawRender = window.indigo.render(data, options); 5 | const img = new Image(); 6 | img.src = `data:image/svg+xml;base64,${rawRender}`; 7 | return img.src; 8 | }; 9 | 10 | export default renderSvg; 11 | -------------------------------------------------------------------------------- /examples/sine.py: -------------------------------------------------------------------------------- 1 | import funix 2 | import matplotlib.pyplot, matplotlib.figure 3 | import numpy 4 | 5 | @funix.funix( 6 | autorun=True, 7 | ) 8 | def sine(omega: funix.hint.FloatSlider(0, 4, 0.1)) -> matplotlib.figure.Figure: 9 | fig = matplotlib.pyplot.figure() 10 | x = numpy.linspace(0, 20, 200) 11 | y = numpy.sin(x*omega) 12 | matplotlib.pyplot.plot(x, y, linewidth=5) 13 | return fig -------------------------------------------------------------------------------- /examples/multimedia/hashit.py: -------------------------------------------------------------------------------- 1 | import hashlib 2 | from typing import List 3 | from funix import funix 4 | from funix.hint import BytesFile 5 | 6 | 7 | @funix( 8 | title="Get File's SHA256", 9 | ) 10 | def hashit(datas: List[BytesFile]) -> list: 11 | results = [] 12 | for data in datas: 13 | sha256 = hashlib.sha256() 14 | sha256.update(data) 15 | results.append(sha256.hexdigest()) 16 | return results 17 | -------------------------------------------------------------------------------- /frontend/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /frontend/src/components/Common/SliderValueLabel.tsx: -------------------------------------------------------------------------------- 1 | import { SliderValueLabelProps, Tooltip } from "@mui/material"; 2 | import React from "react"; 3 | 4 | export default function SliderValueLabel(props: SliderValueLabelProps) { 5 | return ( 6 | 12 | {props.children} 13 | 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | 25 | .eslintcache 26 | 27 | .env 28 | -------------------------------------------------------------------------------- /examples/advanced_usage/sessions_manual.py: -------------------------------------------------------------------------------- 1 | from funix import funix 2 | from funix.session import get_global_variable, set_global_variable, set_default_global_variable 3 | 4 | 5 | set_default_global_variable("user_word", "Funix.io rocks! ") 6 | 7 | 8 | @funix() 9 | def set_word(word: str) -> str: 10 | set_global_variable("user_word", word) 11 | return "Success" 12 | 13 | 14 | @funix() 15 | def get_word() -> str: 16 | return get_global_variable("user_word") 17 | -------------------------------------------------------------------------------- /examples/bmi.py: -------------------------------------------------------------------------------- 1 | # This example shows how to change display labels for arguments 2 | 3 | import IPython.display 4 | import funix 5 | 6 | 7 | @funix.funix( 8 | title="BMI Calculator", 9 | description="**Calculate** _your_ BMI", 10 | argument_labels={"weight": "Weight (kg)", "height": "Height (m)"}, 11 | show_source=True, 12 | ) 13 | def BMI(weight: float, height: float) -> IPython.display.Markdown: 14 | bmi = weight / (height**2) 15 | return f"## Your BMI is: \n ### {bmi:.2f}" 16 | -------------------------------------------------------------------------------- /examples/multimedia/rgb2gray.py: -------------------------------------------------------------------------------- 1 | import io # Python's native 2 | 3 | import PIL # the Python Image Library 4 | import IPython 5 | import funix 6 | 7 | @funix.funix( 8 | title="Convert color images to grayscale images", 9 | ) 10 | def gray_it(image: funix.hint.BytesImage) -> IPython.display.Image: 11 | img = PIL.Image.open(io.BytesIO(image)) 12 | gray = PIL.ImageOps.grayscale(img) 13 | output = io.BytesIO() 14 | gray.save(output, format="PNG") 15 | return output.getvalue() 16 | -------------------------------------------------------------------------------- /frontend/src/components/Common/InlineBox.tsx: -------------------------------------------------------------------------------- 1 | import { Box } from "@mui/material"; 2 | import React from "react"; 3 | 4 | const InlineBox = (props: { children: React.ReactNode }) => { 5 | return ( 6 | 16 | {props.children} 17 | 18 | ); 19 | }; 20 | 21 | export default InlineBox; 22 | -------------------------------------------------------------------------------- /examples/themes/sunset.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sunset", 3 | "widgets": { 4 | "int": [ 5 | "slider", 6 | { 7 | "min": 0, 8 | "max": 120, 9 | "step": 1 10 | } 11 | ], 12 | "float": [ 13 | "slider", 14 | { 15 | "min": 0.0, 16 | "max": 120.0, 17 | "step": 0.1 18 | } 19 | ], 20 | "Literal": "radio" 21 | }, 22 | "palette": { 23 | "background": { 24 | "default": "#ff9e1b" 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /examples/chemistry/inchi_and_smiles_to_structure.py: -------------------------------------------------------------------------------- 1 | # Plot the 2D structure of a chemical compound given its InChI or SMILES 2 | 3 | 4 | from funix.hint import ChemStr 5 | from funix import funix 6 | 7 | @funix( 8 | examples={ 9 | "inchi_or_smiles": [ 10 | "InChI=1S/C9H8O4/c1-6(10)13-8-5-3-2-4-7(8)9(11)12/h2-5H,1H3,(H,11,12)", 11 | "O=C(C)Oc1ccccc1C(=O)O" 12 | ] 13 | } 14 | ) 15 | def inchi_or_smiles_to_structure(inchi_or_smiles: str) -> ChemStr: 16 | return inchi_or_smiles 17 | -------------------------------------------------------------------------------- /examples/new_type.py: -------------------------------------------------------------------------------- 1 | from funix import funix, new_funix_type 2 | 3 | @new_funix_type( 4 | # widget= 5 | { 6 | "widget": "@mui/material/TextField", 7 | "props": { 8 | "type": "password", 9 | }, 10 | } 11 | ) 12 | class blackout(str): 13 | def print(self): 14 | return self + " is the message." 15 | 16 | def hoho(x: blackout = "Funix Rocks!") -> str: 17 | return x.print() 18 | 19 | if __name__ == "__main__": 20 | print (hoho(blackout('Fun'))) -------------------------------------------------------------------------------- /examples/tax_calculator_reactive.py: -------------------------------------------------------------------------------- 1 | from funix import funix 2 | 3 | @funix(disable=True) 4 | def compute_tax(salary: float, income_tax_rate: float) -> int: 5 | return salary * income_tax_rate 6 | 7 | 8 | @funix( 9 | reactive={"tax": compute_tax} 10 | ) 11 | def after_tax_income_calculator( 12 | salary: float, 13 | income_tax_rate: float, 14 | tax: float) -> str: 15 | return f"Your take home money is {salary - tax} dollars,\ 16 | for a salary of {salary} dollars, \ 17 | after a {income_tax_rate*100}% income tax." 18 | -------------------------------------------------------------------------------- /examples/themes/kanagawa.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "kanagawa", 3 | "palette": { 4 | "background": { 5 | "default": "#f9f0d8", 6 | "paper": "#f9f0d8" 7 | }, 8 | "text": { 9 | "primary": "#505762", 10 | "secondary": "#5a606a", 11 | "hint": "#5b616a" 12 | }, 13 | "primary": "#24415f", 14 | "secondary": "#a63624", 15 | "error": "#973121", 16 | "warning": "#c27b5c", 17 | "info": "#437ea1", 18 | "success": "#80ab6f" 19 | } 20 | } -------------------------------------------------------------------------------- /frontend/src/components/FunixFunction/OutputComponents/OutputPlotImage.tsx: -------------------------------------------------------------------------------- 1 | import { Card, CardMedia } from "@mui/material"; 2 | 3 | export default function OutputPlotMedias(props: { media: string }) { 4 | return ( 5 | 10 | 19 | 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /examples/multimedia/csv_reader.py: -------------------------------------------------------------------------------- 1 | import pandas as pd 2 | from funix import funix 3 | from io import StringIO 4 | from funix.hint import BytesFile 5 | 6 | def do_calc(a: int, b: int, op: str) -> int: 7 | return a + b if op == "add" else a - b 8 | 9 | 10 | @funix( 11 | description="""Drag and drop a two-column numeric-only CSV file **WITHOUT** the header to try out. """ 12 | ) 13 | def csv_in(csv_bytes: BytesFile) -> list: 14 | df = pd.read_csv(StringIO(csv_bytes.decode())) 15 | arguments = df.values.tolist() 16 | return [do_calc(*arg) for arg in arguments] 17 | -------------------------------------------------------------------------------- /frontend/src/components/Common/ThemeReactJson.tsx: -------------------------------------------------------------------------------- 1 | import ReactJson from "react-json-view"; 2 | import type { ReactJsonViewProps } from "react-json-view"; 3 | import { useTheme } from "@mui/material"; 4 | 5 | const ThemeReactJson = (props: ReactJsonViewProps) => { 6 | const muiTheme = useTheme(); 7 | 8 | const theme = 9 | "theme" in props 10 | ? props.theme 11 | : muiTheme.palette.mode === "dark" 12 | ? "summerfruit" 13 | : "rjv-default"; 14 | 15 | return ; 16 | }; 17 | 18 | export default ThemeReactJson; 19 | -------------------------------------------------------------------------------- /examples/advanced_usage/panel_direction.py: -------------------------------------------------------------------------------- 1 | import funix 2 | 3 | 4 | @funix.funix() 5 | def foo_default(x:int) -> str: 6 | return f"{x} appears to the row, default" 7 | 8 | @funix.funix( 9 | direction="column" 10 | ) 11 | def foo_bottom(x:int) -> str: 12 | return f"{x} appears at the bottom" 13 | 14 | @funix.funix( 15 | direction="column-reverse" 16 | ) 17 | def foo_top(x:int) -> str: 18 | return f"{x} appears at the top" 19 | 20 | @funix.funix( 21 | direction="row-reverse" 22 | ) 23 | def foo_left(x:int) -> str: 24 | return f"{x} appears to the left" -------------------------------------------------------------------------------- /frontend/src/components/FunixFunction/OutputComponents/OutputError.tsx: -------------------------------------------------------------------------------- 1 | import { Alert, AlertTitle } from "@mui/material"; 2 | 3 | export default function OutputError(props: { error: { error_body: string } }) { 4 | return ( 5 | 6 | Oops! Something went wrong 7 |
14 |         {props.error.error_body}
15 |       
16 |
17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /examples/chemistry/Ketcher.py: -------------------------------------------------------------------------------- 1 | # Embedding an EPAM Ketcher chemistry drawer as the input and return the InChi, SMILES, and SVG-format structure of the substance you just draw. 2 | 3 | 4 | from typing import Tuple 5 | 6 | from IPython.display import Markdown 7 | 8 | from funix.hint import Ketcher, ChemStr 9 | from funix import funix 10 | 11 | 12 | 13 | 14 | @funix() 15 | def Ketcher_demo(x: Ketcher) -> Tuple[Markdown, ChemStr]: 16 | markdown = f"""The substance you just drew, has the following: 17 | * InChi: {x.inchi} 18 | * SMILES: {x.smiles}""" 19 | 20 | return markdown, x.smiles 21 | -------------------------------------------------------------------------------- /examples/widgets/input_widgets.py: -------------------------------------------------------------------------------- 1 | import typing 2 | 3 | import ipywidgets 4 | 5 | import funix 6 | 7 | # @funix.funix( 8 | # widgets={ 9 | # "model": "input" 10 | # } 11 | # ) 12 | 13 | @funix.funix( 14 | show_source=True, 15 | ) 16 | def input_widgets_basic( 17 | prompt: str, 18 | advanced_features: bool = True, 19 | model: typing.Literal['GPT-3.5', 'GPT-4.0', 'Falcon-7B']= 'GPT-4.0', 20 | max_token: range(100, 200, 20)=140, 21 | openai_key: ipywidgets.Password = "1234556", 22 | ) -> str: 23 | return "This is a dummy function. It returns nothing. " -------------------------------------------------------------------------------- /frontend/src/components/Common/WidgetSyntaxParser.ts: -------------------------------------------------------------------------------- 1 | const sliderWidgetParser = (widget: string) => { 2 | let args = [0, 100, 1]; 3 | if (widget.indexOf("[") !== -1 && widget.indexOf("]") !== -1) { 4 | args = JSON.parse(widget.trim().split("slider")[1]); 5 | } 6 | return args; 7 | }; 8 | 9 | const textareaWidgetParser = (widget: string): number[] | "default" => { 10 | if (widget.indexOf("[") !== -1 && widget.indexOf("]") !== -1) { 11 | return JSON.parse(widget.trim().split("textarea")[1]); 12 | } 13 | return "default"; 14 | }; 15 | 16 | export { sliderWidgetParser, textareaWidgetParser }; 17 | -------------------------------------------------------------------------------- /frontend/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Funix", 3 | "name": "Textea Funix React Frontend", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /backend/funix/build/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Funix", 3 | "name": "Textea Funix React Frontend", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /examples/stream.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | def stream() -> str: 4 | """ 5 | 6 | ## Streaming demo in Funix 7 | 8 | To see it, simply click the "Run" button. 9 | """ 10 | message = "We the People of the United States, in Order to form a more perfect Union, establish Justice, insure domestic Tranquility, provide for the common defence, promote the general Welfare, and secure the Blessings of Liberty to ourselves and our Posterity, do ordain and establish this Constitution for the United States of America. " 11 | 12 | for i in range(len(message)): 13 | time.sleep(0.01) 14 | yield message[0:i] 15 | -------------------------------------------------------------------------------- /examples/advanced_usage/per_argument_config.py: -------------------------------------------------------------------------------- 1 | import random, os, typing 2 | import funix 3 | 4 | @funix.funix( 5 | title="Per-argument configuration", 6 | description="""This example shows off per-argument customizations where configurations are aggregated by the name of arguments in the `argument_config` parameter. 7 | """, 8 | argument_config={ 9 | "x": { 10 | "widget": "textarea", 11 | "label": "a text area string"}, 12 | "y": { 13 | "widget": "password", 14 | "label": "a password string"}, 15 | }, 16 | show_source=True, 17 | ) 18 | def per_argument(x: str, y:str): 19 | pass -------------------------------------------------------------------------------- /examples/default_and_candidate_values.py: -------------------------------------------------------------------------------- 1 | # Default and example values for variables 2 | 3 | import typing 4 | import funix 5 | 6 | @funix.funix( 7 | description="This example shows how to provide default and candiadate/example values for UI widgets. For default values, simpliy take advantage of Python's default value syntax for keyword arguments. For example values, use the `example` parameter in Funix. ", 8 | examples={"arg3": [1, 5, 7]}, 9 | show_source=True, 10 | ) 11 | def default_and_candidate_values( 12 | arg1: str = "Default value prefilled. ", 13 | arg2: typing.Literal["is", "is not"]="is not", 14 | arg3: int = 3, 15 | ) -> str: 16 | return f"The number {arg3} {arg2} {arg1}." -------------------------------------------------------------------------------- /examples/archive/memory.py: -------------------------------------------------------------------------------- 1 | from decorator import TexteaExport 2 | 3 | 4 | # @TexteaExport(path='/calc', type=['add', 'minus']) 5 | def calc(a: int, b: int, type: str): 6 | if type == "add": 7 | return a + b 8 | elif type == "minus": 9 | return a - b 10 | else: 11 | raise "invalid parameter type" 12 | 13 | 14 | @TexteaExport(path="/test", username={"possible": "turx"}, pi={"example": 3.1415926535}) 15 | def test(username: str, pi: float, d: dict, arr: list): 16 | s = "" 17 | s += ("{}, {}\n").format(username, type(username)) 18 | s += ("{}, {}\n").format(pi, type(pi)) 19 | s += ("{}, {}\n").format(d, type(d)) 20 | s += ("{}, {}\n").format(arr, type(arr)) 21 | return s 22 | -------------------------------------------------------------------------------- /frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "noFallthroughCasesInSwitch": true, 16 | "module": "esnext", 17 | "moduleResolution": "node", 18 | "resolveJsonModule": true, 19 | "isolatedModules": true, 20 | "noEmit": true, 21 | "jsx": "react-jsx", 22 | "typeRoots": [ 23 | "node_modules/@types" 24 | ] 25 | }, 26 | "include": [ 27 | "src" 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /examples/advanced_usage/conditional_visibility.py: -------------------------------------------------------------------------------- 1 | import typing 2 | 3 | import ipywidgets 4 | 5 | import funix 6 | 7 | 8 | @funix.funix( 9 | widgets={"prompt": "textarea"}, 10 | conditional_visible=[ 11 | { 12 | "when": { 13 | "show_advanced": True, 14 | }, 15 | "show": ["max_tokens", "model", "openai_key"], 16 | } 17 | ], 18 | ) 19 | def conditional_visible( 20 | prompt: str, 21 | show_advanced: bool = False, 22 | model: typing.Literal["gpt-3.5-turbo", "gpt-3.5-turbo-0301"] = "gpt-3.5-turbo", 23 | max_tokens: range(100, 200, 20) = 140, 24 | openai_key: ipywidgets.Password = "", 25 | ) -> str: 26 | return "This is a dummy function." 27 | -------------------------------------------------------------------------------- /examples/games/hangman.py: -------------------------------------------------------------------------------- 1 | # The hangman game with funix 2 | 3 | import funix 4 | from IPython.display import Markdown 5 | 6 | secret_word = "funix" 7 | used_letters = [] # a global variable 8 | # to maintain the state/session 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | def guess_letter(Enter_a_letter: str) -> Markdown: 21 | letter = Enter_a_letter # rename 22 | global used_letters # state/session as global 23 | used_letters.append(letter) 24 | answer = "".join([ 25 | (letter if letter in used_letters else "_") 26 | for letter in secret_word 27 | ]) 28 | return f"### Hangman \n `{answer}` \n\n ---- \n ### Used letters \n {', '.join(used_letters)}" 29 | -------------------------------------------------------------------------------- /examples/advanced_usage/pre_fill.py: -------------------------------------------------------------------------------- 1 | from funix import funix 2 | 3 | # This example shows how to plug in results on one page to the input widgets of another. 4 | 5 | 6 | @funix() 7 | def foo(x: int) -> int: 8 | return x - 1 9 | 10 | 11 | @funix() 12 | def bar(message: str) -> list[str]: 13 | return message.split(" ") 14 | 15 | 16 | @funix() 17 | def goh(x: int, y: int) -> dict: 18 | return {"x": x, "y": y} 19 | 20 | 21 | @funix( 22 | pre_fill={ 23 | "a": foo, # plug the return of function foo to a 24 | "b": (bar, -1), # plug the last (-1) return of function bar to b 25 | "c": (goh, "x")} # plug the return of key "x" of function goh to c 26 | ) 27 | def together(a: int, b: str, c: int) -> str: 28 | return f"{a} {b} {c}" 29 | -------------------------------------------------------------------------------- /backend/funix/decorator/encoder.py: -------------------------------------------------------------------------------- 1 | from json import JSONEncoder 2 | from datetime import datetime 3 | 4 | 5 | class FunixJsonEncoder(JSONEncoder): 6 | def default(self, o): 7 | if isinstance(o, datetime): 8 | return o.isoformat() 9 | if hasattr(o, "__class__"): 10 | clz = o.__class__ 11 | if hasattr(clz, "__base__"): 12 | base = clz.__base__ 13 | # Check pydantic 14 | try: 15 | from pydantic import BaseModel 16 | 17 | if issubclass(base, BaseModel): 18 | return o.model_dump(mode="json") 19 | except ImportError: 20 | return super().default(o) 21 | return super().default(o) 22 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [TexteaInc] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 13 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 14 | -------------------------------------------------------------------------------- /.github/workflows/run-backend-test.yml: -------------------------------------------------------------------------------- 1 | name: Run Backend Test 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | - dev 8 | paths: 9 | - "backend/**" 10 | pull_request: 11 | paths: 12 | - "backend/**" 13 | 14 | jobs: 15 | backend-test: 16 | runs-on: ubuntu-latest 17 | 18 | steps: 19 | - name: Checkout 20 | uses: actions/checkout@v3 21 | with: 22 | fetch-depth: 0 23 | - name: Setup Python 24 | uses: actions/setup-python@v4 25 | with: 26 | python-version: "3.10" 27 | cache: "pip" 28 | - name: Install funix 29 | run: "pip install -e ." 30 | - name: Run test 31 | run: "python -m unittest discover . -v" 32 | working-directory: backend/funix/test 33 | 34 | 35 | -------------------------------------------------------------------------------- /examples/class.py: -------------------------------------------------------------------------------- 1 | from funix import funix_method, funix_class 2 | from IPython.display import Markdown 3 | 4 | @funix_class() 5 | class A: 6 | @funix_method(title="Initialize A", print_to_web=True) 7 | def __init__(self, a: int): 8 | self.a = a 9 | print(f"`self.a` has been initialized to {self.a}") 10 | 11 | def set(self, b: int) -> Markdown: 12 | self.a = b 13 | return f"`self.a` has been updated to {self.a}" 14 | 15 | def get(self) -> Markdown: 16 | return f"The value of `self.a` is {self.a}" 17 | 18 | @staticmethod 19 | @funix_method(disable=True) 20 | def add(a: int, b: int) -> int: 21 | return a + b 22 | 23 | @staticmethod 24 | @funix_method(title="Returns 1", disable=True) 25 | def return_1() -> int: 26 | return 1 27 | -------------------------------------------------------------------------------- /frontend/src/components/FunixFunction/OutputComponents/OutputCode.tsx: -------------------------------------------------------------------------------- 1 | import Editor from "@monaco-editor/react"; 2 | import { useTheme } from "@mui/material"; 3 | 4 | interface OutputCodeProps { 5 | code: string; 6 | language?: string; 7 | } 8 | 9 | export default function OutputCode(props: OutputCodeProps) { 10 | const theme = useTheme(); 11 | 12 | return ( 13 | { 18 | const height = editor.getContentHeight(); 19 | editor.layout({ height }); 20 | }} 21 | theme={theme.palette.mode === "dark" ? "vs-dark" : "light"} 22 | options={{ 23 | readOnly: true, 24 | minimap: { 25 | enabled: false, 26 | }, 27 | }} 28 | /> 29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /frontend/src/components/Common/PDFViewer.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Grid } from "@mui/material"; 3 | const PDFViewer = (props: { pdf: string | File }) => { 4 | const data = 5 | typeof props.pdf === "string" ? props.pdf : URL.createObjectURL(props.pdf); 6 | 7 | return ( 8 | <> 9 | 20 | 28 | 29 | 30 | ); 31 | }; 32 | 33 | export default PDFViewer; 34 | -------------------------------------------------------------------------------- /backend/funix/decorator/response.py: -------------------------------------------------------------------------------- 1 | from inspect import isclass 2 | from typing import Any 3 | from datetime import datetime 4 | 5 | from pydantic import BaseModel 6 | 7 | 8 | def response_item_to_class(response_item: Any, clazz: type) -> Any: 9 | """ 10 | Convert a response item to a class instance. 11 | 12 | Parameters: 13 | response_item (Any): The response item to convert. 14 | clazz (type): The class to convert the response item to. 15 | 16 | Returns: 17 | Any: An instance of the class with the response item data. 18 | """ 19 | if clazz is datetime: 20 | return datetime.fromisoformat(response_item) 21 | if isclass(clazz) and issubclass(clazz, BaseModel): 22 | return clazz.model_validate(response_item) 23 | try: 24 | return clazz(response_item) 25 | except Exception as e: 26 | return response_item 27 | -------------------------------------------------------------------------------- /backend/funix/build/index.html: -------------------------------------------------------------------------------- 1 | Funix
2 | -------------------------------------------------------------------------------- /examples/advanced_usage/vectorization.py: -------------------------------------------------------------------------------- 1 | import funix 2 | 3 | 4 | @funix.funix( 5 | title="""Automatic vectorization""", 6 | description="""Funix automatically vectorizes a scalar function on arguments declared to be cells in a sheet. See the source code for details. In this example, arguments `a` and `b` are declared so and hence the function is partially mapped onto them -- partial because the argument `isAdd` is not declared so. 7 | 8 | **Usage:** Simply click `Add a row` button to create new rows and then double-click cells to add numeric values. Then click the Run button. In the output panel, click the `Sheet` radio button to view the result as a headed table. 9 | 10 | """, 11 | widgets={ 12 | ("a", "b"): "sheet", 13 | }, 14 | treat_as={("a", "b"): "cell"}, 15 | show_source=True, 16 | ) 17 | def cell_test(a: int, b: int, isAdd: bool) -> int: 18 | return a + b if isAdd else a - b -------------------------------------------------------------------------------- /frontend/src/components/Common/TemplateString.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from "react"; 2 | import reactStringReplace from "react-string-replace"; 3 | 4 | const TemplateString = ({ 5 | template, 6 | records, 7 | }: { 8 | template: string; 9 | records: Record; 10 | }) => { 11 | const keys = Object.keys(records); 12 | let result: string | ReactNode[] = template; 13 | result = reactStringReplace(result, /{{([^}]+)}}/g, (match, _i) => { 14 | if (keys.includes(match)) { 15 | return records[match]; 16 | } 17 | return match; 18 | }); 19 | 20 | return result; 21 | }; 22 | 23 | const stringTemplate = ( 24 | template: string, 25 | records: Record, 26 | ): string => { 27 | const keys = Object.keys(records); 28 | return template.replace(/{{([^}]+)}}/g, (match, key) => { 29 | if (keys.includes(key)) { 30 | return records[key]; 31 | } 32 | return match; 33 | }); 34 | }; 35 | 36 | export { TemplateString, stringTemplate }; 37 | -------------------------------------------------------------------------------- /examples/AI/DallE.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import openai # pip install openai 4 | 5 | import IPython 6 | 7 | import funix 8 | 9 | 10 | cfg = { 11 | 'title': 'OpenAI: Dall-E', 12 | 'description': 'Generate an image with DALL-E in [Funix](http://funix.io), the minimalist way to build apps in Python. An OpenAI key needs to be set. A rate limit is applied. ', 13 | 'rate_limit': funix.decorator.Limiter.session(max_calls=1, period=60*60*24), 'show_source': True} 14 | 15 | @funix.funix(**cfg) 16 | def dalle_create( 17 | Prompt: str = "a cat on a red jeep" 18 | ) -> IPython.display.Image: 19 | 20 | client = openai.OpenAI() # defaults to os.environ.get("OPENAI_API_KEY") 21 | 22 | response = client.images.generate( 23 | prompt=Prompt, 24 | ) 25 | 26 | return response.data[0].url 27 | 28 | 29 | # **Note:** 30 | # * An OpenAI key needs to be set in the environment variable OPENAI_KEY. 31 | # * A rate limit of 1 call per day per browser session is set. 32 | 33 | # Like us? Please star us on [GitHub](https://github.com/TexteaInc/funix)]. 34 | -------------------------------------------------------------------------------- /examples/themes/on_the_fly_theme.py: -------------------------------------------------------------------------------- 1 | import typing 2 | 3 | import funix 4 | 5 | 6 | @funix.new_funix_type( 7 | widget={ 8 | "name": "textarea", 9 | "config": {"minRows": 5, "maxRows": 8, "multiline": "True"} 10 | # This config is to be overwritten by the theme below 11 | } 12 | ) 13 | class long_str(str): 14 | pass 15 | 16 | 17 | theme = { 18 | "name": "my_new_theme", 19 | "widgets": { 20 | "long_str": ["textarea", {"minRows": 2, "maxRows": 4}], 21 | # "long_str": "textarea(minRows=2, maxRows=3)", 22 | # TODO: shall we support the function call syntax above? 23 | # Comment: Oh, here we go again 24 | "int": "slider(0, 10, 50)", 25 | }, 26 | } 27 | 28 | funix.import_theme(theme, alias="my_new_theme") 29 | 30 | funix.set_default_theme("my_new_theme") 31 | 32 | 33 | @funix.funix( 34 | # theme="my_new_theme", # This line not needed if set_default_theme above 35 | ) 36 | def on_the_fly_theme(x: long_str = "\n".join(map(str, range(8))), y: int = 40) -> str: 37 | return f"{x} {y}" 38 | -------------------------------------------------------------------------------- /backend/funix/decorator/all_of.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | 3 | from funix.hint import ConditionalVisibleType 4 | 5 | 6 | def parse_all_of( 7 | conditional_visible: ConditionalVisibleType, json_schema_props: dict 8 | ) -> list: 9 | all_of = [] 10 | delete_keys = set() 11 | 12 | for conditional_visible_item in conditional_visible: 13 | config = { 14 | "if": {"properties": {}}, 15 | "then": {"properties": {}}, 16 | "required": [], 17 | } 18 | if_items: Any = conditional_visible_item["when"] 19 | then_items = conditional_visible_item["show"] 20 | for if_item in if_items.keys(): 21 | config["if"]["properties"][if_item] = {"const": if_items[if_item]} 22 | for then_item in then_items: 23 | config["then"]["properties"][then_item] = json_schema_props[then_item] 24 | config["required"].append(then_item) 25 | delete_keys.add(then_item) 26 | all_of.append(config) 27 | 28 | for key in delete_keys: 29 | json_schema_props.pop(key) 30 | 31 | return all_of 32 | -------------------------------------------------------------------------------- /backend/funix/jupyter/__init__.py: -------------------------------------------------------------------------------- 1 | from ipaddress import ip_address 2 | from random import randint 3 | from threading import Thread 4 | 5 | from flask import Flask 6 | 7 | from funix.app import GlobalSwitchOption 8 | from funix.decorator.file import enable_file_service 9 | from funix.decorator.lists import enable_list 10 | from funix.util.network import get_unused_port_from, is_port_used 11 | 12 | 13 | def jupyter(app: Flask) -> None: 14 | if GlobalSwitchOption.in_notebook: 15 | import logging 16 | 17 | logging.getLogger("werkzeug").setLevel(logging.ERROR) 18 | enable_file_service(app) 19 | enable_list(app) 20 | from IPython.display import IFrame, display 21 | 22 | port = get_unused_port_from(randint(3000, 4000), ip_address("127.0.0.1")) 23 | Thread( 24 | target=lambda: app.run( 25 | host="127.0.0.1", port=port, debug=True, use_reloader=False 26 | ) 27 | ).start() 28 | while not is_port_used(port, "127.0.0.1"): 29 | pass 30 | GlobalSwitchOption.NOTEBOOK_PORT = port 31 | display(IFrame(f"http://127.0.0.1:{port}", width="100%", height=800)) 32 | -------------------------------------------------------------------------------- /examples/AI/chatGPT.py: -------------------------------------------------------------------------------- 1 | import os # Python's native 2 | 3 | import openai 4 | import IPython # a famous library for interactive python 5 | 6 | def ChatGPT(prompt: str="Who is Cauchy?") -> IPython.display.Markdown: 7 | client = openai.OpenAI() # defaults to os.environ.get("OPENAI_API_KEY") 8 | response = client.chat.completions.create( 9 | model="gpt-3.5-turbo", 10 | messages=[ 11 | {"role": "system", "content": "You are a helpful assistant."}, 12 | {"role": "user", "content": prompt} 13 | ] 14 | ) 15 | return response.choices[0].message.content 16 | 17 | def ChatGPT_stream(prompt: str="Who is Cauchy?") -> IPython.display.Markdown: 18 | client = openai.OpenAI() # defaults to os.environ.get("OPENAI_API_KEY") 19 | response = client.chat.completions.create( 20 | messages=[{"role": "user", "content": prompt}], 21 | model="gpt-3.5-turbo", 22 | stream=True, 23 | ) 24 | message = [] 25 | for chunk in response: # streaming from OpenAI 26 | message.append(chunk.choices[0].delta.content or "") 27 | yield "".join(message) 28 | -------------------------------------------------------------------------------- /backend/funix/util/secret.py: -------------------------------------------------------------------------------- 1 | """ 2 | Generate and parse 3 | """ 4 | 5 | import secrets 6 | from typing import Union 7 | 8 | false_words: list[str] = ["false", "no", "off", "0", "null", "none", "nil", ""] 9 | 10 | true_words: list[str] = ["true", "yes", "on", "1"] 11 | 12 | 13 | def get_secret_key_from_option(secret: Union[str, bool, None]) -> Union[str, bool]: 14 | """ 15 | Get the secret key from the option, if it is true value, generate a new secret key. 16 | if it is false value, return false no whatever the value is. 17 | if it is a string, return the string. 18 | 19 | Parameters: 20 | secret (str | bool): The secret key. 21 | 22 | Returns: 23 | str | bool: The secret key. 24 | """ 25 | if isinstance(secret, str): 26 | if secret.lower() in true_words: 27 | return secrets.token_hex(16) 28 | elif secret.lower() in false_words: 29 | return False 30 | else: 31 | return secret 32 | elif isinstance(secret, bool): 33 | return secrets.token_hex(16) if secret else False 34 | else: 35 | # None 36 | return False 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022-2023 Textea Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.github/workflows/build-and-push.yml: -------------------------------------------------------------------------------- 1 | # For main branch only 2 | name: Build Frontend and Push 3 | 4 | on: 5 | push: 6 | branches: 7 | - main 8 | paths: 9 | - "frontend/**" 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - name: Checkout 17 | uses: actions/checkout@v3 18 | with: 19 | fetch-depth: 0 20 | - name: Setup Node.js 21 | uses: actions/setup-node@v3 22 | with: 23 | node-version-file: ".nvmrc" 24 | node-version: "20.x" 25 | - name: Install dependencies 26 | run: yarn install 27 | working-directory: frontend 28 | - name: Build frontend 29 | run: yarn funix:build 30 | working-directory: frontend 31 | - name: Set git 32 | run: | 33 | git config --global user.name "GitHub Actions" 34 | git config --global user.email "github-actions[bot]@users.noreply.github.com" 35 | - name: Push to main branch 36 | run: | 37 | git add -f backend/funix/build 38 | git commit -m "chore: auto build frontend" 39 | git push origin HEAD:main 40 | -------------------------------------------------------------------------------- /frontend/src/components/FunixFunction/OutputComponents/OutputNextTo.tsx: -------------------------------------------------------------------------------- 1 | import { useAtom } from "jotai"; 2 | import { callableDefaultAtom } from "../../../store"; 3 | import { useNavigate } from "react-router-dom"; 4 | import { useCallback, useEffect } from "react"; 5 | 6 | interface OutputNextToProps { 7 | response: string; 8 | } 9 | 10 | type NextToResponse = { 11 | args: any; 12 | path: string; 13 | }; 14 | 15 | const OutputNextTo = (props: OutputNextToProps) => { 16 | const response = JSON.parse(props.response) as NextToResponse; 17 | const [callableDefault, setCallableDefault] = useAtom(callableDefaultAtom); 18 | const navigate = useNavigate(); 19 | 20 | const toPath = useCallback(() => { 21 | const newCallableDefault = { ...callableDefault }; 22 | newCallableDefault[response.path] = response.args; 23 | setCallableDefault(newCallableDefault); 24 | navigate(response.path); 25 | }, [ 26 | callableDefault, 27 | response.args, 28 | response.path, 29 | setCallableDefault, 30 | navigate, 31 | ]); 32 | 33 | useEffect(() => { 34 | toPath(); 35 | }, [toPath]); 36 | return <>; 37 | }; 38 | 39 | export default OutputNextTo; 40 | -------------------------------------------------------------------------------- /frontend/src/components/History/HistoryLoader.tsx: -------------------------------------------------------------------------------- 1 | import { useAtom } from "jotai"; 2 | import useFunixHistory from "../../shared/useFunixHistory"; 3 | import { useEffect, useRef } from "react"; 4 | import { historiesAtom } from "../../store"; 5 | 6 | const HistoryLoader = (props: { children: React.ReactNode }) => { 7 | const [, setHistories] = useAtom(historiesAtom); 8 | const { getHistories } = useFunixHistory(); 9 | const historyLoaded = useRef(false); 10 | 11 | useEffect(() => { 12 | if (historyLoaded.current) { 13 | return; 14 | } 15 | historyLoaded.current = true; 16 | getHistories().then((histories) => { 17 | setHistories(histories); 18 | }); 19 | }, [getHistories, setHistories]); 20 | 21 | useEffect(() => { 22 | const handleHistoryUpdate = () => { 23 | getHistories().then((histories) => { 24 | setHistories(histories); 25 | }); 26 | }; 27 | 28 | window.addEventListener("funix-history-update", handleHistoryUpdate); 29 | 30 | return () => { 31 | window.removeEventListener("funix-history-update", handleHistoryUpdate); 32 | }; 33 | }, [getHistories, setHistories]); 34 | 35 | return <>{props.children}; 36 | }; 37 | 38 | export default HistoryLoader; 39 | -------------------------------------------------------------------------------- /examples/advanced_usage/sessions_simple.py: -------------------------------------------------------------------------------- 1 | import funix 2 | 3 | 4 | y = "The default value of y." 5 | 6 | description = """ 7 | This demo shows how to pass data between pages. 8 | 9 | Each Funix-decorated function becomes a page. So just use a global variable to pass data between pages. The global variable is automatically sessionized to separate connections from different browser windows. 10 | 11 | After executing this page, you can go to Page 2 to call `get_y()` to display the changed global variable `y`. 12 | 13 | Another example is OpenAI demos where OpenAI key is set in one page while DallE and GPT3 demos use the key in other pages. Check them out using the function selector above""" 14 | 15 | @funix.funix( 16 | title="Session/state: set", 17 | description="Set the global variable `y` to a new value. ", 18 | ) 19 | def set_y(new_y: str="123") -> str: 20 | global y 21 | y = new_y 22 | return "Y has been changed. Now check it in the `Session/State: get` page." 23 | 24 | 25 | @funix.funix( 26 | title="Session/State: get", 27 | description=""" 28 | Retrieve the sessionized global variable `y`. If you have NOT run `set_y()`, then after clicking the Run button, you will see the default value of `y`. 29 | """, 30 | ) 31 | def get_y() -> str: 32 | return y -------------------------------------------------------------------------------- /frontend/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom/client"; 3 | import "./index.css"; 4 | import App from "./App"; 5 | import { SWRConfig } from "swr"; 6 | import { BrowserRouter } from "react-router-dom"; 7 | import { closeSnackbar, SnackbarProvider } from "notistack"; 8 | import { IconButton } from "@mui/material"; 9 | import { Close } from "@mui/icons-material"; 10 | import { registerLicense } from "./Key"; 11 | 12 | const root = ReactDOM.createRoot( 13 | document.getElementById("root") as HTMLElement, 14 | ); 15 | 16 | registerLicense(); 17 | 18 | root.render( 19 | 20 | 23 | fetch(resource, init).then((res) => res.json()), 24 | suspense: true, 25 | }} 26 | > 27 | 28 | ( 32 | closeSnackbar(key)} color="inherit"> 33 | 34 | 35 | )} 36 | > 37 | 38 | 39 | 40 | 41 | , 42 | ); 43 | -------------------------------------------------------------------------------- /backend/funix/util/text.py: -------------------------------------------------------------------------------- 1 | """ 2 | Handle text processing 3 | """ 4 | 5 | # Copied from [indoc](https://github.com/dtolnay/indoc) 6 | 7 | 8 | def count_space(line: str) -> None | int: 9 | """ 10 | Document is on the way 11 | """ 12 | for i in range(len(line)): 13 | if line[i] != " " and line[i] != "\t": 14 | return i 15 | return None 16 | 17 | 18 | def un_indent(message: str) -> str: 19 | """ 20 | Document is on the way 21 | """ 22 | new_message = message 23 | 24 | ignore_first_line = new_message.startswith("\n") or new_message.startswith("\r\n") 25 | 26 | lines = new_message.splitlines() 27 | 28 | min_spaces = [] 29 | 30 | for i in lines[1:]: 31 | result = count_space(i) 32 | if result is not None: 33 | min_spaces.append(result) 34 | 35 | if len(min_spaces) > 0: 36 | min_space = sorted(min_spaces)[0] 37 | else: 38 | min_space = 0 39 | 40 | result = "" 41 | 42 | for i in range(len(lines)): 43 | if i > 1 or (i == 1 and not ignore_first_line): 44 | result += "\n" 45 | 46 | if i == 0: 47 | result += lines[i] 48 | elif len(lines[i]) > min_space: 49 | result += lines[i][min_space:] 50 | 51 | return result.strip() 52 | -------------------------------------------------------------------------------- /frontend/src/components/FunixFunction/DateTimePickerWidget.tsx: -------------------------------------------------------------------------------- 1 | import { DateTimePicker } from "@mui/x-date-pickers"; 2 | import { WidgetProps } from "@rjsf/utils"; 3 | import dayjs, { Dayjs } from "dayjs"; 4 | import React, { useEffect } from "react"; 5 | 6 | interface DateTimePickerWidgetProps { 7 | widget: WidgetProps; 8 | data: any; 9 | } 10 | 11 | const DateTimePickerWidget = (props: DateTimePickerWidgetProps) => { 12 | const [value, setValue] = React.useState(() => { 13 | if (props.widget.schema.default) { 14 | return dayjs(props.widget.schema.default.toString()); 15 | } 16 | return null; 17 | }); 18 | 19 | const handleChange = (date: Dayjs | null) => { 20 | if (date) { 21 | setValue(date); 22 | props.widget.onChange(date.toISOString()); 23 | } else { 24 | setValue(null); 25 | props.widget.onChange(null); 26 | } 27 | }; 28 | 29 | useEffect(() => { 30 | if (props.data) { 31 | setValue(dayjs(props.data.toString())); 32 | } 33 | }, [props.data]); 34 | 35 | return ( 36 | 44 | ); 45 | }; 46 | 47 | export default DateTimePickerWidget; 48 | -------------------------------------------------------------------------------- /examples/AI/chatGPT_advanced.py: -------------------------------------------------------------------------------- 1 | import typing 2 | import IPython 3 | 4 | import openai 5 | 6 | import funix 7 | 8 | cfg = { 9 | "conditional_visible": [ 10 | {"when": {"show_advanced_options": True}, "show": ["model", "max_tokens", "stream"]} 11 | ], 12 | # "rate_limit": funix.decorator.Limiter.session(max_calls=2, period=60 * 60 * 24), 13 | } 14 | 15 | 16 | @funix.funix(**cfg) 17 | def ChatGPT_advanced_stream( 18 | prompt: str, 19 | show_advanced_options: bool = False, 20 | stream: bool = True, 21 | model: typing.Literal["gpt-3.5-turbo", "gpt-4"] = "gpt-3.5-turbo", 22 | max_tokens: range(100, 500, 50) = 150 23 | ) -> IPython.display.Markdown: 24 | """ 25 | The ChatGPT app built in [Funix.io](http://funix.io) 26 | """ 27 | client = openai.OpenAI() # defaults to os.environ.get("OPENAI_API_KEY") 28 | response = client.chat.completions.create( 29 | messages=[{"role": "user", "content": prompt}], 30 | model=model, 31 | stream=stream, 32 | max_tokens=max_tokens 33 | ) 34 | if stream: 35 | message = [] 36 | for chunk in response: 37 | message.append(chunk.choices[0].delta.content or "") 38 | yield "".join(message) 39 | else: # no stream 40 | yield response.choices[0].message.content 41 | -------------------------------------------------------------------------------- /examples/AI/chatGPT_muilti_turn.py: -------------------------------------------------------------------------------- 1 | import os 2 | import IPython 3 | from openai import OpenAI 4 | import funix 5 | 6 | client = OpenAI(api_key=os.environ.get("OPENAI_KEY")) 7 | 8 | messages = [] # list of dicts, dict keys: role, content, system 9 | 10 | @funix.funix( 11 | disable=True, 12 | ) 13 | def print_messages_html(messages): 14 | printout = "" 15 | for message in messages: 16 | if message["role"] == "user": 17 | align, name = "left", "You" 18 | elif message["role"] == "assistant": 19 | align, name = "right", "ChatGPT" 20 | printout += f'
{name}: {message["content"]}
' 21 | return printout 22 | 23 | 24 | @funix.funix( 25 | direction="column-reverse", 26 | ) 27 | def ChatGPT_multi_turn(current_message: str) -> IPython.display.HTML: 28 | current_message = current_message.strip() 29 | messages.append({"role": "user", "content": current_message}) 30 | completion = client.chat.completions.create(messages=messages, 31 | model='gpt-3.5-turbo', 32 | max_tokens=100) 33 | chatgpt_response = completion.choices[0].message.content 34 | messages.append({"role": "assistant", "content": chatgpt_response}) 35 | 36 | # return print_messages_markdown(messages) 37 | return print_messages_html(messages) 38 | -------------------------------------------------------------------------------- /frontend/src/shared/type.d.ts: -------------------------------------------------------------------------------- 1 | export declare global { 2 | interface WindowEventMap { 3 | "funix-history-update": CustomEvent; 4 | "funix-rollback-now": CustomEvent; 5 | } 6 | } 7 | 8 | export declare module "json-schema" { 9 | type inputRowItem = { 10 | type: "markdown" | "argument" | "divider" | "html"; 11 | content?: string; 12 | argument?: string; 13 | width?: number; 14 | offset?: number; 15 | position?: "left" | "center" | "right"; 16 | }; 17 | 18 | type outputRowItem = { 19 | type: 20 | | "markdown" 21 | | "html" 22 | | "divider" 23 | | "images" 24 | | "videos" 25 | | "audios" 26 | | "files" 27 | | "code" 28 | | "return_index"; 29 | content?: string | string[]; 30 | index?: number | number[]; 31 | argument?: string; 32 | width?: number; 33 | offset?: number; 34 | position?: "left" | "center" | "right"; 35 | lang?: string; 36 | }; 37 | 38 | type inputRow = inputRowItem[]; 39 | type outputRow = outputRowItem[]; 40 | 41 | interface JSONSchema7 { 42 | widget?: string; 43 | keys?: { [key: string]: string }; 44 | customLayout: boolean; 45 | input_layout: inputRow[]; 46 | output_layout: outputRow[]; 47 | output_indexes: number[]; 48 | advanced_examples: null | Record[]; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /frontend/src/components/SheetComponents/SheetCheckBox.tsx: -------------------------------------------------------------------------------- 1 | import { SheetInterface } from "./SheetInterface"; 2 | import React, { useEffect } from "react"; 3 | import { 4 | Checkbox, 5 | FormControl, 6 | FormControlLabel, 7 | FormGroup, 8 | Switch, 9 | } from "@mui/material"; 10 | 11 | export default function SheetCheckBox( 12 | props: SheetInterface & { isSwitch: boolean }, 13 | ) { 14 | const [checked, setChecked] = React.useState(props.params.value); 15 | 16 | useEffect(() => { 17 | setChecked(props.params.value); 18 | }, [props.params.value]); 19 | 20 | const onChange = (event: React.ChangeEvent) => { 21 | setChecked(event.target.checked); 22 | props.customChange( 23 | props.params.row.id, 24 | props.params.field, 25 | event.target.checked, 26 | ); 27 | }; 28 | 29 | const controlElement = props.isSwitch ? ( 30 | 31 | ) : ( 32 | 33 | ); 34 | 35 | return ( 36 | 37 | 38 | 43 | 44 | 45 | ); 46 | } 47 | -------------------------------------------------------------------------------- /frontend/src/components/FunixFunction/OutputComponents/OutputChem.tsx: -------------------------------------------------------------------------------- 1 | import { Card, CardMedia } from "@mui/material"; 2 | import React, { useEffect, useState } from "react"; 3 | import renderSvg from "../../../shared/indigo-render"; 4 | 5 | declare global { 6 | interface Window { 7 | indigo: any; 8 | } 9 | } 10 | 11 | const OutputChem = (props: { data: string }) => { 12 | const [src, setSrc] = useState(null); 13 | const [loading, setLoading] = useState(true); 14 | 15 | useEffect(() => { 16 | if (!props.data) return; 17 | 18 | const loadAndRender = async () => { 19 | setLoading(true); 20 | try { 21 | const svg = renderSvg(props.data); 22 | setSrc(svg); 23 | } catch (e) { 24 | console.error(e); 25 | } 26 | setLoading(false); 27 | }; 28 | 29 | loadAndRender(); 30 | }, [props.data]); 31 | 32 | return ( 33 | <> 34 | {loading ? ( 35 |
Indigo Drawing...
36 | ) : ( 37 | 38 | 48 | 49 | )} 50 | 51 | ); 52 | }; 53 | 54 | export default React.memo(OutputChem); 55 | -------------------------------------------------------------------------------- /backend/funix/app/websocket.py: -------------------------------------------------------------------------------- 1 | """ 2 | Websocket class to redirect 3 | """ 4 | from io import StringIO 5 | from json import dumps 6 | 7 | 8 | class StdoutToWebsocket: 9 | """ 10 | Stdout to websocket. 11 | """ 12 | 13 | encoding = "utf-8" 14 | 15 | def __init__(self, ws, is_err=False): 16 | """ 17 | Initialize the StdoutToWebsocket. 18 | 19 | Parameters: 20 | ws (WebSocket): The websocket. 21 | """ 22 | self.ws = ws 23 | self.is_err = is_err 24 | self.value = StringIO() 25 | 26 | def _get_html(self): 27 | """ 28 | Get the html data. 29 | """ 30 | value = self.value.getvalue() 31 | if self.is_err and value.strip(): 32 | return f"`{value}`" 33 | return value 34 | 35 | def write(self, data): 36 | """ 37 | Write the data to the websocket. 38 | """ 39 | self.value.write(data) 40 | self.ws.send(dumps([self._get_html()])) 41 | 42 | def writelines(self, data): 43 | """ 44 | Write the lines to the websocket. 45 | """ 46 | self.value.writelines(data) 47 | self.ws.send(dumps([self._get_html()])) 48 | 49 | def flush(self): 50 | """ 51 | Flush the data to the websocket. 52 | """ 53 | self.value.flush() 54 | self.ws.send(dumps([self._get_html()])) 55 | self.value = StringIO() 56 | -------------------------------------------------------------------------------- /frontend/src/components/Common/ValueOperation.ts: -------------------------------------------------------------------------------- 1 | const getInitValue = ( 2 | type: string | undefined, 3 | ): number | string | boolean | object | null => { 4 | switch (type) { 5 | case "string": 6 | case "text": 7 | return ""; 8 | case "number": 9 | return 0.0; 10 | case "boolean": 11 | return false; 12 | case "integer": 13 | return 0; 14 | default: 15 | return null; 16 | } 17 | }; 18 | 19 | const castValue = ( 20 | value: any, 21 | type: string | undefined, 22 | ): number | string | boolean | object | null | undefined => { 23 | if (value === null || value == undefined) return value; 24 | switch (type) { 25 | case "string": 26 | case "text": 27 | return value.toString(); 28 | case "number": { 29 | const parsedFloat = parseFloat(value); 30 | if (!isNaN(parsedFloat) && isFinite(parsedFloat)) return parsedFloat; 31 | else return 0.0; 32 | } 33 | case "integer": { 34 | const parsedInt = parseInt(value); 35 | if (!isNaN(parsedInt) && isFinite(parsedInt)) return parsedInt; 36 | else return 0; 37 | } 38 | case "boolean": 39 | if (typeof value === "boolean") return value; 40 | else if (typeof value === "string") { 41 | if (value.toLowerCase() === "false") return false; 42 | else return value.toLowerCase() === "true"; 43 | } else return !!value; 44 | default: 45 | return value; 46 | } 47 | }; 48 | 49 | export { getInitValue, castValue }; 50 | -------------------------------------------------------------------------------- /frontend/src/components/FunixFunction/SwitchWidget.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Checkbox, 3 | FormControl, 4 | FormControlLabel, 5 | FormGroup, 6 | Switch, 7 | } from "@mui/material"; 8 | import React, { useEffect } from "react"; 9 | import MarkdownDiv from "../Common/MarkdownDiv"; 10 | import { WidgetProps } from "@rjsf/utils"; 11 | 12 | const SwitchWidget = (props: WidgetProps) => { 13 | const [checked, setChecked] = React.useState( 14 | !!(props.value || props.schema.default), 15 | ); 16 | 17 | useEffect(() => { 18 | if (props.value === checked) return; 19 | setChecked(!!(props.value || props.schema.default)); 20 | }, [props.value]); 21 | 22 | const onChange = (event: React.ChangeEvent) => { 23 | setChecked(event.target.checked); 24 | props.onChange(event.target.checked); 25 | }; 26 | 27 | const control = 28 | "widget" in props.schema && props.schema.widget === "switch" ? ( 29 | 30 | ) : ( 31 | 32 | ); 33 | 34 | return ( 35 | 36 | 37 | 44 | } 45 | /> 46 | 47 | 48 | ); 49 | }; 50 | 51 | export default SwitchWidget; 52 | -------------------------------------------------------------------------------- /backend/funix/decorator/pre_fill.py: -------------------------------------------------------------------------------- 1 | from typing import Union 2 | 3 | from funix.hint import PreFillEmpty, PreFillType 4 | 5 | pre_fill_metadata: dict[str, list[str | int | PreFillEmpty]] = {} 6 | """ 7 | A dict, key is function ID, value is a list of indexes/keys of pre-fill parameters. 8 | """ 9 | 10 | 11 | def parse_pre_fill(pre_fill: PreFillType): 12 | global pre_fill_metadata 13 | for _, from_arg_function_info in pre_fill.items(): 14 | if isinstance(from_arg_function_info, tuple): 15 | from_arg_function_address = str(id(from_arg_function_info[0])) 16 | from_arg_function_index_or_key = from_arg_function_info[1] 17 | if from_arg_function_address in pre_fill_metadata: 18 | pre_fill_metadata[from_arg_function_address].append( 19 | from_arg_function_index_or_key 20 | ) 21 | else: 22 | pre_fill_metadata[from_arg_function_address] = [ 23 | from_arg_function_index_or_key 24 | ] 25 | else: 26 | from_arg_function_address = str(id(from_arg_function_info)) 27 | if from_arg_function_address in pre_fill_metadata: 28 | pre_fill_metadata[from_arg_function_address].append(PreFillEmpty) 29 | else: 30 | pre_fill_metadata[from_arg_function_address] = [PreFillEmpty] 31 | 32 | 33 | def get_pre_fill_metadata( 34 | function_id: str, 35 | ) -> Union[list[str | int | PreFillEmpty], None]: 36 | return pre_fill_metadata.get(function_id, None) 37 | -------------------------------------------------------------------------------- /frontend/src/components/FunixFunction/components/KetcherEditor.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Editor } from "ketcher-react"; 3 | import { Box, SxProps, Theme } from "@mui/material"; 4 | import "miew/dist/miew.min.css"; 5 | import "ketcher-react/dist/index.css"; 6 | import { 7 | useKetcherEditor, 8 | UseKetcherEditorOptions, 9 | } from "../hooks/useKetcherEditor"; 10 | 11 | declare global { 12 | interface Window { 13 | ketcher: any; 14 | } 15 | } 16 | 17 | export interface KetcherEditorProps extends UseKetcherEditorOptions { 18 | width?: string | number; 19 | height?: string | number; 20 | sx?: SxProps; 21 | staticResourcesUrl?: string; 22 | errorHandler?: (error: any) => void; 23 | } 24 | 25 | const KetcherEditor: React.FC = React.memo((props) => { 26 | const { 27 | width = "100%", 28 | height = "700px", 29 | sx, 30 | staticResourcesUrl = "", 31 | errorHandler = () => {}, 32 | ...hookOptions 33 | } = props; 34 | 35 | const { structServiceProvider, handleInit } = useKetcherEditor(hookOptions); 36 | 37 | return ( 38 | 46 | 52 | 53 | ); 54 | }); 55 | 56 | KetcherEditor.displayName = "KetcherEditor"; 57 | 58 | export default KetcherEditor; 59 | -------------------------------------------------------------------------------- /examples/themes/theme_showroom.py: -------------------------------------------------------------------------------- 1 | from funix import funix 2 | from funix.hint import StrCode 3 | import IPython 4 | 5 | 6 | description = """ 7 | This a markdown playground in the {theme} mode theme. You can write markdown here and see the result on the right. 8 | """ 9 | 10 | 11 | @funix( 12 | title="Dark", 13 | description=description.format(theme="dark"), 14 | theme="./dark_test.json", 15 | show_source=True, 16 | ) 17 | def theme_test_markdown_playground( 18 | markdown: StrCode("markdown"), 19 | ) -> IPython.display.Markdown: 20 | return markdown 21 | 22 | 23 | @funix( 24 | title="Sunset", 25 | description=description.format(theme="sunset"), 26 | theme="./sunset.json", 27 | show_source=True, 28 | ) 29 | def theme_sunset(markdown: StrCode("markdown")) -> IPython.display.Markdown: 30 | return markdown 31 | 32 | 33 | @funix( 34 | title="Pink", 35 | description=description.format(theme="pink"), 36 | theme="./pink.json", 37 | show_source=True, 38 | ) 39 | def theme_test_pink(markdown: StrCode("markdown")) -> IPython.display.Markdown: 40 | return markdown 41 | 42 | 43 | @funix( 44 | title="Comic Sans", 45 | description=description.format(theme="Comic Sans"), 46 | theme="./comic_sans.json", 47 | show_source=True, 48 | ) 49 | def theme_comic_sans(markdown: StrCode("markdown")) -> IPython.display.Markdown: 50 | return markdown 51 | 52 | 53 | @funix( 54 | title="Kanagawa", 55 | description=description.format(theme="Kanagawa"), 56 | theme="./kanagawa.json", 57 | show_source=True, 58 | ) 59 | def theme_kanagawa(markdown: StrCode("markdown")) -> IPython.display.Markdown: 60 | return markdown 61 | -------------------------------------------------------------------------------- /backend/funix/util/uri.py: -------------------------------------------------------------------------------- 1 | """ 2 | URI utilities 3 | """ 4 | from typing import Optional 5 | from urllib.parse import urlparse 6 | 7 | from funix.config import banned_function_name_and_path 8 | 9 | 10 | def is_valid_uri(uri: str) -> bool: 11 | """ 12 | Check if a URI is valid. 13 | Your URI must have a scheme, a netloc, and a path. 14 | For example: `https://example.com/`. 15 | 16 | Parameters: 17 | uri (str): The URI. 18 | 19 | Returns: 20 | bool: If the URI is valid. 21 | """ 22 | try: 23 | result = urlparse(uri) 24 | return all([result.scheme, result.netloc, result.path]) 25 | except: 26 | return False 27 | 28 | 29 | def get_endpoint( 30 | path: Optional[str], unique_function_name: Optional[str], function_name: str 31 | ) -> str: 32 | """ 33 | Get the endpoint of a function. 34 | If the path is not provided, the unique function name or the function name will be used. 35 | If the path is provided, the path will be used. 36 | 37 | Parameters: 38 | path (str | None): The path. 39 | unique_function_name (str | None): The unique function name. 40 | function_name (str): The function name. 41 | 42 | Returns: 43 | str: The endpoint. 44 | 45 | Raises: 46 | Exception: If the path is not allowed. 47 | """ 48 | if not path: 49 | if unique_function_name: 50 | return unique_function_name 51 | else: 52 | return function_name 53 | else: 54 | if path in banned_function_name_and_path: 55 | raise Exception(f"{function_name}'s path: {path} is not allowed") 56 | return path.strip("/") 57 | -------------------------------------------------------------------------------- /backend/funix/build/static/js/264.dfcca1b2.chunk.js.LICENSE.txt: -------------------------------------------------------------------------------- 1 | /*! 2 | 3 | JSZip v3.10.1 - A JavaScript class for generating and reading zip files 4 | 5 | 6 | (c) 2009-2016 Stuart Knightley 7 | Dual licenced under the MIT license or GPLv3. See https://raw.github.com/Stuk/jszip/main/LICENSE.markdown. 8 | 9 | JSZip uses the library pako released under the MIT license : 10 | https://github.com/nodeca/pako/blob/main/LICENSE 11 | */ 12 | 13 | /*! 14 | * The buffer module from node.js, for the browser. 15 | * 16 | * @author Feross Aboukhadijeh 17 | * @license MIT 18 | */ 19 | 20 | /*! ExcelJS 19-10-2023 */ 21 | 22 | /*! ieee754. BSD-3-Clause License. Feross Aboukhadijeh */ 23 | 24 | /*! safe-buffer. MIT License. Feross Aboukhadijeh */ 25 | 26 | /** 27 | * Character class utilities for XML NS 1.0 edition 3. 28 | * 29 | * @author Louis-Dominique Dubeau 30 | * @license MIT 31 | * @copyright Louis-Dominique Dubeau 32 | */ 33 | 34 | /** 35 | * Character classes and associated utilities for the 2nd edition of XML 1.1. 36 | * 37 | * @author Louis-Dominique Dubeau 38 | * @license MIT 39 | * @copyright Louis-Dominique Dubeau 40 | */ 41 | 42 | /** 43 | * Character classes and associated utilities for the 5th edition of XML 1.0. 44 | * 45 | * @author Louis-Dominique Dubeau 46 | * @license MIT 47 | * @copyright Louis-Dominique Dubeau 48 | */ 49 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "funix" 3 | version = "0.6.4" 4 | authors = [ 5 | {name = "Textea Inc.", email = "forrestbao@gmail.com"} 6 | ] 7 | license = {file = "LICENSE"} 8 | description = "Building web apps without manually creating widgets" 9 | readme = "README.md" 10 | requires-python = ">=3.10" 11 | classifiers = [ 12 | "Development Status :: 4 - Beta", 13 | "License :: OSI Approved :: MIT License", 14 | "Programming Language :: Python :: 3.10", 15 | "Programming Language :: Python :: 3.11", 16 | "Operating System :: OS Independent", 17 | "Framework :: Flask", 18 | "Framework :: Matplotlib" 19 | ] 20 | dependencies = [ 21 | "flask>=2.2.2", 22 | "functions-framework==3.*", 23 | "requests>=2.28.1", 24 | "plac>=1.3.5", 25 | "gitignore-parser>=0.1.9", 26 | "flask-sock>=0.7.0", 27 | "SQLAlchemy>=2.0.23", 28 | "matplotlib>=3.4.3", 29 | "pandas>=2.0.3", 30 | "docstring_parser>=0.16", 31 | "tornado>=6.4.2", 32 | "pydantic>=2.10.6", 33 | "GitPython>=3.1.31", 34 | ] 35 | 36 | [project.optional-dependencies] 37 | pendera = [ 38 | "pandera>=0.17.2", 39 | ] 40 | ipython = [ 41 | "IPython>=8.14.0", 42 | "ipywidgets>=8.0.7", 43 | ] 44 | all = [ 45 | "IPython>=8.14.0", 46 | "ipywidgets>=8.0.7", 47 | "pandera>=0.17.2", 48 | ] 49 | 50 | [project.urls] 51 | homepage = "https://github.com/TexteaInc/funix" 52 | 53 | [project.scripts] 54 | funix = "funix.__main__:cli_main" 55 | 56 | # [tool.pyright] 57 | # include = ["src"] 58 | # exclude = ["**/node_modules", "**/__pycache__"] 59 | # strict = ["src"] 60 | # typeCheckingMode = "strict" 61 | 62 | # useLibraryCodeForTypes = true 63 | # reportMissingImports = true 64 | # reportMissingTypeStubs = false 65 | 66 | [tool.setuptools.packages.find] 67 | where = ["backend"] 68 | -------------------------------------------------------------------------------- /frontend/src/components/FunixFunction/OutputComponents/OutputRedirectButton.tsx: -------------------------------------------------------------------------------- 1 | import { useAtom } from "jotai"; 2 | import { callableDefaultAtom } from "../../../store"; 3 | import { useNavigate } from "react-router-dom"; 4 | import Button from "@mui/material/Button"; 5 | import { useCallback, useEffect, useMemo } from "react"; 6 | 7 | type RedirectButtonResponse = { 8 | path: string; 9 | args: string; 10 | text: string; 11 | auto_direct: boolean; 12 | }; 13 | 14 | interface OutputRedirectButtonProps { 15 | response: RedirectButtonResponse; 16 | } 17 | 18 | const OutputRedirectButton = (props: OutputRedirectButtonProps) => { 19 | const [callableDefault, setCallableDefault] = useAtom(callableDefaultAtom); 20 | const navigate = useNavigate(); 21 | const { response } = props; 22 | 23 | const parsedArgs = useMemo(() => { 24 | try { 25 | return JSON.parse(response.args); 26 | } catch (error) { 27 | console.error("Failed to parse args:", error); 28 | return {}; 29 | } 30 | }, [response.args]); 31 | 32 | const toPath = useCallback(() => { 33 | const newCallableDefault = { ...callableDefault }; 34 | newCallableDefault[response.path] = parsedArgs; 35 | setCallableDefault(newCallableDefault); 36 | navigate(response.path); 37 | }, [ 38 | callableDefault, 39 | parsedArgs, 40 | response.path, 41 | setCallableDefault, 42 | navigate, 43 | ]); 44 | 45 | useEffect(() => { 46 | if (response.auto_direct) { 47 | toPath(); 48 | } 49 | }, [response.auto_direct, toPath]); 50 | 51 | return ( 52 | 55 | ); 56 | }; 57 | 58 | export default OutputRedirectButton; 59 | -------------------------------------------------------------------------------- /frontend/src/components/FunixFunction/OutputComponents/OutputPlot.tsx: -------------------------------------------------------------------------------- 1 | import { Box } from "@mui/material"; 2 | import { useRef } from "react"; 3 | 4 | type PlotCode = { 5 | fig: number | string; 6 | }; 7 | 8 | export default function OutputPlot(props: { 9 | plotCode: PlotCode; 10 | indexId: string; 11 | backend: URL; 12 | }) { 13 | const lock = useRef(false); 14 | return ( 15 | 25 |
{ 32 | if (ref) { 33 | if (lock.current) { 34 | return; 35 | } 36 | lock.current = true; 37 | const websocket = 38 | (props.backend.protocol === "https:" ? "wss" : "ws") + 39 | "://" + 40 | props.backend.host + 41 | "/ws-plot/" + 42 | props.plotCode.fig; 43 | 44 | // @ts-expect-error that's good here 45 | new mpl.figure( 46 | props.plotCode.fig, 47 | new WebSocket(websocket), 48 | (figure: any, format: string) => { 49 | window.open( 50 | new URL( 51 | `/plot-download/${props.plotCode.fig}/${format}`, 52 | props.backend, 53 | ), 54 | ); 55 | }, 56 | ref, 57 | ); 58 | } 59 | }} 60 | /> 61 | 62 | ); 63 | } 64 | -------------------------------------------------------------------------------- /frontend/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | 12 | 13 | 17 | 18 | 22 | 23 | 26 | 41 | Funix 42 | 43 | 44 | 45 |
46 | 47 | 48 | -------------------------------------------------------------------------------- /frontend/src/components/FunixFunctionSelected.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Alert, 3 | Card, 4 | CardContent, 5 | CircularProgress, 6 | Stack, 7 | Typography, 8 | } from "@mui/material"; 9 | import React, { Suspense, useState } from "react"; 10 | import { useAtom } from "jotai"; 11 | import FunixFunction from "./FunixFunction"; 12 | import { useLocation } from "react-router-dom"; 13 | import _ from "lodash"; 14 | import { selectedFunctionAtom } from "../store"; 15 | 16 | export type FunctionSelectedProps = { 17 | backend: URL; 18 | }; 19 | 20 | const FunixFunctionSelected: React.FC = ({ 21 | backend, 22 | }) => { 23 | const [selectedFunction] = useAtom(selectedFunctionAtom); 24 | const { pathname } = useLocation(); 25 | const pathParams = useState( 26 | pathname.split("/").filter((value) => value !== ""), 27 | ); 28 | 29 | if (pathParams[0].length !== 0 && !selectedFunction) { 30 | return Your function is not found.; 31 | } 32 | 33 | if (!selectedFunction) { 34 | return Select a function to start; 35 | } 36 | 37 | const suspenseFallback = ( 38 | 39 | 40 | 41 | 42 | Loading selected function … 43 | 44 | 45 | 46 | ); 47 | return ( 48 | 49 | 54 | 55 | ); 56 | }; 57 | 58 | export default React.memo(FunixFunctionSelected, (prevProps, nextProps) => { 59 | return _.isEqual(prevProps.backend, nextProps.backend); 60 | }); 61 | -------------------------------------------------------------------------------- /examples/AI/huggingface.py: -------------------------------------------------------------------------------- 1 | # Building a one-turn chatbot from any causal language model hosted on HuggingFace using free Inference API 2 | 3 | # Copyleft 2023 Forrest Sheng Bao http://forrestbao.github.io 4 | # The purpose of this code is to demonstrate the use of Funix to turn a simple API call function to a web app. 5 | 6 | # To turn this code into a web app, run the following command in the terminal: 7 | # funix huggingface.py -l # the -l flag is very important. It tells Funix to load the function as a web app. 8 | 9 | import os, json, typing # Python's native 10 | import requests # pip install requests 11 | import ipywidgets 12 | 13 | # API_TOKEN = os.getenv("HF_TOKEN") # "Please set your API token as an environment variable named HF_TOKEN. You can get your token from https://huggingface.co/settings/token" 14 | 15 | import funix 16 | 17 | 18 | @funix.funix( 19 | description="""Talk to LLMs hosted at HuggingFace. A HuggingFace token needs to be set in the environment variable HF_TOKEN.""", 20 | # rate_limit=funix.decorator.Limiter.session(max_calls=20, period=60*60*24), 21 | ) 22 | def huggingface( 23 | model_name: typing.Literal[ 24 | "gpt2", 25 | "bigcode/starcoder", 26 | "google/flan-t5-base"] = "gpt2", 27 | prompt: str = "Who is Einstein?", 28 | HuggingFace_API_TOKEN: ipywidgets.Password = None 29 | ) -> str: 30 | 31 | payload = {"inputs": prompt, "max_tokens":200} # not all models use this query and output formats. Hence, we limit the models above. 32 | 33 | API_URL = f"https://api-inference.huggingface.co/models/{model_name}" 34 | headers = {"Authorization": f"Bearer {HuggingFace_API_TOKEN.value}"} 35 | 36 | response = requests.post(API_URL, headers=headers, json=payload) 37 | 38 | if "error" in response.json(): 39 | return response.json()["error"] 40 | else: 41 | return response.json()[0]["generated_text"] 42 | -------------------------------------------------------------------------------- /examples/table_and_plot.py: -------------------------------------------------------------------------------- 1 | from typing import List # Python native 2 | 3 | import matplotlib.figure, matplotlib.pyplot # the de facto standard for plotting in Python 4 | 5 | import funix 6 | 7 | # @funix.funix(widgets={"a": "sheet", "b": ["sheet", "slider[0,5,0.2]"]}) 8 | # def table_plot( 9 | # a: List[int] = [5, 17, 29], b: List[float] = [3.1415, 2.6342, 1.98964] 10 | # ) -> Figure: 11 | # fig = matplotlib.pyplot.figure() 12 | # matplotlib.pyplot.plot(a, b) 13 | # return fig 14 | 15 | 16 | # New Implementation after FEP 11 17 | 18 | import pandas # the de facto standard for tabular data in Python 19 | import pandera # the typing library for pandas dataframes 20 | 21 | 22 | # class InputSchema(pandera.DataFrameModel): 23 | # a: pandera.typing.Series[int] 24 | # b: pandera.typing.Series[float] 25 | 26 | 27 | # @funix.funix() 28 | # def table_and_plot( 29 | # df: pandera.typing.DataFrame[InputSchema] = pandas.DataFrame( 30 | # {"a": [5, 17, 29], "b": [3.1415, 2.6342, 1.98964]} # default values 31 | # ) 32 | # ) -> (matplotlib.figure.Figure, pandas.DataFrame): 33 | # fig = matplotlib.pyplot.figure() 34 | # matplotlib.pyplot.plot(df["a"], df["b"]) 35 | 36 | # output_data_frame = pandas.DataFrame( 37 | # {"a-b": df["a"] - df["b"], "a+b": df["a"] + df["b"]} 38 | # ) 39 | 40 | # return fig, output_data_frame 41 | 42 | # Latest implmentation after a discussion in Nov. 2023 43 | 44 | def table_and_plot( 45 | df: pandas.DataFrame = pandas.DataFrame( 46 | {"a": [5, 17, 29], "b": [3.1415, 2.6342, 1.98964]} # default values 47 | ) 48 | ) -> (matplotlib.figure.Figure, pandas.DataFrame): 49 | fig = matplotlib.pyplot.figure() 50 | matplotlib.pyplot.plot(df["a"], df["b"]) 51 | 52 | output_data_frame = pandas.DataFrame( 53 | {"a-b": df["a"] - df["b"], "a+b": df["a"] + df["b"]} 54 | ) 55 | 56 | return fig, output_data_frame -------------------------------------------------------------------------------- /frontend/src/Key.tsx: -------------------------------------------------------------------------------- 1 | import { DataGrid, DataGridProps } from "@mui/x-data-grid"; 2 | import { DataGridPro, DataGridProProps } from "@mui/x-data-grid-pro"; 3 | import { 4 | DataGridPremium, 5 | DataGridPremiumProps, 6 | } from "@mui/x-data-grid-premium"; 7 | import { LicenseInfo } from "@mui/x-license"; 8 | import { useGridApiRef as useGridApiRefNormal } from "@mui/x-data-grid"; 9 | import { useGridApiRef as useGridApiRefPro } from "@mui/x-data-grid-pro"; 10 | import { useGridApiRef as useGridApiRefPremium } from "@mui/x-data-grid-premium"; 11 | 12 | type MUI_LICENSE = { 13 | pro?: string; 14 | premium?: string; 15 | }; 16 | 17 | const LICENSE_KEY: MUI_LICENSE = { 18 | pro: process.env.REACT_APP_MUI_PRO_LICENSE_KEY, 19 | premium: process.env.REACT_APP_MUI_PERMIUM_LICENSE_KEY, 20 | }; 21 | 22 | const registerLicense = () => { 23 | if (LICENSE_KEY.premium) { 24 | LicenseInfo.setLicenseKey(LICENSE_KEY.premium); 25 | } else if (LICENSE_KEY.pro) { 26 | LicenseInfo.setLicenseKey(LICENSE_KEY.pro); 27 | } 28 | }; 29 | 30 | const WrappedDataGrid = ( 31 | props: DataGridProps & DataGridProProps & DataGridPremiumProps, 32 | ) => { 33 | return LICENSE_KEY.premium ? ( 34 | 40 | ) : LICENSE_KEY.pro ? ( 41 | 47 | ) : ( 48 | 54 | ); 55 | }; 56 | 57 | const useGridApiRef = LICENSE_KEY.premium 58 | ? useGridApiRefPremium 59 | : LICENSE_KEY.pro 60 | ? useGridApiRefPro 61 | : useGridApiRefNormal; 62 | 63 | export { WrappedDataGrid as DataGrid, registerLicense, useGridApiRef }; 64 | -------------------------------------------------------------------------------- /frontend/eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import eslintPluginPrettierRecommended from "eslint-plugin-prettier/recommended"; 2 | import eslint from "@eslint/js"; 3 | import tseslint from "typescript-eslint"; 4 | 5 | // export default tseslint.config({ 6 | // rules: { 7 | // "@typescript-eslint/interface-name-prefix": "off", 8 | // "@typescript-eslint/explicit-function-return-type": "off", 9 | // "@typescript-eslint/explicit-module-boundary-types": "off", 10 | // "@typescript-eslint/no-explicit-any": "off", 11 | // "@typescript-eslint/no-unused-vars": [ 12 | // "error", 13 | // { argsIgnorePattern: "^_" } 14 | // ] 15 | // }, 16 | // extends: [ 17 | // eslint.configs.recommended, 18 | // ...tseslint.configs.recommended, 19 | // eslintPluginPrettierRecommended, 20 | // ], 21 | // languageOptions: { 22 | // parserOptions: { 23 | // project: "tsconfig.json", 24 | // tsConfigRootDir: import.meta.dirname, 25 | // }, 26 | // }, 27 | // ignores: [ 28 | // "node_modules/", 29 | // "dist/", 30 | // "build/", 31 | // "package.json", 32 | // "public", 33 | // ".yarn", 34 | // ], 35 | // }); 36 | 37 | export default tseslint.config( 38 | eslint.configs.recommended, 39 | ...tseslint.configs.recommended, 40 | eslintPluginPrettierRecommended, 41 | { 42 | rules: { 43 | "@typescript-eslint/interface-name-prefix": "off", 44 | "@typescript-eslint/explicit-function-return-type": "off", 45 | "@typescript-eslint/explicit-module-boundary-types": "off", 46 | "@typescript-eslint/no-explicit-any": "off", 47 | "@typescript-eslint/no-unused-vars": [ 48 | "error", 49 | { 50 | argsIgnorePattern: "^_", 51 | varsIgnorePattern: "^_", 52 | }, 53 | ], 54 | }, 55 | ignores: [ 56 | "node_modules/", 57 | "dist/", 58 | "build/", 59 | "package.json", 60 | "public", 61 | ".yarn", 62 | ], 63 | }, 64 | ); 65 | -------------------------------------------------------------------------------- /frontend/src/store.ts: -------------------------------------------------------------------------------- 1 | import { atom } from "jotai"; 2 | import { FunctionPreview, PostCallResponse } from "./shared"; 3 | import { History } from "./shared/useFunixHistory"; 4 | 5 | export type LastStore = { 6 | input: Record; 7 | output: PostCallResponse | string; 8 | }; 9 | 10 | // atomWithStorage("saveHistory", true); 11 | 12 | const fromLocalStorage = () => { 13 | let saveHistory = localStorage.getItem("saveHistory"); 14 | if (saveHistory === null) { 15 | saveHistory = "true"; 16 | localStorage.setItem("saveHistory", saveHistory); 17 | } 18 | 19 | let showFunctionDetail = localStorage.getItem("showFunctionDetail"); 20 | if (showFunctionDetail === null) { 21 | showFunctionDetail = "false"; 22 | localStorage.setItem("showFunctionDetail", showFunctionDetail); 23 | } 24 | 25 | return { 26 | saveHistory: saveHistory === "true", 27 | showFunctionDetail: showFunctionDetail === "true", 28 | }; 29 | }; 30 | 31 | export const selectedFunctionAtom = atom(null); 32 | export const functionsAtom = atom(null); 33 | export const themeAtom = atom>(null); 34 | export const showFunctionDetailAtom = atom( 35 | fromLocalStorage().showFunctionDetail, 36 | ); 37 | export const viewTypeAtom = atom<"json" | "sheet">("json"); 38 | export const functionSecretAtom = atom>({}); 39 | export const backendAtom = atom(null); 40 | export const backHistoryAtom = atom(null); 41 | export const backConsensusAtom = atom([false, false, false]); 42 | export const saveHistoryAtom = atom(fromLocalStorage().saveHistory); 43 | export const appSecretAtom = atom(null); 44 | export const historiesAtom = atom([]); 45 | export const lastAtom = atom>({}); 46 | export const showFunctionTitleAtom = atom(false); 47 | export const callableDefaultAtom = atom>>({}); 48 | -------------------------------------------------------------------------------- /frontend/src/shared/theme.ts: -------------------------------------------------------------------------------- 1 | import { Theme } from "@mui/material"; 2 | import _ from "lodash"; 3 | 4 | const defaultLightTheme = { 5 | components: { 6 | MuiAppBar: { 7 | styleOverrides: { 8 | root: { 9 | backgroundColor: "#ffffff", 10 | color: "inherit", 11 | boxShadow: 12 | "0px 2px 4px -1px rgba(25,118,210,0.2),0px 4px 5px 0px rgba(25,118,210,0.14),0px 1px 10px 0px rgba(25,118,210,0.12)", 13 | }, 14 | }, 15 | }, 16 | }, 17 | }; 18 | 19 | const defaultDarkTheme = { 20 | palette: { 21 | type: "dark", 22 | }, 23 | }; 24 | 25 | const mergeIcon = (theme: Record): Theme => { 26 | const url = theme.funix_icon as string | false; 27 | if (!url) { 28 | return theme as unknown as Theme; 29 | } 30 | 31 | const iconConfig = { 32 | content: '""', 33 | "background-image": `url(${url})`, 34 | display: "block", 35 | "background-size": "contain", 36 | "background-repeat": "no-repeat", 37 | "background-position": "left", 38 | "margin-right": "10px", 39 | height: theme.funix_icon_height || "100%", 40 | width: theme.funix_icon_width || "100%", 41 | }; 42 | 43 | return _.merge(theme, { 44 | components: { 45 | MuiAppBar: { 46 | styleOverrides: { 47 | root: { 48 | "& .MuiToolbar-root:before": iconConfig, 49 | }, 50 | }, 51 | }, 52 | }, 53 | }) as unknown as Theme; 54 | }; 55 | 56 | const mergeTheme = (theme: Record | undefined): Theme => { 57 | if (!theme) { 58 | return defaultLightTheme as unknown as Theme; 59 | } 60 | const mode = theme?.palette?.type === "dark" ? "dark" : "light"; 61 | let modifiedTheme = theme; 62 | modifiedTheme = mergeIcon(modifiedTheme); 63 | 64 | return mode === "dark" 65 | ? (_.merge(defaultDarkTheme, modifiedTheme) as unknown as Theme) 66 | : (_.merge(defaultLightTheme, modifiedTheme) as unknown as Theme); 67 | }; 68 | 69 | export default mergeTheme; 70 | -------------------------------------------------------------------------------- /frontend/src/components/FunixFunction/OutputComponents/OutputMedias.tsx: -------------------------------------------------------------------------------- 1 | import { Card, CardMedia } from "@mui/material"; 2 | import PDFViewer from "../../Common/PDFViewer"; 3 | 4 | type FileType = string | File; 5 | 6 | export default function OutputMedias(props: { 7 | medias: FileType[] | FileType; 8 | type: string; 9 | backend: string; 10 | outline?: boolean; 11 | }) { 12 | const medias = Array.isArray(props.medias) ? props.medias : [props.medias]; 13 | 14 | const component = 15 | props.type.toLowerCase().startsWith("image") || 16 | props.type.toLowerCase().startsWith("figure") 17 | ? "img" 18 | : props.type.toLowerCase().startsWith("video") 19 | ? "video" 20 | : props.type.toLowerCase().startsWith("application/pdf") 21 | ? "pdf" 22 | : "audio"; 23 | 24 | return ( 25 | <> 26 | {medias.map((media, index) => { 27 | const relativeMedia = 28 | typeof media === "string" 29 | ? media.startsWith("/file/") 30 | ? new URL(media, props.backend).toString() 31 | : media 32 | : URL.createObjectURL(media); 33 | 34 | const isPDF = component === "pdf" || relativeMedia.endsWith(".pdf"); 35 | 36 | const sx = isPDF 37 | ? { 38 | width: "100%", 39 | height: "100%", 40 | overflow: "auto", 41 | } 42 | : { 43 | width: "100%", 44 | height: "auto", 45 | maxWidth: "100%", 46 | maxHeight: "100%", 47 | }; 48 | 49 | return ( 50 | 55 | {isPDF ? ( 56 | 57 | ) : ( 58 | 59 | )} 60 | 61 | ); 62 | })} 63 | 64 | ); 65 | } 66 | -------------------------------------------------------------------------------- /examples/pydantic_test.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel, Field 2 | from typing import Optional 3 | from datetime import datetime 4 | from funix import funix, pydantic_ui, generate_redirect_link, generate_redirect_button 5 | from funix.hint import RedirectButton 6 | 7 | 8 | @pydantic_ui( 9 | title="model", 10 | layout=[ 11 | [{"argument": "url", "width": 0.5}, {"argument": "display", "width": 0.5}], 12 | ], 13 | ) 14 | class URLModel(BaseModel): 15 | url: str = Field( 16 | ..., 17 | description="A valid URL that points to a resource.", 18 | ) 19 | display: bool = False 20 | 21 | 22 | @pydantic_ui( 23 | widgets={ 24 | "age": "slider[0, 100]", 25 | "password": "password", 26 | } 27 | ) 28 | class SingleModel(BaseModel): 29 | name: str 30 | password: str 31 | age: int 32 | weight: float 33 | signup_date: Optional[datetime] 34 | bio: str = Field( 35 | default="No bio provided", description="A short biography of the user." 36 | ) 37 | links: list[URLModel] = Field(description="A list of URL objects.") 38 | 39 | 40 | class PushReason(BaseModel): 41 | user: str 42 | reason: str = Field() 43 | other_model: SingleModel 44 | 45 | 46 | def where_is_the_datetime( 47 | t: PushReason = PushReason( 48 | user="shionsan", 49 | reason="Please", 50 | other_model=SingleModel( 51 | name="shionsan", 52 | age=1, 53 | password="123456", 54 | weight=2.0, 55 | signup_date=datetime.now(), 56 | bio="No bio provided", 57 | links=[URLModel(url="https://www.google.com", display=True)], 58 | ), 59 | ) 60 | ) -> str: 61 | return f"User: {t.user}, Reason: {t.reason}, Time: {datetime.now()}" 62 | 63 | 64 | def list_test(a: SingleModel) -> str: 65 | return f"User: {a.user}, Reason: {a.reason}, Time: {datetime.now()}" 66 | 67 | @funix(next_to=list_test) 68 | def test_generate_redirect_button_2(): 69 | return {"a": {"links": [{"url": "https://www.google.com", "display": True}]}} 70 | 71 | -------------------------------------------------------------------------------- /frontend/src/components/FunixFunction/OutputComponents/OutputFiles.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | IconButton, 3 | Paper, 4 | Table, 5 | TableBody, 6 | TableCell, 7 | TableContainer, 8 | TableRow, 9 | } from "@mui/material"; 10 | import { FileDownload } from "@mui/icons-material"; 11 | import PDFViewer from "../../Common/PDFViewer"; 12 | 13 | export default function OutputFiles(props: { 14 | files: string[] | string; 15 | backend: string; 16 | }) { 17 | const files = Array.isArray(props.files) ? props.files : [props.files]; 18 | 19 | return ( 20 | 21 | 27 | 28 | {files.map((file, index) => { 29 | const relativeFile = file.startsWith("/file/") 30 | ? new URL(file, props.backend).toString() 31 | : file; 32 | 33 | const isPDF = relativeFile.endsWith(".pdf"); 34 | 35 | if (isPDF) { 36 | return ; 37 | } 38 | 39 | return ( 40 | 41 | 48 | { 51 | const link = document.createElement("a"); 52 | link.download = ""; 53 | link.href = relativeFile; 54 | link.click(); 55 | }} 56 | > 57 | 58 | 59 | 60 | 61 | {relativeFile} 62 | 63 | 64 | ); 65 | })} 66 | 67 |
68 |
69 | ); 70 | } 71 | -------------------------------------------------------------------------------- /backend/funix/config/switch.py: -------------------------------------------------------------------------------- 1 | """ 2 | This file contains some shared config for debug and production. 3 | It's a global switch for quick turning on/off some key features in Funix. 4 | """ 5 | 6 | from secrets import token_hex 7 | from sys import modules 8 | from typing import Union 9 | 10 | 11 | class SwitchOption: 12 | """Switch option.""" 13 | 14 | AUTO_CONVERT_UNDERSCORE_TO_SPACE_IN_NAME: bool = True 15 | """Auto convert underscore to space in name""" 16 | 17 | DISABLE_FUNCTION_LIST_CACHE: bool = False 18 | """No cache for function list""" 19 | 20 | AUTO_READ_DOCSTRING_TO_FUNCTION_DESCRIPTION: bool = True 21 | """Auto read docstring to function description""" 22 | 23 | AUTO_READ_DOCSTRING_TO_PARSE: bool = True 24 | """Auto parse docstring to function widgets and description""" 25 | 26 | DOCSTRING_TO_ARGUMENT_HELP: bool = True 27 | """Auto parse docstring to function argument help""" 28 | 29 | DOCSTRING_FIRST_LINE_TO_TITLE: bool = False 30 | """Auto parse docstring first line to function title""" 31 | 32 | USE_FIXED_SESSION_KEY: Union[bool, str] = False 33 | """Use fixed session key""" 34 | 35 | FILE_LINK_EXPIRE_TIME: int = 60 * 60 36 | """The file link expire time (seconds), -1 for never expire""" 37 | 38 | BIGGER_DATA_SAVE_TO_TEMP: int = 1024 * 1024 * 10 39 | """The bigger data size to save to temp (bytes), -1 for always in memory""" 40 | 41 | NOTEBOOK_AUTO_EXECUTION: bool = False 42 | """In notebook, auto run the flask app""" 43 | 44 | AUTO_INPUTBOX_ARGS_NUMBER: int = 8 45 | """Args above this number are automatically changed to inputbox.""" 46 | 47 | __session_key = None 48 | 49 | @property 50 | def in_notebook(self) -> bool: 51 | """Whether in notebook.""" 52 | return "ipykernel" in modules and self.NOTEBOOK_AUTO_EXECUTION 53 | 54 | def get_session_key(self) -> str: 55 | """Get the session key.""" 56 | if self.__session_key is not None: 57 | return self.__session_key 58 | 59 | if isinstance(self.USE_FIXED_SESSION_KEY, str): 60 | self.__session_key = self.USE_FIXED_SESSION_KEY 61 | return self.USE_FIXED_SESSION_KEY 62 | 63 | self.__session_key = token_hex(16) 64 | return self.__session_key 65 | 66 | 67 | GlobalSwitchOption = SwitchOption() 68 | -------------------------------------------------------------------------------- /backend/funix/config/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Shared config. 3 | """ 4 | 5 | supported_basic_types_dict = { 6 | "int": "integer", 7 | "float": "number", 8 | "str": "string", 9 | "bool": "boolean", 10 | } 11 | """ 12 | A dict, key is the basic type name, value is the type name in frontend (and for yodas right?). 13 | """ 14 | 15 | supported_basic_types = list(supported_basic_types_dict.keys()) 16 | """ 17 | A list, contains the basic type names. 18 | """ 19 | 20 | supported_basic_file_types = ["Images", "Videos", "Audios", "Files"] 21 | """ 22 | A list, contains the basic file type names. 23 | """ 24 | 25 | supported_upload_widgets = ["image", "video", "audio", "file"] 26 | """ 27 | A list, contains the upload widget names. 28 | """ 29 | 30 | banned_function_name_and_path = ["list", "file", "static", "config", "param", "call", "update"] 31 | """ 32 | The banned function name and path. 33 | 34 | Reason: Funix has used these paths. 35 | """ 36 | 37 | basic_widgets = [ 38 | "slider", 39 | "input", 40 | "textField", 41 | "switch", 42 | "button", 43 | "checkbox", 44 | "radio", 45 | "datetime", 46 | "textarea", 47 | ] 48 | """ 49 | Basic widgets for MUI components. 50 | """ 51 | 52 | builtin_widgets = { 53 | "Literal": "radio", 54 | } 55 | """ 56 | A dict, key is the type name, value is the MUI component name. 57 | 58 | This is the funix builtin widgets. 59 | """ 60 | 61 | ipython_type_convert_dict = { 62 | "IPython.core.display.Markdown": "Markdown", 63 | "IPython.lib.display.Markdown": "Markdown", 64 | "IPython.core.display.HTML": "HTML", 65 | "IPython.lib.display.HTML": "HTML", 66 | "IPython.core.display.Javascript": "HTML", 67 | "IPython.lib.display.Javascript": "HTML", 68 | "IPython.core.display.Image": "Images", 69 | "IPython.lib.display.Image": "Images", 70 | "IPython.core.display.Audio": "Audios", 71 | "IPython.lib.display.Audio": "Audios", 72 | "IPython.core.display.Video": "Videos", 73 | "IPython.lib.display.Video": "Videos", 74 | } 75 | """ 76 | A dict, key is the IPython type name, value is the Funix type name. 77 | """ 78 | 79 | dataframe_convert_dict = { 80 | "pandera.typing.pandas.DataFrame": "Dataframe", 81 | "pandas.core.frame.DataFrame": "Dataframe", 82 | } 83 | """ 84 | A dict, key is the dataframe type name, value is the Funix type name. 85 | """ 86 | -------------------------------------------------------------------------------- /Funix_vs_them.md: -------------------------------------------------------------------------------- 1 | # Funix vs. them 2 | 3 | Here we will show how Funix and other solutions compare in building apps in Python transforming a Python function to a web app. 4 | 5 | 6 | ## Hello, world! 7 | 8 | In Funix, all you need is a simple function: 9 | 10 | ```py 11 | def hello(your_name: str) -> str: 12 | return f"Hello, {your_name}." 13 | ``` 14 | 15 | Run the magic command `funix hello.py -l` and you get a web app. 16 | 17 | In Gradio, this is the code ([source](https://www.gradio.app/docs/interface)): 18 | 19 | ```py 20 | import gradio as gr 21 | 22 | def hello(your_name: str) -> str: # type hint is optional 23 | return f"Hello, {your_name}." 24 | 25 | demo = gr.Interface(fn=hello, inputs="text", outputs="text") 26 | 27 | if __name__ == "__main__": 28 | demo.launch() 29 | ``` 30 | 31 | In Streamlit, this is the code: 32 | ```py 33 | 34 | import streamlit as st 35 | 36 | def hello(your_name: str) -> str: 37 | return f"Hello, {your_name}." 38 | 39 | col1, col2 = st.columns(2) 40 | 41 | with col1: 42 | your_name = st.text_input('Your name') 43 | 44 | with col2: 45 | st.write(hello(your_name)) 46 | ``` 47 | 48 | Then you need to run the command `streamlit run THIS_FILE.py` 49 | 50 | ### Takeaway 51 | Streamlit requires you to manually create widgets. It does not by default creates the input and output panel. So you write the most amount of code in Streamlit. Gradio leverages the types of `inputs` and `outputs`to pick the UI widgets. But Funix goes on step furthe by simply using Python's native type hint. 52 | 53 | 54 | ## Hangman 55 | 56 | Here is a comparison between Funix and Gradio in building a hangman game app. 57 | ![Hangman Funix vs Gradio](https://raw.githubusercontent.com/TexteaInc/funix-doc/main/screenshots/hangman_gradio_vs_funix.png) 58 | 59 | The resulting UI from Funix does not look too bad! 60 | ![Hangman in Funix](https://raw.githubusercontent.com/TexteaInc/funix-doc/main/screenshots/hangman.png) 61 | 62 | ### Takeaway 63 | Funix code is much shorter than Gradio one. In the Gradio's case, because a state variable `used_letters_var` is maintained and should not become a user input, the layout has to be manually arranged instead of being automatically generated. 64 | But Funix take a slightly risky approach by treating such a variable as a global variable (using Python's built-in keyword `global`) so it does not have to become part of the argument/input of the core function. 65 | -------------------------------------------------------------------------------- /examples/layout/layout_simple.py: -------------------------------------------------------------------------------- 1 | import typing 2 | 3 | import funix 4 | 5 | 6 | @funix.funix( 7 | widgets={"state": "select"}, 8 | input_layout=[ 9 | [{"markdown": "### Sender information"}], # row 1 10 | [ 11 | {"argument": "first_name", "width": 4}, 12 | {"argument": "last_name", "width": 4}, 13 | ], # row 2 14 | [{"argument": "address", "width": 7}], # row 3 15 | [ # row 4 16 | {"argument": "city", "width": 4}, 17 | {"argument": "state", "width": 4}, 18 | {"argument": "zip_code", "width": 3}, 19 | ], 20 | [{"html": "We love Funix"}], # row 5 21 | ], 22 | output_layout=[ 23 | [{"dividing": "zip code is "}], 24 | [{"return": 2}], 25 | [{"dividing": "from the town"}], 26 | [{"return": 0}, {"return": 1}], 27 | ], 28 | ) 29 | def layout_shipping( 30 | first_name: str = "Funix", 31 | last_name: str = "Rocks", 32 | address: str = "1 Freedom Way", 33 | city: str = "Pythonia", 34 | state: typing.Literal[ 35 | "ALABAMA", 36 | "ALASKA", 37 | "ARIZONA", 38 | "ARKANSAS", 39 | "CALIFORNIA", 40 | "COLORADO", 41 | "CONNECTICUT", 42 | "DELAWARE", 43 | "FLORIDA", 44 | "GEORGIA", 45 | "HAWAII", 46 | "IDAHO", 47 | "ILLINOIS", 48 | "INDIANA", 49 | "IOWA", 50 | "KANSAS", 51 | "KENTUCKY", 52 | "LOUISIANA", 53 | "MAINE", 54 | "MARYLAND", 55 | "MASSACHUSETTS", 56 | "MICHIGAN", 57 | "MINNESOTA", 58 | "MISSISSIPPI", 59 | "MISSOURI", 60 | "MONTANA", 61 | "NEBRASKA", 62 | "NEVADA", 63 | "NEW HAMPSHIRE", 64 | "NEW JERSEY", 65 | "NEW MEXICO", 66 | "NEW YORK", 67 | "NORTH CAROLINA", 68 | "NORTH DAKOTA", 69 | "OHIO", 70 | "OKLAHOMA", 71 | "OREGON", 72 | "PENNSYLVANIA", 73 | "RHODE ISLAND", 74 | "SOUTH CAROLINA", 75 | "SOUTH DAKOTA", 76 | "TENNESSEE", 77 | "TEXAS", 78 | "UTAH", 79 | "VERMONT", 80 | "VIRGINIA", 81 | "WASHINGTON", 82 | "WEST VIRGINIA", 83 | "WISCONSIN", 84 | "WYOMING", 85 | ] = "IOWA", 86 | zip_code: str = "1984", 87 | ) -> str: 88 | return f"You are {first_name} {last_name}, from the city of {city}, in the state of {state}." 89 | -------------------------------------------------------------------------------- /frontend/src/components/FunixFunction/hooks/useKetcherEditor.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useMemo, useState } from "react"; 2 | import { Ketcher } from "ketcher-core"; 3 | import { StandaloneStructServiceProvider } from "ketcher-standalone"; 4 | 5 | export type ChemEditorValue = { 6 | smiles: string; 7 | inchi: string | null; 8 | inchiAuxInfo: string | null; 9 | inchiKey: string | null; 10 | smarts: string; 11 | ket: string; 12 | }; 13 | 14 | export interface UseKetcherEditorOptions { 15 | initialValue?: ChemEditorValue | null; 16 | onChange?: (value: ChemEditorValue) => void; 17 | } 18 | 19 | export const useKetcherEditor = (options: UseKetcherEditorOptions = {}) => { 20 | const { initialValue, onChange } = options; 21 | 22 | const [value, setValue] = useState( 23 | initialValue ?? null, 24 | ); 25 | 26 | const syncSetValue = useCallback( 27 | (newValue: ChemEditorValue) => { 28 | setValue(newValue); 29 | onChange?.(newValue); 30 | }, 31 | [onChange], 32 | ); 33 | 34 | const structServiceProvider = useMemo( 35 | () => new StandaloneStructServiceProvider(), 36 | [], 37 | ); 38 | 39 | const handleInit = useCallback( 40 | (ketcher: Ketcher) => { 41 | if (window.ketcher) { 42 | ketcher.editor.clear(); 43 | ketcher.editor.clearHistory(); 44 | } 45 | window.ketcher = ketcher; 46 | 47 | if (initialValue) { 48 | ketcher.setMolecule(initialValue.ket); 49 | } 50 | 51 | ketcher.editor.subscribe("change", async () => { 52 | const ket = await ketcher.getKet(); 53 | if (ketcher.containsReaction()) { 54 | const smiles = await ketcher.getSmiles(); 55 | const smarts = await ketcher.getSmarts(); 56 | syncSetValue({ 57 | ket, 58 | smiles, 59 | inchi: null, 60 | inchiAuxInfo: null, 61 | inchiKey: null, 62 | smarts, 63 | }); 64 | } else { 65 | const smiles = await ketcher.getSmiles(); 66 | const inchi = await ketcher.getInchi(); 67 | const inchiAuxInfo = await ketcher.getInchi(true); 68 | const inchiKey = await ketcher.getInChIKey(); 69 | const smarts = await ketcher.getSmarts(); 70 | syncSetValue({ 71 | ket, 72 | smiles, 73 | inchi, 74 | inchiAuxInfo, 75 | inchiKey, 76 | smarts, 77 | }); 78 | } 79 | }); 80 | }, 81 | [initialValue, syncSetValue], 82 | ); 83 | 84 | return { 85 | value, 86 | setValue: syncSetValue, 87 | structServiceProvider, 88 | handleInit, 89 | }; 90 | }; 91 | -------------------------------------------------------------------------------- /backend/funix/session/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Control the global variables. 3 | """ 4 | 5 | from copy import deepcopy 6 | from typing import Any 7 | 8 | from flask import session 9 | 10 | UserID = str 11 | """ 12 | User ID, `__funix_id` in session. 13 | """ 14 | 15 | VariableName = str 16 | """ 17 | Global variable name. 18 | """ 19 | 20 | VariableValue = Any 21 | """ 22 | Global variable value. 23 | """ 24 | 25 | __funix_global_variables: dict[UserID, dict[VariableName, VariableValue]] = {} 26 | """ 27 | Funix global variables. 28 | 29 | Record the global variables of each user. 30 | """ 31 | 32 | __funix_default_global_variables: dict[VariableName, VariableValue] = {} 33 | """ 34 | Funix default global variables. 35 | 36 | Record the default global variables. 37 | """ 38 | 39 | 40 | def set_global_variable(name: str, value: Any) -> None: 41 | """ 42 | Set the global variable. 43 | 44 | Parameters: 45 | name (str): The global variable name. 46 | value (Any): The global variable value. 47 | 48 | Raises: 49 | RuntimeError: If the user id is not found in session. 50 | """ 51 | global __funix_global_variables 52 | user_id = session.get("__funix_id") 53 | if not user_id: 54 | raise RuntimeError("User ID not found in session.") 55 | if user_id not in __funix_global_variables: 56 | __funix_global_variables[user_id] = {} 57 | __funix_global_variables[user_id][name] = value 58 | 59 | 60 | def set_default_global_variable(name: str, value: Any) -> None: 61 | """ 62 | Set the default global variable. 63 | 64 | Parameters: 65 | name (str): The global variable name. 66 | value (Any): The global variable value. 67 | """ 68 | global __funix_default_global_variables 69 | __funix_default_global_variables[name] = value 70 | 71 | 72 | def get_global_variable(name: str) -> Any: 73 | """ 74 | Get the global variable. 75 | 76 | Parameters: 77 | name (str): The global variable name. 78 | 79 | Returns: 80 | Any: The global variable value. 81 | 82 | Raises: 83 | RuntimeError: If the user id is not found in session. 84 | """ 85 | global __funix_global_variables, __funix_default_global_variables 86 | user_id = session.get("__funix_id") 87 | if not user_id: 88 | raise RuntimeError("User ID not found in session.") 89 | if user_id not in __funix_global_variables: 90 | __funix_global_variables[user_id] = {} 91 | if name not in __funix_global_variables[user_id]: 92 | __funix_global_variables[user_id][name] = deepcopy( 93 | __funix_default_global_variables.get(name, None) 94 | ) 95 | return __funix_global_variables[user_id].get(name, None) 96 | -------------------------------------------------------------------------------- /examples/jchem.py: -------------------------------------------------------------------------------- 1 | import requests 2 | from funix.hint import ChemStr, Ketcher, Markdown 3 | from typing import List, Tuple 4 | import json 5 | 6 | def __data_to_complex(data: List[dict]) -> List[Tuple[Markdown, ChemStr]]: 7 | processed = [] 8 | for item in data: 9 | markdown = f"""## {item['molecule']} 10 | 11 | ```json 12 | {json.dumps(item['additionalData'], indent=2, ensure_ascii=False)} 13 | ``` 14 | """ 15 | 16 | chemstr = item["molecule"] 17 | processed.append((markdown, chemstr)) 18 | return processed 19 | 20 | def similarity( 21 | structure: Ketcher, 22 | similarity_threshold: float = 0.8, 23 | hit_count: int = 10, 24 | filter_tolds: List[int] = [], 25 | timeout_in_milliseconds: int = 1000 26 | ) -> List[Tuple[Markdown, ChemStr]]: 27 | params = { 28 | "structure": structure.smiles, 29 | "similarityThreshold": similarity_threshold, 30 | "hitCount": hit_count, 31 | } 32 | if filter_tolds: 33 | params["filterTolds"] = filter_tolds 34 | if timeout_in_milliseconds: 35 | params["timeoutInMilliseconds"] = timeout_in_milliseconds 36 | 37 | response = requests.get( 38 | "https://jchem-microservices.chemaxon.com/jwsdb/rest-v1/db/additional/demoTable/similarity", 39 | params=params, 40 | ) 41 | data = response.json() 42 | return __data_to_complex(data) 43 | 44 | def substructure( 45 | structure: Ketcher, 46 | hit_count: int = 10, 47 | with_fingerprint_distance: bool = False, 48 | stereo_search_on_marked_double_bond_only: bool = False, 49 | stereo_search_ingore_tetrahedral_stereo: bool = False, 50 | ignore_charge: bool = False, 51 | ignore_isotope: bool = False, 52 | filter_tolds: List[int] = [], 53 | timeout_in_milliseconds: int = 1000 54 | ) -> List[Tuple[Markdown, ChemStr]]: 55 | params = { 56 | "structure": structure.smiles, 57 | "hitCount": hit_count, 58 | "withFingerprintDistance": with_fingerprint_distance, 59 | "stereoSearchOnMarkedDoubleBondOnly": stereo_search_on_marked_double_bond_only, 60 | "stereoSearchIgnoreTetrahedralStereo": stereo_search_ingore_tetrahedral_stereo, 61 | "ignoreCharge": ignore_charge, 62 | "ignoreIsotope": ignore_isotope, 63 | } 64 | if filter_tolds: 65 | params["filterTolds"] = filter_tolds 66 | if timeout_in_milliseconds: 67 | params["timeoutInMilliseconds"] = timeout_in_milliseconds 68 | 69 | response = requests.get( 70 | "https://jchem-microservices.chemaxon.com/jwsdb/rest-v1/db/additional/demoTable/substructure", 71 | params=params, 72 | ) 73 | data = response.json() 74 | return __data_to_complex(data) 75 | -------------------------------------------------------------------------------- /frontend/src/components/History/HistoryUtils.tsx: -------------------------------------------------------------------------------- 1 | import { History } from "../../shared/useFunixHistory"; 2 | import { Step, StepLabel, Stepper } from "@mui/material"; 3 | import { Done, Error, Pending, QuestionMark } from "@mui/icons-material"; 4 | 5 | export enum HistoryEnum { 6 | // No input and/nor no output 7 | Unknown, 8 | // Just input 9 | Pending, 10 | // Input and output 11 | Done, 12 | // Error 13 | Error, 14 | } 15 | 16 | export type HistoryInfoProps = { 17 | status: HistoryEnum; 18 | hasSecret: boolean; 19 | }; 20 | 21 | export const getHistoryInfo = (history: History): HistoryInfoProps => { 22 | let status: HistoryEnum; 23 | 24 | if (history.input === null || history.input === undefined) { 25 | status = HistoryEnum.Unknown; 26 | } else { 27 | if (history.output === null || history.output === undefined) { 28 | status = HistoryEnum.Pending; 29 | } else { 30 | status = HistoryEnum.Done; 31 | } 32 | } 33 | 34 | const hasSecret = 35 | history.input !== null && history.input !== undefined 36 | ? "__funix_secret" in history.input 37 | : false; 38 | if (history.output !== null && history.output !== undefined) { 39 | try { 40 | let output = history.output; 41 | if (typeof output === "string") { 42 | output = JSON.parse(output); 43 | } 44 | if (typeof output === "object" && "error_body" in output) { 45 | status = HistoryEnum.Error; 46 | } 47 | } catch { 48 | // Do nothing 49 | } 50 | } 51 | return { 52 | status, 53 | hasSecret, 54 | }; 55 | }; 56 | 57 | export const FunixHistoryStepper = (props: { status: HistoryEnum }) => { 58 | const activeStep = 59 | props.status === HistoryEnum.Done || props.status === HistoryEnum.Error 60 | ? 2 61 | : props.status === HistoryEnum.Pending 62 | ? 1 63 | : 0; 64 | return ( 65 | 71 | 72 | Input 73 | 74 | 75 | 76 | Output 77 | 78 | 79 | ); 80 | }; 81 | 82 | export const getHistoryStatusColor = (status: HistoryEnum) => { 83 | return status === HistoryEnum.Done 84 | ? "success" 85 | : status === HistoryEnum.Error 86 | ? "error" 87 | : "primary"; 88 | }; 89 | 90 | export const getHistoryStatusIcon = (status: HistoryEnum) => { 91 | return status === HistoryEnum.Done ? ( 92 | 93 | ) : status === HistoryEnum.Error ? ( 94 | 95 | ) : status === HistoryEnum.Pending ? ( 96 | 97 | ) : ( 98 | 99 | ); 100 | }; 101 | -------------------------------------------------------------------------------- /frontend/config-overrides.js: -------------------------------------------------------------------------------- 1 | const ProgressBarPlugin = require("progress-bar-webpack-plugin"); 2 | const SaveRemoteFilePlugin = require("save-remote-file-webpack-plugin"); 3 | const webpack = require("webpack"); 4 | const CopyWebpackPlugin = require("copy-webpack-plugin"); 5 | const path = require("path"); 6 | 7 | const scripts = process.env.REACT_APP_IN_FUNIX 8 | ? ` 9 | 10 | 11 | 12 | ` 13 | : ` 14 | 15 | 16 | 17 | `; 18 | 19 | module.exports = function override(config) { 20 | if (process.env.MUI_PRO_LICENSE_KEY) { 21 | config.plugins.push( 22 | new webpack.DefinePlugin({ 23 | "process.env.REACT_APP_MUI_PRO_LICENSE_KEY": JSON.stringify( 24 | process.env.MUI_PRO_LICENSE_KEY, 25 | ), 26 | }), 27 | ); 28 | } 29 | 30 | config.module.rules.push({ 31 | test: /\.html$/i, 32 | type: "asset/source", 33 | }); 34 | config.module.rules.push({ 35 | test: /index\.html$/, 36 | loader: "string-replace-loader", 37 | options: { 38 | search: "", 39 | replace: scripts, 40 | }, 41 | }); 42 | 43 | if (process.env.REACT_APP_IN_FUNIX) { 44 | config.plugins.push( 45 | new SaveRemoteFilePlugin([ 46 | { 47 | url: "https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.js", 48 | filepath: "static/js/jquery-3.7.1.min.js", 49 | hash: false, 50 | }, 51 | { 52 | url: "https://cdn.jsdelivr.net/gh/matplotlib/matplotlib@v3.10.x/lib/matplotlib/backends/web_backend/css/fbm.css", 53 | filepath: "static/css/fbm.css", 54 | hash: false, 55 | }, 56 | { 57 | url: "https://cdn.jsdelivr.net/gh/matplotlib/matplotlib@v3.10.x/lib/matplotlib/backends/web_backend/css/mpl.css", 58 | filepath: "static/css/mpl.css", 59 | hash: false, 60 | }, 61 | ]), 62 | ); 63 | } 64 | 65 | config.plugins.push( 66 | new CopyWebpackPlugin({ 67 | patterns: [ 68 | { 69 | from: path.resolve(__dirname, "node_modules/indigo-ketcher/indigo-ketcher-[0-9\.]+.wasm"), 70 | to: "static/indigo/[name][ext]", 71 | }, 72 | { 73 | from: path.resolve(__dirname, "node_modules/indigo-ketcher/indigo-ketcher-separate-wasm.js"), 74 | to: "static/indigo/[name][ext]", 75 | }, 76 | ], 77 | }), 78 | ); 79 | 80 | config.plugins.push(new ProgressBarPlugin()); 81 | return config; 82 | }; 83 | -------------------------------------------------------------------------------- /frontend/src/components/FunixFunction/JSONEditorWidget.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from "react"; 2 | import type { InteractionProps } from "react-json-view"; 3 | import { Stack } from "@mui/material"; 4 | import { castValue, getInitValue } from "../Common/ValueOperation"; 5 | import MarkdownDiv from "../Common/MarkdownDiv"; 6 | import ThemeReactJson from "../Common/ThemeReactJson"; 7 | import { WidgetProps } from "@rjsf/utils"; 8 | 9 | interface JSONEditorWidgetInterface { 10 | widget: WidgetProps; 11 | checkType: string; 12 | keys?: { [key: string]: string }; 13 | data?: any; 14 | } 15 | 16 | type JSONType = number | string | boolean | object | null | undefined; 17 | type JSONObjectType = { [key: string]: JSONType | any }; 18 | 19 | const JSONEditorWidget = (props: JSONEditorWidgetInterface) => { 20 | let value: { [key: string]: JSONType } | JSONType[] = {}; 21 | if (props.widget.schema.type === "array") { 22 | value = []; 23 | } else if (props.keys) { 24 | for (const key in props.keys) { 25 | value[key] = getInitValue(props.keys[key]); 26 | } 27 | } 28 | 29 | const [src, setSrc] = React.useState( 30 | props.data || props.widget.value || props.widget.schema.default || value, 31 | ); 32 | 33 | useEffect(() => { 34 | if (props.data === src) return; 35 | setSrc( 36 | props.data || props.widget.value || props.widget.schema.default || value, 37 | ); 38 | }, [props.data]); 39 | 40 | const handleEdit = React.useCallback((value: InteractionProps) => { 41 | if (value.updated_src instanceof Array) { 42 | const formatList = value.updated_src.map((item) => { 43 | return castValue(item, props.checkType); 44 | }); 45 | setSrc(formatList); 46 | props.widget.onChange(formatList); 47 | } else if (props.keys) { 48 | const formatDict: { [key: string]: JSONType } = {}; 49 | const JSONTypedUpdatedSource: JSONObjectType = value.updated_src; 50 | for (const key in JSONTypedUpdatedSource) { 51 | if (key in props.keys) { 52 | formatDict[key] = castValue( 53 | JSONTypedUpdatedSource[key], 54 | props.keys[key], 55 | ); 56 | } 57 | } 58 | setSrc(formatDict); 59 | props.widget.onChange(formatDict); 60 | } else { 61 | setSrc(value.updated_src); 62 | props.widget.onChange(value.updated_src); 63 | } 64 | }, []); 65 | 66 | const reactJSON = props.keys ? ( 67 | 68 | ) : ( 69 | 75 | ); 76 | 77 | return ( 78 | 79 | 83 | {reactJSON} 84 | 85 | ); 86 | }; 87 | 88 | export default JSONEditorWidget; 89 | -------------------------------------------------------------------------------- /frontend/src/shared/media.ts: -------------------------------------------------------------------------------- 1 | const FunixRecorder = class { 2 | private audioMediaRecorder: MediaRecorder | null = null; 3 | private videoMediaRecorder: MediaRecorder | null = null; 4 | private audioChunks: Blob[] = []; 5 | private videoChunks: Blob[] = []; 6 | 7 | constructor() { 8 | this.audioMediaRecorder = null; 9 | this.videoMediaRecorder = null; 10 | this.audioChunks = []; 11 | this.videoChunks = []; 12 | } 13 | 14 | public audioRecord = (callback: (file: File) => void) => { 15 | navigator.mediaDevices 16 | .getUserMedia({ 17 | audio: true, 18 | video: false, 19 | }) 20 | .then((stream) => { 21 | this.audioMediaRecorder = new MediaRecorder(stream); 22 | this.audioMediaRecorder.ondataavailable = (e) => { 23 | this.audioChunks.push(e.data); 24 | }; 25 | this.audioMediaRecorder.onstop = () => { 26 | const blob = new Blob(this.audioChunks, { 27 | type: "audio/ogg", 28 | }); 29 | this.audioChunks = []; 30 | const now = new Date().getTime(); 31 | const file = new File([blob], `audio-${now}.ogg`, { 32 | type: "audio/ogg", 33 | lastModified: now, 34 | }); 35 | callback(file); 36 | }; 37 | const element = document.getElementById("audioRecord"); 38 | 39 | if (element !== null) { 40 | element.addEventListener("click", () => { 41 | if (this.audioMediaRecorder !== null) { 42 | this.audioMediaRecorder.stop(); 43 | } 44 | }); 45 | } 46 | 47 | this.audioMediaRecorder.start(); 48 | }); 49 | }; 50 | 51 | public videoRecord = (callback: (file: File) => void, withAudio: boolean) => { 52 | navigator.mediaDevices 53 | .getUserMedia({ audio: withAudio, video: true }) 54 | .then((stream) => { 55 | this.videoMediaRecorder = new MediaRecorder(stream); 56 | this.videoMediaRecorder.ondataavailable = (e) => { 57 | this.videoChunks.push(e.data); 58 | }; 59 | this.videoMediaRecorder.onstop = () => { 60 | const blob = new Blob(this.videoChunks, { 61 | type: "video/webm", 62 | }); 63 | this.videoChunks = []; 64 | const now = new Date().getTime(); 65 | const file = new File([blob], `video-${now}.webm`, { 66 | type: "video/webm", 67 | lastModified: now, 68 | }); 69 | callback(file); 70 | }; 71 | const element = document.getElementById("videoRecord"); 72 | 73 | if (element !== null) { 74 | element.addEventListener("click", () => { 75 | if (this.videoMediaRecorder !== null) { 76 | this.videoMediaRecorder.stop(); 77 | } 78 | }); 79 | } 80 | 81 | this.videoMediaRecorder.start(); 82 | }); 83 | }; 84 | }; 85 | 86 | export default FunixRecorder; 87 | -------------------------------------------------------------------------------- /frontend/src/components/PrivacyDialog.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Button, 3 | Dialog, 4 | DialogActions, 5 | DialogContent, 6 | DialogTitle, 7 | } from "@mui/material"; 8 | import MarkdownDiv from "./Common/MarkdownDiv"; 9 | import { useEffect, useRef, useState } from "react"; 10 | import { getCookie, setCookie } from "typescript-cookie"; 11 | import React from "react"; 12 | import _ from "lodash"; 13 | 14 | export type PrivacyDialogProps = { 15 | backend: URL | undefined; 16 | }; 17 | 18 | const PrivacyDialog: React.FC = ({ backend }) => { 19 | const [privacy, setPrivacy] = useState(false); 20 | const [privacyText, setPrivacyText] = useState(""); 21 | const [logLevel, setLogLevel] = useState(0); 22 | const [lastPrivacyHash, setLastPrivacyHash] = useState(""); 23 | const privacyDone = useRef(false); 24 | 25 | useEffect(() => { 26 | if (privacyDone.current) return; 27 | if (backend === undefined) return; 28 | fetch(new URL("/privacy", backend), { 29 | method: "GET", 30 | }) 31 | .then((body) => { 32 | return body.json(); 33 | }) 34 | .then((json: { text: string; log_level: number; hash: string }) => { 35 | if (json.log_level !== 0) { 36 | setPrivacyText(json.text); 37 | setLogLevel(json.log_level); 38 | setLastPrivacyHash(json.hash); 39 | 40 | if (localStorage.getItem("privacy-hash") !== json.hash) { 41 | setPrivacy(true); 42 | } else { 43 | setPrivacy( 44 | json.log_level === 0 45 | ? false 46 | : getCookie("first-join") === undefined, 47 | ); 48 | } 49 | } 50 | privacyDone.current = true; 51 | }) 52 | .catch(() => { 53 | console.warn("No privacy text!"); 54 | }); 55 | }, [backend]); 56 | return ( 57 | 58 | Welcome to Funix 59 | 60 | 61 | 62 | 63 | 75 | 84 | 85 | 86 | ); 87 | }; 88 | 89 | export default React.memo(PrivacyDialog, (prevProps, nextProps) => { 90 | return _.isEqual(prevProps.backend, nextProps.backend); 91 | }); 92 | -------------------------------------------------------------------------------- /backend/funix/__main__.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | import plac 5 | 6 | from funix import run 7 | from funix.util.secret import get_secret_key_from_option 8 | 9 | 10 | @plac.pos( 11 | "file_folder_or_module_name", 12 | "The Python module containing functions to be turned into web apps by Funix. " 13 | "For example, if your functions are in the file `hello.py`, you should pass `hello.py` here. " 14 | "if you want to turn a module called `hello` into a web app, you should pass `hello` here, " 15 | "and with --package or -P flag. " 16 | "if you want to turn a full folder called `examples` into a web app, you should pass `examples` here.", 17 | ) 18 | @plac.opt("host", "Host of Funix", abbrev="H") 19 | @plac.opt("port", "Port of Funix", abbrev="p") 20 | @plac.flg("no_frontend", "Disable frontend server", abbrev="F") 21 | @plac.flg("no_browser", "Disable auto open browser", abbrev="B") 22 | @plac.flg("package", "Enable package mode", abbrev="P") 23 | @plac.flg("dev", "Enable development mode", abbrev="d") 24 | @plac.flg("transform", "Transform the globals to a session variables", abbrev="t") 25 | @plac.opt("from_git", "Import module from git", abbrev="g") 26 | @plac.opt("repo_dir", "The directories in the repo that need to be used", abbrev="r") 27 | @plac.opt("secret", "The secret key for the full app", abbrev="s") 28 | @plac.opt("default", "The default function to run", abbrev="D") 29 | def main( 30 | file_folder_or_module_name=None, 31 | host="0.0.0.0", 32 | port=3000, 33 | no_frontend=False, 34 | no_browser=False, 35 | package=False, 36 | dev=False, 37 | transform=False, 38 | from_git=None, 39 | repo_dir=None, 40 | secret=None, 41 | default=None, 42 | ): 43 | """Funix: Building web apps without manually creating widgets 44 | 45 | Funix turns your Python function into a web app 46 | by building the UI from the function's signature, 47 | based on the mapping from variable types to UI widgets, 48 | customizable per-widget or kept consistent across apps via themes. 49 | 50 | Just write your core logic and leave the rest to Funix. 51 | Visit us at http://funix.io""" 52 | 53 | if not file_folder_or_module_name and not from_git: 54 | print( 55 | 'Error: No Python module, file or git repo provided.\nPlease run "funix --help" for more information.' 56 | ) 57 | sys.exit(1) 58 | 59 | sys.path.append(os.getcwd()) 60 | parsed_secret: bool | str = get_secret_key_from_option(secret) 61 | 62 | run( 63 | host=host, 64 | port=port, 65 | file_or_module_name=file_folder_or_module_name, 66 | no_frontend=no_frontend, 67 | no_browser=no_browser, 68 | package_mode=package, 69 | from_git=from_git, 70 | repo_dir=repo_dir, 71 | dev=dev, 72 | transform=transform, 73 | app_secret=parsed_secret, 74 | default=default, 75 | ) 76 | 77 | 78 | def cli_main(): 79 | """ 80 | The entry point for the command line interface. 81 | 82 | This function is called when you run `python -m funix` or `funix` from the command line. 83 | """ 84 | plac.call(main, version="Funix 0.6.4") 85 | 86 | 87 | if __name__ == "__main__": 88 | cli_main() 89 | -------------------------------------------------------------------------------- /frontend/src/components/FunixFunction/FunixCustom.tsx: -------------------------------------------------------------------------------- 1 | import { Checkbox, Rating, Slider, Switch, TextField } from "@mui/material"; 2 | import React, { useCallback, useEffect, useState } from "react"; 3 | import { WidgetProps } from "@rjsf/utils"; 4 | 5 | export interface FunixCustomProps { 6 | component: string; 7 | props: null | Record; 8 | widget: WidgetProps; 9 | } 10 | 11 | const FunixCustom: React.FC = React.memo((props) => { 12 | const [value, setValue] = useState( 13 | props.widget.value ?? props.widget.formData ?? undefined, 14 | ); 15 | 16 | useEffect(() => { 17 | const newValue = props.widget.value ?? props.widget.formData; 18 | if (newValue !== value) { 19 | setValue(newValue); 20 | } 21 | }, [props.widget.value, props.widget.formData]); 22 | 23 | const handleTextFieldChange = useCallback( 24 | (event: React.ChangeEvent) => { 25 | const newValue = event.target.value; 26 | setValue(newValue); 27 | props.widget.onChange(newValue); 28 | }, 29 | [props.widget], 30 | ); 31 | 32 | const handleBooleanChange = useCallback( 33 | (event: React.ChangeEvent) => { 34 | const newValue = event.target.checked; 35 | setValue(newValue); 36 | props.widget.onChange(newValue); 37 | }, 38 | [props.widget], 39 | ); 40 | 41 | const handleSliderChange = useCallback( 42 | (_event: Event, newValue: number | number[]) => { 43 | setValue(newValue); 44 | props.widget.onChange(newValue); 45 | }, 46 | [props.widget], 47 | ); 48 | 49 | const handleRatingChange = useCallback( 50 | (_event: React.SyntheticEvent, newValue: number | null) => { 51 | setValue(newValue); 52 | props.widget.onChange(newValue); 53 | }, 54 | [props.widget], 55 | ); 56 | 57 | switch (props.component) { 58 | case "@mui/material/TextField": 59 | return ( 60 | 65 | ); 66 | case "@mui/material/Switch": 67 | return ( 68 | 73 | ); 74 | case "@mui/material/Checkbox": 75 | return ( 76 | 81 | ); 82 | case "@mui/material/Slider": 83 | return ( 84 | 89 | ); 90 | case "@mui/material/Rating": 91 | return ( 92 | 97 | ); 98 | default: 99 | return
Unknown component: {props.component}
; 100 | } 101 | }); 102 | 103 | FunixCustom.displayName = "FunixCustom"; 104 | 105 | export default FunixCustom; 106 | -------------------------------------------------------------------------------- /backend/funix/build/static/js/main.d08a20a0.js.LICENSE.txt: -------------------------------------------------------------------------------- 1 | /*! 2 | localForage -- Offline Storage, Improved 3 | Version 1.10.0 4 | https://localforage.github.io/localForage 5 | (c) 2013-2017 Mozilla, Apache License 2.0 6 | */ 7 | 8 | /** 9 | * @license 10 | * Lodash 11 | * Copyright OpenJS Foundation and other contributors 12 | * Released under MIT license 13 | * Based on Underscore.js 1.8.3 14 | * Copyright Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors 15 | */ 16 | 17 | /** 18 | * @license React 19 | * react-dom.production.min.js 20 | * 21 | * Copyright (c) Facebook, Inc. and its affiliates. 22 | * 23 | * This source code is licensed under the MIT license found in the 24 | * LICENSE file in the root directory of this source tree. 25 | */ 26 | 27 | /** 28 | * @license React 29 | * react-is.production.min.js 30 | * 31 | * Copyright (c) Facebook, Inc. and its affiliates. 32 | * 33 | * This source code is licensed under the MIT license found in the 34 | * LICENSE file in the root directory of this source tree. 35 | */ 36 | 37 | /** 38 | * @license React 39 | * react-jsx-runtime.production.min.js 40 | * 41 | * Copyright (c) Facebook, Inc. and its affiliates. 42 | * 43 | * This source code is licensed under the MIT license found in the 44 | * LICENSE file in the root directory of this source tree. 45 | */ 46 | 47 | /** 48 | * @license React 49 | * react.production.min.js 50 | * 51 | * Copyright (c) Facebook, Inc. and its affiliates. 52 | * 53 | * This source code is licensed under the MIT license found in the 54 | * LICENSE file in the root directory of this source tree. 55 | */ 56 | 57 | /** 58 | * @license React 59 | * scheduler.production.min.js 60 | * 61 | * Copyright (c) Facebook, Inc. and its affiliates. 62 | * 63 | * This source code is licensed under the MIT license found in the 64 | * LICENSE file in the root directory of this source tree. 65 | */ 66 | 67 | /** 68 | * @license React 69 | * use-sync-external-store-shim.production.min.js 70 | * 71 | * Copyright (c) Facebook, Inc. and its affiliates. 72 | * 73 | * This source code is licensed under the MIT license found in the 74 | * LICENSE file in the root directory of this source tree. 75 | */ 76 | 77 | /** 78 | * @remix-run/router v1.20.0 79 | * 80 | * Copyright (c) Remix Software Inc. 81 | * 82 | * This source code is licensed under the MIT license found in the 83 | * LICENSE.md file in the root directory of this source tree. 84 | * 85 | * @license MIT 86 | */ 87 | 88 | /** 89 | * React Router v6.27.0 90 | * 91 | * Copyright (c) Remix Software Inc. 92 | * 93 | * This source code is licensed under the MIT license found in the 94 | * LICENSE.md file in the root directory of this source tree. 95 | * 96 | * @license MIT 97 | */ 98 | 99 | /** @license React v16.13.1 100 | * react-is.production.min.js 101 | * 102 | * Copyright (c) Facebook, Inc. and its affiliates. 103 | * 104 | * This source code is licensed under the MIT license found in the 105 | * LICENSE file in the root directory of this source tree. 106 | */ 107 | -------------------------------------------------------------------------------- /backend/funix/decorator/theme.py: -------------------------------------------------------------------------------- 1 | """ 2 | Handle theme for decorator 3 | """ 4 | from typing import Optional 5 | 6 | from funix.theme import get_dict_theme, parse_theme 7 | from funix.util.uri import is_valid_uri 8 | 9 | default_theme: dict = {} 10 | """ 11 | The default funix theme. 12 | """ 13 | 14 | themes = {} 15 | """ 16 | A dict, key is theme name, value is funix theme. 17 | """ 18 | 19 | parsed_themes = {} 20 | """ 21 | A dict, key is theme name, value is parsed MUI theme. 22 | """ 23 | 24 | 25 | def set_default_theme(theme: str) -> None: 26 | """ 27 | Set the default theme. 28 | 29 | Parameters: 30 | theme (str): The theme alias, path or url. 31 | """ 32 | global default_theme, parsed_themes, themes 33 | if theme in themes: 34 | theme_dict = themes[theme] 35 | else: 36 | if is_valid_uri(theme): 37 | theme_dict = get_dict_theme(None, theme) 38 | else: 39 | theme_dict = get_dict_theme(theme, None) 40 | default_theme = theme_dict 41 | parsed_themes["__default"] = parse_theme(default_theme) 42 | 43 | 44 | def import_theme( 45 | source: str | dict, 46 | alias: Optional[str] = None, 47 | ) -> None: 48 | """ 49 | Import a theme from path, url or dict. 50 | 51 | Parameters: 52 | source (str | dict): The path, url or dict of the theme. 53 | alias (str): The theme alias. 54 | 55 | Raises: 56 | ValueError: If the theme already exists. 57 | 58 | Notes: 59 | Check the `funix.theme.get_dict_theme` function for more information. 60 | """ 61 | global themes 62 | if isinstance(source, str): 63 | if is_valid_uri(source): 64 | theme = get_dict_theme(None, source) 65 | else: 66 | theme = get_dict_theme(source, None) 67 | else: 68 | theme = source 69 | name = theme["name"] 70 | if alias is not None: 71 | name = alias 72 | if name in themes: 73 | raise ValueError(f"Theme {name} already exists") 74 | themes[name] = theme 75 | 76 | 77 | def clear_default_theme() -> None: 78 | """ 79 | Clear the default theme. 80 | """ 81 | global default_theme, parsed_themes 82 | default_theme = {} 83 | parsed_themes.pop("__default") 84 | 85 | 86 | def get_parsed_theme_fot_funix(theme: Optional[str] = None): 87 | """ 88 | Get the parsed theme for funix. 89 | 90 | Parameters: 91 | theme (str): The theme name. 92 | 93 | Returns: 94 | The parsed theme for funix. 95 | """ 96 | global parsed_themes 97 | if not theme: 98 | if "__default" in parsed_themes: 99 | return parsed_themes["__default"] 100 | else: 101 | return [], {}, {}, {}, {} 102 | else: 103 | if theme in parsed_themes: 104 | return parsed_themes[theme] 105 | else: 106 | # Cache 107 | if theme in themes: 108 | parsed_theme = parse_theme(themes[theme]) 109 | parsed_themes[theme] = parsed_theme 110 | else: 111 | import_theme(theme, alias=theme) # alias is not important here 112 | parsed_theme = parse_theme(themes[theme]) 113 | parsed_themes[theme] = parsed_theme 114 | return parsed_theme 115 | -------------------------------------------------------------------------------- /examples/games/wordle.py: -------------------------------------------------------------------------------- 1 | import random 2 | import string 3 | from funix import funix, funix_class, funix_method 4 | from funix.hint import HTML 5 | 6 | import json 7 | 8 | 9 | with open("./words.json", "r") as f: 10 | words = json.load(f) 11 | 12 | 13 | @funix( 14 | title="Get hint", 15 | description="Get 5 random letters" 16 | ) 17 | def random_5_letters() -> str: 18 | """ 19 | Get random 5 letters 20 | """ 21 | return "".join(random.choices(string.ascii_lowercase, k=5)) 22 | 23 | 24 | @funix_class() 25 | class Wordle: 26 | @funix_method( 27 | title="Start a game", 28 | argument_labels={ 29 | "random_word": "Random 5-letter" 30 | }, 31 | print_to_web=True 32 | ) 33 | def __init__(self, random_word: bool = False): 34 | """ 35 | Start a game 36 | """ 37 | self.random_word = random_word 38 | 39 | self.word = random_5_letters() if random_word else random.choice(words) 40 | self.history = [] 41 | 42 | self.mismatch = False 43 | 44 | print("Game started. Now go to Play to play. ") 45 | 46 | def __reset(self): 47 | self.word = random_5_letters() if self.random_word else random.choice(words) 48 | self.history = [] 49 | 50 | def __push(self, word: str): 51 | if len(word) != 5 or not word.isalpha(): 52 | self.mismatch = True 53 | else: 54 | self.mismatch = False 55 | if len(self.history) > 6: 56 | self.__reset() 57 | self.history.append(word.lower()) 58 | 59 | def __generate(self) -> HTML: 60 | html_code = "" 61 | for i in range(6): 62 | html_code += f"
0 else ''}'>" 63 | if i < len(self.history): 64 | for single_word_index in range(5): 65 | single_word = self.history[i][single_word_index] 66 | single_word_color = "rgb(156, 163, 175)" 67 | if single_word in self.word: 68 | single_word_color = "rgb(251, 191, 36)" 69 | if single_word == self.word[single_word_index]: 70 | single_word_color = "rgb(34, 197, 94)" 71 | html_code += f"
{single_word}
" 72 | else: 73 | for j in range(5): 74 | html_code += f"
" 75 | html_code += "
" 76 | if self.mismatch: 77 | html_code += "Word mismatch, please enter a 5-letter word" 78 | if len(self.history) > 0 and self.history[-1] == self.word: 79 | self.__reset() 80 | html_code += "Good game, you win!" 81 | elif len(self.history) == 6: 82 | html_code += f"Game Over, answer is: {self.word}" 83 | return html_code 84 | 85 | def Play(self, word: str) -> HTML: 86 | """ 87 | Enter a five-letter word and click the "Run" button to see your guess result. 88 | """ 89 | self.__push(word) 90 | return self.__generate() 91 | -------------------------------------------------------------------------------- /frontend/src/components/FunixFunction/ChemEditor.tsx: -------------------------------------------------------------------------------- 1 | import { WidgetProps } from "@rjsf/utils"; 2 | import React, { useState } from "react"; 3 | import { 4 | Box, 5 | Button, 6 | Card, 7 | CardMedia, 8 | Dialog, 9 | DialogActions, 10 | DialogContent, 11 | DialogTitle, 12 | Typography, 13 | } from "@mui/material"; 14 | import { KetcherEditor } from "./components"; 15 | import { ChemEditorValue } from "./hooks"; 16 | import renderSvg from "../../shared/indigo-render"; 17 | 18 | interface ChemEditorProps { 19 | widget: WidgetProps; 20 | popup?: boolean; 21 | } 22 | 23 | const SimpleRenderBox = (props: { data: string | null }) => { 24 | return ( 25 | 36 | {props.data ? ( 37 | 47 | ) : ( 48 | No data 49 | )} 50 | 51 | ); 52 | }; 53 | 54 | const ChemEditor: React.FC = React.memo((props) => { 55 | const [popUpKet, setPopUpKet] = useState(null); 56 | const [popUpOpen, setPopUpOpen] = useState(false); 57 | const [popUpKetTemp, setPopUpKetTemp] = useState( 58 | null, 59 | ); 60 | 61 | const initialValue = props.widget.value ?? props.widget.formData ?? null; 62 | 63 | const handleChange = (newValue: ChemEditorValue) => { 64 | props.widget.onChange(newValue); 65 | setPopUpKet(newValue); 66 | }; 67 | 68 | if (props.popup) { 69 | return ( 70 | 79 | 80 | 87 | setPopUpOpen(false)} 90 | fullWidth 91 | maxWidth="xl" 92 | > 93 | Editor 94 | 95 | setPopUpKetTemp(value)} 98 | height="80vh" 99 | /> 100 | 101 | 102 | 103 | 115 | 116 | 117 | 118 | ); 119 | } 120 | 121 | return ; 122 | }); 123 | 124 | export default ChemEditor; 125 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@textea/funix", 3 | "repository": "https://github.com/TexteaInc/funix", 4 | "author": "Textea Inc.", 5 | "license": "MIT", 6 | "dependencies": { 7 | "@emotion/react": "^11.13.0", 8 | "@emotion/styled": "^11.13.0", 9 | "@icons-pack/react-simple-icons": "^9.6.0", 10 | "@monaco-editor/react": "^4.6.0", 11 | "@mui/icons-material": "^5.16.5", 12 | "@mui/lab": "^5.0.0-alpha.173", 13 | "@mui/material": "^5.16.5", 14 | "@mui/x-data-grid": "^7.11.1", 15 | "@mui/x-data-grid-premium": "^7.22.0", 16 | "@mui/x-data-grid-pro": "^7.11.1", 17 | "@mui/x-date-pickers": "^7.29.4", 18 | "@mui/x-license": "^7.11.1", 19 | "@rjsf/core": "^5.19.4", 20 | "@rjsf/mui": "^5.19.4", 21 | "@rjsf/utils": "^5.19.4", 22 | "@rjsf/validator-ajv8": "^5.19.4", 23 | "@types/jest": "^29.5.12", 24 | "@types/node": "^18.7.6", 25 | "@types/react": "^18.3.3", 26 | "@types/react-dom": "^18.3.0", 27 | "@types/uuid": "^10.0.0", 28 | "dangerously-set-html-content": "^1.1.0", 29 | "dayjs": "^1.11.13", 30 | "indigo-ketcher": "^1.34.0", 31 | "jotai": "^2.12.5", 32 | "ketcher-core": "^3.4.1", 33 | "ketcher-react": "^3.4.1", 34 | "ketcher-standalone": "^3.4.1", 35 | "localforage": "^1.10.0", 36 | "lodash": "^4.17.21", 37 | "material-ui-popup-state": "^5.1.2", 38 | "notistack": "^3.0.1", 39 | "react": "^18.3.1", 40 | "react-dom": "^18.3.1", 41 | "react-dropzone": "^14.2.3", 42 | "react-json-view": "^1.21.3", 43 | "react-markdown": "^10.1.0", 44 | "react-router-dom": "^6.25.1", 45 | "react-scripts": "5.0.1", 46 | "react-string-replace": "^1.1.1", 47 | "react-syntax-highlighter": "^15.5.0", 48 | "rehype-katex": "^7.0.0", 49 | "rehype-mermaid": "^3.0.0", 50 | "rehype-raw": "^7.0.0", 51 | "remark-gfm": "^4.0.0", 52 | "remark-math": "^6.0.0", 53 | "swr": "^2.2.5", 54 | "typescript": "^5.5.4", 55 | "typescript-cookie": "^1.0.6", 56 | "uuid": "^10.0.0", 57 | "web-vitals": "^4.2.2" 58 | }, 59 | "scripts": { 60 | "start": "react-app-rewired start", 61 | "build": "react-app-rewired build", 62 | "test": "react-app-rewired test", 63 | "eject": "react-scripts eject", 64 | "lint": "npx eslint . -c eslint.config.mjs --cache --fix", 65 | "lint:ci": "npx eslint . -c eslint.config.mjs --cache", 66 | "funix:test": "env REACT_APP_FUNIX_BACKEND=http://127.0.0.1:8080 react-app-rewired start", 67 | "funix:build": "env BUILD_PATH=../backend/funix/build GENERATE_SOURCEMAP=false REACT_APP_IN_FUNIX=true react-app-rewired build" 68 | }, 69 | "browserslist": { 70 | "production": [ 71 | ">0.2%", 72 | "not dead", 73 | "not op_mini all" 74 | ], 75 | "development": [ 76 | "last 1 chrome version", 77 | "last 1 firefox version", 78 | "last 1 safari version" 79 | ] 80 | }, 81 | "devDependencies": { 82 | "@eslint/js": "^9.8.0", 83 | "@types/eslint__js": "^8.42.3", 84 | "@types/lodash": "^4.17.7", 85 | "@types/markdown-it": "^14.1.2", 86 | "@types/react-syntax-highlighter": "^15.5.13", 87 | "copy-webpack-plugin": "^13.0.1", 88 | "customize-cra": "^1.0.0", 89 | "eslint": "^9.8.0", 90 | "eslint-config-prettier": "^9.1.0", 91 | "eslint-plugin-prettier": "^5.2.1", 92 | "prettier": "^3.3.3", 93 | "progress-bar-webpack-plugin": "^2.1.0", 94 | "react-app-rewired": "^2.2.1", 95 | "save-remote-file-webpack-plugin": "^1.1.0", 96 | "string-replace-loader": "^3.1.0", 97 | "typescript-eslint": "^7.18.0" 98 | }, 99 | "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e" 100 | } 101 | -------------------------------------------------------------------------------- /examples/AI/use_your_own_openAI_token.py: -------------------------------------------------------------------------------- 1 | import os, json 2 | import typing 3 | 4 | import requests 5 | import IPython 6 | import ipywidgets 7 | 8 | from openai import OpenAI 9 | 10 | client = OpenAI() 11 | 12 | import funix 13 | from funix.session import get_global_variable, set_global_variable 14 | 15 | # openai.api_key = os.environ.get("OPENAI_KEY") 16 | # openai_key = os.environ.get("OPENAI_KEY") 17 | openai_key = "" 18 | 19 | 20 | # @funix.funix( # Funix.io, the laziest way to build web apps in Python 21 | # title="Set OpenAI key", 22 | # argument_labels={ 23 | # "api_key": "Enter your API key", 24 | # "use_sys_env_var": "Use system environment variable", 25 | # }, 26 | # conditional_visible=[ 27 | # { 28 | # "when": {"use_sys_env_var": False}, 29 | # "show": ["api_key"], 30 | # } 31 | # ], 32 | # show_source=True, 33 | # ) 34 | # def set_openAI_key(api_key: str = "") -> str: 35 | # if api_key == "": 36 | # return "You entered an empty string. Try again." 37 | # else: 38 | # global openai_key 39 | # openai_key = api_key 40 | # return f"Your openAI key has been set to: {openai_key}" 41 | 42 | # # openai.api_key = api_key 43 | # # set_global_variable(openai.api_key, api_key) 44 | # # FIXME: The two lines above are both useless to change openai.api_key. That's why we have to use POST method below to query all OpenAI endpoints. This is something to be fixed after grand opening. 45 | # return "OpenAI API key has been set via the web form! If it was set via an environment variable before, it's now overwritten." 46 | 47 | 48 | # @funix.funix() 49 | # def get_openAI_key() -> str: 50 | # return f"Your openAI key has been set to: {openai_key}" 51 | 52 | 53 | @funix.funix() 54 | def ChatGPT_POST( 55 | openai_key: ipywidgets.Password, 56 | prompt: str="What is the meaning of life?", ) -> str: 57 | header = { 58 | "Content-Type": "application/json", 59 | "Authorization": f"Bearer {openai_key.value}", 60 | } 61 | payload = { 62 | "model": "gpt-3.5-turbo", 63 | "messages": [{"role": "user", "content": f"{prompt}"}], 64 | } 65 | response = requests.post( 66 | "https://api.openai.com/v1/chat/completions", headers=header, json=payload 67 | ) 68 | 69 | if "error" in response.json(): 70 | return response.json()["error"]["message"] 71 | else: 72 | return response.json()["choices"][0]["message"]["content"] 73 | 74 | 75 | @funix.funix() 76 | def ChatGPT(prompt: str) -> str: 77 | completion = client.chat.completions.create(messages=[{"role": "user", "content": prompt}], model="gpt-3.5-turbo") 78 | return completion.choices[0].message.content 79 | 80 | 81 | @funix.funix() 82 | def dalle_POST( 83 | openai_key: ipywidgets.Password, 84 | prompt: str="a flying cat in a red pajama") -> (str, IPython.display.Image): 85 | header = { 86 | "Content-Type": "application/json", 87 | "Authorization": f"Bearer {openai_key.value}", 88 | } 89 | payload = {"prompt": f"{prompt}"} 90 | response = requests.post( 91 | "https://api.openai.com/v1/images/generations", headers=header, json=payload 92 | ) 93 | if "error" in response.json(): 94 | return response.json()["error"]["message"] + "\n A place holder image below", "https://picsum.photos/200/300" 95 | else: 96 | return "here is your picture", response.json()["data"][0]["url"] 97 | 98 | 99 | # @funix.funix() 100 | def dalle(Prompt: str) -> IPython.display.Image: 101 | response = client.images.generate(prompt=Prompt) 102 | return response.data[0].url 103 | 104 | 105 | if __name__ == "__main__": 106 | from funix import run 107 | 108 | run(port=3000, file_or_module_name=__file__) 109 | -------------------------------------------------------------------------------- /frontend/src/components/SheetComponents/SheetSlider.tsx: -------------------------------------------------------------------------------- 1 | import { SheetInterface } from "./SheetInterface"; 2 | import React, { useEffect } from "react"; 3 | import { FormControl, Grid, Input, Slider } from "@mui/material"; 4 | import SliderValueLabel from "../Common/SliderValueLabel"; 5 | 6 | export default function SheetSlider( 7 | props: SheetInterface & { 8 | min: number; 9 | max: number; 10 | step: number; 11 | }, 12 | ) { 13 | const [value, setValue] = React.useState< 14 | number | string | Array 15 | >(props.params.value === undefined ? props.min : props.params.value); 16 | 17 | useEffect(() => { 18 | setValue(props.params.value === undefined ? props.min : props.params.value); 19 | }, [props.params.value]); 20 | 21 | const customSetValue = (value: number | string | Array) => { 22 | setValue(value); 23 | props.customChange(props.params.row.id, props.params.field, value); 24 | }; 25 | 26 | const handleSliderChange = (_event: Event, newValue: number | number[]) => 27 | customSetValue(newValue); 28 | 29 | const handleSliderInputChange = ( 30 | event: React.ChangeEvent, 31 | ) => { 32 | customSetValue( 33 | event.target.value === "" 34 | ? "" 35 | : props.type === "integer" 36 | ? parseInt(event.target.value) 37 | : parseFloat(event.target.value), 38 | ); 39 | }; 40 | 41 | const handleSliderInputBlur = () => { 42 | if (Array.isArray(value)) { 43 | const newValue = (value as Array).map((v) => { 44 | const value = 45 | typeof v === "number" 46 | ? v 47 | : props.type === "integer" 48 | ? parseInt(v) 49 | : parseFloat(v); 50 | if (value < props.min) { 51 | return props.min; 52 | } else if (value > props.max) { 53 | return props.max; 54 | } 55 | return value; 56 | }); 57 | customSetValue(newValue); 58 | } else { 59 | const newValue = 60 | typeof value === "number" 61 | ? value 62 | : props.type === "integer" 63 | ? parseInt(value) 64 | : parseFloat(value); 65 | if (newValue < props.min) { 66 | customSetValue(props.min); 67 | } else if (newValue > props.max) { 68 | customSetValue(props.max); 69 | } 70 | } 71 | }; 72 | 73 | return ( 74 | 75 | 81 | 82 | 101 | 102 | 103 | 115 | 116 | 117 | 118 | ); 119 | } 120 | -------------------------------------------------------------------------------- /examples/user_manage.py: -------------------------------------------------------------------------------- 1 | import hashlib 2 | import re 3 | import sqlite3 4 | from typing import Tuple 5 | 6 | from funix import funix_method, funix_class, funix 7 | from funix.hint import Image, Video 8 | 9 | 10 | def __init_database(): 11 | con = sqlite3.connect("tutorial.db", check_same_thread=False) 12 | cur = con.cursor() 13 | cur.execute(""" 14 | CREATE TABLE IF NOT EXISTS users( 15 | id INTEGER PRIMARY KEY AUTOINCREMENT, 16 | user TEXT NOT NULL UNIQUE, 17 | password TEXT NOT NULL 18 | ) 19 | """) 20 | con.commit() 21 | return con 22 | 23 | 24 | db = __init_database() 25 | 26 | 27 | def __is_valid_username(username: str): 28 | if len(username) < 3: 29 | return False 30 | 31 | if not username[0].isalpha(): 32 | return False 33 | 34 | if not re.match(r'^[a-zA-Z0-9_]+$', username): 35 | return False 36 | 37 | if username.endswith('_'): 38 | return False 39 | 40 | return True 41 | 42 | 43 | def __is_valid_password(pwd: str): 44 | if len(pwd) < 8: 45 | return False 46 | 47 | if not re.match(r'^[a-zA-Z0-9_!@#$%^&*]+$', pwd): 48 | return False 49 | 50 | if pwd.endswith('_'): 51 | return False 52 | 53 | return True 54 | 55 | 56 | @funix(disable=True) 57 | def validate_user(username: str, password: str) -> bool: 58 | cur = db.cursor() 59 | _hash: str = cur.execute( 60 | "SELECT password FROM users WHERE user = ?;", 61 | (username,) 62 | ).fetchone() 63 | if _hash is None: 64 | return False 65 | _hash = _hash[0] 66 | 67 | login_hash = hashlib.sha256(password.encode('utf-8')).hexdigest() 68 | 69 | return login_hash == _hash 70 | 71 | 72 | @funix( 73 | title="Register", 74 | widgets={"password": "password"}, 75 | ) 76 | def register(username: str, password: str) -> str: 77 | """ 78 | Simple register for demo purpose 79 | """ 80 | if not __is_valid_username(username): 81 | return "Invalid username" 82 | 83 | cur = db.cursor() 84 | count = cur.execute( 85 | "SELECT COUNT(*) FROM users WHERE user = ?;", 86 | (username,) 87 | ).fetchone() 88 | if count is None: 89 | return "Database error" 90 | count = count[0] 91 | 92 | if count > 0: 93 | return "This username is already taken" 94 | 95 | pwd_hash = hashlib.sha256(password.encode('utf-8')).hexdigest() 96 | 97 | cur.execute("INSERT INTO users (user, password) VALUES (?, ?)", (username, pwd_hash)) 98 | db.commit() 99 | 100 | return "Register successfully!" 101 | 102 | 103 | @funix_class() 104 | class Manager: 105 | session = None 106 | 107 | @funix_method( 108 | title="Login to System", 109 | description="Login to the system using your username and password", 110 | widgets={"password": "password"}, 111 | print_to_web=True, 112 | ) 113 | def __init__(self, username: str, password: str): 114 | if validate_user(username, password): 115 | self.session = username # fake session for demo purpose 116 | print(f"Welcome {username}") 117 | else: 118 | raise Exception("Invalid username or password") 119 | 120 | @funix_method( 121 | title="My Info" 122 | ) 123 | def my_info(self) -> str: 124 | return f"Your username: {self.session}" 125 | 126 | @funix_method( 127 | title="Review Multimedia", 128 | ) 129 | def review_multimedia(self) -> Tuple[Image, Video]: 130 | if self.session is None: 131 | raise Exception("You need to login first") 132 | else: 133 | return ( 134 | "https://picsum.photos/200/300", 135 | "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ElephantsDream.mp4", 136 | ) 137 | 138 | -------------------------------------------------------------------------------- /backend/funix/decorator/secret.py: -------------------------------------------------------------------------------- 1 | from json import dumps 2 | 3 | from flask import Response, request 4 | 5 | __decorated_id_to_function_dict: dict[str, dict[str, str]] = {} 6 | """ 7 | A dict, key is function id, value is function name. 8 | """ 9 | 10 | __decorated_secret_functions_dict: dict[str, dict[str, str]] = {} 11 | """ 12 | A dict, key is function id, value is secret. 13 | For checking if the secret is correct. 14 | """ 15 | 16 | __app_secret: dict[str, str | None] = {} 17 | """ 18 | App secret, for all functions. 19 | """ 20 | 21 | 22 | def get_secret_by_id(app_name: str, function_id: str) -> str | None: 23 | """ 24 | Get the secret of a function by id. 25 | 26 | Parameters: 27 | app_name (str): The app name. 28 | function_id (str): The function id. 29 | 30 | Returns: 31 | str | None: The secret. 32 | """ 33 | global __decorated_secret_functions_dict 34 | if app_name not in __decorated_secret_functions_dict: 35 | return None 36 | return __decorated_secret_functions_dict[app_name].get(function_id, None) 37 | 38 | 39 | def set_function_secret( 40 | app_name: str, secret: str, function_id: str, function_name: str 41 | ) -> None: 42 | """ 43 | Set the secret of a function. 44 | 45 | Parameters: 46 | app_name (str): The app name. 47 | secret (str): The secret. 48 | function_id (str): The function id. 49 | function_name (str): The function name (or with path). 50 | """ 51 | global __decorated_secret_functions_dict, __decorated_id_to_function_dict 52 | if app_name not in __decorated_secret_functions_dict: 53 | __decorated_secret_functions_dict[app_name] = { 54 | function_id: secret, 55 | } 56 | __decorated_id_to_function_dict[app_name] = { 57 | function_id: function_name, 58 | } 59 | else: 60 | __decorated_secret_functions_dict[app_name][function_id] = secret 61 | __decorated_id_to_function_dict[app_name][function_id] = function_name 62 | 63 | 64 | def set_app_secret(app_name: str, secret: str) -> None: 65 | """ 66 | Set the app secret, it will be used for all functions. 67 | 68 | Parameters: 69 | app_name (str): The app name. 70 | secret (str): The secret. 71 | """ 72 | global __app_secret 73 | __app_secret[app_name] = secret 74 | 75 | 76 | def get_app_secret(app_name: str) -> str | None: 77 | """ 78 | Get the app secret. 79 | 80 | Parameters: 81 | app_name (str): The app name. 82 | 83 | Returns: 84 | str | None: The app secret. 85 | """ 86 | global __app_secret 87 | return __app_secret.get(app_name, None) 88 | 89 | 90 | def export_secrets(app_name: str): 91 | """ 92 | Export all secrets from the decorated functions. 93 | 94 | Parameters: 95 | app_name (str): The app name. 96 | """ 97 | __new_dict: dict[str, str] = {} 98 | if app_name in __decorated_secret_functions_dict: 99 | for function_id, secret in __decorated_secret_functions_dict[app_name].items(): 100 | __new_dict[__decorated_id_to_function_dict[app_name][function_id]] = secret 101 | return __new_dict 102 | 103 | 104 | def check_secret(app_name: str, function_id: str): 105 | data = request.get_json() 106 | 107 | failed_data = Response( 108 | dumps( 109 | { 110 | "success": False, 111 | } 112 | ), 113 | mimetype="application/json", 114 | status=400, 115 | ) 116 | 117 | if data is None: 118 | return failed_data 119 | 120 | if "secret" not in data: 121 | return failed_data 122 | 123 | user_secret = request.get_json()["secret"] 124 | if user_secret == __decorated_secret_functions_dict[app_name][function_id]: 125 | return Response( 126 | dumps( 127 | { 128 | "success": True, 129 | } 130 | ), 131 | mimetype="application/json", 132 | status=200, 133 | ) 134 | else: 135 | return failed_data 136 | -------------------------------------------------------------------------------- /frontend/src/components/FunixFunction/MultipleInput.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { 3 | Autocomplete, 4 | Checkbox, 5 | Chip, 6 | FormControl, 7 | FormControlLabel, 8 | FormGroup, 9 | FormLabel, 10 | TextField, 11 | } from "@mui/material"; 12 | import MarkdownDiv from "../Common/MarkdownDiv"; 13 | import { WidgetProps } from "@rjsf/utils"; 14 | import { castValue } from "../Common/ValueOperation"; 15 | 16 | interface MultipleInput { 17 | widget: WidgetProps; 18 | data: any; 19 | useCheckbox: boolean; 20 | acceptValues: string[]; 21 | acceptNewValues: boolean; 22 | type?: string; 23 | } 24 | 25 | const MultipleInput = (props: MultipleInput) => { 26 | const [value, setValue] = React.useState(props.data || []); 27 | 28 | React.useEffect(() => { 29 | if (props.data === value) return; 30 | if (props.data !== undefined && props.data !== null) { 31 | setValue(props.data); 32 | } 33 | }, [props.data]); 34 | 35 | const _setValue = (newValue: unknown[]) => { 36 | const castedValue = newValue.map((v) => { 37 | return castValue(v, props.type || "string"); 38 | }); 39 | setValue(castedValue); 40 | props.widget.onChange(castedValue); 41 | }; 42 | 43 | if (props.useCheckbox) { 44 | return ( 45 | 46 | 47 | 55 | 56 | 57 | {props.acceptValues.map((item, index) => ( 58 | { 64 | if (event.target.checked) { 65 | _setValue([...value, item]); 66 | } else { 67 | _setValue(value.filter((v) => v !== item)); 68 | } 69 | }} 70 | /> 71 | } 72 | label={item} 73 | /> 74 | ))} 75 | 76 | 77 | ); 78 | } 79 | 80 | return ( 81 | option.toString()} 88 | disableCloseOnSelect 89 | onChange={(_event, newValue) => _setValue(newValue)} 90 | renderInput={(params) => ( 91 | 108 | } 109 | fullWidth 110 | required={props.widget.required} 111 | disabled={props.widget.disabled || props.widget.readonly} 112 | onKeyDown={(event) => { 113 | if ( 114 | event.key === "Enter" && 115 | props.acceptNewValues && 116 | "value" in event.target 117 | ) { 118 | _setValue([...value, event.target.value]); 119 | } 120 | }} 121 | /> 122 | )} 123 | renderTags={(value, getTagProps) => { 124 | return value.map((option: any, index) => ( 125 | 131 | )); 132 | }} 133 | /> 134 | ); 135 | }; 136 | 137 | export default MultipleInput; 138 | -------------------------------------------------------------------------------- /backend/funix/util/file.py: -------------------------------------------------------------------------------- 1 | """ 2 | File utils for funix. 3 | """ 4 | 5 | import base64 6 | from atexit import register 7 | from os import listdir 8 | from os.path import abspath, exists, isdir, join, normpath, sep, splitext 9 | from shutil import rmtree 10 | from sys import path 11 | from tempfile import mkdtemp 12 | from typing import Any, Generator 13 | 14 | 15 | def create_safe_tempdir() -> bytes | str: 16 | """ 17 | Create a safe tempdir. It will be deleted when the program normally exits. 18 | 19 | Returns: 20 | bytes | str: See `tempfile.mkdtemp` for more info. 21 | """ 22 | tempdir = mkdtemp() 23 | 24 | register(lambda: exists(tempdir) and rmtree(tempdir)) 25 | 26 | return tempdir 27 | 28 | 29 | def get_path_difference(base_path: str, target_path: str) -> str: 30 | """ 31 | Get path difference from base dir and target_dir, and turn it to Python module like string. 32 | 33 | Parameters: 34 | base_path (str): The base directory. 35 | target_path (str): The target directory. 36 | 37 | Returns: 38 | str: The path difference. 39 | 40 | Raises: 41 | ValueError: If the base directory is not a prefix of the target directory. 42 | 43 | Examples / Doctest: 44 | >>> assert get_path_difference("/a/b", "/a/b/c/d") == "c.d" 45 | >>> assert get_path_difference("/abc", "/abc/de/c") == "de.c" 46 | """ 47 | base_components = normpath(base_path).split(sep) 48 | target_components = normpath(target_path).split(sep) 49 | 50 | if not target_path.startswith(base_path): 51 | raise ValueError("The base directory is not a prefix of the target directory.") 52 | 53 | if len(base_components) == 1 and base_components[0] == ".": 54 | return ".".join(target_components) 55 | 56 | path_diff = target_components 57 | 58 | for _ in range(len(base_components)): 59 | path_diff.pop(0) 60 | 61 | return ".".join(path_diff) 62 | 63 | 64 | def get_python_files_in_dir( 65 | base_dir: str, 66 | add_to_sys_path: bool, 67 | need_full_path: bool, 68 | is_dir: bool, 69 | matches: Any, 70 | ) -> Generator[str, None, None]: 71 | """ 72 | Get all the Python files in a directory. 73 | 74 | Parameters: 75 | base_dir (str): The path. 76 | add_to_sys_path (bool): If the path should be added to sys.path. 77 | need_full_path (bool): If the full path is needed. 78 | is_dir (bool): If mode is dir mode. 79 | matches (Any): The matches. 80 | 81 | Returns: 82 | Generator[str, None, None]: The Python files. 83 | 84 | Notes: 85 | Need think a better way to get the Python files and think about the module. 86 | """ 87 | if add_to_sys_path: 88 | path.append(base_dir) 89 | files = listdir(base_dir) 90 | for file in files: 91 | if isdir(join(base_dir, file)): 92 | yield from get_python_files_in_dir( 93 | join(base_dir, file), add_to_sys_path, need_full_path, is_dir, matches 94 | ) 95 | if file.endswith(".py") and file != "__init__.py": 96 | full_path = join(base_dir, file) 97 | if matches: 98 | if matches(abspath(full_path)): 99 | continue 100 | if need_full_path: 101 | yield join(base_dir, file) 102 | else: 103 | yield splitext(file)[0] 104 | 105 | 106 | def mime_encoded( 107 | data: bytes | str, mime_type: str, encoding: str = "utf-8", errors: str = "strict" 108 | ) -> str: 109 | """ 110 | Encode or decode data to/from a specified MIME type. 111 | 112 | Parameters: 113 | data (bytes | str): The data to encode or decode. 114 | mime_type (str): The MIME type to use for encoding or decoding. 115 | encoding (str): The character encoding to use. Defaults to 'utf-8'. 116 | errors (str): The error handling scheme to use. Defaults to 'strict'. 117 | """ 118 | if isinstance(data, bytes): 119 | base64_text = base64.b64encode(data).decode(encoding, errors) 120 | return f"data:{mime_type};base64,{base64_text}" 121 | elif isinstance(data, str): 122 | return f"data:{mime_type};base64,{data}" 123 | else: 124 | raise TypeError("Data must be bytes or string.") 125 | -------------------------------------------------------------------------------- /backend/funix/util/module.py: -------------------------------------------------------------------------------- 1 | """ 2 | Handle module 3 | """ 4 | 5 | from importlib.util import module_from_spec, spec_from_file_location 6 | from inspect import getsourcefile, isclass, isfunction 7 | from os.path import basename 8 | from string import ascii_letters, digits 9 | from types import ModuleType 10 | from typing import Any, Optional 11 | from uuid import uuid4 12 | 13 | from funix import decorator, hint 14 | from funix.app import app 15 | 16 | 17 | def getsourcefile_safe(obj: Any) -> str | None: 18 | """ 19 | Get the source file of the object. 20 | 21 | Note: 22 | Need think a better way to handle the class. 23 | 24 | Parameters: 25 | obj (Any): The object to get the source file. 26 | 27 | Returns: 28 | str: The source file. 29 | None: If the source file is not found, it may be a built-in object. 30 | """ 31 | try: 32 | if isclass(obj): 33 | return getsourcefile(obj.__init__) 34 | return getsourcefile(obj) 35 | except: 36 | return None 37 | 38 | 39 | def import_module_from_file(path: str, need_name: bool) -> ModuleType: 40 | """ 41 | Import module from file. Like the name. 42 | 43 | Parameters: 44 | path (str): The path to the file. 45 | need_name (bool): If the name of the module is also important, so set it to True, I hope this will work. 46 | 47 | Returns: 48 | types.ModuleType: The module. 49 | """ 50 | if need_name: 51 | name = basename(path).replace(".py", "") 52 | else: 53 | name = uuid4().hex 54 | spec = spec_from_file_location(name, path) 55 | module = module_from_spec(spec) 56 | spec.loader.exec_module(module) 57 | return module 58 | 59 | 60 | def funix_menu_to_safe_function_name(name: str) -> str: 61 | """ 62 | Convert the funix menu name to a safe function name. 63 | 64 | Parameters: 65 | name (str): The funix menu name. 66 | 67 | Returns: 68 | str: The safe function name. 69 | """ 70 | # safe_words = digits + ascii_letters + "_" 71 | # return "".join( 72 | # map( 73 | # lambda x: x 74 | # if x in safe_words 75 | # else "_" 76 | # if x == "." 77 | # else f"__Unicode_{ord(x)}__", 78 | # name, 79 | # ) 80 | # ) 81 | return name 82 | 83 | 84 | def handle_module( 85 | module: Any, 86 | need_path: bool, 87 | base_dir: Optional[str], 88 | path_difference: Optional[str], 89 | ) -> None: 90 | """ 91 | Import module's functions and classes to funix. 92 | It won't handle the object that is already handled by funix, 93 | or the object that is customized by the funix or private. 94 | 95 | Parameters: 96 | module (Any): The module to handle. 97 | need_path (bool): If the path is needed. 98 | base_dir (str | None): The base directory. 99 | path_difference (str | None): The path difference. See `funix.util.get_path_difference` for more info. 100 | """ 101 | members = reversed(dir(module)) 102 | for member in members: 103 | module_member = getattr(module, member) 104 | is_func = isfunction(module_member) 105 | is_cls = isclass(module_member) 106 | if is_func or is_cls: 107 | if getsourcefile_safe(module_member) != module.__file__: 108 | if hasattr(module_member, "__wrapped__"): 109 | if getsourcefile_safe(module_member.__wrapped__) != module.__file__: 110 | continue 111 | else: 112 | continue 113 | in_funix = ( 114 | decorator.object_is_handled(app, id(module_member)) 115 | or id(module_member) in hint.custom_cls_ids 116 | ) 117 | if in_funix: 118 | continue 119 | use_func = decorator.funix if is_func else decorator.funix_class 120 | if member.startswith("__") or member.startswith("_FUNIX_"): 121 | continue 122 | if need_path: 123 | if base_dir: 124 | use_func(menu=path_difference)(module_member) 125 | else: 126 | use_func(menu=f"{module.__name__}")(module_member) 127 | else: 128 | use_func()(module_member) 129 | -------------------------------------------------------------------------------- /backend/funix/frontend/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Handle frontend requests and start the frontend. 3 | """ 4 | 5 | from ipaddress import IPv4Address, IPv6Address 6 | from os.path import abspath, exists, join 7 | from threading import Thread 8 | from uuid import uuid4 9 | from webbrowser import open 10 | 11 | from flask import Flask, send_from_directory, session 12 | 13 | from funix.util.network import get_compressed_ip_address_as_str, is_port_used 14 | 15 | folder = abspath(join(abspath(__file__), "../../build")) # Best abs path ever 16 | 17 | 18 | class OpenFrontend(Thread): 19 | """ 20 | Open the frontend in the browser. 21 | 22 | Base Class: 23 | threading.Thread: The thread. 24 | 25 | Attributes: 26 | host (IPv4Address | IPv6Address): The host. 27 | port (int): The port. 28 | """ 29 | 30 | def __init__(self, host: IPv4Address | IPv6Address, port: int): 31 | """ 32 | Create a new OpenFrontend instance. 33 | 34 | Parameters: 35 | host (IPv4Address | IPv6Address): The host. 36 | port (int): The port. 37 | 38 | Returns: 39 | OpenFrontend: The new OpenFrontend instance. 40 | """ 41 | super(OpenFrontend, self).__init__() 42 | self.host = get_compressed_ip_address_as_str(host) 43 | self.port = port 44 | 45 | def is_server_online(self) -> bool: 46 | """ 47 | Check if the server is online. 48 | 49 | Returns: 50 | bool: If the server is online. 51 | """ 52 | return is_port_used(self.port, self.host) 53 | 54 | def run(self) -> None: 55 | """ 56 | Open the frontend in the browser. 57 | """ 58 | while not self.is_server_online(): 59 | pass 60 | open(f"http://{self.host}:{self.port}") 61 | 62 | 63 | def run_open_frontend(host: IPv4Address | IPv6Address, port: int) -> None: 64 | """ 65 | Run the OpenFrontend thread. 66 | 67 | Parameters: 68 | host (IPv4Address | IPv6Address): The host. 69 | port (int): The port. 70 | """ 71 | web_browser = OpenFrontend(host, port) 72 | web_browser.daemon = True # Die when the main thread dies 73 | web_browser.start() 74 | 75 | 76 | def start(flask_app: Flask) -> None: 77 | """ 78 | Start the frontend. 79 | """ 80 | 81 | name = flask_app.name 82 | 83 | def __send_index(): 84 | """ 85 | Send the index.html file. 86 | 87 | Routes: 88 | /: The index.html file. 89 | 90 | Returns: 91 | flask.Response: The index.html file. 92 | """ 93 | if not session.get("__funix_id"): 94 | session["__funix_id"] = uuid4().hex 95 | return send_from_directory(folder, "index.html") 96 | 97 | setattr(__send_index, "__name__", f"{name}__send_index") 98 | flask_app.get("/")(__send_index) 99 | 100 | def __send_root_files(path): 101 | """ 102 | Send the static files in root or the index.html file for funix frontend. 103 | If the file doesn't exist, it will send the index.html file. 104 | 105 | Routes: 106 | /: The static files or the index.html file. 107 | 108 | Parameters: 109 | path (str): The path to the file. 110 | 111 | Returns: 112 | flask.Response: The static files or the index.html file. 113 | """ 114 | if exists(join(folder, path)): 115 | return send_from_directory(folder, path) 116 | return send_from_directory(folder, "index.html") 117 | 118 | setattr(__send_root_files, "__name__", f"{name}__send_root_files") 119 | flask_app.get("/")(__send_root_files) 120 | 121 | def __send_static_files(res, path): 122 | """ 123 | Send the static files. 124 | 125 | Routes: 126 | /static//: The static files. 127 | 128 | Parameters: 129 | res (str): The resource folder. 130 | path (str): The path to the file. 131 | 132 | Returns: 133 | flask.Response: The static files. 134 | """ 135 | return send_from_directory(abspath(join(folder, f"static/{res}/")), path) 136 | 137 | setattr(__send_static_files, "__name__", f"{name}__send_static_files") 138 | flask_app.get("/static//")(__send_static_files) 139 | -------------------------------------------------------------------------------- /backend/funix/build/static/js/main.ff2fb548.js.LICENSE.txt: -------------------------------------------------------------------------------- 1 | /*! 2 | localForage -- Offline Storage, Improved 3 | Version 1.10.0 4 | https://localforage.github.io/localForage 5 | (c) 2013-2017 Mozilla, Apache License 2.0 6 | */ 7 | 8 | /*! 9 | * Determine if an object is a Buffer 10 | * 11 | * @author Feross Aboukhadijeh 12 | * @license MIT 13 | */ 14 | 15 | /*! ***************************************************************************** 16 | Copyright (c) Microsoft Corporation. 17 | 18 | Permission to use, copy, modify, and/or distribute this software for any 19 | purpose with or without fee is hereby granted. 20 | 21 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 22 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY 23 | AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 24 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM 25 | LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR 26 | OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 27 | PERFORMANCE OF THIS SOFTWARE. 28 | ***************************************************************************** */ 29 | 30 | /*! regenerator-runtime -- Copyright (c) 2014-present, Facebook, Inc. -- license (MIT): https://github.com/facebook/regenerator/blob/main/LICENSE */ 31 | 32 | /** 33 | * @license 34 | * Lodash 35 | * Copyright OpenJS Foundation and other contributors 36 | * Released under MIT license 37 | * Based on Underscore.js 1.8.3 38 | * Copyright Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors 39 | */ 40 | 41 | /** 42 | * @license React 43 | * react-dom.production.min.js 44 | * 45 | * Copyright (c) Facebook, Inc. and its affiliates. 46 | * 47 | * This source code is licensed under the MIT license found in the 48 | * LICENSE file in the root directory of this source tree. 49 | */ 50 | 51 | /** 52 | * @license React 53 | * react-is.production.min.js 54 | * 55 | * Copyright (c) Facebook, Inc. and its affiliates. 56 | * 57 | * This source code is licensed under the MIT license found in the 58 | * LICENSE file in the root directory of this source tree. 59 | */ 60 | 61 | /** 62 | * @license React 63 | * react-jsx-runtime.production.min.js 64 | * 65 | * Copyright (c) Facebook, Inc. and its affiliates. 66 | * 67 | * This source code is licensed under the MIT license found in the 68 | * LICENSE file in the root directory of this source tree. 69 | */ 70 | 71 | /** 72 | * @license React 73 | * react.production.min.js 74 | * 75 | * Copyright (c) Facebook, Inc. and its affiliates. 76 | * 77 | * This source code is licensed under the MIT license found in the 78 | * LICENSE file in the root directory of this source tree. 79 | */ 80 | 81 | /** 82 | * @license React 83 | * scheduler.production.min.js 84 | * 85 | * Copyright (c) Facebook, Inc. and its affiliates. 86 | * 87 | * This source code is licensed under the MIT license found in the 88 | * LICENSE file in the root directory of this source tree. 89 | */ 90 | 91 | /** 92 | * @mui/styled-engine v5.12.0 93 | * 94 | * @license MIT 95 | * This source code is licensed under the MIT license found in the 96 | * LICENSE file in the root directory of this source tree. 97 | */ 98 | 99 | /** 100 | * @mui/styled-engine v5.14.7 101 | * 102 | * @license MIT 103 | * This source code is licensed under the MIT license found in the 104 | * LICENSE file in the root directory of this source tree. 105 | */ 106 | 107 | /** 108 | * @remix-run/router v1.5.0 109 | * 110 | * Copyright (c) Remix Software Inc. 111 | * 112 | * This source code is licensed under the MIT license found in the 113 | * LICENSE.md file in the root directory of this source tree. 114 | * 115 | * @license MIT 116 | */ 117 | 118 | /** 119 | * React Router v6.10.0 120 | * 121 | * Copyright (c) Remix Software Inc. 122 | * 123 | * This source code is licensed under the MIT license found in the 124 | * LICENSE.md file in the root directory of this source tree. 125 | * 126 | * @license MIT 127 | */ 128 | 129 | /** @license React v16.13.1 130 | * react-is.production.min.js 131 | * 132 | * Copyright (c) Facebook, Inc. and its affiliates. 133 | * 134 | * This source code is licensed under the MIT license found in the 135 | * LICENSE file in the root directory of this source tree. 136 | */ 137 | 138 | /** @license URI.js v4.4.1 (c) 2011 Gary Court. License: http://github.com/garycourt/uri-js */ 139 | -------------------------------------------------------------------------------- /backend/funix/util/network.py: -------------------------------------------------------------------------------- 1 | """ 2 | Network utils for funix. 3 | """ 4 | 5 | from ipaddress import IPv4Address, IPv6Address 6 | from socket import AF_INET, SOCK_STREAM, socket 7 | 8 | 9 | def get_compressed_ip_address_as_str(host: IPv4Address | IPv6Address) -> str: 10 | """ 11 | Get the str ip and handle the local ip. 12 | Just 0.0.0.0 and :: will be formatted, others will be stringify and returned. 13 | Sorry for the bad name, haha. 14 | 15 | Parameters: 16 | host (IPv4Address | IPv6Address): The host to handle. 17 | 18 | Returns: 19 | str: The handled host. 20 | """ 21 | is_v4 = host.version == 4 22 | if is_ip_private(host): 23 | return "127.0.0.1" if is_v4 else "[::1]" 24 | return host.compressed 25 | 26 | 27 | def is_port_used(port: int, host: str) -> bool: 28 | """ 29 | Check if the port is used. 30 | 31 | Parameters: 32 | port (int): The port to check. 33 | host (str): The host to check. 34 | 35 | Returns: 36 | bool: If the port is used. 37 | """ 38 | try: 39 | socket(AF_INET, SOCK_STREAM).connect((host, port)) 40 | return True 41 | except: 42 | return False 43 | 44 | 45 | def is_ip_private(ip: IPv4Address | IPv6Address) -> bool: 46 | """ 47 | Check if the ip is a private ip. 48 | 49 | Parameters: 50 | ip (IPv4Address | IPv6Address): The ip to check. 51 | 52 | Returns: 53 | bool: If the ip is a private ip. 54 | """ 55 | return ip.is_private 56 | 57 | 58 | def get_next_unused_port(port: int, host: str) -> int | None: 59 | """ 60 | Get the next unused port from the host, starting from the port. If the port is used, it will try to find the next 61 | port. If the port is not used, it will return the port. If the port is out of range, it will return None. 62 | 63 | Parameters: 64 | port (int): The port to start from. 65 | host (str): The host to check. 66 | 67 | Returns: 68 | int | None: Port or failure. 69 | int: The next unused port. 70 | None: If the port is out of range. 71 | """ 72 | now_port = port 73 | while is_port_used(now_port, host): 74 | if now_port > 65535: 75 | return None 76 | now_port += 1 77 | return now_port 78 | 79 | 80 | def get_previous_unused_port(port: int, host: str) -> int | None: 81 | """ 82 | Get the previous unused port from the host, starting from the port. If the port is used, it will try to find the 83 | previous port. If the port is not used, it will return the port. If the port is out of range, it will return None. 84 | 85 | Parameters: 86 | port (int): The port to start from. 87 | host (str): The host to check. 88 | 89 | Returns: 90 | int | None: Port or failure. 91 | int: The previous unused port. 92 | None: If the port is out of range. 93 | """ 94 | now_port = port 95 | while is_port_used(now_port, host): 96 | if now_port < 0: 97 | return None 98 | now_port -= 1 99 | return now_port 100 | 101 | 102 | def get_unused_port_from(port: int, host: IPv4Address | IPv6Address) -> int: 103 | """ 104 | Get an unused port from the host, starting from the port. If the port is used, it will try to find the next or 105 | previous port. If the port is not used, it will return the port. If the port is out of range, it will raise an 106 | exception to tell you that there is no available port. 107 | 108 | Parameters: 109 | port (int): The port to start from. 110 | host (IPv4Address | IPv6Address): The host to check. 111 | 112 | Returns: 113 | int: The unused port. 114 | 115 | Raises: 116 | RuntimeError: If there is no available port. (Means the port is out of range.) 117 | 118 | Notes: 119 | Ah, Funix will definitely call this function when it starts up. For Funix, there is no need to catch this 120 | exception, just let Funix crash when all ports are used. 121 | """ 122 | new_port = port 123 | new_host = get_compressed_ip_address_as_str(host) 124 | if is_port_used(new_port, new_host): 125 | print(f"port {port} is used, try to find next or previous port.") 126 | next_port = get_next_unused_port(new_port, new_host) 127 | if next_port is None: 128 | previous_port = get_previous_unused_port(new_port, new_host) 129 | if previous_port is None: 130 | raise RuntimeError(f"No available port for {new_host}, base: {port}") 131 | else: 132 | return previous_port 133 | else: 134 | return next_port 135 | return new_port 136 | -------------------------------------------------------------------------------- /backend/funix/decorator/layout.py: -------------------------------------------------------------------------------- 1 | """ 2 | Layout decorator 3 | """ 4 | 5 | from typing import Callable, Any 6 | 7 | from funix.decorator.widget import parse_widget 8 | from funix.hint import InputLayout 9 | 10 | pydantic_layout_dict = {} 11 | pydantic_name_dict = {} 12 | pydantic_widget_dict = {} 13 | 14 | 15 | def convert_row_item(row_item: dict, item_type: str) -> dict: 16 | """ 17 | Convert a layout row item(block) to frontend-readable item. 18 | 19 | Parameters: 20 | row_item (dict): The row item. 21 | item_type (str): The item type. 22 | 23 | Returns: 24 | dict: The converted item. 25 | """ 26 | converted_item = row_item 27 | converted_item["type"] = item_type 28 | converted_item["content"] = row_item[item_type] 29 | converted_item.pop(item_type) 30 | return converted_item 31 | 32 | 33 | def handle_input_layout(input_layout: list) -> tuple[list, dict]: 34 | return_input_layout = [] 35 | decorated_params = {} 36 | for row in input_layout: 37 | row_layout = [] 38 | for row_item in row: 39 | row_item_done = row_item 40 | for common_row_item_key in ["markdown", "html"]: 41 | if common_row_item_key in row_item: 42 | row_item_done = convert_row_item(row_item, common_row_item_key) 43 | if "argument" in row_item: 44 | if row_item["argument"] not in decorated_params: 45 | decorated_params[row_item["argument"]] = {} 46 | decorated_params[row_item["argument"]]["customLayout"] = True 47 | row_item_done["type"] = "argument" 48 | elif "divider" in row_item: 49 | row_item_done["type"] = "divider" 50 | if isinstance(row_item["divider"], str): 51 | row_item_done["content"] = row_item_done["divider"] 52 | row_item_done.pop("divider") 53 | row_layout.append(row_item_done) 54 | return_input_layout.append(row_layout) 55 | return return_input_layout, decorated_params 56 | 57 | 58 | def handle_output_layout(output_layout: list) -> tuple[list, list]: 59 | return_output_layout = [] 60 | return_output_indexes = [] 61 | for row in output_layout: 62 | row_layout = [] 63 | for row_item in row: 64 | row_item_done = row_item 65 | for common_row_item_key in [ 66 | "markdown", 67 | "html", 68 | "images", 69 | "videos", 70 | "audios", 71 | "files", 72 | ]: 73 | if common_row_item_key in row_item: 74 | row_item_done = convert_row_item(row_item, common_row_item_key) 75 | if "divider" in row_item: 76 | row_item_done["type"] = "divider" 77 | if isinstance(row_item["divider"], str): 78 | row_item_done["content"] = row_item_done["divider"] 79 | row_item_done.pop("divider") 80 | elif "code" in row_item: 81 | row_item_done = row_item 82 | row_item_done["type"] = "code" 83 | row_item_done["content"] = row_item_done["code"] 84 | row_item_done.pop("code") 85 | elif "return_index" in row_item: 86 | row_item_done["type"] = "return_index" 87 | row_item_done["index"] = row_item_done["return_index"] 88 | row_item_done.pop("return_index") 89 | if isinstance(row_item_done["index"], int): 90 | return_output_indexes.append(row_item_done["index"]) 91 | elif isinstance(row_item_done["index"], list): 92 | return_output_indexes.extend(row_item_done["index"]) 93 | row_layout.append(row_item_done) 94 | return_output_layout.append(row_layout) 95 | return return_output_layout, return_output_indexes 96 | 97 | 98 | def pydantic_ui( 99 | layout: InputLayout | None = None, 100 | title: str | None = None, 101 | widgets: dict[str, str] = None, 102 | ) -> Callable[[Any], Any]: 103 | def decorator(cls): 104 | global pydantic_layout_dict 105 | global pydantic_name_dict 106 | global pydantic_widget_dict 107 | if layout: 108 | pydantic_layout_dict[id(cls)] = handle_input_layout(layout) 109 | if title: 110 | pydantic_name_dict[id(cls)] = title 111 | if widgets: 112 | new_widgets = {} 113 | for key, value in widgets.items(): 114 | new_widgets[key] = parse_widget(value) 115 | pydantic_widget_dict[id(cls)] = new_widgets 116 | return cls 117 | 118 | return decorator 119 | -------------------------------------------------------------------------------- /backend/funix/decorator/reactive.py: -------------------------------------------------------------------------------- 1 | """ 2 | Reactive 3 | """ 4 | 5 | from inspect import Parameter, signature 6 | from types import MappingProxyType 7 | from typing import Callable 8 | 9 | from flask import request 10 | 11 | from funix.hint import ReactiveType 12 | from funix.decorator.param import get_real_callable 13 | 14 | ReturnType = dict[str, tuple[Callable, dict[str, str]]] 15 | 16 | 17 | def get_reactive_config( 18 | reactive: ReactiveType, 19 | function_params: MappingProxyType[str, Parameter], 20 | function_name: str, 21 | ) -> ReturnType: 22 | reactive_config: ReturnType = {} 23 | for reactive_param in reactive.keys(): 24 | if isinstance(reactive_param, tuple): 25 | for param in reactive_param: 26 | if param not in function_params: 27 | raise ValueError( 28 | f"Reactive param `{param}` not found in function `{function_name}`" 29 | ) 30 | else: 31 | if reactive_param not in function_params: 32 | raise ValueError( 33 | f"Reactive param `{reactive_param}` not found in function `{function_name}`" 34 | ) 35 | callable_or_with_config = reactive[reactive_param] 36 | 37 | if isinstance(callable_or_with_config, tuple): 38 | callable_ = callable_or_with_config[0] 39 | full_config = callable_or_with_config[1] 40 | else: 41 | callable_ = callable_or_with_config 42 | full_config = None 43 | 44 | callable_params = signature(callable_).parameters 45 | 46 | for callable_param in dict(callable_params.items()).values(): 47 | if ( 48 | callable_param.kind == Parameter.VAR_KEYWORD 49 | or callable_param.kind == Parameter.VAR_POSITIONAL 50 | ): 51 | reactive_config[reactive_param] = (callable_, {}) 52 | break 53 | 54 | if reactive_param not in reactive_config: 55 | if full_config: 56 | reactive_config[reactive_param] = (callable_, full_config) 57 | else: 58 | reactive_config[reactive_param] = (callable_, {}) 59 | for key in dict(callable_params.items()).keys(): 60 | if key in function_params: 61 | reactive_config[reactive_param][1][key] = key 62 | return reactive_config 63 | 64 | 65 | def function_reactive_update( 66 | reactive_config: ReturnType, app_name: str, qualname: str 67 | ) -> dict: 68 | reactive_param_value = {} 69 | cached_callable = {} 70 | 71 | form_data = request.get_json() 72 | 73 | def wrapped_callable( 74 | callable_function_: Callable, key: str, index: int, is_tuple: bool, **kwargs 75 | ): 76 | """ 77 | A wrapper function to call the callable with the provided kwargs. 78 | """ 79 | if id(callable_function_) in cached_callable: 80 | data = cached_callable[id(callable_function_)] 81 | else: 82 | data = callable_function_(**kwargs) 83 | cached_callable[id(callable_function_)] = data 84 | if is_tuple: 85 | if isinstance(data, tuple): 86 | return data[index] 87 | elif isinstance(data, list): 88 | return data[index] 89 | elif isinstance(data, dict): 90 | return data.get(key) 91 | else: 92 | return data 93 | 94 | index = 0 95 | for key_, item_ in reactive_config.items(): 96 | argument_key: tuple = () 97 | is_tuple = False 98 | if isinstance(key_, tuple): 99 | is_tuple = True 100 | argument_key = key_ 101 | else: 102 | is_tuple = False 103 | argument_key = (key_,) 104 | for argument in argument_key: 105 | callable_function: Callable = get_real_callable( 106 | app_name, item_[0], qualname 107 | ) 108 | callable_config: dict[str, str] = item_[1] 109 | 110 | try: 111 | if callable_config == {}: 112 | reactive_param_value[argument] = wrapped_callable( 113 | callable_function, argument, index, is_tuple, **form_data 114 | ) 115 | else: 116 | new_form_data = {} 117 | for key__, value in callable_config.items(): 118 | new_form_data[key__] = form_data[value] 119 | reactive_param_value[argument] = wrapped_callable( 120 | callable_function, argument, index, is_tuple, **new_form_data 121 | ) 122 | except Exception as e: 123 | print(f"Error in reactive function `{callable_function.__name__}`: {e}") 124 | pass 125 | index = index + 1 126 | 127 | if reactive_param_value == {}: 128 | return {"result": None} 129 | 130 | return {"result": reactive_param_value} 131 | --------------------------------------------------------------------------------